diff --git a/.github/resources/firebase.asc.gpg b/.github/resources/firebase.asc.gpg index a946776c7..8fd7d0769 100644 Binary files a/.github/resources/firebase.asc.gpg and b/.github/resources/firebase.asc.gpg differ diff --git a/.github/resources/settings.xml b/.github/resources/settings.xml index 708bbcb75..4afbaca26 100644 --- a/.github/resources/settings.xml +++ b/.github/resources/settings.xml @@ -21,7 +21,7 @@ gpg - B652FFD3865AF7A75830876F5F55C8F6985BB9DD + A9B90B41060565F56F348F948B6B459CFD695DE8 ${env.GPG_PASSPHRASE} diff --git a/.github/scripts/publish_artifacts.sh b/.github/scripts/publish_artifacts.sh index f4a2f1734..cd1a5b75c 100755 --- a/.github/scripts/publish_artifacts.sh +++ b/.github/scripts/publish_artifacts.sh @@ -20,7 +20,7 @@ set -u gpg --quiet --batch --yes --decrypt --passphrase="${GPG_PRIVATE_KEY}" \ --output firebase.asc .github/resources/firebase.asc.gpg -gpg --import firebase.asc +gpg --import --no-tty --batch --yes firebase.asc # Does the following: # 1. Compiles the source (compile phase) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0e1c307bc..985ce94d6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -91,6 +91,14 @@ jobs: id: preflight run: ./.github/scripts/publish_preflight_check.sh + - name: Publish to Maven Central + run: ./.github/scripts/publish_artifacts.sh + env: + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} + NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} + # We pull this action from a custom fork of a contributor until # https://github.com/actions/create-release/pull/32 is merged. Also note that v1 of # this action does not support the "body" parameter. @@ -105,14 +113,6 @@ jobs: draft: false prerelease: false - - name: Publish to Maven Central - run: ./.github/scripts/publish_artifacts.sh - env: - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} - NEXUS_OSSRH_USERNAME: ${{ secrets.NEXUS_OSSRH_USERNAME }} - NEXUS_OSSRH_PASSWORD: ${{ secrets.NEXUS_OSSRH_PASSWORD }} - # Post to Twitter if explicitly opted-in by adding the label 'release:tweet'. - name: Post to Twitter if: success() && diff --git a/pom.xml b/pom.xml index af25b87d7..6a2989169 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ com.google.firebase firebase-admin - 6.12.3-SNAPSHOT + 6.13.0 jar firebase-admin @@ -59,7 +59,7 @@ UTF-8 UTF-8 ${skipTests} - 4.1.34.Final + 4.1.45.Final @@ -99,6 +99,7 @@ maven-javadoc-plugin + 2.10.4 site @@ -173,6 +174,12 @@ sign + + + --pinentry-mode + loopback + + @@ -303,6 +310,7 @@ maven-javadoc-plugin + 2.10.4 attach-javadocs diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index b9b563e69..f779ae9df 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -38,8 +38,11 @@ import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -762,6 +765,140 @@ protected UserImportResult execute() throws FirebaseAuthException { }; } + /** + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. Must + * have 100 or fewer entries. + * @return The corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + * @throws FirebaseAuthException If an error occurs while retrieving user data. + */ + public GetUsersResult getUsers(@NonNull Collection identifiers) + throws FirebaseAuthException { + return getUsersOp(identifiers).call(); + } + + /** + * Gets the user data corresponding to the specified identifiers. + * + *

There are no ordering guarantees; in particular, the nth entry in the users result list is + * not guaranteed to correspond to the nth entry in the input parameters list. + * + *

A maximum of 100 identifiers may be specified. If more than 100 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + * @param identifiers The identifiers used to indicate which user records should be returned. + * Must have 100 or fewer entries. + * @return An {@code ApiFuture} that resolves to the corresponding user records. + * @throws IllegalArgumentException If any of the identifiers are invalid or if more than 100 + * identifiers are specified. + * @throws NullPointerException If the identifiers parameter is null. + */ + public ApiFuture getUsersAsync(@NonNull Collection identifiers) { + return getUsersOp(identifiers).callAsync(firebaseApp); + } + + private CallableOperation getUsersOp( + @NonNull final Collection identifiers) { + checkNotDestroyed(); + checkNotNull(identifiers, "identifiers must not be null"); + checkArgument(identifiers.size() <= FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE, + "identifiers parameter must have <= " + FirebaseUserManager.MAX_GET_ACCOUNTS_BATCH_SIZE + + " entries."); + + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected GetUsersResult execute() throws FirebaseAuthException { + Set users = userManager.getAccountInfo(identifiers); + Set notFound = new HashSet<>(); + for (UserIdentifier id : identifiers) { + if (!isUserFound(id, users)) { + notFound.add(id); + } + } + return new GetUsersResult(users, notFound); + } + }; + } + + private boolean isUserFound(UserIdentifier id, Collection userRecords) { + for (UserRecord userRecord : userRecords) { + if (id.matches(userRecord)) { + return true; + } + } + return false; + } + + /** + * Deletes the users specified by the given identifiers. + * + *

Deleting a non-existing user does not generate an error (the method is idempotent). + * Non-existing users are considered to be successfully deleted and are therefore included in the + * DeleteUsersResult.getSuccessCount() value. + * + *

A maximum of 1000 identifiers may be supplied. If more than 1000 identifiers are + * supplied, this method throws an {@link IllegalArgumentException}. + * + *

This API has a rate limit of 1 QPS. Exceeding the limit may result in a quota exceeded + * error. If you want to delete more than 1000 users, we suggest adding a delay to ensure you + * don't exceed this limit. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return The total number of successful/failed deletions, as well as the array of errors that + * correspond to the failed deletions. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + * @throws FirebaseAuthException If an error occurs while deleting users. + */ + public DeleteUsersResult deleteUsers(List uids) throws FirebaseAuthException { + return deleteUsersOp(uids).call(); + } + + /** + * Similar to {@link #deleteUsers(List)} but performs the operation asynchronously. + * + * @param uids The uids of the users to be deleted. Must have <= 1000 entries. + * @return An {@code ApiFuture} that resolves to the total number of successful/failed + * deletions, as well as the array of errors that correspond to the failed deletions. If an + * error occurs while deleting the user account, the future throws a + * {@link FirebaseAuthException}. + * @throw IllegalArgumentException If any of the identifiers are invalid or if more than 1000 + * identifiers are specified. + */ + public ApiFuture deleteUsersAsync(List uids) { + return deleteUsersOp(uids).callAsync(firebaseApp); + } + + private CallableOperation deleteUsersOp( + final List uids) { + checkNotDestroyed(); + checkNotNull(uids, "uids must not be null"); + for (String uid : uids) { + UserRecord.checkUid(uid); + } + checkArgument(uids.size() <= FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE, + "uids parameter must have <= " + FirebaseUserManager.MAX_DELETE_ACCOUNTS_BATCH_SIZE + + " entries."); + final FirebaseUserManager userManager = getUserManager(); + return new CallableOperation() { + @Override + protected DeleteUsersResult execute() throws FirebaseAuthException { + return userManager.deleteUsers(uids); + } + }; + } + /** * Generates the out-of-band email action link for password reset flows for the specified email * address. diff --git a/src/main/java/com/google/firebase/auth/DeleteUsersResult.java b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java new file mode 100644 index 000000000..e8ca7dba5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/DeleteUsersResult.java @@ -0,0 +1,74 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.auth.internal.BatchDeleteResponse; +import com.google.firebase.internal.NonNull; +import java.util.List; + +/** + * Represents the result of the {@link FirebaseAuth#deleteUsersAsync(List)} API. + */ +public final class DeleteUsersResult { + + private final int successCount; + private final List errors; + + DeleteUsersResult(int users, BatchDeleteResponse response) { + ImmutableList.Builder errorsBuilder = ImmutableList.builder(); + List responseErrors = response.getErrors(); + if (responseErrors != null) { + checkArgument(users >= responseErrors.size()); + for (BatchDeleteResponse.ErrorInfo error : responseErrors) { + errorsBuilder.add(new ErrorInfo(error.getIndex(), error.getMessage())); + } + } + errors = errorsBuilder.build(); + successCount = users - errors.size(); + } + + /** + * Returns the number of users that were deleted successfully (possibly zero). Users that did not + * exist prior to calling {@link FirebaseAuth#deleteUsersAsync(List)} are considered to be + * successfully deleted. + */ + public int getSuccessCount() { + return successCount; + } + + /** + * Returns the number of users that failed to be deleted (possibly zero). + */ + public int getFailureCount() { + return errors.size(); + } + + /** + * A list of {@link ErrorInfo} instances describing the errors that were encountered during + * the deletion. Length of this list is equal to the return value of + * {@link #getFailureCount()}. + * + * @return A non-null list (possibly empty). + */ + @NonNull + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/google/firebase/auth/EmailIdentifier.java b/src/main/java/com/google/firebase/auth/EmailIdentifier.java new file mode 100644 index 000000000..8e729c220 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/EmailIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by email. + * + * @see {FirebaseAuth#getUsers} + */ +public final class EmailIdentifier extends UserIdentifier { + private final String email; + + public EmailIdentifier(@NonNull String email) { + UserRecord.checkEmail(email); + this.email = email; + } + + @Override + public String toString() { + return "EmailIdentifier(" + email + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addEmail(email); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return email.equals(userRecord.getEmail()); + } +} diff --git a/src/main/java/com/google/firebase/auth/FirebaseAuth.java b/src/main/java/com/google/firebase/auth/FirebaseAuth.java index ce8c30a11..ff52acad7 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/FirebaseAuth.java @@ -26,7 +26,6 @@ import com.google.common.base.Supplier; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.AbstractFirebaseAuth.Builder; import com.google.firebase.auth.internal.FirebaseTokenFactory; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.FirebaseService; @@ -41,7 +40,7 @@ * custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or * creating new FirebaseApp instances that are scoped to a particular authentication UID. */ -public class FirebaseAuth extends AbstractFirebaseAuth { +public final class FirebaseAuth extends AbstractFirebaseAuth { private static final String SERVICE_ID = FirebaseAuth.class.getName(); diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index 4e6ce0eab..e5d39b87f 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -39,8 +39,9 @@ import com.google.common.collect.ImmutableSortedSet; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; -import com.google.firebase.auth.UserRecord; +import com.google.firebase.auth.internal.BatchDeleteResponse; import com.google.firebase.auth.internal.DownloadAccountResponse; +import com.google.firebase.auth.internal.GetAccountInfoRequest; import com.google.firebase.auth.internal.GetAccountInfoResponse; import com.google.firebase.auth.internal.HttpErrorResponse; import com.google.firebase.auth.internal.ListTenantsResponse; @@ -51,8 +52,11 @@ import com.google.firebase.internal.SdkUtils; import java.io.IOException; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its @@ -94,6 +98,8 @@ class FirebaseUserManager { .build(); static final int MAX_LIST_TENANTS_RESULTS = 1000; + static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100; + static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000; static final int MAX_LIST_USERS_RESULTS = 1000; static final int MAX_IMPORT_USERS = 1000; @@ -175,6 +181,33 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException return new UserRecord(response.getUsers().get(0), jsonFactory); } + Set getAccountInfo(@NonNull Collection identifiers) + throws FirebaseAuthException { + if (identifiers.isEmpty()) { + return new HashSet<>(); + } + + GetAccountInfoRequest payload = new GetAccountInfoRequest(); + for (UserIdentifier id : identifiers) { + id.populate(payload); + } + + GetAccountInfoResponse response = post( + "/accounts:lookup", payload, GetAccountInfoResponse.class); + + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + } + + Set results = new HashSet<>(); + if (response.getUsers() != null) { + for (GetAccountInfoResponse.User user : response.getUsers()) { + results.add(new UserRecord(user, jsonFactory)); + } + } + return results; + } + String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException { GenericJson response = post( "/accounts", request.getProperties(), GenericJson.class); @@ -205,6 +238,23 @@ void deleteUser(String uid) throws FirebaseAuthException { } } + /** + * @pre uids != null + * @pre uids.size() <= MAX_DELETE_ACCOUNTS_BATCH_SIZE + */ + DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthException { + final Map payload = ImmutableMap.of( + "localIds", uids, + "force", true); + BatchDeleteResponse response = post( + "/accounts:batchDelete", payload, BatchDeleteResponse.class); + if (response == null) { + throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + } + + return new DeleteUsersResult(uids.size(), response); + } + DownloadAccountResponse listUsers(int maxResults, String pageToken) throws FirebaseAuthException { ImmutableMap.Builder builder = ImmutableMap.builder() .put("maxResults", maxResults); diff --git a/src/main/java/com/google/firebase/auth/GetUsersResult.java b/src/main/java/com/google/firebase/auth/GetUsersResult.java new file mode 100644 index 000000000..3ceec01cb --- /dev/null +++ b/src/main/java/com/google/firebase/auth/GetUsersResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.firebase.internal.NonNull; +import java.util.Set; + +/** + * Represents the result of the {@link FirebaseAuth#getUsersAsync(Collection)} API. + */ +public final class GetUsersResult { + private final Set users; + private final Set notFound; + + GetUsersResult(@NonNull Set users, @NonNull Set notFound) { + this.users = checkNotNull(users); + this.notFound = checkNotNull(notFound); + } + + /** + * Set of user records corresponding to the set of users that were requested. Only users + * that were found are listed here. The result set is unordered. + */ + @NonNull + public Set getUsers() { + return this.users; + } + + /** + * Set of identifiers that were requested, but not found. + */ + @NonNull + public Set getNotFound() { + return this.notFound; + } +} diff --git a/src/main/java/com/google/firebase/auth/PhoneIdentifier.java b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java new file mode 100644 index 000000000..bdc84fe92 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/PhoneIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by phone number. + * + * @see {FirebaseAuth#getUsers} + */ +public final class PhoneIdentifier extends UserIdentifier { + private final String phoneNumber; + + public PhoneIdentifier(@NonNull String phoneNumber) { + UserRecord.checkPhoneNumber(phoneNumber); + this.phoneNumber = phoneNumber; + } + + @Override + public String toString() { + return "PhoneIdentifier(" + phoneNumber + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addPhoneNumber(phoneNumber); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return phoneNumber.equals(userRecord.getPhoneNumber()); + } +} diff --git a/src/main/java/com/google/firebase/auth/ProviderIdentifier.java b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java new file mode 100644 index 000000000..25e00026d --- /dev/null +++ b/src/main/java/com/google/firebase/auth/ProviderIdentifier.java @@ -0,0 +1,56 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by provider. + * + * @see {FirebaseAuth#getUsers} + */ +public final class ProviderIdentifier extends UserIdentifier { + private final String providerId; + private final String providerUid; + + public ProviderIdentifier(@NonNull String providerId, @NonNull String providerUid) { + UserRecord.checkProvider(providerId, providerUid); + this.providerId = providerId; + this.providerUid = providerUid; + } + + @Override + public String toString() { + return "ProviderIdentifier(" + providerId + ", " + providerUid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addFederatedUserId(providerId, providerUid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + for (UserInfo userInfo : userRecord.getProviderData()) { + if (providerId.equals(userInfo.getProviderId()) && providerUid.equals(userInfo.getUid())) { + return true; + } + } + return false; + } +} diff --git a/src/main/java/com/google/firebase/auth/UidIdentifier.java b/src/main/java/com/google/firebase/auth/UidIdentifier.java new file mode 100644 index 000000000..a4f7069d9 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UidIdentifier.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Used for looking up an account by uid. + * + * @see {FirebaseAuth#getUsers} + */ +public final class UidIdentifier extends UserIdentifier { + private final String uid; + + public UidIdentifier(@NonNull String uid) { + UserRecord.checkUid(uid); + this.uid = uid; + } + + @Override + public String toString() { + return "UidIdentifier(" + uid + ")"; + } + + @Override + void populate(@NonNull GetAccountInfoRequest payload) { + payload.addUid(uid); + } + + @Override + boolean matches(@NonNull UserRecord userRecord) { + return uid.equals(userRecord.getUid()); + } +} diff --git a/src/main/java/com/google/firebase/auth/UserIdentifier.java b/src/main/java/com/google/firebase/auth/UserIdentifier.java new file mode 100644 index 000000000..7ec9699e6 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/UserIdentifier.java @@ -0,0 +1,31 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import com.google.firebase.auth.internal.GetAccountInfoRequest; +import com.google.firebase.internal.NonNull; + +/** + * Identifies a user to be looked up. + */ +public abstract class UserIdentifier { + public abstract String toString(); + + abstract void populate(@NonNull GetAccountInfoRequest payload); + + abstract boolean matches(@NonNull UserRecord userRecord); +} diff --git a/src/main/java/com/google/firebase/auth/UserMetadata.java b/src/main/java/com/google/firebase/auth/UserMetadata.java index a2872371f..85a24a0fd 100644 --- a/src/main/java/com/google/firebase/auth/UserMetadata.java +++ b/src/main/java/com/google/firebase/auth/UserMetadata.java @@ -23,14 +23,16 @@ public class UserMetadata { private final long creationTimestamp; private final long lastSignInTimestamp; + private final long lastRefreshTimestamp; public UserMetadata(long creationTimestamp) { - this(creationTimestamp, 0L); + this(creationTimestamp, 0L, 0L); } - public UserMetadata(long creationTimestamp, long lastSignInTimestamp) { + public UserMetadata(long creationTimestamp, long lastSignInTimestamp, long lastRefreshTimestamp) { this.creationTimestamp = creationTimestamp; this.lastSignInTimestamp = lastSignInTimestamp; + this.lastRefreshTimestamp = lastRefreshTimestamp; } /** @@ -50,4 +52,13 @@ public long getCreationTimestamp() { public long getLastSignInTimestamp() { return lastSignInTimestamp; } + + /** + * Returns the time at which the user was last active (ID token refreshed). + *  + * @return Milliseconds since epoch timestamp, or 0 if the user was never active. + */ + public long getLastRefreshTimestamp() { + return lastRefreshTimestamp; + } } diff --git a/src/main/java/com/google/firebase/auth/UserRecord.java b/src/main/java/com/google/firebase/auth/UserRecord.java index e8896de92..0af08f65b 100644 --- a/src/main/java/com/google/firebase/auth/UserRecord.java +++ b/src/main/java/com/google/firebase/auth/UserRecord.java @@ -20,6 +20,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.DateTime; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -82,7 +83,15 @@ public class UserRecord implements UserInfo { } } this.tokensValidAfterTimestamp = response.getValidSince() * 1000; - this.userMetadata = new UserMetadata(response.getCreatedAt(), response.getLastLoginAt()); + + String lastRefreshAtRfc3339 = response.getLastRefreshAt(); + long lastRefreshAtMillis = 0; + if (!Strings.isNullOrEmpty(lastRefreshAtRfc3339)) { + lastRefreshAtMillis = DateTime.parseRfc3339(lastRefreshAtRfc3339).getValue(); + } + + this.userMetadata = new UserMetadata( + response.getCreatedAt(), response.getLastLoginAt(), lastRefreshAtMillis); this.customClaims = parseCustomClaims(response.getCustomClaims(), jsonFactory); } @@ -259,6 +268,11 @@ static void checkPhoneNumber(String phoneNumber) { "phone number must be a valid, E.164 compliant identifier starting with a '+' sign"); } + static void checkProvider(String providerId, String providerUid) { + checkArgument(!Strings.isNullOrEmpty(providerId), "providerId must be a non-empty string"); + checkArgument(!Strings.isNullOrEmpty(providerUid), "providerUid must be a non-empty string"); + } + static void checkUrl(String photoUrl) { checkArgument(!Strings.isNullOrEmpty(photoUrl), "url cannot be null or empty"); try { diff --git a/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java new file mode 100644 index 000000000..728cf6358 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/BatchDeleteResponse.java @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import java.util.List; + +/** + * Represents the response from Google identity Toolkit for a batch delete request. + */ +public class BatchDeleteResponse { + + @Key("errors") + private List errors; + + public List getErrors() { + return errors; + } + + public static class ErrorInfo { + @Key("index") + private int index; + + @Key("message") + private String message; + + // A 'localId' field also exists here, but is not currently exposed in the Admin SDK. + + public int getIndex() { + return index; + } + + public String getMessage() { + return message; + } + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java new file mode 100644 index 000000000..67c4d0ee7 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoRequest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth.internal; + +import com.google.api.client.util.Key; +import java.util.ArrayList; +import java.util.List; + +/** + * Represents the request to look up account information. + */ +public final class GetAccountInfoRequest { + + @Key("localId") + private List uids = null; + + @Key("email") + private List emails = null; + + @Key("phoneNumber") + private List phoneNumbers = null; + + @Key("federatedUserId") + private List federatedUserIds = null; + + private static final class FederatedUserId { + @Key("providerId") + private String providerId = null; + + @Key("rawId") + private String rawId = null; + + FederatedUserId(String providerId, String rawId) { + this.providerId = providerId; + this.rawId = rawId; + } + } + + public void addUid(String uid) { + if (uids == null) { + uids = new ArrayList<>(); + } + uids.add(uid); + } + + public void addEmail(String email) { + if (emails == null) { + emails = new ArrayList<>(); + } + emails.add(email); + } + + public void addPhoneNumber(String phoneNumber) { + if (phoneNumbers == null) { + phoneNumbers = new ArrayList<>(); + } + phoneNumbers.add(phoneNumber); + } + + public void addFederatedUserId(String providerId, String providerUid) { + if (federatedUserIds == null) { + federatedUserIds = new ArrayList<>(); + } + federatedUserIds.add(new FederatedUserId(providerId, providerUid)); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java index 0c7a92e0e..7bde3eb39 100644 --- a/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/GetAccountInfoResponse.java @@ -76,6 +76,9 @@ public static class User { @Key("lastLoginAt") private long lastLoginAt; + @Key("lastRefreshAt") + private String lastRefreshAt; + @Key("validSince") private long validSince; @@ -126,6 +129,10 @@ public long getLastLoginAt() { return lastLoginAt; } + public String getLastRefreshAt() { + return lastRefreshAt; + } + public long getValidSince() { return validSince; } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index bb9838938..c41772293 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -136,6 +136,78 @@ public void testDeleteNonExistingUser() throws Exception { } } + @Test + public void testDeleteUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + UserRecord user2 = newUserWithParams(); + UserRecord user3 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), user2.getUid(), user3.getUid())) + .get(); + + assertEquals(3, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync( + ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier(user2.getUid()), new UidIdentifier(user3.getUid()))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(3, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteExistingAndNonExistingUsers() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult deleteUsersResult = + slowDeleteUsersAsync(ImmutableList.of(user1.getUid(), "uid-that-doesnt-exist")).get(); + + assertEquals(2, deleteUsersResult.getSuccessCount()); + assertEquals(0, deleteUsersResult.getFailureCount()); + assertTrue(deleteUsersResult.getErrors().isEmpty()); + + GetUsersResult getUsersResult = + auth.getUsersAsync(ImmutableList.of(new UidIdentifier(user1.getUid()), + new UidIdentifier("uid-that-doesnt-exist"))) + .get(); + + assertTrue(getUsersResult.getUsers().isEmpty()); + assertEquals(2, getUsersResult.getNotFound().size()); + } + + @Test + public void testDeleteUsersIsIdempotent() throws Exception { + UserRecord user1 = newUserWithParams(); + + DeleteUsersResult result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + + // Delete the user again to ensure that everything still counts as a success. + result = slowDeleteUsersAsync(ImmutableList.of(user1.getUid())).get(); + + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + + /** + * The {@code batchDelete} endpoint has a rate limit of 1 QPS. Use this test + * helper to ensure you don't exceed the quota. + */ + // TODO(rsgowman): When/if the rate limit is relaxed, eliminate this helper. + private ApiFuture slowDeleteUsersAsync(List uids) throws Exception { + TimeUnit.SECONDS.sleep(1); + return auth.deleteUsersAsync(uids); + } + @Test public void testCreateUserWithParams() throws Exception { RandomUser randomUser = RandomUser.create(); @@ -243,6 +315,35 @@ public void testUserLifecycle() throws Exception { assertUserDoesNotExist(auth, userRecord.getUid()); } + @Test + public void testLastRefreshTime() throws Exception { + RandomUser user = RandomUser.create(); + UserRecord newUserRecord = auth.createUser(new UserRecord.CreateRequest() + .setUid(user.uid) + .setEmail(user.email) + .setEmailVerified(false) + .setPassword("password")); + + try { + // New users should not have a lastRefreshTimestamp set. + assertEquals(0, newUserRecord.getUserMetadata().getLastRefreshTimestamp()); + + // Login to cause the lastRefreshTimestamp to be set. + signInWithPassword(newUserRecord.getEmail(), "password"); + + UserRecord userRecord = auth.getUser(newUserRecord.getUid()); + + // Ensure the lastRefreshTimestamp is approximately "now" (with a tollerance of 10 minutes). + long now = System.currentTimeMillis(); + long tollerance = TimeUnit.MINUTES.toMillis(10); + long lastRefreshTimestamp = userRecord.getUserMetadata().getLastRefreshTimestamp(); + assertTrue(now - tollerance <= lastRefreshTimestamp); + assertTrue(lastRefreshTimestamp <= now + tollerance); + } finally { + auth.deleteUser(newUserRecord.getUid()); + } + } + @Test public void testListUsers() throws Exception { final List uids = new ArrayList<>(); @@ -974,7 +1075,7 @@ private Map parseLinkParameters(String link) throws Exception { return result; } - private String randomPhoneNumber() { + static String randomPhoneNumber() { Random random = new Random(); StringBuilder builder = new StringBuilder("+1"); for (int i = 0; i < 10; i++) { @@ -1013,7 +1114,7 @@ private String signInWithPassword(String email, String password) throws IOExcept GenericUrl url = new GenericUrl(VERIFY_PASSWORD_URL + "?key=" + IntegrationTestUtils.getApiKey()); Map content = ImmutableMap.of( - "email", email, "password", password); + "email", email, "password", password, "returnSecureToken", true); HttpRequest request = transport.createRequestFactory().buildPostRequest(url, new JsonHttpContent(jsonFactory, content)); request.setParser(new JsonObjectParser(jsonFactory)); @@ -1072,9 +1173,9 @@ private void checkRecreateUser(String uid) throws Exception { } } - private static class RandomUser { - private final String uid; - private final String email; + static class RandomUser { + final String uid; + final String email; private RandomUser(String uid, String email) { this.uid = uid; @@ -1100,4 +1201,21 @@ private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, St ((FirebaseAuthException) e.getCause()).getErrorCode()); } } + + static UserRecord newUserWithParams() throws Exception { + return newUserWithParams(auth); + } + + static UserRecord newUserWithParams(FirebaseAuth auth) throws Exception { + // TODO(rsgowman): This function could be used throughout this file (similar to the other + // ports). + RandomUser randomUser = RandomUser.create(); + return auth.createUser(new UserRecord.CreateRequest() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(randomPhoneNumber()) + .setDisplayName("Random User") + .setPhotoUrl("https://example.com/photo.png") + .setPassword("password")); + } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 20eb56222..ac8860c72 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -35,6 +35,7 @@ import com.google.api.client.testing.http.MockHttpTransport; import com.google.api.client.testing.http.MockLowLevelHttpResponse; import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Strings; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -52,6 +53,8 @@ import java.io.IOException; import java.math.BigDecimal; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Map; @@ -170,6 +173,153 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } } + @Test + public void testGetUsersExceeds100() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List identifiers = new ArrayList<>(); + for (int i = 0; i < 101; i++) { + identifiers.add(new UidIdentifier("uid_" + i)); + } + + try { + FirebaseAuth.getInstance().getUsers(identifiers); + fail("No error thrown for too many supplied identifiers"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testGetUsersNull() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().getUsers(null); + fail("No error thrown for null identifiers"); + } catch (NullPointerException expected) { + // expected + } + } + + @Test + public void testGetUsersEmpty() throws Exception { + initializeAppForUserManagement(); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(new ArrayList()); + assertTrue(result.getUsers().isEmpty()); + assertTrue(result.getNotFound().isEmpty()); + } + + @Test + public void testGetUsersAllNonExisting() throws Exception { + initializeAppForUserManagement("{ \"users\": [] }"); + List ids = ImmutableList.of( + new UidIdentifier("id-that-doesnt-exist")); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + assertTrue(result.getUsers().isEmpty()); + assertEquals(ids.size(), result.getNotFound().size()); + assertTrue(result.getNotFound().containsAll(ids)); + } + + @Test + public void testGetUsersMultipleIdentifierTypes() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'users': [{ " + + " 'localId': 'uid1', " + + " 'email': 'user1@example.com', " + + " 'phoneNumber': '+15555550001' " + + " }, { " + + " 'localId': 'uid2', " + + " 'email': 'user2@example.com', " + + " 'phoneNumber': '+15555550002' " + + " }, { " + + " 'localId': 'uid3', " + + " 'email': 'user3@example.com', " + + " 'phoneNumber': '+15555550003' " + + " }, { " + + " 'localId': 'uid4', " + + " 'email': 'user4@example.com', " + + " 'phoneNumber': '+15555550004', " + + " 'providerUserInfo': [{ " + + " 'providerId': 'google.com', " + + " 'rawId': 'google_uid4' " + + " }] " + + " }] " + + "} " + ).replace("'", "\"")); + + UidIdentifier doesntExist = new UidIdentifier("this-uid-doesnt-exist"); + List ids = ImmutableList.of( + new UidIdentifier("uid1"), + new EmailIdentifier("user2@example.com"), + new PhoneIdentifier("+15555550003"), + new ProviderIdentifier("google.com", "google_uid4"), + doesntExist); + GetUsersResult result = FirebaseAuth.getInstance().getUsers(ids); + Collection uids = userRecordsToUids(result.getUsers()); + assertTrue(uids.containsAll(ImmutableList.of("uid1", "uid2", "uid3", "uid4"))); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExist)); + } + + private Collection userRecordsToUids(Collection userRecords) { + Collection uids = new HashSet<>(); + for (UserRecord userRecord : userRecords) { + uids.add(userRecord.getUid()); + } + return uids; + } + + @Test + public void testInvalidUidIdentifier() throws Exception { + try { + new UidIdentifier("too long " + Strings.repeat(".", 128)); + fail("No error thrown for invalid uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidEmailIdentifier() throws Exception { + try { + new EmailIdentifier("invalid email addr"); + fail("No error thrown for invalid email"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidPhoneIdentifier() throws Exception { + try { + new PhoneIdentifier("invalid phone number"); + fail("No error thrown for invalid phone number"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testInvalidProviderIdentifier() throws Exception { + try { + new ProviderIdentifier("", "valid-uid"); + fail("No error thrown for invalid provider id"); + } catch (IllegalArgumentException expected) { + // expected + } + + try { + new ProviderIdentifier("valid-id", ""); + fail("No error thrown for invalid provider uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + @Test public void testListUsers() throws Exception { final TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -276,6 +426,81 @@ public void testDeleteUser() throws Exception { checkRequestHeaders(interceptor); } + @Test + public void testDeleteUsersExceeds1000() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + List ids = new ArrayList<>(); + for (int i = 0; i < 1001; i++) { + ids.add("id" + i); + } + try { + FirebaseAuth.getInstance().deleteUsersAsync(ids); + fail("No error thrown for too many uids"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersInvalidId() throws Exception { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .build()); + try { + FirebaseAuth.getInstance().deleteUsersAsync( + ImmutableList.of("too long " + Strings.repeat(".", 128))); + fail("No error thrown for too long uid"); + } catch (IllegalArgumentException expected) { + // expected + } + } + + @Test + public void testDeleteUsersIndexesErrorsCorrectly() throws Exception { + initializeAppForUserManagement(("" + + "{ " + + " 'errors': [{ " + + " 'index': 0, " + + " 'localId': 'uid1', " + + " 'message': 'NOT_DISABLED : Disable the account before batch deletion.' " + + " }, { " + + " 'index': 2, " + + " 'localId': 'uid3', " + + " 'message': 'something awful' " + + " }] " + + "} " + ).replace("'", "\"")); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3", "uid4" + )).get(); + + assertEquals(2, result.getSuccessCount()); + assertEquals(2, result.getFailureCount()); + assertEquals(2, result.getErrors().size()); + assertEquals(0, result.getErrors().get(0).getIndex()); + assertEquals( + "NOT_DISABLED : Disable the account before batch deletion.", + result.getErrors().get(0).getReason()); + assertEquals(2, result.getErrors().get(1).getIndex()); + assertEquals("something awful", result.getErrors().get(1).getReason()); + } + + @Test + public void testDeleteUsersSuccess() throws Exception { + initializeAppForUserManagement("{}"); + + DeleteUsersResult result = FirebaseAuth.getInstance().deleteUsersAsync(ImmutableList.of( + "uid1", "uid2", "uid3" + )).get(); + + assertEquals(3, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + assertTrue(result.getErrors().isEmpty()); + } + @Test public void testImportUsers() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); diff --git a/src/test/java/com/google/firebase/auth/GetUsersIT.java b/src/test/java/com/google/firebase/auth/GetUsersIT.java new file mode 100644 index 000000000..efe2f783f --- /dev/null +++ b/src/test/java/com/google/firebase/auth/GetUsersIT.java @@ -0,0 +1,152 @@ +/* + * Copyright 2020 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.auth; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import com.google.common.collect.ImmutableList; +import com.google.firebase.FirebaseApp; +import com.google.firebase.testing.IntegrationTestUtils; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +public class GetUsersIT { + private static FirebaseAuth auth; + private static UserRecord testUser1; + private static UserRecord testUser2; + private static UserRecord testUser3; + private static String importUserUid; + + @BeforeClass + public static void setUpClass() throws Exception { + FirebaseApp masterApp = IntegrationTestUtils.ensureDefaultApp(); + auth = FirebaseAuth.getInstance(masterApp); + + testUser1 = FirebaseAuthIT.newUserWithParams(auth); + testUser2 = FirebaseAuthIT.newUserWithParams(auth); + testUser3 = FirebaseAuthIT.newUserWithParams(auth); + + FirebaseAuthIT.RandomUser randomUser = FirebaseAuthIT.RandomUser.create(); + importUserUid = randomUser.uid; + String phone = FirebaseAuthIT.randomPhoneNumber(); + UserImportResult result = auth.importUsers(ImmutableList.of( + ImportUserRecord.builder() + .setUid(randomUser.uid) + .setEmail(randomUser.email) + .setPhoneNumber(phone) + .addUserProvider( + UserProvider.builder() + .setProviderId("google.com") + .setUid("google_" + randomUser.uid) + .build()) + .build() + )); + assertEquals(1, result.getSuccessCount()); + assertEquals(0, result.getFailureCount()); + } + + @AfterClass + public static void cleanup() throws Exception { + // TODO(rsgowman): deleteUsers (plural) would make more sense here, but it's currently rate + // limited to 1qps. When/if that's relaxed, change this to just delete them all at once. + auth.deleteUser(testUser1.getUid()); + auth.deleteUser(testUser2.getUid()); + auth.deleteUser(testUser3.getUid()); + auth.deleteUser(importUserUid); + } + + @Test + public void testVariousIdentifiers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new EmailIdentifier(testUser2.getEmail()), + new PhoneIdentifier(testUser3.getPhoneNumber()), + new ProviderIdentifier("google.com", "google_" + importUserUid) + )).get(); + + Collection expectedUids = ImmutableList.of( + testUser1.getUid(), testUser2.getUid(), testUser3.getUid(), importUserUid); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + @Test + public void testIgnoresNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + doesntExistId, + new UidIdentifier(testUser3.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid(), testUser3.getUid()); + + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testOnlyNonExistingUsers() throws Exception { + UidIdentifier doesntExistId = new UidIdentifier("uid_that_doesnt_exist"); + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + doesntExistId + )).get(); + + assertEquals(0, result.getUsers().size()); + assertEquals(1, result.getNotFound().size()); + assertTrue(result.getNotFound().contains(doesntExistId)); + } + + @Test + public void testDedupsDuplicateUsers() throws Exception { + GetUsersResult result = auth.getUsersAsync(ImmutableList.of( + new UidIdentifier(testUser1.getUid()), + new UidIdentifier(testUser1.getUid()) + )).get(); + + Collection expectedUids = ImmutableList.of(testUser1.getUid()); + + assertEquals(1, result.getUsers().size()); + assertTrue(sameUsers(result.getUsers(), expectedUids)); + assertEquals(0, result.getNotFound().size()); + } + + /** + * Checks to see if the userRecords collection contains the given uids. + * + *

Behaviour is undefined if there are duplicate entries in either of the parameters. + */ + private boolean sameUsers(Collection userRecords, Collection uids) { + if (userRecords.size() != uids.size()) { + return false; + } + + for (UserRecord userRecord : userRecords) { + if (!uids.contains(userRecord.getUid())) { + return false; + } + } + + return true; + } +} diff --git a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java index 011a5cc04..e2ae36c09 100644 --- a/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java +++ b/src/test/java/com/google/firebase/auth/ImportUserRecordTest.java @@ -62,7 +62,7 @@ public void testAllProperties() throws IOException { .setDisplayName("Test User") .setPhotoUrl("https://test.com/user.png") .setPhoneNumber("+1234567890") - .setUserMetadata(new UserMetadata(date.getTime(), date.getTime())) + .setUserMetadata(new UserMetadata(date.getTime(), date.getTime(), date.getTime())) .setDisabled(false) .setEmailVerified(true) .setPasswordHash("password".getBytes())