-
Notifications
You must be signed in to change notification settings - Fork 132
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
Add the ability to link/unlink a provider user id to an account. #143
base: master
Are you sure you want to change the base?
Conversation
} | ||
|
||
if (args.providerToLink != null) | ||
{ |
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 also need to allow other non-federated providers to work here. For instance, if providerToLink.ProviderId == "phone"
, then we should set this.PhoneNumber = CheckPhoneNumber(providerToLink.ProviderUid)
and not set this.ProviderToLink
at all. (See the node port for how I did it... firebase/firebase-admin-node#770. But that's not fully reviewed yet, so take it with a grain of salt.)
I'm not sure what this means for the email provider. Under node, the email parameter is optional. But under C#, it looks like it might be required? If so, I think it would be sufficient to immediately throw should providerToLink.ProviderId == "email"
.
@@ -30,6 +30,8 @@ public sealed class UserRecordArgs | |||
private Optional<string> photoUrl; | |||
private Optional<string> phoneNumber; | |||
private Optional<IReadOnlyDictionary<string, object>> customClaims; | |||
private Optional<ProviderUserInfo> providerToLink; | |||
private Optional<IList<string>> providersToDelete; |
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.
Rather than IList, you could probably use IEnumerable.
var iteratedIds = new HashSet<string>(); | ||
foreach (string providerId in providerIds) | ||
{ | ||
if (iteratedIds.Contains(providerId)) |
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.
This block could become:
if (!iteratedIds.Add(providerId)) {
throw ...;
}
(ISet.Add returns false if the item was already present.)
@@ -312,6 +360,7 @@ internal UpdateUserRequest(UserRecordArgs args) | |||
|
|||
if (args.phoneNumber != null) | |||
{ | |||
providerIdsToUpdate.Add("phone"); |
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.
This is an interesting pattern. As an alternative, you could:
void AddOrThrow(ISet<T> s, T t) {
if (!s.add(t)) {
throw ...;
}
}
And then use it here like this:
AddOrThrow(providerIdsToUpdate, "phone");
(And switch providerIdsToUpdate to a hashset and remove AssertDistinctProviderIds())
Another alternative is to just check everything up front, since there aren't actually many cases where we can run into this.
Both optional.
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.
+1 to this. Let's call it AddIfAbsent()
or something similar.
@@ -66,7 +66,7 @@ internal UserRecord(GetAccountInfoResponse.User user) | |||
this.ProviderData = new IUserInfo[count]; | |||
for (int i = 0; i < count; i++) | |||
{ | |||
this.ProviderData[i] = new ProviderUserInfo(user.Providers[i]); | |||
this.ProviderData[i] = ProviderUserInfo.Create(user.Providers[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.
OOC, why did you convert the ctor to a Create() method?
|
||
namespace FirebaseAdmin.Auth | ||
{ | ||
/// <summary> | ||
/// Contains metadata regarding how a user is known by a particular identity provider (IdP). | ||
/// Instances of this class are immutable and thread safe. | ||
/// </summary> | ||
internal sealed class ProviderUserInfo : IUserInfo | ||
public sealed class ProviderUserInfo : IUserInfo |
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.
docstring claims that these are immutable and thread safe. I don't think that's the case anymore (for either).
Options:
- Remove that sentence.
- Remove the setters. Do we need them after construction? (They were present, though private previously. I don't know if they actually needed to be present at all though.)
- Keep the setters, but mark them private.
...
Oh, I see! You've made the properties publicly settable so that uses can construct them more naturally, i.e.:
var x = new ProviderUserInfo()
{
Email: "[email protected]"
};
But that means that the object looses immutability/threadsafety. Elsewhere in the code base, this pattern is used:
public ProviderUserInfo(ProviderUserInfoArgs x) { ... }
which would allow it to be used almost as nicely, but with thread safety, etc. i.e.:
var x = new ProviderUserInfo(new ProviderUserInfoArgs() {
Email: "[email protected]"
});
(or something like that.)
Check out UserRecord and UserRecordArgs for a concrete example.
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.
The API proposal has the following:
public class UserProviderArgs : IUserInfo
{
public string ProviderId { get; set; }
public string Uid { get; set; }
public string DisplayName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public string PhotoUrl ( get; set; }
}
So just implement that?
Assert.Empty(handler.Requests); | ||
|
||
// Empty provider UID | ||
args.ProviderToLink.Uid = string.Empty; |
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.
Could we parameterize these tests instead? (grep for [Theory]
to see an example or two in our code base. I can't find much documentation on xunit, but there's also a bunch of blog posts that go into more detail.)
@@ -218,13 +220,49 @@ public async Task UserLifecycle() | |||
Assert.False(user.Disabled); | |||
Assert.NotNull(user.UserMetaData.CreationTimestamp); | |||
Assert.Null(user.UserMetaData.LastSignInTimestamp); | |||
Assert.Equal(2, user.ProviderData.Length); | |||
Assert.Equal(3, user.ProviderData.Length); | |||
var providerIds = new HashSet<string>(); |
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.
HashSet has a ctor that takes an enumerable. Combining that with Select, I think you could replace lines 224-228 with:
var providerIds = new HashSet<string>(user.ProviderData.Select((providerData) => providerData.ProviderId));
Optional.
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.
Just a few pointers in addition to what @rsgowman has already commented on.
|
||
namespace FirebaseAdmin.Auth | ||
{ | ||
/// <summary> | ||
/// Contains metadata regarding how a user is known by a particular identity provider (IdP). | ||
/// Instances of this class are immutable and thread safe. | ||
/// </summary> | ||
internal sealed class ProviderUserInfo : IUserInfo | ||
public sealed class ProviderUserInfo : IUserInfo |
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.
The API proposal has the following:
public class UserProviderArgs : IUserInfo
{
public string ProviderId { get; set; }
public string Uid { get; set; }
public string DisplayName { get; set; }
public string Email { get; set; }
public string PhoneNumber { get; set; }
public string PhotoUrl ( get; set; }
}
So just implement that?
public ProviderUserInfo ProviderToLink | ||
{ | ||
get => this.providerToLink?.Value; | ||
set => this.providerToLink = this.Wrap(value); |
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 think you can use a plain old {get; set;}
here. Wrap()
is only used when setting a null value has special meanin. For example PhotoUrl = null
has special meaning in the Auth API -- it's a request to unset the currently set photo URL. I don't think that applies here.
/// </summary> | ||
public IList<string> ProvidersToDelete | ||
{ | ||
get => this.providersToDelete?.Value; |
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.
Same here.
@@ -312,6 +360,7 @@ internal UpdateUserRequest(UserRecordArgs args) | |||
|
|||
if (args.phoneNumber != null) | |||
{ | |||
providerIdsToUpdate.Add("phone"); |
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.
+1 to this. Let's call it AddIfAbsent()
or something similar.
providerIds.Add(providerData.ProviderId); | ||
} | ||
|
||
Assert.Equal(providerIds, new HashSet<string>() { "phone", "password", "google.com" }); |
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.
Expected value goes first.
Assert.Empty(user.CustomClaims); | ||
|
||
// Get user by email | ||
user = await FirebaseAuth.DefaultInstance.GetUserByEmailAsync(randomUser.Email); | ||
Assert.Equal(uid, user.Uid); | ||
|
||
// Delete the linked provider and phone number | ||
var unlinkArgs = new UserRecordArgs() |
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.
Shall we merge this into the existing logic that disables the user account and removes other properties?
var disableArgs = new UserRecordArgs()
{
Uid = uid,
Disabled = true,
DisplayName = null,
PhoneNumber = null,
PhotoUrl = null,
ProvidersToDelete = ....
};
Why this pull request got stuck since february? |
No description provided.