Skip to content

Custom Claims and List Users Support #92

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

Merged
merged 41 commits into from
Dec 5, 2017
Merged

Conversation

hiranya911
Copy link
Contributor

Adding two new operations to the FirebaseAuth API:

  • listUsersAsync(String pageToken, int maxResults): Facilitates paging through all the user accounts of a Firebase project.
  • setCustomClaimsAsync(String uid, Map<String, Object> claims): Sets custom attributes on a user account.

go/firebase-admin-custom-claims

… in default thread managers to ensure clean JVM exit
…e check until we have to serialize the update parameters
@hiranya911
Copy link
Contributor Author

@mikelehen This is a recently approved API (already released in Node.js). Please review when you get a chance. Not urgent as of now.


/**
* Contains metadata associated with a Firebase user account, along with password hash and salt.
* Instances of this class are immutable and thread safe.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/thread safe/thread-safe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

this.passwordSalt = response.getPasswordSalt();
}

@Nullable
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add documentation here and below. Also explain when this might be null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

throw new FirebaseAuthException(LIST_USERS_ERROR,
"IO error while downloading user accounts.", e);
}
if (response == null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this might be more readable if you move this into the try block.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

private ListUsersPage(@NonNull UserSource source, int maxResults, @Nullable String pageToken) {
this.source = source;
this.maxResults = maxResults;
this.currentBatch = source.fetch(maxResults, pageToken);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is performing a synchronous network operation inside the constructor. Can you move this code to the accessors?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is much easier to reason about when it's immutable. Therefore I'd rather fetch the data upon construction, and assign to a final attribute. Also lazy loading is not a requirement for this class. FirebaseAuth.listUsers() and Page.getNextPage() are expected to make RPC calls, and return fully populated pages as the result.

We can move the RPC call out of the constructor, but since we create pages in 2 places the constructor seems like a natural place to place that bit of code. Alternatively we can try a private ensureCurrentBatch() method to load the data on demand, but we are not really saving anything since all operations on Page will have to call it.

Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Nov 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generally frowned upon as it makes testing hard (See http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/).

One easy way to mitigate would be to create a factory method and pass in currentBatch.

try {
DownloadAccountResponse response = userManager.listUsers(maxResults, pageToken);
ImmutableList.Builder<ExportedUserRecord> builder = ImmutableList.builder();
if (response.getUsers() != null) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/getUsers/hasUsers()/?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

}
}
String nextPageToken = response.getPageToken() != null
? response.getPageToken() : END_OF_LIST;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you though about using "null" to mark the final page token and just validating that the end user never passes it in?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users should be able to pass null here, which stands for the first page.

if (parsed.isEmpty()) {
this.customClaims = null;
} else {
this.customClaims = ImmutableMap.copyOf(parsed);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to delete a single custom claim?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Something like:

Map<String, Object> copy = new HashMap(userRecord.getCustomClaims());
copy.remove("someClaim");
updateRequest.setCustomClaims(copy);

@@ -177,6 +194,16 @@ public UserMetadata getUserMetadata() {
}

/**
* Returns additional custom claims set on this user.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what additional means in this context. I suppose it returns all the custom claims?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed additional

*/
@Nullable
public Map<String,Object> getCustomClaims() {
return customClaims;
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 we should return an immutable copy here.

Also - can we return an empty map if there are no custom claims?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting to empty map when there are no claims. This always get set to ImmutableMap in the constructor.


private static String serializeCustomClaims(Map customClaims, JsonFactory jsonFactory) {
checkNotNull(jsonFactory, "JsonFactory must not be null");
if (customClaims == null || customClaims.isEmpty()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you initialize customClaims to an empty map, this would go away as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a value directly passed in by the user via UpdateRequest.setCustomClaims(). Therefore null values are allowed here.

@hiranya911
Copy link
Contributor Author

@schmidt-sebastian Made most of the suggested changes. Provided explanations on the rest. On to the next round.

ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
.put("maxResults", maxResults);
if (pageToken != null) {
checkArgument(!pageToken.isEmpty(), "invalid end of list page token");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about pageToken.equals(ListUsersPage.END_OF_LIST)? That way, that implementation detail only exists in one place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Done.

private ListUsersPage(@NonNull UserSource source, int maxResults, @Nullable String pageToken) {
this.source = source;
this.maxResults = maxResults;
this.currentBatch = source.fetch(maxResults, pageToken);
Copy link
Contributor

@schmidt-sebastian schmidt-sebastian Nov 9, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is generally frowned upon as it makes testing hard (See http://misko.hevery.com/code-reviewers-guide/flaw-constructor-does-real-work/).

One easy way to mitigate would be to create a factory method and pass in currentBatch.

update.getProperties(null);
fail("No error thrown for null JsonFactory");
} catch (Exception ignore) {
// expected
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there any specifics you can verify here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Checking for NPE now.

@hiranya911
Copy link
Contributor Author

Made the suggested changes. Moved the RPC calls in ListUsersPage constructor to the PageFactory helper class,

@schmidt-sebastian
Copy link
Contributor

Thanks so much! I am a big fan.

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.

3 participants