Skip to content

Commit

Permalink
Add SSO management
Browse files Browse the repository at this point in the history
  • Loading branch information
itaihanski committed Mar 28, 2024
1 parent f77ec86 commit 91af115
Show file tree
Hide file tree
Showing 9 changed files with 603 additions and 16 deletions.
154 changes: 154 additions & 0 deletions Descope.Test/IntegrationTests/Management/SsoTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
using Xunit;

namespace Descope.Test.Integration
{
public class SsoTests
{
private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient();

[Fact]
public async Task Sso_SamlSetAndDelete()
{
string? tenantId = null;
string? roleName = null;
try
{
// Create a tenant
tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString()));
roleName = Guid.NewGuid().ToString()[..20];
await _descopeClient.Management.Role.Create(roleName, tenantId: tenantId);

// Update sso settings
var settings = new SsoSamlSettings("https://sometestidp.com", "entityId", "cert")
{
RoleMappings = new List<RoleMapping> { new RoleMapping(new List<string> { "group1", "group2" }, roleName) }
};
await _descopeClient.Management.Sso.ConfigureSAMLSettings(tenantId, settings, "https://myredirect.com", new List<string> { "domain1.com" });

var loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId);

// Make sure the settings match
Assert.Equal(settings.IdpUrl, loadedSetting.Saml?.IdpSsoUrl);
Assert.Equal(settings.IdpEntityId, loadedSetting.Saml?.IdpEntityId);
Assert.Equal(settings.IdpCertificate, loadedSetting.Saml!.IdpCertificate);
Assert.NotEmpty(loadedSetting.Saml.GroupsMapping!.First().Role!.Id);
Assert.Equal("group1", loadedSetting.Saml.GroupsMapping!.First().Groups![0]);
Assert.Equal("group2", loadedSetting.Saml.GroupsMapping!.First().Groups![1]);
Assert.Equal("https://myredirect.com", loadedSetting.Saml?.RedirectUrl);
Assert.Equal("domain1.com", loadedSetting.Tenant.Domains.First());

// Delete the settings
await _descopeClient.Management.Sso.DeleteSettings(tenantId);
loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId);
Assert.Empty(loadedSetting.Saml.IdpSsoUrl ?? "");
}
finally
{
if (!string.IsNullOrEmpty(tenantId))
{
try { await _descopeClient.Management.Tenant.Delete(tenantId); }
catch { }
}
if (!string.IsNullOrEmpty(roleName))
{
try { await _descopeClient.Management.Role.Delete(roleName); }
catch { }
}
}
}

[Fact]
public async Task Sso_SamlByMetadata()
{
string? tenantId = null;
string? roleName = null;
try
{
// Create a tenant
tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString()));
roleName = Guid.NewGuid().ToString()[..20];
await _descopeClient.Management.Role.Create(roleName, tenantId: tenantId);

// update sso settings
var settings = new SsoSamlSettingsByMetadata("https://sometestidpmd.com")
{
RoleMappings = new List<RoleMapping> { new RoleMapping(new List<string> { "group1", "group2" }, roleName) }
};
await _descopeClient.Management.Sso.ConfigureSamlSettingsByMetadata(tenantId, settings, "https://myredirect.com", new List<string> { "domain1.com" });

var loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId);

// Make sure the settings match
Assert.Equal(settings.IdpMetadataUrl, loadedSetting.Saml.IdpMetadataUrl);
Assert.NotEmpty(loadedSetting.Saml.GroupsMapping?.First()?.Role?.Id ?? "");
Assert.Equal("group1", loadedSetting.Saml.GroupsMapping?.First()?.Groups?[0]);
Assert.Equal("group2", loadedSetting.Saml.GroupsMapping?.First()?.Groups?[1]);
Assert.Equal("https://myredirect.com", loadedSetting.Saml?.RedirectUrl);
Assert.Equal("domain1.com", loadedSetting.Tenant.Domains.First());
}
finally
{
if (!string.IsNullOrEmpty(tenantId))
{
try { await _descopeClient.Management.Tenant.Delete(tenantId); }
catch { }
}
if (!string.IsNullOrEmpty(roleName))
{
try { await _descopeClient.Management.Role.Delete(roleName); }
catch { }
}
}
}

[Fact]
public async Task Sso_Oidc()
{
string? tenantId = null;
string? roleName = null;
try
{
// Create a tenant
tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString()));
roleName = Guid.NewGuid().ToString()[..20];
await _descopeClient.Management.Role.Create(roleName, tenantId: tenantId);

// Update sso settings
var settings = new SsoOidcSettings
{
Name = "Name",
ClientId = "ClientId",
ClientSecret = "ClientSecret",
AuthUrl = "https://mytestauth.com",
TokenUrl = "https://mytestauth.com",
JwksUrl = "https://mytestauth.com",
AttributeMapping = new OidcAttributeMapping { }
};
await _descopeClient.Management.Sso.ConfigureOidcSettings(tenantId, settings, new List<string> { "domain1.com" });

var loadedSetting = await _descopeClient.Management.Sso.LoadSettings(tenantId);

// Make sure the settings match
Assert.Equal(settings.Name, loadedSetting.Oidc.Name);
Assert.Equal(settings.ClientId, loadedSetting.Oidc.ClientId);
Assert.Equal(settings.AuthUrl, loadedSetting.Oidc.AuthUrl);
Assert.Equal(settings.TokenUrl, loadedSetting.Oidc.TokenUrl);
Assert.Equal(settings.JwksUrl, loadedSetting.Oidc.JwksUrl);
Assert.Equal("domain1.com", loadedSetting.Tenant.Domains.First());
}
finally
{
if (!string.IsNullOrEmpty(tenantId))
{
try { await _descopeClient.Management.Tenant.Delete(tenantId); }
catch { }
}
if (!string.IsNullOrEmpty(roleName))
{
try { await _descopeClient.Management.Role.Delete(roleName); }
catch { }
}
}
}
}
}
6 changes: 3 additions & 3 deletions Descope/Internal/Http/HttpClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public interface IHttpClient

Task<TResponse> Post<TResponse>(string resource, string? pswd = null, object? body = null, Dictionary<string, string?>? queryParams = null);

Task<TResponse> Delete<TResponse>(string resource, string pswd);
Task<TResponse> Delete<TResponse>(string resource, string pswd, Dictionary<string, string?>? queryParams = null);
}

public class HttpClient : IHttpClient
Expand Down Expand Up @@ -47,9 +47,9 @@ public async Task<TResponse> Post<TResponse>(string resource, string? pswd = nul
return await Call<TResponse>(resource, Method.Post, pswd, body: body, queryParams: queryParams);
}

public async Task<TResponse> Delete<TResponse>(string resource, string? pswd = null)
public async Task<TResponse> Delete<TResponse>(string resource, string? pswd = null, Dictionary<string, string?>? queryParams = null)
{
return await Call<TResponse>(resource, Method.Delete, pswd);
return await Call<TResponse>(resource, Method.Delete, pswd, queryParams: queryParams);
}

private async Task<TResponse> Call<TResponse>(string resource, Method method, string? pswd, object? body = null, Dictionary<string, string?>? queryParams = null)
Expand Down
10 changes: 10 additions & 0 deletions Descope/Internal/Http/Routes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,16 @@ public static class Routes

#endregion AccessKey

#region SSO

public const string SsoLoadSettings = "/v2/mgmt/sso/settings";
public const string SsoSetSaml = "/v1/mgmt/sso/saml";
public const string SsoSetSamlByMetadata = "/v1/mgmt/sso/saml/metadata";
public const string SsoSetOidc = "/v1/mgmt/sso/oidc";
public const string SsoDeleteSettings = "/v1/mgmt/sso/settings";

#endregion SSO

#region Permission

public const string PermissionCreate = "/v1/mgmt/permission/create";
Expand Down
3 changes: 3 additions & 0 deletions Descope/Internal/Management/Managment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ internal class Management : IManagement
public ITenant Tenant => _tenant;
public IUser User => _user;
public IAccessKey AccessKey => _accessKey;
public ISso Sso => _sso;
public IPasswordSettings Password => _password;
public IJwt Jwt => _jwt;
public IPermission Permission => _permission;
Expand All @@ -14,6 +15,7 @@ internal class Management : IManagement
private readonly Tenant _tenant;
private readonly User _user;
private readonly AccessKey _accessKey;
private readonly Sso _sso;
private readonly Password _password;
private readonly Jwt _jwt;
private readonly Permission _permission;
Expand All @@ -25,6 +27,7 @@ public Management(IHttpClient client, string managementKey)
_tenant = new Tenant(client, managementKey);
_user = new User(client, managementKey);
_accessKey = new AccessKey(client, managementKey);
_sso = new Sso(client, managementKey);
_password = new Password(client, managementKey);
_jwt = new Jwt(client, managementKey);
_permission = new Permission(client, managementKey);
Expand Down
91 changes: 91 additions & 0 deletions Descope/Internal/Management/Sso.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
using System.Text.Json.Serialization;

namespace Descope.Internal.Management
{
internal class Sso : ISso
{
private readonly IHttpClient _httpClient;
private readonly string _managementKey;

internal Sso(IHttpClient httpClient, string managementKey)
{
_httpClient = httpClient;
_managementKey = managementKey;
}

public async Task<SsoTenantSettings> LoadSettings(string tenantId)
{
Utils.EnforceRequiredArgs(("tenantId", tenantId));
return await _httpClient.Get<SsoTenantSettings>(Routes.SsoLoadSettings, _managementKey, queryParams: new Dictionary<string, string?> { { "tenantId", tenantId } });
}

public async Task ConfigureSAMLSettings(string tenantId, SsoSamlSettings settings, string? redirectUrl = null, List<string>? domains = null)
{
Utils.EnforceRequiredArgs(
("tenantId", tenantId),
("IdpUrl", settings.IdpUrl),
("IdpCertificate", settings.IdpCertificate),
("IdpEntityId", settings.IdpEntityId));

var body = new
{
tenantId,
settings = new
{
idpUrl = settings.IdpUrl,
entityId = settings.IdpEntityId,
idpCert = settings.IdpCertificate,
roleMappings = ConvertRoleMapping(settings.RoleMappings),
attributeMapping = settings.AttributeMapping,
},
redirectUrl,
domains,
};
await _httpClient.Post<object>(Routes.SsoSetSaml, _managementKey, body: body);
}

public async Task ConfigureSamlSettingsByMetadata(string tenantId, SsoSamlSettingsByMetadata settings, string? redirectUrl = null, List<string>? domains = null)
{
Utils.EnforceRequiredArgs(("tenantId", tenantId), ("IdpMetadataUrl", settings.IdpMetadataUrl));
var body = new
{
tenantId,
settings = new
{
idpMetadataUrl = settings.IdpMetadataUrl,
roleMappings = ConvertRoleMapping(settings.RoleMappings),
attributeMapping = settings.AttributeMapping,
},
redirectUrl,
domains,
};
await _httpClient.Post<object>(Routes.SsoSetSamlByMetadata, _managementKey, body: body);
}

public async Task ConfigureOidcSettings(string tenantId, SsoOidcSettings settings, List<string>? domains = null)
{
Utils.EnforceRequiredArgs(("tenantId", tenantId));
var body = new
{
tenantId,
settings,
domains,
};
await _httpClient.Post<object>(Routes.SsoSetOidc, _managementKey, body: body);
}

public async Task DeleteSettings(string tenantId)
{
Utils.EnforceRequiredArgs(("tenantId", tenantId));
await _httpClient.Delete<object>(Routes.SsoDeleteSettings, _managementKey, queryParams: new Dictionary<string, string?> { { "tenantId", tenantId } });
}

private static List<Dictionary<string, object>> ConvertRoleMapping(List<RoleMapping>? roleMappings)
{
var mappings = new List<Dictionary<string, object>>();
roleMappings?.ForEach(entry => mappings.Add(new Dictionary<string, object> { { "groups", entry.Groups }, { "roleName", entry.Role } }));
return mappings;
}

}
}
16 changes: 16 additions & 0 deletions Descope/Internal/Utils/Utils.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
namespace Descope.Internal.Management
{
internal class Utils
{
internal static void EnforceRequiredArgs(params (string, object?)[] args)
{
foreach (var arg in args)
{
if (arg.Item2 == null || (arg.Item2 is string s && string.IsNullOrEmpty(s)))
{
throw new DescopeException($"The {arg.Item1} argument is required");
}
}
}
}
}
Loading

0 comments on commit 91af115

Please sign in to comment.