From 94e6f0c4a14a542fd8fe3039ae41b5fc73eba7a1 Mon Sep 17 00:00:00 2001 From: Slavik Markovich Date: Wed, 7 Feb 2024 11:30:56 -0800 Subject: [PATCH] Password settings and other minor fixes (#95) * Get and set password settings for project and tenants * pom version --- examples/management-cli/pom.xml | 2 +- pom.xml | 2 +- .../exception/ClientFunctionalException.java | 10 ++ .../java/com/descope/literals/Routes.java | 12 +- .../descope/model/mgmt/AccessKeyRequest.java | 2 +- .../model/mgmt/AccessKeyResponseDetails.java | 1 + .../model/mgmt/ManagementServices.java | 2 + .../passwordsettings/PasswordSettings.java | 25 ++++ .../java/com/descope/model/tenant/Tenant.java | 2 + .../descope/model/tenant/TenantSettings.java | 26 ++++ .../user/response/UserHistoryResponse.java | 21 +++ src/main/java/com/descope/proxy/ApiProxy.java | 5 + .../descope/proxy/impl/AbstractProxyImpl.java | 29 ++++- .../com/descope/proxy/impl/ApiProxyImpl.java | 11 ++ .../sdk/auth/AuthenticationService.java | 20 +++ .../auth/impl/AuthenticationServiceImpl.java | 38 +++++- .../auth/impl/EnchantedLinkServiceImpl.java | 3 +- .../descope/sdk/mgmt/AccessKeyService.java | 6 +- .../sdk/mgmt/PasswordSettingsService.java | 41 ++++++ .../com/descope/sdk/mgmt/TenantService.java | 26 ++++ .../com/descope/sdk/mgmt/UserService.java | 10 ++ .../sdk/mgmt/impl/AccessKeyServiceImpl.java | 20 ++- .../mgmt/impl/ManagementServiceBuilder.java | 1 + .../impl/PasswordSettingsServiceImpl.java | 82 ++++++++++++ .../sdk/mgmt/impl/TenantServiceImpl.java | 89 +++++++++++-- .../sdk/mgmt/impl/UserServiceImpl.java | 14 ++ src/main/java/com/descope/utils/JwtUtils.java | 9 +- .../impl/AuthenticationServiceImplTest.java | 7 +- .../impl/PasswordSettingsServiceImplTest.java | 123 ++++++++++++++++++ .../sdk/mgmt/impl/TenantServiceImplTest.java | 73 +++++++++++ .../sdk/mgmt/impl/UserServiceImplTest.java | 5 + 31 files changed, 678 insertions(+), 39 deletions(-) create mode 100644 src/main/java/com/descope/model/passwordsettings/PasswordSettings.java create mode 100644 src/main/java/com/descope/model/tenant/TenantSettings.java create mode 100644 src/main/java/com/descope/model/user/response/UserHistoryResponse.java create mode 100644 src/main/java/com/descope/sdk/mgmt/PasswordSettingsService.java create mode 100644 src/main/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImpl.java create mode 100644 src/test/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImplTest.java diff --git a/examples/management-cli/pom.xml b/examples/management-cli/pom.xml index ddca77e9..e7bba2de 100644 --- a/examples/management-cli/pom.xml +++ b/examples/management-cli/pom.xml @@ -19,7 +19,7 @@ com.descope java-sdk - 1.0.12 + 1.0.13 info.picocli diff --git a/pom.xml b/pom.xml index 901003d4..da62baa9 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.descope java-sdk 4.0.0 - 1.0.12 + 1.0.13 ${project.groupId}:${project.artifactId} Java library used to integrate with Descope. https://github.com/descope/descope-java diff --git a/src/main/java/com/descope/exception/ClientFunctionalException.java b/src/main/java/com/descope/exception/ClientFunctionalException.java index bd4df1a8..414df1e2 100644 --- a/src/main/java/com/descope/exception/ClientFunctionalException.java +++ b/src/main/java/com/descope/exception/ClientFunctionalException.java @@ -9,8 +9,18 @@ protected ClientFunctionalException(String message, String code) { setCode(code); } + protected ClientFunctionalException(String message, String code, Throwable cause) { + super(message, cause); + setCode(code); + } + public static ClientFunctionalException invalidToken() { String message = "Invalid Token"; return new ClientFunctionalException(message, INVALID_TOKEN); } + + public static ClientFunctionalException invalidToken(Throwable cause) { + String message = "Invalid Token"; + return new ClientFunctionalException(message, INVALID_TOKEN); + } } diff --git a/src/main/java/com/descope/literals/Routes.java b/src/main/java/com/descope/literals/Routes.java index abb9116b..e9eac543 100644 --- a/src/main/java/com/descope/literals/Routes.java +++ b/src/main/java/com/descope/literals/Routes.java @@ -11,6 +11,10 @@ public static class AuthEndPoints { public static final String LOG_OUT_LINK = "/v1/auth/logout"; public static final String LOG_OUT_ALL_LINK = "/v1/auth/logoutall"; + // My details + public static final String ME_LINK = "/v1/auth/me"; + public static final String HISTORY_LINK = "/v1/auth/me/history"; + // MagicLink public static final String SIGN_IN_MAGIC_LINK = "/v1/auth/magiclink/signin"; public static final String SIGN_UP_MAGIC_LINK = "/v1/auth/magiclink/signup"; @@ -102,14 +106,17 @@ public static class ManagementEndPoints { public static final String USER_SET_PASSWORD_LINK = "/v1/mgmt/user/password/set"; public static final String USER_EXPIRE_PASSWORD_LINK = "/v1/mgmt/user/password/expire"; public static final String USER_CREATE_EMBEDDED_LINK = "/v1/mgmt/user/signin/embeddedlink"; + public static final String USER_HISTORY_LINK = "/v1/mgmt/user/history"; // Tenant public static final String CREATE_TENANT_LINK = "/v1/mgmt/tenant/create"; public static final String UPDATE_TENANT_LINK = "/v1/mgmt/tenant/update"; public static final String DELETE_TENANT_LINK = "/v1/mgmt/tenant/delete"; + public static final String LOAD_TENANT_LINK = "/v1/mgmt/tenant"; public static final String LOAD_ALL_TENANTS_LINK = "/v1/mgmt/tenant/all"; public static final String TENANT_SEARCH_ALL_LINK = "/v1/mgmt/tenant/search"; - + public static final String GET_TENANT_SETTINGS_LINK = "/v1/mgmt/tenant/settings"; + // SSO public static final String SSO_GET_SETTINGS_LINK = "/mgmt/sso/settings"; public static final String SSO_DELETE_SETTINGS_LINK = "/mgmt/sso/settings"; @@ -179,5 +186,8 @@ public static class ManagementEndPoints { public static final String MANAGEMENT_AUTHZ_RE_TARGETS = "/v1/mgmt/authz/re/targets"; public static final String MANAGEMENT_AUTHZ_RE_TARGET_ALL = "/v1/mgmt/authz/re/targetall"; public static final String MANAGEMENT_AUTHZ_GET_MODIFIED = "/v1/mgmt/authz/getmodified"; + + // Password settings + public static final String MANAGEMENT_PASSWORD_SETTINGS = "/v1/mgmt/password/settings"; } } diff --git a/src/main/java/com/descope/model/mgmt/AccessKeyRequest.java b/src/main/java/com/descope/model/mgmt/AccessKeyRequest.java index ab60a53f..75512035 100644 --- a/src/main/java/com/descope/model/mgmt/AccessKeyRequest.java +++ b/src/main/java/com/descope/model/mgmt/AccessKeyRequest.java @@ -8,9 +8,9 @@ @Data @Builder public class AccessKeyRequest { - private String name; private long expireTime; private List roleNames; private List> keyTenants; + private String userId; } diff --git a/src/main/java/com/descope/model/mgmt/AccessKeyResponseDetails.java b/src/main/java/com/descope/model/mgmt/AccessKeyResponseDetails.java index 9e1fc7ad..4d1a0449 100644 --- a/src/main/java/com/descope/model/mgmt/AccessKeyResponseDetails.java +++ b/src/main/java/com/descope/model/mgmt/AccessKeyResponseDetails.java @@ -21,4 +21,5 @@ public class AccessKeyResponseDetails { private long expireTime; private String createdBy; private String clientId; + private String userId; } diff --git a/src/main/java/com/descope/model/mgmt/ManagementServices.java b/src/main/java/com/descope/model/mgmt/ManagementServices.java index 85028b3e..8c7778e8 100644 --- a/src/main/java/com/descope/model/mgmt/ManagementServices.java +++ b/src/main/java/com/descope/model/mgmt/ManagementServices.java @@ -6,6 +6,7 @@ import com.descope.sdk.mgmt.FlowService; import com.descope.sdk.mgmt.GroupService; import com.descope.sdk.mgmt.JwtService; +import com.descope.sdk.mgmt.PasswordSettingsService; import com.descope.sdk.mgmt.PermissionService; import com.descope.sdk.mgmt.ProjectService; import com.descope.sdk.mgmt.RolesService; @@ -30,4 +31,5 @@ public class ManagementServices { AuditService auditService; AuthzService authzService; ProjectService projectService; + PasswordSettingsService passwordSettingsService; } diff --git a/src/main/java/com/descope/model/passwordsettings/PasswordSettings.java b/src/main/java/com/descope/model/passwordsettings/PasswordSettings.java new file mode 100644 index 00000000..02db7859 --- /dev/null +++ b/src/main/java/com/descope/model/passwordsettings/PasswordSettings.java @@ -0,0 +1,25 @@ +package com.descope.model.passwordsettings; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class PasswordSettings { + private Boolean enabled; + private Integer minLength; + private Boolean lowercase; + private Boolean uppercase; + private Boolean number; + private Boolean nonAlphanumeric; + private Boolean expiration; + private Integer expirationWeeks; + private Boolean reuse; + private Integer reuseAmount; + private Boolean lock; + private Integer lockAttempts; +} diff --git a/src/main/java/com/descope/model/tenant/Tenant.java b/src/main/java/com/descope/model/tenant/Tenant.java index fbe5cdb5..d0a3598b 100644 --- a/src/main/java/com/descope/model/tenant/Tenant.java +++ b/src/main/java/com/descope/model/tenant/Tenant.java @@ -16,4 +16,6 @@ public class Tenant { String name; List selfProvisioningDomains; Map customAttributes; + String authType; + List domains; } diff --git a/src/main/java/com/descope/model/tenant/TenantSettings.java b/src/main/java/com/descope/model/tenant/TenantSettings.java new file mode 100644 index 00000000..f69fc22a --- /dev/null +++ b/src/main/java/com/descope/model/tenant/TenantSettings.java @@ -0,0 +1,26 @@ +package com.descope.model.tenant; + +import com.fasterxml.jackson.annotation.JsonAlias; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TenantSettings { + List domains; + List selfProvisioningDomains; + @JsonAlias({"enabled"}) + Boolean sessionSettingsEnabled; + Integer refreshTokenExpiration; + String refreshTokenExpirationUnit; + Integer sessionTokenExpiration; + String sessionTokenExpirationUnit; + Boolean enableInactivity; + Integer inactivityTime; + String inactivityTimeUnit; +} diff --git a/src/main/java/com/descope/model/user/response/UserHistoryResponse.java b/src/main/java/com/descope/model/user/response/UserHistoryResponse.java new file mode 100644 index 00000000..59a12b36 --- /dev/null +++ b/src/main/java/com/descope/model/user/response/UserHistoryResponse.java @@ -0,0 +1,21 @@ +package com.descope.model.user.response; + +import java.time.Instant; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UserHistoryResponse { + String userId; + Integer loginTime; + String city; + String country; + String ip; + + public Instant getLoginTimeInstant() { + return Instant.ofEpochSecond(loginTime); + } +} diff --git a/src/main/java/com/descope/proxy/ApiProxy.java b/src/main/java/com/descope/proxy/ApiProxy.java index dc91f58c..81804e69 100644 --- a/src/main/java/com/descope/proxy/ApiProxy.java +++ b/src/main/java/com/descope/proxy/ApiProxy.java @@ -1,11 +1,16 @@ package com.descope.proxy; +import com.fasterxml.jackson.core.type.TypeReference; import java.net.URI; public interface ApiProxy { R get(URI uri, Class returnClz); + R getArray(URI uri, TypeReference typeReference); + R post(URI uri, B body, Class returnClz); + R postAndGetArray(URI uri, B body, TypeReference typeReference); + R delete(URI uri, B body, Class returnClz); } diff --git a/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java b/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java index 4177c7c4..3ed7d68f 100644 --- a/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java +++ b/src/main/java/com/descope/proxy/impl/AbstractProxyImpl.java @@ -5,6 +5,7 @@ import com.descope.exception.ServerCommonException; import com.descope.model.client.SdkInfo; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.ByteArrayOutputStream; @@ -61,11 +62,12 @@ long getRetryHeader(final ClassicHttpResponse res) { } @SneakyThrows - R exchange(ClassicHttpRequest req, Class returnClz) { + R exchange(ClassicHttpRequest req, Class returnClz, TypeReference typeReference) { addHeaders(req); log.debug(String.format("Sending %s request to %s", req.getMethod(), req.getRequestUri())); try (final CloseableHttpClient httpClient = HttpClients.createDefault()) { return httpClient.execute(req, new HttpClientResponseHandler() { + @SuppressWarnings("resource") @Override public R handleResponse(ClassicHttpResponse response) throws HttpException, IOException { try (final ClassicHttpResponse res = response) { @@ -102,7 +104,9 @@ public R handleResponse(ClassicHttpResponse response) throws HttpException, IOEx bs.toString(), String.valueOf(res.getCode()), bs.toString()); } } - R r = objectMapper.readValue(tee, returnClz); + R r = returnClz != null + ? objectMapper.readValue(tee, returnClz) + : objectMapper.readValue(tee, typeReference); if (log.isDebugEnabled()) { String resStr = bs.toString(); log.debug(String.format("Received response %s", @@ -145,14 +149,29 @@ protected R post(URI uri, B body, Class returnClz) { final byte[] payload = objectMapper.writeValueAsBytes(body); builder.setEntity(new ByteArrayEntity(payload, ContentType.APPLICATION_JSON)); } - return exchange(builder.build(), returnClz); + return exchange(builder.build(), returnClz, null); + } + + @SneakyThrows + protected R post(URI uri, B body, TypeReference typeReference) { + final ClassicRequestBuilder builder = ClassicRequestBuilder.post(uri); + if (body != null) { + final ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(Include.NON_NULL); + final byte[] payload = objectMapper.writeValueAsBytes(body); + builder.setEntity(new ByteArrayEntity(payload, ContentType.APPLICATION_JSON)); + } + return exchange(builder.build(), null, typeReference); } protected R get(URI uri, Class returnClz) { - return exchange(ClassicRequestBuilder.get(uri).build(), returnClz); + return exchange(ClassicRequestBuilder.get(uri).build(), returnClz, null); + } + + protected R get(URI uri, TypeReference typeReference) { + return exchange(ClassicRequestBuilder.get(uri).build(), null, typeReference); } protected R delete(URI uri, B body, Class returnClz) { - return exchange(ClassicRequestBuilder.delete(uri).build(), returnClz); + return exchange(ClassicRequestBuilder.delete(uri).build(), returnClz, null); } } diff --git a/src/main/java/com/descope/proxy/impl/ApiProxyImpl.java b/src/main/java/com/descope/proxy/impl/ApiProxyImpl.java index c623a74a..c5b95c0d 100644 --- a/src/main/java/com/descope/proxy/impl/ApiProxyImpl.java +++ b/src/main/java/com/descope/proxy/impl/ApiProxyImpl.java @@ -2,6 +2,7 @@ import com.descope.model.client.SdkInfo; import com.descope.proxy.ApiProxy; +import com.fasterxml.jackson.core.type.TypeReference; import java.net.URI; import java.util.function.Supplier; @@ -21,11 +22,21 @@ public R post(URI uri, B body, Class returnClz) { return super.post(uri, body, returnClz); } + @Override + public R postAndGetArray(URI uri, B body, TypeReference typeReference) { + return super.post(uri, body, typeReference); + } + @Override public R get(URI uri, Class returnClz) { return super.get(uri, returnClz); } + @Override + public R getArray(URI uri, TypeReference typeReference) { + return super.get(uri, typeReference); + } + @Override public R delete(URI uri, B body, Class returnClz) { return super.delete(uri, body, returnClz); diff --git a/src/main/java/com/descope/sdk/auth/AuthenticationService.java b/src/main/java/com/descope/sdk/auth/AuthenticationService.java index 24ce7eea..cd74d244 100644 --- a/src/main/java/com/descope/sdk/auth/AuthenticationService.java +++ b/src/main/java/com/descope/sdk/auth/AuthenticationService.java @@ -2,6 +2,8 @@ import com.descope.exception.DescopeException; import com.descope.model.jwt.Token; +import com.descope.model.user.response.UserHistoryResponse; +import com.descope.model.user.response.UserResponse; import java.util.List; public interface AuthenticationService { @@ -204,4 +206,22 @@ boolean validatePermissions(Token token, String tenant, List permissions * @throws DescopeException if there is an error */ void logoutAll(String refreshToken) throws DescopeException; + + /** + * Use to retrieve current session user details. The request requires a valid refresh token. + * + * @param refreshToken a valid refresh token + * @return {@link UserResponse} returns the user details. + * @throws DescopeException if there is an error or token is not valid + */ + UserResponse me(String refreshToken) throws DescopeException; + + /** + * Use to retrieve current session user history. The request requires a valid refresh token. + * + * @param refreshToken a valid refresh token + * @return {@link UserHistoryResponse} returns the user authentication history. + * @throws DescopeException if there is an error or token is not valid + */ + List history(String refreshToken) throws DescopeException; } diff --git a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java index 21935fd8..670d2b22 100644 --- a/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/AuthenticationServiceImpl.java @@ -3,8 +3,10 @@ import static com.descope.literals.AppConstants.PERMISSIONS_CLAIM_KEY; import static com.descope.literals.AppConstants.ROLES_CLAIM_KEY; import static com.descope.literals.Routes.AuthEndPoints.EXCHANGE_ACCESS_KEY_LINK; +import static com.descope.literals.Routes.AuthEndPoints.HISTORY_LINK; import static com.descope.literals.Routes.AuthEndPoints.LOG_OUT_ALL_LINK; import static com.descope.literals.Routes.AuthEndPoints.LOG_OUT_LINK; +import static com.descope.literals.Routes.AuthEndPoints.ME_LINK; import com.descope.exception.DescopeException; import com.descope.exception.ServerCommonException; @@ -13,7 +15,10 @@ import com.descope.model.client.Client; import com.descope.model.jwt.Token; import com.descope.model.jwt.response.JWTResponse; +import com.descope.model.user.response.UserHistoryResponse; +import com.descope.model.user.response.UserResponse; import com.descope.proxy.ApiProxy; +import com.fasterxml.jackson.core.type.TypeReference; import java.net.URI; import java.util.ArrayList; import java.util.Collection; @@ -21,7 +26,6 @@ import java.util.List; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.util.Strings; class AuthenticationServiceImpl extends AuthenticationsBase { @@ -39,8 +43,8 @@ public Token validateSessionWithToken(String sessionToken) throws DescopeExcepti @Override public Token refreshSessionWithToken(String refreshToken) throws DescopeException { - if (Strings.isEmpty(refreshToken)) { - throw ServerCommonException.missingArguments("Request doesn't contain refresh token"); + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.missingArguments("refresh token"); } return refreshSession(refreshToken); @@ -159,8 +163,8 @@ public List getPermissions(Token token) throws DescopeException { @Override public void logout(String refreshToken) throws DescopeException { - if (Strings.isEmpty(refreshToken)) { - throw ServerCommonException.missingArguments("Request doesn't contain refresh token"); + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.missingArguments("refresh token"); } ApiProxy apiProxy = getApiProxy(refreshToken); URI logOutURL = composeLogOutLinkURL(); @@ -169,14 +173,34 @@ public void logout(String refreshToken) throws DescopeException { @Override public void logoutAll(String refreshToken) throws DescopeException { - if (Strings.isEmpty(refreshToken)) { - throw ServerCommonException.missingArguments("Request doesn't contain refresh token"); + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.missingArguments("refresh token"); } ApiProxy apiProxy = getApiProxy(refreshToken); URI logOutAllURL = composeLogOutAllLinkURL(); apiProxy.post(logOutAllURL, null, JWTResponse.class); } + @Override + public UserResponse me(String refreshToken) throws DescopeException { + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.missingArguments("refresh token"); + } + validateJWT(refreshToken); // Will make sure token is still valid + ApiProxy apiProxy = getApiProxy(refreshToken); + return apiProxy.get(getUri(ME_LINK), UserResponse.class); + } + + @Override + public List history(String refreshToken) throws DescopeException { + if (StringUtils.isBlank(refreshToken)) { + throw ServerCommonException.missingArguments("refresh token"); + } + validateJWT(refreshToken); // Will make sure token is still valid + ApiProxy apiProxy = getApiProxy(refreshToken); + return apiProxy.getArray(getUri(HISTORY_LINK), new TypeReference>(){}); + } + AuthenticationInfo exchangeToken(String code, URI url) { if (StringUtils.isBlank(code)) { throw ServerCommonException.invalidArgument("Code"); diff --git a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java index 2e5272ce..2d2b8220 100644 --- a/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java +++ b/src/main/java/com/descope/sdk/auth/impl/EnchantedLinkServiceImpl.java @@ -8,7 +8,6 @@ import static com.descope.literals.Routes.AuthEndPoints.UPDATE_EMAIL_ENCHANTED_LINK; import static com.descope.literals.Routes.AuthEndPoints.VERIFY_ENCHANTED_LINK; import static com.descope.utils.PatternUtils.EMAIL_PATTERN; -import static org.apache.logging.log4j.util.Strings.isEmpty; import com.descope.exception.DescopeException; import com.descope.exception.ServerCommonException; @@ -66,7 +65,7 @@ public EnchantedLinkResponse signUp(String loginId, String uri, User user) URI enchantedLinkSignUpURL = composeEnchantedLinkSignUpURL(); SignUpRequest.SignUpRequestBuilder signUpRequestBuilder = SignUpRequest.builder().loginId(loginId).uri(uri).user(user).email(loginId); - if (isEmpty(user.getEmail())) { + if (StringUtils.isBlank(user.getEmail())) { user.setEmail(loginId); } SignUpRequest signUpRequest = signUpRequestBuilder.user(user).build(); diff --git a/src/main/java/com/descope/sdk/mgmt/AccessKeyService.java b/src/main/java/com/descope/sdk/mgmt/AccessKeyService.java index d7c4c34a..dcab6884 100644 --- a/src/main/java/com/descope/sdk/mgmt/AccessKeyService.java +++ b/src/main/java/com/descope/sdk/mgmt/AccessKeyService.java @@ -8,10 +8,12 @@ public interface AccessKeyService { - AccessKeyResponse create( - String name, int expireTime, List roleNames, List keyTenants) + AccessKeyResponse create(String name, int expireTime, List roleNames, List keyTenants) throws DescopeException; + AccessKeyResponse create(String name, int expireTime, List roleNames, List keyTenants, + String userId) throws DescopeException; + AccessKeyResponse load(String id) throws DescopeException; AccessKeyResponseList searchAll(List tenantIDs) throws DescopeException; diff --git a/src/main/java/com/descope/sdk/mgmt/PasswordSettingsService.java b/src/main/java/com/descope/sdk/mgmt/PasswordSettingsService.java new file mode 100644 index 00000000..d34b31b9 --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/PasswordSettingsService.java @@ -0,0 +1,41 @@ +package com.descope.sdk.mgmt; + +import com.descope.exception.DescopeException; +import com.descope.model.passwordsettings.PasswordSettings; + +/** Provides functions for managing password policies for both project and tenants. */ +public interface PasswordSettingsService { + /** + * Get the project password settings. + * + * @return {@link PasswordSettings} + * @throws DescopeException in case of errors + */ + PasswordSettings getSettings() throws DescopeException; + + /** + * Get the tenant password settings. + * + * @param tenantId Tenant ID + * @return {@link PasswordSettings} + * @throws DescopeException in case of errors + */ + PasswordSettings getSettings(String tenantId) throws DescopeException; + + /** + * Configure the project settings. + * + * @param settings The settings to set for the tenant + * @throws DescopeException in case of errors + */ + void configureSettings(PasswordSettings settings) throws DescopeException; + + /** + * Configure the tenant settings. + * + * @param tenantId Tenant ID + * @param settings The settings to set for the tenant + * @throws DescopeException in case of errors + */ + void configureSettings(String tenantId, PasswordSettings settings) throws DescopeException; +} diff --git a/src/main/java/com/descope/sdk/mgmt/TenantService.java b/src/main/java/com/descope/sdk/mgmt/TenantService.java index 1fa3bc3a..66a51435 100644 --- a/src/main/java/com/descope/sdk/mgmt/TenantService.java +++ b/src/main/java/com/descope/sdk/mgmt/TenantService.java @@ -2,6 +2,7 @@ import com.descope.exception.DescopeException; import com.descope.model.tenant.Tenant; +import com.descope.model.tenant.TenantSettings; import com.descope.model.tenant.request.TenantSearchRequest; import java.util.List; import java.util.Map; @@ -97,6 +98,14 @@ void update(String id, String name, List selfProvisioningDomains, Map selfProvisioningDomains, Map searchAll(TenantSearchRequest request) throws DescopeException; + /** + * Get the tenant settings. + * + * @param id Tenant ID + * @return {@link TenantSettings} + * @throws DescopeException in case of errors + */ + TenantSettings getSettings(String id) throws DescopeException; + + /** + * Configure the tenant settings. + * + * @param id Tenant ID + * @param settings The settings to set for the tenant + * @throws DescopeException in case of errors + */ + void configureSettings(String id, TenantSettings settings) throws DescopeException; } diff --git a/src/main/java/com/descope/sdk/mgmt/UserService.java b/src/main/java/com/descope/sdk/mgmt/UserService.java index 85507932..5888341c 100644 --- a/src/main/java/com/descope/sdk/mgmt/UserService.java +++ b/src/main/java/com/descope/sdk/mgmt/UserService.java @@ -11,6 +11,7 @@ import com.descope.model.user.response.MagicLinkTestUserResponse; import com.descope.model.user.response.OTPTestUserResponse; import com.descope.model.user.response.ProviderTokenResponse; +import com.descope.model.user.response.UserHistoryResponse; import com.descope.model.user.response.UserResponseDetails; import com.descope.model.user.response.UsersBatchResponse; import java.util.List; @@ -472,4 +473,13 @@ EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser(String loginId, S */ String generateEmbeddedLink(String loginId, Map customClaims) throws DescopeException; + + /** + * Use to retrieve users' authentication history, by the given user's ids. + * + * @param userIds List of user IDs to retrieve the history for + * @return {{@link List} of {@link UserHistoryResponse}} of all requested users login history + * @throws DescopeException If there occurs any exception, a subtype of this exception will be + */ + List history(List userIds) throws DescopeException; } diff --git a/src/main/java/com/descope/sdk/mgmt/impl/AccessKeyServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/AccessKeyServiceImpl.java index 6d90c8fd..31161b7b 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/AccessKeyServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/AccessKeyServiceImpl.java @@ -36,7 +36,22 @@ public AccessKeyResponse create( if (StringUtils.isBlank(name)) { throw ServerCommonException.invalidArgument("Name"); } - AccessKeyRequest body = createAccessKeyBody(name, expireTime, roleNames, keyTenants); + AccessKeyRequest body = createAccessKeyBody(name, expireTime, roleNames, keyTenants, null); + ApiProxy apiProxy = getApiProxy(); + return apiProxy.post(getUri(MANAGEMENT_ACCESS_KEY_CREATE_LINK), body, AccessKeyResponse.class); + } + + @Override + public AccessKeyResponse create( + String name, int expireTime, List roleNames, List keyTenants, String userId) + throws DescopeException { + if (StringUtils.isBlank(name)) { + throw ServerCommonException.invalidArgument("Name"); + } + if (StringUtils.isBlank(userId)) { + throw ServerCommonException.invalidArgument("user id"); + } + AccessKeyRequest body = createAccessKeyBody(name, expireTime, roleNames, keyTenants, userId); ApiProxy apiProxy = getApiProxy(); return apiProxy.post(getUri(MANAGEMENT_ACCESS_KEY_CREATE_LINK), body, AccessKeyResponse.class); } @@ -109,12 +124,13 @@ public void delete(String id) throws DescopeException { } private AccessKeyRequest createAccessKeyBody( - String name, int expireTime, List roleNames, List keyTenants) { + String name, int expireTime, List roleNames, List keyTenants, String userId) { return AccessKeyRequest.builder() .name(name) .expireTime(expireTime) .roleNames(roleNames) .keyTenants(MgmtUtils.createAssociatedTenantList(keyTenants)) + .userId(userId) .build(); } } diff --git a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java index e42b8cff..cdab40d8 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/ManagementServiceBuilder.java @@ -20,6 +20,7 @@ public static ManagementServices buildServices(Client client) { .auditService(new AuditServiceImpl(client)) .authzService(new AuthzServiceImpl(client)) .projectService(new ProjectServiceImpl(client)) + .passwordSettingsService(new PasswordSettingsServiceImpl(client)) .build(); } } diff --git a/src/main/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImpl.java new file mode 100644 index 00000000..de28aae7 --- /dev/null +++ b/src/main/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImpl.java @@ -0,0 +1,82 @@ +package com.descope.sdk.mgmt.impl; + +import static com.descope.literals.Routes.ManagementEndPoints.MANAGEMENT_PASSWORD_SETTINGS; +import static com.descope.utils.CollectionUtils.addIfNotBlank; +import static com.descope.utils.CollectionUtils.addIfNotNull; +import static com.descope.utils.CollectionUtils.mapOf; + +import com.descope.exception.DescopeException; +import com.descope.exception.ServerCommonException; +import com.descope.model.client.Client; +import com.descope.model.passwordsettings.PasswordSettings; +import com.descope.proxy.ApiProxy; +import com.descope.sdk.mgmt.PasswordSettingsService; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import org.apache.commons.lang3.StringUtils; + +@SuppressWarnings("checkstyle:OverloadMethodsDeclarationOrder") +class PasswordSettingsServiceImpl extends ManagementsBase implements PasswordSettingsService { + + PasswordSettingsServiceImpl(Client client) { + super(client); + } + + @Override + public PasswordSettings getSettings() throws DescopeException { + ApiProxy apiProxy = getApiProxy(); + return apiProxy.get(managementPasswordSettingsUri(null), PasswordSettings.class); + } + + @Override + public PasswordSettings getSettings(String tenantId) throws DescopeException { + if (StringUtils.isBlank(tenantId)) { + throw ServerCommonException.invalidArgument("tenantId"); + } + ApiProxy apiProxy = getApiProxy(); + return apiProxy.get(managementPasswordSettingsUri(tenantId), PasswordSettings.class); + } + + @Override + public void configureSettings(PasswordSettings settings) throws DescopeException { + configureSettings(null, settings, true); + } + + @Override + public void configureSettings(String id, PasswordSettings settings) throws DescopeException { + configureSettings(id, settings, false); + } + + private void configureSettings(String id, PasswordSettings settings, boolean ignoreEmptyId) throws DescopeException { + if (!ignoreEmptyId && StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + if (settings == null) { + throw ServerCommonException.invalidArgument("settings"); + } + Map req = new HashMap<>(); + addIfNotBlank(req, "tenantId", id); + addIfNotNull(req, "enabled", settings.getEnabled()); + addIfNotNull(req, "minLength", settings.getMinLength()); + addIfNotNull(req, "lowercase", settings.getLowercase()); + addIfNotNull(req, "uppercase", settings.getUppercase()); + addIfNotNull(req, "number", settings.getNumber()); + addIfNotNull(req, "nonAlphanumeric", settings.getNonAlphanumeric()); + addIfNotNull(req, "expiration", settings.getExpiration()); + addIfNotNull(req, "expirationWeeks", settings.getExpirationWeeks()); + addIfNotNull(req, "reuse", settings.getReuse()); + addIfNotNull(req, "reuseAmount", settings.getReuseAmount()); + addIfNotNull(req, "lock", settings.getLock()); + addIfNotNull(req, "lockAttempts", settings.getLockAttempts()); + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(managementPasswordSettingsUri(null), req, Void.class); + } + + private URI managementPasswordSettingsUri(String id) { + if (StringUtils.isBlank(id)) { + return getUri(MANAGEMENT_PASSWORD_SETTINGS); + } + return getQueryParamUri(MANAGEMENT_PASSWORD_SETTINGS, mapOf("tenantId", id)); + } +} diff --git a/src/main/java/com/descope/sdk/mgmt/impl/TenantServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/TenantServiceImpl.java index ba799b24..e204d9be 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/TenantServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/TenantServiceImpl.java @@ -2,15 +2,19 @@ import static com.descope.literals.Routes.ManagementEndPoints.CREATE_TENANT_LINK; import static com.descope.literals.Routes.ManagementEndPoints.DELETE_TENANT_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.GET_TENANT_SETTINGS_LINK; import static com.descope.literals.Routes.ManagementEndPoints.LOAD_ALL_TENANTS_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.LOAD_TENANT_LINK; import static com.descope.literals.Routes.ManagementEndPoints.TENANT_SEARCH_ALL_LINK; import static com.descope.literals.Routes.ManagementEndPoints.UPDATE_TENANT_LINK; +import static com.descope.utils.CollectionUtils.addIfNotNull; import static com.descope.utils.CollectionUtils.mapOf; import com.descope.exception.DescopeException; import com.descope.exception.ServerCommonException; import com.descope.model.client.Client; import com.descope.model.tenant.Tenant; +import com.descope.model.tenant.TenantSettings; import com.descope.model.tenant.request.TenantSearchRequest; import com.descope.model.tenant.response.GetAllTenantsResponse; import com.descope.proxy.ApiProxy; @@ -32,8 +36,7 @@ public String create(String name, List selfProvisioningDomains) throws D if (StringUtils.isBlank(name)) { throw ServerCommonException.invalidArgument("name"); } - Tenant tenant = new Tenant("", name, selfProvisioningDomains, null); - return create(tenant); + return create(Tenant.builder().name(name).selfProvisioningDomains(selfProvisioningDomains).build()); } @Override @@ -42,8 +45,11 @@ public String create(String name, List selfProvisioningDomains, Map selfProvisioningDo if (StringUtils.isAnyBlank(id, name)) { throw ServerCommonException.invalidArgument("id or name"); } - - Tenant tenant = new Tenant(id, name, selfProvisioningDomains, null); - create(tenant); + create(Tenant.builder().id(id).name(name).selfProvisioningDomains(selfProvisioningDomains).build()); } @Override @@ -64,9 +68,12 @@ public void createWithId(String id, String name, List selfProvisioningDo if (StringUtils.isAnyBlank(id, name)) { throw ServerCommonException.invalidArgument("id or name"); } - - Tenant tenant = new Tenant(id, name, selfProvisioningDomains, customAttributes); - create(tenant); + create(Tenant.builder() + .id(id) + .name(name) + .selfProvisioningDomains(selfProvisioningDomains) + .customAttributes(customAttributes) + .build()); } private String create(Tenant tenant) { @@ -82,9 +89,12 @@ public void update(String id, String name, List selfProvisioningDomains, if (StringUtils.isAnyBlank(id, name)) { throw ServerCommonException.invalidArgument("id or name"); } - - Tenant tenant = new Tenant(id, name, selfProvisioningDomains, customAttributes); - update(tenant); + update(Tenant.builder() + .id(id) + .name(name) + .selfProvisioningDomains(selfProvisioningDomains) + .customAttributes(customAttributes) + .build()); } private void update(Tenant tenant) { @@ -104,6 +114,15 @@ public void delete(String id) throws DescopeException { apiProxy.post(deleteTenantUri, mapOf("id", id), Void.class); } + @Override + public Tenant load(String id) throws DescopeException { + if (StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + ApiProxy apiProxy = getApiProxy(); + return apiProxy.get(loadTenantUri(id), Tenant.class); + } + @Override public List loadAll() throws DescopeException { URI loadAllTenantsUri = loadAllTenantsUri(); @@ -125,6 +144,38 @@ public List searchAll(TenantSearchRequest request) return response.getTenants(); } + @Override + public TenantSettings getSettings(String id) throws DescopeException { + if (StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + ApiProxy apiProxy = getApiProxy(); + return apiProxy.get(getSettingsUri(id), TenantSettings.class); + } + + @Override + public void configureSettings(String id, TenantSettings settings) throws DescopeException { + if (StringUtils.isBlank(id)) { + throw ServerCommonException.invalidArgument("id"); + } + if (settings == null) { + throw ServerCommonException.invalidArgument("settings"); + } + Map req = mapOf("tenantId", id); + addIfNotNull(req, "selfProvisioningDomains", settings.getSelfProvisioningDomains()); + addIfNotNull(req, "enabled", settings.getSessionSettingsEnabled()); + addIfNotNull(req, "sessionTokenExpiration", settings.getSessionTokenExpiration()); + addIfNotNull(req, "refreshTokenExpiration", settings.getRefreshTokenExpiration()); + addIfNotNull(req, "sessionTokenExpirationUnit", settings.getSessionTokenExpirationUnit()); + addIfNotNull(req, "refreshTokenExpirationUnit", settings.getRefreshTokenExpirationUnit()); + addIfNotNull(req, "inactivityTime", settings.getInactivityTime()); + addIfNotNull(req, "inactivityTimeUnit", settings.getInactivityTimeUnit()); + addIfNotNull(req, "enableInactivity", settings.getEnableInactivity()); + addIfNotNull(req, "domains", settings.getDomains()); + ApiProxy apiProxy = getApiProxy(); + apiProxy.post(configureSettingsUri(), req, Void.class); + } + private URI composeCreateTenantUri() { return getUri(CREATE_TENANT_LINK); } @@ -137,6 +188,10 @@ private URI composeDeleteTenantUri() { return getUri(DELETE_TENANT_LINK); } + private URI loadTenantUri(String id) { + return getQueryParamUri(LOAD_TENANT_LINK, mapOf("id", id)); + } + private URI loadAllTenantsUri() { return getUri(LOAD_ALL_TENANTS_LINK); } @@ -144,4 +199,12 @@ private URI loadAllTenantsUri() { private URI composeSearchAllUri() { return getUri(TENANT_SEARCH_ALL_LINK); } + + private URI getSettingsUri(String id) { + return getQueryParamUri(GET_TENANT_SETTINGS_LINK, mapOf("id", id)); + } + + private URI configureSettingsUri() { + return getUri(GET_TENANT_SETTINGS_LINK); + } } diff --git a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java index b1490036..58df910b 100644 --- a/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java +++ b/src/main/java/com/descope/sdk/mgmt/impl/UserServiceImpl.java @@ -19,6 +19,7 @@ import static com.descope.literals.Routes.ManagementEndPoints.USER_ADD_TENANT_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_CREATE_EMBEDDED_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_EXPIRE_PASSWORD_LINK; +import static com.descope.literals.Routes.ManagementEndPoints.USER_HISTORY_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_REMOVE_ROLES_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_REMOVE_TENANT_LINK; import static com.descope.literals.Routes.ManagementEndPoints.USER_SEARCH_ALL_LINK; @@ -49,13 +50,16 @@ import com.descope.model.user.response.MagicLinkTestUserResponse; import com.descope.model.user.response.OTPTestUserResponse; import com.descope.model.user.response.ProviderTokenResponse; +import com.descope.model.user.response.UserHistoryResponse; import com.descope.model.user.response.UserResponseDetails; import com.descope.model.user.response.UsersBatchResponse; import com.descope.proxy.ApiProxy; import com.descope.sdk.mgmt.UserService; +import com.fasterxml.jackson.core.type.TypeReference; import java.net.URI; import java.util.List; import java.util.Map; +import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; class UserServiceImpl extends ManagementsBase implements UserService { @@ -508,6 +512,16 @@ public EnchantedLinkTestUserResponse generateEnchantedLinkForTestUser( return apiProxy.post(enchantedLinkForTestUserUri, request, EnchantedLinkTestUserResponse.class); } + @Override + public List history(List userIds) throws DescopeException { + if (CollectionUtils.isEmpty(userIds)) { + throw ServerCommonException.invalidArgument("User IDs"); + } + ApiProxy apiProxy = getApiProxy(); + return apiProxy.postAndGetArray(getUri(USER_HISTORY_LINK), userIds, + new TypeReference>() {}); + } + public String generateEmbeddedLink( String loginId, Map customClaims) throws DescopeException { if (StringUtils.isBlank(loginId)) { diff --git a/src/main/java/com/descope/utils/JwtUtils.java b/src/main/java/com/descope/utils/JwtUtils.java index 905334de..e1a3ce9d 100644 --- a/src/main/java/com/descope/utils/JwtUtils.java +++ b/src/main/java/com/descope/utils/JwtUtils.java @@ -1,5 +1,6 @@ package com.descope.utils; +import com.descope.exception.ClientFunctionalException; import com.descope.exception.ServerCommonException; import com.descope.model.client.Client; import com.descope.model.jwt.Token; @@ -47,8 +48,12 @@ public Key resolveSigningKey(JwsHeader header, Claims claims) { return k; } }).setAllowedClockSkewSeconds(SKEW_SECONDS).build(); - Jws claimsJws = jwtParser.parseClaimsJws(jwt); - return claimsJws; + try { + Jws claimsJws = jwtParser.parseClaimsJws(jwt); + return claimsJws; + } catch (Exception e) { + throw ClientFunctionalException.invalidToken(e); + } } public static boolean isJWTRequired(LoginOptions loginOptions) { diff --git a/src/test/java/com/descope/sdk/auth/impl/AuthenticationServiceImplTest.java b/src/test/java/com/descope/sdk/auth/impl/AuthenticationServiceImplTest.java index 16b92ba6..c7eadddb 100644 --- a/src/test/java/com/descope/sdk/auth/impl/AuthenticationServiceImplTest.java +++ b/src/test/java/com/descope/sdk/auth/impl/AuthenticationServiceImplTest.java @@ -16,6 +16,7 @@ import com.descope.model.mgmt.ManagementServices; import com.descope.model.user.request.UserRequest; import com.descope.model.user.response.OTPTestUserResponse; +import com.descope.model.user.response.UserResponse; import com.descope.sdk.TestUtils; import com.descope.sdk.auth.AuthenticationService; import com.descope.sdk.auth.OTPService; @@ -61,7 +62,7 @@ void testPermissionsAndRoles() { } @Test - void textGetMatchedPermissionsAndRoles() { + void testGetMatchedPermissionsAndRoles() { assertEquals(Arrays.asList("tp1", "tp2"), authenticationService.getMatchedPermissions(MOCK_TOKEN, "someTenant", Arrays.asList("tp1", "tp2"))); assertEquals(Arrays.asList(), @@ -105,7 +106,7 @@ void testFunctionalPermissions() { } @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) - void testFunctionalFullCycle() { + void testFunctionalFullCycle() throws Exception { String loginId = TestUtils.getRandomName("u-") + "@descope.com"; userService.createTestUser(loginId, UserRequest.builder().email(loginId).verifiedEmail(true).build()); OTPTestUserResponse code = userService.generateOtpForTestUser(loginId, DeliveryMethod.EMAIL); @@ -116,6 +117,8 @@ void testFunctionalFullCycle() { assertThat(token.getJwt()).isNotBlank(); token = authenticationService.refreshSessionWithToken(authInfo.getRefreshToken().getJwt()); assertThat(token.getJwt()).isNotBlank(); + UserResponse u = authenticationService.me(authInfo.getRefreshToken().getJwt()); + assertThat(u.getEmail()).isEqualTo(loginId); authenticationService.logout(authInfo.getRefreshToken().getJwt()); userService.delete(loginId); } diff --git a/src/test/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImplTest.java new file mode 100644 index 00000000..90d813aa --- /dev/null +++ b/src/test/java/com/descope/sdk/mgmt/impl/PasswordSettingsServiceImplTest.java @@ -0,0 +1,123 @@ +package com.descope.sdk.mgmt.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import com.descope.exception.RateLimitExceededException; +import com.descope.exception.ServerCommonException; +import com.descope.model.client.Client; +import com.descope.model.mgmt.ManagementServices; +import com.descope.model.passwordsettings.PasswordSettings; +import com.descope.proxy.ApiProxy; +import com.descope.proxy.impl.ApiProxyBuilder; +import com.descope.sdk.TestUtils; +import com.descope.sdk.mgmt.PasswordSettingsService; +import com.descope.sdk.mgmt.TenantService; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.RetryingTest; +import org.mockito.MockedStatic; + +public class PasswordSettingsServiceImplTest { + + private final PasswordSettings mockSettings = PasswordSettings.builder() + .enabled(true) + .expiration(true) + .expirationWeeks(2) + .lock(true) + .lockAttempts(3) + .lowercase(true) + .minLength(10) + .nonAlphanumeric(true) + .number(true) + .reuse(true) + .reuseAmount(10) + .uppercase(true) + .build(); + private PasswordSettingsService passwordSettingsService; + private TenantService tenantService; + + @BeforeEach + void setUp() { + Client client = TestUtils.getClient(); + ManagementServices mgmt = ManagementServiceBuilder.buildServices(client); + this.passwordSettingsService = mgmt.getPasswordSettingsService(); + this.tenantService = mgmt.getTenantService(); + } + + @Test + void testGetSettingsForEmptyId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> passwordSettingsService.getSettings("")); + assertNotNull(thrown); + assertEquals("The tenantId argument is invalid", thrown.getMessage()); + } + + @Test + void testGetSettingsProjectForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(mockSettings).when(apiProxy).get(any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + PasswordSettings response = passwordSettingsService.getSettings(); + assertThat(response).isEqualTo(mockSettings); + } + } + + @Test + void testGetSettingsTenantForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(mockSettings).when(apiProxy).get(any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + PasswordSettings response = passwordSettingsService.getSettings("a"); + assertThat(response).isEqualTo(mockSettings); + } + } + + @Test + void testConfigureSettingsForEmptyId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> passwordSettingsService.configureSettings("", null)); + assertNotNull(thrown); + assertEquals("The id argument is invalid", thrown.getMessage()); + } + + @Test + void testConfigureSettingsForNoSettings() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> passwordSettingsService.configureSettings("a", null)); + assertNotNull(thrown); + assertEquals("The settings argument is invalid", thrown.getMessage()); + } + + @Test + void testConfigureSettingsForNoSettingsProject() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> passwordSettingsService.configureSettings(null)); + assertNotNull(thrown); + assertEquals("The settings argument is invalid", thrown.getMessage()); + } + + @RetryingTest(value = 3, suspendForMs = 30000, onExceptions = RateLimitExceededException.class) + void testFunctionalFullCycle() { + String name = TestUtils.getRandomName("t-"); + String tenantId = tenantService.create(name, Arrays.asList(name + ".com", name + "1.com")); + assertThat(tenantId).isNotBlank(); + passwordSettingsService.configureSettings(tenantId, mockSettings); + PasswordSettings ps = passwordSettingsService.getSettings(tenantId); + assertThat(ps).isEqualTo(mockSettings); + tenantService.delete(tenantId); + ps = passwordSettingsService.getSettings(); + assertThat(ps).isNotNull(); + } +} diff --git a/src/test/java/com/descope/sdk/mgmt/impl/TenantServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/TenantServiceImplTest.java index 66b1e4cf..68748000 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/TenantServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/TenantServiceImplTest.java @@ -16,6 +16,7 @@ import com.descope.exception.ServerCommonException; import com.descope.model.client.Client; import com.descope.model.tenant.Tenant; +import com.descope.model.tenant.TenantSettings; import com.descope.model.tenant.request.TenantSearchRequest; import com.descope.model.tenant.response.GetAllTenantsResponse; import com.descope.proxy.ApiProxy; @@ -38,6 +39,18 @@ public class TenantServiceImplTest { .selfProvisioningDomains(selfProvisioningDomains) .build(); + TenantSettings mockSettings = TenantSettings.builder() + .sessionSettingsEnabled(true) + .domains(Arrays.asList("d1", "d2")) + .enableInactivity(true) + .inactivityTime(3) + .inactivityTimeUnit("days") + .refreshTokenExpiration(30) + .refreshTokenExpirationUnit("days") + .selfProvisioningDomains(Arrays.asList("dd1", "dd2")) + .sessionTokenExpiration(5) + .sessionTokenExpirationUnit("minutes") + .build(); private TenantService tenantService; @BeforeEach @@ -127,6 +140,60 @@ void testDeleteForSuccess() { } } + @Test + void testLoadForEmptyId() { + ServerCommonException thrown = assertThrows(ServerCommonException.class, () -> tenantService.load("")); + assertNotNull(thrown); + assertEquals("The id argument is invalid", thrown.getMessage()); + } + + @Test + void testLoadForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(mockTenant).when(apiProxy).get(any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + Tenant response = tenantService.load(mockTenant.getId()); + assertThat(response).isEqualTo(mockTenant); + } + } + + @Test + void testGetSettingsForEmptyId() { + ServerCommonException thrown = assertThrows(ServerCommonException.class, () -> tenantService.getSettings("")); + assertNotNull(thrown); + assertEquals("The id argument is invalid", thrown.getMessage()); + } + + @Test + void testGetSettingsForSuccess() { + ApiProxy apiProxy = mock(ApiProxy.class); + doReturn(mockSettings).when(apiProxy).get(any(), any()); + try (MockedStatic mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) { + mockedApiProxyBuilder.when( + () -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy); + TenantSettings response = tenantService.getSettings(mockTenant.getId()); + assertThat(response).isEqualTo(mockSettings); + } + } + + @Test + void testConfigureSettingsForEmptyId() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> tenantService.configureSettings("", null)); + assertNotNull(thrown); + assertEquals("The id argument is invalid", thrown.getMessage()); + } + + @Test + void testConfigureSettingsForNoSettings() { + ServerCommonException thrown = + assertThrows(ServerCommonException.class, () -> tenantService.configureSettings("a", null)); + assertNotNull(thrown); + assertEquals("The settings argument is invalid", thrown.getMessage()); + } + @Test void testLoadAllForSuccess() { GetAllTenantsResponse mockTenantsResponse = @@ -157,6 +224,12 @@ void testFunctionalFullCycle() { } } assertTrue(found); + Tenant tenant = tenantService.load(tenantId); + assertThat(tenant).isNotNull(); + assertThat(tenant.getId()).isEqualTo(tenantId); + TenantSettings tenantSettings = tenantService.getSettings(tenantId); + assertThat(tenantSettings).isNotNull(); + assertThat(tenantSettings.getSelfProvisioningDomains()).containsOnly(name + ".com", name + "1.com"); tenantService.update(tenantId, name + "1", Arrays.asList(name + ".com"), null); tenants = tenantService.loadAll(); assertThat(tenants).isNotEmpty(); diff --git a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java index ef8b4405..0e114243 100644 --- a/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java +++ b/src/test/java/com/descope/sdk/mgmt/impl/UserServiceImplTest.java @@ -35,6 +35,7 @@ import com.descope.model.user.response.MagicLinkTestUserResponse; import com.descope.model.user.response.OTPTestUserResponse; import com.descope.model.user.response.ProviderTokenResponse; +import com.descope.model.user.response.UserHistoryResponse; import com.descope.model.user.response.UserResponse; import com.descope.model.user.response.UserResponseDetails; import com.descope.model.user.response.UsersBatchResponse; @@ -950,6 +951,10 @@ void testFunctionalGenerateEmbeddedLink() { } } + List history = userService.history(Arrays.asList(user.getUserId())); + assertThat(history).isNotEmpty(); + history = authenticationService.history(authInfo.getRefreshToken().getJwt()); + assertThat(history).isNotEmpty(); // now logout and see that we logged out successfully userService.logoutUser(loginId); boolean gotExc = false;