Skip to content

Commit

Permalink
Fixed permission and role checking and added some util helpers (#57)
Browse files Browse the repository at this point in the history
* Fixed permission and role checking and added some util helpers

* Return false if there is no tenant association

* Return false if there is no tenant association
  • Loading branch information
slavikm authored Jul 21, 2023
1 parent 2b05f8c commit 9c35dbe
Show file tree
Hide file tree
Showing 12 changed files with 175 additions and 20 deletions.
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<groupId>com.descope</groupId>
<artifactId>java-sdk</artifactId>
<modelVersion>4.0.0</modelVersion>
<version>1.0.2</version>
<version>1.0.3</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Java library used to integrate with Descope.</description>
<url>https://github.com/descope/descope-java</url>
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/com/descope/literals/AppConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,7 @@ public class AppConstants {
public static final String COOKIE = "Cookie";
public static final String SESSION_COOKIE_NAME = "DS";
public static final String REFRESH_COOKIE_NAME = "DSR";
public static final String TENANTS_CLAIM_KEY = "tenants";
public static final String PERMISSIONS_CLAIM_KEY = "permissions";
public static final String ROLES_CLAIM_KEY = "roles";
}
5 changes: 5 additions & 0 deletions src/main/java/com/descope/model/auth/AssociatedTenant.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
package com.descope.model.auth;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class AssociatedTenant {
private String tenantId;
private String tenantName;
private List<String> roleNames;
}
38 changes: 38 additions & 0 deletions src/main/java/com/descope/sdk/auth/AuthenticationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,44 @@ boolean validatePermissions(Token token, String tenant, List<String> permissions
*/
boolean validateRoles(Token token, String tenant, List<String> roles) throws DescopeException;

/**
* Return the list of roles granted to the validated session token in the given tenant.
*
* @param token - {@link Token Token}
* @param tenant - Tenant ID.
* @return {@link List} of {@link String} roles the user has in the tenant
* @throws DescopeException if there is an error
*/
List<String> getRoles(Token token, String tenant) throws DescopeException;

/**
* Return the list of roles granted to the validated session token.
*
* @param token - {@link Token Token}
* @return {@link List} of {@link String} roles the user has globally in the project
* @throws DescopeException if there is an error
*/
List<String> getRoles(Token token) throws DescopeException;

/**
* Return the list of permissions granted to the validated session token in the given tenant.
*
* @param token - {@link Token Token}
* @param tenant - Tenant ID.
* @return {@link List} of {@link String} permissions the user has in the tenant
* @throws DescopeException if there is an error
*/
List<String> getPermissions(Token token, String tenant) throws DescopeException;

/**
* Return the list of permissions granted to the validated session token.
*
* @param token - {@link Token Token}
* @return {@link List} of {@link String} permissions the user has globally in the project
* @throws DescopeException if there is an error
*/
List<String> getPermissions(Token token) throws DescopeException;

/**
* Used to log out of current device session.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.descope.sdk.auth.impl;

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.LOG_OUT_ALL_LINK;
import static com.descope.literals.Routes.AuthEndPoints.LOG_OUT_LINK;
Expand Down Expand Up @@ -72,8 +74,11 @@ public boolean validatePermissions(Token token, List<String> permissions)
@Override
public boolean validatePermissions(Token token, String tenant, List<String> permissions)
throws DescopeException {
List<String> authorizationClaimItems = getAuthorizationClaimItems(token, tenant, permissions);
return CollectionUtils.isEqualCollection(authorizationClaimItems, permissions);
if (StringUtils.isNotBlank(tenant) && !isTenantAssociated(token, tenant)) {
return false;
}
List<String> grantedPermissions = getPermissions(token, tenant);
return CollectionUtils.isSubCollection(permissions, grantedPermissions);
}

@Override
Expand All @@ -84,8 +89,27 @@ public boolean validateRoles(Token token, List<String> roles) throws DescopeExce
@Override
public boolean validateRoles(Token token, String tenant, List<String> roles)
throws DescopeException {
List<String> authorizationClaimItems = getAuthorizationClaimItems(token, tenant, roles);
return CollectionUtils.isEqualCollection(authorizationClaimItems, roles);
if (StringUtils.isNotBlank(tenant) && !isTenantAssociated(token, tenant)) {
return false;
}
List<String> grantedRoles = getRoles(token, tenant);
return CollectionUtils.isSubCollection(roles, grantedRoles);
}

public List<String> getRoles(Token token, String tenant) throws DescopeException {
return getAuthorizationClaimItems(token, tenant, ROLES_CLAIM_KEY);
}

public List<String> getRoles(Token token) throws DescopeException {
return getAuthorizationClaimItems(token, "", ROLES_CLAIM_KEY);
}

public List<String> getPermissions(Token token, String tenant) throws DescopeException {
return getAuthorizationClaimItems(token, tenant, PERMISSIONS_CLAIM_KEY);
}

public List<String> getPermissions(Token token) throws DescopeException {
return getAuthorizationClaimItems(token, "", PERMISSIONS_CLAIM_KEY);
}

@Override
Expand Down
41 changes: 34 additions & 7 deletions src/main/java/com/descope/sdk/auth/impl/AuthenticationsBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static com.descope.literals.AppConstants.COOKIE;
import static com.descope.literals.AppConstants.REFRESH_COOKIE_NAME;
import static com.descope.literals.AppConstants.SESSION_COOKIE_NAME;
import static com.descope.literals.AppConstants.TENANTS_CLAIM_KEY;
import static com.descope.literals.Routes.AuthEndPoints.REFRESH_TOKEN_LINK;
import static com.descope.utils.PatternUtils.EMAIL_PATTERN;
import static com.descope.utils.PatternUtils.PHONE_PATTERN;
Expand Down Expand Up @@ -32,8 +33,8 @@
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
Expand Down Expand Up @@ -199,14 +200,40 @@ AuthenticationInfo getAuthenticationInfo(JWTResponse jwtResponse) {
sessionToken, refreshToken, jwtResponse.getUser(), jwtResponse.getFirstSeen());
}

List<String> getAuthorizationClaimItems(Token token, String tenant, List<String> permissions) {
if (tenant == null || MapUtils.isEmpty(token.getClaims())) {
return Collections.emptyList();
@SuppressWarnings("unchecked")
boolean isTenantAssociated(Token token, String tenant) {
if (MapUtils.isEmpty(token.getClaims())) {
return false;
}
var claims = token.getClaims();
if (claims.get(TENANTS_CLAIM_KEY) == null) {
return false;
}
claims = (Map<String, Object>) claims.get(TENANTS_CLAIM_KEY);
if (claims.get(tenant) == null) {
return false;
}
return true;
}

return token.getClaims().keySet().stream()
.filter(permissions::contains)
.collect(Collectors.toList());
@SuppressWarnings("unchecked")
List<String> getAuthorizationClaimItems(Token token, String tenant, String root) {
if (MapUtils.isEmpty(token.getClaims())) {
return Collections.emptyList();
}
var claims = token.getClaims();
if (StringUtils.isNotBlank(tenant)) {
if (claims.get(TENANTS_CLAIM_KEY) == null) {
return Collections.emptyList();
}
claims = (Map<String, Object>) claims.get(TENANTS_CLAIM_KEY);
if (claims.get(tenant) == null) {
return Collections.emptyList();
}
claims = (Map<String, Object>) claims.get(tenant);
}
var res = (List<String>) claims.get(root);
return res == null ? Collections.emptyList() : res;
}

private URI composeRefreshTokenLinkURL() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ public UserResponseDetails addTenantRoles(String loginId, String tenantId, List<
throw ServerCommonException.invalidArgument("Login ID");
}
URI addTenantRolesUri = composeAddTenantRolesUri();
Map<String, Object> request = Map.of("loginId", loginId, "tenantId", "", "roleNames", roles);
Map<String, Object> request = Map.of("loginId", loginId, "tenantId", tenantId, "roleNames", roles);
var apiProxy = getApiProxy();
return apiProxy.post(addTenantRolesUri, request, UserResponseDetails.class);
}
Expand Down
6 changes: 5 additions & 1 deletion src/test/java/com/descope/sdk/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -62,12 +62,16 @@ public class TestUtils {
1234567890,
MOCK_USER_RESPONSE,
true);
public static final Map<String, Object> TENANTS_AUTHZ =
Map.of("permissions", List.of("tp1", "tp2"), "roles", List.of("tr1", "tr2"));
public static final Token MOCK_TOKEN =
Token.builder()
.id("1")
.projectId(PROJECT_ID)
.jwt("someJwtToken")
.claims(Map.of("someClaim", 1))
.claims(Map.of("someClaim", 1,
"tenants", Map.of("someTenant", TENANTS_AUTHZ),
"permissions", List.of("p1", "p2"), "roles", List.of("r1", "r2")))
.build();
@SuppressWarnings("checkstyle:LineLength")
public static final SigningKey MOCK_SIGNING_KEY =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
package com.descope.sdk.auth.impl;

import static com.descope.sdk.TestUtils.MOCK_TOKEN;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.descope.enums.DeliveryMethod;
import com.descope.model.jwt.Token;
import com.descope.model.user.request.UserRequest;
import com.descope.sdk.TestUtils;
import com.descope.sdk.auth.AuthenticationService;
import com.descope.sdk.auth.OTPService;
import com.descope.sdk.mgmt.RolesService;
import com.descope.sdk.mgmt.TenantService;
import com.descope.sdk.mgmt.UserService;
import com.descope.sdk.mgmt.impl.ManagementServiceBuilder;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

Expand All @@ -19,6 +25,8 @@ public class AuthenticationServiceImplTest {
private AuthenticationService authenticationService;
private UserService userService;
private OTPService otpService;
private RolesService roleService;
private TenantService tenantService;

@BeforeEach
void setUp() {
Expand All @@ -28,7 +36,50 @@ void setUp() {
this.authenticationService = authService.getAuthService();
this.otpService = authService.getOtpService();
var mgmtParams = TestUtils.getManagementParams();
this.userService = ManagementServiceBuilder.buildServices(client, mgmtParams).getUserService();
var mgmtServices = ManagementServiceBuilder.buildServices(client, mgmtParams);
this.userService = mgmtServices.getUserService();
this.roleService = mgmtServices.getRolesService();
this.tenantService = mgmtServices.getTenantService();
}

@Test
void testPermissionsAndRoles() {
assertTrue(authenticationService.validatePermissions(MOCK_TOKEN, "someTenant", List.of("tp1", "tp2")));
assertFalse(authenticationService.validatePermissions(MOCK_TOKEN, "someTenant", List.of("tp2", "tp3")));
assertTrue(authenticationService.validatePermissions(MOCK_TOKEN, List.of("p1", "p2")));
assertFalse(authenticationService.validatePermissions(MOCK_TOKEN, List.of("p2", "p3")));
assertTrue(authenticationService.validateRoles(MOCK_TOKEN, "someTenant", List.of("tr1", "tr2")));
assertFalse(authenticationService.validateRoles(MOCK_TOKEN, "someTenant", List.of("tr2", "tr3")));
assertTrue(authenticationService.validateRoles(MOCK_TOKEN, List.of("r1", "r2")));
assertFalse(authenticationService.validateRoles(MOCK_TOKEN, List.of("r2", "r3")));
}

@Test
void testFunctionalPermissions() {
String roleName = TestUtils.getRandomName("r-").substring(0, 20);
roleService.create(roleName, "ttt", null);
String tenantName = TestUtils.getRandomName("t-");
String tenantId = tenantService.create(tenantName, List.of(tenantName + ".com"));
String loginId = TestUtils.getRandomName("u-") + "@descope.com";
userService.createTestUser(loginId,
UserRequest.builder()
.email(loginId)
.verifiedEmail(true)
.roleNames(List.of(roleName))
.build());
userService.addTenant(loginId, tenantId);
userService.addTenantRoles(loginId, tenantId, List.of(roleName));
var code = userService.generateOtpForTestUser(loginId, DeliveryMethod.EMAIL);
var authInfo = otpService.verifyCode(DeliveryMethod.EMAIL, loginId, code.getCode());
assertNotNull(authInfo.getToken());
assertThat(authInfo.getToken().getJwt()).isNotBlank();
assertTrue(authenticationService.validateRoles(authInfo.getToken(), List.of(roleName)));
assertFalse(authenticationService.validateRoles(authInfo.getToken(), List.of(roleName + "x")));
assertTrue(authenticationService.validateRoles(authInfo.getToken(), tenantId, List.of(roleName)));
assertFalse(authenticationService.validateRoles(authInfo.getToken(), tenantId, List.of(roleName + "x")));
userService.delete(loginId);
tenantService.delete(tenantId);
roleService.delete(roleName);
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@

class AccessKeyServiceImplTest {
private final List<String> mockRoles = List.of("Test");
private final AssociatedTenant associatedTenant = new AssociatedTenant("test", mockRoles);
private final AssociatedTenant associatedTenant = new AssociatedTenant("test", "", mockRoles);
private final List<AssociatedTenant> mockKeyTenants = List.of(associatedTenant);
private final AccessKeyResponseDetails mockResponse =
AccessKeyResponseDetails.builder()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.descope.model.flow.FlowResponse;
import com.descope.model.flow.Screen;
import com.descope.model.flow.Theme;
import com.descope.model.flow.ThemeResponse;
import com.descope.proxy.ApiProxy;
import com.descope.proxy.impl.ApiProxyBuilder;
import com.descope.sdk.TestUtils;
Expand Down Expand Up @@ -84,8 +85,9 @@ void testImportFlowForSuccess() {
@Test
void testExportThemeForSuccess() {
var theme = mock(Theme.class);
var themeResponse = new ThemeResponse(theme);
var apiProxy = mock(ApiProxy.class);
doReturn(theme).when(apiProxy).post(any(), any(), any());
doReturn(themeResponse).when(apiProxy).post(any(), any(), any());
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
mockedApiProxyBuilder.when(
() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
Expand All @@ -105,8 +107,9 @@ void testImportThemeForEmptyTheme() {
@Test
void testImportThemeForSuccess() {
var theme = mock(Theme.class);
var themeResponse = new ThemeResponse(theme);
var apiProxy = mock(ApiProxy.class);
doReturn(theme).when(apiProxy).post(any(), any(), any());
doReturn(themeResponse).when(apiProxy).post(any(), any(), any());
try (MockedStatic<ApiProxyBuilder> mockedApiProxyBuilder = mockStatic(ApiProxyBuilder.class)) {
mockedApiProxyBuilder.when(
() -> ApiProxyBuilder.buildProxy(any(), any())).thenReturn(apiProxy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -797,7 +797,7 @@ void testFunctionalUserWithTenantAndRole() {
.verifiedPhone(true)
.displayName("Testing Test")
.invite(false)
.userTenants(List.of(new AssociatedTenant(tenantId, List.of(roleName))))
.userTenants(List.of(AssociatedTenant.builder().tenantId(tenantId).roleNames(List.of(roleName)).build()))
.build());
UserResponse user = createResponse.getUser();
assertNotNull(user);
Expand All @@ -809,7 +809,7 @@ void testFunctionalUserWithTenantAndRole() {
assertEquals("Testing Test", user.getName());
assertEquals("invited", user.getStatus());
assertThat(user.getUserTenants()).containsExactly(
new AssociatedTenant(tenantId, List.of(roleName)));
AssociatedTenant.builder().tenantId(tenantId).roleNames(List.of(roleName)).build());
// Delete
userService.delete(loginId);
tenantService.delete(tenantId);
Expand Down

0 comments on commit 9c35dbe

Please sign in to comment.