Skip to content

Commit e16aa6f

Browse files
authored
Merge pull request #26 from UnityFoundation-io/user-crud
Add endpoints to accomodate crud actions for users
2 parents e7740e5 + da606f0 commit e16aa6f

File tree

13 files changed

+690
-60
lines changed

13 files changed

+690
-60
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
package io.unityfoundation.auth;
22

3-
import io.micronaut.core.annotation.Introspected;
43
import io.micronaut.core.annotation.Nullable;
54
import io.micronaut.http.HttpResponse;
65
import io.micronaut.http.HttpStatus;
7-
import io.micronaut.http.annotation.Body;
8-
import io.micronaut.http.annotation.Controller;
9-
import io.micronaut.http.annotation.Post;
6+
import io.micronaut.http.annotation.*;
107
import io.micronaut.http.exceptions.HttpStatusException;
118
import io.micronaut.security.annotation.Secured;
129
import io.micronaut.security.authentication.Authentication;
1310
import io.micronaut.security.rules.SecurityRule;
1411
import io.micronaut.serde.annotation.Serdeable;
15-
import io.unityfoundation.auth.entities.Permission.PermissionScope;
16-
import io.unityfoundation.auth.entities.Service;
12+
import io.unityfoundation.auth.entities.*;
1713
import io.unityfoundation.auth.entities.Service.ServiceStatus;
18-
import io.unityfoundation.auth.entities.ServiceRepo;
19-
import io.unityfoundation.auth.entities.Tenant;
20-
import io.unityfoundation.auth.entities.Tenant.TenantStatus;
21-
import io.unityfoundation.auth.entities.TenantRepo;
22-
import io.unityfoundation.auth.entities.User;
23-
import io.unityfoundation.auth.entities.UserRepo;
2414
import jakarta.validation.constraints.NotNull;
2515
import java.util.List;
2616
import java.util.Optional;
27-
import java.util.function.BiPredicate;
2817

2918
@Secured(SecurityRule.IS_AUTHENTICATED)
3019
@Controller("/api")
@@ -33,11 +22,15 @@ public class AuthController {
3322
private final UserRepo userRepo;
3423
private final ServiceRepo serviceRepo;
3524
private final TenantRepo tenantRepo;
25+
private final RoleRepo roleRepo;
26+
private final PermissionsService permissionsService;
3627

37-
public AuthController(UserRepo userRepo, ServiceRepo serviceRepo, TenantRepo tenantRepo) {
28+
public AuthController(UserRepo userRepo, ServiceRepo serviceRepo, TenantRepo tenantRepo, RoleRepo roleRepo, PermissionsService permissionsService) {
3829
this.userRepo = userRepo;
3930
this.serviceRepo = serviceRepo;
4031
this.tenantRepo = tenantRepo;
32+
this.roleRepo = roleRepo;
33+
this.permissionsService = permissionsService;
4134
}
4235

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

52-
if (!tenant.getStatus().equals(TenantStatus.ENABLED)){
45+
if (!tenant.getStatus().equals(Tenant.TenantStatus.ENABLED)){
5346
return new UserPermissionsResponse.Failure("The tenant is not enabled.");
5447
}
5548

5649
User user = userRepo.findByEmail(authentication.getName()).orElse(null);
5750
if (checkUserStatus(user)) {
58-
return new UserPermissionsResponse.Failure("The users account has been disabled.");
51+
return new UserPermissionsResponse.Failure("The user's account has been disabled.");
5952
}
6053

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

77-
return new UserPermissionsResponse.Success(getPermissionsFor(user, tenant));
70+
return new UserPermissionsResponse.Success(permissionsService.getPermissionsFor(user, tenant));
7871
}
7972

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

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

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

106+
@Get("/roles")
107+
public HttpResponse<List<RoleDTO>> getRoles(Authentication authentication) {
108+
109+
User user = userRepo.findByEmail(authentication.getName()).orElse(null);
110+
if (checkUserStatus(user)) {
111+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
112+
}
113+
114+
List<String> commonPermissions = permissionsService.checkUserPermissionsAcrossAllTenants(
115+
user, List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
116+
if (commonPermissions.isEmpty()) {
117+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
118+
}
119+
120+
return HttpResponse.ok(roleRepo.findAll().stream()
121+
.map(role -> new RoleDTO(role.getId(), role.getName(), role.getDescription()))
122+
.toList());
123+
}
124+
125+
@Get("/tenants")
126+
public HttpResponse<List<TenantDTO>> getTenants(Authentication authentication) {
127+
128+
String authenticatedUserEmail = authentication.getName();
129+
User user = userRepo.findByEmail(authenticatedUserEmail).orElse(null);
130+
if (checkUserStatus(user)) {
131+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
132+
}
133+
134+
List<String> commonPermissions = permissionsService.checkUserPermissionsAcrossAllTenants(
135+
user, List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
136+
if (commonPermissions.isEmpty()) {
137+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
138+
}
139+
140+
List<Tenant> tenants = userRepo.existsByEmailAndRoleEqualsUnityAdmin(authenticatedUserEmail) ?
141+
tenantRepo.findAll() : tenantRepo.findAllByUserEmail(authenticatedUserEmail);
142+
143+
return HttpResponse.ok(tenants.stream()
144+
.map(tenant -> new TenantDTO(tenant.getId(), tenant.getName()))
145+
.toList());
146+
}
147+
148+
@Get("/tenants/{id}/users")
149+
public HttpResponse<List<UserResponse>> getTenantUsers(@PathVariable Long id, Authentication authentication) {
150+
151+
// reject if the declared tenant does not exist
152+
Optional<Tenant> tenantOptional = tenantRepo.findById(id);
153+
if (tenantOptional.isEmpty()) {
154+
throw new HttpStatusException(HttpStatus.NOT_FOUND, "Tenant not found.");
155+
}
156+
157+
User user = userRepo.findByEmail(authentication.getName()).orElse(null);
158+
if (checkUserStatus(user)) {
159+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user is disabled.");
160+
}
161+
162+
List<String> commonPermissions = permissionsService.checkUserPermission(user, tenantOptional.get(),
163+
List.of("AUTH_SERVICE_VIEW-SYSTEM", "AUTH_SERVICE_VIEW-TENANT"));
164+
if (commonPermissions.isEmpty()) {
165+
throw new HttpStatusException(HttpStatus.FORBIDDEN, "The user does not have permission!");
166+
}
167+
168+
// todo: it would be nice to capture the roles and have them automatically mapped to UserResponse.roles
169+
List<UserResponse> tenantUsers = userRepo.findAllByTenantId(id).stream().map(tenantUser ->
170+
new UserResponse(tenantUser.getId(), tenantUser.getEmail(), tenantUser.getFirstName(), tenantUser.getLastName(),
171+
userRepo.getUserRolesByUserId(tenantUser.getId())
172+
)).toList();
173+
174+
return HttpResponse.ok(tenantUsers);
175+
}
176+
113177
private boolean checkUserStatus(User user) {
114178
return user == null || user.getStatus() != User.UserStatus.ENABLED;
115179
}
@@ -128,55 +192,33 @@ private String checkServiceStatus(Optional<Service> service) {
128192
return null;
129193
}
130194

131-
private final BiPredicate<TenantPermission, Tenant> isTenantOrSystemOrSubtenantScopeAndBelongsToTenant = (tp, t) ->
132-
PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
133-
(PermissionScope.TENANT.equals(tp.permissionScope())
134-
|| PermissionScope.SUBTENANT.equals(tp.permissionScope()))
135-
&& tp.tenantId == t.getId());
136-
137-
138-
private List<String> checkUserPermission(User user, Tenant tenant, List<String> permissions) {
139-
List<String> commonPermissions = getPermissionsFor(user, tenant).stream()
140-
.filter(permissions::contains).toList();
141-
142-
return commonPermissions;
143-
}
144-
145-
private List<String> getPermissionsFor(User user, Tenant tenant) {
146-
return userRepo.getTenantPermissionsFor(user.getId()).stream()
147-
.filter(tenantPermission ->
148-
isTenantOrSystemOrSubtenantScopeAndBelongsToTenant.test(tenantPermission, tenant))
149-
.map(TenantPermission::permissionName)
150-
.toList();
151-
}
152-
153195
private HttpResponse<HasPermissionResponse> createHasPermissionResponse(boolean hasPermission,
154196
String userEmail,
155197
String message,
156198
List<String> permissions) {
157199
return HttpResponse.ok(new HasPermissionResponse(hasPermission, userEmail, message, permissions));
158200
}
159201

202+
@Serdeable
203+
public record TenantDTO(
204+
Long id,
205+
String name
206+
) {}
207+
208+
@Serdeable
209+
public record RoleDTO(
210+
Long id,
211+
String name,
212+
String description
213+
) {}
214+
160215
@Serdeable
161216
public record HasPermissionResponse(
162217
boolean hasPermission,
163218
@Nullable String userEmail,
164219
@Nullable String errorMessage,
165220
List<String> permissions
166-
) {
167-
168-
}
169-
170-
@Introspected
171-
public record TenantPermission(
172-
long tenantId,
173-
String permissionName,
174-
PermissionScope permissionScope
175-
176-
) {
177-
178-
}
179-
221+
) {}
180222

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

188230
@Serdeable
189231
public record UserPermissionsRequest(@NotNull Long tenantId,
190-
@NotNull Long serviceId) {
191-
192-
}
232+
@NotNull Long serviceId) {}
193233

194234
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.unityfoundation.auth;
2+
3+
import jakarta.validation.Constraint;
4+
import jakarta.validation.Payload;
5+
6+
import java.lang.annotation.Documented;
7+
import java.lang.annotation.ElementType;
8+
import java.lang.annotation.Retention;
9+
import java.lang.annotation.Target;
10+
11+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
12+
13+
@Target({ElementType.FIELD})
14+
@Retention(RUNTIME)
15+
@Documented
16+
@Constraint(validatedBy = NullOrNotBlankValidator.class)
17+
public @interface NullOrNotBlank {
18+
String message() default "{javax.validation.constraints.NullOrNotBlank.message}";
19+
Class<?>[] groups() default { };
20+
Class<? extends Payload>[] payload() default {};
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package io.unityfoundation.auth;
2+
3+
import jakarta.validation.ConstraintValidator;
4+
import jakarta.validation.ConstraintValidatorContext;
5+
6+
public class NullOrNotBlankValidator implements ConstraintValidator<NullOrNotBlank, String> {
7+
8+
@Override
9+
public boolean isValid(String value, ConstraintValidatorContext context) {
10+
return value == null || !value.trim().isEmpty();
11+
}
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package io.unityfoundation.auth;
2+
3+
import io.micronaut.core.annotation.Introspected;
4+
import io.unityfoundation.auth.entities.Permission;
5+
import io.unityfoundation.auth.entities.Tenant;
6+
import io.unityfoundation.auth.entities.User;
7+
import io.unityfoundation.auth.entities.UserRepo;
8+
import jakarta.inject.Singleton;
9+
10+
import java.util.List;
11+
import java.util.function.BiPredicate;
12+
import java.util.function.Predicate;
13+
14+
@Singleton
15+
public class PermissionsService {
16+
17+
private final UserRepo userRepo;
18+
19+
private final BiPredicate<TenantPermission, Tenant> isTenantOrSystemOrSubtenantScopeAndBelongsToTenant = (tp, t) ->
20+
Permission.PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
21+
(Permission.PermissionScope.TENANT.equals(tp.permissionScope())
22+
|| Permission.PermissionScope.SUBTENANT.equals(tp.permissionScope()))
23+
&& tp.tenantId == t.getId());
24+
25+
private final Predicate<TenantPermission> isTenantOrSystemOrSubtenantScope = (tp) ->
26+
Permission.PermissionScope.SYSTEM.equals(tp.permissionScope()) || (
27+
(Permission.PermissionScope.TENANT.equals(tp.permissionScope())
28+
|| Permission.PermissionScope.SUBTENANT.equals(tp.permissionScope())));
29+
30+
public PermissionsService(UserRepo userRepo) {
31+
this.userRepo = userRepo;
32+
}
33+
34+
public List<String> checkUserPermission(User user, Tenant tenant, List<String> permissions) {
35+
return getPermissionsFor(user, tenant).stream()
36+
.filter(permissions::contains).toList();
37+
}
38+
39+
public List<String> getPermissionsFor(User user, Tenant tenant) {
40+
return userRepo.getTenantPermissionsFor(user.getId()).stream()
41+
.filter(tenantPermission ->
42+
isTenantOrSystemOrSubtenantScopeAndBelongsToTenant.test(tenantPermission, tenant))
43+
.map(TenantPermission::permissionName)
44+
.toList();
45+
}
46+
47+
public List<String> checkUserPermissionsAcrossAllTenants(User user, List<String> permissions) {
48+
return userRepo.getTenantPermissionsFor(user.getId()).stream()
49+
.filter(isTenantOrSystemOrSubtenantScope)
50+
.map(TenantPermission::permissionName)
51+
.filter(permissions::contains)
52+
.toList();
53+
}
54+
55+
@Introspected
56+
public record TenantPermission(
57+
long tenantId,
58+
String permissionName,
59+
Permission.PermissionScope permissionScope
60+
) {}
61+
}

0 commit comments

Comments
 (0)