Skip to content

Commit 6c1fea1

Browse files
authored
feat(auth): Added TenantManager, Tenant and GetTenantAsync APIs (#222)
1 parent 6bef3b2 commit 6c1fea1

File tree

6 files changed

+410
-0
lines changed

6 files changed

+410
-0
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// Copyright 2020, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using System;
16+
using System.Collections.Generic;
17+
using System.Net;
18+
using System.Net.Http;
19+
using System.Threading.Tasks;
20+
using FirebaseAdmin.Tests;
21+
using FirebaseAdmin.Util;
22+
using Google.Apis.Auth.OAuth2;
23+
using Xunit;
24+
25+
namespace FirebaseAdmin.Auth.Multitenancy.Tests
26+
{
27+
public class TenantManagerTest
28+
{
29+
public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>()
30+
{
31+
new object[] { null },
32+
new object[] { string.Empty },
33+
};
34+
35+
private const string TenantResponse = @"{
36+
""name"": ""projects/project1/tenants/tenant1"",
37+
""displayName"": ""Test Tenant"",
38+
""allowPasswordSignup"": true,
39+
""enableEmailLinkSignin"": true
40+
}";
41+
42+
private const string TenantNotFoundResponse = @"{
43+
""error"": {
44+
""message"": ""TENANT_NOT_FOUND""
45+
}
46+
}";
47+
48+
private static readonly string ClientVersion =
49+
$"DotNet/Admin/{FirebaseApp.GetSdkVersion()}";
50+
51+
private static readonly GoogleCredential MockCredential =
52+
GoogleCredential.FromAccessToken("test-token");
53+
54+
[Fact]
55+
public async Task GetTenant()
56+
{
57+
var handler = new MockMessageHandler()
58+
{
59+
Response = TenantResponse,
60+
};
61+
var auth = CreateFirebaseAuth(handler);
62+
63+
var provider = await auth.TenantManager.GetTenantAsync("tenant1");
64+
65+
AssertTenant(provider);
66+
Assert.Equal(1, handler.Requests.Count);
67+
var request = handler.Requests[0];
68+
Assert.Equal(HttpMethod.Get, request.Method);
69+
Assert.Equal("/v2/projects/project1/tenants/tenant1", request.Url.PathAndQuery);
70+
AssertClientVersionHeader(request);
71+
}
72+
73+
[Theory]
74+
[MemberData(nameof(InvalidStrings))]
75+
public async Task GetTenantNoId(string tenantId)
76+
{
77+
var auth = CreateFirebaseAuth();
78+
79+
var exception = await Assert.ThrowsAsync<ArgumentException>(
80+
() => auth.TenantManager.GetTenantAsync(tenantId));
81+
Assert.Equal("Tenant ID cannot be null or empty.", exception.Message);
82+
}
83+
84+
[Fact]
85+
public async Task GetTenantNotFoundError()
86+
{
87+
var handler = new MockMessageHandler()
88+
{
89+
StatusCode = HttpStatusCode.NotFound,
90+
Response = TenantNotFoundResponse,
91+
};
92+
var auth = CreateFirebaseAuth(handler);
93+
94+
var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
95+
() => auth.TenantManager.GetTenantAsync("tenant1"));
96+
Assert.Equal(ErrorCode.NotFound, exception.ErrorCode);
97+
Assert.Equal(AuthErrorCode.TenantNotFound, exception.AuthErrorCode);
98+
Assert.Equal(
99+
"No tenant found for the given identifier (TENANT_NOT_FOUND).",
100+
exception.Message);
101+
Assert.NotNull(exception.HttpResponse);
102+
Assert.Null(exception.InnerException);
103+
}
104+
105+
private static FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler = null)
106+
{
107+
var tenantManager = new TenantManager(new TenantManager.Args
108+
{
109+
Credential = MockCredential,
110+
ProjectId = "project1",
111+
ClientFactory = new MockHttpClientFactory(handler ?? new MockMessageHandler()),
112+
RetryOptions = RetryOptions.NoBackOff,
113+
});
114+
var args = FirebaseAuth.Args.CreateDefault();
115+
args.TenantManager = new Lazy<TenantManager>(tenantManager);
116+
return new FirebaseAuth(args);
117+
}
118+
119+
private static void AssertTenant(Tenant tenant)
120+
{
121+
Assert.Equal("tenant1", tenant.TenantId);
122+
Assert.Equal("Test Tenant", tenant.DisplayName);
123+
Assert.True(tenant.PasswordSignUpAllowed);
124+
Assert.True(tenant.EmailLinkSignInEnabled);
125+
}
126+
127+
private static void AssertClientVersionHeader(MockMessageHandler.IncomingRequest request)
128+
{
129+
Assert.Contains(ClientVersion, request.Headers.GetValues("X-Client-Version"));
130+
}
131+
}
132+
}

FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,10 @@ public enum AuthErrorCode
8888
/// No identity provider configuration found for the given identifier.
8989
/// </summary>
9090
ConfigurationNotFound,
91+
92+
/// <summary>
93+
/// No tenant found for the given identifier.
94+
/// </summary>
95+
TenantNotFound,
9196
}
9297
}

FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,13 @@ internal sealed class AuthErrorHandler
7777
AuthErrorCode.PhoneNumberAlreadyExists,
7878
"The user with the provided phone number already exists")
7979
},
80+
{
81+
"TENANT_NOT_FOUND",
82+
new ErrorInfo(
83+
ErrorCode.NotFound,
84+
AuthErrorCode.TenantNotFound,
85+
"No tenant found for the given identifier")
86+
},
8087
{
8188
"USER_NOT_FOUND",
8289
new ErrorInfo(

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Collections.Generic;
1717
using System.Threading;
1818
using System.Threading.Tasks;
19+
using FirebaseAdmin.Auth.Multitenancy;
1920
using FirebaseAdmin.Auth.Providers;
2021
using Google.Api.Gax;
2122
using Google.Apis.Util;
@@ -33,6 +34,7 @@ public sealed class FirebaseAuth : IFirebaseService
3334
private readonly Lazy<FirebaseTokenVerifier> sessionCookieVerifier;
3435
private readonly Lazy<FirebaseUserManager> userManager;
3536
private readonly Lazy<ProviderConfigManager> providerConfigManager;
37+
private readonly Lazy<TenantManager> tenantManager;
3638
private readonly object authLock = new object();
3739
private bool deleted;
3840

@@ -46,6 +48,7 @@ internal FirebaseAuth(Args args)
4648
this.userManager = args.UserManager.ThrowIfNull(nameof(args.UserManager));
4749
this.providerConfigManager = args.ProviderConfigManager.ThrowIfNull(
4850
nameof(args.ProviderConfigManager));
51+
this.tenantManager = args.TenantManager.ThrowIfNull(nameof(args.TenantManager));
4952
}
5053

5154
/// <summary>
@@ -66,6 +69,11 @@ public static FirebaseAuth DefaultInstance
6669
}
6770
}
6871

72+
/// <summary>
73+
/// Gets the <see cref="TenantManager"/> instance associated with the current project.
74+
/// </summary>
75+
public TenantManager TenantManager => this.tenantManager.Value;
76+
6977
/// <summary>
7078
/// Returns the auth instance for the specified app.
7179
/// </summary>
@@ -1401,6 +1409,7 @@ void IFirebaseService.Delete()
14011409
this.tokenFactory.DisposeIfCreated();
14021410
this.userManager.DisposeIfCreated();
14031411
this.providerConfigManager.DisposeIfCreated();
1412+
this.tenantManager.DisposeIfCreated();
14041413
}
14051414
}
14061415

@@ -1438,6 +1447,8 @@ internal sealed class Args
14381447

14391448
internal Lazy<ProviderConfigManager> ProviderConfigManager { get; set; }
14401449

1450+
internal Lazy<TenantManager> TenantManager { get; set; }
1451+
14411452
internal static Args Create(FirebaseApp app)
14421453
{
14431454
return new Args()
@@ -1452,6 +1463,8 @@ internal static Args Create(FirebaseApp app)
14521463
() => FirebaseUserManager.Create(app), true),
14531464
ProviderConfigManager = new Lazy<ProviderConfigManager>(
14541465
() => Providers.ProviderConfigManager.Create(app), true),
1466+
TenantManager = new Lazy<TenantManager>(
1467+
() => Multitenancy.TenantManager.Create(app), true),
14551468
};
14561469
}
14571470

@@ -1464,6 +1477,7 @@ internal static Args CreateDefault()
14641477
SessionCookieVerifier = new Lazy<FirebaseTokenVerifier>(),
14651478
UserManager = new Lazy<FirebaseUserManager>(),
14661479
ProviderConfigManager = new Lazy<ProviderConfigManager>(),
1480+
TenantManager = new Lazy<TenantManager>(),
14671481
};
14681482
}
14691483
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2020, Google Inc. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
using Google.Apis.Util;
16+
using Newtonsoft.Json;
17+
18+
namespace FirebaseAdmin.Auth.Multitenancy
19+
{
20+
/// <summary>
21+
/// Represents a tenant in a multi-tenant application..
22+
///
23+
/// <para>Multitenancy support requires Google Cloud Identity Platform (GCIP). To learn more
24+
/// about GCIP, including pricing and features, see the
25+
/// <a href="https://cloud.google.com/identity-platform">GCIP documentation</a>.</para>
26+
///
27+
/// <para>Before multitenancy can be used in a Google Cloud Identity Platform project, tenants
28+
/// must be allowed on that project via the Cloud Console UI.</para>
29+
///
30+
/// <para>A tenant configuration provides information such as the display name, tenant
31+
/// identifier and email authentication configuration. For OIDC/SAML provider configuration
32+
/// management, TenantAwareFirebaseAuth instances should be used instead of a Tenant to
33+
/// retrieve the list of configured IdPs on a tenant. When configuring these providers, note
34+
/// that tenants will inherit whitelisted domains and authenticated redirect URIs of their
35+
/// parent project.</para>
36+
///
37+
/// <para>All other settings of a tenant will also be inherited. These will need to be managed
38+
/// from the Cloud Console UI.</para>
39+
/// </summary>
40+
public sealed class Tenant
41+
{
42+
private readonly Args args;
43+
44+
internal Tenant(Args args)
45+
{
46+
this.args = args.ThrowIfNull(nameof(args));
47+
}
48+
49+
/// <summary>
50+
/// Gets the tenant identifier.
51+
/// </summary>
52+
public string TenantId => this.ExtractResourceId(this.args.Name);
53+
54+
/// <summary>
55+
/// Gets the tenant display name.
56+
/// </summary>
57+
public string DisplayName => args.DisplayName;
58+
59+
/// <summary>
60+
/// Gets a value indicating whether the email sign-in provider is enabled.
61+
/// </summary>
62+
public bool PasswordSignUpAllowed => args.PasswordSignUpAllowed;
63+
64+
/// <summary>
65+
/// Gets a value indicating whether the email link sign-in is enabled.
66+
/// </summary>
67+
public bool EmailLinkSignInEnabled => args.EmailLinkSignInEnabled;
68+
69+
private string ExtractResourceId(string resourceName)
70+
{
71+
var segments = resourceName.Split('/');
72+
return segments[segments.Length - 1];
73+
}
74+
75+
internal sealed class Args
76+
{
77+
[JsonProperty("name")]
78+
internal string Name { get; set; }
79+
80+
[JsonProperty("displayName")]
81+
internal string DisplayName { get; set; }
82+
83+
[JsonProperty("allowPasswordSignup")]
84+
internal bool PasswordSignUpAllowed { get; set; }
85+
86+
[JsonProperty("enableEmailLinkSignin")]
87+
internal bool EmailLinkSignInEnabled { get; set; }
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)