Skip to content

Commit

Permalink
Merge pull request #26 from UnityFoundation-io/user-crud
Browse files Browse the repository at this point in the history
Add endpoints to accomodate crud actions for users
  • Loading branch information
montesmoci authored May 1, 2024
2 parents e7740e5 + da606f0 commit e16aa6f
Show file tree
Hide file tree
Showing 13 changed files with 690 additions and 60 deletions.
154 changes: 97 additions & 57 deletions UnityAuth/src/main/java/io/unityfoundation/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,30 +1,19 @@
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.HttpStatus;
import io.micronaut.http.annotation.Body;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.annotation.*;
import io.micronaut.http.exceptions.HttpStatusException;
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.*;
import io.unityfoundation.auth.entities.Service.ServiceStatus;
import io.unityfoundation.auth.entities.ServiceRepo;
import io.unityfoundation.auth.entities.Tenant;
import io.unityfoundation.auth.entities.Tenant.TenantStatus;
import io.unityfoundation.auth.entities.TenantRepo;
import io.unityfoundation.auth.entities.User;
import io.unityfoundation.auth.entities.UserRepo;
import jakarta.validation.constraints.NotNull;
import java.util.List;
import java.util.Optional;
import java.util.function.BiPredicate;

@Secured(SecurityRule.IS_AUTHENTICATED)
@Controller("/api")
Expand All @@ -33,11 +22,15 @@ public class AuthController {
private final UserRepo userRepo;
private final ServiceRepo serviceRepo;
private final TenantRepo tenantRepo;
private final RoleRepo roleRepo;
private final PermissionsService permissionsService;

public AuthController(UserRepo userRepo, ServiceRepo serviceRepo, TenantRepo tenantRepo) {
public AuthController(UserRepo userRepo, ServiceRepo serviceRepo, TenantRepo tenantRepo, RoleRepo roleRepo, PermissionsService permissionsService) {
this.userRepo = userRepo;
this.serviceRepo = serviceRepo;
this.tenantRepo = tenantRepo;
this.roleRepo = roleRepo;
this.permissionsService = permissionsService;
}

@Post("/principal/permissions")
Expand All @@ -49,13 +42,13 @@ public UserPermissionsResponse permissions(@Body UserPermissionsRequest requestD
}
Tenant tenant = maybeTenant.get();

if (!tenant.getStatus().equals(TenantStatus.ENABLED)){
if (!tenant.getStatus().equals(Tenant.TenantStatus.ENABLED)){
return new UserPermissionsResponse.Failure("The tenant is not enabled.");
}

User user = userRepo.findByEmail(authentication.getName()).orElse(null);
if (checkUserStatus(user)) {
return new UserPermissionsResponse.Failure("The users account has been disabled.");
return new UserPermissionsResponse.Failure("The user's account has been disabled.");
}

Service service = serviceRepo.findById(requestDTO.serviceId())
Expand All @@ -74,7 +67,7 @@ public UserPermissionsResponse permissions(@Body UserPermissionsRequest requestD
"The Tenant and/or Service is not available for this user");
}

return new UserPermissionsResponse.Success(getPermissionsFor(user, tenant));
return new UserPermissionsResponse.Success(permissionsService.getPermissionsFor(user, tenant));
}

@Post("/hasPermission")
Expand Down Expand Up @@ -102,14 +95,85 @@ public HttpResponse<HasPermissionResponse> hasPermission(@Body HasPermissionRequ
return createHasPermissionResponse(false, user.getEmail(), "The requested service is not enabled for the requested tenant!", List.of());
}

List<String> commonPermissions = checkUserPermission(user, tenantOptional.get(), requestDTO.permissions());
List<String> commonPermissions = permissionsService.checkUserPermission(user, tenantOptional.get(), requestDTO.permissions());
if (commonPermissions.isEmpty()) {
return createHasPermissionResponse(false, user.getEmail(), "The user does not have permission!", commonPermissions);
}

return createHasPermissionResponse(true, user.getEmail(), null, commonPermissions);
}

@Get("/roles")
public HttpResponse<List<RoleDTO>> getRoles(Authentication authentication) {

User user = userRepo.findByEmail(authentication.getName()).orElse(null);
if (checkUserStatus(user)) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
}

List<String> commonPermissions = permissionsService.checkUserPermissionsAcrossAllTenants(
user, List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
if (commonPermissions.isEmpty()) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
}

return HttpResponse.ok(roleRepo.findAll().stream()
.map(role -> new RoleDTO(role.getId(), role.getName(), role.getDescription()))
.toList());
}

@Get("/tenants")
public HttpResponse<List<TenantDTO>> getTenants(Authentication authentication) {

String authenticatedUserEmail = authentication.getName();
User user = userRepo.findByEmail(authenticatedUserEmail).orElse(null);
if (checkUserStatus(user)) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
}

List<String> commonPermissions = permissionsService.checkUserPermissionsAcrossAllTenants(
user, List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
if (commonPermissions.isEmpty()) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
}

List<Tenant> tenants = userRepo.existsByEmailAndRoleEqualsUnityAdmin(authenticatedUserEmail) ?
tenantRepo.findAll() : tenantRepo.findAllByUserEmail(authenticatedUserEmail);

return HttpResponse.ok(tenants.stream()
.map(tenant -> new TenantDTO(tenant.getId(), tenant.getName()))
.toList());
}

@Get("/tenants/{id}/users")
public HttpResponse<List<UserResponse>> getTenantUsers(@PathVariable Long id, Authentication authentication) {

// reject if the declared tenant does not exist
Optional<Tenant> tenantOptional = tenantRepo.findById(id);
if (tenantOptional.isEmpty()) {
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Tenant not found.");
}

User user = userRepo.findByEmail(authentication.getName()).orElse(null);
if (checkUserStatus(user)) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
}

List<String> commonPermissions = permissionsService.checkUserPermission(user, tenantOptional.get(),
List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
if (commonPermissions.isEmpty()) {
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
}

// todo: it would be nice to capture the roles and have them automatically mapped to UserResponse.roles
List<UserResponse> tenantUsers = userRepo.findAllByTenantId(id).stream().map(tenantUser ->
new UserResponse(tenantUser.getId(), tenantUser.getEmail(), tenantUser.getFirstName(), tenantUser.getLastName(),
userRepo.getUserRolesByUserId(tenantUser.getId())
)).toList();

return HttpResponse.ok(tenantUsers);
}

private boolean checkUserStatus(User user) {
return user == null || user.getStatus() != User.UserStatus.ENABLED;
}
Expand All @@ -128,55 +192,33 @@ private String checkServiceStatus(Optional<Service> service) {
return null;
}

private final BiPredicate<TenantPermission, Tenant> isTenantOrSystemOrSubtenantScopeAndBelongsToTenant = (tp, t) ->
PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
(PermissionScope.TENANT.equals(tp.permissionScope())
|| PermissionScope.SUBTENANT.equals(tp.permissionScope()))
&& tp.tenantId == t.getId());


private List<String> checkUserPermission(User user, Tenant tenant, List<String> permissions) {
List<String> commonPermissions = getPermissionsFor(user, tenant).stream()
.filter(permissions::contains).toList();

return commonPermissions;
}

private List<String> getPermissionsFor(User user, Tenant tenant) {
return userRepo.getTenantPermissionsFor(user.getId()).stream()
.filter(tenantPermission ->
isTenantOrSystemOrSubtenantScopeAndBelongsToTenant.test(tenantPermission, tenant))
.map(TenantPermission::permissionName)
.toList();
}

private HttpResponse<HasPermissionResponse> createHasPermissionResponse(boolean hasPermission,
String userEmail,
String message,
List<String> permissions) {
return HttpResponse.ok(new HasPermissionResponse(hasPermission, userEmail, message, permissions));
}

@Serdeable
public record TenantDTO(
Long id,
String name
) {}

@Serdeable
public record RoleDTO(
Long id,
String name,
String description
) {}

@Serdeable
public record HasPermissionResponse(
boolean hasPermission,
@Nullable String userEmail,
@Nullable String errorMessage,
List<String> permissions
) {

}

@Introspected
public record TenantPermission(
long tenantId,
String permissionName,
PermissionScope permissionScope

) {

}

) {}

public sealed interface UserPermissionsResponse {
@Serdeable
Expand All @@ -187,8 +229,6 @@ record Failure(String errorMessage) implements UserPermissionsResponse {}

@Serdeable
public record UserPermissionsRequest(@NotNull Long tenantId,
@NotNull Long serviceId) {

}
@NotNull Long serviceId) {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.unityfoundation.auth;

import jakarta.validation.Constraint;
import jakarta.validation.Payload;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ElementType.FIELD})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = NullOrNotBlankValidator.class)
public @interface NullOrNotBlank {
String message() default "{javax.validation.constraints.NullOrNotBlank.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.unityfoundation.auth;

import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class NullOrNotBlankValidator implements ConstraintValidator<NullOrNotBlank, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || !value.trim().isEmpty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package io.unityfoundation.auth;

import io.micronaut.core.annotation.Introspected;
import io.unityfoundation.auth.entities.Permission;
import io.unityfoundation.auth.entities.Tenant;
import io.unityfoundation.auth.entities.User;
import io.unityfoundation.auth.entities.UserRepo;
import jakarta.inject.Singleton;

import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Predicate;

@Singleton
public class PermissionsService {

private final UserRepo userRepo;

private final BiPredicate<TenantPermission, Tenant> isTenantOrSystemOrSubtenantScopeAndBelongsToTenant = (tp, t) ->
Permission.PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
(Permission.PermissionScope.TENANT.equals(tp.permissionScope())
|| Permission.PermissionScope.SUBTENANT.equals(tp.permissionScope()))
&& tp.tenantId == t.getId());

private final Predicate<TenantPermission> isTenantOrSystemOrSubtenantScope = (tp) ->
Permission.PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
(Permission.PermissionScope.TENANT.equals(tp.permissionScope())
|| Permission.PermissionScope.SUBTENANT.equals(tp.permissionScope())));

public PermissionsService(UserRepo userRepo) {
this.userRepo = userRepo;
}

public List<String> checkUserPermission(User user, Tenant tenant, List<String> permissions) {
return getPermissionsFor(user, tenant).stream()
.filter(permissions::contains).toList();
}

public List<String> getPermissionsFor(User user, Tenant tenant) {
return userRepo.getTenantPermissionsFor(user.getId()).stream()
.filter(tenantPermission ->
isTenantOrSystemOrSubtenantScopeAndBelongsToTenant.test(tenantPermission, tenant))
.map(TenantPermission::permissionName)
.toList();
}

public List<String> checkUserPermissionsAcrossAllTenants(User user, List<String> permissions) {
return userRepo.getTenantPermissionsFor(user.getId()).stream()
.filter(isTenantOrSystemOrSubtenantScope)
.map(TenantPermission::permissionName)
.filter(permissions::contains)
.toList();
}

@Introspected
public record TenantPermission(
long tenantId,
String permissionName,
Permission.PermissionScope permissionScope
) {}
}
Loading

0 comments on commit e16aa6f

Please sign in to comment.