Skip to content

Commit d6b3d49

Browse files
authored
Bulk User Import API (#174)
* Initial code for the importUsers() API * Added documentation and more tests * Added integration tests; Scrypt hash; more documentation * Added more tests * updated test * Adding other hash types * Added the remaining hash algorithms * Renamed to ImportUserRecord * Updated documentation * Some minor cleanup * Updated documentation * Updated documentation; Made hash required in UserImportOptions; Other minor nits * Renamed BasicHash to RepeatableHash; Made rounds range test configurable * Added javadoc comment
1 parent c5f7f2a commit d6b3d49

35 files changed

+2603
-25
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Unreleased
22

3-
-
3+
- [added] Added new `importUsersAsync()` API for bulk importing users
4+
into Firebase Auth.
45

56
# v6.1.0
67

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2018 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 java.util.List;
20+
21+
/**
22+
* Represents an error encountered while importing an {@link ImportUserRecord}.
23+
*/
24+
public final class ErrorInfo {
25+
26+
private final int index;
27+
private final String reason;
28+
29+
ErrorInfo(int index, String reason) {
30+
this.index = index;
31+
this.reason = reason;
32+
}
33+
34+
/**
35+
* The index of the failed user in the list passed to the
36+
* {@link FirebaseAuth#importUsersAsync(List, UserImportOptions)} method.
37+
*
38+
* @return an integer index.
39+
*/
40+
public int getIndex() {
41+
return index;
42+
}
43+
44+
/**
45+
* A string describing the error.
46+
*
47+
* @return A string error message.
48+
*/
49+
public String getReason() {
50+
return reason;
51+
}
52+
}

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

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import com.google.common.base.Strings;
3030
import com.google.firebase.FirebaseApp;
3131
import com.google.firebase.ImplFirebaseTrampolines;
32+
import com.google.firebase.auth.FirebaseUserManager.UserImportRequest;
3233
import com.google.firebase.auth.ListUsersPage.DefaultUserSource;
3334
import com.google.firebase.auth.ListUsersPage.PageFactory;
3435
import com.google.firebase.auth.UserRecord.CreateRequest;
@@ -42,6 +43,7 @@
4243
import com.google.firebase.internal.Nullable;
4344
import java.io.IOException;
4445
import java.security.GeneralSecurityException;
46+
import java.util.List;
4547
import java.util.Map;
4648
import java.util.concurrent.atomic.AtomicBoolean;
4749

@@ -860,6 +862,86 @@ protected Void execute() throws FirebaseAuthException {
860862
};
861863
}
862864

865+
/**
866+
* Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a
867+
* time. This operation is optimized for bulk imports and will ignore checks on identifier
868+
* uniqueness which could result in duplications.
869+
*
870+
* <p>{@link UserImportOptions} is required to import users with passwords. See
871+
* {@link #importUsers(List, UserImportOptions)}.
872+
*
873+
* @param users A non-empty list of users to be imported. Length must not exceed 1000.
874+
* @return A {@link UserImportResult} instance.
875+
* @throws IllegalArgumentException If the users list is null, empty or has more than 1000
876+
* elements. Or if at least one user specifies a password.
877+
* @throws FirebaseAuthException If an error occurs while importing users.
878+
*/
879+
public UserImportResult importUsers(List<ImportUserRecord> users) throws FirebaseAuthException {
880+
return importUsers(users, null);
881+
}
882+
883+
/**
884+
* Imports the provided list of users into Firebase Auth. At most 1000 users can be imported at a
885+
* time. This operation is optimized for bulk imports and will ignore checks on identifier
886+
* uniqueness which could result in duplications.
887+
*
888+
* @param users A non-empty list of users to be imported. Length must not exceed 1000.
889+
* @param options a {@link UserImportOptions} instance or null. Required when importing users
890+
* with passwords.
891+
* @return A {@link UserImportResult} instance.
892+
* @throws IllegalArgumentException If the users list is null, empty or has more than 1000
893+
* elements. Or if at least one user specifies a password, and options is null.
894+
* @throws FirebaseAuthException If an error occurs while importing users.
895+
*/
896+
public UserImportResult importUsers(List<ImportUserRecord> users,
897+
@Nullable UserImportOptions options) throws FirebaseAuthException {
898+
return importUsersOp(users, options).call();
899+
}
900+
901+
/**
902+
* Similar to {@link #importUsers(List)} but performs the operation asynchronously.
903+
*
904+
* @param users A non-empty list of users to be imported. Length must not exceed 1000.
905+
* @return An {@code ApiFuture} which will complete successfully when the user accounts are
906+
* imported. If an error occurs while importing the users, the future throws a
907+
* {@link FirebaseAuthException}.
908+
* @throws IllegalArgumentException If the users list is null, empty or has more than 1000
909+
* elements. Or if at least one user specifies a password.
910+
*/
911+
public ApiFuture<UserImportResult> importUsersAsync(List<ImportUserRecord> users) {
912+
return importUsersAsync(users, null);
913+
}
914+
915+
/**
916+
* Similar to {@link #importUsers(List, UserImportOptions)} but performs the operation
917+
* asynchronously.
918+
*
919+
* @param users A non-empty list of users to be imported. Length must not exceed 1000.
920+
* @param options a {@link UserImportOptions} instance or null. Required when importing users
921+
* with passwords.
922+
* @return An {@code ApiFuture} which will complete successfully when the user accounts are
923+
* imported. If an error occurs while importing the users, the future throws a
924+
* {@link FirebaseAuthException}.
925+
* @throws IllegalArgumentException If the users list is null, empty or has more than 1000
926+
* elements. Or if at least one user specifies a password, and options is null.
927+
*/
928+
public ApiFuture<UserImportResult> importUsersAsync(List<ImportUserRecord> users,
929+
@Nullable UserImportOptions options) {
930+
return importUsersOp(users, options).callAsync(firebaseApp);
931+
}
932+
933+
private CallableOperation<UserImportResult, FirebaseAuthException> importUsersOp(
934+
List<ImportUserRecord> users, UserImportOptions options) {
935+
checkNotDestroyed();
936+
final UserImportRequest request = new UserImportRequest(users, options, jsonFactory);
937+
return new CallableOperation<UserImportResult, FirebaseAuthException>() {
938+
@Override
939+
protected UserImportResult execute() throws FirebaseAuthException {
940+
return userManager.importUsers(request);
941+
}
942+
};
943+
}
944+
863945
@VisibleForTesting
864946
FirebaseUserManager getUserManager() {
865947
return this.userManager;

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.google.api.client.json.GenericJson;
3131
import com.google.api.client.json.JsonFactory;
3232
import com.google.api.client.json.JsonObjectParser;
33+
import com.google.api.client.util.Key;
3334
import com.google.common.annotations.VisibleForTesting;
3435
import com.google.common.base.Strings;
3536
import com.google.common.collect.ImmutableList;
@@ -41,6 +42,7 @@
4142
import com.google.firebase.auth.internal.GetAccountInfoResponse;
4243

4344
import com.google.firebase.auth.internal.HttpErrorResponse;
45+
import com.google.firebase.auth.internal.UploadAccountResponse;
4446
import com.google.firebase.internal.FirebaseRequestInitializer;
4547
import com.google.firebase.internal.NonNull;
4648
import com.google.firebase.internal.SdkUtils;
@@ -82,6 +84,7 @@ class FirebaseUserManager {
8284
.build();
8385

8486
static final int MAX_LIST_USERS_RESULTS = 1000;
87+
static final int MAX_IMPORT_USERS = 1000;
8588

8689
static final List<String> RESERVED_CLAIMS = ImmutableList.of(
8790
"amr", "at_hash", "aud", "auth_time", "azp", "cnf", "c_hash", "exp", "iat",
@@ -195,6 +198,15 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb
195198
return response;
196199
}
197200

201+
UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthException {
202+
checkNotNull(request);
203+
UploadAccountResponse response = post("uploadAccount", request, UploadAccountResponse.class);
204+
if (response == null) {
205+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users.");
206+
}
207+
return new UserImportResult(request.getUsersCount(), response);
208+
}
209+
198210
String createSessionCookie(String idToken,
199211
SessionCookieOptions options) throws FirebaseAuthException {
200212
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
@@ -257,4 +269,38 @@ private void handleHttpError(HttpResponseException e) throws FirebaseAuthExcepti
257269
"Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent());
258270
throw new FirebaseAuthException(INTERNAL_ERROR, msg, e);
259271
}
272+
273+
static class UserImportRequest extends GenericJson {
274+
275+
@Key("users")
276+
private final List<Map<String, Object>> users;
277+
278+
UserImportRequest(List<ImportUserRecord> users, UserImportOptions options,
279+
JsonFactory jsonFactory) {
280+
checkArgument(users != null && !users.isEmpty(), "users must not be null or empty");
281+
checkArgument(users.size() <= FirebaseUserManager.MAX_IMPORT_USERS,
282+
"users list must not contain more than %s items", FirebaseUserManager.MAX_IMPORT_USERS);
283+
284+
boolean hasPassword = false;
285+
ImmutableList.Builder<Map<String, Object>> usersBuilder = ImmutableList.builder();
286+
for (ImportUserRecord user : users) {
287+
if (user.hasPassword()) {
288+
hasPassword = true;
289+
}
290+
usersBuilder.add(user.getProperties(jsonFactory));
291+
}
292+
this.users = usersBuilder.build();
293+
294+
if (hasPassword) {
295+
checkArgument(options != null && options.getHash() != null,
296+
"UserImportHash option is required when at least one user has a password. Provide "
297+
+ "a UserImportHash via UserImportOptions.withHash().");
298+
this.putAll(options.getProperties());
299+
}
300+
}
301+
302+
int getUsersCount() {
303+
return users.size();
304+
}
305+
}
260306
}

0 commit comments

Comments
 (0)