diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java index 3eb8431..0f141c4 100644 --- a/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java +++ b/src/main/java/com/ecmsp/userservice/api/grpc/UserGrpcService.java @@ -1,11 +1,15 @@ package com.ecmsp.userservice.api.grpc; import com.ecmsp.user.v1.*; +import com.ecmsp.userservice.api.grpc.context.ContextAuthorization; +import com.ecmsp.userservice.api.grpc.context.UserContextData; +import com.ecmsp.userservice.api.grpc.context.UserContextGrpcHolder; import com.ecmsp.userservice.user.domain.Permission; import com.ecmsp.userservice.user.domain.RoleFacade; import com.ecmsp.userservice.user.domain.RoleToCreate; import com.ecmsp.userservice.user.domain.UserFacade; import com.ecmsp.userservice.user.domain.UserView; +import io.grpc.Context; import io.grpc.Status; import io.grpc.stub.StreamObserver; import net.devh.boot.grpc.server.service.GrpcService; @@ -26,12 +30,18 @@ public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase { private final RoleFacade roleFacade; private final UserGrpcMapper mapper; private final RoleGrpcMapper roleMapper; + private final ContextAuthorization contextAuthorization; - public UserGrpcService(UserFacade userFacade, RoleFacade roleFacade, UserGrpcMapper mapper, RoleGrpcMapper roleMapper) { + public UserGrpcService(UserFacade userFacade, + RoleFacade roleFacade, + UserGrpcMapper mapper, + RoleGrpcMapper roleMapper, + ContextAuthorization contextAuthorization) { this.userFacade = userFacade; this.roleFacade = roleFacade; this.mapper = mapper; this.roleMapper = roleMapper; + this.contextAuthorization = contextAuthorization; } @Override @@ -40,6 +50,19 @@ public void getUser(GetUserRequest request, StreamObserver resp com.ecmsp.user.v1.UserId protoUserId = com.ecmsp.user.v1.UserId.newBuilder() .setValue(request.getUserId()) .build(); + + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!request.getUserId().equals(userContextData.userId()) || !contextAuthorization.isHimselfOrHasPermission(userContextData, + request.getUserId(), + Permission.READ_USERS)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to access user with id: " + request.getUserId() + + "need to be himself or have READ_USERS permission") + .asRuntimeException()); + return; + } + com.ecmsp.userservice.user.domain.UserId userId = mapper.toDomainUserId(protoUserId); Optional userOptional = userFacade.findUserById(userId); @@ -123,6 +146,19 @@ public void createUser(CreateUserRequest request, StreamObserver responseObserver) { try { com.ecmsp.userservice.user.domain.UserId userId = mapper.toDomainUserId(request.getUser().getId()); + + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!userId.value().toString().equals(userContextData.userId()) || !contextAuthorization.isHimselfOrHasPermission(userContextData, + userId.value().toString(), + Permission.MANAGE_USERS)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to access user with id: " + userId.value() + + " need to be himself or have MANAGE_USERS permission") + .asRuntimeException()); + return; + } + String newLogin = request.getUser().getLogin(); if (newLogin == null || newLogin.isBlank()) { @@ -170,6 +206,18 @@ public void deleteUser(DeleteUserRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.READ_USERS)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to list users, READ_USERS permission required") + .asRuntimeException()); + return; + } + String filterLogin = request.getFilterLogin(); List users = userFacade.listUsers(filterLogin); @@ -214,6 +273,16 @@ public void listUsers(ListUsersRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to create role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } log.info("Creating role: {}", request.getRole().getName()); RoleToCreate roleToCreate = roleMapper.toDomainRoleToCreate(request.getRole()); @@ -253,6 +322,17 @@ public void createRole(CreateRoleRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to update role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } + String roleName = request.getRole().getName(); log.info("Updating role: {}", roleName); @@ -308,6 +388,15 @@ public void updateRole(UpdateRoleRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to delete role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } String roleName = request.getRoleId(); log.info("Deleting role: {}", roleName); @@ -334,6 +423,17 @@ public void deleteRole(DeleteRoleRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to list roles, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } + log.info("Listing all roles"); List roles = roleFacade.getAllRoles(); @@ -360,6 +460,16 @@ public void listRoles(ListRolesRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to delete role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } + String roleName = request.getRoleName(); log.info("Assigning role {} to {} users", roleName, request.getUserIdsList().size()); @@ -396,6 +506,16 @@ public void assignRoleToUsers(AssignRoleToUsersRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to delete role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } + String roleName = request.getRoleName(); log.info("Removing role {} from {} users", roleName, request.getUserIdsList().size()); @@ -432,6 +552,25 @@ public void removeRoleFromUsers(RemoveRoleFromUsersRequest request, StreamObserv @Override public void listAllPermissions(ListAllPermissionsRequest request, StreamObserver responseObserver) { try { + UserContextData userContextData = UserContextGrpcHolder.getUserContext(); + if(!contextAuthorization.hasPermission( + userContextData, + Permission.MANAGE_ROLES)) { + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to delete role, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } + + if(!contextAuthorization.hasPermission( + UserContextGrpcHolder.getUserContext(), + Permission.MANAGE_ROLES)) { + + responseObserver.onError(Status.PERMISSION_DENIED + .withDescription("Permission denied to list all permissions, MANAGE_ROLES permission required") + .asRuntimeException()); + return; + } log.info("Listing all available permissions"); List permissions = Arrays.stream(Permission.values()) diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/ContextAuthorization.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/ContextAuthorization.java new file mode 100644 index 0000000..73a4aaf --- /dev/null +++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/ContextAuthorization.java @@ -0,0 +1,15 @@ +package com.ecmsp.userservice.api.grpc.context; + +import com.ecmsp.userservice.user.domain.Permission; +import org.springframework.stereotype.Component; + +@Component +public class ContextAuthorization { + public boolean hasPermission(UserContextData userContext, Permission permission) { + return userContext.permissions().contains(permission); + } + + public boolean isHimselfOrHasPermission(UserContextData userContext, String userId, Permission permission) { + return userContext.userId().equals(userId) || hasPermission(userContext, permission); + } +} diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java index 9ad440a..cf5d1c2 100644 --- a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java +++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextData.java @@ -1,5 +1,11 @@ package com.ecmsp.userservice.api.grpc.context; + +import com.ecmsp.userservice.user.domain.Permission; + +import java.util.Set; + public record UserContextData(String userId, - String login) { + String login, + Set permissions) { } diff --git a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java index d37de61..837e95c 100644 --- a/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java +++ b/src/main/java/com/ecmsp/userservice/api/grpc/context/UserContextGrpcInterceptor.java @@ -1,8 +1,13 @@ package com.ecmsp.userservice.api.grpc.context; +import com.ecmsp.userservice.user.domain.Permission; import io.grpc.*; import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor; +import java.util.Arrays; +import java.util.Optional; +import java.util.stream.Collectors; + @GrpcGlobalServerInterceptor public class UserContextGrpcInterceptor implements ServerInterceptor { @@ -11,10 +16,15 @@ public ServerCall.Listener interceptCall( ServerCall call, Metadata headers, ServerCallHandler next) { - UserContextData userContextData = new UserContextData( headers.get(Metadata.Key.of("X-User-ID", Metadata.ASCII_STRING_MARSHALLER)), - headers.get(Metadata.Key.of("X-Login", Metadata.ASCII_STRING_MARSHALLER)) + headers.get(Metadata.Key.of("X-Login", Metadata.ASCII_STRING_MARSHALLER)), + Arrays.stream(headers.get(Metadata.Key.of("X-Permissions", Metadata.ASCII_STRING_MARSHALLER)) + .split(",")) + .filter(s -> !s.trim().isBlank()) + .map(Permission::getPermissionByName) + .filter(Optional::isPresent).map(Optional::get) + .collect(Collectors.toSet()) ); Context context = Context.current() diff --git a/src/main/java/com/ecmsp/userservice/user/domain/Permission.java b/src/main/java/com/ecmsp/userservice/user/domain/Permission.java index 3ca403e..7188f90 100644 --- a/src/main/java/com/ecmsp/userservice/user/domain/Permission.java +++ b/src/main/java/com/ecmsp/userservice/user/domain/Permission.java @@ -1,5 +1,10 @@ package com.ecmsp.userservice.user.domain; +import lombok.extern.slf4j.Slf4j; + +import java.util.Optional; + +@Slf4j public enum Permission { // Product permissions WRITE_PRODUCTS, @@ -11,6 +16,7 @@ public enum Permission { CANCEL_ORDERS, // User management permissions + READ_USERS, MANAGE_USERS, // Role management permissions @@ -18,5 +24,15 @@ public enum Permission { // Payment permissions PROCESS_PAYMENTS, - REFUND_PAYMENTS + REFUND_PAYMENTS; + + public static Optional getPermissionByName(String name) { + for (Permission permission : Permission.values()) { + if (permission.name().equalsIgnoreCase(name)) { + return Optional.of(permission); + } + } + log.error("Permission with name {} not found", name); + return Optional.empty(); + } } diff --git a/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql index 69d5913..c8b267b 100644 --- a/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql +++ b/src/main/resources/db/migration/V3__Create_roles_and_permissions.sql @@ -35,6 +35,25 @@ INSERT INTO permissions (permission_name) VALUES ('PROCESS_PAYMENTS'), ('REFUND_PAYMENTS'); +INSERT INTO roles (role_id, role_name) VALUES + (gen_random_uuid(), 'ADMIN'), + (gen_random_uuid(), 'MANAGER'), + (gen_random_uuid(), 'CUSTOMER_SUPPORT'); + +INSERT INTO role_permissions (role_id, permission_name) VALUES + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'WRITE_PRODUCTS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'DELETE_PRODUCTS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'READ_ORDERS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'WRITE_ORDERS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'CANCEL_ORDERS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'MANAGE_USERS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'MANAGE_ROLES'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'PROCESS_PAYMENTS'), + ((SELECT role_id FROM roles WHERE role_name = 'ADMIN'), 'REFUND_PAYMENTS'); + +INSERT INTO user_roles (user_id, role_id) VALUES + ((SELECT user_id FROM users WHERE login = 'andy'), (SELECT role_id FROM roles WHERE role_name = 'ADMIN')); + -- Create indexes for performance CREATE INDEX idx_role_permissions_role_id ON role_permissions(role_id); CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);