-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature(U-6932054538): Implement the /api/hasPermission endpoint
The update includes new and updated dependencies in build.gradle for authentication and permissions-related functionality. Changes to the /api/hasPermission and /login endpoints allow for handling of permissions and user login respectively. New exceptions have been added to handle validation of users, and changes to the UserRepo, User, and Tenant entity classes have been made to accommodate these changes. The schema for the `user` table has also been updated. Unit tests for these new functionalities have also been added. The expanded authentication tests now cover multiple permission-scenarios (system, tenant, subtenant) to improve robustness of the code. New entities Service and Permission have been created to refine the attribute cluster and support database operations. Minor adjustments were made to the AuthController to align with the test and entity changes. The expanded authentication tests now cover multiple permission-scenarios (system, tenant, subtenant) to improve robustness of the code. New entities Service and Permission have been created to refine the attribute cluster and support database operations. Minor adjustments were made to the AuthController to align with the test and entity changes. Added TRUNCATE TABLE commands to 'afterMigrate.sql' to clear database before insertion of new testing data. Also introduced a new insert into the 'V1__initial_schema.sql' file to support the migration tests. This will ensure a blank slate for each test and maintain database consistency across different test runs. The email addresses used in tests have been updated to a new domain. The change affects the "afterMigrate.sql" script, various methods in "UnityIamTest.java" and the "authenticate" method in "MockAuthenticationProvider.java". Add, configure and test password encoding functionality Implemented password encoding and verification using BCrypt. Added a new interface `PasswordEncoder` and two implementing classes `BCryptPasswordEncoder` and `UnityAuthenticationProvider`. Adjusted the `User` and `UserRepo` models accordingly and added necessary dependencies in `build.gradle`. The new implementation helps providing secure authentication mechanism by password validating and hashing. The MockAuthenticationProvider file was deleted as it has become obsolete. Altered the UnityAuthenticationProvider class to remove the restriction of not being activated in the test environment. Also, updated test user passwords in the afterMigrate.sql file, from a plain text format to a hashed one for increased security. Removed unwanted comment that prevents UnityAuthenticationProvider from running in test environment. Also, this commit includes the deletion of MockAuthenticationProvider which became obsolete. Further, passwords in afterMigrate.sql have been updated to hashed ones from plain text for enhanced security. Added a new test method to check disabled user status. Refactored how permissions are checked in the authentication process. Also, removed unused code related to the subtenant flag as it is no longer necessary. Updated the user-role queries in UserRepo to return a new TenantPermission object which contains the information necessary for adequate permission checking. These changes introduce a clearer, more intuitive way of managing user permissions. This commit simplifies the return statement in the UnityIamTest file. It eliminates the need for an extra variable by directly returning the result from the getAccessToken() method, making the code neater and more efficient. Updated the parameters on tenant and service entities to use the name instead of numerical IDs in the 'hasPermission' requests. Refactored tests accordingly in 'UnityIamTest.java'. Also, have added description field in the tenant structure in database schema and tests. Updated the authentication method to use executor service for blocking tasks. Updated the schema of several database tables (role, tenant_service, service, etc.) to include uniqueness constraints on the 'name' field. Additionally, modified the authentication error message when the user is not found from 'USER_NOT_FOUND' to 'CREDENTIALS_DO_NOT_MATCH' in UnityAuthenticationProvider.java.
- Loading branch information
1 parent
8327ff1
commit b3ace4a
Showing
22 changed files
with
764 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
124 changes: 124 additions & 0 deletions
124
src/main/java/io/unityfoundation/auth/AuthController.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package io.unityfoundation.auth; | ||
|
||
import io.micronaut.core.annotation.Introspected; | ||
import io.micronaut.core.annotation.Nullable; | ||
import io.micronaut.http.HttpResponse; | ||
import io.micronaut.http.annotation.Body; | ||
import io.micronaut.http.annotation.Controller; | ||
import io.micronaut.http.annotation.Post; | ||
import io.micronaut.security.annotation.Secured; | ||
import io.micronaut.security.authentication.Authentication; | ||
import io.micronaut.security.rules.SecurityRule; | ||
import io.micronaut.serde.annotation.Serdeable; | ||
import io.unityfoundation.auth.entities.Permission.PermissionScope; | ||
import io.unityfoundation.auth.entities.Service; | ||
import io.unityfoundation.auth.entities.Service.ServiceStatus; | ||
import io.unityfoundation.auth.entities.ServiceRepo; | ||
import io.unityfoundation.auth.entities.Tenant; | ||
import io.unityfoundation.auth.entities.TenantRepo; | ||
import io.unityfoundation.auth.entities.User; | ||
import io.unityfoundation.auth.entities.UserRepo; | ||
import java.util.List; | ||
import java.util.Optional; | ||
|
||
@Secured(SecurityRule.IS_AUTHENTICATED) | ||
@Controller("/api") | ||
public class AuthController { | ||
|
||
private final UserRepo userRepo; | ||
private final ServiceRepo serviceRepo; | ||
private final TenantRepo tenantRepo; | ||
|
||
public AuthController(UserRepo userRepo, ServiceRepo serviceRepo, TenantRepo tenantRepo) { | ||
this.userRepo = userRepo; | ||
this.serviceRepo = serviceRepo; | ||
this.tenantRepo = tenantRepo; | ||
} | ||
|
||
@Post("/hasPermission") | ||
public HttpResponse<HasPermissionResponse> hasPermission(@Body HasPermissionRequest requestDTO, | ||
Authentication authentication) { | ||
|
||
User user = userRepo.findByEmail(authentication.getName()).orElse(null); | ||
if (checkUserStatus(user)) { | ||
return createHasPermissionResponse(false, "The user’s account has been disabled!"); | ||
} | ||
|
||
Optional<Service> service = serviceRepo.findByName(requestDTO.serviceId()); | ||
|
||
String serviceStatusCheckResult = checkServiceStatus(service); | ||
if (serviceStatusCheckResult != null) { | ||
return createHasPermissionResponse(false, serviceStatusCheckResult); | ||
} | ||
|
||
if (!userRepo.isServiceAvailable(user.getId(), service.get().getId())) { | ||
return createHasPermissionResponse(false, | ||
"The requested service is not enabled for the requested tenant!"); | ||
} | ||
|
||
if (!checkUserPermission(user, requestDTO)) { | ||
return createHasPermissionResponse(false, "The user does not have permission!"); | ||
} | ||
|
||
return createHasPermissionResponse(true, null); | ||
} | ||
|
||
private boolean checkUserStatus(User user) { | ||
return user == null || user.getStatus() != User.UserStatus.ENABLED; | ||
} | ||
|
||
private String checkServiceStatus(Optional<Service> service) { | ||
if (service.isEmpty()) { | ||
return "The service does not exists!"; | ||
} else { | ||
ServiceStatus status = service.get().getStatus(); | ||
if (ServiceStatus.DISABLED.equals(status)) { | ||
return "The service is disabled!"; | ||
} else if (ServiceStatus.DOWN_FOR_MAINTENANCE.equals(status)) { | ||
return "The service is temporarily down for maintenance!"; | ||
} | ||
} | ||
return null; | ||
} | ||
|
||
private boolean checkUserPermission(User user, HasPermissionRequest requestDTO) { | ||
Tenant tenant = tenantRepo.findByName(requestDTO.tenantId()); | ||
List<TenantPermission> userPermissions = userRepo.getTenantPermissionsFor(user.getId()).stream() | ||
.filter(tenantPermission -> | ||
PermissionScope.SYSTEM.equals(tenantPermission.permissionScope()) || | ||
((PermissionScope.TENANT.equals(tenantPermission.permissionScope()) || PermissionScope.SUBTENANT.equals(tenantPermission.permissionScope())) | ||
&& tenantPermission.tenantId == tenant.getId())) | ||
.toList(); | ||
|
||
List<String> commonPermissions = userPermissions.stream() | ||
.map(TenantPermission::permissionName) | ||
.filter(requestDTO.permissions()::contains) | ||
.toList(); | ||
|
||
return !commonPermissions.isEmpty(); | ||
} | ||
|
||
private HttpResponse<HasPermissionResponse> createHasPermissionResponse(boolean hasPermission, | ||
String message) { | ||
return HttpResponse.ok(new HasPermissionResponse(hasPermission, message)); | ||
} | ||
|
||
@Serdeable | ||
public record HasPermissionResponse( | ||
boolean hasPermission, | ||
@Nullable String errorMessage | ||
) { | ||
|
||
} | ||
|
||
@Introspected | ||
public record TenantPermission( | ||
long tenantId, | ||
String permissionName, | ||
PermissionScope permissionScope | ||
|
||
) { | ||
|
||
} | ||
|
||
} |
32 changes: 32 additions & 0 deletions
32
src/main/java/io/unityfoundation/auth/BCryptPasswordEncoder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package io.unityfoundation.auth; | ||
|
||
import io.micronaut.core.annotation.NonNull; | ||
import jakarta.inject.Singleton; | ||
import jakarta.validation.constraints.NotBlank; | ||
import at.favre.lib.crypto.bcrypt.BCrypt; | ||
|
||
@Singleton | ||
class BCryptPasswordEncoder implements PasswordEncoder { | ||
|
||
private final BCrypt.Verifyer pwdVerifier = BCrypt.verifyer(); | ||
|
||
public String encode(@NotBlank @NonNull String rawPassword) { | ||
char[] passwordCharacters = rawPassword.toCharArray(); | ||
return hashPassword(passwordCharacters); | ||
} | ||
|
||
private String hashPassword(char[] passwordCharacters) { | ||
return BCrypt.withDefaults().hashToString(10, passwordCharacters); | ||
} | ||
|
||
@Override | ||
public boolean matches(@NotBlank @NonNull String rawPassword, | ||
@NotBlank @NonNull String encodedPassword) { | ||
BCrypt.Result verificationResult = verifyPassword(rawPassword, encodedPassword); | ||
return verificationResult.verified; | ||
} | ||
|
||
private BCrypt.Result verifyPassword(String rawPassword, String encodedPassword) { | ||
return pwdVerifier.verify(rawPassword.getBytes(), encodedPassword.getBytes()); | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
src/main/java/io/unityfoundation/auth/HasPermissionRequest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package io.unityfoundation.auth; | ||
|
||
import io.micronaut.serde.annotation.Serdeable; | ||
import jakarta.validation.constraints.NotNull; | ||
import java.util.List; | ||
|
||
@Serdeable | ||
public record HasPermissionRequest( | ||
@NotNull String tenantId, | ||
@NotNull String serviceId, | ||
List<String> permissions | ||
|
||
) {} |
18 changes: 18 additions & 0 deletions
18
src/main/java/io/unityfoundation/auth/PasswordEncoder.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package io.unityfoundation.auth; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
public interface PasswordEncoder { | ||
|
||
String encode(@NotBlank String rawPassword); | ||
|
||
/** | ||
* Checks if the provided raw password matches the encoded password. | ||
* | ||
* @param rawPassword The raw password that needs to be checked. | ||
* @param encodedPassword The encoded password to compare against. | ||
* @return {@code true} if the raw password matches the encoded password, otherwise {@code false}. | ||
*/ | ||
boolean matches(@NotBlank String rawPassword, | ||
@NotBlank String encodedPassword); | ||
} |
72 changes: 72 additions & 0 deletions
72
src/main/java/io/unityfoundation/auth/UnityAuthenticationProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package io.unityfoundation.auth; | ||
|
||
import static io.micronaut.security.authentication.AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH; | ||
|
||
import io.micronaut.core.annotation.Nullable; | ||
import io.micronaut.http.HttpRequest; | ||
import io.micronaut.security.authentication.AuthenticationException; | ||
import io.micronaut.security.authentication.AuthenticationFailed; | ||
import io.micronaut.security.authentication.AuthenticationProvider; | ||
import io.micronaut.security.authentication.AuthenticationRequest; | ||
import io.micronaut.security.authentication.AuthenticationResponse; | ||
import io.unityfoundation.auth.entities.User; | ||
import io.unityfoundation.auth.entities.UserRepo; | ||
import jakarta.inject.Singleton; | ||
import org.reactivestreams.Publisher; | ||
import reactor.core.publisher.Mono; | ||
import reactor.core.scheduler.Schedulers; | ||
|
||
@Singleton | ||
public class UnityAuthenticationProvider implements AuthenticationProvider<HttpRequest<?>> { | ||
|
||
private final UserRepo userRepo; | ||
private final PasswordEncoder passwordEncoder; | ||
|
||
public UnityAuthenticationProvider(UserRepo userRepo, | ||
PasswordEncoder passwordEncoder) { | ||
this.userRepo = userRepo; | ||
this.passwordEncoder = passwordEncoder; | ||
} | ||
|
||
/** | ||
* Authenticates the user with the given authentication request. | ||
* | ||
* @param httpRequest The HTTP request associated with the authentication. | ||
* @param authenticationRequest The authentication request containing user credentials. | ||
* @return A Publisher emitting an AuthenticationResponse upon successful authentication, or throwing | ||
* an AuthenticationException if authentication fails. | ||
*/ | ||
@Override | ||
public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest, | ||
AuthenticationRequest<?, ?> authenticationRequest) { | ||
return Mono.fromCallable(() -> findUser(authenticationRequest)) | ||
.subscribeOn(Schedulers.boundedElastic()) | ||
.flatMap(user -> { | ||
AuthenticationFailed authenticationFailed = validate(user, authenticationRequest); | ||
if (authenticationFailed != null) { | ||
return Mono.error(new AuthenticationException(authenticationFailed)); | ||
} else { | ||
return Mono.just(AuthenticationResponse.success((String) authenticationRequest.getIdentity())); | ||
} | ||
}); } | ||
|
||
private AuthenticationFailed validate(User user, | ||
AuthenticationRequest<?, ?> authenticationRequest) { | ||
AuthenticationFailed authenticationFailed = null; | ||
if (user == null) { | ||
authenticationFailed = new AuthenticationFailed(CREDENTIALS_DO_NOT_MATCH); | ||
} else if (!passwordEncoder.matches(authenticationRequest.getSecret().toString(), | ||
user.getPassword())) { | ||
authenticationFailed = new AuthenticationFailed(CREDENTIALS_DO_NOT_MATCH); | ||
} | ||
|
||
return authenticationFailed; | ||
} | ||
|
||
private User findUser(AuthenticationRequest<?, ?> authRequest) { | ||
final Object username = authRequest.getIdentity(); | ||
return userRepo.findUserForAuthentication(username.toString()).orElse(null); | ||
} | ||
|
||
} | ||
|
21 changes: 21 additions & 0 deletions
21
src/main/java/io/unityfoundation/auth/entities/Permission.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package io.unityfoundation.auth.entities; | ||
|
||
import io.micronaut.data.annotation.GeneratedValue; | ||
import io.micronaut.data.annotation.Id; | ||
import io.micronaut.data.annotation.MappedEntity; | ||
|
||
@MappedEntity() | ||
public class Permission { | ||
@Id | ||
@GeneratedValue | ||
private Long id; | ||
|
||
private String name; | ||
private String description; | ||
private PermissionScope scope; | ||
|
||
public enum PermissionScope { | ||
SYSTEM, TENANT, SUBTENANT | ||
} | ||
|
||
} |
54 changes: 54 additions & 0 deletions
54
src/main/java/io/unityfoundation/auth/entities/Service.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
package io.unityfoundation.auth.entities; | ||
|
||
import io.micronaut.data.annotation.GeneratedValue; | ||
import io.micronaut.data.annotation.Id; | ||
import io.micronaut.data.annotation.MappedEntity; | ||
|
||
@MappedEntity() | ||
public class Service { | ||
@Id | ||
@GeneratedValue | ||
private Long id; | ||
|
||
private String name; | ||
private String description; | ||
private ServiceStatus status; | ||
|
||
public enum ServiceStatus { | ||
ENABLED, DISABLED, DOWN_FOR_MAINTENANCE | ||
} | ||
|
||
public String getDescription() { | ||
return description; | ||
} | ||
|
||
public void setDescription(String description) { | ||
this.description = description; | ||
} | ||
|
||
|
||
public Long getId() { | ||
return id; | ||
} | ||
|
||
public void setId(Long id) { | ||
this.id = id; | ||
} | ||
|
||
public String getName() { | ||
return name; | ||
} | ||
|
||
public void setName(String name) { | ||
this.name = name; | ||
} | ||
|
||
public ServiceStatus getStatus() { | ||
return status; | ||
} | ||
|
||
public void setStatus(ServiceStatus status) { | ||
this.status = status; | ||
} | ||
|
||
} |
Oops, something went wrong.