From f77ec86a740649aa8e1ab5bb20cca6d47112db2d Mon Sep 17 00:00:00 2001 From: Itai Hanski Date: Tue, 26 Mar 2024 21:42:10 +0200 Subject: [PATCH] Add password settings management (#15) --- .../Management/PasswordSettingsTests.cs | 43 ++++++++++++++++ .../IntegrationTests/Management/UserTests.cs | 4 +- Descope/Internal/Http/HttpClient.cs | 24 ++++++--- Descope/Internal/Http/Routes.cs | 6 +++ Descope/Internal/Management/Managment.cs | 3 ++ Descope/Internal/Management/Password.cs | 50 +++++++++++++++++++ Descope/Sdk/Managment.cs | 31 +++++++++++- Descope/Types/Types.cs | 28 +++++++++++ README.md | 37 ++++++++++++++ 9 files changed, 216 insertions(+), 10 deletions(-) create mode 100644 Descope.Test/IntegrationTests/Management/PasswordSettingsTests.cs create mode 100644 Descope/Internal/Management/Password.cs diff --git a/Descope.Test/IntegrationTests/Management/PasswordSettingsTests.cs b/Descope.Test/IntegrationTests/Management/PasswordSettingsTests.cs new file mode 100644 index 0000000..3a6c8a9 --- /dev/null +++ b/Descope.Test/IntegrationTests/Management/PasswordSettingsTests.cs @@ -0,0 +1,43 @@ +using Xunit; + +namespace Descope.Test.Integration +{ + public class PasswordSettingsTests + { + private readonly DescopeClient _descopeClient = IntegrationTestSetup.InitDescopeClient(); + + [Fact] + public async Task PasswordSettings_GetAndUpdate() + { + string? tenantId = null; + try + { + // Create a tenant + tenantId = await _descopeClient.Management.Tenant.Create(new TenantOptions(Guid.NewGuid().ToString())); + + // update project level + var settings = await _descopeClient.Management.Password.GetSettings(); + settings.MinLength = 6; + await _descopeClient.Management.Password.ConfigureSettings(settings); + + // update tenant level + settings.MinLength = 7; + await _descopeClient.Management.Password.ConfigureSettings(settings, tenantId); + + // make sure changes don't clash + var projectSettings = await _descopeClient.Management.Password.GetSettings(); + Assert.Equal(6, projectSettings.MinLength); + var tenantSettings = await _descopeClient.Management.Password.GetSettings(tenantId); + Assert.Equal(7, tenantSettings.MinLength); + } + finally + { + if (!string.IsNullOrEmpty(tenantId)) + { + try { await _descopeClient.Management.Tenant.Delete(tenantId); } + catch { } + } + } + } + } +} diff --git a/Descope.Test/IntegrationTests/Management/UserTests.cs b/Descope.Test/IntegrationTests/Management/UserTests.cs index 1e07964..6885108 100644 --- a/Descope.Test/IntegrationTests/Management/UserTests.cs +++ b/Descope.Test/IntegrationTests/Management/UserTests.cs @@ -540,11 +540,11 @@ public async Task User_Password() Assert.False(createResult.Password); // Set a temporary password - await _descopeClient.Management.User.SetActivePassword(loginId, "abCD123#$"); + await _descopeClient.Management.User.SetActivePassword(loginId, "abCD123#$abCD123#$"); var loadResult = await _descopeClient.Management.User.Load(loginId); Assert.True(loadResult.Password); await _descopeClient.Management.User.ExpirePassword(loginId); - await _descopeClient.Management.User.SetTemporaryPassword(loginId, "abCD123#$"); + await _descopeClient.Management.User.SetTemporaryPassword(loginId, "abCD123#$abCD123#$"); } finally { diff --git a/Descope/Internal/Http/HttpClient.cs b/Descope/Internal/Http/HttpClient.cs index d8fef8a..d806867 100644 --- a/Descope/Internal/Http/HttpClient.cs +++ b/Descope/Internal/Http/HttpClient.cs @@ -7,9 +7,9 @@ public interface IHttpClient { DescopeConfig DescopeConfig { get; set; } - Task Get(string resource, string? pswd = null); + Task Get(string resource, string? pswd = null, Dictionary? queryParams = null); - Task Post(string resource, string? pswd = null, object? body = null); + Task Post(string resource, string? pswd = null, object? body = null, Dictionary? queryParams = null); Task Delete(string resource, string pswd); } @@ -37,14 +37,14 @@ public HttpClient(DescopeConfig descopeConfig) _client.AddDefaultHeader("x-descope-sdk-dotnet-version", Environment.Version.ToString()); } - public async Task Get(string resource, string? pswd = null) + public async Task Get(string resource, string? pswd = null, Dictionary? queryParams = null) { - return await Call(resource, Method.Get, pswd); + return await Call(resource, Method.Get, pswd, queryParams: queryParams); } - public async Task Post(string resource, string? pswd = null, object? body = null) + public async Task Post(string resource, string? pswd = null, object? body = null, Dictionary? queryParams = null) { - return await Call(resource, Method.Post, pswd, body); + return await Call(resource, Method.Post, pswd, body: body, queryParams: queryParams); } public async Task Delete(string resource, string? pswd = null) @@ -52,7 +52,7 @@ public async Task Delete(string resource, string? pswd = n return await Call(resource, Method.Delete, pswd); } - private async Task Call(string resource, Method method, string? pswd, object? body = null) + private async Task Call(string resource, Method method, string? pswd, object? body = null, Dictionary? queryParams = null) { var request = new RestRequest(resource, method); @@ -61,12 +61,22 @@ private async Task Call(string resource, Method method, st if (!string.IsNullOrEmpty(pswd)) bearer = $"{bearer}:{pswd}"; request.AddHeader("Authorization", "Bearer " + bearer); + // Add body if available if (body != null) { var jsonBody = JsonSerializer.Serialize(body); request.AddJsonBody(jsonBody); } + // Add query params if available + if (queryParams != null) + { + foreach (var param in queryParams) + { + request.AddQueryParameter(param.Key, param.Value); + } + } + var response = await _client.ExecuteAsync(request); if (response.StatusCode == System.Net.HttpStatusCode.OK) diff --git a/Descope/Internal/Http/Routes.cs b/Descope/Internal/Http/Routes.cs index 4c27bb0..f7f2246 100644 --- a/Descope/Internal/Http/Routes.cs +++ b/Descope/Internal/Http/Routes.cs @@ -78,6 +78,12 @@ public static class Routes #endregion User + #region Password + + public const string PasswordSettings = "/v1/mgmt/password/settings"; + + #endregion Password + #region JWT public const string JwtUpdate = "/v1/mgmt/jwt/update"; diff --git a/Descope/Internal/Management/Managment.cs b/Descope/Internal/Management/Managment.cs index 6762640..8f9be5a 100644 --- a/Descope/Internal/Management/Managment.cs +++ b/Descope/Internal/Management/Managment.cs @@ -5,6 +5,7 @@ internal class Management : IManagement public ITenant Tenant => _tenant; public IUser User => _user; public IAccessKey AccessKey => _accessKey; + public IPasswordSettings Password => _password; public IJwt Jwt => _jwt; public IPermission Permission => _permission; public IRole Role => _role; @@ -13,6 +14,7 @@ internal class Management : IManagement private readonly Tenant _tenant; private readonly User _user; private readonly AccessKey _accessKey; + private readonly Password _password; private readonly Jwt _jwt; private readonly Permission _permission; private readonly Role _role; @@ -23,6 +25,7 @@ public Management(IHttpClient client, string managementKey) _tenant = new Tenant(client, managementKey); _user = new User(client, managementKey); _accessKey = new AccessKey(client, managementKey); + _password = new Password(client, managementKey); _jwt = new Jwt(client, managementKey); _permission = new Permission(client, managementKey); _role = new Role(client, managementKey); diff --git a/Descope/Internal/Management/Password.cs b/Descope/Internal/Management/Password.cs new file mode 100644 index 0000000..68b2a63 --- /dev/null +++ b/Descope/Internal/Management/Password.cs @@ -0,0 +1,50 @@ +using System.Text.Json.Serialization; + +namespace Descope.Internal.Management +{ + internal class Password : IPasswordSettings + { + private readonly IHttpClient _httpClient; + private readonly string _managementKey; + + internal Password(IHttpClient httpClient, string managementKey) + { + _httpClient = httpClient; + _managementKey = managementKey; + } + + public async Task GetSettings(string? tenantId = null) + { + return await _httpClient.Get(Routes.PasswordSettings, _managementKey, queryParams: new Dictionary { { "tenantId", tenantId } }); + } + + public async Task ConfigureSettings(PasswordSettings settings, string? tenantId = null) + { + var body = new WrappedSettings + { + TenantId = tenantId, + Enabled = settings.Enabled, + MinLength = settings.MinLength, + Lowercase = settings.Lowercase, + Uppercase = settings.Uppercase, + Number = settings.Number, + NonAlphanumeric = settings.NonAlphanumeric, + Expiration = settings.Expiration, + ExpirationWeeks = settings.ExpirationWeeks, + Reuse = settings.Reuse, + ReuseAmount = settings.ReuseAmount, + Lock = settings.Lock, + LockAttempts = settings.LockAttempts, + }; + await _httpClient.Post(Routes.PasswordSettings, _managementKey, body); + } + + } + + internal class WrappedSettings : PasswordSettings + { + [JsonPropertyName("tenantId")] + public string? TenantId { get; set; } + } + +} diff --git a/Descope/Sdk/Managment.cs b/Descope/Sdk/Managment.cs index 0f6aa35..7749146 100644 --- a/Descope/Sdk/Managment.cs +++ b/Descope/Sdk/Managment.cs @@ -608,6 +608,30 @@ public interface IRole Task> SearchAll(RoleSearchOptions? options); } + /// + /// Provides functions for managing password policy for a project or a tenant. + /// + public interface IPasswordSettings + { + /// + /// Get password settings for a project or tenant. + /// + /// Optionally scope the settings to a tenant + /// The current password settings + Task GetSettings(string? tenantId = null); + + /// + /// Configure Password settings for a project or a tenant manually. + /// + /// NOTE: The settings parameter is taken as is and overrides any current settings. + /// Use carefully. + /// + /// + /// The settings to set - taken as is + /// Optionally scope the settings to a tenant + Task ConfigureSettings(PasswordSettings settings, string? tenantId = null); + } + /// /// Provide functions for manipulating valid JWT /// @@ -624,7 +648,7 @@ public interface IJwt /// /// Impersonate another user /// - /// The impersonator user must have the Impersonation permission in order for this request to work + /// The impersonator user must have the Impersonation permission in order for this request to work /// /// /// The user ID performing the impersonation @@ -703,6 +727,11 @@ public interface IManagement /// public IAccessKey AccessKey { get; } + /// + /// Provides functions for managing password policy for a project or a tenant. + /// + public IPasswordSettings Password { get; } + /// /// Provides functions for manipulating valid JWTs /// diff --git a/Descope/Types/Types.cs b/Descope/Types/Types.cs index 4cfe1bb..fe5780f 100644 --- a/Descope/Types/Types.cs +++ b/Descope/Types/Types.cs @@ -646,4 +646,32 @@ public class RoleSearchOptions [JsonPropertyName("permissionNames")] public List? PermissionNames { get; set; } } + + public class PasswordSettings + { + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + [JsonPropertyName("minLength")] + public int MinLength { get; set; } + [JsonPropertyName("lowercase")] + public bool Lowercase { get; set; } + [JsonPropertyName("uppercase")] + public bool Uppercase { get; set; } + [JsonPropertyName("number")] + public bool Number { get; set; } + [JsonPropertyName("nonAlphanumeric")] + public bool NonAlphanumeric { get; set; } + [JsonPropertyName("expiration")] + public bool Expiration { get; set; } + [JsonPropertyName("expirationWeeks")] + public int ExpirationWeeks { get; set; } + [JsonPropertyName("reuse")] + public bool Reuse { get; set; } + [JsonPropertyName("reuseAmount")] + public int ReuseAmount { get; set; } + [JsonPropertyName("lock")] + public bool Lock { get; set; } + [JsonPropertyName("lockAttempts")] + public int LockAttempts { get; set; } + } } diff --git a/README.md b/README.md index ed3eac9..66b535a 100644 --- a/README.md +++ b/README.md @@ -455,6 +455,43 @@ var loginOptions = new AccessKeyLoginOptions var token = await descopeClient.Auth.ExchangeAccessKey("accessKey", loginOptions); ``` +### Manage Password Settings + +You can manage password settings for your project or tenants. + +```cs +try +{ + // You can get password settings for the project or for a specific tenant ID. + var settings = await _descopeClient.Management.Password.GetSettings("optional-tenant-id"); + + // You can configure the project level settings, by leaving the optional tenant ID empty, + // or tenant level password settings by providing a tenant ID. + // The update is performed as-is in an overriding manner - use carefully. + var updatedSettings = new PasswordSettings + { + Enabled = true, + MinLength = 8, + Lowercase = true, + Uppercase = true, + Number = true, + NonAlphanumeric = true, + Expiration = true, + ExpirationWeeks = 3, + Reuse = true, + ReuseAmount = 3, + Lock = true, + LockAttempts = 5, + }; + await _descopeClient.Management.Password.ConfigureSettings(updatedSettings, "optional-tenant-id"); + +} +catch (DescopeException e) +{ + // handle errors +} +``` + ### Manage and Manipulate JWTs You can update custom claims on a valid JWT or even impersonate a different user - as long as the