Skip to content

Commit

Permalink
Simplified UserService.setUser and updated tests #1058
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisala committed Jan 28, 2025
1 parent 0bcae50 commit ab3b192
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 44 deletions.
37 changes: 16 additions & 21 deletions grails-app/services/au/org/ala/ecodata/UserService.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import org.grails.web.servlet.mvc.GrailsWebRequest
import org.pac4j.core.config.Config
import org.pac4j.core.context.WebContextFactory
import org.pac4j.core.context.session.SessionStoreFactory
import org.pac4j.core.profile.UserProfile
import org.pac4j.core.profile.factory.ProfileManagerFactory
import org.pac4j.jee.context.JEEFrameworkParameters
import org.springframework.beans.factory.annotation.Autowired

import javax.servlet.http.HttpServletRequest
Expand Down Expand Up @@ -187,42 +185,39 @@ class UserService {
}


private String getUserIdFromPac4jProfile(HttpServletRequest request, HttpServletResponse response) {
// A simpler alternative here could be to check for the existence of the
// pac4j http request wrapper and pull the profile manager from it.
def params = new JEEFrameworkParameters(request, response)
def webContext = (this.webContextFactory ?: config.getWebContextFactory()).newContext(params)
def sessionStore = (this.sessionStoreFactory ?: config.getSessionStoreFactory()).newSessionStore(params)
def profileManager = (this.profileManagerFactory ?: config.getProfileManagerFactory()).apply(webContext, sessionStore)
Optional<UserProfile> pac4jProfile = profileManager.getProfile()
private static String checkForDelegatedUserId(HttpServletRequest request) {
// When BioCollect or MERIT calls ecodata, they use a M2M access token which is able to be identified via
// the profile type. We can then trust the userId header to be the user MERIT or BioCollect is representing.
def principal = request.getUserPrincipal()

def profile = null
if (pac4jProfile.isPresent()) {
profile = pac4jProfile.get()

if (profile instanceof AlaM2MUserProfile) {
return request.getHeader(AuditInterceptor.httpRequestHeaderForUserId)
}
if (principal && principal instanceof AlaM2MUserProfile) {
return request.getHeader(AuditInterceptor.httpRequestHeaderForUserId)
}
return profile?.userId
return null
}

def setUser() {
GrailsWebRequest grailsWebRequest = GrailsWebRequest.lookup()
HttpServletRequest request = grailsWebRequest.getCurrentRequest()
HttpServletResponse response = grailsWebRequest.getCurrentResponse()

// First check if we've already saved the profile.
def userDetails = request.getAttribute(UserDetails.REQUEST_USER_DETAILS_KEY)

if (userDetails) {
return userDetails
}
// If the user has logged in interactively or supplies a bearer token which identifies the user
// (e.g. the Monitor app passes the user token) the authService will be able to resolve the user from the token.
String userId = authService.getUserId()

String userId = getUserIdFromPac4jProfile(request, response)

// Otherwise, if the token is an ALA M2M token from MERIT or BioCollect, we can obtain the userId
// from a separate header.
if (!userId) {
userId = checkForDelegatedUserId(request)
}
if (userId) {
log.debug("Setting current user to ${userId}")

userDetails = setCurrentUser(userId)
if (userDetails) {
// We set the current user details in the request scope because
Expand Down
59 changes: 36 additions & 23 deletions src/test/groovy/au/org/ala/ecodata/UserServiceSpec.groovy
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package au.org.ala.ecodata

import au.org.ala.web.AuthService
import au.org.ala.ws.security.client.AlaOidcClient
import au.org.ala.ws.security.profile.AlaOidcUserProfile
import au.org.ala.ws.security.profile.AlaM2MUserProfile
import grails.test.mongodb.MongoSpec
import grails.testing.services.ServiceUnitTest
import grails.testing.web.GrailsWebUnitTest
import org.pac4j.core.config.Config
import org.pac4j.core.credentials.AnonymousCredentials
import org.pac4j.core.credentials.Credentials
import org.pac4j.core.profile.UserProfile
import spock.lang.Unroll

/**
Expand All @@ -20,7 +16,6 @@ class UserServiceSpec extends MongoSpec implements ServiceUnitTest<UserService>,

WebService webService = Mock(WebService)
AuthService authService = Mock(AuthService)
AlaOidcClient alaOidcClient
Config pack4jConfig

def user
Expand All @@ -43,6 +38,7 @@ class UserServiceSpec extends MongoSpec implements ServiceUnitTest<UserService>,
def cleanup() {
User.findAll().each{it.delete(flush:true)}
Hub.findAll().each{it.delete(flush:true)}
service.clearCurrentUser()
}

def "The recordLoginTime method requires a hubId and userId to be supplied"() {
Expand Down Expand Up @@ -160,28 +156,45 @@ class UserServiceSpec extends MongoSpec implements ServiceUnitTest<UserService>,
"h1" | "2021-04-15T00:00:00Z" | "2021-05-01T00:00:00Z" | 0
}

void "getUserFromJWT returns user when Authorization header is passed"() {
void "The user service can identify the user using the authService and make it available on a ThreadLocal"() {
when:
service.setUser()

then:
1 * authService.getUserId() >> user.userId
1 * authService.getUserForUserId(user.userId) >> userDetails

UserService.currentUser() == userDetails
}

void "The user service can identify an ALA M2M access token and identify the user from a request header"() {
setup:
def result
alaOidcClient = GroovyMock([global: true], AlaOidcClient)
pack4jConfig = GroovyMock([global: true], Config)
service.alaOidcClient = alaOidcClient
service.config = pack4jConfig
AlaOidcUserProfile person = new AlaOidcUserProfile(user.userId)
Optional<Credentials> credentials = new Optional<Credentials>(AnonymousCredentials.INSTANCE)
Optional<UserProfile> userProfile = new Optional<UserProfile>(person)
AuditInterceptor.httpRequestHeaderForUserId = 'userId'
request.setUserPrincipal(new AlaM2MUserProfile('clientId', 'test', []))

when:
request.addHeader('Authorization', 'Bearer abcdef')
result = service.getUserFromJWT()
request.addHeader('userId', user.userId)
service.setUser()

then:
alaOidcClient.getCredentials(*_) >> credentials
alaOidcClient.getUserProfile(*_) >> userProfile
authService.getUserForUserId(user.userId) >> userDetails
result.userName == user.userName
result.displayName == "${user.firstName} ${user.lastName}"
result.userId == user.userId
1 * authService.getUserId() >> null

1 * authService.getUserForUserId(user.userId) >> userDetails

UserService.currentUser() == userDetails
}

void "The user service will not read the userId from the header if the caller isn't an ALA system"() {
when:
request.addHeader('userId', user.userId)
service.setUser()

then:
1 * authService.getUserId() >> null
0 * authService.getUserDetailsById(_)
UserService.currentUser() == null


}


Expand Down

0 comments on commit ab3b192

Please sign in to comment.