Skip to content

Commit

Permalink
Merge pull request #3270 from AtlasOfLivingAustralia/feature/issue3265
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisala authored Jul 22, 2024
2 parents fd66a7b + 4b8236c commit c81720d
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 78 deletions.
18 changes: 14 additions & 4 deletions grails-app/conf/application.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ if(!app.http.header.userId){
if (!app.enableALAHarvestSetting) {
app.enableALAHarvestSetting = false
}
if(!app.domain.whiteList) {
app.domain.whiteList = "ala.org.au,localhost"
}

ecodata.baseUrl = "https://ecodata-test.ala.org.au/ws/"
// This is for biocollect/ecodata-client-plugin compatibility
Expand Down Expand Up @@ -242,17 +245,17 @@ security {
discoveryUri = "${auth.baseUrl}/cas/oidc/.well-known"
clientId = "changeMe"
secret = "changeMe"
scope = "openid,profile,email,roles,user_defined,ala"
scope = "openid profile email roles user_defined ala"
}
jwt {
enabled = false
enabled = true
discoveryUrl = "${auth.baseUrl}/cas/oidc/.well-known"
requiredClaims = ["sub", "iat", "exp", "jti", "client_id"]
}
}

webservice.jwt = false
webservice['jwt-scopes'] = "ala/internal users/read ala/attrs users/read ecodata/write_test ecodata/read_test"
webservice.jwt = true
webservice['jwt-scopes'] = "ala/internal users/read ala/attrs ecodata/read ecodata/write"
webservice['client-id']='changeMe'
webservice['client-secret'] = 'changeMe'

Expand Down Expand Up @@ -290,6 +293,13 @@ environments {
wiremock.port = 8018
security.oidc.discoveryUri = "http://localhost:${wiremock.port}/cas/oidc/.well-known"
security.oidc.allowUnsignedIdTokens = true
security.oidc.clientId="oidcId"
security.oidc.secret="oidcSecret"
webservice['client-id']="jwtId"
webservice['client-secret'] = "jwtSecret"
tokenURI = "http://localhost:${wiremock.port}/cas/oidc/oidcAccessToken"
jwkURI = "http://localhost:${wiremock.port}/cas/oidc/jwks"
issuerURI = "http://localhost:${wiremock.port}/cas/oidc"
def casBaseUrl = "http://localhost:${wiremock.port}"
ehcache.directory = './ehcache'
security.cas.appServerName=serverName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import org.apache.http.HttpStatus
import org.apache.poi.ss.usermodel.Workbook
import org.apache.poi.ss.usermodel.WorkbookFactory
import org.apache.poi.ss.util.CellReference
import org.grails.web.json.JSONArray
import org.grails.web.json.JSONObject


class ActivityController {

Expand Down Expand Up @@ -617,7 +614,7 @@ class ActivityController {
}
else {
url += "?type=${params.type?.encodeAsURL()}&listName=${params.listName?.encodeAsURL()}"
webService.proxyGetRequest(response, url)
webService.proxyGetRequest(response, url, true, true)
}

return null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ class DocumentController {

if (documentService.canView(document)) {
String url = buildDownloadUrl(path, filename)
webService.proxyGetRequest(response, url, false, false)
webService.proxyGetRequest(response, url, false, true)
return null
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ class ProxyController {
def excelOutputTemplate() {
String url = "${grailsApplication.config.getProperty('ecodata.baseUrl')}metadata/excelOutputTemplate?type=${params.type?.encodeAsURL()}&listName=${params.listName?.encodeAsURL()}"

webService.proxyGetRequest(response, url)
webService.proxyGetRequest(response, url, true, true)
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class ResourceController {
String url = grailsApplication.config.getProperty('pdfgen.baseURL')+'api/pdf'+commonService.buildUrlParamsFromMap(docUrl:params.file, cacheable:false)
Map result
try {
result = webService.proxyGetRequest(response, url, false, false, 10*60*1000)
result = webService.proxyGetRequest(response, url, false, true, 10*60*1000)
}
catch (Exception e) {
log.error("Error generating a PDF", e)
Expand Down
9 changes: 1 addition & 8 deletions grails-app/services/au/org/ala/merit/AdminService.groovy
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
package au.org.ala.merit

import com.drew.imaging.ImageMetadataReader
import com.drew.lang.GeoLocation
import com.drew.metadata.Directory
import com.drew.metadata.Metadata
import com.drew.metadata.exif.ExifSubIFDDirectory
import com.drew.metadata.exif.GpsDirectory

import java.text.SimpleDateFormat

/**
* A delegate to the ecodata admin services.
*/
Expand All @@ -32,7 +25,7 @@ class AdminService {
def url = "${grailsApplication.config.getProperty('ecodata.baseUrl')}admin/syncCollectoryOrgs"
webService.doPost(url, [
api_key: grailsApplication.config.getProperty('api_key')
])
], true)
}

/**
Expand Down
129 changes: 91 additions & 38 deletions grails-app/services/au/org/ala/merit/WebService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import org.grails.web.converters.exceptions.ConverterException
import org.springframework.http.MediaType
import org.springframework.web.multipart.MultipartFile

import javax.annotation.PostConstruct
import javax.servlet.http.Cookie
import javax.servlet.http.HttpServletResponse

Expand All @@ -54,9 +55,15 @@ class WebService {
static String AUTHORIZATION_HEADER_TYPE_EXTERNAL_TOKEN = 'externalToken'

static String AUTHORIZATION_HEADER_TYPE_NONE = 'none'
List WHITE_LISTED_DOMAINS = []


TokenService tokenService
@PostConstruct
void init() {
String whiteListed = grailsApplication.config.getProperty('app.domain.whiteList', "")
WHITE_LISTED_DOMAINS = Arrays.asList(whiteListed.split(','))
}

// Used to avoid a circular dependency during initialisation
def getUserService() {
Expand Down Expand Up @@ -118,27 +125,32 @@ class WebService {
}

private URLConnection configureConnection(String url, boolean includeUserId, Integer timeout = null, boolean useToken = false) {
useToken = useToken || useJWT()
String authHeaderType = useToken ? AUTHORIZATION_HEADER_TYPE_SYSTEM_BEAREN_TOKEN : AUTHORIZATION_HEADER_TYPE_API_KEY
configureConnection(url, authHeaderType, timeout)
}

private URLConnection configureConnection(String url, String authorizationHeaderType, Integer timeout = null) {
URLConnection conn = createAndConfigureConnection(url, timeout)
if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_API_KEY) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, grailsApplication.config.getProperty('api_key'))
def user = getUserService().getUser()
if (user) {
conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId)
boolean addUserId = false
if(canAddSecret(url)) {
if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_API_KEY) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, grailsApplication.config.getProperty('api_key'))
addUserId = true
} else if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_USER_BEARER_TOKEN) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, getToken(true))
} else if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_SYSTEM_BEAREN_TOKEN) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, getToken(false))
addUserId = true
}

if (addUserId) {
def user = getUserService().getUser()
if (user) {
conn.setRequestProperty(grailsApplication.config.getProperty('app.http.header.userId'), user.userId)
}
}
}
else if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_USER_BEARER_TOKEN) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, getToken(true))
}
else if (authorizationHeaderType == AUTHORIZATION_HEADER_TYPE_SYSTEM_BEAREN_TOKEN) {
conn.setRequestProperty(HttpHeaders.AUTHORIZATION, getToken(false))
}

conn
}

Expand All @@ -157,7 +169,11 @@ class WebService {
}

def proxyGetRequest(HttpServletResponse response, String url, boolean includeUserId = true, boolean includeApiKey = false, Integer timeout = null) {
String authHeaderType = includeApiKey ? AUTHORIZATION_HEADER_TYPE_API_KEY : AUTHORIZATION_HEADER_TYPE_NONE
String authHeaderType = AUTHORIZATION_HEADER_TYPE_NONE
if (includeApiKey) {
authHeaderType = useJWT() ? AUTHORIZATION_HEADER_TYPE_SYSTEM_BEAREN_TOKEN : AUTHORIZATION_HEADER_TYPE_API_KEY
}

proxyGetRequest(response, url, authHeaderType, timeout)
}

Expand Down Expand Up @@ -199,7 +215,7 @@ class WebService {
* Proxies a request URL with post data but doesn't assume the response is text based. (Used for proxying requests to
* ecodata for excel-based reports)
*/
def proxyPostRequest(HttpServletResponse response, String url, Map postBody, boolean includeUserId = true, boolean includeApiKey = false, Integer timeout = null) {
def proxyPostRequest(HttpServletResponse response, String url, Map postBody, boolean includeUserId = true, boolean includeApiKey = true, Integer timeout = null, boolean requireUserToken = false) {

def charEncoding = 'utf-8'

Expand All @@ -212,8 +228,13 @@ class WebService {
conn.setReadTimeout(readTimeout)
conn.setDoOutput ( true );

if (includeApiKey) {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
if (canAddSecret(url)) {
if (useJWT()) {
addTokenHeader(conn, requireUserToken)
}
else if (includeApiKey) {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
}
}

OutputStreamWriter wr = new OutputStreamWriter(conn.getOutputStream(), charEncoding)
Expand Down Expand Up @@ -357,7 +378,12 @@ class WebService {
conn.setRequestMethod("POST")
conn.setDoOutput(true)
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
if(canAddSecret(url)) {
if (useJWT())
addTokenHeader(conn)
else
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
}

def user = getUserService().getUser()
if (user) {
Expand Down Expand Up @@ -389,17 +415,19 @@ class WebService {
}

def doPost(String url, Map postBody, boolean useToken = false) {
useToken = useToken || useJWT()
def conn = null
def charEncoding = 'utf-8'
try {
conn = new URL(url).openConnection()
conn.setDoOutput(true)
conn.setRequestProperty("Content-Type", "application/json;charset=${charEncoding}");
if (useToken) {
addTokenHeader(conn)
}
else {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'));
if (canAddSecret(url)) {
if (useToken) {
addTokenHeader(conn)
} else {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'));
}
}
def user = getUserService().getUser()
if (user) {
Expand Down Expand Up @@ -432,6 +460,7 @@ class WebService {
}

def doDelete(String url, boolean useToken = false) {
useToken = useToken || useJWT()
if (!useToken) {
url += (url.indexOf('?') == -1 ? '?' : '&') + "api_key=${grailsApplication.config.getProperty('api_key')}"
}
Expand All @@ -440,11 +469,12 @@ class WebService {
try {
conn = new URL(url).openConnection()
conn.setRequestMethod("DELETE")
if (useToken) {
addTokenHeader(conn)
}
else {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
if (canAddSecret(url)) {
if (useToken) {
addTokenHeader(conn)
} else {
conn.setRequestProperty("Authorization", grailsApplication.config.getProperty('api_key'))
}
}
def user = getUserService().getUser()
if (user) {
Expand Down Expand Up @@ -486,7 +516,7 @@ class WebService {
* @return [status:<request status>, content:<The response content from the server, assumed to be JSON>
*/
def postMultipart(url, Map params, InputStream contentIn, contentType, originalFilename, fileParamName = 'files', Closure successHandler = null, boolean useToken = false) {

useToken = useToken || useJWT()
def result = [:]
def user = userService.getUser()

Expand All @@ -500,17 +530,19 @@ class WebService {
content.addPart(key, new StringBody(value.toString()))
}
}
if (useToken) {
if (grailsApplication.config.getProperty('spatial.supports_jwt', Boolean.class, true)) {
headers.'Authorization' = getToken()
if (canAddSecret(url)) {
if (useToken) {
if (useJWT()) {
headers.'Authorization' = getToken()
}
else {
headers.'apiKey' = grailsApplication.config.getProperty('api_key')
}
}
else {
headers.'apiKey' = grailsApplication.config.getProperty('api_key')
headers.'Authorization' = grailsApplication.config.getProperty('api_key')
}
}
else {
headers.'Authorization' = grailsApplication.config.getProperty('api_key')
}
if (user) {
headers[grailsApplication.config.getProperty('app.http.header.userId')] = user.userId
}
Expand Down Expand Up @@ -551,13 +583,34 @@ class WebService {
result
}

private void addTokenHeader(conn) {
if (grailsApplication.config.getProperty('spatial.supports_jwt', Boolean.class, true)) {
conn.setRequestProperty("Authorization", getToken())
private void addTokenHeader(conn, boolean requireUser = false) {
if (useJWT()) {
conn.setRequestProperty("Authorization", getToken(requireUser))
}
else {
conn.setRequestProperty("apiKey", grailsApplication.config.getProperty('api_key'));
}
}

/**
* Check if url is in the configured domain
* @param url
* @return
*/
boolean canAddSecret(String url) {
try {
URL urlObj = new URL(url)
String host = urlObj.getHost()
return WHITE_LISTED_DOMAINS.find { host.endsWith(it) } != null
} catch (Exception e) {
log.error("Error parsing URL: ${url}")
}

return false
}

boolean useJWT() {
grailsApplication.config.getProperty('ala.supports_jwt', Boolean.class, true)
}
}

2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"devDependencies": {
"@metahub/karma-jasmine-jquery": "^2.0.1",
"chromedriver": "126.0.4",
"chromedriver": "^126.0.4",
"jasmine-core": "^3.5.0",
"jasmine-jquery": "^2.0.0",
"jquery": "3.6.2",
Expand Down
Loading

0 comments on commit c81720d

Please sign in to comment.