-
-
Notifications
You must be signed in to change notification settings - Fork 964
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
chore: refactor parameter parsing in ListIdentities and disallow combining filters #4244
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -119,10 +119,7 @@ | |
// 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 @@ | |
|
||
// 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 @@ | |
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 @@ | |
// 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 @@ | |
} | ||
u := *r.URL | ||
pagepagination.PaginationHeader(w, &u, total, params.PagePagination.Page, params.PagePagination.ItemsPerPage) | ||
} else { | ||
} else if nextPage != nil { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When using the ID filter, pagination doesn't make sense. In https://github.com/ory-corp/cloud/pull/7471, I'm returning a nil paginator. Mostly because I don't know how to construct a "no-next-page" paginator and didn't want to change ory/x. |
||
u := *r.URL | ||
keysetpagination.Header(w, &u, nextPage) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(), | ||
"[email protected]", | ||
"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) | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could consider to reduce the impact of this breaking change by just ignoring additional filters. If
ids=a&ids=b&organization_id=x
previously returned onlya
, it could now returna,b
ignoring the second filter. The result should always be a super-set of the previous version, therefore significantly limiting the impact.On the other side, someone might rely on the behavior and this could in the worst case lead to security issues if the assumption is that all returned identities are part of the given organization.
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've checked our logs and could not find anyone combining filters, so I think we're good.
Changing semantics silently is a recipe for disaster.