Skip to content

Commit d30a4d0

Browse files
committed
Add ListTenantsPage class.
1 parent d76fc20 commit d30a4d0

File tree

5 files changed

+721
-12
lines changed

5 files changed

+721
-12
lines changed

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

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242
import com.google.firebase.auth.UserRecord.UpdateRequest;
4343
import com.google.firebase.auth.internal.DownloadAccountResponse;
4444
import com.google.firebase.auth.internal.GetAccountInfoResponse;
45-
4645
import com.google.firebase.auth.internal.HttpErrorResponse;
46+
import com.google.firebase.auth.internal.ListTenantsResponse;
4747
import com.google.firebase.auth.internal.UploadAccountResponse;
4848
import com.google.firebase.internal.FirebaseRequestInitializer;
4949
import com.google.firebase.internal.NonNull;
@@ -57,6 +57,9 @@
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.
6063
*
6164
* @see <a href="https://developers.google.com/identity/toolkit/web/reference/relyingparty">
6265
* Google Identity Toolkit</a>
@@ -87,6 +90,7 @@ class FirebaseUserManager {
8790
.put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
8891
.build();
8992

93+
static final int MAX_LIST_TENANTS_RESULTS = 1000;
9094
static final int MAX_LIST_USERS_RESULTS = 1000;
9195
static final int MAX_IMPORT_USERS = 1000;
9296

@@ -95,10 +99,11 @@ class FirebaseUserManager {
9599
"iss", "jti", "nbf", "nonce", "sub", "firebase");
96100

97101
private static final String ID_TOOLKIT_URL =
98-
"https://identitytoolkit.googleapis.com/v1/projects/%s";
102+
"https://identitytoolkit.googleapis.com/%s/projects/%s";
99103
private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
100104

101-
private final String baseUrl;
105+
private final String userBaseUrl;
106+
private final String tenantBaseUrl;
102107
private final JsonFactory jsonFactory;
103108
private final HttpRequestFactory requestFactory;
104109
private final String clientVersion = "Java/Admin/" + SdkUtils.getVersion();
@@ -117,7 +122,8 @@ class FirebaseUserManager {
117122
"Project ID is required to access the auth service. Use a service account credential or "
118123
+ "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
119124
+ "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
120-
this.baseUrl = String.format(ID_TOOLKIT_URL, projectId);
125+
this.userBaseUrl = String.format(ID_TOOLKIT_URL, "v1", projectId);
126+
this.tenantBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
121127
this.jsonFactory = app.getOptions().getJsonFactory();
122128
HttpTransport transport = app.getOptions().getHttpTransport();
123129
this.requestFactory = transport.createRequestFactory(new FirebaseRequestInitializer(app));
@@ -201,7 +207,7 @@ DownloadAccountResponse listUsers(int maxResults, String pageToken) throws Fireb
201207
builder.put("nextPageToken", pageToken);
202208
}
203209

204-
GenericUrl url = new GenericUrl(baseUrl + "/accounts:batchGet");
210+
GenericUrl url = new GenericUrl(userBaseUrl + "/accounts:batchGet");
205211
url.putAll(builder.build());
206212
DownloadAccountResponse response = sendRequest(
207213
"GET", url, null, DownloadAccountResponse.class);
@@ -221,6 +227,24 @@ UserImportResult importUsers(UserImportRequest request) throws FirebaseAuthExcep
221227
return new UserImportResult(request.getUsersCount(), response);
222228
}
223229

230+
ListTenantsResponse listTenants(int maxResults, String pageToken) throws FirebaseAuthException {
231+
ImmutableMap.Builder<String, Object> builder = ImmutableMap.<String, Object>builder()
232+
.put("maxResults", maxResults);
233+
if (pageToken != null) {
234+
checkArgument(!pageToken.equals(
235+
ListTenantsPage.END_OF_LIST), "invalid end of list page token");
236+
builder.put("nextPageToken", pageToken);
237+
}
238+
239+
GenericUrl url = new GenericUrl(tenantBaseUrl + "/tenants:tenants");
240+
url.putAll(builder.build());
241+
ListTenantsResponse response = sendRequest("GET", url, null, ListTenantsResponse.class);
242+
if (response == null) {
243+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve tenants.");
244+
}
245+
return response;
246+
}
247+
224248
String createSessionCookie(String idToken,
225249
SessionCookieOptions options) throws FirebaseAuthException {
226250
final Map<String, Object> payload = ImmutableMap.<String, Object>of(
@@ -257,7 +281,7 @@ String getEmailActionLink(EmailLinkType type, String email,
257281
private <T> T post(String path, Object content, Class<T> clazz) throws FirebaseAuthException {
258282
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
259283
checkNotNull(content, "content must not be null for POST requests");
260-
GenericUrl url = new GenericUrl(baseUrl + path);
284+
GenericUrl url = new GenericUrl(userBaseUrl + path);
261285
return sendRequest("POST", url, content, clazz);
262286
}
263287

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
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.checkArgument;
20+
import static com.google.common.base.Preconditions.checkNotNull;
21+
22+
import com.google.api.client.json.JsonFactory;
23+
import com.google.api.gax.paging.Page;
24+
import com.google.common.collect.ImmutableList;
25+
import com.google.firebase.auth.internal.DownloadAccountResponse;
26+
import com.google.firebase.auth.internal.ListTenantsResponse;
27+
import com.google.firebase.internal.NonNull;
28+
import com.google.firebase.internal.Nullable;
29+
import java.util.Iterator;
30+
import java.util.List;
31+
import java.util.NoSuchElementException;
32+
33+
/**
34+
* Represents a page of {@link Tenant} instances.
35+
*
36+
* <p>Provides methods for iterating over the tenants in the current page, and calling up
37+
* subsequent pages of tenants.
38+
*
39+
* <p>Instances of this class are thread-safe and immutable.
40+
*/
41+
public class ListTenantsPage implements Page<Tenant> {
42+
43+
static final String END_OF_LIST = "";
44+
45+
private final ListTenantsResult currentBatch;
46+
private final TenantSource source;
47+
private final int maxResults;
48+
49+
private ListTenantsPage(
50+
@NonNull ListTenantsResult currentBatch, @NonNull TenantSource source, int maxResults) {
51+
this.currentBatch = checkNotNull(currentBatch);
52+
this.source = checkNotNull(source);
53+
this.maxResults = maxResults;
54+
}
55+
56+
/**
57+
* Checks if there is another page of tenants available to retrieve.
58+
*
59+
* @return true if another page is available, or false otherwise.
60+
*/
61+
@Override
62+
public boolean hasNextPage() {
63+
return !END_OF_LIST.equals(currentBatch.getNextPageToken());
64+
}
65+
66+
/**
67+
* Returns the string token that identifies the next page.
68+
*
69+
* <p>Never returns null. Returns empty string if there are no more pages available to be
70+
* retrieved.
71+
*
72+
* @return A non-null string token (possibly empty, representing no more pages)
73+
*/
74+
@NonNull
75+
@Override
76+
public String getNextPageToken() {
77+
return currentBatch.getNextPageToken();
78+
}
79+
80+
/**
81+
* Returns the next page of tenants.
82+
*
83+
* @return A new {@link ListTenantsPage} instance, or null if there are no more pages.
84+
*/
85+
@Nullable
86+
@Override
87+
public ListTenantsPage getNextPage() {
88+
if (hasNextPage()) {
89+
PageFactory factory = new PageFactory(source, maxResults, currentBatch.getNextPageToken());
90+
try {
91+
return factory.create();
92+
} catch (FirebaseAuthException e) {
93+
throw new RuntimeException(e);
94+
}
95+
}
96+
return null;
97+
}
98+
99+
/**
100+
* Returns an {@link Iterable} that facilitates transparently iterating over all the tenants in
101+
* the current Firebase project, starting from this page.
102+
*
103+
* <p>The {@link Iterator} instances produced by the returned {@link Iterable} never buffers more
104+
* than one page of tenants at a time. It is safe to abandon the iterators (i.e. break the loops)
105+
* at any time.
106+
*
107+
* @return a new {@link Iterable} instance.
108+
*/
109+
@NonNull
110+
@Override
111+
public Iterable<Tenant> iterateAll() {
112+
return new TenantIterable(this);
113+
}
114+
115+
/**
116+
* Returns an {@code Iterable} over the users in this page.
117+
*
118+
* @return a {@code Iterable<Tenant>} instance.
119+
*/
120+
@NonNull
121+
@Override
122+
public Iterable<Tenant> getValues() {
123+
return currentBatch.getTenants();
124+
}
125+
126+
private static class TenantIterable implements Iterable<Tenant> {
127+
128+
private final ListTenantsPage startingPage;
129+
130+
TenantIterable(@NonNull ListTenantsPage startingPage) {
131+
this.startingPage = checkNotNull(startingPage, "starting page must not be null");
132+
}
133+
134+
@Override
135+
@NonNull
136+
public Iterator<Tenant> iterator() {
137+
return new TenantIterator(startingPage);
138+
}
139+
140+
/**
141+
* An {@link Iterator} that cycles through tenants, one at a time.
142+
*
143+
* <p>It buffers the last retrieved batch of tenants in memory. The {@code maxResults} parameter
144+
* is an upper bound on the batch size.
145+
*/
146+
private static class TenantIterator implements Iterator<Tenant> {
147+
148+
private ListTenantsPage currentPage;
149+
private List<Tenant> batch;
150+
private int index = 0;
151+
152+
private TenantIterator(ListTenantsPage startingPage) {
153+
setCurrentPage(startingPage);
154+
}
155+
156+
@Override
157+
public boolean hasNext() {
158+
if (index == batch.size()) {
159+
if (currentPage.hasNextPage()) {
160+
setCurrentPage(currentPage.getNextPage());
161+
} else {
162+
return false;
163+
}
164+
}
165+
166+
return index < batch.size();
167+
}
168+
169+
@Override
170+
public Tenant next() {
171+
if (!hasNext()) {
172+
throw new NoSuchElementException();
173+
}
174+
return batch.get(index++);
175+
}
176+
177+
@Override
178+
public void remove() {
179+
throw new UnsupportedOperationException("remove operation not supported");
180+
}
181+
182+
private void setCurrentPage(ListTenantsPage page) {
183+
this.currentPage = checkNotNull(page);
184+
this.batch = ImmutableList.copyOf(page.getValues());
185+
this.index = 0;
186+
}
187+
}
188+
}
189+
190+
/**
191+
* Represents a source of tenant data that can be queried to load a batch of tenants.
192+
*/
193+
interface TenantSource {
194+
@NonNull
195+
ListTenantsResult fetch(int maxResults, String pageToken) throws FirebaseAuthException;
196+
}
197+
198+
static class DefaultTenantSource implements TenantSource {
199+
200+
private final FirebaseUserManager userManager;
201+
private final JsonFactory jsonFactory;
202+
203+
DefaultTenantSource(FirebaseUserManager userManager, JsonFactory jsonFactory) {
204+
this.userManager = checkNotNull(userManager, "user manager must not be null");
205+
this.jsonFactory = checkNotNull(jsonFactory, "json factory must not be null");
206+
}
207+
208+
@Override
209+
public ListTenantsResult fetch(int maxResults, String pageToken) throws FirebaseAuthException {
210+
ListTenantsResponse response = userManager.listTenants(maxResults, pageToken);
211+
ImmutableList.Builder<Tenant> builder = ImmutableList.builder();
212+
if (response.hasTenants()) {
213+
builder.addAll(response.getTenants());
214+
}
215+
String nextPageToken = response.getPageToken() != null
216+
? response.getPageToken() : END_OF_LIST;
217+
return new ListTenantsResult(builder.build(), nextPageToken);
218+
}
219+
}
220+
221+
static final class ListTenantsResult {
222+
223+
private final List<Tenant> tenants;
224+
private final String nextPageToken;
225+
226+
ListTenantsResult(@NonNull List<Tenant> tenants, @NonNull String nextPageToken) {
227+
this.tenants = checkNotNull(tenants);
228+
this.nextPageToken = checkNotNull(nextPageToken); // Can be empty
229+
}
230+
231+
@NonNull
232+
List<Tenant> getTenants() {
233+
return tenants;
234+
}
235+
236+
@NonNull
237+
String getNextPageToken() {
238+
return nextPageToken;
239+
}
240+
}
241+
242+
/**
243+
* A simple factory class for {@link ListTenantsPage} instances.
244+
*
245+
* <p>Performs argument validation before attempting to load any tenant data (which is expensive,
246+
* and hence may be performed asynchronously on a separate thread).
247+
*/
248+
static class PageFactory {
249+
250+
private final TenantSource source;
251+
private final int maxResults;
252+
private final String pageToken;
253+
254+
PageFactory(@NonNull TenantSource source) {
255+
this(source, FirebaseUserManager.MAX_LIST_TENANTS_RESULTS, null);
256+
}
257+
258+
PageFactory(@NonNull TenantSource source, int maxResults, @Nullable String pageToken) {
259+
checkArgument(maxResults > 0 && maxResults <= FirebaseUserManager.MAX_LIST_TENANTS_RESULTS,
260+
"maxResults must be a positive integer that does not exceed %s",
261+
FirebaseUserManager.MAX_LIST_TENANTS_RESULTS);
262+
checkArgument(!END_OF_LIST.equals(pageToken), "invalid end of list page token");
263+
this.source = checkNotNull(source, "source must not be null");
264+
this.maxResults = maxResults;
265+
this.pageToken = pageToken;
266+
}
267+
268+
ListTenantsPage create() throws FirebaseAuthException {
269+
ListTenantsResult batch = source.fetch(maxResults, pageToken);
270+
return new ListTenantsPage(batch, source, maxResults);
271+
}
272+
}
273+
}

0 commit comments

Comments
 (0)