-
Notifications
You must be signed in to change notification settings - Fork 977
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
feat: identity by identifier #3077
Conversation
d9c3dec
to
969a909
Compare
Codecov Report
@@ Coverage Diff @@
## master #3077 +/- ##
==========================================
+ Coverage 77.35% 77.41% +0.05%
==========================================
Files 315 315
Lines 19817 19885 +68
==========================================
+ Hits 15330 15394 +64
- Misses 3297 3300 +3
- Partials 1190 1191 +1
Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here. |
ad7f864
to
19bd10f
Compare
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.
Looking good already. Have a few comments on the semantics of the API.
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.
One more detail on how we want the identifier
parameter to behave. IMO, it should behave like a filter to the outside consumer, even though technically speaking, there can only ever be one result returned from this (right now).
Btw. I do recall discussions about whether we can widen the scope of the identifier
parameter filter to other fields, such as recovery addresses, OIDC connections, etc. Did you look into that? Is that possible?
Because if so, there could very well be more than one identity (I think) in the result set.
identity/handler_test.go
Outdated
@@ -117,6 +117,10 @@ func TestHandler(t *testing.T) { | |||
} | |||
}) | |||
|
|||
t.Run("case=should return 404 on a non-existing resource for identity lookup", func(t *testing.T) { |
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.
Well, no. IMO, this should return an empty array, since clients expect an array here.
How would this feature be used in the context of the ory network?
We'd probably add a "search" input to the "List identities" table, similar to the email delivery page:
Or do you have something else in mind?
And we already (should) have the logic for displaying a message for "no results found" if the array is empty. So if this endpoint all of a sudden returns 404 this will probably break that and we'd need to implement better error handling on that page.
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.
identity/handler_test.go
Outdated
res := get(t, adminTS, "/[email protected]", http.StatusOK) | ||
assert.EqualValues(t, i1.ID.String(), res.Get("0.id").String(), "%s", res.Raw) | ||
assert.EqualValues(t, "[email protected]", res.Get("0.traits.username").String(), "%s", res.Raw) | ||
assert.EqualValues(t, defaultSchemaExternalURL, res.Get("0.schema_url").String(), "%s", res.Raw) | ||
assert.EqualValues(t, config.DefaultIdentityTraitsSchemaID, res.Get("0.schema_id").String(), "%s", res.Raw) | ||
assert.EqualValues(t, identity.StateActive, res.Get("0.state").String(), "%s", res.Raw) | ||
assert.Empty(t, res.Get("credentials").String(), "%s", res.Raw) |
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.
Nit: could use a snapshot instead.
Okay, so OIDC connections and webauthn credentials would be stored in the same way the credentials are. So that should work. But I think it would be great, if we had a test that also checks whether accounts with those kinds of credentials can be found, too. Right now, we're only testing for password credentials, right? |
89565f8
to
8166ba1
Compare
identity/handler.go
Outdated
return | ||
} | ||
|
||
h.r.Writer().Write(w, r, []Identity{*i}) |
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.
Shouldn't this be analogous to:
// Identities using the marshaler for including metadata_admin
isam := make([]WithAdminMetadataInJSON, len(is))
for i, identity := range is {
isam[i] = WithAdminMetadataInJSON(identity)
}
And more generally speaking, I think that the implementation does not cover both use cases:
- Search users as an administrator to e.g. delete them
- Show the user ways he/she can sign in (e.g. user A has password and social enabled -> show those two fields during login)
The second case is not (yet) covered, because we do not return the credentials metadata. Thus, the consumer of the API wouldn't know which credentials are available for the user.
Here too I'm fine with shipping this in its current state and adding credentials metainfo later.
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.
Credentials use case is now fixed - 4a16b0f
identity/test/pool.go
Outdated
@@ -594,13 +594,33 @@ func TestPool(ctx context.Context, conf *config.Config, p interface { | |||
}) | |||
|
|||
t.Run("case=find identity by its credentials identifier", func(t *testing.T) { | |||
expected := passwordIdentity("", "[email protected]") |
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.
Note: The test has a medium score - it does test properly that the persister is able to find the identity by its identifier, but since we only create one identity it could also simply return the first identity in the list. What saves us is the implicit knowledge that we created 25 identities in the test prior, but that implies that we don't care about test isolation. In isolation, the test doesn't properly satisfy our requirements (of 10 identities give me exactly the one that has identifier X; for 10 identities tell me "no identity found" if I search for a non-existing user identifier)
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.
Fair point about the test isolation, I'll fix the test to create identities of different credential types as well
"identity_credentials", | ||
"identity_credential_identifiers", | ||
), | ||
match, |
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.
Normalization like we do it in the original function is missing:
match = p.normalizeIdentifier(ct, match)
That means that the match is to be case sensitive, which will return different results depending on which persister API you use. Please normalize the match
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.
@aeneasr Normalization was left out because OIDC credential identifier would be case-sensitive while Password/WebAuthn identifier's are lower-cased during identity creation. Prior to the query, we just have the identifier and not the credential type to perform the appropriate normalization
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 see, that's a fair assumption. Let's view it in the full context:
Only webauthn and password use user-based input for their identifiers (e.g. the email or username), which is why they get normalized (because users mess up capitalization on mobile). All other methods use either the identity's UUID or in the case of Social Sign In a concatenated string provider:user_id_from_provider
IMO we only need to cover the use case where the identifier is based on user input (aka registration or settings form - email, username, phone number, ...) because it doesn't really make sense to search for these system generated strings from an API perspective.
By the way, an addition to the logic would be to search for recovery_addresses
or verification_addresses
using the via
. Not saying that we have to do it now, but can add later.
@@ -83,7 +83,7 @@ func (p *Persister) normalizeIdentifier(ct identity.CredentialsType, match strin | |||
return match | |||
} | |||
|
|||
func (p *Persister) FindByCredentialsIdentifier(ctx context.Context, ct identity.CredentialsType, match string) (*identity.Identity, *identity.Credentials, error) { | |||
func (p *Persister) FindByCredentialsIdentifier(ctx context.Context, match string) (*identity.Identity, error) { |
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.
Note: this is more or less a copy of FindByCredentialsTypeAndIdentifier
with two distinctions
- Credentials are not returned
- We do not care about credentials type
It would be much nicer (and less error prone) to have a common functionality to cover "search for identity credentials identifier with and without the credentials type".
There's always a balance between abstraction and copy/pasting code. The latter does come with the usualy problems such as forgetting to add some type of transformation (in this case p.normalizeIdentifier
).
If it was using the same underlying mechanism, existing tests might have caught that normalization is missing. But since this is net new code we need to prove again that it behaves like the other method:
- Properly finds credentials by their identifier
- Can deal with normalization
- Doesn't return invalid results
I'm OK to keep it this way for the sake of getting it shipped, but only if we cover the same test requirements we have for FindByCredentialsTypeAndIdentifier
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.
@aeneasr I didn't forget the normalization 😓 I left it out on purpose because the credential type isn't known before the query. I left a note in the github issue about that earlier - https://github.com/ory-corp/cloud/issues/3947#issuecomment-1422394212
identity/handler.go
Outdated
if errors.Is(err, sqlcon.ErrNoRows) { | ||
h.r.Writer().Write(w, r, []Identity{}) | ||
return | ||
} |
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.
Note: This should be handled the persister / pool. It's doesn't affect the behavior of the code but would be "cleaner" in terms of architecture. It's ok to keep it here though, we can clean it up if when FindByCredentialsIdentifier
is used somewhere else.
identity/handler_test.go
Outdated
t.Run("case=should return an empty array on a failed lookup with identifier", func(t *testing.T) { | ||
res := get(t, adminTS, "/[email protected]", http.StatusOK) | ||
assert.EqualValues(t, int64(0), res.Get("#").Int(), "%s", res.Raw) | ||
}) | ||
|
||
t.Run("case=should be able to lookup the identity using identifier", func(t *testing.T) { | ||
i1 := &identity.Identity{ | ||
Credentials: map[identity.CredentialsType]identity.Credentials{ | ||
identity.CredentialsTypePassword: { | ||
Type: identity.CredentialsTypePassword, | ||
Identifiers: []string{"[email protected]"}, | ||
Config: sqlxx.JSONRawMessage(`{"hashed_password":"$2a$08$.cOYmAd.vCpDOoiVJrO5B.hjTLKQQ6cAK40u8uB.FnZDyPvVvQ9Q."}`), // foobar | ||
}}, | ||
State: identity.StateActive, | ||
Traits: identity.Traits(`{"username":"[email protected]"}`), | ||
} |
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.
Note: If the persister test proves that "out of a list of X elements I find exactly the one element that matches the query", you don't need to test this again in the handler. So the test is fine here like this. This comment is in reference to another comment in the persister tests.
4ac6c3e
to
884e6d2
Compare
Co-authored-by: Jonas Hungershausen <[email protected]>
884e6d2
to
f3020d3
Compare
dadb7f3
to
4a16b0f
Compare
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.
Nice changes.
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.
LGTM
Changes
Related issue(s)
https://github.com/ory-corp/cloud/issues/3947
Checklist
introduces a new feature.
contributing code guidelines.
vulnerability. If this pull request addresses a security vulnerability, I
confirm that I got the approval (please contact
[email protected]) from the maintainers to push
the changes.
works.