diff --git a/build.gradle b/build.gradle index 561bd50..2c9e5ec 100644 --- a/build.gradle +++ b/build.gradle @@ -53,7 +53,9 @@ dependencies { compile "org.grails.plugins:views-json" compile "org.grails.plugins:views-json-templates" compileOnly "io.micronaut:micronaut-inject-groovy" - console "org.grails:grails-console" + implementation group: 'com.nimbusds', name: 'nimbus-jose-jwt', version: '9.7' + implementation group: 'com.google.api-client', name: 'google-api-client', version: '1.31.2' + implementation group: 'com.google.api-client', name: 'google-api-client-jackson2', version: '1.20.0' profile "org.grails.profiles:rest-api" runtime "org.glassfish.web:el-impl:2.1.2-b03" runtime "com.h2database:h2" diff --git a/grails-app/controllers/org/xena/analysis/AuthenticatedUserController.groovy b/grails-app/controllers/org/xena/analysis/AuthenticatedUserController.groovy new file mode 100644 index 0000000..c56db7a --- /dev/null +++ b/grails-app/controllers/org/xena/analysis/AuthenticatedUserController.groovy @@ -0,0 +1,83 @@ +package org.xena.analysis + +import grails.validation.ValidationException +import static org.springframework.http.HttpStatus.CREATED +import static org.springframework.http.HttpStatus.NOT_FOUND +import static org.springframework.http.HttpStatus.NO_CONTENT +import static org.springframework.http.HttpStatus.OK +import static org.springframework.http.HttpStatus.UNPROCESSABLE_ENTITY + +import grails.gorm.transactions.ReadOnly +import grails.gorm.transactions.Transactional + +@ReadOnly +class AuthenticatedUserController { + + AuthenticatedUserService authenticatedUserService + + static responseFormats = ['json', 'xml'] + static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"] + + def index(Integer max) { + params.max = Math.min(max ?: 10, 100) + respond authenticatedUserService.list(params), model:[authenticatedUserCount: authenticatedUserService.count()] + } + + def show(Long id) { + respond authenticatedUserService.get(id) + } + + @Transactional + def save(AuthenticatedUser authenticatedUser) { + if (authenticatedUser == null) { + render status: NOT_FOUND + return + } + if (authenticatedUser.hasErrors()) { + transactionStatus.setRollbackOnly() + respond authenticatedUser.errors + return + } + + try { + authenticatedUserService.save(authenticatedUser) + } catch (ValidationException e) { + respond authenticatedUser.errors + return + } + + respond authenticatedUser, [status: CREATED, view:"show"] + } + + @Transactional + def update(AuthenticatedUser authenticatedUser) { + if (authenticatedUser == null) { + render status: NOT_FOUND + return + } + if (authenticatedUser.hasErrors()) { + transactionStatus.setRollbackOnly() + respond authenticatedUser.errors + return + } + + try { + authenticatedUserService.save(authenticatedUser) + } catch (ValidationException e) { + respond authenticatedUser.errors + return + } + + respond authenticatedUser, [status: OK, view:"show"] + } + + @Transactional + def delete(Long id) { + if (id == null || authenticatedUserService.delete(id) == null) { + render status: NOT_FOUND + return + } + + render status: NO_CONTENT + } +} diff --git a/grails-app/controllers/org/xena/analysis/GmtController.groovy b/grails-app/controllers/org/xena/analysis/GmtController.groovy index ea351b0..350af1f 100644 --- a/grails-app/controllers/org/xena/analysis/GmtController.groovy +++ b/grails-app/controllers/org/xena/analysis/GmtController.groovy @@ -20,6 +20,7 @@ class GmtController { GmtService gmtService AnalysisService analysisService TpmAnalysisService tpmAnalysisService + UserService userService static responseFormats = ['json', 'xml'] static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE",deleteByMethodAndName: "DELETE", analyzeGmt: "POST"] @@ -88,21 +89,43 @@ class GmtController { } + @Transactional def names(String method) { println "method: ${method}" - def gmtList = Gmt.executeQuery(" select g.name,g.geneSetCount,g.availableTpmCount,count(r) from Gmt g left outer join g.results r group by g") - println "gmtlist ${gmtList}" +// println "req: ${request.getHeader('Authorization')}" + def publicGmtList = Gmt.executeQuery(" select g.name,g.geneSetCount,g.availableTpmCount,g.isPublic,count(r) from Gmt g left outer join g.results r where g.isPublic = 't' group by g") + println "gmt list: ${publicGmtList}" + + if(request.getHeader('Authorization')){ + AuthenticatedUser user = userService.getUserFromRequest(request) + if(user){ + def privateList = [] + if(user.role == RoleEnum.ADMIN){ + privateList = Gmt.executeQuery(" select g.name,g.geneSetCount,g.availableTpmCount,g.isPublic,count(r),u from Gmt g left outer join g.results r join g.authenticatedUser u where g.isPublic != 't' group by g, u") + } + else + if(user.role == RoleEnum.USER){ + privateList = Gmt.executeQuery(" select g.name,g.geneSetCount,g.availableTpmCount,g.isPublic,count(r),u from Gmt g left outer join g.results r join g.authenticatedUser u where g.authenticatedUser=:user group by g, u",[user:user]) + } + publicGmtList = publicGmtList + privateList + } + } + + +// println "gmtlist ${publicGmtList as JSON}" JSONArray jsonArray = new JSONArray() - gmtList.sort{ a,b -> a[0].toString().compareTo(b[0].toString())} .each { def gmtEntry -> + publicGmtList.sort{ a,b -> a[0].toString().compareTo(b[0].toString())} .each { def gmtEntry -> def obj = new JSONObject() obj.name = gmtEntry[0] obj.geneCount = gmtEntry[1] -// obj.hash = it.hash -// obj.id = it.id obj.method = method obj.availableCount = gmtEntry[2] - obj.readyCount = gmtEntry[3] + obj.public = gmtEntry[3] + obj.readyCount = gmtEntry[4] obj.ready = obj.availableCount == obj.readyCount + if(gmtEntry.size()>5){ + obj.user = gmtEntry[5].firstName + " " + gmtEntry[5].lastName + } jsonArray.add(obj) } render jsonArray as JSON @@ -115,6 +138,13 @@ class GmtController { @Transactional def store() { + + AuthenticatedUser user = userService.getUserFromRequest(request) + if(!user){ + throw new RuntimeException("Not authorized") + } + + def json = request.JSON String method = json.method String gmtname = json.gmtname @@ -122,15 +152,15 @@ class GmtController { def geneCount = json.gmtdata.split("\n").findAll{it.split("\t").size()>2 }.size() - println "stroring with method '${method}' and gmt name '${gmtname}' '${gmtDataHash}" +// println "stroring with method '${method}' and gmt name '${gmtname}' '${gmtDataHash}" Gmt gmt = Gmt.findByName(gmtname) if (gmt == null) { def sameDataGmt = Gmt.findByHashAndMethod(gmtDataHash,method) if(sameDataGmt){ - gmt = new Gmt(name: gmtname, hash: gmtDataHash, data: sameDataGmt.data, method: method, geneSetCount: geneCount) + gmt = new Gmt(name: gmtname, hash: gmtDataHash, data: sameDataGmt.data, method: method, geneSetCount: geneCount,authenticatedUser:user,isPublic: false) } else{ - gmt = new Gmt(name: gmtname, hash: gmtDataHash, data: json.gmtdata, method: method, geneSetCount: geneCount) + gmt = new Gmt(name: gmtname, hash: gmtDataHash, data: json.gmtdata, method: method, geneSetCount: geneCount,authenticatedUser:user,isPublic: false) } gmt.save(failOnError: true) } diff --git a/grails-app/controllers/xena/analysis/grails/UrlMappings.groovy b/grails-app/controllers/xena/analysis/grails/UrlMappings.groovy index 2e6a5bb..c4fd7e8 100644 --- a/grails-app/controllers/xena/analysis/grails/UrlMappings.groovy +++ b/grails-app/controllers/xena/analysis/grails/UrlMappings.groovy @@ -18,6 +18,9 @@ class UrlMappings { } } + "/user"(resources:'authenticatedUser') + + // "/result/test"(controller: 'result', action: 'test') // "/result/analyze"(controller: 'result', action: 'analyze') "/"(controller: 'application', action: 'index') diff --git a/grails-app/domain/org/xena/analysis/AuthenticatedUser.groovy b/grails-app/domain/org/xena/analysis/AuthenticatedUser.groovy new file mode 100644 index 0000000..9707d57 --- /dev/null +++ b/grails-app/domain/org/xena/analysis/AuthenticatedUser.groovy @@ -0,0 +1,21 @@ +package org.xena.analysis + +class AuthenticatedUser { + + static constraints = { + email email: true,nullable: false,blank: false,unique: true + firstName nullable: true + lastName nullable: true + role blank: false,nullable: false + } + + String firstName + String lastName + String email + RoleEnum role // + + static hasMany = [ + gmts:Gmt + ] + +} diff --git a/grails-app/domain/org/xena/analysis/Gmt.groovy b/grails-app/domain/org/xena/analysis/Gmt.groovy index 1ac5227..964958d 100644 --- a/grails-app/domain/org/xena/analysis/Gmt.groovy +++ b/grails-app/domain/org/xena/analysis/Gmt.groovy @@ -8,6 +8,8 @@ class Gmt { String data int geneSetCount int availableTpmCount // count from defaultGeneSet on initial load + AuthenticatedUser authenticatedUser + Boolean isPublic // use gene names as key String stats // { 'ABC':{mean: 0.11212, std: 0.272 }, 'DEF': { mean:0.17, std:0.3 } } @@ -15,6 +17,8 @@ class Gmt { static constraints = { name blank: false, unique: true stats nullable: true + authenticatedUser nullable: true + isPublic nullable: true } static mapping = { diff --git a/grails-app/domain/org/xena/analysis/RoleEnum.groovy b/grails-app/domain/org/xena/analysis/RoleEnum.groovy new file mode 100644 index 0000000..e08b7cc --- /dev/null +++ b/grails-app/domain/org/xena/analysis/RoleEnum.groovy @@ -0,0 +1,19 @@ +package org.xena.analysis + +enum RoleEnum { + + USER("user",10), + BANNED("banned",1), + INACTIVE("inactive",5), + ADMIN("admin",100); + + private String display; // pertains to the 1.0 value + private Integer rank; + + RoleEnum(String display , int rank) { + this.display = display; + this.rank = rank; + } + + +} diff --git a/grails-app/init/xena/analysis/grails/BootStrap.groovy b/grails-app/init/xena/analysis/grails/BootStrap.groovy index 1c84a33..0675fe4 100644 --- a/grails-app/init/xena/analysis/grails/BootStrap.groovy +++ b/grails-app/init/xena/analysis/grails/BootStrap.groovy @@ -1,14 +1,18 @@ package xena.analysis.grails -import org.xena.analysis.AnalysisService +import org.xena.analysis.AuthenticatedUser import org.xena.analysis.CohortService +import org.xena.analysis.RoleEnum +import org.xena.analysis.UserService class BootStrap { CohortService cohortService + UserService userService def init = { servletContext -> cohortService.validateCohorts() + userService.createAdmins() } def destroy = { } diff --git a/grails-app/services/org/xena/analysis/AuthenticatedUserService.groovy b/grails-app/services/org/xena/analysis/AuthenticatedUserService.groovy new file mode 100644 index 0000000..d3dee4b --- /dev/null +++ b/grails-app/services/org/xena/analysis/AuthenticatedUserService.groovy @@ -0,0 +1,18 @@ +package org.xena.analysis + +import grails.gorm.services.Service + +@Service(AuthenticatedUser) +interface AuthenticatedUserService { + + AuthenticatedUser get(Serializable id) + + List list(Map args) + + Long count() + + AuthenticatedUser delete(Serializable id) + + AuthenticatedUser save(AuthenticatedUser authenticatedUser) + +} diff --git a/grails-app/services/org/xena/analysis/UserService.groovy b/grails-app/services/org/xena/analysis/UserService.groovy new file mode 100644 index 0000000..017d936 --- /dev/null +++ b/grails-app/services/org/xena/analysis/UserService.groovy @@ -0,0 +1,116 @@ +package org.xena.analysis + +//import com.nimbusds.jose.JWSSigner +//import com.nimbusds.jose.JWSVerifier +//import com.nimbusds.jose.crypto.RSASSASigner +//import com.nimbusds.jose.crypto.RSASSAVerifier +//import com.nimbusds.jose.jwk.RSAKey +//import com.nimbusds.jose.jwk.gen.RSAKeyGenerator +//import com.nimbusds.jwt.JWT +//import com.nimbusds.jwt.JWTParser +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload; +import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.jackson2.JacksonFactory; +import com.nimbusds.jwt.SignedJWT +import grails.gorm.transactions.Transactional +import org.grails.web.json.JSONObject + +import javax.servlet.http.HttpServletRequest + +@Transactional +class UserService { + + public final static String[] ROLES = [ "ADMIN", "USER","BANNED","INACTIVE"] + + private final String CLIENT_ID = System.getenv("GOOGLE_ID") + + private final String[] ADMIN_USERS = [ + "nathandunn@lbl.gov", + "jing", + "bcraft", + "mariangoldman" + ] + + AuthenticatedUser getUserFromRequest(HttpServletRequest httpServletRequest) { + + String authHeader = httpServletRequest.getHeader('Authorization') + println "authHeader" + println authHeader + String jwtString = authHeader.split("jwt=")[1] + String accessToken = httpServletRequest.getHeader('GoogleAccessToken') + println "accessToken" + println accessToken + +// // TODO: +// // from https://connect2id.com/products/nimbus-jose-jwt/examples/jwt-with-rsa-signature +// // +// JWT jwt = JWTParser.parse(jwtString) +// println "is signed? ${jwt instanceof SignedJWT}" +// RSAKey rsaJWK = new RSAKeyGenerator(2048) +// .keyID(System.getenv("GOOGLE_ID")) +// .generate(); +// RSAKey rsaPublicJWK = rsaJWK.toPublicJWK(); +//// Create RSA-signer with the private key +// JWSSigner signer = new RSASSASigner(rsaJWK); +// JWSVerifier verifier = new RSASSAVerifier(rsaPublicJWK); +// Boolean validated = signedJWT.verify(verifier) +// assertTrue(signedJWT.verify(verifier)); +// println "is valid token: ${validated}" + + SignedJWT signedJWT = SignedJWT.parse(jwtString); + def claimObject = new JSONObject(signedJWT.getJWTClaimsSet().toJSONObject()) + Boolean isEmailVerified = claimObject.getBoolean("email_verified") + if(!isEmailVerified){ + throw new RuntimeException("Not validated") + } + + // TODO: do stuff to extract the token and clean stuff up +// def tokens = jwtString.split("\\.") +// println tokens.length +// def headers = new String(java.util.Base64.decoder.decode(tokens[1])) +// println headers +// def jsonObject = new JSONObject(headers) + + + // TODO: need to get the access token to work here +// GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory()) +// // Specify the CLIENT_ID of the app that accesses the backend: +// .setAudience(Collections.singletonList(CLIENT_ID)) +// // Or, if multiple clients access the backend: +// //.setAudience(Arrays.asList(CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3)) +// .build(); +// println "E" +// GoogleIdToken idToken = verifier.verify(accessToken); +// println "id token ${idToken}" +// + + String username = claimObject.email + // TODO: get user object + AuthenticatedUser user = AuthenticatedUser.findByEmail(username) + if(!user){ + user = new AuthenticatedUser( + firstName: claimObject["given_name"], + lastName: claimObject["family_name"], + email: claimObject["email"], + role: RoleEnum.USER + ).save(flush: true, failOnError:true,insert:true) + } + else{ + // update if different + user.firstName = claimObject["given_name"] + user.lastName = claimObject["family_name"] + user.save(insert:false,flush: true,failOnError: true) + } + return user + + } + + def createAdmins(){ + AuthenticatedUser.findOrSaveByEmailAndRole("ndunnme@gmail.com",RoleEnum.ADMIN) + AuthenticatedUser.findOrSaveByEmailAndRole("jzhu@soe.ucsc.edu",RoleEnum.ADMIN) + AuthenticatedUser.findOrSaveByEmailAndRole("craft@soe.ucsc.edu",RoleEnum.ADMIN) + AuthenticatedUser.findOrSaveByEmailAndRole("mary@soe.ucsc.edu",RoleEnum.ADMIN) + } +}