Skip to content

Commit 2679904

Browse files
authored
Create TenantManager class and wire through listTenants operation. (#369)
Add the TenantManager class and wire through the listTenants operation. Also add unit tests to FirebaseUserManagerTest.
1 parent 4453362 commit 2679904

File tree

8 files changed

+222
-24
lines changed

8 files changed

+222
-24
lines changed

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -690,8 +690,7 @@ protected UserRecord execute() throws FirebaseAuthException {
690690
* UpdateRequest}.
691691
*
692692
* @param request A non-null {@link UpdateRequest} instance.
693-
* @return A {@link UserRecord} instance corresponding to the updated user account. account, the
694-
* task fails with a {@link FirebaseAuthException}.
693+
* @return A {@link UserRecord} instance corresponding to the updated user account.
695694
* @throws NullPointerException if the provided update request is null.
696695
* @throws FirebaseAuthException if an error occurs while updating the user account.
697696
*/
@@ -1053,7 +1052,6 @@ public ApiFuture<String> generateSignInWithEmailLinkAsync(
10531052
.callAsync(firebaseApp);
10541053
}
10551054

1056-
@VisibleForTesting
10571055
FirebaseUserManager getUserManager() {
10581056
return this.userManager.get();
10591057
}
@@ -1074,7 +1072,7 @@ protected String execute() throws FirebaseAuthException {
10741072
};
10751073
}
10761074

1077-
private <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
1075+
protected <T> Supplier<T> threadSafeMemoize(final Supplier<T> supplier) {
10781076
checkNotNull(supplier);
10791077
return Suppliers.memoize(
10801078
new Supplier<T>() {

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

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,29 @@
3131
* then use it to perform a variety of authentication-related operations, including generating
3232
* custom tokens for use by client-side code, verifying Firebase ID Tokens received from clients, or
3333
* creating new FirebaseApp instances that are scoped to a particular authentication UID.
34-
*
35-
* <p>TODO(micahstairs): Add getTenantManager() method.
3634
*/
3735
public class FirebaseAuth extends AbstractFirebaseAuth {
3836

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

41-
private FirebaseAuth(Builder builder) {
39+
private final Supplier<TenantManager> tenantManager;
40+
41+
private FirebaseAuth(final Builder builder) {
4242
super(
4343
builder.firebaseApp,
4444
builder.tokenFactory,
4545
builder.idTokenVerifier,
4646
builder.cookieVerifier);
47+
tenantManager = threadSafeMemoize(new Supplier<TenantManager>() {
48+
@Override
49+
public TenantManager get() {
50+
return new TenantManager(builder.firebaseApp, getUserManager());
51+
}
52+
});
53+
}
54+
55+
public TenantManager getTenantManager() {
56+
return tenantManager.get();
4757
}
4858

4959
/**

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,8 @@
5757
/**
5858
* FirebaseUserManager provides methods for interacting with the Google Identity Toolkit via its
5959
* REST API. This class does not hold any mutable state, and is thread safe.
60-
*
61-
* <p>TODO(micahstairs): Consider renaming this to FirebaseAuthManager since this also supports
62-
* tenants.
60+
*
61+
* <p>TODO(micahstairs): Rename this class to IdentityToolkitClient.
6362
*
6463
* @see <a href="https://developers.google.com/identity/toolkit/web/reference/relyingparty">
6564
* Google Identity Toolkit</a>
@@ -227,7 +226,8 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep
227226
return new UserImportResult(request.getUsersCount(), response);
228227
}
229228

230-
ListTenantsResponse listTenants(int maxResults, String pageToken) throws FirebaseAuthException {
229+
ListTenantsResponse listTenants(int maxResults, String pageToken)
230+
throws FirebaseAuthException {
231231
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
232232
.put("pageSize", maxResults);
233233
if (pageToken != null) {

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

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,10 @@
3232

3333
/**
3434
* Represents a page of {@link Tenant} instances.
35-
*
35+
*
3636
* <p>Provides methods for iterating over the tenants in the current page, and calling up
3737
* subsequent pages of tenants.
38-
*
38+
*
3939
* <p>Instances of this class are thread-safe and immutable.
4040
*/
4141
public class ListTenantsPage implements Page<Tenant> {
@@ -65,7 +65,7 @@ public boolean hasNextPage() {
6565

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

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

201201
private final FirebaseUserManager userManager;
202-
private final JsonFactory jsonFactory;
203202

204-
DefaultTenantSource(FirebaseUserManager userManager, JsonFactory jsonFactory) {
203+
DefaultTenantSource(FirebaseUserManager userManager) {
205204
this.userManager = checkNotNull(userManager, "user manager must not be null");
206-
this.jsonFactory = checkNotNull(jsonFactory, "json factory must not be null");
207205
}
208206

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

216214
/**
217215
* A simple factory class for {@link ListTenantsPage} instances.
218-
*
216+
*
219217
* <p>Performs argument validation before attempting to load any tenant data (which is expensive,
220218
* and hence may be performed asynchronously on a separate thread).
221219
*/
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2020 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.firebase.auth;
18+
19+
import static com.google.common.base.Preconditions.checkNotNull;
20+
21+
import com.google.api.client.json.JsonFactory;
22+
import com.google.api.core.ApiFuture;
23+
import com.google.firebase.FirebaseApp;
24+
import com.google.firebase.auth.ListTenantsPage.DefaultTenantSource;
25+
import com.google.firebase.auth.ListTenantsPage.PageFactory;
26+
import com.google.firebase.auth.ListTenantsPage.TenantSource;
27+
import com.google.firebase.auth.Tenant.CreateRequest;
28+
import com.google.firebase.auth.Tenant.UpdateRequest;
29+
import com.google.firebase.internal.CallableOperation;
30+
import com.google.firebase.internal.NonNull;
31+
import com.google.firebase.internal.Nullable;
32+
33+
/**
34+
* This class can be used to perform a variety of tenant-related operations, including creating,
35+
* updating, and listing tenants.
36+
*
37+
* <p>TODO(micahstairs): Implement the following methods: getAuthForTenant(), getTenant(),
38+
* deleteTenant(), createTenant(), and updateTenant().
39+
*/
40+
public final class TenantManager {
41+
42+
private final FirebaseApp firebaseApp;
43+
private final FirebaseUserManager userManager;
44+
45+
TenantManager(FirebaseApp firebaseApp, FirebaseUserManager userManager) {
46+
this.firebaseApp = firebaseApp;
47+
this.userManager = userManager;
48+
}
49+
50+
/**
51+
* Gets a page of tenants starting from the specified {@code pageToken}. Page size will be limited
52+
* to 1000 tenants.
53+
*
54+
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
55+
* @return A {@link ListTenantsPage} instance.
56+
* @throws IllegalArgumentException If the specified page token is empty.
57+
* @throws FirebaseAuthException If an error occurs while retrieving tenant data.
58+
*/
59+
public ListTenantsPage listTenants(@Nullable String pageToken) throws FirebaseAuthException {
60+
return listTenants(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
61+
}
62+
63+
/**
64+
* Gets a page of tenants starting from the specified {@code pageToken}.
65+
*
66+
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
67+
* @param maxResults Maximum number of tenants to include in the returned page. This may not
68+
* exceed 1000.
69+
* @return A {@link ListTenantsPage} instance.
70+
* @throws IllegalArgumentException If the specified page token is empty, or max results value is
71+
* invalid.
72+
* @throws FirebaseAuthException If an error occurs while retrieving tenant data.
73+
*/
74+
public ListTenantsPage listTenants(@Nullable String pageToken, int maxResults)
75+
throws FirebaseAuthException {
76+
return listTenantsOp(pageToken, maxResults).call();
77+
}
78+
79+
/**
80+
* Similar to {@link #listTenants(String)} but performs the operation asynchronously.
81+
*
82+
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
83+
* @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage}
84+
* instance. If an error occurs while retrieving tenant data, the future throws an exception.
85+
* @throws IllegalArgumentException If the specified page token is empty.
86+
*/
87+
public ApiFuture<ListTenantsPage> listTenantsAsync(@Nullable String pageToken) {
88+
return listTenantsAsync(pageToken, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
89+
}
90+
91+
/**
92+
* Similar to {@link #listTenants(String, int)} but performs the operation asynchronously.
93+
*
94+
* @param pageToken A non-empty page token string, or null to retrieve the first page of tenants.
95+
* @param maxResults Maximum number of tenants to include in the returned page. This may not
96+
* exceed 1000.
97+
* @return An {@code ApiFuture} which will complete successfully with a {@link ListTenantsPage}
98+
* instance. If an error occurs while retrieving tenant data, the future throws an exception.
99+
* @throws IllegalArgumentException If the specified page token is empty, or max results value is
100+
* invalid.
101+
*/
102+
public ApiFuture<ListTenantsPage> listTenantsAsync(@Nullable String pageToken, int maxResults) {
103+
return listTenantsOp(pageToken, maxResults).callAsync(firebaseApp);
104+
}
105+
106+
private CallableOperation<ListTenantsPage, FirebaseAuthException> listTenantsOp(
107+
@Nullable final String pageToken, final int maxResults) {
108+
// TODO(micahstairs): Add a check to make sure the app has not been destroyed yet.
109+
final TenantSource tenantSource = new DefaultTenantSource(userManager);
110+
final PageFactory factory = new PageFactory(tenantSource, maxResults, pageToken);
111+
return new CallableOperation<ListTenantsPage, FirebaseAuthException>() {
112+
@Override
113+
protected ListTenantsPage execute() throws FirebaseAuthException {
114+
return factory.create();
115+
}
116+
};
117+
}
118+
}

src/main/java/com/google/firebase/auth/internal/ListTenantsResponse.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,24 @@
1717
package com.google.firebase.auth.internal;
1818

1919
import com.google.api.client.util.Key;
20+
import com.google.common.annotations.VisibleForTesting;
21+
import com.google.common.collect.ImmutableList;
22+
import com.google.firebase.auth.ListTenantsPage;
2023
import com.google.firebase.auth.Tenant;
2124
import java.util.List;
2225

2326
/**
2427
* JSON data binding for ListTenantsResponse messages sent by Google identity toolkit service.
2528
*/
26-
public class ListTenantsResponse {
29+
public final class ListTenantsResponse {
2730

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

3134
@Key("pageToken")
3235
private String pageToken;
3336

37+
@VisibleForTesting
3438
public ListTenantsResponse(List<Tenant> tenants, String pageToken) {
3539
this.tenants = tenants;
3640
this.pageToken = pageToken;
@@ -39,14 +43,14 @@ public ListTenantsResponse(List<Tenant> tenants, String pageToken) {
3943
public ListTenantsResponse() { }
4044

4145
public List<Tenant> getTenants() {
42-
return tenants;
46+
return tenants == null ? ImmutableList.<Tenant>of() : tenants;
4347
}
4448

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

4953
public String getPageToken() {
50-
return pageToken;
54+
return pageToken == null ? "" : pageToken;
5155
}
5256
}

src/test/java/com/google/firebase/auth/FirebaseUserManagerTest.java

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,52 @@ public void testImportUsersLargeList() {
430430
}
431431
}
432432

433+
@Test
434+
public void testListTenants() throws Exception {
435+
final TestResponseInterceptor interceptor = initializeAppForUserManagement(
436+
TestUtils.loadResource("listTenants.json"));
437+
ListTenantsPage page =
438+
FirebaseAuth.getInstance().getTenantManager().listTenantsAsync(null, 999).get();
439+
ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
440+
assertEquals(2, tenants.size());
441+
checkTenant(tenants.get(0), "TENANT_1");
442+
checkTenant(tenants.get(1), "TENANT_2");
443+
assertEquals("", page.getNextPageToken());
444+
checkRequestHeaders(interceptor);
445+
446+
GenericUrl url = interceptor.getResponse().getRequest().getUrl();
447+
assertEquals(999, url.getFirst("pageSize"));
448+
assertNull(url.getFirst("pageToken"));
449+
}
450+
451+
@Test
452+
public void testListTenantsWithPageToken() throws Exception {
453+
final TestResponseInterceptor interceptor = initializeAppForUserManagement(
454+
TestUtils.loadResource("listTenants.json"));
455+
ListTenantsPage page =
456+
FirebaseAuth.getInstance().getTenantManager().listTenantsAsync("token", 999).get();
457+
ImmutableList<Tenant> tenants = ImmutableList.copyOf(page.getValues());
458+
assertEquals(2, tenants.size());
459+
checkTenant(tenants.get(0), "TENANT_1");
460+
checkTenant(tenants.get(1), "TENANT_2");
461+
assertEquals("", page.getNextPageToken());
462+
checkRequestHeaders(interceptor);
463+
464+
GenericUrl url = interceptor.getResponse().getRequest().getUrl();
465+
assertEquals(999, url.getFirst("pageSize"));
466+
assertEquals("token", url.getFirst("pageToken"));
467+
}
468+
469+
@Test
470+
public void testListZeroTenants() throws Exception {
471+
final TestResponseInterceptor interceptor = initializeAppForUserManagement("{}");
472+
ListTenantsPage page =
473+
FirebaseAuth.getInstance().getTenantManager().listTenantsAsync(null).get();
474+
assertTrue(Iterables.isEmpty(page.getValues()));
475+
assertEquals("", page.getNextPageToken());
476+
checkRequestHeaders(interceptor);
477+
}
478+
433479
@Test
434480
public void testCreateSessionCookie() throws Exception {
435481
TestResponseInterceptor interceptor = initializeAppForUserManagement(
@@ -1264,6 +1310,13 @@ private static void checkUserRecord(UserRecord userRecord) {
12641310
assertEquals("gold", claims.get("package"));
12651311
}
12661312

1313+
private static void checkTenant(Tenant tenant, String tenantId) {
1314+
assertEquals(tenantId, tenant.getTenantId());
1315+
assertEquals("DISPLAY_NAME", tenant.getDisplayName());
1316+
assertTrue(tenant.isPasswordSignInAllowed());
1317+
assertFalse(tenant.isEmailLinkSignInEnabled());
1318+
}
1319+
12671320
private static void checkRequestHeaders(TestResponseInterceptor interceptor) {
12681321
HttpHeaders headers = interceptor.getResponse().getRequest().getHeaders();
12691322
String auth = "Bearer " + TEST_TOKEN;
@@ -1276,5 +1329,5 @@ private static void checkRequestHeaders(TestResponseInterceptor interceptor) {
12761329
private interface UserManagerOp {
12771330
void call(FirebaseAuth auth) throws Exception;
12781331
}
1279-
1332+
12801333
}

src/test/resources/listTenants.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"tenants" : [ {
3+
"name" : "TENANT_1",
4+
"displayName" : "DISPLAY_NAME",
5+
"allowPasswordSignup" : true,
6+
"enableEmailLinkSignin" : false,
7+
"disableAuth" : true,
8+
"enableAnonymousUser" : false
9+
}, {
10+
"name" : "TENANT_2",
11+
"displayName" : "DISPLAY_NAME",
12+
"allowPasswordSignup" : true,
13+
"enableEmailLinkSignin" : false,
14+
"disableAuth" : true,
15+
"enableAnonymousUser" : false
16+
} ]
17+
}

0 commit comments

Comments
 (0)