Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

nrsim
Copy link

@nrsim nrsim commented Feb 3, 2020

No description provided.

}

if (args.providerToLink != null)
{
Copy link
Member

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;
Copy link
Member

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))
Copy link
Member

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");
Copy link
Member

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.

Copy link
Contributor

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]);
Copy link
Member

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
Copy link
Member

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:

  1. Remove that sentence.
  2. 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.)
  3. 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.

Copy link
Contributor

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;
Copy link
Member

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>();
Copy link
Member

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.

@rsgowman rsgowman assigned nrsim and unassigned rsgowman Feb 5, 2020
Copy link
Contributor

@hiranya911 hiranya911 left a 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
Copy link
Contributor

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);
Copy link
Contributor

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;
Copy link
Contributor

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");
Copy link
Contributor

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" });
Copy link
Contributor

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()
Copy link
Contributor

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 = ....
                };

@Thaina
Copy link

Thaina commented Dec 30, 2020

Why this pull request got stuck since february?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants