diff --git a/.secrets.baseline b/.secrets.baseline index 4539301..51c7443 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "go.sum|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-03-01T20:51:33Z", + "generated_at": "2024-03-11T21:41:11Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -70,7 +70,7 @@ { "hashed_secret": "f9fdc64928c96c7ad56bf7da557f70345d83a6ed", "is_verified": false, - "line_number": 1684, + "line_number": 1688, "type": "Base64 High Entropy String" } ], diff --git a/arborist/server.go b/arborist/server.go index 4589e61..4b29c17 100644 --- a/arborist/server.go +++ b/arborist/server.go @@ -176,28 +176,34 @@ 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) { - body := server.parseJsonBody(w, r) + body, err := server.parseJsonBody(w, r) + if err != nil { + err.log.write(server.logger) + _ = err.write(w, r) + return + } + if body == nil { + err := newErrorResponse("expected JSON body in the request", 400, nil) + err.log.write(server.logger) + _ = err.write(w, r) + return + } baseHandler(w, r, body) } return handler } -func (server *Server) parseJsonBody(w http.ResponseWriter, r *http.Request) []byte { +func (server *Server) parseJsonBody(w http.ResponseWriter, r *http.Request) ([]byte, *ErrorResponse) { 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 + return nil, 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 + err := newErrorResponse(msg, 400, nil) + return nil, err } - return body + return body, nil } var regWhitespace *regexp.Regexp = regexp.MustCompile(`\s`) @@ -286,6 +292,13 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque ClientID string `json:"clientID"` }{} + body, err := server.parseJsonBody(w, r) + if err != nil { + err.log.write(server.logger) + _ = err.write(w, r) + return + } + username := "" clientID := "" if authHeader := r.Header.Get("Authorization"); authHeader != "" { @@ -320,10 +333,9 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque _ = errResponse.write(w, r) return } - } else { + } else if len(body) > 0 { // 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()) @@ -342,6 +354,17 @@ func (server *Server) handleAuthMappingPOST(w http.ResponseWriter, r *http.Reque _ = errResponse.write(w, r) return } + } else { + // If no username or client ID provided in query string or JWT, return the + // auth mapping for the `anonymous` group. (See `docs/username.md` for more detail) + mappings, errResponse := authMappingForGroups(server.db, AnonymousGroup) + if errResponse != nil { + errResponse.log.write(server.logger) + _ = errResponse.write(w, r) + return + } + _ = jsonResponseFrom(mappings, http.StatusOK).write(w, r) + return } var mappings AuthMapping diff --git a/arborist/server_test.go b/arborist/server_test.go index e25ae49..9cd38b3 100644 --- a/arborist/server_test.go +++ b/arborist/server_test.go @@ -2927,18 +2927,36 @@ func TestServer(t *testing.T) { assert.Contains(t, result[resourcePath], action, msg) // Expect response to also contain anonymous and loggedIn groups. - msg = fmt.Sprintf("Expected to see these auth mappings from anonymous group in response: %v", anonymousAuthMapping) + msg = fmt.Sprintf("Expected to see these auth mappings from anonymous group in response: %v, but got: %v", anonymousAuthMapping, result) for resource, actions := range anonymousAuthMapping { assert.Contains(t, result, resource, msg) assert.ElementsMatch(t, result[resource], actions, msg) } - msg = fmt.Sprintf("Expected to see these auth mappings from loggedIn group in response: %v", loggedInAuthMapping) + msg = fmt.Sprintf("Expected to see these auth mappings from loggedIn group in response: %v, but got: %v", loggedInAuthMapping, result) for resource, actions := range loggedInAuthMapping { assert.Contains(t, result, resource, msg) assert.ElementsMatch(t, result[resource], actions, msg) } } + // testAnonymousAuthMappingResponse checks whether the AuthMapping in the HTTP response 'w' + // ONLY contains the correct resources and actions that belong to the anonymous group. + testAnonymousAuthMappingResponse := func(t *testing.T, w *httptest.ResponseRecorder) { + assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") + + // expect result to contain only authMappings of anonymous policies + result := make(arborist.AuthMapping) + err = json.Unmarshal(w.Body.Bytes(), &result) + if err != nil { + httpError(t, w, "couldn't read response from auth mapping") + } + msg := fmt.Sprintf("Expected these auth mappings from anonymous group: %v \t Got: %v", anonymousAuthMapping, result) + for resource, actions := range result { + assert.Contains(t, anonymousAuthMapping, resource, msg) + assert.ElementsMatch(t, anonymousAuthMapping[resource], actions, msg) + } + } + // testClientAuthMappingResponse checks whether the AuthMapping in the HTTP // response 'w' contains the correct resources and actions that belong to the client. // This does NOT include the resources and actions that belong to the anonymous and loggedIn groups. @@ -2980,7 +2998,7 @@ func TestServer(t *testing.T) { for k, v := range loggedInAuthMapping { expectedMappings[k] = v } - msg := fmt.Sprintf("Expected to see these auth mappings from anonymous and logged-in groups in response: %v", expectedMappings) + msg := fmt.Sprintf("Expected to see these auth mappings from anonymous and logged-in groups in response: %v, but got: %v", expectedMappings, result) for resource, actions := range result { assert.Contains(t, expectedMappings, resource, msg) assert.ElementsMatch(t, expectedMappings[resource], actions, msg) @@ -3035,24 +3053,7 @@ func TestServer(t *testing.T) { url := "/auth/mapping" req := newRequest("GET", url, nil) handler.ServeHTTP(w, req) - if w.Code != http.StatusOK { - httpError(t, w, "expected to get policies for Anonymous group; got bad response instead") - } - - // expect a 200 OK response - assert.Equal(t, w.Code, http.StatusOK, "expected a 200 OK") - - // expect result to contain only authMappings of anonymous policies - result := make(arborist.AuthMapping) - err = json.Unmarshal(w.Body.Bytes(), &result) - if err != nil { - httpError(t, w, "couldn't read response from auth mapping") - } - msg := fmt.Sprintf("Expected these auth mappings from anonymous group: %v \t Got: %v", anonymousAuthMapping, result) - for resource, actions := range result { - assert.Contains(t, anonymousAuthMapping, resource, msg) - assert.ElementsMatch(t, anonymousAuthMapping[resource], actions, msg) - } + testAnonymousAuthMappingResponse(t, w) }) t.Run("GET_expiredPolicy", func(t *testing.T) { @@ -3119,7 +3120,7 @@ func TestServer(t *testing.T) { 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") + testAnonymousAuthMappingResponse(t, w) }) t.Run("bothUsernameAndClientIdProvided", func(t *testing.T) { @@ -3243,7 +3244,7 @@ func TestServer(t *testing.T) { w := httptest.NewRecorder() req := newRequest("POST", "/auth/mapping", nil) handler.ServeHTTP(w, req) - assert.Equal(t, w.Code, http.StatusBadRequest, "expected a 400 response") + testAnonymousAuthMappingResponse(t, w) }) }) diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 994ef41..64a0d3d 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -30,7 +30,7 @@ paths: Note: Tokens which are linked to both a user and a client (token belonging to a client acting on behalf of a user) are supported, but the client's access is NOT taken into account. Only the user's access is returned. - Note that this endpoint does NOT support tokens generated through the OIDC "client_credentials" grant, which are linked to a client but not to a user. Calling this endpoint with such a token will not return the client's access. + Note: This endpoint does NOT support tokens generated through the OIDC "client_credentials" grant, which are linked to a client but not to a user. Calling this endpoint with such a token will not return the client's access. If the specified user is not recognized by arborist, this endpoint returns @@ -98,6 +98,10 @@ paths: an empty response. + If no username or client ID is provided (no token is provided in the Authorization header, and there is no request body), + this endpoint returns ONLY the mappings available to members of the `anonymous` group. + + Note: accepting a username or client ID in the body means that anyone can check anyone's authorization if they have their username. For this reason, this API is not meant to be exposed publicly. requestBody: