Skip to content

Commit 4b4049c

Browse files
authored
feat(auth): Added TenantAwareFirebaseAuth class (#227)
1 parent a1e8234 commit 4b4049c

File tree

6 files changed

+221
-28
lines changed

6 files changed

+221
-28
lines changed

FirebaseAdmin/FirebaseAdmin.Tests/Auth/FirebaseAuthTest.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ await Assert.ThrowsAsync<InvalidOperationException>(
7272
async () => await auth.SetCustomUserClaimsAsync("user", null));
7373
await Assert.ThrowsAsync<InvalidOperationException>(
7474
async () => await auth.GetOidcProviderConfigAsync("oidc.provider"));
75+
Assert.Throws<InvalidOperationException>(() => auth.TenantManager);
7576
}
7677

7778
[Fact]

FirebaseAdmin/FirebaseAdmin.Tests/Auth/Multitenancy/TenantManagerTest.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,51 @@ await Assert.ThrowsAsync<ArgumentException>(
603603
Assert.Empty(handler.Requests);
604604
}
605605

606+
[Fact]
607+
public void AuthForTenant()
608+
{
609+
var auth = CreateFirebaseAuth();
610+
611+
var tenantAwareAuth = auth.TenantManager.AuthForTenant("tenant1");
612+
613+
Assert.Equal("tenant1", tenantAwareAuth.TenantId);
614+
}
615+
616+
[Fact]
617+
public void AuthForTenantCaching()
618+
{
619+
var auth = CreateFirebaseAuth();
620+
621+
var tenantAwareAuth1 = auth.TenantManager.AuthForTenant("tenant1");
622+
var tenantAwareAuth2 = auth.TenantManager.AuthForTenant("tenant1");
623+
624+
Assert.Same(tenantAwareAuth1, tenantAwareAuth2);
625+
}
626+
627+
[Theory]
628+
[MemberData(nameof(InvalidStrings))]
629+
public void AuthForTenantNoTenantId(string tenantId)
630+
{
631+
var auth = CreateFirebaseAuth();
632+
633+
var exception = Assert.Throws<ArgumentException>(
634+
() => auth.TenantManager.AuthForTenant(tenantId));
635+
Assert.Equal("Tenant ID cannot be null or empty.", exception.Message);
636+
}
637+
638+
[Fact]
639+
public async Task UseAfterDelete()
640+
{
641+
var auth = CreateFirebaseAuth();
642+
var tenantManager = auth.TenantManager;
643+
(auth as IFirebaseService).Delete();
644+
645+
await Assert.ThrowsAsync<ObjectDisposedException>(
646+
() => tenantManager.GetTenantAsync("tenant1"));
647+
Assert.Throws<ObjectDisposedException>(
648+
() => tenantManager.AuthForTenant("tenant1"));
649+
}
650+
606651
private static FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler = null)
607652
{
608653
var tenantManager = new TenantManager(new TenantManager.Args
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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 Google.Apis.Util;
17+
18+
namespace FirebaseAdmin.Auth
19+
{
20+
/// <summary>
21+
/// Exposes Firebase Auth operations that are available in both tenant-aware and tenant-unaware
22+
/// contexts.
23+
/// </summary>
24+
public abstract class AbstractFirebaseAuth : IFirebaseService
25+
{
26+
private readonly object authLock = new object();
27+
private bool deleted;
28+
29+
internal AbstractFirebaseAuth(Args args)
30+
{
31+
args.ThrowIfNull(nameof(args));
32+
}
33+
34+
/// <summary>
35+
/// Deletes this <see cref="FirebaseAuth"/> service instance.
36+
/// </summary>
37+
void IFirebaseService.Delete()
38+
{
39+
lock (this.authLock)
40+
{
41+
this.deleted = true;
42+
this.Cleanup();
43+
}
44+
}
45+
46+
internal virtual void Cleanup() { }
47+
48+
internal TResult IfNotDeleted<TResult>(Func<TResult> func)
49+
{
50+
lock (this.authLock)
51+
{
52+
if (this.deleted)
53+
{
54+
throw new InvalidOperationException("Cannot invoke after deleting the app.");
55+
}
56+
57+
return func();
58+
}
59+
}
60+
61+
internal class Args { }
62+
}
63+
}

FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,20 +28,18 @@ namespace FirebaseAdmin.Auth
2828
/// This is the entry point to all server-side Firebase Authentication operations. You can
2929
/// get an instance of this class via <c>FirebaseAuth.DefaultInstance</c>.
3030
/// </summary>
31-
public sealed class FirebaseAuth : IFirebaseService
31+
public sealed class FirebaseAuth : AbstractFirebaseAuth
3232
{
3333
private readonly Lazy<FirebaseTokenFactory> tokenFactory;
3434
private readonly Lazy<FirebaseTokenVerifier> idTokenVerifier;
3535
private readonly Lazy<FirebaseTokenVerifier> sessionCookieVerifier;
3636
private readonly Lazy<FirebaseUserManager> userManager;
3737
private readonly Lazy<ProviderConfigManager> providerConfigManager;
3838
private readonly Lazy<TenantManager> tenantManager;
39-
private readonly object authLock = new object();
40-
private bool deleted;
4139

4240
internal FirebaseAuth(Args args)
41+
: base(args)
4342
{
44-
args.ThrowIfNull(nameof(args));
4543
this.tokenFactory = args.TokenFactory.ThrowIfNull(nameof(args.TokenFactory));
4644
this.idTokenVerifier = args.IdTokenVerifier.ThrowIfNull(nameof(args.IdTokenVerifier));
4745
this.sessionCookieVerifier = args.SessionCookieVerifier.ThrowIfNull(
@@ -73,7 +71,7 @@ public static FirebaseAuth DefaultInstance
7371
/// <summary>
7472
/// Gets the <see cref="TenantManager"/> instance associated with the current project.
7573
/// </summary>
76-
public TenantManager TenantManager => this.tenantManager.Value;
74+
public TenantManager TenantManager => this.IfNotDeleted(() => this.tenantManager.Value);
7775

7876
/// <summary>
7977
/// Returns the auth instance for the specified app.
@@ -1402,16 +1400,12 @@ public PagedAsyncEnumerable<AuthProviderConfigs<SamlProviderConfig>, SamlProvide
14021400
/// <summary>
14031401
/// Deletes this <see cref="FirebaseAuth"/> service instance.
14041402
/// </summary>
1405-
void IFirebaseService.Delete()
1403+
internal override void Cleanup()
14061404
{
1407-
lock (this.authLock)
1408-
{
1409-
this.deleted = true;
1410-
this.tokenFactory.DisposeIfCreated();
1411-
this.userManager.DisposeIfCreated();
1412-
this.providerConfigManager.DisposeIfCreated();
1413-
this.tenantManager.DisposeIfCreated();
1414-
}
1405+
this.tokenFactory.DisposeIfCreated();
1406+
this.userManager.DisposeIfCreated();
1407+
this.providerConfigManager.DisposeIfCreated();
1408+
this.tenantManager.DisposeIfCreated();
14151409
}
14161410

14171411
private async Task<bool> IsRevokedAsync(
@@ -1423,20 +1417,7 @@ private async Task<bool> IsRevokedAsync(
14231417
return token.IssuedAtTimeSeconds < cutoff;
14241418
}
14251419

1426-
private TResult IfNotDeleted<TResult>(Func<TResult> func)
1427-
{
1428-
lock (this.authLock)
1429-
{
1430-
if (this.deleted)
1431-
{
1432-
throw new InvalidOperationException("Cannot invoke after deleting the app.");
1433-
}
1434-
1435-
return func();
1436-
}
1437-
}
1438-
1439-
internal sealed class Args
1420+
internal sealed new class Args : AbstractFirebaseAuth.Args
14401421
{
14411422
internal Lazy<FirebaseTokenFactory> TokenFactory { get; set; }
14421423

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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+
17+
namespace FirebaseAdmin.Auth.Multitenancy
18+
{
19+
/// <summary>
20+
/// The tenant-aware Firebase client. This can be used to perform a variety of
21+
/// authentication-related operations, scoped to a particular tenant.
22+
/// </summary>
23+
public sealed class TenantAwareFirebaseAuth : AbstractFirebaseAuth
24+
{
25+
private TenantAwareFirebaseAuth(Args args)
26+
: base(args)
27+
{
28+
this.TenantId = args.TenantId;
29+
if (string.IsNullOrEmpty(this.TenantId))
30+
{
31+
throw new ArgumentException("Tenant ID cannot be null or empty.");
32+
}
33+
}
34+
35+
/// <summary>
36+
/// Gets the tenant ID associated with this instance.
37+
/// </summary>
38+
public string TenantId { get; }
39+
40+
internal static TenantAwareFirebaseAuth Create(string tenantId)
41+
{
42+
var args = new Args()
43+
{
44+
TenantId = tenantId,
45+
};
46+
return new TenantAwareFirebaseAuth(args);
47+
}
48+
49+
internal new class Args : AbstractFirebaseAuth.Args
50+
{
51+
public string TenantId { get; set; }
52+
}
53+
}
54+
}

FirebaseAdmin/FirebaseAdmin/Auth/Multitenancy/TenantManager.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,17 @@ public sealed class TenantManager : IDisposable
5252

5353
private static readonly string ClientVersion = $"DotNet/Admin/{FirebaseApp.GetSdkVersion()}";
5454

55+
private readonly IDictionary<string, TenantAwareFirebaseAuth> tenants =
56+
new Dictionary<string, TenantAwareFirebaseAuth>();
57+
58+
private readonly object tenantsLock = new object();
59+
5560
private readonly string baseUrl;
5661

5762
private readonly ErrorHandlingHttpClient<FirebaseAuthException> httpClient;
5863

64+
private bool disposed;
65+
5966
internal TenantManager(Args args)
6067
{
6168
if (string.IsNullOrEmpty(args.ProjectId))
@@ -277,12 +284,54 @@ public PagedAsyncEnumerable<TenantsPage, Tenant> ListTenantsAsync(ListTenantsOpt
277284
<ListTenantsRequest, TenantsPage, Tenant>(() => request, new PageManager());
278285
}
279286

287+
/// <summary>
288+
/// Gets a <see cref="TenantAwareFirebaseAuth"/> instance scoped to the specified tenant.
289+
/// </summary>
290+
/// <param name="tenantId">A tenant identifier string.</param>
291+
/// <returns>An object that can be used to perform tenant-aware operations.</returns>
292+
/// <exception cref="ArgumentException">If the tenant ID argument is null or empty.
293+
/// </exception>
294+
public TenantAwareFirebaseAuth AuthForTenant(string tenantId)
295+
{
296+
if (string.IsNullOrEmpty(tenantId))
297+
{
298+
throw new ArgumentException("Tenant ID cannot be null or empty.");
299+
}
300+
301+
TenantAwareFirebaseAuth auth;
302+
lock (this.tenantsLock)
303+
{
304+
if (this.disposed)
305+
{
306+
throw new ObjectDisposedException("TenantManager instance already disposed.");
307+
}
308+
309+
if (!this.tenants.TryGetValue(tenantId, out auth))
310+
{
311+
auth = TenantAwareFirebaseAuth.Create(tenantId);
312+
this.tenants[tenantId] = auth;
313+
}
314+
}
315+
316+
return auth;
317+
}
318+
280319
/// <summary>
281320
/// Cleans up and invalidates this instance. For internal use only.
282321
/// </summary>
283322
void IDisposable.Dispose()
284323
{
285324
this.httpClient.Dispose();
325+
lock (this.tenantsLock)
326+
{
327+
this.disposed = true;
328+
foreach (var auth in this.tenants.Values)
329+
{
330+
(auth as IFirebaseService).Delete();
331+
}
332+
333+
this.tenants.Clear();
334+
}
286335
}
287336

288337
internal static TenantManager Create(FirebaseApp app)

0 commit comments

Comments
 (0)