Skip to content

Commit a8459f7

Browse files
author
Adam Collins
committed
#158 change remote layer copy to use JWT
1 parent 22880e3 commit a8459f7

File tree

13 files changed

+124
-36
lines changed

13 files changed

+124
-36
lines changed

grails-app/conf/application.yml

+3
Original file line numberDiff line numberDiff line change
@@ -474,3 +474,6 @@ sandboxSolrCollection: sandbox
474474
sandboxThreadCount: 2
475475
pipelinesCmd: "java -Dspark.local.dir=/data/spatial-data/sandbox/tmp -Djava.io.tmpdir=/data/spatial-data/sandbox/tmp -Dlog4j.configuration=file:/data/spatial-data/modelling/la-pipelines/log4j.properties -cp /data/spatial-data/modelling/la-pipelines/pipelines-2.19.0-SNAPSHOT-shaded.jar"
476476
pipelinesConfig: "--config=/data/spatial-data/modelling/la-pipelines/la-pipelines.yaml"
477+
478+
# For the copying of layers between spatial-service instances, a JWT with the following role must be provided in the `/manageLayers/remote` page.
479+
layerCopyRole: ROLE_ADMIN

grails-app/controllers/au/org/ala/spatial/ManageLayersController.groovy

+37-9
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@
1515

1616
package au.org.ala.spatial
1717

18+
import au.ala.org.ws.security.RequireApiKey
19+
import au.org.ala.web.AuthService
1820
import grails.converters.JSON
1921
import grails.converters.XML
2022
import org.apache.commons.io.FileUtils
21-
import org.apache.commons.io.IOUtils
2223
import org.hibernate.criterion.CriteriaSpecification
2324
import org.json.simple.JSONObject
24-
import org.json.simple.parser.JSONParser
2525

26+
import javax.servlet.http.HttpServletResponse
2627
import javax.transaction.Transactional
2728
import java.text.SimpleDateFormat
2829

@@ -37,6 +38,7 @@ class ManageLayersController {
3738

3839
FieldService fieldService
3940
LayerService layerService
41+
AuthService authService
4042

4143
/**
4244
* admin only or api_key
@@ -614,8 +616,9 @@ class ManageLayersController {
614616
def copy() {
615617
String spatialServiceUrl = params.spatialServiceUrl
616618
String fieldId = params.fieldId
619+
String jwt = params.jwt
617620

618-
manageLayersService.updateFromRemote(spatialServiceUrl, fieldId)
621+
manageLayersService.updateFromRemote(spatialServiceUrl, fieldId, jwt)
619622
redirect(controller: "Tasks", action: "index")
620623

621624
}
@@ -628,11 +631,11 @@ class ManageLayersController {
628631
@RequireAdmin
629632
def enable() {
630633
if (params.id.isNumber()) {
631-
def layer = layerDao.getLayerById(params.id.toInteger(), false)
634+
def layer = layerService.getLayerById(params.id.toInteger(), false)
632635
layer.enabled = true
633636
layerService.updateLayer(layer)
634637
} else {
635-
def field = fieldDao.getFieldById(params.id, false)
638+
def field = fieldService.getFieldById(params.id, false)
636639
field.enabled = true
637640
fieldService.updateField(field)
638641
}
@@ -645,11 +648,22 @@ class ManageLayersController {
645648
* a layer: 'cl..._res', 'el..._res', will provide the standardized files at the requested resolution
646649
* (or next detailed) - shape files or diva grids
647650
*
648-
* admin only or api_key, do not redirect to CAS
651+
* Requires JWT with the configured layerCopyRole
652+
*
649653
* @return
650654
*/
651-
@RequireAdmin
655+
@RequireApiKey
652656
def resource() {
657+
// do permission check
658+
if (!authService.userInRole(spatialConfig.layerCopyRole)) {
659+
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, required role: " + spatialConfig.layerCopyRole)
660+
return
661+
}
662+
663+
if (!isLayerCopyFile(params.resource)) {
664+
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, only allowed access to layer files")
665+
}
666+
653667
OutputStream outputStream = null
654668
try {
655669
outputStream = response.outputStream as OutputStream
@@ -674,13 +688,27 @@ class ManageLayersController {
674688
/**
675689
* for slaves to peek at a resource on the master
676690
*
677-
* admin only or api_key, do not redirect to CAS
691+
* Requires JWT with the configured layerCopyRole
678692
*
679693
* @return
680694
*/
681-
@RequireAdmin
695+
@RequireApiKey
682696
def resourcePeek() {
697+
// do permission check
698+
if (!authService.userInRole(spatialConfig.layerCopyRole)) {
699+
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, required role: " + spatialConfig.layerCopyRole)
700+
return
701+
}
702+
703+
if (!isLayerCopyFile(params.resource)) {
704+
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Forbidden, only allowed access to layer files")
705+
}
706+
683707
//write resource
684708
render fileService.info(params.resource.toString()) as JSON
685709
}
710+
711+
private boolean isLayerCopyFile(String resource) {
712+
return resource.startsWith("/layer/") || resource.startsWith("/standard_layer/") || resource.startsWith("/public/layerDistances.properties")
713+
}
686714
}

grails-app/services/au/org/ala/spatial/FieldService.groovy

+2-1
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,8 @@ class FieldService {
171171

172172
Map map = field.properties
173173
map.put('id', field.id)
174-
Fields.executeUpdate(sql, map)
174+
175+
Sql.newInstance(dataSource).executeUpdate(sql, map)
175176
}
176177

177178
List<Layers> getLayersByCriteria(String keywords) {

grails-app/services/au/org/ala/spatial/LayerService.groovy

+2-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ class LayerService {
6565
void updateLayer(Layers layer) {
6666
log.debug("Updating layer metadata for " + layer.getName())
6767
String sql = "update layers set citation_date=:citation_date, classification1=:classification1, classification2=:classification2, datalang=:datalang, description=:description, displayname=:displayname, displaypath=:displaypath, enabled=:enabled, domain=:domain, environmentalvaluemax=:environmentalvaluemax, environmentalvaluemin=:environmentalvaluemin, environmentalvalueunits=:environmentalvalueunits, extents=:extents, keywords=:keywords, licence_link=:licence_link, licence_notes=:licence_notes, licence_level=:licence_level, lookuptablepath=:lookuptablepath, maxlatitude=:maxlatitude, maxlongitude=:maxlongitude, mddatest=:mddatest, mdhrlv=:mdhrlv, metadatapath=:metadatapath, minlatitude=:minlatitude, minlongitude=:minlongitude, name=:name, notes=:notes, path=:path, path_1km=:path_1km, path_250m=:path_250m, path_orig=:path_orig, pid=:pid, respparty_role=:respparty_role, scale=:scale, source=:source, source_link=:source_link, type=:type, uid=:uid where id=:id"
68-
Layers.executeUpdate(sql, layer)
68+
69+
Sql.newInstance(dataSource).executeUpdate(sql, layer)
6970
}
7071

7172

grails-app/services/au/org/ala/spatial/ManageLayersService.groovy

+2-2
Original file line numberDiff line numberDiff line change
@@ -1389,7 +1389,7 @@ class ManageLayersService {
13891389
* @param fieldId
13901390
* @return
13911391
*/
1392-
def updateFromRemote(String spatialServiceUrl, String fieldId) {
1392+
def updateFromRemote(String spatialServiceUrl, String fieldId, String jwt) {
13931393
def f = JSON.parse(httpCall("GET",
13941394
spatialServiceUrl + "/field/${fieldId}?pageSize=0",
13951395
null, null,
@@ -1429,7 +1429,7 @@ class ManageLayersService {
14291429
createOrUpdateField(field, field.id + '', false)
14301430
}
14311431

1432-
Map input = [layerId: layer.requestedId, fieldId: field.id, sourceUrl: spatialServiceUrl, displayPath: origDisplayPath] as Map
1432+
Map input = [layerId: layer.requestedId, fieldId: field.id, sourceUrl: spatialServiceUrl, displayPath: origDisplayPath, jwt: jwt] as Map
14331433
Task task = tasksService.create("LayerCopy", UUID.randomUUID(), input).task
14341434

14351435
task

grails-app/services/au/org/ala/spatial/TaskQueueService.groovy

+7
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,13 @@ class TaskQueueService {
193193
taskWrapper.task.message = 'finished'
194194
taskWrapper.task.history.put(System.currentTimeMillis() as String, "finished (id:${taskWrapper.task.id})" as String)
195195

196+
// map output.task to task when null to prevent error when flushing task
197+
taskWrapper.task.output.each {
198+
if (!it.task) {
199+
it.task = taskWrapper.task
200+
}
201+
}
202+
196203
// flush task because it is finished
197204
Task.withTransaction {
198205
if (!taskWrapper.task.save(flush: true)) {

grails-app/views/manageLayers/remote.gsp

+34-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<h1>Copy Layers from Remote Server</h1>
1919
</div>
2020

21+
<g:set var="spatialConfig" bean="spatialConfig"/>
22+
2123
<g:if test="${spatialServiceUrl == localUrl}">
2224
<div class="col-lg-8">
2325
<div class="warning">The local and remote server is the same so layers cannot be copied</div>
@@ -44,12 +46,27 @@
4446
<br/>
4547
<br/>
4648
</g:if>
47-
4849
<div class="container-fluid">
4950
This will copy a layer from a remote spatial-service to the local spatial-service.
50-
51+
<br/>
52+
<h3>Config</h3>
53+
<table>
54+
<tbody>
55+
<tr>
56+
<td>URL of the remote spatial-service</td>
57+
<td><input type="text" id="spatialServiceUrl" value="${spatialServiceUrl}" style='width:500px;margin-left:20px;'/><button onclick="refreshLayerList()">Refresh layer list</button></td>
58+
</tr>
59+
<tr>
60+
<td>JWT for the remote service. It must have the role ${spatialConfig.layerCopyRole} and not expire before the task is finished.</td>
61+
<td><textarea id="jwt" style='margin-left:20px;' cols="100" rows="10"></textarea></td>
62+
</tr>
63+
</tbody>
64+
</table>
65+
</div>
66+
<div class="container-fluid">
5167
<br/>
5268
<br/>
69+
<h3>List of layers</h3>
5370
Layer filter
5471
<select id="listSelector">
5572
<option value="divLocal">local only</option>
@@ -229,7 +246,14 @@
229246
});
230247
231248
function copy(id) {
232-
$.post("${localUrl}/manageLayers/copy?fieldId=" + id + "&spatialServiceUrl=" + encodeURIComponent("${spatialServiceUrl}"))
249+
var jwt = document.getElementById('jwt').value;
250+
if (jwt == "") {
251+
alert("Please provide a JWT");
252+
return;
253+
}
254+
255+
$.post("${localUrl}/manageLayers/copy?fieldId=" + id + "&spatialServiceUrl=" + encodeURIComponent("${spatialServiceUrl}") +
256+
"&jwt=" + encodeURIComponent(jwt));
233257
$('#copy' + id)[0].remove();
234258
$('#txtcopy' + id)[0].innerText = 'copying';
235259
}
@@ -256,6 +280,13 @@
256280
}
257281
location.href = downloadurl;
258282
}
283+
284+
function refreshLayerList() {
285+
const remoteUrl = document.getElementById('spatialServiceUrl').value;
286+
const url = new URL(window.location.href);
287+
url.searchParams.set('remoteUrl', remoteUrl);
288+
window.location.href = url.toString();
289+
}
259290
</script>
260291
</div>
261292
</body>

src/main/groovy/au/org/ala/spatial/SpatialConfig.groovy

+2
Original file line numberDiff line numberDiff line change
@@ -223,4 +223,6 @@ class SpatialConfig {
223223
String pipelinesConfig
224224
String sandboxSolrCollection
225225
Integer sandboxThreadCount
226+
227+
String layerCopyRole
226228
}

src/main/groovy/au/org/ala/spatial/Util.groovy

+5-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ class Util {
6363
}
6464
}
6565

66-
static Map<String, Object> getStream(String url) {
66+
static Map<String, Object> getStream(String url, String jwt) {
6767
HttpClient client = null
6868
HttpMethodBase call = null
6969
try {
@@ -76,6 +76,10 @@ class Util {
7676
try {
7777
call = new GetMethod(url)
7878

79+
if (jwt) {
80+
call.addRequestHeader("Authorization", "Bearer " + jwt)
81+
}
82+
7983
client.executeMethod(call)
8084
} catch (Exception e) {
8185
log.error url, e

src/main/groovy/au/org/ala/spatial/process/LayerCopy.groovy

+5-8
Original file line numberDiff line numberDiff line change
@@ -22,17 +22,14 @@ import grails.converters.JSON
2222
import groovy.util.logging.Slf4j
2323
import org.grails.web.json.JSONObject
2424

25-
//@CompileStatic
2625
@Slf4j
2726
class LayerCopy extends SlaveProcess {
2827

2928
void start() {
3029
String layerId = getInput('layerId')
3130
String fieldId = getInput('fieldId')
32-
String sourceUrl = getInput('sourceUrl')
33-
34-
//TODO: fetch default sld from geoserver
35-
String displayPath = getInput('displayPath')
31+
String sourceUrl = getInput('sourceUrl')
32+
String sourceJwt = getInput('jwt')
3633

3734
Fields field = getField(fieldId)
3835
Layers layer = getLayer(layerId)
@@ -46,7 +43,7 @@ class LayerCopy extends SlaveProcess {
4643
}
4744

4845
//get layer files
49-
getFile("/layer/${layer.name}", sourceUrl)
46+
getFile("/layer/${layer.name}", sourceUrl, sourceJwt)
5047
addOutputFiles("/layer/${layer.name}", true)
5148

5249
//get standardized files
@@ -64,13 +61,13 @@ class LayerCopy extends SlaveProcess {
6461
}
6562

6663
resolutions.each { res ->
67-
getFile("/standard_layer/${res}/${field.id}", sourceUrl)
64+
getFile("/standard_layer/${res}/${field.id}", sourceUrl, sourceJwt)
6865
addOutputFiles("/standard_layer/${res}/${field.id}")
6966
}
7067

7168
//get layerdistances
7269
taskLog("get layer distances")
73-
getFile('/public/layerDistances.properties', sourceUrl)
70+
getFile('/public/layerDistances.properties', sourceUrl, sourceJwt)
7471
JSONObject dists = JSON.parse(Util.getUrl(sourceUrl + "/layerDistances/layerdistancesJSON.json")) as JSONObject
7572
def distString = ''
7673
for (def f : getFields()) {

src/main/groovy/au/org/ala/spatial/process/SlaveProcess.groovy

+22-10
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ class SlaveProcess {
8888
void stop() {}
8989

9090

91-
def getFile(String path, String remoteSpatialServiceUrl) {
92-
def remote = peekFile(path, remoteSpatialServiceUrl)
91+
def getFile(String path, String remoteSpatialServiceUrl, String jwt) {
92+
def remote = peekFile(path, remoteSpatialServiceUrl, jwt)
9393

9494
//compare p list with local files
9595
def fetch = []
@@ -105,7 +105,7 @@ class SlaveProcess {
105105
if (fetch.size() < remote.size()) {
106106
//fetch only some
107107
fetch.each {
108-
getFile(it, remoteSpatialServiceUrl)
108+
getFile(it, remoteSpatialServiceUrl, jwt)
109109
}
110110
} else if (fetch.size() > 0) {
111111
//fetch all files
@@ -114,11 +114,10 @@ class SlaveProcess {
114114

115115
try {
116116
def shortpath = path.replace(spatialConfig.data.dir, '')
117-
def url = remoteSpatialServiceUrl + "/master/resource?resource=" + URLEncoder.encode(shortpath, 'UTF-8') +
118-
"&api_key=" + spatialConfig.serviceKey
117+
def url = remoteSpatialServiceUrl + "/master/resource?resource=" + URLEncoder.encode(shortpath, 'UTF-8')
119118

120119
def os = new BufferedOutputStream(new FileOutputStream(tmpFile))
121-
def streamObj = Util.getStream(url)
120+
def streamObj = Util.getStream(url, jwt)
122121
try {
123122
if (streamObj?.call) {
124123
os << streamObj?.call?.getResponseBodyAsStream()
@@ -167,20 +166,33 @@ class SlaveProcess {
167166
}
168167
}
169168

170-
List peekFile(String path, String spatialServiceUrl = spatialConfig.spatialService.url) {
169+
/**
170+
* Get some info on file/s (path, exists, lastModified) from the local fs or remote spatial service.
171+
*
172+
* The result will include info for the file "spatialConfig.data.dir + path" if it exists, otherwise it will
173+
* include info for the files "spatialConfig.data.dir + path + ".*" if they exist.
174+
*
175+
* @param path begins with '/' and is relative to spatialConfig.data.dir
176+
* @param spatialServiceUrl specify if a remote spatial service is to be used
177+
* @param jwt required if a remote spatial service is used
178+
* @return
179+
*/
180+
List peekFile(String path, String spatialServiceUrl = spatialConfig.spatialService.url, String jwt = null) {
171181
String shortpath = path.replace(spatialConfig.data.dir.toString(), '')
172182

183+
// if the spatial service is the same as the local service, use the file service
173184
if (spatialServiceUrl.equals(spatialConfig.grails.serverURL)) {
174185
return fileService.info(shortpath.toString())
175186
}
176187

177188
List map = [[path: '', exists: false, lastModified: System.currentTimeMillis()]]
178189

179190
try {
180-
String url = spatialServiceUrl + "/master/resourcePeek?resource=" + URLEncoder.encode(shortpath, 'UTF-8') +
181-
"&api_key=" + spatialConfig.serviceKey
191+
String url = spatialServiceUrl + "/master/resourcePeek?resource=" + URLEncoder.encode(shortpath, 'UTF-8')
192+
193+
// use the provided JWT authentication in the request header, and assign the JSON GET result to the map
194+
map = JSON.parse(Util.urlResponse("GET", url, null, ['Authorization': 'Bearer ' + jwt])?.text) as List
182195

183-
map = JSON.parse(Util.getUrl(url)) as List
184196
} catch (err) {
185197
log.error "failed to get: " + path, err
186198
}

src/main/groovy/au/org/ala/spatial/process/StandardizeLayers.groovy

-1
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ class StandardizeLayers extends SlaveProcess {
157157

158158
//copy txt for 'a' and 'b'
159159
if (new File('/layer/' + l.name + '.txt').exists()) {
160-
getFile('/layer/' + l.name + '.txt')
161160
FileUtils.copyFile(new File(spatialConfig.data.dir.toString() + '/layer/' + l.name + '.txt'),
162161
new File(spatialConfig.data.dir.toString() + '/standard_layer/' + res + '/' + f.id + '.txt'))
163162
addOutput('file', '/standard_layer/' + res + '/' + f.id + '.txt')

src/main/resources/processes/LayerCopy.json

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
},
2323
"displayPath": {
2424
"type": "string"
25+
},
26+
"jwt": {
27+
"type": "string"
2528
}
2629
},
2730
"output": {

0 commit comments

Comments
 (0)