Skip to content

feat(auth): Added TenantManager, Tenant and GetTenantAsync APIs #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2020, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using FirebaseAdmin.Tests;
using FirebaseAdmin.Util;
using Google.Apis.Auth.OAuth2;
using Xunit;

namespace FirebaseAdmin.Auth.Multitenancy.Tests
{
public class TenantManagerTest
{
public static readonly IEnumerable<object[]> InvalidStrings = new List<object[]>()
{
new object[] { null },
new object[] { string.Empty },
};

private const string TenantResponse = @"{
""name"": ""projects/project1/tenants/tenant1"",
""displayName"": ""Test Tenant"",
""allowPasswordSignup"": true,
""enableEmailLinkSignin"": true
}";

private const string TenantNotFoundResponse = @"{
""error"": {
""message"": ""TENANT_NOT_FOUND""
}
}";

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

private static readonly GoogleCredential MockCredential =
GoogleCredential.FromAccessToken("test-token");

[Fact]
public async Task GetTenant()
{
var handler = new MockMessageHandler()
{
Response = TenantResponse,
};
var auth = CreateFirebaseAuth(handler);

var provider = await auth.TenantManager.GetTenantAsync("tenant1");

AssertTenant(provider);
Assert.Equal(1, handler.Requests.Count);
var request = handler.Requests[0];
Assert.Equal(HttpMethod.Get, request.Method);
Assert.Equal("/v2/projects/project1/tenants/tenant1", request.Url.PathAndQuery);
AssertClientVersionHeader(request);
}

[Theory]
[MemberData(nameof(InvalidStrings))]
public async Task GetTenantNoId(string tenantId)
{
var auth = CreateFirebaseAuth();

var exception = await Assert.ThrowsAsync<ArgumentException>(
() => auth.TenantManager.GetTenantAsync(tenantId));
Assert.Equal("Tenant ID cannot be null or empty.", exception.Message);
}

[Fact]
public async Task GetTenantNotFoundError()
{
var handler = new MockMessageHandler()
{
StatusCode = HttpStatusCode.NotFound,
Response = TenantNotFoundResponse,
};
var auth = CreateFirebaseAuth(handler);

var exception = await Assert.ThrowsAsync<FirebaseAuthException>(
() => auth.TenantManager.GetTenantAsync("tenant1"));
Assert.Equal(ErrorCode.NotFound, exception.ErrorCode);
Assert.Equal(AuthErrorCode.TenantNotFound, exception.AuthErrorCode);
Assert.Equal(
"No tenant found for the given identifier (TENANT_NOT_FOUND).",
exception.Message);
Assert.NotNull(exception.HttpResponse);
Assert.Null(exception.InnerException);
}

private static FirebaseAuth CreateFirebaseAuth(HttpMessageHandler handler = null)
{
var tenantManager = new TenantManager(new TenantManager.Args
{
Credential = MockCredential,
ProjectId = "project1",
ClientFactory = new MockHttpClientFactory(handler ?? new MockMessageHandler()),
RetryOptions = RetryOptions.NoBackOff,
});
var args = FirebaseAuth.Args.CreateDefault();
args.TenantManager = new Lazy<TenantManager>(tenantManager);
return new FirebaseAuth(args);
}

private static void AssertTenant(Tenant tenant)
{
Assert.Equal("tenant1", tenant.TenantId);
Assert.Equal("Test Tenant", tenant.DisplayName);
Assert.True(tenant.PasswordSignUpAllowed);
Assert.True(tenant.EmailLinkSignInEnabled);
}

private static void AssertClientVersionHeader(MockMessageHandler.IncomingRequest request)
{
Assert.Contains(ClientVersion, request.Headers.GetValues("X-Client-Version"));
}
}
}
5 changes: 5 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,10 @@ public enum AuthErrorCode
/// No identity provider configuration found for the given identifier.
/// </summary>
ConfigurationNotFound,

/// <summary>
/// No tenant found for the given identifier.
/// </summary>
TenantNotFound,
}
}
7 changes: 7 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/AuthErrorHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ internal sealed class AuthErrorHandler
AuthErrorCode.PhoneNumberAlreadyExists,
"The user with the provided phone number already exists")
},
{
"TENANT_NOT_FOUND",
new ErrorInfo(
ErrorCode.NotFound,
AuthErrorCode.TenantNotFound,
"No tenant found for the given identifier")
},
{
"USER_NOT_FOUND",
new ErrorInfo(
Expand Down
14 changes: 14 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/FirebaseAuth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using FirebaseAdmin.Auth.Multitenancy;
using FirebaseAdmin.Auth.Providers;
using Google.Api.Gax;
using Google.Apis.Util;
Expand All @@ -33,6 +34,7 @@ public sealed class FirebaseAuth : IFirebaseService
private readonly Lazy<FirebaseTokenVerifier> sessionCookieVerifier;
private readonly Lazy<FirebaseUserManager> userManager;
private readonly Lazy<ProviderConfigManager> providerConfigManager;
private readonly Lazy<TenantManager> tenantManager;
private readonly object authLock = new object();
private bool deleted;

Expand All @@ -46,6 +48,7 @@ internal FirebaseAuth(Args args)
this.userManager = args.UserManager.ThrowIfNull(nameof(args.UserManager));
this.providerConfigManager = args.ProviderConfigManager.ThrowIfNull(
nameof(args.ProviderConfigManager));
this.tenantManager = args.TenantManager.ThrowIfNull(nameof(args.TenantManager));
}

/// <summary>
Expand All @@ -66,6 +69,11 @@ public static FirebaseAuth DefaultInstance
}
}

/// <summary>
/// Gets the <see cref="TenantManager"/> instance associated with the current project.
/// </summary>
public TenantManager TenantManager => this.tenantManager.Value;

/// <summary>
/// Returns the auth instance for the specified app.
/// </summary>
Expand Down Expand Up @@ -1401,6 +1409,7 @@ void IFirebaseService.Delete()
this.tokenFactory.DisposeIfCreated();
this.userManager.DisposeIfCreated();
this.providerConfigManager.DisposeIfCreated();
this.tenantManager.DisposeIfCreated();
}
}

Expand Down Expand Up @@ -1438,6 +1447,8 @@ internal sealed class Args

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

internal Lazy<TenantManager> TenantManager { get; set; }

internal static Args Create(FirebaseApp app)
{
return new Args()
Expand All @@ -1452,6 +1463,8 @@ internal static Args Create(FirebaseApp app)
() => FirebaseUserManager.Create(app), true),
ProviderConfigManager = new Lazy<ProviderConfigManager>(
() => Providers.ProviderConfigManager.Create(app), true),
TenantManager = new Lazy<TenantManager>(
() => Multitenancy.TenantManager.Create(app), true),
};
}

Expand All @@ -1464,6 +1477,7 @@ internal static Args CreateDefault()
SessionCookieVerifier = new Lazy<FirebaseTokenVerifier>(),
UserManager = new Lazy<FirebaseUserManager>(),
ProviderConfigManager = new Lazy<ProviderConfigManager>(),
TenantManager = new Lazy<TenantManager>(),
};
}
}
Expand Down
90 changes: 90 additions & 0 deletions FirebaseAdmin/FirebaseAdmin/Auth/Multitenancy/Tenant.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright 2020, Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using Google.Apis.Util;
using Newtonsoft.Json;

namespace FirebaseAdmin.Auth.Multitenancy
{
/// <summary>
/// Represents a tenant in a multi-tenant application..
///
/// <para>Multitenancy support requires Google Cloud Identity Platform (GCIP). To learn more
/// about GCIP, including pricing and features, see the
/// <a href="https://cloud.google.com/identity-platform">GCIP documentation</a>.</para>
///
/// <para>Before multitenancy can be used in a Google Cloud Identity Platform project, tenants
/// must be allowed on that project via the Cloud Console UI.</para>
///
/// <para>A tenant configuration provides information such as the display name, tenant
/// identifier and email authentication configuration. For OIDC/SAML provider configuration
/// management, TenantAwareFirebaseAuth instances should be used instead of a Tenant to
/// retrieve the list of configured IdPs on a tenant. When configuring these providers, note
/// that tenants will inherit whitelisted domains and authenticated redirect URIs of their
/// parent project.</para>
///
/// <para>All other settings of a tenant will also be inherited. These will need to be managed
/// from the Cloud Console UI.</para>
/// </summary>
public sealed class Tenant
{
private readonly Args args;

internal Tenant(Args args)
{
this.args = args.ThrowIfNull(nameof(args));
}

/// <summary>
/// Gets the tenant identifier.
/// </summary>
public string TenantId => this.ExtractResourceId(this.args.Name);

/// <summary>
/// Gets the tenant display name.
/// </summary>
public string DisplayName => args.DisplayName;

/// <summary>
/// Gets a value indicating whether the email sign-in provider is enabled.
/// </summary>
public bool PasswordSignUpAllowed => args.PasswordSignUpAllowed;

/// <summary>
/// Gets a value indicating whether the email link sign-in is enabled.
/// </summary>
public bool EmailLinkSignInEnabled => args.EmailLinkSignInEnabled;

private string ExtractResourceId(string resourceName)
{
var segments = resourceName.Split('/');
return segments[segments.Length - 1];
}

internal sealed class Args
{
[JsonProperty("name")]
internal string Name { get; set; }

[JsonProperty("displayName")]
internal string DisplayName { get; set; }

[JsonProperty("allowPasswordSignup")]
internal bool PasswordSignUpAllowed { get; set; }

[JsonProperty("enableEmailLinkSignin")]
internal bool EmailLinkSignInEnabled { get; set; }
}
}
}
Loading