diff --git a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java index a864c4f6c..35f222793 100644 --- a/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java +++ b/src/main/java/com/google/firebase/auth/AbstractFirebaseAuth.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkState; import com.google.api.client.json.JsonFactory; +import com.google.api.client.util.Clock; import com.google.api.core.ApiFuture; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; @@ -64,7 +65,7 @@ public abstract class AbstractFirebaseAuth { private final Supplier userManager; private final JsonFactory jsonFactory; - AbstractFirebaseAuth(Builder builder) { + protected AbstractFirebaseAuth(Builder builder) { this.firebaseApp = checkNotNull(builder.firebaseApp); this.tokenFactory = threadSafeMemoize(builder.tokenFactory); this.idTokenVerifier = threadSafeMemoize(builder.idTokenVerifier); @@ -73,6 +74,43 @@ public abstract class AbstractFirebaseAuth { this.jsonFactory = builder.firebaseApp.getOptions().getJsonFactory(); } + protected static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { + return AbstractFirebaseAuth.builder() + .setFirebaseApp(app) + .setTokenFactory( + new Supplier() { + @Override + public FirebaseTokenFactory get() { + return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); + } + }) + .setIdTokenVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); + } + }) + .setCookieVerifier( + new Supplier() { + @Override + public FirebaseTokenVerifier get() { + return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); + } + }) + .setUserManager( + new Supplier() { + @Override + public FirebaseUserManager get() { + return FirebaseUserManager + .builder() + .setFirebaseApp(app) + .setTenantId(tenantId) + .build(); + } + }); + } + /** * Creates a Firebase custom token for the given UID. This token can then be sent back to a client * application to be used with the tenantManager; - private final AtomicBoolean tenantManagerCreated = new AtomicBoolean(false); FirebaseAuth(final Builder builder) { super(builder); tenantManager = threadSafeMemoize(new Supplier() { @Override public TenantManager get() { - tenantManagerCreated.set(true); - return new TenantManager(builder.firebaseApp, getUserManager()); + return new TenantManager(builder.firebaseApp); } }); } @@ -213,12 +211,7 @@ FirebaseTokenVerifier getSessionCookieVerifier(boolean checkRevoked) { } @Override - protected void doDestroy() { - // Only destroy the tenant manager if it has been created. - if (tenantManagerCreated.get()) { - getTenantManager().destroy(); - } - } + protected void doDestroy() { } private static FirebaseAuth fromApp(final FirebaseApp app) { return new FirebaseAuth( diff --git a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java index a1c1f6102..e1a5a9a19 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java +++ b/src/main/java/com/google/firebase/auth/FirebaseTokenVerifierImpl.java @@ -46,6 +46,7 @@ final class FirebaseTokenVerifierImpl implements FirebaseTokenVerifier { "https://identitytoolkit.googleapis.com/google.identity.identitytoolkit.v1.IdentityToolkit"; private static final String ERROR_INVALID_CREDENTIAL = "ERROR_INVALID_CREDENTIAL"; private static final String ERROR_RUNTIME_EXCEPTION = "ERROR_RUNTIME_EXCEPTION"; + static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; private final JsonFactory jsonFactory; private final GooglePublicKeysManager publicKeysManager; @@ -287,7 +288,7 @@ private void checkTenantId(final FirebaseToken firebaseToken) throws FirebaseAut String tokenTenantId = Strings.nullToEmpty(firebaseToken.getTenantId()); if (!this.tenantId.equals(tokenTenantId)) { throw new FirebaseAuthException( - FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, + TENANT_ID_MISMATCH_ERROR, String.format( "The tenant ID ('%s') of the token did not match the expected value ('%s')", tokenTenantId, diff --git a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java index c65e44deb..a2c17aa8b 100644 --- a/src/main/java/com/google/firebase/auth/FirebaseUserManager.java +++ b/src/main/java/com/google/firebase/auth/FirebaseUserManager.java @@ -20,40 +20,30 @@ import static com.google.common.base.Preconditions.checkNotNull; import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpContent; -import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.HttpResponseException; import com.google.api.client.http.HttpResponseInterceptor; -import com.google.api.client.http.json.JsonHttpContent; import com.google.api.client.json.GenericJson; import com.google.api.client.json.JsonFactory; -import com.google.api.client.json.JsonObjectParser; import com.google.api.client.util.Key; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.common.collect.ImmutableSortedSet; import com.google.firebase.FirebaseApp; import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.internal.AuthHttpClient; 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.ListOidcProviderConfigsResponse; import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse; -import com.google.firebase.auth.internal.ListTenantsResponse; import com.google.firebase.auth.internal.UploadAccountResponse; import com.google.firebase.internal.ApiClientUtils; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; -import com.google.firebase.internal.SdkUtils; -import java.io.IOException; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -71,37 +61,7 @@ */ class FirebaseUserManager { - static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; - static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch"; - static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; - static final String USER_NOT_FOUND_ERROR = "user-not-found"; - static final String INTERNAL_ERROR = "internal-error"; - - // Map of server-side error codes to SDK error codes. - // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors - private static final Map ERROR_CODES = ImmutableMap.builder() - .put("CLAIMS_TOO_LARGE", "claims-too-large") - .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) - .put("INSUFFICIENT_PERMISSION", "insufficient-permission") - .put("DUPLICATE_EMAIL", "email-already-exists") - .put("DUPLICATE_LOCAL_ID", "uid-already-exists") - .put("EMAIL_EXISTS", "email-already-exists") - .put("INVALID_CLAIMS", "invalid-claims") - .put("INVALID_EMAIL", "invalid-email") - .put("INVALID_PAGE_SELECTION", "invalid-page-token") - .put("INVALID_PHONE_NUMBER", "invalid-phone-number") - .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") - .put("PROJECT_NOT_FOUND", "project-not-found") - .put("TENANT_ID_MISMATCH", TENANT_ID_MISMATCH_ERROR) - .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) - .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) - .put("WEAK_PASSWORD", "invalid-password") - .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") - .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") - .build(); - static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100; - 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; @@ -113,18 +73,13 @@ class FirebaseUserManager { private static final String ID_TOOLKIT_URL = "https://identitytoolkit.googleapis.com/%s/projects/%s"; - private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; private final String userMgtBaseUrl; private final String idpConfigMgtBaseUrl; - private final String tenantMgtBaseUrl; private final JsonFactory jsonFactory; - private final HttpRequestFactory requestFactory; - private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); - - private HttpResponseInterceptor interceptor; + private final AuthHttpClient httpClient; - FirebaseUserManager(Builder builder) { + private FirebaseUserManager(Builder builder) { FirebaseApp app = checkNotNull(builder.app, "FirebaseApp must not be null"); String projectId = ImplFirebaseTrampolines.getProjectId(app); checkArgument(!Strings.isNullOrEmpty(projectId), @@ -139,18 +94,19 @@ class FirebaseUserManager { this.idpConfigMgtBaseUrl = idToolkitUrlV2; } else { checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty."); - this.userMgtBaseUrl = idToolkitUrlV1 + getTenantUrlSuffix(tenantId); - this.idpConfigMgtBaseUrl = idToolkitUrlV2 + getTenantUrlSuffix(tenantId); + this.userMgtBaseUrl = idToolkitUrlV1 + "/tenants/" + tenantId; + this.idpConfigMgtBaseUrl = idToolkitUrlV2 + "/tenants/" + tenantId; } - this.tenantMgtBaseUrl = idToolkitUrlV2; + this.jsonFactory = app.getOptions().getJsonFactory(); - this.requestFactory = builder.requestFactory == null - ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; + HttpRequestFactory requestFactory = builder.requestFactory == null + ? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory; + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); } @VisibleForTesting void setInterceptor(HttpResponseInterceptor interceptor) { - this.interceptor = interceptor; + httpClient.setInterceptor(interceptor); } UserRecord getUserById(String uid) throws FirebaseAuthException { @@ -159,7 +115,8 @@ UserRecord getUserById(String uid) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided user ID: " + uid); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -171,7 +128,8 @@ UserRecord getUserByEmail(String email) throws FirebaseAuthException { GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided email: " + email); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -183,7 +141,8 @@ UserRecord getUserByPhoneNumber(String phoneNumber) throws FirebaseAuthException GetAccountInfoResponse response = post( "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null || response.getUsers() == null || response.getUsers().isEmpty()) { - throw new FirebaseAuthException(USER_NOT_FOUND_ERROR, + throw new FirebaseAuthException( + AuthHttpClient.USER_NOT_FOUND_ERROR, "No user record found for the provided phone number: " + phoneNumber); } return new UserRecord(response.getUsers().get(0), jsonFactory); @@ -204,7 +163,8 @@ Set getAccountInfo(@NonNull Collection identifiers) "/accounts:lookup", payload, GetAccountInfoResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to parse server response"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to parse server response"); } Set results = new HashSet<>(); @@ -225,7 +185,7 @@ String createUser(UserRecord.CreateRequest request) throws FirebaseAuthException return uid; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create new user"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to create new user"); } void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) @@ -233,7 +193,8 @@ void updateUser(UserRecord.UpdateRequest request, JsonFactory jsonFactory) GenericJson response = post( "/accounts:update", request.getProperties(jsonFactory), GenericJson.class); if (response == null || !request.getUid().equals(response.get("localId"))) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to update user: " + request.getUid()); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to update user: " + request.getUid()); } } @@ -242,7 +203,8 @@ void deleteUser(String uid) throws FirebaseAuthException { GenericJson response = post( "/accounts:delete", payload, GenericJson.class); if (response == null || !response.containsKey("kind")) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete user: " + uid); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to delete user: " + uid); } } @@ -257,7 +219,7 @@ DeleteUsersResult deleteUsers(@NonNull List uids) throws FirebaseAuthExc BatchDeleteResponse response = post( "/accounts:batchDelete", payload, BatchDeleteResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to delete users"); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to delete users"); } return new DeleteUsersResult(uids.size(), response); @@ -273,10 +235,10 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb GenericUrl url = new GenericUrl(userMgtBaseUrl + "/accounts:batchGet"); url.putAll(builder.build()); - DownloadAccountResponse response = sendRequest( + DownloadAccountResponse response = httpClient.sendRequest( "GET", url, null, DownloadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve users."); } return response; } @@ -286,52 +248,11 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep UploadAccountResponse response = post( "/accounts:batchCreate", request, UploadAccountResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to import users."); + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to import users."); } return new UserImportResult(request.getUsersCount(), response); } - Tenant getTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - return sendRequest("GET", url, null, Tenant.class); - } - - Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - return sendRequest("POST", url, request.getProperties(), Tenant.class); - } - - Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { - Map properties = request.getProperties(); - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); - url.put("updateMask", Joiner.on(",").join(generateMask(properties))); - return sendRequest("PATCH", url, properties, Tenant.class); - } - - void deleteTenant(String tenantId) throws FirebaseAuthException { - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); - sendRequest("DELETE", url, null, GenericJson.class); - } - - ListTenantsResponse listTenants(int maxResults, String pageToken) - throws FirebaseAuthException { - ImmutableMap.Builder builder = - ImmutableMap.builder().put("pageSize", maxResults); - if (pageToken != null) { - checkArgument(!pageToken.equals( - ListTenantsPage.END_OF_LIST), "Invalid end of list page token"); - builder.put("pageToken", pageToken); - } - - GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); - url.putAll(builder.build()); - ListTenantsResponse response = sendRequest("GET", url, null, ListTenantsResponse.class); - if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve tenants."); - } - return response; - } - String createSessionCookie(String idToken, SessionCookieOptions options) throws FirebaseAuthException { final Map payload = ImmutableMap.of( @@ -343,7 +264,8 @@ String createSessionCookie(String idToken, return cookie; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create session cookie"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to create session cookie"); } String getEmailActionLink(EmailLinkType type, String email, @@ -362,21 +284,22 @@ String getEmailActionLink(EmailLinkType type, String email, return link; } } - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link"); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to create email action link"); } OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); url.set("oauthIdpConfigId", request.getProviderId()); - return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); + return httpClient.sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class); } SamlProviderConfig createSamlProviderConfig( SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); url.set("inboundSamlConfigId", request.getProviderId()); - return sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); + return httpClient.sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class); } OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request) @@ -384,8 +307,8 @@ OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest req Map properties = request.getProperties(); GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(generateMask(properties))); - return sendRequest("PATCH", url, properties, OidcProviderConfig.class); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, OidcProviderConfig.class); } SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request) @@ -393,18 +316,18 @@ SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest req Map properties = request.getProperties(); GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId())); - url.put("updateMask", Joiner.on(",").join(generateMask(properties))); - return sendRequest("PATCH", url, properties, SamlProviderConfig.class); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, SamlProviderConfig.class); } OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - return sendRequest("GET", url, null, OidcProviderConfig.class); + return httpClient.sendRequest("GET", url, null, OidcProviderConfig.class); } SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - return sendRequest("GET", url, null, SamlProviderConfig.class); + return httpClient.sendRequest("GET", url, null, SamlProviderConfig.class); } ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken) @@ -420,9 +343,10 @@ ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String p GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs"); url.putAll(builder.build()); ListOidcProviderConfigsResponse response = - sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); + httpClient.sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs."); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } return response; } @@ -440,41 +364,22 @@ ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String p GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs"); url.putAll(builder.build()); ListSamlProviderConfigsResponse response = - sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); + httpClient.sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class); if (response == null) { - throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs."); + throw new FirebaseAuthException( + AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve provider configs."); } return response; } void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId)); - sendRequest("DELETE", url, null, GenericJson.class); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); } void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId)); - sendRequest("DELETE", url, null, GenericJson.class); - } - - private static Set generateMask(Map properties) { - ImmutableSortedSet.Builder maskBuilder = ImmutableSortedSet.naturalOrder(); - for (Map.Entry entry : properties.entrySet()) { - if (entry.getValue() instanceof Map) { - Set childMask = generateMask((Map) entry.getValue()); - for (String childProperty : childMask) { - maskBuilder.add(entry.getKey() + "." + childProperty); - } - } else { - maskBuilder.add(entry.getKey()); - } - } - return maskBuilder.build(); - } - - private static String getTenantUrlSuffix(String tenantId) { - checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); - return "/tenants/" + tenantId; + httpClient.sendRequest("DELETE", url, null, GenericJson.class); } private static String getOidcUrlSuffix(String providerId) { @@ -491,61 +396,7 @@ private T post(String path, Object content, Class clazz) throws FirebaseA checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty"); checkNotNull(content, "content must not be null for POST requests"); GenericUrl url = new GenericUrl(userMgtBaseUrl + path); - return sendRequest("POST", url, content, clazz); - } - - private T sendRequest( - String method, GenericUrl url, - @Nullable Object content, Class clazz) throws FirebaseAuthException { - - checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); - checkNotNull(url, "url must not be null"); - checkNotNull(clazz, "response class must not be null"); - HttpResponse response = null; - try { - HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; - HttpRequest request = - requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); - request.setParser(new JsonObjectParser(jsonFactory)); - request.getHeaders().set(CLIENT_VERSION_HEADER, clientVersion); - if (method.equals("PATCH")) { - request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); - } - request.setResponseInterceptor(interceptor); - response = request.execute(); - return response.parseAs(clazz); - } catch (HttpResponseException e) { - // Server responded with an HTTP error - handleHttpError(e); - return null; - } catch (IOException e) { - // All other IO errors (Connection refused, reset, parse error etc.) - throw new FirebaseAuthException( - INTERNAL_ERROR, "Error while calling user management backend service", e); - } finally { - if (response != null) { - try { - response.disconnect(); - } catch (IOException ignored) { - // Ignored - } - } - } - } - - private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { - try { - HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); - String code = ERROR_CODES.get(response.getErrorCode()); - if (code != null) { - throw new FirebaseAuthException(code, "User management service responded with an error", e); - } - } catch (IOException ignored) { - // Ignored - } - String msg = String.format( - "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); - throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + return httpClient.sendRequest("POST", url, content, clazz); } static class UserImportRequest extends GenericJson { diff --git a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java deleted file mode 100644 index f16586a7a..000000000 --- a/src/main/java/com/google/firebase/auth/TenantAwareFirebaseAuth.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 Google LLC - * - * 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.api.client.util.Clock; -import com.google.common.base.Strings; -import com.google.common.base.Supplier; -import com.google.firebase.FirebaseApp; -import com.google.firebase.auth.internal.FirebaseTokenFactory; - -/** - * The tenant-aware Firebase client. - * - *

This can be used to perform a variety of authentication-related operations, scoped to a - * particular tenant. - */ -public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { - - private final String tenantId; - - TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { - super(builderFromAppAndTenantId(firebaseApp, tenantId)); - checkArgument(!Strings.isNullOrEmpty(tenantId)); - this.tenantId = tenantId; - } - - private static Builder builderFromAppAndTenantId(final FirebaseApp app, final String tenantId) { - return AbstractFirebaseAuth.builder() - .setFirebaseApp(app) - .setTokenFactory( - new Supplier() { - @Override - public FirebaseTokenFactory get() { - return FirebaseTokenUtils.createTokenFactory(app, Clock.SYSTEM, tenantId); - } - }) - .setIdTokenVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createIdTokenVerifier(app, Clock.SYSTEM, tenantId); - } - }) - .setCookieVerifier( - new Supplier() { - @Override - public FirebaseTokenVerifier get() { - return FirebaseTokenUtils.createSessionCookieVerifier(app, Clock.SYSTEM); - } - }) - .setUserManager( - new Supplier() { - @Override - public FirebaseUserManager get() { - return FirebaseUserManager - .builder() - .setFirebaseApp(app) - .setTenantId(tenantId) - .build(); - } - }); - } - - /** Returns the client's tenant ID. */ - public String getTenantId() { - return tenantId; - } - - @Override - protected void doDestroy() { - // Nothing extra needs to be destroyed. - } -} diff --git a/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java new file mode 100644 index 000000000..ad77236d1 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/internal/AuthHttpClient.java @@ -0,0 +1,160 @@ +/* + * 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 static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpContent; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.HttpResponseException; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.http.json.JsonHttpContent; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.internal.Nullable; +import com.google.firebase.internal.SdkUtils; +import java.io.IOException; +import java.util.Map; +import java.util.Set; + +/** + * Provides a convenient API for making REST calls to the Firebase Auth backend servers. + */ +public final class AuthHttpClient { + + public static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found"; + public static final String INTERNAL_ERROR = "internal-error"; + public static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found"; + public static final String USER_NOT_FOUND_ERROR = "user-not-found"; + + private static final String CLIENT_VERSION_HEADER = "X-Client-Version"; + + private static final String CLIENT_VERSION = "Java/Admin/" + SdkUtils.getVersion(); + + // Map of server-side error codes to SDK error codes. + // SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors + private static final Map ERROR_CODES = ImmutableMap.builder() + .put("CLAIMS_TOO_LARGE", "claims-too-large") + .put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR) + .put("INSUFFICIENT_PERMISSION", "insufficient-permission") + .put("DUPLICATE_EMAIL", "email-already-exists") + .put("DUPLICATE_LOCAL_ID", "uid-already-exists") + .put("EMAIL_EXISTS", "email-already-exists") + .put("INVALID_CLAIMS", "invalid-claims") + .put("INVALID_EMAIL", "invalid-email") + .put("INVALID_PAGE_SELECTION", "invalid-page-token") + .put("INVALID_PHONE_NUMBER", "invalid-phone-number") + .put("PHONE_NUMBER_EXISTS", "phone-number-already-exists") + .put("PROJECT_NOT_FOUND", "project-not-found") + .put("USER_NOT_FOUND", USER_NOT_FOUND_ERROR) + .put("WEAK_PASSWORD", "invalid-password") + .put("UNAUTHORIZED_DOMAIN", "unauthorized-continue-uri") + .put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain") + .put("TENANT_NOT_FOUND", TENANT_NOT_FOUND_ERROR) + .build(); + + private final JsonFactory jsonFactory; + private final HttpRequestFactory requestFactory; + + private HttpResponseInterceptor interceptor; + + public AuthHttpClient(JsonFactory jsonFactory, HttpRequestFactory requestFactory) { + this.jsonFactory = jsonFactory; + this.requestFactory = requestFactory; + } + + public static Set generateMask(Map properties) { + ImmutableSortedSet.Builder maskBuilder = ImmutableSortedSet.naturalOrder(); + for (Map.Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Map) { + Set childMask = generateMask((Map) entry.getValue()); + for (String childProperty : childMask) { + maskBuilder.add(entry.getKey() + "." + childProperty); + } + } else { + maskBuilder.add(entry.getKey()); + } + } + return maskBuilder.build(); + } + + public void setInterceptor(HttpResponseInterceptor interceptor) { + this.interceptor = interceptor; + } + + public T sendRequest( + String method, GenericUrl url, + @Nullable Object content, Class clazz) throws FirebaseAuthException { + + checkArgument(!Strings.isNullOrEmpty(method), "method must not be null or empty"); + checkNotNull(url, "url must not be null"); + checkNotNull(clazz, "response class must not be null"); + HttpResponse response = null; + try { + HttpContent httpContent = content != null ? new JsonHttpContent(jsonFactory, content) : null; + HttpRequest request = + requestFactory.buildRequest(method.equals("PATCH") ? "POST" : method, url, httpContent); + request.setParser(new JsonObjectParser(jsonFactory)); + request.getHeaders().set(CLIENT_VERSION_HEADER, CLIENT_VERSION); + if (method.equals("PATCH")) { + request.getHeaders().set("X-HTTP-Method-Override", "PATCH"); + } + request.setResponseInterceptor(interceptor); + response = request.execute(); + return response.parseAs(clazz); + } catch (HttpResponseException e) { + // Server responded with an HTTP error + handleHttpError(e); + return null; + } catch (IOException e) { + // All other IO errors (Connection refused, reset, parse error etc.) + throw new FirebaseAuthException( + INTERNAL_ERROR, "Error while calling the Firebase Auth backend service", e); + } finally { + if (response != null) { + try { + response.disconnect(); + } catch (IOException ignored) { + // Ignored + } + } + } + } + + private void handleHttpError(HttpResponseException e) throws FirebaseAuthException { + try { + HttpErrorResponse response = jsonFactory.fromString(e.getContent(), HttpErrorResponse.class); + String code = ERROR_CODES.get(response.getErrorCode()); + if (code != null) { + throw new FirebaseAuthException(code, "Firebase Auth service responded with an error", e); + } + } catch (IOException ignored) { + // Ignored + } + String msg = String.format( + "Unexpected HTTP response with status: %d; body: %s", e.getStatusCode(), e.getContent()); + throw new FirebaseAuthException(INTERNAL_ERROR, msg, e); + } +} diff --git a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java index c81205a56..2f25ae623 100644 --- a/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/ListProviderConfigsResponse.java @@ -16,12 +16,7 @@ package com.google.firebase.auth.internal; -import com.google.api.client.util.Key; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; -import com.google.firebase.auth.ListProviderConfigsPage; import com.google.firebase.auth.ProviderConfig; -import com.google.firebase.auth.Tenant; import java.util.List; /** diff --git a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java index b612793d2..f1086921f 100644 --- a/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java +++ b/src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java @@ -19,7 +19,7 @@ import com.google.api.client.util.Key; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; -import com.google.firebase.auth.Tenant; +import com.google.firebase.auth.multitenancy.Tenant; import java.util.List; /** diff --git a/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java new file mode 100644 index 000000000..1278e63d5 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/FirebaseTenantClient.java @@ -0,0 +1,111 @@ +/* + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponseInterceptor; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +import com.google.common.base.Joiner; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.google.firebase.FirebaseApp; +import com.google.firebase.ImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.internal.ApiClientUtils; +import java.util.Map; + +final class FirebaseTenantClient { + + static final int MAX_LIST_TENANTS_RESULTS = 100; + + private static final String ID_TOOLKIT_URL = + "https://identitytoolkit.googleapis.com/%s/projects/%s"; + + private final String tenantMgtBaseUrl; + private final AuthHttpClient httpClient; + + FirebaseTenantClient(FirebaseApp app) { + checkNotNull(app, "FirebaseApp must not be null"); + String projectId = ImplFirebaseTrampolines.getProjectId(app); + checkArgument(!Strings.isNullOrEmpty(projectId), + "Project ID is required to access the auth service. Use a service account credential or " + + "set the project ID explicitly via FirebaseOptions. Alternatively you can also " + + "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable."); + this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId); + JsonFactory jsonFactory = app.getOptions().getJsonFactory(); + HttpRequestFactory requestFactory = ApiClientUtils.newAuthorizedRequestFactory(app); + this.httpClient = new AuthHttpClient(jsonFactory, requestFactory); + } + + void setInterceptor(HttpResponseInterceptor interceptor) { + httpClient.setInterceptor(interceptor); + } + + Tenant getTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + return httpClient.sendRequest("GET", url, null, Tenant.class); + } + + Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + return httpClient.sendRequest("POST", url, request.getProperties(), Tenant.class); + } + + Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException { + Map properties = request.getProperties(); + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId())); + url.put("updateMask", Joiner.on(",").join(AuthHttpClient.generateMask(properties))); + return httpClient.sendRequest("PATCH", url, properties, Tenant.class); + } + + void deleteTenant(String tenantId) throws FirebaseAuthException { + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId)); + httpClient.sendRequest("DELETE", url, null, GenericJson.class); + } + + ListTenantsResponse listTenants(int maxResults, String pageToken) + throws FirebaseAuthException { + ImmutableMap.Builder builder = + ImmutableMap.builder().put("pageSize", maxResults); + if (pageToken != null) { + checkArgument(!pageToken.equals( + ListTenantsPage.END_OF_LIST), "Invalid end of list page token."); + builder.put("pageToken", pageToken); + } + + GenericUrl url = new GenericUrl(tenantMgtBaseUrl + "/tenants"); + url.putAll(builder.build()); + ListTenantsResponse response = httpClient.sendRequest( + "GET", url, null, ListTenantsResponse.class); + if (response == null) { + throw new FirebaseAuthException(AuthHttpClient.INTERNAL_ERROR, "Failed to retrieve tenants."); + } + return response; + } + + private static String getTenantUrlSuffix(String tenantId) { + checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); + return "/tenants/" + tenantId; + } +} diff --git a/src/main/java/com/google/firebase/auth/ListTenantsPage.java b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java similarity index 91% rename from src/main/java/com/google/firebase/auth/ListTenantsPage.java rename to src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java index d637174c7..c1f393ddb 100644 --- a/src/main/java/com/google/firebase/auth/ListTenantsPage.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/ListTenantsPage.java @@ -14,15 +14,14 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import com.google.api.client.json.JsonFactory; import com.google.api.gax.paging.Page; import com.google.common.collect.ImmutableList; -import com.google.firebase.auth.internal.DownloadAccountResponse; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.internal.ListTenantsResponse; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; @@ -198,16 +197,16 @@ ListTenantsResponse fetch(int maxResults, String pageToken) static class DefaultTenantSource implements TenantSource { - private final FirebaseUserManager userManager; + private final FirebaseTenantClient tenantClient; - DefaultTenantSource(FirebaseUserManager userManager) { - this.userManager = checkNotNull(userManager, "User manager must not be null."); + DefaultTenantSource(FirebaseTenantClient tenantClient) { + this.tenantClient = checkNotNull(tenantClient, "Tenant client must not be null."); } @Override public ListTenantsResponse fetch(int maxResults, String pageToken) throws FirebaseAuthException { - return userManager.listTenants(maxResults, pageToken); + return tenantClient.listTenants(maxResults, pageToken); } } @@ -224,13 +223,13 @@ static class PageFactory { private final String pageToken; PageFactory(@NonNull TenantSource source) { - this(source, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS, null); + this(source, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, null); } PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) { - checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_TENANTS_RESULTS, + checkArgument(maxResults > 0 && maxResults <= FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS, "maxResults must be a positive integer that does not exceed %s", - FirebaseUserManager.MAX_LIST_TENANTS_RESULTS); + FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); checkArgument(!END_OF_LIST.equals(pageToken), "Invalid end of list page token."); this.source = checkNotNull(source, "Source must not be null."); this.maxResults = maxResults; diff --git a/src/main/java/com/google/firebase/auth/Tenant.java b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java similarity index 99% rename from src/main/java/com/google/firebase/auth/Tenant.java rename to src/main/java/com/google/firebase/auth/multitenancy/Tenant.java index 61f52572d..57d215e96 100644 --- a/src/main/java/com/google/firebase/auth/Tenant.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/Tenant.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static com.google.common.base.Preconditions.checkArgument; diff --git a/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java new file mode 100644 index 000000000..540437404 --- /dev/null +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuth.java @@ -0,0 +1,50 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.common.base.Strings; +import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.AbstractFirebaseAuth; + +/** + * The tenant-aware Firebase client. + * + *

This can be used to perform a variety of authentication-related operations, scoped to a + * particular tenant. + */ +public final class TenantAwareFirebaseAuth extends AbstractFirebaseAuth { + + private final String tenantId; + + TenantAwareFirebaseAuth(final FirebaseApp firebaseApp, final String tenantId) { + super(builderFromAppAndTenantId(firebaseApp, tenantId)); + checkArgument(!Strings.isNullOrEmpty(tenantId)); + this.tenantId = tenantId; + } + + /** Returns the client's tenant ID. */ + public String getTenantId() { + return tenantId; + } + + @Override + protected void doDestroy() { + // Nothing extra needs to be destroyed. + } +} diff --git a/src/main/java/com/google/firebase/auth/TenantManager.java b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java similarity index 86% rename from src/main/java/com/google/firebase/auth/TenantManager.java rename to src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java index 4b3bbb948..11f26b096 100644 --- a/src/main/java/com/google/firebase/auth/TenantManager.java +++ b/src/main/java/com/google/firebase/auth/multitenancy/TenantManager.java @@ -14,27 +14,28 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; -import static com.google.common.base.Preconditions.checkState; -import com.google.api.client.json.JsonFactory; +import com.google.api.client.http.HttpResponseInterceptor; import com.google.api.core.ApiFuture; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; import com.google.firebase.FirebaseApp; -import com.google.firebase.auth.ListTenantsPage.DefaultTenantSource; -import com.google.firebase.auth.ListTenantsPage.PageFactory; -import com.google.firebase.auth.ListTenantsPage.TenantSource; -import com.google.firebase.auth.Tenant.CreateRequest; -import com.google.firebase.auth.Tenant.UpdateRequest; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.multitenancy.ListTenantsPage.DefaultTenantSource; +import com.google.firebase.auth.multitenancy.ListTenantsPage.PageFactory; +import com.google.firebase.auth.multitenancy.ListTenantsPage.TenantSource; +import com.google.firebase.auth.multitenancy.Tenant.CreateRequest; +import com.google.firebase.auth.multitenancy.Tenant.UpdateRequest; import com.google.firebase.internal.CallableOperation; import com.google.firebase.internal.NonNull; import com.google.firebase.internal.Nullable; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; /** * This class can be used to perform a variety of tenant-related operations, including creating, @@ -42,17 +43,25 @@ */ public final class TenantManager { - private final Object lock = new Object(); - private final AtomicBoolean destroyed = new AtomicBoolean(false); - private final FirebaseApp firebaseApp; - private final FirebaseUserManager userManager; + private final FirebaseTenantClient tenantClient; private final Map tenantAwareAuths; - TenantManager(FirebaseApp firebaseApp, FirebaseUserManager userManager) { + /** + * Creates a new {@link TenantManager} instance. For internal use only. Use + * {@link FirebaseAuth#getTenantManager()} to obtain an instance for regular use. + * + * @hide + */ + public TenantManager(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; - this.userManager = userManager; - tenantAwareAuths = new HashMap(); + this.tenantClient = new FirebaseTenantClient(firebaseApp); + this.tenantAwareAuths = new HashMap<>(); + } + + @VisibleForTesting + void setInterceptor(HttpResponseInterceptor interceptor) { + this.tenantClient.setInterceptor(interceptor); } /** @@ -89,12 +98,11 @@ public ApiFuture getTenantAsync(@NonNull String tenantId) { } private CallableOperation getTenantOp(final String tenantId) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); return new CallableOperation() { @Override protected Tenant execute() throws FirebaseAuthException { - return userManager.getTenant(tenantId); + return tenantClient.getTenant(tenantId); } }; } @@ -109,7 +117,7 @@ protected Tenant execute() throws FirebaseAuthException { * @throws FirebaseAuthException If an error occurs while retrieving tenant data. */ public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException { - return listTenants(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS); + return listTenants(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); } /** @@ -137,7 +145,7 @@ public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults) * @throws IllegalArgumentException If the specified page token is empty. */ public ApiFuture listTenantsAsync(@Nullable String pageToken) { - return listTenantsAsync(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS); + return listTenantsAsync(pageToken, FirebaseTenantClient.MAX_LIST_TENANTS_RESULTS); } /** @@ -157,8 +165,7 @@ public ApiFuture listTenantsAsync(@Nullable String pageToken, i private CallableOperation listTenantsOp( @Nullable final String pageToken, final int maxResults) { - checkNotDestroyed(); - final TenantSource tenantSource = new DefaultTenantSource(userManager); + final TenantSource tenantSource = new DefaultTenantSource(tenantClient); final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken); return new CallableOperation() { @Override @@ -195,12 +202,11 @@ public ApiFuture createTenantAsync(@NonNull CreateRequest request) { private CallableOperation createTenantOp( final CreateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Create request must not be null."); return new CallableOperation() { @Override protected Tenant execute() throws FirebaseAuthException { - return userManager.createTenant(request); + return tenantClient.createTenant(request); } }; } @@ -233,14 +239,13 @@ public ApiFuture updateTenantAsync(@NonNull UpdateRequest request) { private CallableOperation updateTenantOp( final UpdateRequest request) { - checkNotDestroyed(); checkNotNull(request, "Update request must not be null."); checkArgument(!request.getProperties().isEmpty(), "Tenant update must have at least one property set."); return new CallableOperation() { @Override protected Tenant execute() throws FirebaseAuthException { - return userManager.updateTenant(request); + return tenantClient.updateTenant(request); } }; } @@ -270,27 +275,13 @@ public ApiFuture deleteTenantAsync(String tenantId) { } private CallableOperation deleteTenantOp(final String tenantId) { - checkNotDestroyed(); checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty."); return new CallableOperation() { @Override protected Void execute() throws FirebaseAuthException { - userManager.deleteTenant(tenantId); + tenantClient.deleteTenant(tenantId); return null; } }; } - - void checkNotDestroyed() { - synchronized (lock) { - checkState( - !destroyed.get(), - "TenantManager instance is no longer alive. This happens when " - + "the parent FirebaseApp instance has been deleted."); - } - } - - protected void destroy() { - destroyed.set(true); - } } diff --git a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java index f651e3d3f..d5bfbae7f 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/FirebaseAuthIT.java @@ -50,6 +50,7 @@ import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.auth.hash.Scrypt; +import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.internal.Nullable; import com.google.firebase.testing.IntegrationTestUtils; import java.io.IOException; @@ -66,7 +67,6 @@ import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExternalResource; public class FirebaseAuthIT { @@ -96,7 +96,7 @@ public void testGetNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -108,7 +108,7 @@ public void testGetNonExistingUserByEmail() throws Exception { fail("No error thrown for non existing email"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -120,7 +120,7 @@ public void testUpdateNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -132,7 +132,7 @@ public void testDeleteNonExistingUser() throws Exception { fail("No error thrown for non existing uid"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -1021,18 +1021,6 @@ private boolean checkSamlProviderConfig(List providerIds, SamlProviderCo return false; } - private static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) - throws Exception { - try { - firebaseAuth.getUserAsync(uid).get(); - fail("No error thrown for getting a user which was expected to be absent."); - } catch (ExecutionException e) { - assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, - ((FirebaseAuthException) e.getCause()).getErrorCode()); - } - } - static UserRecord newUserWithParams() throws Exception { return newUserWithParams(auth); } diff --git a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java index a7c1d4d10..5dd1e9c14 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseTokenVerifierImplTest.java @@ -30,7 +30,6 @@ import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; -import com.google.firebase.auth.internal.FirebaseCustomAuthToken; import com.google.firebase.testing.ServiceAccount; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -219,14 +218,14 @@ public void testMalformedToken() throws Exception { } @Test - public void testVerifyTokenDifferentTenantIds() throws Exception { + public void testVerifyTokenDifferentTenantIds() { try { fullyPopulatedBuilder() .setTenantId("TENANT_1") .build() .verifyToken(createTokenWithTenantId("TENANT_2")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); assertEquals( "The tenant ID ('TENANT_2') of the token did not match the expected value ('TENANT_1')", e.getMessage()); @@ -234,14 +233,14 @@ public void testVerifyTokenDifferentTenantIds() throws Exception { } @Test - public void testVerifyTokenMissingTenantId() throws Exception { + public void testVerifyTokenMissingTenantId() { try { fullyPopulatedBuilder() .setTenantId("TENANT_ID") .build() .verifyToken(tokenFactory.createToken()); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); assertEquals( "The tenant ID ('') of the token did not match the expected value ('TENANT_ID')", e.getMessage()); @@ -249,13 +248,13 @@ public void testVerifyTokenMissingTenantId() throws Exception { } @Test - public void testVerifyTokenUnexpectedTenantId() throws Exception { + public void testVerifyTokenUnexpectedTenantId() { try { fullyPopulatedBuilder() .build() .verifyToken(createTokenWithTenantId("TENANT_ID")); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); + assertEquals(FirebaseTokenVerifierImpl.TENANT_ID_MISMATCH_ERROR, e.getErrorCode()); assertEquals( "The tenant ID ('TENANT_ID') of the token did not match the expected value ('')", e.getMessage()); diff --git a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java index 74b36fa56..2702a1ceb 100644 --- a/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java +++ b/src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java @@ -44,6 +44,9 @@ import com.google.firebase.FirebaseOptions; import com.google.firebase.TestOnlyImplFirebaseTrampolines; import com.google.firebase.auth.FirebaseUserManager.EmailLinkType; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.auth.multitenancy.TenantAwareFirebaseAuth; +import com.google.firebase.auth.multitenancy.TenantManager; import com.google.firebase.internal.SdkUtils; import com.google.firebase.testing.MultiRequestMockHttpTransport; import com.google.firebase.testing.TestResponseInterceptor; @@ -66,7 +69,9 @@ public class FirebaseUserManagerTest { private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + private static final String TEST_TOKEN = "token"; + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); private static final ActionCodeSettings ACTION_CODE_SETTINGS = ActionCodeSettings.builder() @@ -78,11 +83,13 @@ public class FirebaseUserManagerTest { .setAndroidInstallApp(true) .setAndroidMinimumVersion("6") .build(); + private static final Map ACTION_CODE_SETTINGS_MAP = ACTION_CODE_SETTINGS.getProperties(); private static final String PROJECT_BASE_URL = "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; @After @@ -126,7 +133,7 @@ public void testGetUserWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -149,7 +156,7 @@ public void testGetUserByEmailWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -172,7 +179,7 @@ public void testGetUserByPhoneNumberWithNotFoundError() throws Exception { } catch (ExecutionException e) { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } @@ -648,228 +655,6 @@ public void testImportUsersLargeList() { } } - @Test - public void testGetTenant() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - - Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1"); - - checkTenant(tenant, "TENANT_1"); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); - } - - @Test - public void testGetTenantWithNotFoundError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); - try { - FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); - fail("No error thrown for invalid response"); - } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); - } - checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); - } - - @Test - public void testListTenants() throws Exception { - final TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("listTenants.json")); - - ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 999); - - ImmutableList tenants = ImmutableList.copyOf(page.getValues()); - assertEquals(2, tenants.size()); - checkTenant(tenants.get(0), "TENANT_1"); - checkTenant(tenants.get(1), "TENANT_2"); - assertEquals("", page.getNextPageToken()); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "GET", TENANTS_BASE_URL); - GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals(999, url.getFirst("pageSize")); - assertNull(url.getFirst("pageToken")); - } - - @Test - public void testListTenantsWithPageToken() throws Exception { - final TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("listTenants.json")); - - ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 999); - - ImmutableList tenants = ImmutableList.copyOf(page.getValues()); - assertEquals(2, tenants.size()); - checkTenant(tenants.get(0), "TENANT_1"); - checkTenant(tenants.get(1), "TENANT_2"); - assertEquals("", page.getNextPageToken()); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "GET", TENANTS_BASE_URL); - GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals(999, url.getFirst("pageSize")); - assertEquals("token", url.getFirst("pageToken")); - } - - @Test - public void testListZeroTenants() throws Exception { - final TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); - - ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); - - assertTrue(Iterables.isEmpty(page.getValues())); - assertEquals("", page.getNextPageToken()); - checkRequestHeaders(interceptor); - } - - @Test - public void testCreateTenant() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - Tenant.CreateRequest request = new Tenant.CreateRequest() - .setDisplayName("DISPLAY_NAME") - .setPasswordSignInAllowed(true) - .setEmailLinkSignInEnabled(false); - - Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); - - checkTenant(tenant, "TENANT_1"); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "POST", TENANTS_BASE_URL); - GenericJson parsed = parseRequestContent(interceptor); - assertEquals("DISPLAY_NAME", parsed.get("displayName")); - assertEquals(true, parsed.get("allowPasswordSignup")); - assertEquals(false, parsed.get("enableEmailLinkSignin")); - } - - @Test - public void testCreateTenantMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - Tenant.CreateRequest request = new Tenant.CreateRequest(); - - Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); - - checkTenant(tenant, "TENANT_1"); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "POST", TENANTS_BASE_URL); - GenericJson parsed = parseRequestContent(interceptor); - assertNull(parsed.get("displayName")); - assertNull(parsed.get("allowPasswordSignup")); - assertNull(parsed.get("enableEmailLinkSignin")); - } - - @Test - public void testCreateTenantError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); - try { - FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); - fail("No error thrown for invalid response"); - } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); - } - checkUrl(interceptor, "POST", TENANTS_BASE_URL); - } - - @Test - public void testUpdateTenant() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1") - .setDisplayName("DISPLAY_NAME") - .setPasswordSignInAllowed(true) - .setEmailLinkSignInEnabled(false); - - Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); - - checkTenant(tenant, "TENANT_1"); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); - GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin", - url.getFirst("updateMask")); - GenericJson parsed = parseRequestContent(interceptor); - assertEquals("DISPLAY_NAME", parsed.get("displayName")); - assertEquals(true, parsed.get("allowPasswordSignup")); - assertEquals(false, parsed.get("enableEmailLinkSignin")); - } - - @Test - public void testUpdateTenantMinimal() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - Tenant.UpdateRequest request = - new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); - - Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); - - checkTenant(tenant, "TENANT_1"); - checkRequestHeaders(interceptor); - checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); - GenericUrl url = interceptor.getResponse().getRequest().getUrl(); - assertEquals("displayName", url.getFirst("updateMask")); - GenericJson parsed = parseRequestContent(interceptor); - assertEquals("DISPLAY_NAME", parsed.get("displayName")); - assertNull(parsed.get("allowPasswordSignup")); - assertNull(parsed.get("enableEmailLinkSignin")); - } - - @Test - public void testUpdateTenantNoValues() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("tenant.json")); - TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); - try { - tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1")); - fail("No error thrown for empty tenant update"); - } catch (IllegalArgumentException e) { - // expected - } - } - - @Test - public void testUpdateTenantError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); - Tenant.UpdateRequest request = - new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); - try { - FirebaseAuth.getInstance().getTenantManager().updateTenant(request); - fail("No error thrown for invalid response"); - } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); - } - checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); - } - - @Test - public void testDeleteTenant() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); - - FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1"); - - checkRequestHeaders(interceptor); - checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1"); - } - - @Test - public void testDeleteTenantWithNotFoundError() throws Exception { - TestResponseInterceptor interceptor = - initializeAppForUserManagementWithStatusCode(404, - "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); - try { - FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); - fail("No error thrown for invalid response"); - } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); - } - checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); - } - @Test public void testCreateSessionCookie() throws Exception { TestResponseInterceptor interceptor = initializeAppForUserManagement( @@ -1004,7 +789,7 @@ public void call(FirebaseAuth auth) throws Exception { String msg = String.format("Unexpected HTTP response with status: %d; body: {}", code); assertEquals(msg, authException.getMessage()); assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } } @@ -1019,9 +804,9 @@ public void call(FirebaseAuth auth) throws Exception { } catch (ExecutionException e) { assertThat(e.getCause().toString(), e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); - assertEquals("User management service responded with an error", authException.getMessage()); + assertEquals("Firebase Auth service responded with an error", authException.getMessage()); assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, authException.getErrorCode()); } } } @@ -1036,7 +821,7 @@ public void testGetUserMalformedJsonError() throws Exception { assertThat(e.getCause(), instanceOf(FirebaseAuthException.class)); FirebaseAuthException authException = (FirebaseAuthException) e.getCause(); assertThat(authException.getCause(), instanceOf(IOException.class)); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -1055,7 +840,7 @@ public void testGetUserUnexpectedHttpError() throws Exception { assertThat(authException.getCause(), instanceOf(HttpResponseException.class)); assertEquals("Unexpected HTTP response with status: 500; body: {\"not\" json}", authException.getMessage()); - assertEquals(FirebaseUserManager.INTERNAL_ERROR, authException.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, authException.getErrorCode()); } } @@ -1672,15 +1457,14 @@ public void testCreateOidcProviderError() throws Exception { FirebaseAuth.getInstance().createOidcProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); } checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/oauthIdpConfigs"); } @Test public void testCreateOidcProviderMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); OidcProviderConfig.CreateRequest createRequest = new OidcProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -1762,8 +1546,7 @@ public void testUpdateOidcProviderMinimal() throws Exception { @Test public void testUpdateOidcProviderConfigNoValues() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); try { FirebaseAuth.getInstance().updateOidcProviderConfig( new OidcProviderConfig.UpdateRequest("oidc.provider-id")); @@ -1774,7 +1557,7 @@ public void testUpdateOidcProviderConfigNoValues() throws Exception { } @Test - public void testUpdateOidcProviderConfigError() throws Exception { + public void testUpdateOidcProviderConfigError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); @@ -1784,7 +1567,7 @@ public void testUpdateOidcProviderConfigError() throws Exception { FirebaseAuth.getInstance().updateOidcProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); } checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @@ -1833,8 +1616,7 @@ public void testGetOidcProviderConfig() throws Exception { @Test public void testGetOidcProviderConfigMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); try { FirebaseAuth.getInstance().getOidcProviderConfig(null); @@ -1846,8 +1628,7 @@ public void testGetOidcProviderConfigMissingId() throws Exception { @Test public void testGetOidcProviderConfigInvalidId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("oidc.json")); + initializeAppForUserManagement(TestUtils.loadResource("oidc.json")); try { FirebaseAuth.getInstance().getOidcProviderConfig("saml.invalid-oidc-provider-id"); @@ -1866,7 +1647,7 @@ public void testGetOidcProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getOidcProviderConfig("oidc.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.provider-id"); } @@ -1968,7 +1749,7 @@ public void testDeleteOidcProviderConfig() throws Exception { @Test public void testDeleteOidcProviderMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + initializeAppForUserManagement("{}"); try { FirebaseAuth.getInstance().deleteOidcProviderConfig(null); @@ -1980,7 +1761,7 @@ public void testDeleteOidcProviderMissingId() throws Exception { @Test public void testDeleteOidcProviderInvalidId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + initializeAppForUserManagement("{}"); try { FirebaseAuth.getInstance().deleteOidcProviderConfig("saml.invalid-oidc-provider-id"); @@ -1991,7 +1772,7 @@ public void testDeleteOidcProviderInvalidId() throws Exception { } @Test - public void testDeleteOidcProviderConfigWithNotFoundError() throws Exception { + public void testDeleteOidcProviderConfigWithNotFoundError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); @@ -1999,7 +1780,7 @@ public void testDeleteOidcProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().deleteOidcProviderConfig("oidc.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/oauthIdpConfigs/oidc.UNKNOWN"); } @@ -2107,7 +1888,7 @@ public void testCreateSamlProviderMinimal() throws Exception { } @Test - public void testCreateSamlProviderError() throws Exception { + public void testCreateSamlProviderError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); @@ -2117,15 +1898,14 @@ public void testCreateSamlProviderError() throws Exception { FirebaseAuth.getInstance().createSamlProviderConfig(createRequest); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); } checkUrl(interceptor, "POST", PROJECT_BASE_URL + "/inboundSamlConfigs"); } @Test public void testCreateSamlProviderMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); SamlProviderConfig.CreateRequest createRequest = new SamlProviderConfig.CreateRequest() .setDisplayName("DISPLAY_NAME") @@ -2260,7 +2040,7 @@ public void testUpdateSamlProviderConfigError() throws Exception { FirebaseAuth.getInstance().updateSamlProviderConfig(request); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.INTERNAL_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); } checkUrl(interceptor, "PATCH", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @@ -2314,8 +2094,7 @@ public void testGetSamlProviderConfig() throws Exception { @Test public void testGetSamlProviderConfigMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); try { FirebaseAuth.getInstance().getSamlProviderConfig(null); @@ -2327,8 +2106,7 @@ public void testGetSamlProviderConfigMissingId() throws Exception { @Test public void testGetSamlProviderConfigInvalidId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement( - TestUtils.loadResource("saml.json")); + initializeAppForUserManagement(TestUtils.loadResource("saml.json")); try { FirebaseAuth.getInstance().getSamlProviderConfig("oidc.invalid-saml-provider-id"); @@ -2339,7 +2117,7 @@ public void testGetSamlProviderConfigInvalidId() throws Exception { } @Test - public void testGetSamlProviderConfigWithNotFoundError() throws Exception { + public void testGetSamlProviderConfigWithNotFoundError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); @@ -2347,7 +2125,7 @@ public void testGetSamlProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().getSamlProviderConfig("saml.provider-id"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } checkUrl(interceptor, "GET", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.provider-id"); } @@ -2450,7 +2228,7 @@ public void testDeleteSamlProviderConfig() throws Exception { @Test public void testDeleteSamlProviderMissingId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + initializeAppForUserManagement("{}"); try { FirebaseAuth.getInstance().deleteSamlProviderConfig(null); @@ -2462,7 +2240,7 @@ public void testDeleteSamlProviderMissingId() throws Exception { @Test public void testDeleteSamlProviderInvalidId() throws Exception { - TestResponseInterceptor interceptor = initializeAppForUserManagement("{}"); + initializeAppForUserManagement("{}"); try { FirebaseAuth.getInstance().deleteSamlProviderConfig("oidc.invalid-saml-provider-id"); @@ -2473,7 +2251,7 @@ public void testDeleteSamlProviderInvalidId() throws Exception { } @Test - public void testDeleteSamlProviderConfigWithNotFoundError() throws Exception { + public void testDeleteSamlProviderConfigWithNotFoundError() { TestResponseInterceptor interceptor = initializeAppForUserManagementWithStatusCode(404, "{\"error\": {\"message\": \"CONFIGURATION_NOT_FOUND\"}}"); @@ -2481,7 +2259,7 @@ public void testDeleteSamlProviderConfigWithNotFoundError() throws Exception { FirebaseAuth.getInstance().deleteSamlProviderConfig("saml.UNKNOWN"); fail("No error thrown for invalid response"); } catch (FirebaseAuthException e) { - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, e.getErrorCode()); } checkUrl(interceptor, "DELETE", PROJECT_BASE_URL + "/inboundSamlConfigs/saml.UNKNOWN"); } @@ -2521,7 +2299,8 @@ private static TestResponseInterceptor initializeAppForTenantAwareUserManagement initializeAppWithResponses(responses); TestResponseInterceptor interceptor = new TestResponseInterceptor(); TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); - tenantManager.getAuthForTenant(tenantId).getUserManager().setInterceptor(interceptor); + AbstractFirebaseAuth auth = tenantManager.getAuthForTenant(tenantId); + auth.getUserManager().setInterceptor(interceptor); return interceptor; } @@ -2610,13 +2389,6 @@ private static void checkUserRecord(UserRecord userRecord) { assertEquals("gold", claims.get("package")); } - private static void checkTenant(Tenant tenant, String tenantId) { - assertEquals(tenantId, tenant.getTenantId()); - assertEquals("DISPLAY_NAME", tenant.getDisplayName()); - assertTrue(tenant.isPasswordSignInAllowed()); - assertFalse(tenant.isEmailLinkSignInEnabled()); - } - private static void checkOidcProviderConfig(OidcProviderConfig config, String providerId) { assertEquals(providerId, config.getProviderId()); assertEquals("DISPLAY_NAME", config.getDisplayName()); diff --git a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java index 262c48f37..c01ac6501 100644 --- a/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java +++ b/src/test/java/com/google/firebase/auth/ProviderConfigTestUtils.java @@ -21,33 +21,34 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.firebase.auth.internal.AuthHttpClient; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import org.junit.rules.ExternalResource; -class ProviderConfigTestUtils { +public class ProviderConfigTestUtils { - static void assertOidcProviderConfigDoesNotExist( + public static void assertOidcProviderConfigDoesNotExist( AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { try { firebaseAuth.getOidcProviderConfigAsync(providerId).get(); fail("No error thrown for getting a deleted OIDC provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } - static void assertSamlProviderConfigDoesNotExist( + public static void assertSamlProviderConfigDoesNotExist( AbstractFirebaseAuth firebaseAuth, String providerId) throws Exception { try { firebaseAuth.getSamlProviderConfigAsync(providerId).get(); fail("No error thrown for getting a deleted SAML provider config."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.CONFIGURATION_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.CONFIGURATION_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -55,24 +56,25 @@ static void assertSamlProviderConfigDoesNotExist( /** * Creates temporary provider configs for testing, and deletes them at the end of each test case. */ - static final class TemporaryProviderConfig extends ExternalResource { + public static final class TemporaryProviderConfig extends ExternalResource { private final AbstractFirebaseAuth auth; private final List oidcIds = new ArrayList<>(); private final List samlIds = new ArrayList<>(); - TemporaryProviderConfig(AbstractFirebaseAuth auth) { + public TemporaryProviderConfig(AbstractFirebaseAuth auth) { this.auth = auth; } - synchronized OidcProviderConfig createOidcProviderConfig( + public synchronized OidcProviderConfig createOidcProviderConfig( OidcProviderConfig.CreateRequest request) throws FirebaseAuthException { OidcProviderConfig config = auth.createOidcProviderConfig(request); oidcIds.add(config.getProviderId()); return config; } - synchronized void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException { + public synchronized void deleteOidcProviderConfig( + String providerId) throws FirebaseAuthException { checkArgument(oidcIds.contains(providerId), "Provider ID is not currently associated with a temporary OIDC provider config: " + providerId); @@ -80,14 +82,15 @@ synchronized void deleteOidcProviderConfig(String providerId) throws FirebaseAut oidcIds.remove(providerId); } - synchronized SamlProviderConfig createSamlProviderConfig( + public synchronized SamlProviderConfig createSamlProviderConfig( SamlProviderConfig.CreateRequest request) throws FirebaseAuthException { SamlProviderConfig config = auth.createSamlProviderConfig(request); samlIds.add(config.getProviderId()); return config; } - synchronized void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException { + public synchronized void deleteSamlProviderConfig( + String providerId) throws FirebaseAuthException { checkArgument(samlIds.contains(providerId), "Provider ID is not currently associated with a temporary SAML provider config: " + providerId); diff --git a/src/test/java/com/google/firebase/auth/UserTestUtils.java b/src/test/java/com/google/firebase/auth/UserTestUtils.java index 8f16114c4..aa86e6e19 100644 --- a/src/test/java/com/google/firebase/auth/UserTestUtils.java +++ b/src/test/java/com/google/firebase/auth/UserTestUtils.java @@ -16,11 +16,11 @@ package com.google.firebase.auth; -import static com.google.common.base.Preconditions.checkArgument; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import com.google.firebase.auth.internal.AuthHttpClient; import java.util.ArrayList; import java.util.List; import java.util.Random; @@ -28,21 +28,21 @@ import java.util.concurrent.ExecutionException; import org.junit.rules.ExternalResource; -final class UserTestUtils { +public final class UserTestUtils { - static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) + public static void assertUserDoesNotExist(AbstractFirebaseAuth firebaseAuth, String uid) throws Exception { try { firebaseAuth.getUserAsync(uid).get(); fail("No error thrown for getting a user which was expected to be absent."); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.USER_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.USER_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } - static RandomUser generateRandomUserInfo() { + public static RandomUser generateRandomUserInfo() { String uid = UUID.randomUUID().toString().replaceAll("-", ""); String email = String.format( "test%s@example.%s.com", @@ -60,7 +60,7 @@ private static String generateRandomPhoneNumber() { return builder.toString(); } - static class RandomUser { + public static class RandomUser { private final String uid; private final String email; private final String phoneNumber; @@ -71,15 +71,15 @@ private RandomUser(String uid, String email, String phoneNumber) { this.phoneNumber = phoneNumber; } - String getUid() { + public String getUid() { return uid; } - String getEmail() { + public String getEmail() { return email; } - String getPhoneNumber() { + public String getPhoneNumber() { return phoneNumber; } } @@ -88,12 +88,12 @@ String getPhoneNumber() { * Creates temporary Firebase user accounts for testing, and deletes them at the end of each * test case. */ - static final class TemporaryUser extends ExternalResource { + public static final class TemporaryUser extends ExternalResource { private final AbstractFirebaseAuth auth; private final List users = new ArrayList<>(); - TemporaryUser(AbstractFirebaseAuth auth) { + public TemporaryUser(AbstractFirebaseAuth auth) { this.auth = auth; } diff --git a/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java new file mode 100644 index 000000000..36660dc28 --- /dev/null +++ b/src/test/java/com/google/firebase/auth/multitenancy/FirebaseTenantClientTest.java @@ -0,0 +1,363 @@ +/* + * Copyright 2020 Google LLC + * + * 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.multitenancy; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import com.google.api.client.googleapis.util.Utils; +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpHeaders; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.json.GenericJson; +import com.google.api.client.json.JsonFactory; +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.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.google.firebase.FirebaseApp; +import com.google.firebase.FirebaseOptions; +import com.google.firebase.TestOnlyImplFirebaseTrampolines; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.MockGoogleCredentials; +import com.google.firebase.auth.internal.AuthHttpClient; +import com.google.firebase.internal.SdkUtils; +import com.google.firebase.testing.MultiRequestMockHttpTransport; +import com.google.firebase.testing.TestResponseInterceptor; +import com.google.firebase.testing.TestUtils; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.junit.After; +import org.junit.Test; + +public class FirebaseTenantClientTest { + + private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); + + private static final String TEST_TOKEN = "token"; + + private static final GoogleCredentials credentials = new MockGoogleCredentials(TEST_TOKEN); + + private static final String PROJECT_BASE_URL = + "https://identitytoolkit.googleapis.com/v2/projects/test-project-id"; + + private static final String TENANTS_BASE_URL = PROJECT_BASE_URL + "/tenants"; + + @After + public void tearDown() { + TestOnlyImplFirebaseTrampolines.clearInstancesForTest(); + } + + @Test + public void testGetTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().getTenant("TENANT_1"); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testGetTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().getTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "GET", TENANTS_BASE_URL + "/UNKNOWN"); + } + + @Test + public void testListTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null, 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertNull(url.getFirst("pageToken")); + } + + @Test + public void testListTenantsWithPageToken() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("listTenants.json")); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants("token", 99); + + ImmutableList tenants = ImmutableList.copyOf(page.getValues()); + assertEquals(2, tenants.size()); + checkTenant(tenants.get(0), "TENANT_1"); + checkTenant(tenants.get(1), "TENANT_2"); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "GET", TENANTS_BASE_URL); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals(99, url.getFirst("pageSize")); + assertEquals("token", url.getFirst("pageToken")); + } + + @Test + public void testListZeroTenants() throws Exception { + final TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + ListTenantsPage page = FirebaseAuth.getInstance().getTenantManager().listTenants(null); + + assertTrue(Iterables.isEmpty(page.getValues())); + assertEquals("", page.getNextPageToken()); + checkRequestHeaders(interceptor); + } + + @Test + public void testCreateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest() + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.CreateRequest request = new Tenant.CreateRequest(); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().createTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + GenericJson parsed = parseRequestContent(interceptor); + assertNull(parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testCreateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().createTenant(new Tenant.CreateRequest()); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "POST", TENANTS_BASE_URL); + } + + @Test + public void testUpdateTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = new Tenant.UpdateRequest("TENANT_1") + .setDisplayName("DISPLAY_NAME") + .setPasswordSignInAllowed(true) + .setEmailLinkSignInEnabled(false); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("allowPasswordSignup,displayName,enableEmailLinkSignin", + url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertEquals(true, parsed.get("allowPasswordSignup")); + assertEquals(false, parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantMinimal() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement( + TestUtils.loadResource("tenant.json")); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + + Tenant tenant = FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + + checkTenant(tenant, "TENANT_1"); + checkRequestHeaders(interceptor); + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + GenericUrl url = interceptor.getResponse().getRequest().getUrl(); + assertEquals("displayName", url.getFirst("updateMask")); + GenericJson parsed = parseRequestContent(interceptor); + assertEquals("DISPLAY_NAME", parsed.get("displayName")); + assertNull(parsed.get("allowPasswordSignup")); + assertNull(parsed.get("enableEmailLinkSignin")); + } + + @Test + public void testUpdateTenantNoValues() throws Exception { + initializeAppForTenantManagement(TestUtils.loadResource("tenant.json")); + TenantManager tenantManager = FirebaseAuth.getInstance().getTenantManager(); + try { + tenantManager.updateTenant(new Tenant.UpdateRequest("TENANT_1")); + fail("No error thrown for empty tenant update"); + } catch (IllegalArgumentException e) { + // expected + } + } + + @Test + public void testUpdateTenantError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"INTERNAL_ERROR\"}}"); + Tenant.UpdateRequest request = + new Tenant.UpdateRequest("TENANT_1").setDisplayName("DISPLAY_NAME"); + try { + FirebaseAuth.getInstance().getTenantManager().updateTenant(request); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.INTERNAL_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "PATCH", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenant() throws Exception { + TestResponseInterceptor interceptor = initializeAppForTenantManagement("{}"); + + FirebaseAuth.getInstance().getTenantManager().deleteTenant("TENANT_1"); + + checkRequestHeaders(interceptor); + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/TENANT_1"); + } + + @Test + public void testDeleteTenantWithNotFoundError() { + TestResponseInterceptor interceptor = + initializeAppForTenantManagementWithStatusCode(404, + "{\"error\": {\"message\": \"TENANT_NOT_FOUND\"}}"); + try { + FirebaseAuth.getInstance().getTenantManager().deleteTenant("UNKNOWN"); + fail("No error thrown for invalid response"); + } catch (FirebaseAuthException e) { + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, e.getErrorCode()); + } + checkUrl(interceptor, "DELETE", TENANTS_BASE_URL + "/UNKNOWN"); + } + + private static void checkTenant(Tenant tenant, String tenantId) { + assertEquals(tenantId, tenant.getTenantId()); + assertEquals("DISPLAY_NAME", tenant.getDisplayName()); + assertTrue(tenant.isPasswordSignInAllowed()); + assertFalse(tenant.isEmailLinkSignInEnabled()); + } + + private static void checkRequestHeaders(TestResponseInterceptor interceptor) { + HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders(); + String auth = "Bearer " + TEST_TOKEN; + assertEquals(auth, headers.getFirstHeaderStringValue("Authorization")); + + String clientVersion = "Java/Admin/" + SdkUtils.getVersion(); + assertEquals(clientVersion, headers.getFirstHeaderStringValue("X-Client-Version")); + } + + private static void checkUrl(TestResponseInterceptor interceptor, String method, String url) { + HttpRequest request = interceptor.getResponse().getRequest(); + if (method.equals("PATCH")) { + assertEquals("PATCH", + request.getHeaders().getFirstHeaderStringValue("X-HTTP-Method-Override")); + assertEquals("POST", request.getRequestMethod()); + } else { + assertEquals(method, request.getRequestMethod()); + } + assertEquals(url, request.getUrl().toString().split("\\?")[0]); + } + + private static TestResponseInterceptor initializeAppForTenantManagement(String... responses) { + initializeAppWithResponses(responses); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static TestResponseInterceptor initializeAppForTenantManagementWithStatusCode( + int statusCode, String response) { + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport( + new MockHttpTransport.Builder() + .setLowLevelHttpResponse( + new MockLowLevelHttpResponse().setContent(response).setStatusCode(statusCode)) + .build()) + .setProjectId("test-project-id") + .build()); + TestResponseInterceptor interceptor = new TestResponseInterceptor(); + FirebaseAuth.getInstance().getTenantManager().setInterceptor(interceptor); + return interceptor; + } + + private static void initializeAppWithResponses(String... responses) { + List mocks = new ArrayList<>(); + for (String response : responses) { + mocks.add(new MockLowLevelHttpResponse().setContent(response)); + } + MockHttpTransport transport = new MultiRequestMockHttpTransport(mocks); + FirebaseApp.initializeApp(new FirebaseOptions.Builder() + .setCredentials(credentials) + .setHttpTransport(transport) + .setProjectId("test-project-id") + .build()); + } + + private static GenericJson parseRequestContent(TestResponseInterceptor interceptor) + throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + interceptor.getResponse().getRequest().getContent().writeTo(out); + return JSON_FACTORY.fromString(new String(out.toByteArray()), GenericJson.class); + } +} diff --git a/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java similarity index 98% rename from src/test/java/com/google/firebase/auth/ListTenantsPageTest.java rename to src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java index c1d09bc52..99a9786d6 100644 --- a/src/test/java/com/google/firebase/auth/ListTenantsPageTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/ListTenantsPageTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static junit.framework.TestCase.assertTrue; import static org.junit.Assert.assertEquals; @@ -24,10 +24,11 @@ import static org.junit.Assert.fail; import com.google.api.client.googleapis.util.Utils; -import com.google.api.client.json.JsonFactory; import com.google.common.collect.ImmutableList; -import com.google.common.io.BaseEncoding; +import com.google.firebase.auth.FirebaseAuthException; import com.google.firebase.auth.internal.ListTenantsResponse; +import com.google.firebase.auth.multitenancy.ListTenantsPage; +import com.google.firebase.auth.multitenancy.Tenant; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; diff --git a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java similarity index 96% rename from src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java rename to src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java index a6d83dccf..61a841902 100644 --- a/src/test/java/com/google/firebase/auth/TenantAwareFirebaseAuthIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantAwareFirebaseAuthIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -39,7 +39,18 @@ import com.google.common.collect.ImmutableMap; import com.google.common.util.concurrent.MoreExecutors; import com.google.firebase.FirebaseApp; +import com.google.firebase.auth.ExportedUserRecord; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.FirebaseToken; +import com.google.firebase.auth.ListProviderConfigsPage; +import com.google.firebase.auth.ListUsersPage; +import com.google.firebase.auth.OidcProviderConfig; +import com.google.firebase.auth.ProviderConfigTestUtils; import com.google.firebase.auth.ProviderConfigTestUtils.TemporaryProviderConfig; +import com.google.firebase.auth.SamlProviderConfig; +import com.google.firebase.auth.UserRecord; +import com.google.firebase.auth.UserTestUtils; import com.google.firebase.auth.UserTestUtils.RandomUser; import com.google.firebase.auth.UserTestUtils.TemporaryUser; import com.google.firebase.internal.Nullable; @@ -56,7 +67,6 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExternalResource; public class TenantAwareFirebaseAuthIT { @@ -247,7 +257,7 @@ public void testVerifyTokenWithWrongTenantAwareClient() throws Exception { fail("No error thrown for verifying a token with the wrong tenant-aware client"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.TENANT_ID_MISMATCH_ERROR, + assertEquals("tenant-id-mismatch", ((FirebaseAuthException) e.getCause()).getErrorCode()); } } @@ -420,7 +430,7 @@ private String signInWithCustomToken( String customToken, @Nullable String tenantId) throws IOException { final GenericUrl url = new GenericUrl(VERIFY_CUSTOM_TOKEN_URL + "?key=" + IntegrationTestUtils.getApiKey()); - ImmutableMap.Builder content = ImmutableMap.builder(); + ImmutableMap.Builder content = ImmutableMap.builder(); content.put("token", customToken); content.put("returnSecureToken", true); if (tenantId != null) { diff --git a/src/test/java/com/google/firebase/auth/TenantManagerIT.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java similarity index 95% rename from src/test/java/com/google/firebase/auth/TenantManagerIT.java rename to src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java index 6d96a114d..086e25aa2 100644 --- a/src/test/java/com/google/firebase/auth/TenantManagerIT.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantManagerIT.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -27,6 +27,9 @@ import com.google.api.core.ApiFutureCallback; import com.google.api.core.ApiFutures; import com.google.common.util.concurrent.MoreExecutors; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.auth.FirebaseAuthException; +import com.google.firebase.auth.internal.AuthHttpClient; import com.google.firebase.testing.IntegrationTestUtils; import java.util.ArrayList; import java.util.List; @@ -78,7 +81,7 @@ public void testTenantLifecycle() throws Exception { fail("No error thrown for getting a deleted tenant"); } catch (ExecutionException e) { assertTrue(e.getCause() instanceof FirebaseAuthException); - assertEquals(FirebaseUserManager.TENANT_NOT_FOUND_ERROR, + assertEquals(AuthHttpClient.TENANT_NOT_FOUND_ERROR, ((FirebaseAuthException) e.getCause()).getErrorCode()); } } diff --git a/src/test/java/com/google/firebase/auth/TenantTest.java b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java similarity index 97% rename from src/test/java/com/google/firebase/auth/TenantTest.java rename to src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java index f47ccf8fa..39b7de312 100644 --- a/src/test/java/com/google/firebase/auth/TenantTest.java +++ b/src/test/java/com/google/firebase/auth/multitenancy/TenantTest.java @@ -14,9 +14,8 @@ * limitations under the License. */ -package com.google.firebase.auth; +package com.google.firebase.auth.multitenancy; -import static com.google.firebase.auth.Tenant.UpdateRequest; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; @@ -24,6 +23,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.json.JsonFactory; +import com.google.firebase.auth.multitenancy.Tenant; import java.io.IOException; import java.util.Map; import org.junit.Test;