Skip to content

Create TenantManager class and wire through listTenants operation. #369

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -690,8 +690,7 @@ protected UserRecord execute() throws FirebaseAuthException {
* UpdateRequest}.
*
* @param request A non-null {@link UpdateRequest} instance.
* @return A {@link UserRecord} instance corresponding to the updated user account. account, the
* task fails with a {@link FirebaseAuthException}.
* @return A {@link UserRecord} instance corresponding to the updated user account.
* @throws NullPointerException if the provided update request is null.
* @throws FirebaseAuthException if an error occurs while updating the user account.
*/
Expand Down Expand Up @@ -1053,7 +1052,6 @@ public ApiFuture<String> generateSignInWithEmailLinkAsync(
.callAsync(firebaseApp);
}

@VisibleForTesting
FirebaseUserManager getUserManager() {
return this.userManager.get();
}
Expand All @@ -1074,7 +1072,7 @@ protected String execute() throws FirebaseAuthException {
};
}

private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
protected <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
checkNotNull(supplier);
return Suppliers.memoize(
new Supplier<T>() {
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/com/google/firebase/auth/FirebaseAuth.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,19 +31,29 @@
* then use it to perform a variety of authentication-related operations, including generating
* custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or
* creating new FirebaseApp instances that are scoped to a particular authentication UID.
*
* <p>TODO(micahstairs): Add getTenantManager() method.
*/
public class FirebaseAuth extends AbstractFirebaseAuth {

private static final String SERVICE_ID = FirebaseAuth.class.getName();

private FirebaseAuth(Builder builder) {
private final Supplier<TenantManager> tenantManager;

private FirebaseAuth(final Builder builder) {
super(
builder.firebaseApp,
builder.tokenFactory,
builder.idTokenVerifier,
builder.cookieVerifier);
tenantManager = threadSafeMemoize(new Supplier<TenantManager>() {
@Override
public TenantManager get() {
return new TenantManager(builder.firebaseApp, getUserManager());
}
});
}

public TenantManager getTenantManager() {
return tenantManager.get();
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,8 @@
/**
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
* REST API. This class does not hold any mutable state, and is thread safe.
*
* <p>TODO(micahstairs): Consider renaming this to FirebaseAuthManager since this also supports
* tenants.
*
* <p>TODO(micahstairs): Rename this class to IdentityToolkitClient.
*
* @see <a href="https://developers.google.com/identity/toolkit/web/reference/relyingparty">
* Google Identity Toolkit</a>
Expand Down Expand Up @@ -227,7 +226,8 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep
return new UserImportResult(request.getUsersCount(), response);
}

ListTenantsResponse listTenants(int maxResults, String pageToken) throws FirebaseAuthException {
ListTenantsResponse listTenants(int maxResults, String pageToken)
throws FirebaseAuthException {
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
.put("pageSize", maxResults);
if (pageToken != null) {
Expand Down
16 changes: 7 additions & 9 deletions src/main/java/com/google/firebase/auth/ListTenantsPage.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@

/**
* Represents a page of {@link Tenant} instances.
*
*
* <p>Provides methods for iterating over the tenants in the current page, and calling up
* subsequent pages of tenants.
*
*
* <p>Instances of this class are thread-safe and immutable.
*/
public class ListTenantsPage implements Page<Tenant> {
Expand Down Expand Up @@ -65,7 +65,7 @@ public boolean hasNextPage() {

/**
* Returns the string token that identifies the next page.
*
*
* <p>Never returns null. Returns empty string if there are no more pages available to be
* retrieved.
*
Expand Down Expand Up @@ -99,7 +99,7 @@ public ListTenantsPage getNextPage() {
/**
* Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in
* the current Firebase project, starting from this page.
*
*
* <p>The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more
* than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops)
* at any time.
Expand Down Expand Up @@ -139,7 +139,7 @@ public Iterator<Tenant> iterator() {

/**
* An {@link Iterator} that cycles through tenants, one at a time.
*
*
* <p>It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter
* is an upper bound on the batch size.
*/
Expand Down Expand Up @@ -199,11 +199,9 @@ ListTenantsResponse fetch(int maxResults, String pageToken)
static class DefaultTenantSource implements TenantSource {

private final FirebaseUserManager userManager;
private final JsonFactory jsonFactory;

DefaultTenantSource(FirebaseUserManager userManager, JsonFactory jsonFactory) {
DefaultTenantSource(FirebaseUserManager userManager) {
this.userManager = checkNotNull(userManager, "user manager must not be null");
this.jsonFactory = checkNotNull(jsonFactory, "json factory must not be null");
}

@Override
Expand All @@ -215,7 +213,7 @@ public ListTenantsResponse fetch(int maxResults, String pageToken)

/**
* A simple factory class for {@link ListTenantsPage} instances.
*
*
* <p>Performs argument validation before attempting to load any tenant data (which is expensive,
* and hence may be performed asynchronously on a separate thread).
*/
Expand Down
118 changes: 118 additions & 0 deletions src/main/java/com/google/firebase/auth/TenantManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* 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.checkNotNull;

import com.google.api.client.json.JsonFactory;
import com.google.api.core.ApiFuture;
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.internal.CallableOperation;
import com.google.firebase.internal.NonNull;
import com.google.firebase.internal.Nullable;

/**
* This class can be used to perform a variety of tenant-related operations, including creating,
* updating, and listing tenants.
*
* <p>TODO(micahstairs): Implement the following methods: getAuthForTenant(), getTenant(),
* deleteTenant(), createTenant(), and updateTenant().
*/
public final class TenantManager {

private final FirebaseApp firebaseApp;
private final FirebaseUserManager userManager;

TenantManager(FirebaseApp firebaseApp, FirebaseUserManager userManager) {
this.firebaseApp = firebaseApp;
this.userManager = userManager;
}

/**
* Gets a page of tenants starting from the specified {@code pageToken}. Page size will be limited
* to 1000 tenants.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
* @return A {@link ListTenantsPage} instance.
* @throws IllegalArgumentException If the specified page token is empty.
* @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);
}

/**
* Gets a page of tenants starting from the specified {@code pageToken}.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
* @param maxResults Maximum number of tenants to include in the returned page. This may not
* exceed 1000.
* @return A {@link ListTenantsPage} instance.
* @throws IllegalArgumentException If the specified page token is empty, or max results value is
* invalid.
* @throws FirebaseAuthException If an error occurs while retrieving tenant data.
*/
public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults)
throws FirebaseAuthException {
return listTenantsOp(pageToken, maxResults).call();
}

/**
* Similar to {@link #listTenants(String)} but performs the operation asynchronously.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
* @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage}
* instance. If an error occurs while retrieving tenant data, the future throws an exception.
* @throws IllegalArgumentException If the specified page token is empty.
*/
public ApiFuture<ListTenantsPage> listTenantsAsync(@Nullable String pageToken) {
return listTenantsAsync(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
}

/**
* Similar to {@link #listTenants(String, int)} but performs the operation asynchronously.
*
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
* @param maxResults Maximum number of tenants to include in the returned page. This may not
* exceed 1000.
* @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage}
* instance. If an error occurs while retrieving tenant data, the future throws an exception.
* @throws IllegalArgumentException If the specified page token is empty, or max results value is
* invalid.
*/
public ApiFuture<ListTenantsPage> listTenantsAsync(@Nullable String pageToken, int maxResults) {
return listTenantsOp(pageToken, maxResults).callAsync(firebaseApp);
}

private CallableOperation<ListTenantsPage, FirebaseAuthException> listTenantsOp(
@Nullable final String pageToken, final int maxResults) {
// TODO(micahstairs): Add a check to make sure the app has not been destroyed yet.
final TenantSource tenantSource = new DefaultTenantSource(userManager);
final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken);
return new CallableOperation<ListTenantsPage, FirebaseAuthException>() {
@Override
protected ListTenantsPage execute() throws FirebaseAuthException {
return factory.create();
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,24 @@
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.ListTenantsPage;
import com.google.firebase.auth.Tenant;
import java.util.List;

/**
* JSON data binding for ListTenantsResponse messages sent by Google identity toolkit service.
*/
public class ListTenantsResponse {
public final class ListTenantsResponse {

@Key("tenants")
private List<Tenant> tenants;

@Key("pageToken")
private String pageToken;

@VisibleForTesting
public ListTenantsResponse(List<Tenant> tenants, String pageToken) {
this.tenants = tenants;
this.pageToken = pageToken;
Expand All @@ -39,14 +43,14 @@ public ListTenantsResponse(List<Tenant> tenants, String pageToken) {
public ListTenantsResponse() { }

public List<Tenant> getTenants() {
return tenants;
return tenants == null ? ImmutableList.<Tenant>of() : tenants;
}

public boolean hasTenants() {
return tenants != null && !tenants.isEmpty();
}

public String getPageToken() {
return pageToken;
return pageToken == null ? "" : pageToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,52 @@ public void testImportUsersLargeList() {
}
}

@Test
public void testListTenants() throws Exception {
final TestResponseInterceptor interceptor = initializeAppForUserManagement(
TestUtils.loadResource("listTenants.json"));
ListTenantsPage page =
FirebaseAuth.getInstance().getTenantManager().listTenantsAsync(null, 999).get();
ImmutableList<Tenant> 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);

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().listTenantsAsync("token", 999).get();
ImmutableList<Tenant> 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);

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().listTenantsAsync(null).get();
assertTrue(Iterables.isEmpty(page.getValues()));
assertEquals("", page.getNextPageToken());
checkRequestHeaders(interceptor);
}

@Test
public void testCreateSessionCookie() throws Exception {
TestResponseInterceptor interceptor = initializeAppForUserManagement(
Expand Down Expand Up @@ -1264,6 +1310,13 @@ 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 checkRequestHeaders(TestResponseInterceptor interceptor) {
HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders();
String auth = "Bearer " + TEST_TOKEN;
Expand All @@ -1276,5 +1329,5 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) {
private interface UserManagerOp {
void call(FirebaseAuth auth) throws Exception;
}

}
17 changes: 17 additions & 0 deletions src/test/resources/listTenants.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"tenants" : [ {
"name" : "TENANT_1",
"displayName" : "DISPLAY_NAME",
"allowPasswordSignup" : true,
"enableEmailLinkSignin" : false,
"disableAuth" : true,
"enableAnonymousUser" : false
}, {
"name" : "TENANT_2",
"displayName" : "DISPLAY_NAME",
"allowPasswordSignup" : true,
"enableEmailLinkSignin" : false,
"disableAuth" : true,
"enableAnonymousUser" : false
} ]
}