Skip to content

Commit f8052c7

Browse files
authored
feat(auth): Add bulk get/delete methods (#365)
This PR allows callers to retrieve a list of users by unique identifier (uid, email, phone, federated provider uid) as well as to delete a list of users. RELEASE NOTE: Added getUsers() and deleteUsers() APIs for retrieving and deleting user accounts in bulk.
1 parent ab3af3c commit f8052c7

18 files changed

+1220
-13
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth;
18+
19+
import static com.google.common.base.Preconditions.checkArgument;
20+
21+
import com.google.common.collect.ImmutableList;
22+
import com.google.firebase.auth.internal.BatchDeleteResponse;
23+
import com.google.firebase.internal.NonNull;
24+
import java.util.List;
25+
26+
/**
27+
* Represents the result of the {@link FirebaseAuth#deleteUsersAsync(List)} API.
28+
*/
29+
public final class DeleteUsersResult {
30+
31+
private final int successCount;
32+
private final List<ErrorInfo> errors;
33+
34+
DeleteUsersResult(int users, BatchDeleteResponse response) {
35+
ImmutableList.Builder<ErrorInfo> errorsBuilder = ImmutableList.builder();
36+
List<BatchDeleteResponse.ErrorInfo> responseErrors = response.getErrors();
37+
if (responseErrors != null) {
38+
checkArgument(users >= responseErrors.size());
39+
for (BatchDeleteResponse.ErrorInfo error : responseErrors) {
40+
errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage()));
41+
}
42+
}
43+
errors = errorsBuilder.build();
44+
successCount = users - errors.size();
45+
}
46+
47+
/**
48+
* Returns the number of users that were deleted successfully (possibly zero). Users that did not
49+
* exist prior to calling {@link FirebaseAuth#deleteUsersAsync(List)} are considered to be
50+
* successfully deleted.
51+
*/
52+
public int getSuccessCount() {
53+
return successCount;
54+
}
55+
56+
/**
57+
* Returns the number of users that failed to be deleted (possibly zero).
58+
*/
59+
public int getFailureCount() {
60+
return errors.size();
61+
}
62+
63+
/**
64+
* A list of {@link ErrorInfo} instances describing the errors that were encountered during
65+
* the deletion. Length of this list is equal to the return value of
66+
* {@link #getFailureCount()}.
67+
*
68+
* @return A non-null list (possibly empty).
69+
*/
70+
@NonNull
71+
public List<ErrorInfo> getErrors() {
72+
return errors;
73+
}
74+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright 2020 Google Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth;
18+
19+
import com.google.firebase.auth.internal.GetAccountInfoRequest;
20+
import com.google.firebase.internal.NonNull;
21+
22+
/**
23+
* Used for looking up an account by email.
24+
*
25+
* @see {FirebaseAuth#getUsers}
26+
*/
27+
public final class EmailIdentifier extends UserIdentifier {
28+
private final String email;
29+
30+
public EmailIdentifier(@NonNull String email) {
31+
UserRecord.checkEmail(email);
32+
this.email = email;
33+
}
34+
35+
@Override
36+
public String toString() {
37+
return "EmailIdentifier(" + email + ")";
38+
}
39+
40+
@Override
41+
void populate(@NonNull GetAccountInfoRequest payload) {
42+
payload.addEmail(email);
43+
}
44+
45+
@Override
46+
boolean matches(@NonNull UserRecord userRecord) {
47+
return email.equals(userRecord.getEmail());
48+
}
49+
}

src/main/java/com/google/firebase/auth/FirebaseAuth.java

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@
4242
import com.google.firebase.internal.Nullable;
4343

4444
import java.io.IOException;
45+
import java.util.Collection;
46+
import java.util.HashSet;
4547
import java.util.List;
4648
import java.util.Map;
49+
import java.util.Set;
4750
import java.util.concurrent.atomic.AtomicBoolean;
4851

4952
/**
@@ -601,7 +604,82 @@ protected UserRecord execute() throws FirebaseAuthException {
601604
}
602605

603606
/**
604-
* Gets a page of users starting from the specified {@code pageToken}. Page size will be
607+
* Gets the user data corresponding to the specified identifiers.
608+
*
609+
* <p>There are no ordering guarantees; in particular, the nth entry in the users result list is
610+
* not guaranteed to correspond to the nth entry in the input parameters list.
611+
*
612+
* <p>A maximum of 100 identifiers may be specified. If more than 100 identifiers are
613+
* supplied, this method throws an {@link IllegalArgumentException}.
614+
*
615+
* @param identifiers The identifiers used to indicate which user records should be returned. Must
616+
* have 100 or fewer entries.
617+
* @return The corresponding user records.
618+
* @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100
619+
* identifiers are specified.
620+
* @throws NullPointerException If the identifiers parameter is null.
621+
* @throws FirebaseAuthException If an error occurs while retrieving user data.
622+
*/
623+
public GetUsersResult getUsers(@NonNull Collection<UserIdentifier> identifiers)
624+
throws FirebaseAuthException {
625+
return getUsersOp(identifiers).call();
626+
}
627+
628+
/**
629+
* Gets the user data corresponding to the specified identifiers.
630+
*
631+
* <p>There are no ordering guarantees; in particular, the nth entry in the users result list is
632+
* not guaranteed to correspond to the nth entry in the input parameters list.
633+
*
634+
* <p>A maximum of 100 identifiers may be specified. If more than 100 identifiers are
635+
* supplied, this method throws an {@link IllegalArgumentException}.
636+
*
637+
* @param identifiers The identifiers used to indicate which user records should be returned.
638+
* Must have 100 or fewer entries.
639+
* @return An {@code ApiFuture} that resolves to the corresponding user records.
640+
* @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100
641+
* identifiers are specified.
642+
* @throws NullPointerException If the identifiers parameter is null.
643+
*/
644+
public ApiFuture<GetUsersResult> getUsersAsync(@NonNull Collection<UserIdentifier> identifiers) {
645+
return getUsersOp(identifiers).callAsync(firebaseApp);
646+
}
647+
648+
private CallableOperation<GetUsersResult, FirebaseAuthException> getUsersOp(
649+
@NonNull final Collection<UserIdentifier> identifiers) {
650+
checkNotDestroyed();
651+
checkNotNull(identifiers, "identifiers must not be null");
652+
checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE,
653+
"identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE
654+
+ " entries.");
655+
656+
final FirebaseUserManager userManager = getUserManager();
657+
return new CallableOperation<GetUsersResult, FirebaseAuthException>() {
658+
@Override
659+
protected GetUsersResult execute() throws FirebaseAuthException {
660+
Set<UserRecord> users = userManager.getAccountInfo(identifiers);
661+
Set<UserIdentifier> notFound = new HashSet<>();
662+
for (UserIdentifier id : identifiers) {
663+
if (!isUserFound(id, users)) {
664+
notFound.add(id);
665+
}
666+
}
667+
return new GetUsersResult(users, notFound);
668+
}
669+
};
670+
}
671+
672+
private boolean isUserFound(UserIdentifier id, Collection<UserRecord> userRecords) {
673+
for (UserRecord userRecord : userRecords) {
674+
if (id.matches(userRecord)) {
675+
return true;
676+
}
677+
}
678+
return false;
679+
}
680+
681+
/**
682+
* Gets a page of users starting from the specified {@code pageToken}. Page size is
605683
* limited to 1000 users.
606684
*
607685
* @param pageToken A non-empty page token string, or null to retrieve the first page of users.
@@ -842,8 +920,67 @@ protected Void execute() throws FirebaseAuthException {
842920
}
843921

844922
/**
845-
* Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a
846-
* time. This operation is optimized for bulk imports and will ignore checks on identifier
923+
* Deletes the users specified by the given identifiers.
924+
*
925+
* <p>Deleting a non-existing user does not generate an error (the method is idempotent).
926+
* Non-existing users are considered to be successfully deleted and are therefore included in the
927+
* DeleteUsersResult.getSuccessCount() value.
928+
*
929+
* <p>A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are
930+
* supplied, this method throws an {@link IllegalArgumentException}.
931+
*
932+
* <p>This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded
933+
* error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you
934+
* don't exceed this limit.
935+
*
936+
* @param uids The uids of the users to be deleted. Must have <= 1000 entries.
937+
* @return The total number of successful/failed deletions, as well as the array of errors that
938+
* correspond to the failed deletions.
939+
* @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000
940+
* identifiers are specified.
941+
* @throws FirebaseAuthException If an error occurs while deleting users.
942+
*/
943+
public DeleteUsersResult deleteUsers(List<String> uids) throws FirebaseAuthException {
944+
return deleteUsersOp(uids).call();
945+
}
946+
947+
/**
948+
* Similar to {@link #deleteUsers(List)} but performs the operation asynchronously.
949+
*
950+
* @param uids The uids of the users to be deleted. Must have <= 1000 entries.
951+
* @return An {@code ApiFuture} that resolves to the total number of successful/failed
952+
* deletions, as well as the array of errors that correspond to the failed deletions. If an
953+
* error occurs while deleting the user account, the future throws a
954+
* {@link FirebaseAuthException}.
955+
* @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000
956+
* identifiers are specified.
957+
*/
958+
public ApiFuture<DeleteUsersResult> deleteUsersAsync(List<String> uids) {
959+
return deleteUsersOp(uids).callAsync(firebaseApp);
960+
}
961+
962+
private CallableOperation<DeleteUsersResult, FirebaseAuthException> deleteUsersOp(
963+
final List<String> uids) {
964+
checkNotDestroyed();
965+
checkNotNull(uids, "uids must not be null");
966+
for (String uid : uids) {
967+
UserRecord.checkUid(uid);
968+
}
969+
checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE,
970+
"uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE
971+
+ " entries.");
972+
final FirebaseUserManager userManager = getUserManager();
973+
return new CallableOperation<DeleteUsersResult, FirebaseAuthException>() {
974+
@Override
975+
protected DeleteUsersResult execute() throws FirebaseAuthException {
976+
return userManager.deleteUsers(uids);
977+
}
978+
};
979+
}
980+
981+
/**
982+
* Imports the provided list of users into Firebase Auth. You can import a maximum of 1000 users
983+
* at a time. This operation is optimized for bulk imports and does not check identifier
847984
* uniqueness which could result in duplications.
848985
*
849986
* <p>{@link UserImportOptions} is required to import users with passwords. See

src/main/java/com/google/firebase/auth/FirebaseUserManager.java

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,10 @@
3939
import com.google.firebase.ImplFirebaseTrampolines;
4040
import com.google.firebase.auth.UserRecord.CreateRequest;
4141
import com.google.firebase.auth.UserRecord.UpdateRequest;
42+
import com.google.firebase.auth.internal.BatchDeleteResponse;
4243
import com.google.firebase.auth.internal.DownloadAccountResponse;
44+
import com.google.firebase.auth.internal.GetAccountInfoRequest;
4345
import com.google.firebase.auth.internal.GetAccountInfoResponse;
44-
4546
import com.google.firebase.auth.internal.HttpErrorResponse;
4647
import com.google.firebase.auth.internal.UploadAccountResponse;
4748
import com.google.firebase.internal.ApiClientUtils;
@@ -50,8 +51,11 @@
5051
import com.google.firebase.internal.SdkUtils;
5152

5253
import java.io.IOException;
54+
import java.util.Collection;
55+
import java.util.HashSet;
5356
import java.util.List;
5457
import java.util.Map;
58+
import java.util.Set;
5559

5660
/**
5761
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
@@ -86,6 +90,8 @@ class FirebaseUserManager {
8690
.put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
8791
.build();
8892

93+
static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100;
94+
static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
8995
static final int MAX_LIST_USERS_RESULTS = 1000;
9096
static final int MAX_IMPORT_USERS = 1000;
9197

@@ -171,6 +177,33 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException
171177
return new UserRecord(response.getUsers().get(0), jsonFactory);
172178
}
173179

180+
Set<UserRecord> getAccountInfo(@NonNull Collection<UserIdentifier> identifiers)
181+
throws FirebaseAuthException {
182+
if (identifiers.isEmpty()) {
183+
return new HashSet<UserRecord>();
184+
}
185+
186+
GetAccountInfoRequest payload = new GetAccountInfoRequest();
187+
for (UserIdentifier id : identifiers) {
188+
id.populate(payload);
189+
}
190+
191+
GetAccountInfoResponse response = post(
192+
"/accounts:lookup", payload, GetAccountInfoResponse.class);
193+
194+
if (response == null) {
195+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response");
196+
}
197+
198+
Set<UserRecord> results = new HashSet<>();
199+
if (response.getUsers() != null) {
200+
for (GetAccountInfoResponse.User user : response.getUsers()) {
201+
results.add(new UserRecord(user, jsonFactory));
202+
}
203+
}
204+
return results;
205+
}
206+
174207
String createUser(CreateRequest request) throws FirebaseAuthException {
175208
GenericJson response = post(
176209
"/accounts", request.getProperties(), GenericJson.class);
@@ -200,6 +233,23 @@ void deleteUser(String uid) throws FirebaseAuthException {
200233
}
201234
}
202235

236+
/**
237+
* @pre uids != null
238+
* @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE
239+
*/
240+
DeleteUsersResult deleteUsers(@NonNull List<String> uids) throws FirebaseAuthException {
241+
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
242+
"localIds", uids,
243+
"force", true);
244+
BatchDeleteResponse response = post(
245+
"/accounts:batchDelete", payload, BatchDeleteResponse.class);
246+
if (response == null) {
247+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users");
248+
}
249+
250+
return new DeleteUsersResult(uids.size(), response);
251+
}
252+
203253
DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException {
204254
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
205255
.put("maxResults", maxResults);

0 commit comments

Comments
 (0)