From 95674f0c287e4ad721a815967ad11bc1bc05d66f 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 | 14 ++++++++++++- 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, 96 insertions(+), 3 deletions(-) diff --git a/identity/handler.go b/identity/handler.go index 2749396632b3..1c1c80b1c02d 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -155,6 +155,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 } @@ -176,6 +185,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{ @@ -183,13 +204,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) @@ -223,7 +245,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 43d432b55eb4..05e98f38280c 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -1256,7 +1256,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) @@ -1267,6 +1267,18 @@ 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 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 46083d3d525b..c33334e256d0 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -20,6 +20,7 @@ type ( Expand Expandables 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 ae406e98fae0..142f0d3703b6 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -2054,6 +2054,7 @@ type IdentityApiApiListIdentitiesRequest struct { consistency *string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2084,6 +2085,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) @@ -2148,6 +2153,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 ae406e98fae0..142f0d3703b6 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -2054,6 +2054,7 @@ type IdentityApiApiListIdentitiesRequest struct { consistency *string credentialsIdentifier *string previewCredentialsIdentifierSimilar *string + includeCredential *[]string } func (r IdentityApiApiListIdentitiesRequest) PerPage(perPage int64) IdentityApiApiListIdentitiesRequest { @@ -2084,6 +2085,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) @@ -2148,6 +2153,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 e7f7e6360031..6aaf87be285d 100644 --- a/spec/api.json +++ b/spec/api.json @@ -3504,6 +3504,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 a61ca10e51b4..bdaedee20e8a 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -243,6 +243,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": {