Skip to content

Commit ecb2237

Browse files
Merge pull request #86 from uc-cdis/feat/team_project_feature
Feat: team project feature
2 parents 4ba94d3 + 9ac8935 commit ecb2237

File tree

18 files changed

+823
-163
lines changed

18 files changed

+823
-163
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ cd tests/setup_local_db/
118118
JSON summary data endpoints:
119119
```bash
120120
curl http://localhost:8080/sources | python -m json.tool
121-
curl http://localhost:8080/cohortdefinition-stats/by-source-id/1 | python -m json.tool
121+
curl "http://localhost:8080/cohortdefinition-stats/by-source-id/1/by-team-project?team-project=test" | python -m json.tool
122122
curl http://localhost:8080/concept/by-source-id/1 | python -m json.tool
123123
curl -d '{"ConceptIds":[2000000324,2000006885]}' -H "Content-Type: application/json" -X POST http://localhost:8080/concept/by-source-id/1 | python -m json.tool
124124
curl -d '{"ConceptTypes":["Measurement","Person"]}' -H "Content-Type: application/json" -X POST http://localhost:8080/concept/by-source-id/1/by-type | python -m json.tool

config/mocktest.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
arborist_endpoint: 'https://arboristdummyurl'

controllers/cohortdata.go

+35-6
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,21 @@ import (
99
"strconv"
1010

1111
"github.com/gin-gonic/gin"
12+
"github.com/uc-cdis/cohort-middleware/middlewares"
1213
"github.com/uc-cdis/cohort-middleware/models"
1314
"github.com/uc-cdis/cohort-middleware/utils"
1415
)
1516

1617
type CohortDataController struct {
17-
cohortDataModel models.CohortDataI
18+
cohortDataModel models.CohortDataI
19+
teamProjectAuthz middlewares.TeamProjectAuthzI
1820
}
1921

20-
func NewCohortDataController(cohortDataModel models.CohortDataI) CohortDataController {
21-
return CohortDataController{cohortDataModel: cohortDataModel}
22+
func NewCohortDataController(cohortDataModel models.CohortDataI, teamProjectAuthz middlewares.TeamProjectAuthzI) CohortDataController {
23+
return CohortDataController{
24+
cohortDataModel: cohortDataModel,
25+
teamProjectAuthz: teamProjectAuthz,
26+
}
2227
}
2328

2429
func (u CohortDataController) RetrieveHistogramForCohortIdAndConceptId(c *gin.Context) {
@@ -44,6 +49,14 @@ func (u CohortDataController) RetrieveHistogramForCohortIdAndConceptId(c *gin.Co
4449
cohortId, _ := strconv.Atoi(cohortIdStr)
4550
histogramConceptId, _ := strconv.ParseInt(histogramIdStr, 10, 64)
4651

52+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs)
53+
if !validAccessRequest {
54+
log.Printf("Error: invalid request")
55+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
56+
c.Abort()
57+
return
58+
}
59+
4760
cohortData, err := u.cohortDataModel.RetrieveHistogramDataBySourceIdAndCohortIdAndConceptIdsAndCohortPairs(sourceId, cohortId, histogramConceptId, filterConceptIds, cohortPairs)
4861
if err != nil {
4962
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving concept details", "error": err.Error()})
@@ -85,6 +98,14 @@ func (u CohortDataController) RetrieveDataBySourceIdAndCohortIdAndVariables(c *g
8598
sourceId, _ := strconv.Atoi(sourceIdStr)
8699
cohortId, _ := strconv.Atoi(cohortIdStr)
87100

101+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs)
102+
if !validAccessRequest {
103+
log.Printf("Error: invalid request")
104+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
105+
c.Abort()
106+
return
107+
}
108+
88109
// call model method:
89110
cohortData, err := u.cohortDataModel.RetrieveDataBySourceIdAndCohortIdAndConceptIdsOrderedByPersonId(sourceId, cohortId, conceptIds)
90111
if err != nil {
@@ -111,7 +132,7 @@ func generateCohortPairsHeaders(cohortPairs []utils.CustomDichotomousVariableDef
111132
cohortPairsHeaders := []string{}
112133

113134
for _, cohortPair := range cohortPairs {
114-
cohortPairsHeaders = append(cohortPairsHeaders, utils.GetCohortPairKey(cohortPair.CohortId1, cohortPair.CohortId2))
135+
cohortPairsHeaders = append(cohortPairsHeaders, utils.GetCohortPairKey(cohortPair.CohortDefinitionId1, cohortPair.CohortDefinitionId2))
115136
}
116137

117138
return cohortPairsHeaders
@@ -230,6 +251,14 @@ func (u CohortDataController) RetrieveCohortOverlapStatsWithoutFilteringOnConcep
230251
controlCohortId, errors[2] = utils.ParseNumericArg(c, "controlcohortid")
231252
conceptIds, cohortPairs, errors[3] = utils.ParseConceptIdsAndDichotomousDefs(c)
232253

254+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{caseCohortId, controlCohortId}, cohortPairs)
255+
if !validAccessRequest {
256+
log.Printf("Error: invalid request")
257+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
258+
c.Abort()
259+
return
260+
}
261+
233262
if utils.ContainsNonNil(errors) {
234263
c.JSON(http.StatusBadRequest, gin.H{"message": "bad request"})
235264
c.Abort()
@@ -298,8 +327,8 @@ func (u CohortDataController) RetrievePeopleIdAndCohort(sourceId int, cohortId i
298327
*/
299328
personIdToCSVValues := make(map[int64]map[string]string)
300329
for _, cohortPair := range cohortPairs {
301-
firstCohortDefinitionId := cohortPair.CohortId1
302-
secondCohortDefinitionId := cohortPair.CohortId2
330+
firstCohortDefinitionId := cohortPair.CohortDefinitionId1
331+
secondCohortDefinitionId := cohortPair.CohortDefinitionId2
303332
cohortPairKey := utils.GetCohortPairKey(firstCohortDefinitionId, secondCohortDefinitionId)
304333

305334
firstCohortPeopleData, err1 := u.cohortDataModel.RetrieveDataByOriginalCohortAndNewCohort(sourceId, cohortId, firstCohortDefinitionId)

controllers/cohortdefinition.go

+9-29
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ func NewCohortDefinitionController(cohortDefinitionModel models.CohortDefinition
1818
}
1919

2020
func (u CohortDefinitionController) RetriveById(c *gin.Context) {
21+
// TODO - add teamproject validation - check if user has the necessary atlas and arborist permissions
2122
cohortDefinitionId := c.Param("id")
2223

2324
if cohortDefinitionId != "" {
@@ -35,39 +36,18 @@ func (u CohortDefinitionController) RetriveById(c *gin.Context) {
3536
c.Abort()
3637
}
3738

38-
func (u CohortDefinitionController) RetriveByName(c *gin.Context) {
39-
cohortDefinitionName := c.Param("name")
40-
41-
if cohortDefinitionName != "" {
42-
cohortDefinition, err := u.cohortDefinitionModel.GetCohortDefinitionByName(cohortDefinitionName)
43-
if err != nil {
44-
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving cohortDefinition", "error": err.Error()})
45-
c.Abort()
46-
return
47-
}
48-
c.JSON(http.StatusOK, gin.H{"CohortDefinition": cohortDefinition})
49-
return
50-
}
51-
c.JSON(http.StatusBadRequest, gin.H{"message": "bad request"})
52-
c.Abort()
53-
}
54-
55-
func (u CohortDefinitionController) RetriveAll(c *gin.Context) {
56-
cohortDefinitions, err := u.cohortDefinitionModel.GetAllCohortDefinitions()
57-
if err != nil {
58-
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving cohortDefinition", "error": err.Error()})
39+
func (u CohortDefinitionController) RetriveStatsBySourceIdAndTeamProject(c *gin.Context) {
40+
// This method returns ALL cohortdefinition entries with cohort size statistics (for a given source)
41+
sourceId, err1 := utils.ParseNumericArg(c, "sourceid")
42+
teamProject := c.Query("team-project")
43+
if teamProject == "" {
44+
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error while parsing request", "error": "team-project is a mandatory parameter but was found to be empty!"})
5945
c.Abort()
6046
return
6147
}
62-
c.JSON(http.StatusOK, gin.H{"cohort_definitions": cohortDefinitions})
63-
}
64-
65-
func (u CohortDefinitionController) RetriveStatsBySourceId(c *gin.Context) {
66-
// This method returns ALL cohortdefinition entries with cohort size statistics (for a given source)
67-
68-
sourceId, err1 := utils.ParseNumericArg(c, "sourceid")
48+
// TODO - validate teamproject against arborist
6949
if err1 == nil {
70-
cohortDefinitionsAndStats, err := u.cohortDefinitionModel.GetAllCohortDefinitionsAndStatsOrderBySizeDesc(sourceId)
50+
cohortDefinitionsAndStats, err := u.cohortDefinitionModel.GetAllCohortDefinitionsAndStatsOrderBySizeDesc(sourceId, teamProject)
7151
if err != nil {
7252
c.JSON(http.StatusInternalServerError, gin.H{"message": "Error retrieving cohortDefinition", "error": err.Error()})
7353
c.Abort()

controllers/concept.go

+33-2
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,23 @@ import (
1010
"strconv"
1111

1212
"github.com/gin-gonic/gin"
13+
"github.com/uc-cdis/cohort-middleware/middlewares"
1314
"github.com/uc-cdis/cohort-middleware/models"
1415
"github.com/uc-cdis/cohort-middleware/utils"
1516
)
1617

1718
type ConceptController struct {
1819
conceptModel models.ConceptI
1920
cohortDefinitionModel models.CohortDefinitionI
21+
teamProjectAuthz middlewares.TeamProjectAuthzI
2022
}
2123

22-
func NewConceptController(conceptModel models.ConceptI, cohortDefinitionModel models.CohortDefinitionI) ConceptController {
23-
return ConceptController{conceptModel: conceptModel, cohortDefinitionModel: cohortDefinitionModel}
24+
func NewConceptController(conceptModel models.ConceptI, cohortDefinitionModel models.CohortDefinitionI, teamProjectAuthz middlewares.TeamProjectAuthzI) ConceptController {
25+
return ConceptController{
26+
conceptModel: conceptModel,
27+
cohortDefinitionModel: cohortDefinitionModel,
28+
teamProjectAuthz: teamProjectAuthz,
29+
}
2430
}
2531

2632
func (u ConceptController) RetriveAllBySourceId(c *gin.Context) {
@@ -93,6 +99,14 @@ func (u ConceptController) RetrieveBreakdownStatsBySourceIdAndCohortId(c *gin.Co
9399
c.Abort()
94100
return
95101
}
102+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidationForCohort(c, cohortId)
103+
if !validAccessRequest {
104+
log.Printf("Error: invalid request")
105+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
106+
c.Abort()
107+
return
108+
}
109+
96110
breakdownConceptId, err := utils.ParseBigNumericArg(c, "breakdownconceptid")
97111
if err != nil {
98112
log.Printf("Error: %s", err.Error())
@@ -118,6 +132,14 @@ func (u ConceptController) RetrieveBreakdownStatsBySourceIdAndCohortIdAndVariabl
118132
c.Abort()
119133
return
120134
}
135+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs)
136+
if !validAccessRequest {
137+
log.Printf("Error: invalid request")
138+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
139+
c.Abort()
140+
return
141+
}
142+
121143
breakdownConceptId, err := utils.ParseBigNumericArg(c, "breakdownconceptid")
122144
if err != nil {
123145
log.Printf("Error: %s", err.Error())
@@ -175,6 +197,15 @@ func (u ConceptController) RetrieveAttritionTable(c *gin.Context) {
175197
c.Abort()
176198
return
177199
}
200+
_, cohortPairs := utils.GetConceptIdsAndCohortPairsAsSeparateLists(conceptIdsAndCohortPairs)
201+
validAccessRequest := u.teamProjectAuthz.TeamProjectValidation(c, []int{cohortId}, cohortPairs)
202+
if !validAccessRequest {
203+
log.Printf("Error: invalid request")
204+
c.JSON(http.StatusBadRequest, gin.H{"message": "access denied"})
205+
c.Abort()
206+
return
207+
}
208+
178209
breakdownConceptId, err := utils.ParseBigNumericArg(c, "breakdownconceptid")
179210
if err != nil {
180211
log.Printf("Error: %s", err.Error())

middlewares/auth.go

+18-5
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ func AuthMiddleware() gin.HandlerFunc {
2222
}
2323

2424
return func(ctx *gin.Context) {
25-
req, err := PrepareNewArboristRequest(ctx, c.GetString("arborist_endpoint"))
25+
req, err := PrepareNewArboristRequest(ctx)
2626
if err != nil {
2727
ctx.AbortWithStatus(500)
2828
log.Printf("Error while preparing Arborist request: %s", err.Error())
29+
return
2930
}
3031
client := &http.Client{}
3132
// send the request to Arborist:
@@ -44,21 +45,33 @@ func AuthMiddleware() gin.HandlerFunc {
4445
}
4546

4647
// this function will take the request from the given ctx, validated it for the presence of an "Authorization / Bearer" token
47-
// and then return the URL that can be used to consult Arborist regarding access permissions. This function
48+
// and then return the URL that can be used to consult Arborist regarding cohort-middleware access permissions. This function
4849
// returns an error if "Authorization / Bearer" token is missing in ctx
49-
func PrepareNewArboristRequest(ctx *gin.Context, arboristEndpoint string) (*http.Request, error) {
50+
func PrepareNewArboristRequest(ctx *gin.Context) (*http.Request, error) {
51+
52+
resourcePath := fmt.Sprintf("/cohort-middleware%s", ctx.Request.URL.Path)
53+
service := "cohort-middleware"
54+
55+
return PrepareNewArboristRequestForResourceAndService(ctx, resourcePath, service)
56+
}
57+
58+
// this function will take the request from the given ctx, validated it for the presence of an "Authorization / Bearer" token
59+
// and then return the URL that can be used to consult Arborist regarding access permissions for the given
60+
// resource path and service.
61+
func PrepareNewArboristRequestForResourceAndService(ctx *gin.Context, resourcePath string, service string) (*http.Request, error) {
62+
c := config.GetConfig()
63+
arboristEndpoint := c.GetString("arborist_endpoint")
5064
// validate:
5165
authorization := ctx.Request.Header.Get("Authorization")
5266
if authorization == "" {
5367
return nil, errors.New("missing Authorization header")
5468
}
5569

5670
// build up the request URL string:
57-
resourcePath := fmt.Sprintf("/cohort-middleware%s", ctx.Request.URL.Path)
5871
arboristAuth := fmt.Sprintf("%s/auth/proxy?resource=%s&service=%s&method=%s",
5972
arboristEndpoint,
6073
resourcePath,
61-
"cohort-middleware",
74+
service,
6275
"access")
6376

6477
// make request object / validate URL:

middlewares/teamprojectauthz.go

+87
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package middlewares
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/gin-gonic/gin"
8+
"github.com/uc-cdis/cohort-middleware/models"
9+
"github.com/uc-cdis/cohort-middleware/utils"
10+
)
11+
12+
type TeamProjectAuthzI interface {
13+
TeamProjectValidationForCohort(ctx *gin.Context, cohortDefinitionId int) bool
14+
TeamProjectValidation(ctx *gin.Context, cohortDefinitionIds []int, filterCohortPairs []utils.CustomDichotomousVariableDef) bool
15+
TeamProjectValidationForCohortIdsList(ctx *gin.Context, uniqueCohortDefinitionIdsList []int) bool
16+
}
17+
18+
type HttpClientI interface {
19+
Do(req *http.Request) (*http.Response, error)
20+
}
21+
22+
type TeamProjectAuthz struct {
23+
cohortDefinitionModel models.CohortDefinitionI
24+
httpClient HttpClientI
25+
}
26+
27+
func NewTeamProjectAuthz(cohortDefinitionModel models.CohortDefinitionI, httpClient HttpClientI) TeamProjectAuthz {
28+
return TeamProjectAuthz{
29+
cohortDefinitionModel: cohortDefinitionModel,
30+
httpClient: httpClient,
31+
}
32+
}
33+
func (u TeamProjectAuthz) hasAccessToAtLeastOne(ctx *gin.Context, teamProjects []string) bool {
34+
35+
// query Arborist and return as soon as one of the teamProjects access check returns 200:
36+
for _, teamProject := range teamProjects {
37+
teamProjectAsResourcePath := teamProject
38+
teamProjectAccessService := "atlas-argo-wrapper-and-cohort-middleware"
39+
40+
req, err := PrepareNewArboristRequestForResourceAndService(ctx, teamProjectAsResourcePath, teamProjectAccessService)
41+
if err != nil {
42+
ctx.AbortWithStatus(500)
43+
panic("Error while preparing Arborist request")
44+
}
45+
// send the request to Arborist:
46+
resp, _ := u.httpClient.Do(req)
47+
log.Printf("Got response status %d from Arborist...", resp.StatusCode)
48+
49+
// arborist will return with 200 if the user has been granted access to the cohort-middleware URL in ctx:
50+
if resp.StatusCode == 200 {
51+
return true
52+
} else {
53+
// unauthorized or otherwise:
54+
log.Printf("Status %d does NOT give access to team project...", resp.StatusCode)
55+
}
56+
}
57+
return false
58+
}
59+
60+
func (u TeamProjectAuthz) TeamProjectValidationForCohort(ctx *gin.Context, cohortDefinitionId int) bool {
61+
filterCohortPairs := []utils.CustomDichotomousVariableDef{}
62+
return u.TeamProjectValidation(ctx, []int{cohortDefinitionId}, filterCohortPairs)
63+
}
64+
65+
func (u TeamProjectAuthz) TeamProjectValidation(ctx *gin.Context, cohortDefinitionIds []int, filterCohortPairs []utils.CustomDichotomousVariableDef) bool {
66+
67+
uniqueCohortDefinitionIdsList := utils.GetUniqueCohortDefinitionIdsListFromRequest(cohortDefinitionIds, filterCohortPairs)
68+
return u.TeamProjectValidationForCohortIdsList(ctx, uniqueCohortDefinitionIdsList)
69+
}
70+
71+
// "team project" related checks:
72+
// (1) check if all cohorts belong to the same "team project"
73+
// (2) check if the user has permission in the "team project"
74+
// Returns true if both checks above pass, false otherwise.
75+
func (u TeamProjectAuthz) TeamProjectValidationForCohortIdsList(ctx *gin.Context, uniqueCohortDefinitionIdsList []int) bool {
76+
teamProjects, _ := u.cohortDefinitionModel.GetTeamProjectsThatMatchAllCohortDefinitionIds(uniqueCohortDefinitionIdsList)
77+
if len(teamProjects) == 0 {
78+
log.Printf("Invalid request error: could not find a 'team project' that is associated to ALL the cohorts present in this request")
79+
return false
80+
}
81+
if !u.hasAccessToAtLeastOne(ctx, teamProjects) {
82+
log.Printf("Invalid request error: user does not have access to any of the 'team projects' associated with the cohorts in this request")
83+
return false
84+
}
85+
// passed both tests:
86+
return true
87+
}

0 commit comments

Comments
 (0)