From 86f75cf87e271805564b49090eb0827076df500b Mon Sep 17 00:00:00 2001 From: grugna Date: Mon, 5 Feb 2024 14:05:03 -0600 Subject: [PATCH 01/19] adding token validation --- arborist/server.go | 50 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index dbe6e13..675814f 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -280,16 +280,44 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque Username string `json:"username"` ClientID string `json:"clientID"` }{} - err := json.Unmarshal(body, &requestBody) - if err != nil { - msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) - server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) - errResponse = newErrorResponse(msg, 400, nil) + + // Try to get username from the JWT. + username := "" + clientID := "" + if authHeader := r.Header.Get("Authorization"); authHeader != "" { + server.logger.Info("Attempting to get username or clientID from jwt...") + userJWT := strings.TrimPrefix(authHeader, "Bearer ") + userJWT = strings.TrimPrefix(userJWT, "bearer ") + scopes := []string{"openid"} + info, err := server.decodeToken(userJWT, scopes) + if err != nil { + // Return 400 on failure to decode JWT + msg := fmt.Sprintf("tried to get username/clientID from jwt, but jwt decode failed: %s", err.Error()) + server.logger.Info(msg) + errResponse = newErrorResponse(msg, 400, nil) + } + server.logger.Info("found username in jwt: %s", info.username) + username = info.username + clientID = info.clientID } - if (requestBody.Username == "") == (requestBody.ClientID == "") { - msg := "must specify exactly one of `username` or `clientID`" - server.logger.Info(msg) - errResponse = newErrorResponse(msg, 400, nil) + if errResponse == nil { + err := json.Unmarshal(body, &requestBody) + if err != nil { + msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) + server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) + errResponse = newErrorResponse(msg, 400, nil) + } else { + if (requestBody.Username == "") == (requestBody.ClientID == "") { + msg := "must specify exactly one of `username` or `clientID`" + server.logger.Info(msg) + errResponse = newErrorResponse(msg, 400, nil) + } + if (requestBody.Username != username) || (requestBody.ClientID != clientID) { + msg := "The information provided in the payload differs from the one extracted from the token." + server.logger.Info(msg) + errResponse = newErrorResponse(msg, 400, nil) + } + } } if errResponse != nil { _ = errResponse.write(w, r) @@ -298,9 +326,9 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque var mappings AuthMapping if requestBody.ClientID != "" { - mappings, errResponse = authMappingForClient(server.db, requestBody.ClientID) + mappings, errResponse = authMappingForClient(server.db, clientID) } else { - mappings, errResponse = authMapping(server.db, requestBody.Username) + mappings, errResponse = authMapping(server.db, username) } if errResponse != nil { errResponse.log.write(server.logger) From 16c1549c5a499cfa189467df39ae724fb5e0949c Mon Sep 17 00:00:00 2001 From: grugna Date: Mon, 5 Feb 2024 15:11:47 -0600 Subject: [PATCH 02/19] update logic - token presence has priority over body --- arborist/server.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 675814f..f35476d 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -286,6 +286,7 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { server.logger.Info("Attempting to get username or clientID from jwt...") + server.logger.Info("header: %s", authHeader) userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} @@ -295,37 +296,46 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque msg := fmt.Sprintf("tried to get username/clientID from jwt, but jwt decode failed: %s", err.Error()) server.logger.Info(msg) errResponse = newErrorResponse(msg, 400, nil) + } else { + username = info.username + clientID = info.clientID + server.logger.Info("found username in jwt: %s", username) + server.logger.Info("found clientID in jwt: %s", clientID) + } - server.logger.Info("found username in jwt: %s", info.username) - username = info.username - clientID = info.clientID } - if errResponse == nil { + + if (username == "") && (clientID == "") { err := json.Unmarshal(body, &requestBody) if err != nil { msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { + server.logger.Info("client in body: %s", requestBody.ClientID) + server.logger.Info("user in body: %s", requestBody.Username) + if (requestBody.Username == "") == (requestBody.ClientID == "") { msg := "must specify exactly one of `username` or `clientID`" server.logger.Info(msg) errResponse = newErrorResponse(msg, 400, nil) } - if (requestBody.Username != username) || (requestBody.ClientID != clientID) { - msg := "The information provided in the payload differs from the one extracted from the token." - server.logger.Info(msg) - errResponse = newErrorResponse(msg, 400, nil) + + if requestBody.ClientID != "" { + ClientID = requestBody.ClientID + } else { + username = requestBody.Username } } } + if errResponse != nil { _ = errResponse.write(w, r) return } var mappings AuthMapping - if requestBody.ClientID != "" { + if ClientID != "" { mappings, errResponse = authMappingForClient(server.db, clientID) } else { mappings, errResponse = authMapping(server.db, username) From d6d190950091049eb6c711bf8b7418cc66f1d8c2 Mon Sep 17 00:00:00 2001 From: grugna Date: Mon, 5 Feb 2024 15:15:35 -0600 Subject: [PATCH 03/19] Update server.go --- arborist/server.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index f35476d..8ece1ee 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -281,12 +281,15 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque ClientID string `json:"clientID"` }{} - // Try to get username from the JWT. + // Try to get username or clientID from the JWT. username := "" clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { server.logger.Info("Attempting to get username or clientID from jwt...") + + // TODO remove server.logger.Info("header: %s", authHeader) + userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} @@ -297,14 +300,16 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque server.logger.Info(msg) errResponse = newErrorResponse(msg, 400, nil) } else { + + // TODO add logic in the print username = info.username clientID = info.clientID server.logger.Info("found username in jwt: %s", username) server.logger.Info("found clientID in jwt: %s", clientID) - } } + // If they are not present in the token fallback on the request body if (username == "") && (clientID == "") { err := json.Unmarshal(body, &requestBody) if err != nil { @@ -312,6 +317,8 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { + + // TODO remove server.logger.Info("client in body: %s", requestBody.ClientID) server.logger.Info("user in body: %s", requestBody.Username) From a1b48ed60c63698c300311e21377c8c71bad68ef Mon Sep 17 00:00:00 2001 From: grugna Date: Mon, 5 Feb 2024 15:26:29 -0600 Subject: [PATCH 04/19] type --- arborist/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 8ece1ee..a5cb817 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -329,7 +329,7 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque } if requestBody.ClientID != "" { - ClientID = requestBody.ClientID + clientID = requestBody.ClientID } else { username = requestBody.Username } @@ -342,7 +342,7 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque } var mappings AuthMapping - if ClientID != "" { + if clientID != "" { mappings, errResponse = authMappingForClient(server.db, clientID) } else { mappings, errResponse = authMapping(server.db, username) From 21491c6aee442eafcf5c792d9a2e48bf8825849f Mon Sep 17 00:00:00 2001 From: grugna Date: Tue, 6 Feb 2024 12:21:50 -0600 Subject: [PATCH 05/19] change for DEV --- arborist/auth.go | 1 - 1 file changed, 1 deletion(-) diff --git a/arborist/auth.go b/arborist/auth.go index e309536..5a64227 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -709,7 +709,6 @@ func authMapping(db *sqlx.DB, username string) (AuthMapping, *ErrorResponse) { INNER JOIN resource ON resource.path <@ policy_resources.path WHERE ltree2text(resource.path) NOT LIKE ALL ( ARRAY[ - 'programs.pcdc.projects.20231114.%', 'programs.pcdc.projects.20230912.%', 'programs.pcdc.projects.20230523.%', 'programs.pcdc.projects.20230228.%', From c71a56863b0b817f611b735fbe51a81dbae54616 Mon Sep 17 00:00:00 2001 From: grugna Date: Tue, 6 Feb 2024 20:17:49 -0600 Subject: [PATCH 06/19] test parametrizing project exclusion --- arborist/auth.go | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/arborist/auth.go b/arborist/auth.go index 5a64227..27e9e3c 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -664,6 +664,22 @@ type AuthMappingQuery struct { type AuthMapping map[string][]Action + +// TODO This is just a patch to filter out excessive resources. When transitioning to pelican import we should have a project_id = xyz parameter instead +authMappingProjectExclusion := ` +ARRAY[ + 'programs.pcdc.projects.20230912.%', + 'programs.pcdc.projects.20230523.%', + 'programs.pcdc.projects.20230228.%', + 'programs.pcdc.projects.20220808.%', + 'programs.pcdc.projects.20220501_S01.%', + 'programs.pcdc.projects.20220201.%', + 'programs.pcdc.projects.20220110.%', + 'programs.pcdc.projects.20211006.%', + 'programs.pcdc.projects.20210915.%', + 'programs.pcdc.projects.20210212.%' + ] +` // authMapping gets the auth mapping for the user with this username. // The user's auth mapping includes the permissions of the `anonymous` and // `logged-in` groups. @@ -707,22 +723,15 @@ func authMapping(db *sqlx.DB, username string) (AuthMapping, *ErrorResponse) { INNER JOIN policy_role ON policy_role.policy_id = policies.policy_id INNER JOIN permission ON permission.role_id = policy_role.role_id INNER JOIN resource ON resource.path <@ policy_resources.path - WHERE ltree2text(resource.path) NOT LIKE ALL ( - ARRAY[ - 'programs.pcdc.projects.20230912.%', - 'programs.pcdc.projects.20230523.%', - 'programs.pcdc.projects.20230228.%', - 'programs.pcdc.projects.20220808.%', - 'programs.pcdc.projects.20220501_S01.%', - 'programs.pcdc.projects.20220201.%', - 'programs.pcdc.projects.20220110.%', - 'programs.pcdc.projects.20211006.%', - 'programs.pcdc.projects.20210915.%', - 'programs.pcdc.projects.20210212.%' - ] + WHERE ltree2text(resource.path) NOT LIKE ALL (` + + stmt += authMappingProjectExclusion + stmt += ` ) ` + logger.error("LUCAAAAA") + logger.error(stmt) // where resource.path ~ (CAST('programs.pcdc.projects.20230228.*' AS lquery)) // where ltree2text(resource.path) not like 'programs.pcdc.projects.20220201.%' and ltree2text(resource.path) not like 'programs.pcdc.projects.20220808.%') as teat; From b3375773d5c159ff3fc970ee4567645b11aef1ba Mon Sep 17 00:00:00 2001 From: grugna Date: Tue, 6 Feb 2024 21:38:51 -0600 Subject: [PATCH 07/19] typo --- arborist/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arborist/auth.go b/arborist/auth.go index 27e9e3c..14dea97 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -666,7 +666,7 @@ type AuthMapping map[string][]Action // TODO This is just a patch to filter out excessive resources. When transitioning to pelican import we should have a project_id = xyz parameter instead -authMappingProjectExclusion := ` +var authMappingProjectExclusion = ` ARRAY[ 'programs.pcdc.projects.20230912.%', 'programs.pcdc.projects.20230523.%', From ae250ccedf02a93f57ea83f221c6fd95fba151af Mon Sep 17 00:00:00 2001 From: grugna Date: Tue, 6 Feb 2024 21:55:11 -0600 Subject: [PATCH 08/19] typo --- arborist/auth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arborist/auth.go b/arborist/auth.go index 14dea97..e362cf3 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -730,8 +730,8 @@ func authMapping(db *sqlx.DB, username string) (AuthMapping, *ErrorResponse) { ) ` - logger.error("LUCAAAAA") - logger.error(stmt) + fmt.Print("LUCAAAAA") + fmt.Print(stmt) // where resource.path ~ (CAST('programs.pcdc.projects.20230228.*' AS lquery)) // where ltree2text(resource.path) not like 'programs.pcdc.projects.20220201.%' and ltree2text(resource.path) not like 'programs.pcdc.projects.20220808.%') as teat; From 0069eb1ebc31dd6ceb9bb28c6d1a3f25791cea3e Mon Sep 17 00:00:00 2001 From: grugna Date: Tue, 6 Feb 2024 22:13:20 -0600 Subject: [PATCH 09/19] Update auth.go --- arborist/auth.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/arborist/auth.go b/arborist/auth.go index e362cf3..29768d0 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -728,11 +728,7 @@ func authMapping(db *sqlx.DB, username string) (AuthMapping, *ErrorResponse) { stmt += authMappingProjectExclusion stmt += ` ) - ` - fmt.Print("LUCAAAAA") - fmt.Print(stmt) - // where resource.path ~ (CAST('programs.pcdc.projects.20230228.*' AS lquery)) // where ltree2text(resource.path) not like 'programs.pcdc.projects.20220201.%' and ltree2text(resource.path) not like 'programs.pcdc.projects.20220808.%') as teat; @@ -774,6 +770,12 @@ func authMappingForGroups(db *sqlx.DB, groups ...string) (AuthMapping, *ErrorRes INNER JOIN policy_role ON policy_role.policy_id = policies.policy_id INNER JOIN permission ON permission.role_id = policy_role.role_id INNER JOIN resource ON resource.path <@ roots.path + WHERE ltree2text(resource.path) NOT LIKE ALL (` + + stmt += authMappingProjectExclusion + stmt += ` + ) + ` // sqlx.In allows safely binding variable numbers of arguments as bindvars. // See https://jmoiron.github.io/sqlx/#inQueries, @@ -821,6 +823,11 @@ func authMappingForClient(db *sqlx.DB, clientID string) (AuthMapping, *ErrorResp INNER JOIN policy_role ON policy_role.policy_id = policies.policy_id INNER JOIN permission ON permission.role_id = policy_role.role_id INNER JOIN resource ON resource.path <@ roots.path + WHERE ltree2text(resource.path) NOT LIKE ALL (` + + stmt += authMappingProjectExclusion + stmt += ` + ) ` err := db.Select( &mappingQuery, From 2444e84eb05b355ac3c5f0bd06e746f234cde9ac Mon Sep 17 00:00:00 2001 From: grugna Date: Wed, 7 Feb 2024 09:29:05 -0600 Subject: [PATCH 10/19] Update server.go --- arborist/server.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index afccc4f..ff1ea69 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -288,9 +288,6 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque if authHeader := r.Header.Get("Authorization"); authHeader != "" { server.logger.Info("Attempting to get username or clientID from jwt...") - // TODO remove - server.logger.Info("header: %s", authHeader) - userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} @@ -305,8 +302,15 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque // TODO add logic in the print username = info.username clientID = info.clientID - server.logger.Info("found username in jwt: %s", username) - server.logger.Info("found clientID in jwt: %s", clientID) + server.logger.Info("%s", username) + server.logger.Info("%s", clientID) + if username != "" { + server.logger.Info("found username in jwt: %s", username) + } + if clientID != "" { + server.logger.Info("found clientID in jwt: %s", clientID) + } + } } From 7fa01793ec1c81e25f3ea06a26111ae69c8b6dbb Mon Sep 17 00:00:00 2001 From: grugna Date: Wed, 7 Feb 2024 10:12:55 -0600 Subject: [PATCH 11/19] Update server.go --- arborist/server.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index ff1ea69..512b62a 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -299,11 +299,8 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque errResponse = newErrorResponse(msg, 400, nil) } else { - // TODO add logic in the print username = info.username clientID = info.clientID - server.logger.Info("%s", username) - server.logger.Info("%s", clientID) if username != "" { server.logger.Info("found username in jwt: %s", username) } From 4e307ae268207f786601235c6c35bd461574ab65 Mon Sep 17 00:00:00 2001 From: grugna Date: Wed, 7 Feb 2024 10:16:42 -0600 Subject: [PATCH 12/19] Update server.go --- arborist/server.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 512b62a..b2ddcd9 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -319,11 +319,6 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { - - // TODO remove - server.logger.Info("client in body: %s", requestBody.ClientID) - server.logger.Info("user in body: %s", requestBody.Username) - if (requestBody.Username == "") == (requestBody.ClientID == "") { msg := "must specify exactly one of `username` or `clientID`" server.logger.Info(msg) From c72fbb13a9ac067c8e03f34baf5ebb0958d320fe Mon Sep 17 00:00:00 2001 From: Luca Graglia Date: Wed, 7 Feb 2024 10:17:55 -0600 Subject: [PATCH 13/19] remove logs --- arborist/server.go | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index a5cb817..2c7b0c5 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -287,9 +287,6 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque if authHeader := r.Header.Get("Authorization"); authHeader != "" { server.logger.Info("Attempting to get username or clientID from jwt...") - // TODO remove - server.logger.Info("header: %s", authHeader) - userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} @@ -300,12 +297,14 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque server.logger.Info(msg) errResponse = newErrorResponse(msg, 400, nil) } else { - - // TODO add logic in the print username = info.username clientID = info.clientID - server.logger.Info("found username in jwt: %s", username) - server.logger.Info("found clientID in jwt: %s", clientID) + if username != "" { + server.logger.Info("found username in jwt: %s", username) + } + if clientID != "" { + server.logger.Info("found clientID in jwt: %s", clientID) + } } } @@ -317,11 +316,6 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { - - // TODO remove - server.logger.Info("client in body: %s", requestBody.ClientID) - server.logger.Info("user in body: %s", requestBody.Username) - if (requestBody.Username == "") == (requestBody.ClientID == "") { msg := "must specify exactly one of `username` or `clientID`" server.logger.Info(msg) From ca07be3bc26eb9204a4be0c642eb76ab90937a05 Mon Sep 17 00:00:00 2001 From: Pauline Ribeyre <4224001+paulineribeyre@users.noreply.github.com> Date: Wed, 14 Feb 2024 14:26:54 -0600 Subject: [PATCH 14/19] Update x/net through x/crypto (#160) --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 622bccb..fd25677 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 // indirect + golang.org/x/crypto v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect ) diff --git a/go.sum b/go.sum index 1283f38..e374306 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ github.com/uc-cdis/go-authutils v0.1.2 h1:ts9Q1jHs0YIzeErZ6MAsbTrQwfNL4RjE9Wcx/+ github.com/uc-cdis/go-authutils v0.1.2/go.mod h1:NT4wNQiGGq9K/vhZoaJQmPwmTAQXz+haWSBB5QyL4Mc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= From f0128dc3c8cffe24d94fe6791b9a0307f42b466d Mon Sep 17 00:00:00 2001 From: Luca Graglia Date: Wed, 28 Feb 2024 15:41:13 -0600 Subject: [PATCH 15/19] remove 20231114 --- arborist/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/arborist/auth.go b/arborist/auth.go index 29768d0..dd08308 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -668,6 +668,7 @@ type AuthMapping map[string][]Action // TODO This is just a patch to filter out excessive resources. When transitioning to pelican import we should have a project_id = xyz parameter instead var authMappingProjectExclusion = ` ARRAY[ + 'programs.pcdc.projects.20231114.%', 'programs.pcdc.projects.20230912.%', 'programs.pcdc.projects.20230523.%', 'programs.pcdc.projects.20230228.%', From bbd8138bdebc08e9caba854f8758323cbaa4f1f8 Mon Sep 17 00:00:00 2001 From: Luca Graglia Date: Fri, 1 Mar 2024 16:11:41 -0600 Subject: [PATCH 16/19] requested changes --- arborist/server.go | 47 +++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 2c7b0c5..1df0bee 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -286,47 +286,52 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { server.logger.Info("Attempting to get username or clientID from jwt...") - userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} info, err := server.decodeToken(userJWT, scopes) if err != nil { - // Return 400 on failure to decode JWT + // Return 401 on failure to decode JWT msg := fmt.Sprintf("tried to get username/clientID from jwt, but jwt decode failed: %s", err.Error()) server.logger.Info(msg) - errResponse = newErrorResponse(msg, 400, nil) + errResponse = newErrorResponse(msg, 401, nil) + _ = errResponse.write(w, r) + return } else { - username = info.username - clientID = info.clientID - if username != "" { - server.logger.Info("found username in jwt: %s", username) + if (info.username == "") == (info.clientID == "") { + msg := "must provide a token or specify exactly one of `username` or `clientID` in the request body" + server.logger.Info(msg) + errResponse = newErrorResponse(msg, 400, nil) + _ = errResponse.write(w, r) + return } - if clientID != "" { - server.logger.Info("found clientID in jwt: %s", clientID) + + // When there is a username, there could be a client ID too (token belonging to a client acting + // on behalf of a user). But this endpoint only supports returning the user's mapping, not + // the combination of user+client access. So ignore the client ID. + if info.username != "" { + username = info.username + server.logger.Info("found username in jwt: %s", username) + } else { + clientID = info.clientID + server.logger.Info("found clientID in jwt: %s", clientID) } } - } - - // If they are not present in the token fallback on the request body - if (username == "") && (clientID == "") { + } else { + // If they are not present in the token fallback on the request body err := json.Unmarshal(body, &requestBody) if err != nil { msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { - if (requestBody.Username == "") == (requestBody.ClientID == "") { - msg := "must specify exactly one of `username` or `clientID`" + username = requestBody.Username + clientID = requestBody.ClientID + if (username == "") == (clientID == "") { + msg := "must provide a token or specify exactly one of `username` or `clientID` in the request body" server.logger.Info(msg) errResponse = newErrorResponse(msg, 400, nil) } - - if requestBody.ClientID != "" { - clientID = requestBody.ClientID - } else { - username = requestBody.Username - } } } From c6b181cba5887b18fdf3d6bbe502e34217947cd9 Mon Sep 17 00:00:00 2001 From: Pauline <4224001+paulineribeyre@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:17:39 -0600 Subject: [PATCH 17/19] reduce indent --- arborist/server.go | 49 ++++++++++++++++++++++------------------------ 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 1df0bee..5f85d78 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -278,44 +278,42 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque var errResponse *ErrorResponse = nil requestBody := struct { Username string `json:"username"` - ClientID string `json:"clientID"` + ClientID string `json:"clientID"` }{} // Try to get username or clientID from the JWT. username := "" clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { - server.logger.Info("Attempting to get username or clientID from jwt...") + server.logger.Info("Attempting to get username or client ID from jwt...") userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") scopes := []string{"openid"} info, err := server.decodeToken(userJWT, scopes) if err != nil { // Return 401 on failure to decode JWT - msg := fmt.Sprintf("tried to get username/clientID from jwt, but jwt decode failed: %s", err.Error()) + msg := fmt.Sprintf("tried to get username/client ID from jwt, but jwt decode failed: %s", err.Error()) server.logger.Info(msg) errResponse = newErrorResponse(msg, 401, nil) _ = errResponse.write(w, r) return - } else { - if (info.username == "") == (info.clientID == "") { - msg := "must provide a token or specify exactly one of `username` or `clientID` in the request body" - server.logger.Info(msg) - errResponse = newErrorResponse(msg, 400, nil) - _ = errResponse.write(w, r) - return - } + } - // When there is a username, there could be a client ID too (token belonging to a client acting - // on behalf of a user). But this endpoint only supports returning the user's mapping, not - // the combination of user+client access. So ignore the client ID. - if info.username != "" { - username = info.username - server.logger.Info("found username in jwt: %s", username) - } else { - clientID = info.clientID - server.logger.Info("found clientID in jwt: %s", clientID) - } + // When there is a username, there could be a client ID too (token belonging to a client acting + // on behalf of a user). But this endpoint only supports returning the user's mapping, not + // the combination of user+client access. So ignore the client ID. + if info.username != "" { + username = info.username + server.logger.Info("found username in jwt: %s", username) + } else if info.clientID == "" { + clientID = info.clientID + server.logger.Info("found client ID in jwt: %s", clientID) + } else { + msg := "invalid token (no username or client ID)" + server.logger.Info(msg) + errResponse = newErrorResponse(msg, 401, nil) + _ = errResponse.write(w, r) + return } } else { // If they are not present in the token fallback on the request body @@ -333,11 +331,10 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque errResponse = newErrorResponse(msg, 400, nil) } } - } - - if errResponse != nil { - _ = errResponse.write(w, r) - return + if errResponse != nil { + _ = errResponse.write(w, r) + return + } } var mappings AuthMapping From 1b68fa166855102926d7b99ee67b414a63afb951 Mon Sep 17 00:00:00 2001 From: Pauline <4224001+paulineribeyre@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:42:31 -0600 Subject: [PATCH 18/19] handleAuthMappingPOST => accept token without body --- arborist/server.go | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/arborist/server.go b/arborist/server.go index 5f85d78..2dc1d99 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -102,7 +102,7 @@ func (server *Server) MakeRouter(out io.Writer) http.Handler { router.HandleFunc("/health", server.handleHealth).Methods("GET") router.Handle("/auth/mapping", http.HandlerFunc(server.handleAuthMappingGET)).Methods("GET") - router.Handle("/auth/mapping", http.HandlerFunc(server.parseJSON(server.handleAuthMappingPOST))).Methods("POST") + router.Handle("/auth/mapping", http.HandlerFunc(server.handleAuthMappingPOST)).Methods("POST") router.Handle("/auth/proxy", http.HandlerFunc(server.handleAuthProxy)).Methods("GET") router.Handle("/auth/request", http.HandlerFunc(server.parseJSON(server.handleAuthRequest))).Methods("POST") router.Handle("/auth/resources", http.HandlerFunc(server.handleListAuthResourcesGET)).Methods("GET") @@ -176,25 +176,30 @@ func (server *Server) MakeRouter(out io.Writer) http.Handler { // handler signature. func (server *Server) parseJSON(baseHandler func(http.ResponseWriter, *http.Request, []byte)) func(http.ResponseWriter, *http.Request) { handler := func(w http.ResponseWriter, r *http.Request) { - if r.Body == nil { - response := newErrorResponse("expected JSON body in the request", 400, nil) - response.log.write(server.logger) - _ = response.write(w, r) - return - } - body, err := ioutil.ReadAll(r.Body) - if err != nil { - msg := fmt.Sprintf("could not parse valid JSON from request: %s", err.Error()) - response := newErrorResponse(msg, 400, nil) - response.log.write(server.logger) - _ = response.write(w, r) - return - } + body := server.parseJsonBody(w, r) baseHandler(w, r, body) } return handler } +func (server *Server) parseJsonBody(w http.ResponseWriter, r *http.Request) []byte { + if r.Body == nil { + response := newErrorResponse("expected JSON body in the request", 400, nil) + response.log.write(server.logger) + _ = response.write(w, r) + return nil + } + body, err := ioutil.ReadAll(r.Body) + if err != nil { + msg := fmt.Sprintf("could not parse valid JSON from request: %s", err.Error()) + response := newErrorResponse(msg, 400, nil) + response.log.write(server.logger) + _ = response.write(w, r) + return nil + } + return body +} + var regWhitespace *regexp.Regexp = regexp.MustCompile(`\s`) func loggableJSON(bytes []byte) []byte { @@ -274,7 +279,7 @@ func (server *Server) handleAuthMappingGET(w http.ResponseWriter, r *http.Reques } } -func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Request, body []byte) { +func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Request) { var errResponse *ErrorResponse = nil requestBody := struct { Username string `json:"username"` @@ -317,6 +322,7 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque } } else { // If they are not present in the token fallback on the request body + body := server.parseJsonBody(w, r) err := json.Unmarshal(body, &requestBody) if err != nil { msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) From 35f19398c52f8dd54787d647ac91d7231c5cc29f Mon Sep 17 00:00:00 2001 From: Pauline <4224001+paulineribeyre@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:38:03 -0600 Subject: [PATCH 19/19] add unit tests --- arborist/auth.go | 3 +- arborist/server.go | 13 ++- arborist/server_test.go | 241 +++++++++++++++++++++++++++------------- 3 files changed, 171 insertions(+), 86 deletions(-) diff --git a/arborist/auth.go b/arborist/auth.go index cc2e3ce..46196a7 100644 --- a/arborist/auth.go +++ b/arborist/auth.go @@ -758,7 +758,6 @@ func authMappingForGroups(db *sqlx.DB, groups ...string) (AuthMapping, *ErrorRes return mapping, nil } - // authMappingForClient gets the auth mapping for a client ID. // It does NOT includes the permissions of the `anonymous` and // `logged-in` groups. @@ -783,7 +782,7 @@ func authMappingForClient(db *sqlx.DB, clientID string) (AuthMapping, *ErrorResp err := db.Select( &mappingQuery, stmt, - clientID, // $1 + clientID, // $1 ) if err != nil { errResponse := newErrorResponse("mapping query failed", 500, &err) diff --git a/arborist/server.go b/arborist/server.go index 2dc1d99..2591fe1 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -71,7 +71,7 @@ func (server *Server) Init() (*Server, error) { // For some reason this is not allowed: // -// `{resourcePath:/.+}` +// `{resourcePath:/.+}` // // so we put the slash at the front here and fix it in parseResourcePath. const resourcePath string = `/{resourcePath:.+}` @@ -286,10 +286,10 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque ClientID string `json:"clientID"` }{} - // Try to get username or clientID from the JWT. username := "" clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { + // Try to get username or clientID from the JWT. server.logger.Info("Attempting to get username or client ID from jwt...") userJWT := strings.TrimPrefix(authHeader, "Bearer ") userJWT = strings.TrimPrefix(userJWT, "bearer ") @@ -310,23 +310,24 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque if info.username != "" { username = info.username server.logger.Info("found username in jwt: %s", username) - } else if info.clientID == "" { + } else if info.clientID != "" { clientID = info.clientID server.logger.Info("found client ID in jwt: %s", clientID) } else { msg := "invalid token (no username or client ID)" - server.logger.Info(msg) + server.logger.Error(msg) errResponse = newErrorResponse(msg, 401, nil) _ = errResponse.write(w, r) return } } else { - // If they are not present in the token fallback on the request body + // If they are not present in the token, fallback on the request body + server.logger.Info("No jwt provided, checking request body") body := server.parseJsonBody(w, r) err := json.Unmarshal(body, &requestBody) if err != nil { msg := fmt.Sprintf("could not parse JSON: %s", err.Error()) - server.logger.Info("tried to handle auth mapping request but input was invalid: %s", msg) + server.logger.Error("tried to handle auth mapping request but input was invalid: %s", msg) errResponse = newErrorResponse(msg, 400, nil) } else { username = requestBody.Username diff --git a/arborist/server_test.go b/arborist/server_test.go index 878c34a..e25ae49 100644 --- a/arborist/server_test.go +++ b/arborist/server_test.go @@ -51,10 +51,9 @@ func (jwtApp *mockJWTApp) Decode(token string) (*map[string]interface{}, error) // // Example: // -// token := TestJWT{username: username} -// body := []byte(fmt.Sprintf(`{"user": {"token": "%s"}}`, token.Encode())) -// req := newRequest("POST", "/auth/resources", bytes.NewBuffer(body)) -// +// token := TestJWT{username: username} +// body := []byte(fmt.Sprintf(`{"user": {"token": "%s"}}`, token.Encode())) +// req := newRequest("POST", "/auth/resources", bytes.NewBuffer(body)) type TestJWT struct { username string clientID string @@ -2910,10 +2909,10 @@ func TestServer(t *testing.T) { createUserBytes(t, userBody) grantUserPolicy(t, username, policyName, "null") - // testAuthMappingResponse checks whether the AuthMapping in the HTTP + // testUserAuthMappingResponse checks whether the AuthMapping in the HTTP // response 'w' contains the correct resources and actions that belong to the user. // This includes the resources and actions that belong to the anonymous and loggedIn groups. - testAuthMappingResponse := func(t *testing.T, w *httptest.ResponseRecorder) { + testUserAuthMappingResponse := func(t *testing.T, w *httptest.ResponseRecorder) { msg := fmt.Sprintf("got response body: %s", w.Body.String()) assert.Equal(t, 200, w.Code, msg) result := make(map[string][]arborist.Action) @@ -2996,7 +2995,7 @@ func TestServer(t *testing.T) { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) handler.ServeHTTP(w, req) // expect to receive user's auth mappings, as well as auth mappings of anonymous and logged-in policies - testAuthMappingResponse(t, w) + testUserAuthMappingResponse(t, w) }) t.Run("GET_userDoesNotExist", func(t *testing.T) { @@ -3074,91 +3073,177 @@ func TestServer(t *testing.T) { grantUserPolicy(t, username, policyName, "null") }) - t.Run("POST_usernameProvided", func(t *testing.T) { - w := httptest.NewRecorder() - body := []byte(fmt.Sprintf(`{"username": "%s"}`, username)) - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) - handler.ServeHTTP(w, req) - // expect to also receive auth mappings of anonymous and logged-in policies - testAuthMappingResponse(t, w) - }) + t.Run("POST_body", func(t *testing.T) { + t.Run("usernameProvided", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(fmt.Sprintf(`{"username": "%s"}`, username)) + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + // expect to also receive auth mappings of anonymous and logged-in policies + testUserAuthMappingResponse(t, w) + }) - t.Run("POST_userDoesNotExist", func(t *testing.T) { - w := httptest.NewRecorder() - badUsername := "hulkhogan12" - body := []byte(fmt.Sprintf(`{"username": "%s"}`, badUsername)) - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) - handler.ServeHTTP(w, req) + t.Run("userDoesNotExist", func(t *testing.T) { + w := httptest.NewRecorder() + badUsername := "hulkhogan12" + body := []byte(fmt.Sprintf(`{"username": "%s"}`, badUsername)) + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") - // expect a 200 OK response - assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") + // expect result to only contain anonymous and loggedIn auth mappings. + result := make(arborist.AuthMapping) + err = json.Unmarshal(w.Body.Bytes(), &result) + if err != nil { + httpError(t, w, "couldn't read response from auth mapping") + } + expectedMappings := make(arborist.AuthMapping) + for k, v := range anonymousAuthMapping { + expectedMappings[k] = v + } + for k, v := range loggedInAuthMapping { + expectedMappings[k] = v + } + msg := fmt.Sprintf("Expected response to be these auth mappings from anonymous and logged-in groups: %v", expectedMappings) + for resource, actions := range result { + assert.Contains(t, expectedMappings, resource, msg) + assert.ElementsMatch(t, expectedMappings[resource], actions, msg) + } + }) - // expect result to only contain anonymous and loggedIn auth mappings. - result := make(arborist.AuthMapping) - err = json.Unmarshal(w.Body.Bytes(), &result) - if err != nil { - httpError(t, w, "couldn't read response from auth mapping") - } - expectedMappings := make(arborist.AuthMapping) - for k, v := range anonymousAuthMapping { - expectedMappings[k] = v - } - for k, v := range loggedInAuthMapping { - expectedMappings[k] = v - } - msg := fmt.Sprintf("Expected response to be these auth mappings from anonymous and logged-in groups: %v", expectedMappings) - for resource, actions := range result { - assert.Contains(t, expectedMappings, resource, msg) - assert.ElementsMatch(t, expectedMappings[resource], actions, msg) - } - }) + createClientBytes(t, clientBody) + grantClientPolicy(t, clientID, policyName) - createClientBytes(t, clientBody) - grantClientPolicy(t, clientID, policyName) + t.Run("noUsernameOrClientIdProvided", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte("") + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") + }) - t.Run("POST_noUsernameOrClientIdProvided", func(t *testing.T) { - w := httptest.NewRecorder() - body := []byte("") - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) - handler.ServeHTTP(w, req) + t.Run("bothUsernameAndClientIdProvided", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(fmt.Sprintf(`{"username": "%s", "clientID": "%s"}`, username, clientID)) + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") + }) - // expect a 400 response - assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") - }) + t.Run("clientIdProvided", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(fmt.Sprintf(`{"clientID": "%s"}`, clientID)) + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + testClientAuthMappingResponse(t, w) + }) - t.Run("POST_bothUsernameAndClientIdProvided", func(t *testing.T) { - w := httptest.NewRecorder() - body := []byte(fmt.Sprintf(`{"username": "%s", "clientID": "%s"}`, username, clientID)) - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) - handler.ServeHTTP(w, req) + t.Run("clientIdDoesNotExist", func(t *testing.T) { + w := httptest.NewRecorder() + body := []byte(`{"clientID": "badClientId"}`) + req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") - // expect a 400 response - assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") + result := make(map[string][]arborist.Action) + err = json.Unmarshal(w.Body.Bytes(), &result) + if err != nil { + httpError(t, w, "couldn't read response from auth mapping") + } + assert.Equal(t, map[string][]arborist.Action{}, result, "expected an empty result") + }) }) - t.Run("POST_clientIdProvided", func(t *testing.T) { - w := httptest.NewRecorder() - body := []byte(fmt.Sprintf(`{"clientID": "%s"}`, clientID)) - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) - handler.ServeHTTP(w, req) - testClientAuthMappingResponse(t, w) + t.Run("POST_token", func(t *testing.T) { + t.Run("usernameProvided", func(t *testing.T) { + w := httptest.NewRecorder() + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{username: username} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + // expect to also receive auth mappings of anonymous and logged-in policies + testUserAuthMappingResponse(t, w) + }) + + t.Run("userDoesNotExist", func(t *testing.T) { + w := httptest.NewRecorder() + badUsername := "hulkhogan12" + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{username: badUsername} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") + + // expect result to only contain anonymous and loggedIn auth mappings. + result := make(arborist.AuthMapping) + err = json.Unmarshal(w.Body.Bytes(), &result) + if err != nil { + httpError(t, w, "couldn't read response from auth mapping") + } + expectedMappings := make(arborist.AuthMapping) + for k, v := range anonymousAuthMapping { + expectedMappings[k] = v + } + for k, v := range loggedInAuthMapping { + expectedMappings[k] = v + } + msg := fmt.Sprintf("Expected response to be these auth mappings from anonymous and logged-in groups: %v", expectedMappings) + for resource, actions := range result { + assert.Contains(t, expectedMappings, resource, msg) + assert.ElementsMatch(t, expectedMappings[resource], actions, msg) + } + }) + + t.Run("noUsernameOrClientIdProvided", func(t *testing.T) { + w := httptest.NewRecorder() + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusUnauthorized, "expected a 401 response") + }) + + t.Run("bothUsernameAndClientIdProvided", func(t *testing.T) { + // when both are present, the returned access should be the user's + w := httptest.NewRecorder() + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{username: username, clientID: clientID} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + testUserAuthMappingResponse(t, w) + }) + + t.Run("clientIdProvided", func(t *testing.T) { + w := httptest.NewRecorder() + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{clientID: clientID} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + testClientAuthMappingResponse(t, w) + }) + + t.Run("clientIdDoesNotExist", func(t *testing.T) { + w := httptest.NewRecorder() + req := newRequest("POST", "/auth/mapping", nil) + token := TestJWT{clientID: "badClientId"} + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.Encode())) + handler.ServeHTTP(w, req) + assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") + + result := make(map[string][]arborist.Action) + err = json.Unmarshal(w.Body.Bytes(), &result) + if err != nil { + httpError(t, w, "couldn't read response from auth mapping") + } + assert.Equal(t, map[string][]arborist.Action{}, result, "expected an empty result") + }) }) - t.Run("POST_clientIdDoesNotExist", func(t *testing.T) { + t.Run("POST_noBodyOrTokenProvided", func(t *testing.T) { w := httptest.NewRecorder() - body := []byte(`{"clientID": "badClientId"}`) - req := newRequest("POST", "/auth/mapping", bytes.NewBuffer(body)) + req := newRequest("POST", "/auth/mapping", nil) handler.ServeHTTP(w, req) - - // expect a 200 OK response - assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") - - result := make(map[string][]arborist.Action) - err = json.Unmarshal(w.Body.Bytes(), &result) - if err != nil { - httpError(t, w, "couldn't read response from auth mapping") - } - assert.Equal(t, map[string][]arborist.Action{}, result, "expected an empty result") + assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") }) })