From 5cfbf66397749e24c926f2a4593597d1f79e19f5 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Thu, 12 Sep 2024 10:43:54 +0200 Subject: [PATCH 1/6] feat: allow listing identities by organization ID --- identity/handler.go | 7 +++++++ identity/pool.go | 1 + internal/client-go/api_identity.go | 8 ++++++++ internal/httpclient/api_identity.go | 8 ++++++++ persistence/sql/identity/persister_identity.go | 5 +++++ spec/api.json | 8 ++++++++ spec/swagger.json | 6 ++++++ 7 files changed, 43 insertions(+) diff --git a/identity/handler.go b/identity/handler.go index cf85dc792c43..2a089e9b578e 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -171,6 +171,12 @@ type listIdentitiesParameters struct { // in: query DeclassifyCredentials []string `json:"include_credential"` + // OrganizationID is the organization id to filter identities by. + // + // If `ids` is set, this parameter is ignored. + // required: false + OrganizationID string `json:"organization_id"` + crdbx.ConsistencyRequestParameters } @@ -211,6 +217,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para IdsFilter: r.URL.Query()["ids"], CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), + OrganizationID: x.ParseUUID(r.URL.Query().Get("organization_id")), ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), DeclassifyCredentials: declassify, } diff --git a/identity/pool.go b/identity/pool.go index 8a94aad3e075..5e65ba6aac6b 100644 --- a/identity/pool.go +++ b/identity/pool.go @@ -23,6 +23,7 @@ type ( CredentialsIdentifierSimilar string DeclassifyCredentials []CredentialsType KeySetPagination []keysetpagination.Option + OrganizationID uuid.UUID // DEPRECATED PagePagination *x.Page ConsistencyLevel crdbx.ConsistencyLevel diff --git a/internal/client-go/api_identity.go b/internal/client-go/api_identity.go index 47c2eb6cbfc4..9e4aec1b6c58 100644 --- a/internal/client-go/api_identity.go +++ b/internal/client-go/api_identity.go @@ -2087,6 +2087,7 @@ type IdentityAPIApiListIdentitiesRequest struct { credentialsIdentifier *string previewCredentialsIdentifierSimilar *string includeCredential *[]string + organizationId *string } func (r IdentityAPIApiListIdentitiesRequest) PerPage(perPage int64) IdentityAPIApiListIdentitiesRequest { @@ -2125,6 +2126,10 @@ func (r IdentityAPIApiListIdentitiesRequest) IncludeCredential(includeCredential r.includeCredential = &includeCredential return r } +func (r IdentityAPIApiListIdentitiesRequest) OrganizationId(organizationId string) IdentityAPIApiListIdentitiesRequest { + r.organizationId = &organizationId + return r +} func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2211,6 +2216,9 @@ func (a *IdentityAPIService) ListIdentitiesExecute(r IdentityAPIApiListIdentitie localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) } } + if r.organizationId != nil { + localVarQueryParams.Add("organization_id", parameterToString(*r.organizationId, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/internal/httpclient/api_identity.go b/internal/httpclient/api_identity.go index 47c2eb6cbfc4..9e4aec1b6c58 100644 --- a/internal/httpclient/api_identity.go +++ b/internal/httpclient/api_identity.go @@ -2087,6 +2087,7 @@ type IdentityAPIApiListIdentitiesRequest struct { credentialsIdentifier *string previewCredentialsIdentifierSimilar *string includeCredential *[]string + organizationId *string } func (r IdentityAPIApiListIdentitiesRequest) PerPage(perPage int64) IdentityAPIApiListIdentitiesRequest { @@ -2125,6 +2126,10 @@ func (r IdentityAPIApiListIdentitiesRequest) IncludeCredential(includeCredential r.includeCredential = &includeCredential return r } +func (r IdentityAPIApiListIdentitiesRequest) OrganizationId(organizationId string) IdentityAPIApiListIdentitiesRequest { + r.organizationId = &organizationId + return r +} func (r IdentityAPIApiListIdentitiesRequest) Execute() ([]Identity, *http.Response, error) { return r.ApiService.ListIdentitiesExecute(r) @@ -2211,6 +2216,9 @@ func (a *IdentityAPIService) ListIdentitiesExecute(r IdentityAPIApiListIdentitie localVarQueryParams.Add("include_credential", parameterToString(t, "multi")) } } + if r.organizationId != nil { + localVarQueryParams.Add("organization_id", parameterToString(*r.organizationId, "")) + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} diff --git a/persistence/sql/identity/persister_identity.go b/persistence/sql/identity/persister_identity.go index 807d3d67779d..3084393816ae 100644 --- a/persistence/sql/identity/persister_identity.go +++ b/persistence/sql/identity/persister_identity.go @@ -859,6 +859,11 @@ func (p *IdentityPersister) ListIdentities(ctx context.Context, params identity. AND identities.id in (?) ` args = append(args, params.IdsFilter) + } else if !params.OrganizationID.IsNil() { + wheres += ` + AND identities.organization_id = ? + ` + args = append(args, params.OrganizationID.String()) } query := fmt.Sprintf(` diff --git a/spec/api.json b/spec/api.json index c8f296913fa0..d2b4b09047e8 100644 --- a/spec/api.json +++ b/spec/api.json @@ -4032,6 +4032,14 @@ }, "type": "array" } + }, + { + "description": "OrganizationID is the organization id to filter identities by.\n\nIf `ids` is set, this parameter is ignored.", + "in": "query", + "name": "organization_id", + "schema": { + "type": "string" + } } ], "responses": { diff --git a/spec/swagger.json b/spec/swagger.json index 2952837d4c09..1e40703227fd 100755 --- a/spec/swagger.json +++ b/spec/swagger.json @@ -261,6 +261,12 @@ "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" + }, + { + "type": "string", + "description": "OrganizationID is the organization id to filter identities by.\n\nIf `ids` is set, this parameter is ignored.", + "name": "organization_id", + "in": "query" } ], "responses": { From 23670e51080b1ffae80ec3618271052dbb1fcdf8 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 23 Sep 2024 12:31:58 +0200 Subject: [PATCH 2/6] chore: parse UUID, add tests and index --- identity/handler.go | 33 ++++++++++------ identity/handler_test.go | 38 +++++++++++++++++++ ...95000000001_organization_id_index.down.sql | 1 + ...3095000000001_organization_id_index.up.sql | 1 + 4 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql create mode 100644 persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql diff --git a/identity/handler.go b/identity/handler.go index 2a089e9b578e..90755f2e8dad 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -11,6 +11,8 @@ import ( "strings" "time" + "github.com/gofrs/uuid" + "github.com/ory/x/crdbx" "github.com/ory/x/pagination/keysetpagination" @@ -175,6 +177,7 @@ type listIdentitiesParameters struct { // // If `ids` is set, this parameter is ignored. // required: false + // in: query OrganizationID string `json:"organization_id"` crdbx.ConsistencyRequestParameters @@ -199,6 +202,7 @@ type listIdentitiesParameters struct { // 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) @@ -210,18 +214,25 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para } } - var ( - err error - params = ListIdentityParameters{ - Expand: ExpandDefault, - IdsFilter: r.URL.Query()["ids"], - CredentialsIdentifier: r.URL.Query().Get("credentials_identifier"), - CredentialsIdentifierSimilar: r.URL.Query().Get("preview_credentials_identifier_similar"), - OrganizationID: x.ParseUUID(r.URL.Query().Get("organization_id")), - ConsistencyLevel: crdbx.ConsistencyLevelFromRequest(r), - DeclassifyCredentials: declassify, + + 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 } - ) + } + + params := ListIdentityParameters{ + Expand: ExpandDefault, + IdsFilter: r.URL.Query()["ids"], + 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 diff --git a/identity/handler_test.go b/identity/handler_test.go index d97c2f73fae4..bb9979a39095 100644 --- a/identity/handler_test.go +++ b/identity/handler_test.go @@ -1457,6 +1457,44 @@ func TestHandler(t *testing.T) { } }) + t.Run("organizations", func(t *testing.T) { + t.Run("case=should list organization identities", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + orgID := uuid.Must(uuid.NewV4()) + email := x.NewUUID().String() + "@ory.sh" + reg.IdentityManager().Create(ctx, &identity.Identity{ + Traits: identity.Traits(`{"email":"` + email + `"}`), + OrganizationID: uuid.NullUUID{UUID: orgID, Valid: true}, + }) + + res := get(t, ts, "/identities?organization_id="+orgID.String(), http.StatusOK) + assert.Len(t, res.Array(), 1) + assert.EqualValues(t, email, res.Get(`0.traits.email`).String(), "%s", res.Raw) + }) + } + }) + + t.Run("case=malformed organization id should return an error", 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?organization_id=not-a-uuid", http.StatusBadRequest) + assert.Contains(t, res.Get("error.reason").String(), "Invalid UUID value `not-a-uuid` for parameter `organization_id`.", "%s", res.Raw) + }) + } + }) + + t.Run("case=unknown organization id should return an empty list", func(t *testing.T) { + for name, ts := range map[string]*httptest.Server{"admin": adminTS} { + t.Run("endpoint="+name, func(t *testing.T) { + id := x.NewUUID() + res := get(t, ts, "/identities?organization_id="+id.String(), http.StatusOK) + assert.Len(t, res.Array(), 0) + }) + } + }) + }) + t.Run("case=should list all identities with credentials", func(t *testing.T) { t.Run("include_credential=oidc should include OIDC credentials config", func(t *testing.T) { res := get(t, adminTS, "/identities?include_credential=oidc&credentials_identifier=bar:foo.oidc@bar.com", http.StatusOK) diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql new file mode 100644 index 000000000000..a7fe812fe93a --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql @@ -0,0 +1 @@ +DROP INDEX identities_organization_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql new file mode 100644 index 000000000000..2fc9301ddc82 --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql @@ -0,0 +1 @@ +CREATE INDEX identities_organization_id ON identities (organization_id ASC); \ No newline at end of file From 6038a096781eed1c1d7d680d9ba4f51635b188f4 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 23 Sep 2024 12:39:34 +0200 Subject: [PATCH 3/6] chore: format --- identity/handler.go | 1 - 1 file changed, 1 deletion(-) diff --git a/identity/handler.go b/identity/handler.go index 90755f2e8dad..77085df0633d 100644 --- a/identity/handler.go +++ b/identity/handler.go @@ -214,7 +214,6 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para } } - var orgId uuid.UUID if orgIdStr := r.URL.Query().Get("organization_id"); orgIdStr != "" { orgId, err = uuid.FromString(r.URL.Query().Get("organization_id")) From 83ee721ac2c3cc51b48fef65a5513d00fb8142dd Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 23 Sep 2024 12:42:15 +0200 Subject: [PATCH 4/6] chore: adjust index to include nid --- .../sql/20240923095000000001_organization_id_index.down.sql | 2 +- .../sql/20240923095000000001_organization_id_index.up.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql index a7fe812fe93a..67a184b9d640 100644 --- a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql @@ -1 +1 @@ -DROP INDEX identities_organization_id; \ No newline at end of file +DROP INDEX identities_nid_organization_id; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql index 2fc9301ddc82..392c23fe72ae 100644 --- a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql @@ -1 +1 @@ -CREATE INDEX identities_organization_id ON identities (organization_id ASC); \ No newline at end of file +CREATE INDEX identities_nid_organization_id ON identities (nid, organization_id); \ No newline at end of file From 7547e1c6dfcfb0b3b820ce9901fb801bb782c708 Mon Sep 17 00:00:00 2001 From: Jonas Hungershausen Date: Mon, 23 Sep 2024 17:18:57 +0200 Subject: [PATCH 5/6] chore: rename index --- .../sql/20240923095000000001_organization_id_index.down.sql | 2 +- .../sql/20240923095000000001_organization_id_index.up.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql index 67a184b9d640..cca461b9eeab 100644 --- a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.down.sql @@ -1 +1 @@ -DROP INDEX identities_nid_organization_id; \ No newline at end of file +DROP INDEX identities_nid_organization_id_idx; \ No newline at end of file diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql index 392c23fe72ae..1a88832db881 100644 --- a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql @@ -1 +1 @@ -CREATE INDEX identities_nid_organization_id ON identities (nid, organization_id); \ No newline at end of file +CREATE INDEX identities_nid_organization_id_idx ON identities (nid, organization_id); \ No newline at end of file From 1795a7a5677882ecfc14bd3172267d879ea5c7aa Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:24:04 +0200 Subject: [PATCH 6/6] chore: synchronize workspaces --- .../20240923095000000001_organization_id_index.mysql.down.sql | 1 + .../sql/20240923095000000001_organization_id_index.up.sql | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql new file mode 100644 index 000000000000..c2fbff7ff081 --- /dev/null +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.mysql.down.sql @@ -0,0 +1 @@ +DROP INDEX identities_nid_organization_id_idx ON identities; diff --git a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql index 1a88832db881..10a84cb55cd7 100644 --- a/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql +++ b/persistence/sql/migrations/sql/20240923095000000001_organization_id_index.up.sql @@ -1 +1 @@ -CREATE INDEX identities_nid_organization_id_idx ON identities (nid, organization_id); \ No newline at end of file +CREATE INDEX identities_nid_organization_id_idx ON identities (organization_id);