Skip to content

Commit

Permalink
feat: allow listing identities by organization ID (#4115)
Browse files Browse the repository at this point in the history
  • Loading branch information
jonas-jonas authored Oct 28, 2024
1 parent 4d5f644 commit b4c453b
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 0 deletions.
16 changes: 16 additions & 0 deletions identity/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,13 @@ 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
// in: query
OrganizationID string `json:"organization_id"`

crdbx.ConsistencyRequestParameters
}

Expand Down Expand Up @@ -207,6 +214,14 @@ 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"))
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)
Expand All @@ -222,6 +237,7 @@ func (h *Handler) list(w http.ResponseWriter, r *http.Request, _ httprouter.Para
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,
}
Expand Down
38 changes: 38 additions & 0 deletions identity/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1472,6 +1472,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:[email protected]", http.StatusOK)
Expand Down
1 change: 1 addition & 0 deletions identity/pool.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type (
CredentialsIdentifierSimilar string
DeclassifyCredentials []CredentialsType
KeySetPagination []keysetpagination.Option
OrganizationID uuid.UUID
ConsistencyLevel crdbx.ConsistencyLevel
StatementTransformer func(string) string

Expand Down
8 changes: 8 additions & 0 deletions internal/client-go/api_identity.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions internal/httpclient/api_identity.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions persistence/sql/identity/persister_identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,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(`
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX identities_nid_organization_id_idx;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP INDEX identities_nid_organization_id_idx ON identities;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE INDEX identities_nid_organization_id_idx ON identities (organization_id);
8 changes: 8 additions & 0 deletions spec/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -4029,6 +4029,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": {
Expand Down
6 changes: 6 additions & 0 deletions spec/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down

0 comments on commit b4c453b

Please sign in to comment.