Skip to content

Commit b8b18f6

Browse files
authored
Add provider config management operations. (#433)
Adds all of the OIDC and SAML provider config operations, related to adding multi-tenancy support.
1 parent 6d1b15b commit b8b18f6

30 files changed

+4596
-701
lines changed

checkstyle.xml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@
4242
<module name="FileTabCharacter">
4343
<property name="eachLine" value="true"/>
4444
</module>
45-
45+
4646
<module name="SuppressionCommentFilter">
4747
<property name="offCommentFormat" value="CSOFF\: ([\w\|]+)"/>
4848
<property name="onCommentFormat" value="CSON\: ([\w\|]+)"/>
4949
<property name="checkFormat" value="$1"/>
5050
</module>
5151

52-
<module name="TreeWalker">
53-
<module name="FileContentsHolder"/>
52+
<module name="TreeWalker">
53+
<module name="FileContentsHolder"/>
5454
<module name="OuterTypeFilename"/>
5555
<module name="IllegalTokenText">
5656
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
@@ -229,6 +229,9 @@
229229
<property name="allowedAnnotations" value="Override, Test"/>
230230
<property name="allowThrowsTagsForSubclasses" value="true"/>
231231
<property name="allowMissingJavadoc" value="true"/>
232+
<!-- Setting this property helps avoid some strange errors. For more information, see -->
233+
<!-- https://stackoverflow.com/questions/27938039/unable-to-get-class-information-for-checkstyle. -->
234+
<property name="suppressLoadErrors" value="true"/>
232235
</module>
233236
<module name="MethodName">
234237
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>

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

Lines changed: 567 additions & 35 deletions
Large diffs are not rendered by default.

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

Lines changed: 135 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@
4444
import com.google.firebase.auth.internal.GetAccountInfoRequest;
4545
import com.google.firebase.auth.internal.GetAccountInfoResponse;
4646
import com.google.firebase.auth.internal.HttpErrorResponse;
47+
import com.google.firebase.auth.internal.ListOidcProviderConfigsResponse;
48+
import com.google.firebase.auth.internal.ListSamlProviderConfigsResponse;
4749
import com.google.firebase.auth.internal.ListTenantsResponse;
4850
import com.google.firebase.auth.internal.UploadAccountResponse;
4951
import com.google.firebase.internal.ApiClientUtils;
@@ -69,6 +71,7 @@
6971
*/
7072
class FirebaseUserManager {
7173

74+
static final String CONFIGURATION_NOT_FOUND_ERROR = "configuration-not-found";
7275
static final String TENANT_ID_MISMATCH_ERROR = "tenant-id-mismatch";
7376
static final String TENANT_NOT_FOUND_ERROR = "tenant-not-found";
7477
static final String USER_NOT_FOUND_ERROR = "user-not-found";
@@ -78,7 +81,7 @@ class FirebaseUserManager {
7881
// SDK error codes defined at: https://firebase.google.com/docs/auth/admin/errors
7982
private static final Map<String, String> ERROR_CODES = ImmutableMap.<String, String>builder()
8083
.put("CLAIMS_TOO_LARGE", "claims-too-large")
81-
.put("CONFIGURATION_NOT_FOUND", "project-not-found")
84+
.put("CONFIGURATION_NOT_FOUND", CONFIGURATION_NOT_FOUND_ERROR)
8285
.put("INSUFFICIENT_PERMISSION", "insufficient-permission")
8386
.put("DUPLICATE_EMAIL", "email-already-exists")
8487
.put("DUPLICATE_LOCAL_ID", "uid-already-exists")
@@ -97,6 +100,7 @@ class FirebaseUserManager {
97100
.put("INVALID_DYNAMIC_LINK_DOMAIN", "invalid-dynamic-link-domain")
98101
.build();
99102

103+
static final int MAX_LIST_PROVIDER_CONFIGS_RESULTS = 100;
100104
static final int MAX_LIST_TENANTS_RESULTS = 1000;
101105
static final int MAX_GET_ACCOUNTS_BATCH_SIZE = 100;
102106
static final int MAX_DELETE_ACCOUNTS_BATCH_SIZE = 1000;
@@ -112,6 +116,7 @@ class FirebaseUserManager {
112116
private static final String CLIENT_VERSION_HEADER = "X-Client-Version";
113117

114118
private final String userMgtBaseUrl;
119+
private final String idpConfigMgtBaseUrl;
115120
private final String tenantMgtBaseUrl;
116121
private final JsonFactory jsonFactory;
117122
private final HttpRequestFactory requestFactory;
@@ -126,15 +131,18 @@ class FirebaseUserManager {
126131
"Project ID is required to access the auth service. Use a service account credential or "
127132
+ "set the project ID explicitly via FirebaseOptions. Alternatively you can also "
128133
+ "set the project ID via the GOOGLE_CLOUD_PROJECT environment variable.");
129-
String tenantId = builder.tenantId;
130-
if (builder.tenantId == null) {
131-
this.userMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v1", projectId);
134+
final String idToolkitUrlV1 = String.format(ID_TOOLKIT_URL, "v1", projectId);
135+
final String idToolkitUrlV2 = String.format(ID_TOOLKIT_URL, "v2", projectId);
136+
final String tenantId = builder.tenantId;
137+
if (tenantId == null) {
138+
this.userMgtBaseUrl = idToolkitUrlV1;
139+
this.idpConfigMgtBaseUrl = idToolkitUrlV2;
132140
} else {
133-
checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty");
134-
this.userMgtBaseUrl =
135-
String.format(ID_TOOLKIT_URL, "v1", projectId) + getTenantUrlSuffix(tenantId);
141+
checkArgument(!tenantId.isEmpty(), "Tenant ID must not be empty.");
142+
this.userMgtBaseUrl = idToolkitUrlV1 + getTenantUrlSuffix(tenantId);
143+
this.idpConfigMgtBaseUrl = idToolkitUrlV2 + getTenantUrlSuffix(tenantId);
136144
}
137-
this.tenantMgtBaseUrl = String.format(ID_TOOLKIT_URL, "v2", projectId);
145+
this.tenantMgtBaseUrl = idToolkitUrlV2;
138146
this.jsonFactory = app.getOptions().getJsonFactory();
139147
this.requestFactory = builder.requestFactory == null
140148
? ApiClientUtils.newAuthorizedRequestFactory(app) : builder.requestFactory;
@@ -295,20 +303,11 @@ Tenant createTenant(Tenant.CreateRequest request) throws FirebaseAuthException {
295303

296304
Tenant updateTenant(Tenant.UpdateRequest request) throws FirebaseAuthException {
297305
Map<String, Object> properties = request.getProperties();
298-
checkArgument(!properties.isEmpty(), "Tenant update must have at least one property set");
299306
GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(request.getTenantId()));
300-
url.put("updateMask", generateMask(properties));
307+
url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
301308
return sendRequest("PATCH", url, properties, Tenant.class);
302309
}
303310

304-
private static String generateMask(Map<String, Object> properties) {
305-
// This implementation does not currently handle the case of nested properties. This is fine
306-
// since we do not currently generate masks for any properties with nested values. When it
307-
// comes time to implement this, we can check if a property has nested properties by checking
308-
// if it is an instance of the Map class.
309-
return Joiner.on(",").join(ImmutableSortedSet.copyOf(properties.keySet()));
310-
}
311-
312311
void deleteTenant(String tenantId) throws FirebaseAuthException {
313312
GenericUrl url = new GenericUrl(tenantMgtBaseUrl + getTenantUrlSuffix(tenantId));
314313
sendRequest("DELETE", url, null, GenericJson.class);
@@ -366,11 +365,128 @@ String getEmailActionLink(EmailLinkType type, String email,
366365
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to create email action link");
367366
}
368367

368+
OidcProviderConfig createOidcProviderConfig(
369+
OidcProviderConfig.CreateRequest request) throws FirebaseAuthException {
370+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
371+
url.set("oauthIdpConfigId", request.getProviderId());
372+
return sendRequest("POST", url, request.getProperties(), OidcProviderConfig.class);
373+
}
374+
375+
SamlProviderConfig createSamlProviderConfig(
376+
SamlProviderConfig.CreateRequest request) throws FirebaseAuthException {
377+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
378+
url.set("inboundSamlConfigId", request.getProviderId());
379+
return sendRequest("POST", url, request.getProperties(), SamlProviderConfig.class);
380+
}
381+
382+
OidcProviderConfig updateOidcProviderConfig(OidcProviderConfig.UpdateRequest request)
383+
throws FirebaseAuthException {
384+
Map<String, Object> properties = request.getProperties();
385+
GenericUrl url =
386+
new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(request.getProviderId()));
387+
url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
388+
return sendRequest("PATCH", url, properties, OidcProviderConfig.class);
389+
}
390+
391+
SamlProviderConfig updateSamlProviderConfig(SamlProviderConfig.UpdateRequest request)
392+
throws FirebaseAuthException {
393+
Map<String, Object> properties = request.getProperties();
394+
GenericUrl url =
395+
new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(request.getProviderId()));
396+
url.put("updateMask", Joiner.on(",").join(generateMask(properties)));
397+
return sendRequest("PATCH", url, properties, SamlProviderConfig.class);
398+
}
399+
400+
OidcProviderConfig getOidcProviderConfig(String providerId) throws FirebaseAuthException {
401+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
402+
return sendRequest("GET", url, null, OidcProviderConfig.class);
403+
}
404+
405+
SamlProviderConfig getSamlProviderConfig(String providerId) throws FirebaseAuthException {
406+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
407+
return sendRequest("GET", url, null, SamlProviderConfig.class);
408+
}
409+
410+
ListOidcProviderConfigsResponse listOidcProviderConfigs(int maxResults, String pageToken)
411+
throws FirebaseAuthException {
412+
ImmutableMap.Builder<String, Object> builder =
413+
ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
414+
if (pageToken != null) {
415+
checkArgument(!pageToken.equals(
416+
ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token.");
417+
builder.put("nextPageToken", pageToken);
418+
}
419+
420+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/oauthIdpConfigs");
421+
url.putAll(builder.build());
422+
ListOidcProviderConfigsResponse response =
423+
sendRequest("GET", url, null, ListOidcProviderConfigsResponse.class);
424+
if (response == null) {
425+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
426+
}
427+
return response;
428+
}
429+
430+
ListSamlProviderConfigsResponse listSamlProviderConfigs(int maxResults, String pageToken)
431+
throws FirebaseAuthException {
432+
ImmutableMap.Builder<String, Object> builder =
433+
ImmutableMap.<String, Object>builder().put("pageSize", maxResults);
434+
if (pageToken != null) {
435+
checkArgument(!pageToken.equals(
436+
ListProviderConfigsPage.END_OF_LIST), "Invalid end of list page token.");
437+
builder.put("nextPageToken", pageToken);
438+
}
439+
440+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + "/inboundSamlConfigs");
441+
url.putAll(builder.build());
442+
ListSamlProviderConfigsResponse response =
443+
sendRequest("GET", url, null, ListSamlProviderConfigsResponse.class);
444+
if (response == null) {
445+
throw new FirebaseAuthException(INTERNAL_ERROR, "Failed to retrieve provider configs.");
446+
}
447+
return response;
448+
}
449+
450+
void deleteOidcProviderConfig(String providerId) throws FirebaseAuthException {
451+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getOidcUrlSuffix(providerId));
452+
sendRequest("DELETE", url, null, GenericJson.class);
453+
}
454+
455+
void deleteSamlProviderConfig(String providerId) throws FirebaseAuthException {
456+
GenericUrl url = new GenericUrl(idpConfigMgtBaseUrl + getSamlUrlSuffix(providerId));
457+
sendRequest("DELETE", url, null, GenericJson.class);
458+
}
459+
460+
private static Set<String> generateMask(Map<String, Object> properties) {
461+
ImmutableSortedSet.Builder<String> maskBuilder = ImmutableSortedSet.naturalOrder();
462+
for (Map.Entry<String, Object> entry : properties.entrySet()) {
463+
if (entry.getValue() instanceof Map) {
464+
Set<String> childMask = generateMask((Map<String, Object>) entry.getValue());
465+
for (String childProperty : childMask) {
466+
maskBuilder.add(entry.getKey() + "." + childProperty);
467+
}
468+
} else {
469+
maskBuilder.add(entry.getKey());
470+
}
471+
}
472+
return maskBuilder.build();
473+
}
474+
369475
private static String getTenantUrlSuffix(String tenantId) {
370-
checkArgument(!Strings.isNullOrEmpty(tenantId));
476+
checkArgument(!Strings.isNullOrEmpty(tenantId), "Tenant ID must not be null or empty.");
371477
return "/tenants/" + tenantId;
372478
}
373479

480+
private static String getOidcUrlSuffix(String providerId) {
481+
checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
482+
return "/oauthIdpConfigs/" + providerId;
483+
}
484+
485+
private static String getSamlUrlSuffix(String providerId) {
486+
checkArgument(!Strings.isNullOrEmpty(providerId), "Provider ID must not be null or empty.");
487+
return "/inboundSamlConfigs/" + providerId;
488+
}
489+
374490
private <T> T post(String path, Object content, Class<T> clazz) throws FirebaseAuthException {
375491
checkArgument(!Strings.isNullOrEmpty(path), "path must not be null or empty");
376492
checkNotNull(content, "content must not be null for POST requests");

0 commit comments

Comments
 (0)