-
Notifications
You must be signed in to change notification settings - Fork 289
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
Conversation
…read factories used by the SDK
… in default thread managers to ensure clean JVM exit
…to documentation and tests.
…-java into hkj-exec-cleanup
…e check until we have to serialize the update parameters
@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. |
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.
s/thread safe/thread-safe.
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.
Done
this.passwordSalt = response.getPasswordSalt(); | ||
} | ||
|
||
@Nullable |
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.
Please add documentation here and below. Also explain when this might be 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.
Done
throw new FirebaseAuthException(LIST_USERS_ERROR, | ||
"IO error while downloading user accounts.", e); | ||
} | ||
if (response == 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.
Nit: this might be more readable if you move this into the try block.
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.
Done
private ListUsersPage(@NonNull UserSource source, int maxResults, @Nullable String pageToken) { | ||
this.source = source; | ||
this.maxResults = maxResults; | ||
this.currentBatch = source.fetch(maxResults, pageToken); |
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 performing a synchronous network operation inside the constructor. Can you move this code to the accessors?
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 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.
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 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) { |
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.
s/getUsers/hasUsers()/?
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.
Done
} | ||
} | ||
String nextPageToken = response.getPageToken() != null | ||
? response.getPageToken() : END_OF_LIST; |
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.
Have you though about using "null" to mark the final page token and just validating that the end user never passes it in?
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.
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); |
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.
Is it possible to delete a single custom claim?
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.
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. |
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 don't understand what additional means in this context. I suppose it returns all the custom claims?
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.
Removed additional
*/ | ||
@Nullable | ||
public Map<String,Object> getCustomClaims() { | ||
return customClaims; |
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 we should return an immutable copy here.
Also - can we return an empty map if there are no custom claims?
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.
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()) { |
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.
If you initialize customClaims to an empty map, this would go away as well.
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 can be a value directly passed in by the user via UpdateRequest.setCustomClaims()
. Therefore null
values are allowed here.
@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"); |
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.
How about pageToken.equals(ListUsersPage.END_OF_LIST)? That way, that implementation detail only exists in one place.
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.
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); |
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 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 |
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.
Are there any specifics you can verify here?
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.
Done. Checking for NPE now.
Made the suggested changes. Moved the RPC calls in |
Thanks so much! I am a big fan. |
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