Changeset 983


Ignore:
Timestamp:
Oct 22, 2010, 4:18:34 PM (6 years ago)
Author:
robert@…
Message:

New type of authentication and authorization added to the rest controller. See ticket 118

Location:
trunk
Files:
6 added
1 deleted
5 edited

Legend:

Unmodified
Added
Removed
  • trunk/grails-app/conf/Config.groovy

    r976 r983  
    164164grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username' // Use the persons username as salt for encryption
    165165grails.plugins.springsecurity.securityConfigType = grails.plugins.springsecurity.SecurityConfigType.Annotation
     166grails.plugins.springsecurity.successHandler.targetUrlParameter = 'spring-security-redirect'
    166167
    167168// Make sure the different controllers provided by springsecurity.ui are only accessible by administrators
  • trunk/grails-app/controllers/RestController.groovy

    r976 r983  
    3636        /**
    3737         * Authorization closure, which is run before executing any of the REST resource actions
    38          * It fetches a username/password combination from basic HTTP authentication and checks whether
    39          * that is an active (SecuritySpring) account
    40          * @return
     38         * It fetches a consumer/token combination from the url and checks whether
     39         * that is a correct and known combination
     40         *
     41         * @param       consumer        consumer name of the calling module
     42         * @param       token           token for the authenticated user (e.g. session_id)
     43         * @return true if the user is remotely logged in, false otherwise
    4144         */
    4245        private def auth() {
    43             credentials = BasicAuthentication.credentialsFromRequest(request)           
    44         requestUser = AuthenticationService.authenticateUser(credentials.u, credentials.p)
    45                
    46                 if(!requestUser) {
    47                     response.sendError(403)
    48                 return false
    49             }
    50                 else {
     46                if( !AuthenticationService.isRemotelyLoggedIn( params.consumer, params.token ) ) {
     47                        response.sendError(403)
     48                        return false
     49                } else {
    5150                        return true
    5251                }
     
    5453
    5554        /**
    56         * REST resource for data modules.
    57         * Username and password should be supplied via HTTP Basic Authentication.
    58         * Determines whether the given user/password combination is a valid GSCF account.
    59         *
    60         * @return bool {"authenticated":true} when user/password is a valid GSCF account, {"authenticated":false} otherwise.
    61         */
    62         def isUser= {
    63                 boolean isUser
    64                 credentials = BasicAuthentication.credentialsFromRequest(request)
    65                 def reqUser = AuthenticationService.authenticateUser(credentials.u, credentials.p)
    66                 isUser = reqUser ? true : false
     55         * REST resource for data modules.
     56         * Consumer and token should be supplied via URL parameters.
     57         * Determines whether the given user/password combination is a valid GSCF account.
     58         *
     59         * @param       consumer        consumer name of the calling module
     60         * @param       token           token for the authenticated user (e.g. session_id)
     61         * @return bool {"authenticated":true} when user/password is a valid GSCF account, {"authenticated":false} otherwise.
     62         */
     63        def isUser = {
     64                boolean isUser = AuthenticationService.isRemotelyLoggedIn( params.consumer, params.token )
    6765                def reply = ['authenticated':isUser]
    6866                render reply as JSON
     
    7169
    7270        /**
    73         * REST resource for data modules.
    74         * Username and password should be supplied via HTTP Basic Authentication.
    75         * Provide a list of all studies owned by the supplied user.
    76         *
    77         * @return JSON object list containing 'studyToken', and 'name' (title) for each study
    78         */
     71         * REST resource for data modules.
     72         * Consumer and token should be supplied via URL parameters.
     73         * Provide a list of all studies owned by the supplied user.
     74         *
     75         * @param       consumer        consumer name of the calling module
     76         * @param       token           token for the authenticated user (e.g. session_id)
     77         * @return JSON object list containing 'studyToken', and 'name' (title) for each study
     78         */
    7979        def getStudies = {
    8080                List studies = []
    81                 def user = params.user
    82                 Study.findAllByOwner(requestUser).each { study ->
     81                Study.findAllByOwner(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token )).each { study ->
    8382                        studies.push( [ 'title':study.title, 'studyToken':study.getToken()] )
    8483                }
     
    8887
    8988        /**
    90         * REST resource for data modules.
    91         * Username and password should be supplied via HTTP Basic Authentication.
    92         * Provide a list of all subjects belonging to a study.
    93         *
    94         * @param studyToken String The external study id (code) of the target GSCF Study object
    95         * @return JSON object list of subject names
    96         */
     89         * REST resource for data modules.
     90         * Consumer and token should be supplied via URL parameters.
     91         * Provide a list of all subjects belonging to a study.
     92         *
     93         * If the user is not allowed to read the study contents, a 401 error is given
     94         *
     95         * @param       studyToken      String The external study id (code) of the target GSCF Study object
     96         * @param       consumer        consumer name of the calling module
     97         * @param       token           token for the authenticated user (e.g. session_id)
     98         * @return JSON object list of subject names
     99         */
    97100        def getSubjects = {
    98101                List subjects = []
     
    100103                        def id = params.studyToken
    101104                        def study = Study.find( "from Study as s where s.code=?", [id])
    102                         if(study) study.subjects.each { subjects.push it.name }
     105
     106                        if(study) {
     107                                // Check whether the person is allowed to read the data of this study
     108                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) {
     109                                        response.sendError(401)
     110                                        return false
     111                                }
     112
     113                                study.subjects.each { subjects.push it.name }
     114                        }
    103115                }
    104116                render subjects as JSON
     
    107119
    108120        /**
    109         * REST resource for data modules.
    110         * Username and password should be supplied via HTTP Basic Authentication.
    111         * Provide a list of all assays for a given study
    112         *
    113         * Example call of the getAssays REST resource:
    114         * http://localhost:8080/gscf/rest/getAssays?studyToken=PPSH&moduleURL=http://localhost:8182/sam
    115         *
    116         * @param stuyToken String The external study id (code) of the target GSCF Study object
    117         * @param moduleURL String The base URL of the calling dbNP module
    118         * @return list of assays in the study as JSON object list, filtered to only contain assays
    119         *         for the specified module, with 'assayToken' and 'name' for each assay
    120         */
     121         * REST resource for data modules.
     122         * Consumer and token should be supplied via URL parameters.
     123         * Provide a list of all assays for a given study.
     124         *
     125         * If the user is not allowed to read the study contents, a 401 error is given
     126         *
     127         * Example call of the getAssays REST resource:
     128         * http://localhost:8080/gscf/rest/getAssays?studyToken=PPSH&moduleURL=http://localhost:8182/sam
     129         *
     130         * @param       studyToken      String The external study id (code) of the target GSCF Study object
     131         * @param       moduleURL       String The base URL of the calling dbNP module
     132         * @param       consumer        consumer name of the calling module
     133         * @param       token           token for the authenticated user (e.g. session_id)
     134         * @return list of assays in the study as JSON object list, filtered to only contain assays
     135         *         for the specified module, with 'assayToken' and 'name' for each assay
     136         */
    121137        def getAssays = {
    122138                List assays = []
     
    124140                        def id = params.studyToken
    125141                        def study = Study.find( "from Study as s where s.code=?", [id] )
    126                         if(study && study.owner == requestUser) study.assays.each{ assay ->
    127                                 if (assay.module.url.equals(params.moduleURL)) {
    128                                 def map = ['name':assay.name, 'assayToken':assay.getToken()]
    129                                         assays.push( map )
     142
     143                        if(study) {
     144                                // Check whether the person is allowed to read the data of this study
     145                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) {
     146                                        response.sendError(401)
     147                                        return false
     148                                }
     149
     150                                study.assays.each{ assay ->
     151                                        if (assay.module.url.equals(params.moduleURL)) {
     152                                                def map = ['name':assay.name, 'assayToken':assay.getToken()]
     153                                                assays.push( map )
     154                                        }
    130155                                }
    131156                        }
     
    136161
    137162        /**
    138         * REST resource for data modules.
    139         * Username and password should be supplied via HTTP Basic Authentication.
    140         * Provide all samples of a given Assay. The result is an enriched list with additional information for each sample.
    141         *
    142         * @param assayToken String (assayToken of some Assay in GSCF)
    143         * @return As a JSON object list, for each sample in that assay:
    144         * @return 'name' (Sample name, which is unique)
    145         * @return 'material' (Sample material)
    146         * @return 'subject' (The name of the subject from which the sample was taken)
    147         * @return 'event' (the name of the template of the SamplingEvent describing the sampling)
    148         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string)
    149         */
     163         * REST resource for data modules.
     164         * Username and password should be supplied via HTTP Basic Authentication.
     165         * Provide all samples of a given Assay. The result is an enriched list with additional information for each sample.
     166         *
     167         * @param       assayToken      String (assayToken of some Assay in GSCF)
     168         * @param       consumer        consumer name of the calling module
     169         * @param       token           token for the authenticated user (e.g. session_id)
     170         * @return As a JSON object list, for each sample in that assay:
     171         * @return 'name' (Sample name, which is unique)
     172         * @return 'material' (Sample material)
     173         * @return 'subject' (The name of the subject from which the sample was taken)
     174         * @return 'event' (the name of the template of the SamplingEvent describing the sampling)
     175         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string)
     176         */
    150177        def getSamples = {
    151178                def items = []
     
    170197
    171198        /**
    172         * REST resource for dbNP modules.
    173         *
    174         * @param studyToken String, the external identifier of the study
    175         * @return List of all fields of this study
    176         * @return
    177         *
    178         * Example REST call (without authentication):
    179     * http://localhost:8080/gscf/rest/getStudy/study?studyToken=PPSH
    180     *
    181         * Returns the JSON object:
    182         * {"title":"NuGO PPS human study","studyToken":"PPSH","startDate":"2008-01-13T23:00:00Z",
    183         * "Description":"Human study performed at RRI; centres involved: RRI, IFR, TUM, Maastricht U.",
    184         * "Objectives":null,"Consortium":null,"Cohort name":null,"Lab id":null,"Institute":null,
    185         * "Study protocol":null}
     199         * REST resource for dbNP modules.
     200         *
     201         * @param       studyToken String, the external identifier of the study
     202         * @param       consumer        consumer name of the calling module
     203         * @param       token           token for the authenticated user (e.g. session_id)
     204         * @return List of all fields of this study
     205         * @return
     206         *
     207         * If the user is not allowed to read this study, a 401 error is given
     208         *
     209         * Example REST call (without authentication):
     210     * http://localhost:8080/gscf/rest/getStudy/study?studyToken=PPSH
     211     *
     212         * Returns the JSON object:
     213         * {"title":"NuGO PPS human study","studyToken":"PPSH","startDate":"2008-01-13T23:00:00Z",
     214         * "Description":"Human study performed at RRI; centres involved: RRI, IFR, TUM, Maastricht U.",
     215         * "Objectives":null,"Consortium":null,"Cohort name":null,"Lab id":null,"Institute":null,
     216         * "Study protocol":null}
    186217        */
    187218        def getStudy = {
     
    190221                        def study = Study.find( "from Study as s where code=?",[params.studyToken])
    191222                        if(study) {
     223                                // Check whether the person is allowed to read the data of this study
     224                                if( !study.canRead(AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token ))) {
     225                                        response.sendError(401)
     226                                        return false
     227                                }
     228                               
    192229                                study.giveFields().each { field ->
    193230                                        def name = field.name
     
    203240
    204241        /**
    205         * REST resource for dbNP modules.
    206         *
    207         * @param assayToken String, the external identifier of the study
    208         * @return List of all fields of this assay
    209         *
    210         * Example REST call (without authentication):
    211     * http://localhost:8080/gscf/rest/getAssay/assay?assayToken=PPS3_SAM
    212     *
    213         * Returns the JSON object: {"name":"Lipid profiling","module":{"class":"dbnp.studycapturing.AssayModule","id":1,
    214         * "name":"SAM module for clinical data","platform":"clinical measurements","url":"http://sam.nmcdsp.org"},
    215         * "assayToken":"PPS3_SAM","parentStudyToken":"PPS","Description":null}
    216         */
     242         * REST resource for dbNP modules.
     243         *
     244         * @param       assayToken String, the external identifier of the study
     245         * @param       consumer        consumer name of the calling module
     246         * @param       token           token for the authenticated user (e.g. session_id)
     247         * @return List of all fields of this assay
     248         *
     249         * Example REST call (without authentication):
     250     * http://localhost:8080/gscf/rest/getAssay/assay?assayToken=PPS3_SAM
     251     *
     252         * Returns the JSON object: {"name":"Lipid profiling","module":{"class":"dbnp.studycapturing.AssayModule","id":1,
     253         * "name":"SAM module for clinical data","platform":"clinical measurements","url":"http://sam.nmcdsp.org"},
     254         * "assayToken":"PPS3_SAM","parentStudyToken":"PPS","Description":null}
     255         */
    217256        def getAssay = {
    218257                def items = [:]
     
    234273
    235274        /**
    236         * REST resource for data modules.
    237         * Username and password should be supplied via HTTP Basic Authentication.
    238         * One specific sample of a given Assay.
    239         *
    240         * @param assayToken String (id of some Assay in GSCF)
    241         * @return As a JSON object list, for each sample in that assay:
    242         * @return 'name' (Sample name, which is unique)
    243         * @return 'material' (Sample material)
    244         * @return 'subject' (The name of the subject from which the sample was taken)
    245         * @return 'event' (the name of the template of the SamplingEvent describing the sampling)
    246         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string)
    247         *
    248         * Example REST call (without authentication):
    249     * http://localhost:8080/gscf/rest/getSample/sam?assayToken=PPS3_SAM&sampleToken=A30_B
    250     *
    251         * Returns the JSON object:
    252         * {"subject":"A30","event":"Liver extraction","startTime":"1 week, 1 hour",
    253         * "sampleToken":"A30_B","material":{"class":"dbnp.data.Term","id":6,"accession":"BTO:0000131",
    254         * "name":"blood plasma","ontology":{"class":"Ontology","id":2}},"Remarks":null,
    255         * "Text on vial":"T70.91709057820039","Sample measured volume":null}
    256         */
     275         * REST resource for data modules.
     276         * Username and password should be supplied via HTTP Basic Authentication.
     277         * One specific sample of a given Assay.
     278         *
     279         * @param       assayToken      String (id of some Assay in GSCF)
     280         * @param       consumer        consumer name of the calling module
     281         * @param       token           token for the authenticated user (e.g. session_id)
     282         * @return As a JSON object list, for each sample in that assay:
     283         * @return 'name' (Sample name, which is unique)
     284         * @return 'material' (Sample material)
     285         * @return 'subject' (The name of the subject from which the sample was taken)
     286         * @return 'event' (the name of the template of the SamplingEvent describing the sampling)
     287         * @return 'startTime' (the time the sample was taken relative to the start of the study, as a string)
     288         *
     289         * Example REST call (without authentication):
     290     * http://localhost:8080/gscf/rest/getSample/sam?assayToken=PPS3_SAM&sampleToken=A30_B
     291     *
     292         * Returns the JSON object:
     293         * {"subject":"A30","event":"Liver extraction","startTime":"1 week, 1 hour",
     294         * "sampleToken":"A30_B","material":{"class":"dbnp.data.Term","id":6,"accession":"BTO:0000131",
     295         * "name":"blood plasma","ontology":{"class":"Ontology","id":2}},"Remarks":null,
     296         * "Text on vial":"T70.91709057820039","Sample measured volume":null}
     297         */
    257298        def getSample = {
    258299                def items = [:]
     
    279320        }
    280321
     322        /**
     323         * Returns the authorization level the user has for a given study.
     324         *
     325         * If no studyToken is given, a 400 (Bad Request) error is given.
     326         * If the given study doesn't exist, a 404 (Not found) error is given.
     327         *
     328         * @param       consumer        consumer name of the calling module
     329         * @param       token           token for the authenticated user (e.g. session_id)
     330         * @return      JSON Object
     331         * @return  { isOwner: true/false, 'canRead': true/false, 'canWrite': true/false }
     332         */
    281333        def getAuthorizationLevel = {
    282                 // in future the users authorization level will be based on authorization model         
    283334                if( params.studyToken ) {
    284335                        def id = params.studyToken
    285336                        def study = Study.find( "from Study as s where s.code=?", [id])
    286                         if(study) study.subjects.each { subjects.push it.name }
     337
     338                        if( !study ) {
     339                                response.sendError(404)
     340                                return false
     341                        }
     342
     343                        def user = AuthenticationService.getRemotelyLoggedInUser( params.consumer, params.token );
     344                        render( 'isOwner': study.isOwner(user), 'canRead': study.canRead(user), 'canWrite': study.canWrite(user) )
     345                } else {
     346                        response.sendError(400)
     347                        return false
    287348                }
    288 
    289                 def perm = study.getPermissions(requestUser)
    290                
    291                 render ('isOwner': study.isOwner(requestUser),
    292                         'create': perm.create, 'read':perm.read,
    293                         'update': perm.update, 'delete':perm.delete
    294                         ) as JSON
    295349    }
    296350}
  • trunk/grails-app/controllers/dbnp/authentication/LoginController.groovy

    r976 r983  
    2424         */
    2525        def springSecurityService
     26
     27        /**
     28         * Dependency injection for the GSCF authentication service
     29         */
     30        def AuthenticationService
    2631
    2732        /**
     
    5560        }
    5661
     62        /**
     63         * Shows the login page for users from a module
     64         */
     65        def auth_remote = {
     66            def consumer    = params.consumer
     67            def token       = params.token
     68
     69                        if( consumer == null || token == null ) {
     70                                throw new Exception( "Consumer and Token must be given!" );
     71                        }
     72
     73            def returnUrl   = params.returnUrl
     74
     75            // If the user is already authenticated with this session_id, redirect
     76            // him
     77            if( AuthenticationService.isRemotelyLoggedIn( consumer, token ) ) {
     78                if( returnUrl ) {
     79                                        redirect url: returnUrl
     80                } else {
     81                    redirect controller: 'home'
     82                }
     83            }
     84
     85            // If the user is already logged in locally, we log him in and
     86            // immediately redirect him
     87            if (AuthenticationService.isLoggedIn()) {
     88                                AuthenticationService.logInRemotely( consumer, token, AuthenticationService.getLoggedInUser() )
     89
     90                                if( returnUrl ) {
     91                    redirect url: returnUrl
     92                } else {
     93                    redirect controller: 'home'
     94                }
     95            }
     96
     97            // Otherwise we show the login screen
     98                        def config = SpringSecurityUtils.securityConfig
     99            String view = 'auth'
     100            String postUrl = "${request.contextPath}${config.apf.filterProcessesUrl}"
     101            String redirectUrl = g.createLink( absolute: true, controller: 'login', action: 'auth_remote', params: [ consumer: params.consumer, token: params.token, returnUrl: params.returnUrl ] )
     102            render view: view, model: [postUrl: postUrl,
     103                                       rememberMeParameter: config.rememberMe.parameter, redirectUrl: redirectUrl ]
     104        }
     105       
    57106        /**
    58107         * Show denied page.
  • trunk/grails-app/services/dbnp/authentication/AuthenticationService.groovy

    r976 r983  
    1919class AuthenticationService {
    2020    def SpringSecurityService
     21    static final int expiryTime = 60; // Number of minutes a remotely logged in user remains active
    2122
    2223    boolean transactional = true
    2324
    24     protected boolean isLoggedIn() {
    25       def principal = SpringSecurityService.getPrincipal()
    26 
    27       // If the user is logged in, the principal should be a GrailsUser object.
    28       // If the user is not logged in, the principal is the 'anonymous username'
    29       // i.e. a string
    30       if( principal instanceof GrailsUser ) {
    31           return true;
    32       }
    33 
    34       return false;
     25    public boolean isLoggedIn() {
     26        return SpringSecurityService.isLoggedIn();
    3527    }
    3628
    37     protected SecUser getLoggedInUser() {
     29    public SecUser getLoggedInUser() {
    3830      def principal = SpringSecurityService.getPrincipal()
    3931
     
    4739      return null;
    4840    }
     41
     42    /**
     43     * Logs a user in for a remote session
     44     */
     45    public boolean logInRemotely( String consumer, String token, SecUser user ) {
     46        // Make sure there is no other logged in user anymore
     47        logOffRemotely( consumer, token )
     48
     49        def SAUser = new SessionAuthenticatedUser( consumer: consumer, token: token, secUser: user, expiryDate: createExpiryDate() )
     50
     51        return SAUser.save(flush: true)
     52    }
     53   
     54    public boolean logOffRemotely( String consumer, String token ) {
     55        def user = getSessionAuthenticatedUser(consumer, token)
     56       
     57        if( user )
     58            user.delete()
     59       
     60        return true
     61    }
     62
     63    /**
     64     * Checks whether a user is logged in from a remote consumer with the
     65     * given token
     66     */
     67    public boolean isRemotelyLoggedIn( String consumer, String token ) {
     68        // Remove expired users, otherwise they will be kept in the database forever
     69        removeExpiredTokens()
     70
     71        // Check whether a user exists
     72        def user = getSessionAuthenticatedUser(consumer, token)
     73
     74        // Check whether the user is logged in. Since we don't want to return a
     75        // user, we explicitly return true or false
     76        if( user ) {
     77                        // The expiry date should be reset
     78                        updateExpiryDate( user )
     79
     80            return true
     81                } else {
     82            return false
     83                }
     84    }
     85
     86    /**
     87     * Returns the user that is logged in remotely
     88     */
     89    public SecUser getRemotelyLoggedInUser( String consumer, String token ) {
     90        // Remove expired users, otherwise they will be kept in the database forever
     91        removeExpiredTokens()
     92
     93        // Check whether a user exists
     94        def user = getSessionAuthenticatedUser(consumer, token)
     95
     96        return user ? user.secUser : null
     97    }
     98
     99    /**
     100     * Removes all tokens for remote logins that have expired
     101     */
     102    protected boolean removeExpiredTokens() {
     103        SessionAuthenticatedUser.executeUpdate("delete SessionAuthenticatedUser u where u.expiryDate < :expiryDate", [ expiryDate: new Date() ])
     104    }
     105
     106    /**
     107         * Returns the currently logged in user from the database or null if no user is logged in
     108         */
     109        protected SessionAuthenticatedUser getSessionAuthenticatedUser( String consumer, String token ) {
     110        def c = SessionAuthenticatedUser.createCriteria()
     111        def result = c.get {
     112                and {
     113                        eq( "consumer", consumer)
     114                        eq( "token", token)
     115                        gt( "expiryDate", new Date())
     116                }
     117        }
     118
     119        if( result )
     120            return result
     121        else
     122            return null
     123    }
     124
     125        /**
     126         * Returns the expiry date for a user that is active now.
     127         */
     128        protected Date createExpiryDate() {
     129                // Compute expiryDate
     130                long now = new Date().getTime();
     131                return new Date( now + AuthenticationService.expiryTime * 60 * 1000 );
     132
     133        }
     134
     135        /**
     136         * Resets the expiry date of the given user. This should be called every time
     137         * an action occurs with this user. That way, if (in case of a timeout of 60 minutes)
     138         * he logs in and returns 50 minutes later, he will keep a timeout value of
     139         * 60 minutes, instead of only 10 minutes.
     140         */
     141        protected boolean updateExpiryDate( SessionAuthenticatedUser user ) {
     142                user.expiryDate = createExpiryDate()
     143                return user.save( flush: true )
     144        }
    49145}
  • trunk/grails-app/views/login/auth.gsp

    r976 r983  
    6767                                        <input type='submit' value='Login' />
    6868                                </p>
     69
     70                                <g:if test="${redirectUrl}">
     71                                  <g:hiddenField name="spring-security-redirect" value="${redirectUrl}" />
     72                                </g:if>
    6973                        </form>
    7074                </div>
Note: See TracChangeset for help on using the changeset viewer.