diff --git a/go.mod b/go.mod index acdf909951c1..fb7135cdfd55 100644 --- a/go.mod +++ b/go.mod @@ -97,12 +97,12 @@ require ( go.opentelemetry.io/otel v1.32.0 go.opentelemetry.io/otel/sdk v1.32.0 go.opentelemetry.io/otel/trace v1.32.0 - golang.org/x/crypto v0.29.0 + golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/net v0.31.0 golang.org/x/oauth2 v0.24.0 - golang.org/x/sync v0.9.0 - golang.org/x/text v0.20.0 + golang.org/x/sync v0.10.0 + golang.org/x/text v0.21.0 google.golang.org/grpc v1.67.1 ) @@ -118,7 +118,7 @@ require ( github.com/dgraph-io/ristretto/v2 v2.0.0 // indirect github.com/jackc/pgx/v5 v5.6.0 // indirect github.com/rjeczalik/notify v0.9.3 // indirect - golang.org/x/term v0.26.0 // indirect + golang.org/x/term v0.27.0 // indirect gopkg.in/alecthomas/kingpin.v2 v2.2.6 // indirect mvdan.cc/sh/v3 v3.6.0 // indirect ) @@ -313,7 +313,7 @@ require ( go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/mod v0.19.0 // indirect - golang.org/x/sys v0.27.0 // indirect + golang.org/x/sys v0.28.0 // indirect golang.org/x/tools v0.23.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20241104194629-dd2ea8efbc28 // indirect diff --git a/go.sum b/go.sum index d48ddcf554f0..c6d4e25622eb 100644 --- a/go.sum +++ b/go.sum @@ -866,8 +866,8 @@ golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4 golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -981,8 +981,8 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1047,8 +1047,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -1060,8 +1060,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -1074,8 +1074,8 @@ golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/identity/handler.go b/identity/handler.go index 7560724899db..98b73665d553 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -119,10 +119,7 @@ func (h *Handler) RegisterAdminRoutes(admin *x.RouterAdmin) { // Paginated Identity List Response // // swagger:response listIdentities -// -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type listIdentitiesResponse struct { +type _ struct { migrationpagination.ResponseHeaderAnnotation // List of identities @@ -133,11 +130,10 @@ type listIdentitiesResponse struct { // Paginated List Identity Parameters // -// swagger:parameters listIdentities +// Note: Filters cannot be combined. // -//nolint:deadcode,unused -//lint:ignore U1000 Used to generate Swagger and OpenAPI definitions -type listIdentitiesParameters struct { +// swagger:parameters listIdentities +type _ struct { migrationpagination.RequestParameters // List of ids used to filter identities. @@ -183,11 +179,73 @@ type listIdentitiesParameters struct { crdbx.ConsistencyRequestParameters } +func parseListIdentitiesParameters(r *http.Request) (params ListIdentityParameters, err error) { + query := r.URL.Query() + var requestedFilters int + + params.Expand = ExpandDefault + + if ids := query["ids"]; len(ids) > 0 { + requestedFilters++ + for _, v := range ids { + id, err := uuid.FromString(v) + if err != nil { + return params, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid UUID value `%s` for parameter `ids`.", v)) + } + params.IdsFilter = append(params.IdsFilter, id) + } + } + if len(params.IdsFilter) > 500 { + return params, errors.WithStack(herodot.ErrBadRequest.WithReason("The number of ids to filter must not exceed 500.")) + } + + if orgID := query.Get("organization_id"); orgID != "" { + requestedFilters++ + params.OrganizationID, err = uuid.FromString(orgID) + if err != nil { + return params, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid UUID value `%s` for parameter `organization_id`.", orgID)) + } + } + + if identifier := query.Get("credentials_identifier"); identifier != "" { + requestedFilters++ + params.Expand = ExpandEverything + params.CredentialsIdentifier = identifier + } + + if identifier := query.Get("credentials_identifier_similar"); identifier != "" { + requestedFilters++ + params.Expand = ExpandEverything + params.CredentialsIdentifierSimilar = identifier + } + + for _, v := range query["include_credential"] { + params.Expand = ExpandEverything + tc, ok := ParseCredentialsType(v) + if !ok { + return params, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid value `%s` for parameter `include_credential`.", v)) + } + params.DeclassifyCredentials = append(params.DeclassifyCredentials, tc) + } + + if requestedFilters > 1 { + return params, errors.WithStack(herodot.ErrBadRequest.WithReason("You cannot combine multiple filters in this API")) + } + + params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) + if err != nil { + return params, err + } + params.ConsistencyLevel = crdbx.ConsistencyLevelFromRequest(r) + + return params, nil +} + // swagger:route GET /admin/identities identity listIdentities // // # List Identities // -// Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. +// Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. // // Produces: // - application/json @@ -201,54 +259,7 @@ 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 err error - 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 orgId uuid.UUID - if orgIdStr := r.URL.Query().Get("organization_id"); orgIdStr != "" { - orgId, err = uuid.FromString(r.URL.Query().Get("organization_id")) - if err != nil { - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid UUID value `%s` for parameter `organization_id`.", r.URL.Query().Get("organization_id")))) - return - } - } - var idsFilter []uuid.UUID - for _, v := range r.URL.Query()["ids"] { - id, err := uuid.FromString(v) - if err != nil { - h.r.Writer().WriteError(w, r, errors.WithStack(herodot.ErrBadRequest.WithReasonf("Invalid UUID value `%s` for parameter `ids`.", v))) - return - } - idsFilter = append(idsFilter, id) - } - - params := ListIdentityParameters{ - Expand: ExpandDefault, - IdsFilter: idsFilter, - CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), - CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), - OrganizationID: orgId, - 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 != "" || len(params.DeclassifyCredentials) > 0 { - params.Expand = ExpandEverything - } - params.KeySetPagination, params.PagePagination, err = x.ParseKeysetOrPagePagination(r) + params, err := parseListIdentitiesParameters(r) if err != nil { h.r.Writer().WriteError(w, r, err) return @@ -271,7 +282,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para } u := *r.URL pagepagination.PaginationHeader(w, &u, total, params.PagePagination.Page, params.PagePagination.ItemsPerPage) - } else { + } else if nextPage != nil { u := *r.URL keysetpagination.Header(w, &u, nextPage) } diff --git a/identity/handler_test.go b/identity/handler_test.go index e3362a6ecf94..d55448cdce3b 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -369,21 +369,50 @@ func TestHandler(t *testing.T) { id := x.ParseUUID(res.Get("id").String()) ids = append(ids, id) } - require.Equal(t, len(ids), identitiesAmount) + require.Len(t, ids, identitiesAmount) }) t.Run("case=list few identities", func(t *testing.T) { - url := "/identities?ids=" + ids[0].String() + url := "/identities?ids=" + ids[0].String() + "&ids=" + ids[0].String() // duplicate ID is deduplicated in result for i := 1; i < listAmount; i++ { url += "&ids=" + ids[i].String() } res := get(t, adminTS, url, http.StatusOK) identities := res.Array() - require.Equal(t, len(identities), listAmount) + require.Len(t, identities, listAmount) }) }) + t.Run("case=list identities by ID is capped at 500", func(t *testing.T) { + url := "/identities?ids=" + x.NewUUID().String() + for i := 0; i < 501; i++ { + url += "&ids=" + x.NewUUID().String() + } + res := get(t, adminTS, url, http.StatusBadRequest) + assert.Contains(t, res.Get("error.reason").String(), "must not exceed 500") + }) + + t.Run("case=list identities cannot combine filters", func(t *testing.T) { + filters := []string{ + "ids=" + x.NewUUID().String(), + "credentials_identifier=foo@bar.com", + "credentials_identifier_similar=bar.com", + "organization_id=" + x.NewUUID().String(), + } + for i := range filters { + for j := range filters { + if i == j { + continue // OK to use the same filter multiple times. Behavior varies by filter, though. + } + + url := "/identities?" + filters[i] + "&" + filters[j] + res := get(t, adminTS, url, http.StatusBadRequest) + assert.Contains(t, res.Get("error.reason").String(), "cannot combine multiple filters") + } + } + }) + t.Run("case=malformed ids should return an error", func(t *testing.T) { res := get(t, adminTS, "/identities?ids=not-a-uuid", http.StatusBadRequest) assert.Contains(t, res.Get("error.reason").String(), "Invalid UUID value `not-a-uuid` for parameter `ids`.", "%s", res.Raw) diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 9e4aec1b6c58..b7dd012adf83 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -227,7 +227,7 @@ type IdentityAPI interface { /* * ListIdentities List Identities - * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. + * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @return IdentityAPIApiListIdentitiesRequest */ @@ -2137,7 +2137,7 @@ func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Respon /* * ListIdentities List Identities - * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. + * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @return IdentityAPIApiListIdentitiesRequest */ diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 9e4aec1b6c58..b7dd012adf83 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -227,7 +227,7 @@ type IdentityAPI interface { /* * ListIdentities List Identities - * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. + * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @return IdentityAPIApiListIdentitiesRequest */ @@ -2137,7 +2137,7 @@ func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Respon /* * ListIdentities List Identities - * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. + * Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined. * @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). * @return IdentityAPIApiListIdentitiesRequest */ diff --git a/spec/api.json b/spec/api.json index 907845a46f7c..87650d834bb7 100644 --- a/spec/api.json +++ b/spec/api.json @@ -3930,7 +3930,7 @@ }, "/admin/identities": { "get": { - "description": "Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system.", + "description": "Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined.", "operationId": "listIdentities", "parameters": [ { diff --git a/spec/swagger.json b/spec/swagger.json index 031ad06841ba..9071e0d298bf 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -171,7 +171,7 @@ "oryAccessToken": [] } ], - "description": "Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system.", + "description": "Lists all [identities](https://www.ory.sh/docs/kratos/concepts/identity-user-model) in the system. Note: filters cannot be combined.", "produces": [ "application/json" ],