From 40e2d378b29607496c82535895179335a54cc76f Mon Sep 17 00:00:00 2001 From: Boris Schrijver Date: Thu, 2 Nov 2023 13:37:51 +0100 Subject: [PATCH] add include_credential query param to /admin/identities list call --- identity/handler.go | 32 +++++++++++++++++++++++++++-- identity/handler_test.go | 23 ++++++++++++++++++++- identity/pool.go | 1 + internal/client-go/api_identity.go | 16 +++++++++++++++ internal/httpclient/api_identity.go | 16 +++++++++++++++ spec/api.json | 11 ++++++++++ spec/swagger.json | 9 ++++++++ 7 files changed, 105 insertions(+), 3 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index c821a13de844..0343567a0ac7 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -162,6 +162,15 @@ type listIdentitiesParameters struct { // in: query CredentialsIdentifierSimilar string `json:"preview_credentials_identifier_similar"` + // Include Credentials in Response + // + // Include any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return + // the initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available. + // + // required: false + // in: query + DeclassifyCredentials []string `json:"include_credential"` + crdbx.ConsistencyRequestParameters } @@ -183,6 +192,18 @@ type listIdentitiesParameters struct { // 200: listIdentities // default: errorGeneric func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { + includeCredentials := r.URL.Query()["include_credential"] + var declassify []CredentialsType + for _, v := range includeCredentials { + tc, ok := ParseCredentialsType(v) + if ok { + declassify = append(declassify, tc) + } else { + h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid value `%s` for parameter `include_credential`.", declassify))) + return + } + } + var ( err error params = ListIdentityParameters{ @@ -191,13 +212,14 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), + DeclassifyCredentials: declassify, } ) if params.CredentialsIdentifier != "" && params.CredentialsIdentifierSimilar != "" { h.r.Writer().WriteError(w, r, herodot.ErrBadRequest.WithReason("Cannot pass both credentials_identifier and preview_credentials_identifier_similar.")) return } - if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" { + if params.CredentialsIdentifier != "" || params.CredentialsIdentifierSimilar != "" || len(params.DeclassifyCredentials) > 0 { params.Expand = ExpandEverything } params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) @@ -231,7 +253,13 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para // Identities using the marshaler for including metadata_admin isam := make([]WithCredentialsMetadataAndAdminMetadataInJSON, len(is)) for i, identity := range is { - isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(identity) + emit, err := identity.WithDeclassifiedCredentials(r.Context(), h.r, params.DeclassifyCredentials) + if err != nil { + h.r.Writer().WriteError(w, r, err) + return + } + + isam[i] = WithCredentialsMetadataAndAdminMetadataInJSON(*emit) } h.r.Writer().Write(w, r, isam) diff --git a/identity/handler_test.go b/identity/handler_test.go index ab20e8780ca6..c28d67638266 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -1302,7 +1302,7 @@ func TestHandler(t *testing.T) { }) t.Run("case=should list all identities", func(t *testing.T) { - for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { res := get(t, ts, "/identities", http.StatusOK) assert.False(t, res.Get("0.credentials").Exists(), "credentials config should be omitted: %s", res.Raw) @@ -1313,6 +1313,27 @@ func TestHandler(t *testing.T) { } }) + t.Run("case=should list all identities with credentials", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities?include_credential=totp", http.StatusOK) + assert.True(t, res.Get("0.credentials").Exists(), "credentials config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_public").Exists(), "metadata_public config should be included: %s", res.Raw) + assert.True(t, res.Get("0.metadata_admin").Exists(), "metadata_admin config should be included: %s", res.Raw) + assert.EqualValues(t, "baz", res.Get(`#(traits.bar=="baz").traits.bar`).String(), "%s", res.Raw) + }) + } + }) + + t.Run("case=should not be able to list all identities with credentials due to wrong credentials type", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + res := get(t, ts, "/identities?include_credential=XYZ", http.StatusBadRequest) + assert.Contains(t, res.Get("error.message").String(), "The request was malformed or contained invalid parameters", "%s", res.Raw) + }) + } + }) + t.Run("case=should list all identities with eventual consistency", func(t *testing.T) { for name, ts := range map[string]*httptest.Server{"public": publicTS, "admin": adminTS} { t.Run("endpoint="+name, func(t *testing.T) { diff --git a/identity/pool.go b/identity/pool.go index 5316f8a53ff9..89eaf9927637 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -21,6 +21,7 @@ type ( IdsFilter []string CredentialsIdentifier string CredentialsIdentifierSimilar string + DeclassifyCredentials []CredentialsType KeySetPagination []keysetpagination.Option // DEPRECATED PagePagination *x.Page diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index bc1b675876fb..c3c361d16ad4 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -2063,6 +2063,7 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2097,6 +2098,10 @@ func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityApiApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityApiApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2172,6 +2177,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index bc1b675876fb..c3c361d16ad4 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -2063,6 +2063,7 @@ type IdentityApiApiListIdentitiesRequest struct { ids *[]string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2097,6 +2098,10 @@ func (r IdentityApiApiListIdentitiesRequest) PreviewCredentialsIdentifierSimilar r.previewCredentialsIdentifierSimilar = &previewCredentialsIdentifierSimilar return r } +func (r IdentityApiApiListIdentitiesRequest) IncludeCredential(includeCredential []string) IdentityApiApiListIdentitiesRequest { + r.includeCredential = &includeCredential + return r +} func (r IdentityApiApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2172,6 +2177,17 @@ func (a *IdentityApiService) ListIdentitiesExecute(r IdentityApiApiListIdentitie if r.previewCredentialsIdentifierSimilar != nil { localVarQueryParams.Add("preview_credentials_identifier_similar", parameterToString(*r.previewCredentialsIdentifierSimilar, "")) } + if r.includeCredential != nil { + t := *r.includeCredential + if reflect.TypeOf(t).Kind() == reflect.Slice { + s := reflect.ValueOf(t) + for i := 0; i < s.Len(); i++ { + localVarQueryParams.Add("include_credential", parameterToString(s.Index(i), "multi")) + } + } else { + localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) + } + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/spec/api.json b/spec/api.json index c35d62d194e7..428393cf6eb8 100644 --- a/spec/api.json +++ b/spec/api.json @@ -3628,6 +3628,17 @@ "schema": { "type": "string" } + }, + { + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "in": "query", + "name": "include_credential", + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 9f074141f069..a3e4d454bad1 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -252,6 +252,15 @@ "description": "This is an EXPERIMENTAL parameter that WILL CHANGE. Do NOT rely on consistent, deterministic behavior.\nTHIS PARAMETER WILL BE REMOVED IN AN UPCOMING RELEASE WITHOUT ANY MIGRATION PATH.\n\nCredentialsIdentifierSimilar is the (partial) identifier (username, email) of the credentials to look up using similarity search.\nOnly one of CredentialsIdentifier and CredentialsIdentifierSimilar can be used.", "name": "preview_credentials_identifier_similar", "in": "query" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include Credentials in Response\n\nInclude any credential, for example `password` or `oidc`, in the response. When set to `oidc`, This will return\nthe initial OAuth 2.0 Access Token, OAuth 2.0 Refresh Token and the OpenID Connect ID Token if available.", + "name": "include_credential", + "in": "query" } ], "responses": {