From 52d1b56d171606631545f40c587e92ea367900b8 Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Wed, 21 Jan 2026 22:58:33 +0000 Subject: [PATCH 1/9] add permission and role mapping --- .../configuration/TokenAuthFilter.java | 32 +-- .../domain/auth/MemberTypeRoleMapper.java | 127 +++++++++ .../wcc/platform/domain/auth/Permission.java | 29 ++ .../wcc/platform/domain/auth/UserAccount.java | 155 ++++++++++- .../domain/exceptions/ForbiddenException.java | 8 + .../domain/platform/type/RoleType.java | 68 +++-- .../com/wcc/platform/service/AuthService.java | 247 ++++++++++++++++++ ...V23__20260120__update_user_auth_tables.sql | 10 + .../domain/auth/MemberTypeRoleMapperTest.java | 109 ++++++++ 9 files changed, 754 insertions(+), 31 deletions(-) create mode 100644 src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java create mode 100644 src/main/java/com/wcc/platform/domain/auth/Permission.java create mode 100644 src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java create mode 100644 src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql create mode 100644 src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java diff --git a/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java b/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java index 9d0b92a6..532564c9 100644 --- a/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java +++ b/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java @@ -1,12 +1,15 @@ package com.wcc.platform.configuration; +import com.wcc.platform.domain.auth.UserAccount; +import com.wcc.platform.domain.auth.UserAccount.User; import com.wcc.platform.service.AuthService; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; -import java.util.Objects; +import java.util.List; +import java.util.Optional; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; @@ -61,21 +64,20 @@ protected void doFilterInternal( final String authHeader = request.getHeader(AUTHORIZATION); if (StringUtils.hasText(authHeader) && authHeader.startsWith(BEARER)) { final String token = authHeader.substring(AUTH_TOKEN_START); - authService - .authenticateByToken(token) - .ifPresent( - user -> { - final var authorities = - user.getRoles().stream() - .filter(Objects::nonNull) - .map(role -> new SimpleGrantedAuthority(role.name())) - .toList(); + final Optional userOpt = authService.authenticateByTokenWithMember(token); + if (userOpt.isPresent()) { + final UserAccount.User user = userOpt.get(); - SecurityContextHolder.getContext() - .setAuthentication( - new UsernamePasswordAuthenticationToken( - user.getEmail(), null, authorities)); - }); + final var authorities = List.of(new SimpleGrantedAuthority(user.getPrimaryRole().name())); + final UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken( + user, // Principal is now UserAccount.User + null, + authorities // You can add authorities here if needed + ); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } } filterChain.doFilter(request, response); diff --git a/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java new file mode 100644 index 00000000..7cf95f05 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java @@ -0,0 +1,127 @@ +package com.wcc.platform.domain.auth; + +import com.wcc.platform.domain.platform.type.MemberType; +import com.wcc.platform.domain.platform.type.RoleType; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class MemberTypeRoleMapper { + + private static final Map MEMBER_TYPE_TO_ROLE_MAP = + Map.of( + MemberType.DIRECTOR, RoleType.SUPER_ADMIN, + MemberType.LEADER, RoleType.ADMIN, + MemberType.MENTOR, RoleType.MENTOR, + MemberType.MENTEE, RoleType.MENTEE, + MemberType.COLLABORATOR, RoleType.CONTRIBUTOR, + MemberType.EVANGELIST, RoleType.CONTRIBUTOR, + MemberType.SPEAKER, RoleType.CONTRIBUTOR, + MemberType.VOLUNTEER, RoleType.VIEWER, + MemberType.MEMBER, RoleType.VIEWER, + MemberType.PARTNER, RoleType.VIEWER); + + // Role hierarchy: higher number = more privileged + private static final Map ROLE_HIERARCHY = + Map.of( + RoleType.SUPER_ADMIN, 100, + RoleType.ADMIN, 80, + RoleType.MENTOR, 60, + RoleType.CONTRIBUTOR, 50, + RoleType.MENTEE, 40, + RoleType.VIEWER, 20); + + private MemberTypeRoleMapper() { + // Utility class + } + + /** Get role for a single MemberType. */ + public static RoleType getRoleForMemberType(MemberType memberType) { + if (memberType == null) { + throw new IllegalArgumentException("MemberType cannot be null"); + } + return MEMBER_TYPE_TO_ROLE_MAP.getOrDefault(memberType, RoleType.VIEWER); + } + + /** + * Get all roles for a list of MemberTypes. + * + * @param memberTypes list of member types + * @return set of all roles corresponding to the member types + */ + public static Set getRolesForMemberTypes(List memberTypes) { + if (memberTypes == null || memberTypes.isEmpty()) { + return Set.of(RoleType.VIEWER); + } + + return memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .collect(Collectors.toSet()); + } + + /** + * Get the highest privilege role from a list of MemberTypes. This determines the "primary" role + * when a member has multiple types. + * + * @param memberTypes list of member types + * @return the role with highest privilege level + */ + public static RoleType getHighestRole(List memberTypes) { + if (memberTypes == null || memberTypes.isEmpty()) { + return RoleType.VIEWER; + } + + return memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .max(Comparator.comparing(role -> ROLE_HIERARCHY.getOrDefault(role, 0))) + .orElse(RoleType.VIEWER); + } + + /** + * Get all permissions from multiple MemberTypes (union of all permissions). + * + * @param memberTypes list of member types + * @return set of all unique permissions + */ + public static Set getAllPermissionsForMemberTypes(List memberTypes) { + if (memberTypes == null || memberTypes.isEmpty()) { + return RoleType.VIEWER.getPermissions(); + } + + return memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .flatMap(role -> role.getPermissions().stream()) + .collect(Collectors.toSet()); + } + + /** Check if any of the member types maps to SUPER_ADMIN. */ + public static boolean isSuperAdmin(List memberTypes) { + if (memberTypes == null) { + return false; + } + return memberTypes.stream() + .anyMatch(type -> getRoleForMemberType(type) == RoleType.SUPER_ADMIN); + } + + /** Check if any of the member types maps to ADMIN or SUPER_ADMIN. */ + public static boolean isAdmin(List memberTypes) { + if (memberTypes == null) { + return false; + } + return memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .anyMatch(role -> role == RoleType.SUPER_ADMIN || role == RoleType.ADMIN); + } + + /** Check if member has a specific role through any of their member types. */ + public static boolean hasRole(List memberTypes, RoleType targetRole) { + if (memberTypes == null || targetRole == null) { + return false; + } + return memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .anyMatch(role -> role == targetRole); + } +} diff --git a/src/main/java/com/wcc/platform/domain/auth/Permission.java b/src/main/java/com/wcc/platform/domain/auth/Permission.java new file mode 100644 index 00000000..e7035468 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/auth/Permission.java @@ -0,0 +1,29 @@ +package com.wcc.platform.domain.auth; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum Permission { + // User Management + USER_READ("user:read", "View user information"), + USER_WRITE("user:write", "Create and update users"), + USER_DELETE("user:delete", "Delete users"), + + // Mentor Operations + MENTOR_APPLICATION_READ("mentor:application:read", "View mentor applications"), + MENTOR_APPLICATION_WRITE("mentor:application:write", "Accept/decline mentee applications"), + MENTOR_PROFILE_UPDATE("mentor:profile:update", "Update mentor profile"), + + // Mentee Operations + MENTEE_APPLICATION_SUBMIT("mentee:application:submit", "Submit mentee applications"), + MENTEE_APPLICATION_READ("mentee:application:read", "View own application status"), + + // Admin Operations + MENTOR_APPROVE("admin:mentor:approve", "Approve/reject mentors"), + MENTEE_APPROVE("admin:mentee:approve", "Approve/reject mentees"), + CYCLE_EMAIL_SEND("admin:cycle:email", "Send cycle emails"), + MATCH_MANAGE("admin:match:manage", "Manage mentor-mentee matches"); + + private final String code; + private final String description; +} diff --git a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java index be77e878..d68de228 100644 --- a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java +++ b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java @@ -2,7 +2,11 @@ import com.wcc.platform.domain.platform.member.Member; import com.wcc.platform.domain.platform.type.RoleType; +import java.util.Arrays; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -21,6 +25,57 @@ public class UserAccount { private List roles; private boolean enabled; + /** Get all permissions from all assigned roles in UserAccount. */ + public Set getPermissions() { + if (roles == null || roles.isEmpty()) { + return Set.of(); + } + + return roles.stream() + .flatMap(role -> role.getPermissions().stream()) + .collect(Collectors.toSet()); + } + + /* */ + /** Check if user has a specific permission in assigned roles. */ + /* + public boolean hasPermission(Permission permission) { + return getPermissions().contains(permission); + } + + */ + /** Check if user has any of the specified permissions. */ + /* + public boolean hasAnyPermission(Permission... permissions) { + Set userPermissions = getPermissions(); + return Arrays.stream(permissions).anyMatch(userPermissions::contains); + } + + */ + /** Check if user has all of the specified permissions. */ + /* + public boolean hasAllPermissions(Permission... permissions) { + Set userPermissions = getPermissions(); + return Arrays.stream(permissions).allMatch(userPermissions::contains); + } + + */ + /** Check if user has a specific role. */ + /* + public boolean hasRole(RoleType role) { + return roles != null && roles.contains(role); + } + + */ + /** Check if user has any of the specified roles. */ + /* + public boolean hasAnyRole(RoleType... rolesToCheck) { + if (roles == null) { + return false; + } + return Arrays.stream(rolesToCheck).anyMatch(roles::contains); + }*/ + /** * A record that encapsulates a User within the platform. * @@ -28,5 +83,103 @@ public class UserAccount { * A User consists of a user account for authentication purposes and a member entity containing * personal and community-related details. */ - public record User(UserAccount userAccount, Member member) {} + public record User(UserAccount userAccount, Member member) { + + /** + * Get the primary role based on member types. Returns the highest privilege role when member + * has multiple types. + * + *

For example, if a member is both MENTEE and MENTOR, their primary role will be MENTOR + * (since MENTOR has higher privileges). + */ + public RoleType getPrimaryRole() { + if (member == null || member.getMemberTypes() == null || member.getMemberTypes().isEmpty()) { + return RoleType.VIEWER; + } + return MemberTypeRoleMapper.getHighestRole(member.getMemberTypes()); + } + + /** + * Get all roles derived from member types. A member can have multiple roles if they have + * multiple member types. + * + *

For example, a member who is both MENTOR and COLLABORATOR will have both MENTOR_ROLE and + * CONTRIBUTOR roles. + */ + public Set getAllMemberRoles() { + if (member == null || member.getMemberTypes() == null) { + return Set.of(RoleType.VIEWER); + } + return MemberTypeRoleMapper.getRolesForMemberTypes(member.getMemberTypes()); + } + + /** + * Get all roles including both: 1. Roles derived from member types 2. Roles explicitly assigned + * in UserAccount + */ + public Set getAllRoles() { + Set allRoles = new HashSet<>(getAllMemberRoles()); + if (userAccount.getRoles() != null) { + allRoles.addAll(userAccount.getRoles()); + } + return allRoles; + } + + /** + * Get all permissions including those from: 1. All member types (union of permissions) 2. + * Explicitly assigned roles in UserAccount + */ + public Set getAllPermissions() { + Set permissions = new HashSet<>(); + + // Add permissions from member types + if (member != null && member.getMemberTypes() != null) { + permissions.addAll( + MemberTypeRoleMapper.getAllPermissionsForMemberTypes(member.getMemberTypes())); + } + + // Add permissions from explicitly assigned roles + permissions.addAll(userAccount.getPermissions()); + + return permissions; + } + + /** + * Check if user has a specific permission. Checks permissions from both member types and + * assigned roles. + */ + public boolean hasPermission(Permission permission) { + return getAllPermissions().contains(permission); + } + + /** + * Check if user has a specific role. Checks both member-type-derived roles and explicitly + * assigned roles. + */ + public boolean hasRole(RoleType role) { + return getAllRoles().contains(role); + } + + /** Check if user has any of the specified roles. */ + public boolean hasAnyRole(RoleType... roles) { + Set userRoles = getAllRoles(); + return Arrays.stream(roles).anyMatch(userRoles::contains); + } + + /** Check if user is a super admin (has DIRECTOR member type). */ + public boolean isSuperAdmin() { + if (member == null || member.getMemberTypes() == null) { + return false; + } + return MemberTypeRoleMapper.isSuperAdmin(member.getMemberTypes()); + } + + /** Check if user is an admin (has DIRECTOR or LEADER member type). */ + public boolean isAdmin() { + if (member == null || member.getMemberTypes() == null) { + return false; + } + return MemberTypeRoleMapper.isAdmin(member.getMemberTypes()); + } + } } diff --git a/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java b/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java new file mode 100644 index 00000000..009b03aa --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java @@ -0,0 +1,8 @@ +package com.wcc.platform.domain.exceptions; + +public class ForbiddenException extends RuntimeException { + + public ForbiddenException(String message) { + super(message); + } +} diff --git a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java index 2f9c8f94..ff3f91a7 100644 --- a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java +++ b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java @@ -1,5 +1,7 @@ package com.wcc.platform.domain.platform.type; +import com.wcc.platform.domain.auth.Permission; +import java.util.Set; import lombok.AllArgsConstructor; import lombok.Getter; @@ -7,24 +9,35 @@ @Getter @AllArgsConstructor public enum RoleType { - ADMIN(1, "Platform Administrator"), - MEMBER(2, "Community Member"), + SUPER_ADMIN(1, "Platform Super Administrator", Set.of(Permission.values())), + ADMIN( + 4, + "Platform Administrator", + Set.of( + Permission.USER_READ, + Permission.MENTOR_APPROVE, + Permission.MENTEE_APPROVE, + Permission.CYCLE_EMAIL_SEND, + Permission.MATCH_MANAGE, + Permission.MENTOR_APPLICATION_READ)), + MENTEE( + 5, + "Mentee In Community", + Set.of(Permission.MENTEE_APPLICATION_SUBMIT, Permission.MENTEE_APPLICATION_READ)), + MENTOR( + 6, + "Mentor In Community", + Set.of( + Permission.MENTOR_APPLICATION_READ, + Permission.MENTOR_APPLICATION_WRITE, + Permission.MENTOR_PROFILE_UPDATE)), - MENTORSHIP_ADMIN(20, "Mentorship Administrator"), - MENTORSHIP_EDITOR(21, "Mentorship Team"), - - MAIL_ADMIN(30, "Newsletter Administrator"), - MAIL_EDITOR(31, "Newsletter Editor"), - MAIL_PUBLISHER(33, "Newsletter Publisher"), - MAIL_SUBSCRIBER(32, "Newsletter Subscriber Coordinator"), - MAIL_VIEWER(34, "Newsletter Viewer"), - - CONTENT_ADMIN(40, "Website Content Administrator"), - CONTENT_EDITOR(41, "Website Content Editor"), - CONTENT_VIEWER(42, "Website Content Viewer"); + CONTRIBUTOR(2, "Contributor In Community", Set.of(Permission.USER_READ)), + VIEWER(7, "Member In Community", Set.of(Permission.USER_READ)); private final int typeId; private final String description; + private final Set permissions; /** * Retrieves the corresponding {@code MemberType} enum value based on a given type ID. If no match @@ -40,7 +53,32 @@ public static RoleType fromId(final int typeId) { return type; } } - return MEMBER; + return VIEWER; + } + + /** Check if this role has a specific permission. */ + public boolean hasPermission(Permission permission) { + return permissions.contains(permission); + } + + /** Check if this role has any of the specified permissions. */ + public boolean hasAnyPermission(Permission... requiredPermissions) { + for (Permission permission : requiredPermissions) { + if (permissions.contains(permission)) { + return true; + } + } + return false; + } + + /** Check if this role has all of the specified permissions. */ + public boolean hasAllPermissions(Permission... requiredPermissions) { + for (Permission permission : requiredPermissions) { + if (!permissions.contains(permission)) { + return false; + } + } + return true; } @Override diff --git a/src/main/java/com/wcc/platform/service/AuthService.java b/src/main/java/com/wcc/platform/service/AuthService.java index 6878795a..1e4fc4b1 100644 --- a/src/main/java/com/wcc/platform/service/AuthService.java +++ b/src/main/java/com/wcc/platform/service/AuthService.java @@ -1,17 +1,24 @@ package com.wcc.platform.service; +import com.wcc.platform.domain.auth.Permission; import com.wcc.platform.domain.auth.UserAccount; import com.wcc.platform.domain.auth.UserToken; +import com.wcc.platform.domain.exceptions.ForbiddenException; import com.wcc.platform.domain.platform.member.Member; import com.wcc.platform.domain.platform.member.MemberDto; +import com.wcc.platform.domain.platform.type.RoleType; import com.wcc.platform.repository.MemberRepository; import com.wcc.platform.repository.UserAccountRepository; import com.wcc.platform.repository.UserTokenRepository; import java.security.SecureRandom; import java.time.OffsetDateTime; +import java.util.Arrays; import java.util.Base64; import java.util.Optional; +import java.util.Set; import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -134,4 +141,244 @@ private UserToken generateUserToken(final UserAccount user) { userTokenRepository.create(userToken); return userToken; } + + /** + * Retrieves the complete User (UserAccount + Member) for a given user account ID. This is used + * for RBAC permission checking. + * + * @param userId the ID of the user account + * @return an {@code Optional} containing the user and member if found + */ + public Optional getUserWithMember(final Integer userId) { + if (userId == null) { + return Optional.empty(); + } + + Optional userAccountOpt = userAccountRepository.findById(userId); + if (userAccountOpt.isEmpty()) { + return Optional.empty(); + } + + UserAccount userAccount = userAccountOpt.get(); + if (userAccount.getMemberId() == null) { + return Optional.empty(); + } + + Optional memberOpt = memberRepository.findById(userAccount.getMemberId()); + return memberOpt.map(member -> new UserAccount.User(userAccount, member)); + } + + /** + * Retrieves the complete User (UserAccount + Member) by email. This is used for authentication + * and RBAC. + * + * @param email the email of the user + * @return an {@code Optional} containing the user and member if found + */ + public Optional getUserWithMemberByEmail(final String email) { + Optional userAccountOpt = userAccountRepository.findByEmail(email); + if (userAccountOpt.isEmpty()) { + return Optional.empty(); + } + + UserAccount userAccount = userAccountOpt.get(); + if (userAccount.getMemberId() == null) { + return Optional.empty(); + } + + Optional memberOpt = memberRepository.findById(userAccount.getMemberId()); + return memberOpt.map(member -> new UserAccount.User(userAccount, member)); + } + + /** + * Authenticates a user based on the provided token and returns the complete User (UserAccount + + * Member). This is the preferred method for RBAC-enabled authentication. + * + * @param token the authentication token provided by the user + * @return an {@code Optional} containing the user and member if the token is + * valid and associated with an existing user, or an empty {@code Optional} if the token is + * invalid or no user account is found + */ + public Optional authenticateByTokenWithMember(final String token) { + final Optional tokenOpt = + userTokenRepository.findValidByToken(token, OffsetDateTime.now()); + if (tokenOpt.isEmpty()) { + return Optional.empty(); + } + + final UserToken retrievedToken = tokenOpt.get(); + return getUserWithMember(retrievedToken.getUserId()); + } + + /** + * Get the current authenticated user with member information from SecurityContext. + * + * @return the authenticated User (UserAccount + Member) + * @throws ForbiddenException if user is not authenticated or principal is invalid + */ + public UserAccount.User getCurrentUser() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication == null || !authentication.isAuthenticated()) { + throw new ForbiddenException("User is not authenticated"); + } + + Object principal = authentication.getPrincipal(); + + if (principal instanceof UserAccount.User) { + return (UserAccount.User) principal; + } + + throw new ForbiddenException("Invalid authentication principal"); + } + + /** + * Get current user's permissions from all member types and assigned roles. + * + * @return set of all permissions the user has + */ + public Set getCurrentUserPermissions() { + return getCurrentUser().getAllPermissions(); + } + + /** + * Get current user's primary role (highest privilege role from member types). + * + * @return the primary role of the user + */ + public RoleType getCurrentUserPrimaryRole() { + return getCurrentUser().getPrimaryRole(); + } + + /** + * Get all roles the current user has (from member types and assigned roles). + * + * @return set of all roles + */ + public Set getCurrentUserRoles() { + return getCurrentUser().getAllRoles(); + } + + /** + * Require a specific permission. + * + * @param permission the required permission + * @throws ForbiddenException if user doesn't have the permission + */ + public void requirePermission(Permission permission) { + if (!getCurrentUser().hasPermission(permission)) { + throw new ForbiddenException( + String.format("Permission denied. Required: %s", permission.name())); + } + } + + /** + * Require any of the specified permissions (OR logic). + * + * @param permissions the permissions (user needs at least one) + * @throws ForbiddenException if user doesn't have any of the permissions + */ + public void requireAnyPermission(Permission... permissions) { + UserAccount.User user = getCurrentUser(); + Set userPermissions = user.getAllPermissions(); + + boolean hasAny = Arrays.stream(permissions).anyMatch(userPermissions::contains); + + if (!hasAny) { + throw new ForbiddenException( + String.format("Permission denied. Required any of: %s", Arrays.toString(permissions))); + } + } + + /** + * Require all of the specified permissions (AND logic). + * + * @param permissions the permissions (user needs all of them) + * @throws ForbiddenException if user doesn't have all the permissions + */ + public void requireAllPermissions(Permission... permissions) { + UserAccount.User user = getCurrentUser(); + Set userPermissions = user.getAllPermissions(); + + boolean hasAll = Arrays.stream(permissions).allMatch(userPermissions::contains); + + if (!hasAll) { + throw new ForbiddenException( + String.format("Permission denied. Required all of: %s", Arrays.toString(permissions))); + } + } + + /** + * Require specific role(s). User needs at least one of the specified roles (from member types or + * assigned roles). + * + * @param allowedRoles the allowed roles (user needs one of them) + * @throws ForbiddenException if user doesn't have any of the roles + */ + public void requireRole(RoleType... allowedRoles) { + UserAccount.User user = getCurrentUser(); + + if (user.hasAnyRole(allowedRoles)) { + return; + } + + throw new ForbiddenException( + String.format( + "Role denied. User roles: %s, Required any of: %s", + user.getAllRoles(), Arrays.toString(allowedRoles))); + } + + /** + * Check if current user has permission (non-throwing). + * + * @param permission the permission to check + * @return true if user has the permission, false otherwise + */ + public boolean hasPermission(Permission permission) { + try { + return getCurrentUser().hasPermission(permission); + } catch (ForbiddenException e) { + return false; + } + } + + /** + * Check if current user has role (non-throwing). + * + * @param role the role to check + * @return true if user has the role, false otherwise + */ + public boolean hasRole(RoleType role) { + try { + return getCurrentUser().hasRole(role); + } catch (ForbiddenException e) { + return false; + } + } + + /** + * Check if current user is a super admin. + * + * @return true if user is super admin, false otherwise + */ + public boolean isSuperAdmin() { + try { + return getCurrentUser().isSuperAdmin(); + } catch (ForbiddenException e) { + return false; + } + } + + /** + * Check if current user is an admin (SUPER_ADMIN or ADMIN). + * + * @return true if user is admin, false otherwise + */ + public boolean isAdmin() { + try { + return getCurrentUser().isAdmin(); + } catch (ForbiddenException e) { + return false; + } + } } diff --git a/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql b/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql new file mode 100644 index 00000000..71f95fe3 --- /dev/null +++ b/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql @@ -0,0 +1,10 @@ +UPDATE role_types +SET name = 'CONTRIBUTOR', + description = 'Collaborator In Community' +WHERE id = 2; + +-- Insert Role Types +INSERT INTO role_types (id, name, description) +VALUES (5, 'MENTEE', 'Mentee In Community'), + (6, 'MENTOR', 'Mentor In Community'), + (7, 'MEMBER', 'Member In Community'); \ No newline at end of file diff --git a/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java b/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java new file mode 100644 index 00000000..aada0789 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java @@ -0,0 +1,109 @@ +package com.wcc.platform.domain.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.wcc.platform.domain.platform.type.MemberType; +import com.wcc.platform.domain.platform.type.RoleType; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class MemberTypeRoleMapperTest { + + @Test + void getRoleForMemberType_returnsMappedRole() { + assertEquals( + RoleType.SUPER_ADMIN, MemberTypeRoleMapper.getRoleForMemberType(MemberType.DIRECTOR)); + assertEquals(RoleType.ADMIN, MemberTypeRoleMapper.getRoleForMemberType(MemberType.LEADER)); + assertEquals(RoleType.MENTOR, MemberTypeRoleMapper.getRoleForMemberType(MemberType.MENTOR)); + assertEquals(RoleType.MENTEE, MemberTypeRoleMapper.getRoleForMemberType(MemberType.MENTEE)); + assertEquals( + RoleType.CONTRIBUTOR, MemberTypeRoleMapper.getRoleForMemberType(MemberType.COLLABORATOR)); + assertEquals(RoleType.VIEWER, MemberTypeRoleMapper.getRoleForMemberType(MemberType.MEMBER)); + } + + @Test + void getRoleForMemberType_null_throwsIllegalArgumentException() { + assertThrows( + IllegalArgumentException.class, () -> MemberTypeRoleMapper.getRoleForMemberType(null)); + } + + @Test + void getRolesForMemberTypes_nullOrEmpty_returnsViewer() { + Set fromNull = MemberTypeRoleMapper.getRolesForMemberTypes(null); + assertEquals(1, fromNull.size()); + assertTrue(fromNull.contains(RoleType.VIEWER)); + + Set fromEmpty = MemberTypeRoleMapper.getRolesForMemberTypes(List.of()); + assertEquals(1, fromEmpty.size()); + assertTrue(fromEmpty.contains(RoleType.VIEWER)); + } + + @Test + void getRolesForMemberTypes_collectsRoles() { + Set roles = + MemberTypeRoleMapper.getRolesForMemberTypes( + List.of(MemberType.LEADER, MemberType.MENTEE, MemberType.SPEAKER)); + assertTrue(roles.contains(RoleType.ADMIN)); + assertTrue(roles.contains(RoleType.MENTEE)); + assertTrue(roles.contains(RoleType.CONTRIBUTOR)); + } + + @Test + void getHighestRole_returnsMostPrivileged() { + // Director maps to SUPER_ADMIN (highest), Member maps to VIEWER (low) + RoleType highest = + MemberTypeRoleMapper.getHighestRole(List.of(MemberType.MEMBER, MemberType.DIRECTOR)); + assertEquals(RoleType.SUPER_ADMIN, highest); + + // Leader(Admin) vs Mentor -> ADMIN is higher in defined hierarchy + highest = MemberTypeRoleMapper.getHighestRole(List.of(MemberType.MENTOR, MemberType.LEADER)); + assertEquals(RoleType.ADMIN, highest); + } + + @Test + void getHighestRole_nullOrEmpty_returnsViewer() { + assertEquals(RoleType.VIEWER, MemberTypeRoleMapper.getHighestRole(null)); + assertEquals(RoleType.VIEWER, MemberTypeRoleMapper.getHighestRole(List.of())); + } + + @Test + void getAllPermissionsForMemberTypes_nullOrEmpty_returnsViewerPermissions() { + Set viewerPerms = RoleType.VIEWER.getPermissions(); + + Set fromNull = MemberTypeRoleMapper.getAllPermissionsForMemberTypes(null); + assertEquals(viewerPerms, fromNull); + + Set fromEmpty = MemberTypeRoleMapper.getAllPermissionsForMemberTypes(List.of()); + assertEquals(viewerPerms, fromEmpty); + } + + @Test + void isSuperAdmin_checksCorrectly() { + assertTrue(MemberTypeRoleMapper.isSuperAdmin(List.of(MemberType.DIRECTOR))); + assertFalse(MemberTypeRoleMapper.isSuperAdmin(List.of(MemberType.MEMBER, MemberType.MENTEE))); + assertFalse(MemberTypeRoleMapper.isSuperAdmin(null)); + } + + @Test + void isAdmin_checksCorrectly() { + assertTrue(MemberTypeRoleMapper.isAdmin(List.of(MemberType.LEADER))); + // Director (SUPER_ADMIN) should also count as admin + assertTrue(MemberTypeRoleMapper.isAdmin(List.of(MemberType.DIRECTOR))); + assertFalse(MemberTypeRoleMapper.isAdmin(List.of(MemberType.MEMBER))); + assertFalse(MemberTypeRoleMapper.isAdmin(null)); + } + + @Test + void hasRole_checksCorrectly() { + assertTrue( + MemberTypeRoleMapper.hasRole( + List.of(MemberType.LEADER, MemberType.MEMBER), RoleType.ADMIN)); + assertFalse(MemberTypeRoleMapper.hasRole(List.of(MemberType.MEMBER), RoleType.ADMIN)); + assertFalse(MemberTypeRoleMapper.hasRole(null, RoleType.ADMIN)); + assertFalse(MemberTypeRoleMapper.hasRole(List.of(MemberType.LEADER), null)); + } +} From 5869c1c199e4ee93f1ce38ea7469ad9ef865ab6f Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Wed, 21 Jan 2026 23:16:45 +0000 Subject: [PATCH 2/9] create new annotation --- .../configuration/security/LogicalOperator.java | 6 ++++++ .../security/RequiresPermission.java | 15 +++++++++++++++ .../configuration/security/RequiresRole.java | 13 +++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 src/main/java/com/wcc/platform/configuration/security/LogicalOperator.java create mode 100644 src/main/java/com/wcc/platform/configuration/security/RequiresPermission.java create mode 100644 src/main/java/com/wcc/platform/configuration/security/RequiresRole.java diff --git a/src/main/java/com/wcc/platform/configuration/security/LogicalOperator.java b/src/main/java/com/wcc/platform/configuration/security/LogicalOperator.java new file mode 100644 index 00000000..3c34e04b --- /dev/null +++ b/src/main/java/com/wcc/platform/configuration/security/LogicalOperator.java @@ -0,0 +1,6 @@ +package com.wcc.platform.configuration.security; + +public enum LogicalOperator { + AND, + OR +} diff --git a/src/main/java/com/wcc/platform/configuration/security/RequiresPermission.java b/src/main/java/com/wcc/platform/configuration/security/RequiresPermission.java new file mode 100644 index 00000000..440b61bc --- /dev/null +++ b/src/main/java/com/wcc/platform/configuration/security/RequiresPermission.java @@ -0,0 +1,15 @@ +package com.wcc.platform.configuration.security; + +import com.wcc.platform.domain.auth.Permission; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface RequiresPermission { + Permission[] value(); + + LogicalOperator operator() default LogicalOperator.AND; +} diff --git a/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java b/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java new file mode 100644 index 00000000..167f7901 --- /dev/null +++ b/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java @@ -0,0 +1,13 @@ +package com.wcc.platform.configuration.security; + +import com.wcc.platform.domain.platform.type.RoleType; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface RequiresRole { + RoleType[] value(); +} From 219a42388af3caab509866125b7ea881276accbe Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Sat, 24 Jan 2026 16:40:53 +0000 Subject: [PATCH 3/9] aop implementation --- build.gradle.kts | 1 + .../configuration/GlobalExceptionHandler.java | 12 ++++ .../security/AuthorizationAspect.java | 57 +++++++++++++++++++ .../MentorshipApplicationController.java | 8 ++- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java diff --git a/build.gradle.kts b/build.gradle.kts index 3c3b04d1..c4de61ad 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -57,6 +57,7 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-security") implementation("org.springframework.boot:spring-boot-starter-data-jdbc") + implementation("org.springframework.boot:spring-boot-starter-aop") implementation("org.bouncycastle:bcprov-jdk18on:1.78.1") implementation("org.postgresql:postgresql:42.7.2") implementation("org.flywaydb:flyway-core") diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index cab14059..15ba0e8b 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ import com.wcc.platform.domain.exceptions.DuplicatedMemberException; import com.wcc.platform.domain.exceptions.EmailSendException; import com.wcc.platform.domain.exceptions.ErrorDetails; +import com.wcc.platform.domain.exceptions.ForbiddenException; import com.wcc.platform.domain.exceptions.InvalidProgramTypeException; import com.wcc.platform.domain.exceptions.MemberNotFoundException; import com.wcc.platform.domain.exceptions.MenteeNotSavedException; @@ -129,4 +130,15 @@ public ResponseEntity handleMethodArgumentNotValidException( HttpStatus.BAD_REQUEST.value(), errorMessage, request.getDescription(false)); return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); } + + /** Return 403 Forbidden for ForbiddenException. */ + @ExceptionHandler(ForbiddenException.class) + public ResponseEntity handleForbiddenException( + ForbiddenException ex, final WebRequest request) { + var errorResponse = + new ErrorDetails( + HttpStatus.FORBIDDEN.value(), ex.getMessage(), request.getDescription(false)); + + return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN); + } } diff --git a/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java new file mode 100644 index 00000000..84789fe8 --- /dev/null +++ b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java @@ -0,0 +1,57 @@ +package com.wcc.platform.configuration.security; + +import com.wcc.platform.domain.auth.Permission; +import com.wcc.platform.domain.platform.type.RoleType; +import com.wcc.platform.service.AuthService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Order(1) +public class AuthorizationAspect { + + private final AuthService authService; + + public AuthorizationAspect(final AuthService authService) { + this.authService = authService; + } + + @Around("@annotation(requiresPermission)") + public Object checkPermission( + ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { + + Permission[] permissions = requiresPermission.value(); + + if (permissions.length == 0) { + throw new IllegalArgumentException( + "@RequiresPermission must specify at least one permission"); + } + + if (requiresPermission.operator() == LogicalOperator.AND) { + authService.requireAllPermissions(permissions); + } else { + authService.requireAnyPermission(permissions); + } + + return joinPoint.proceed(); + } + + @Around("@annotation(requiresRole)") + public Object checkRole(ProceedingJoinPoint joinPoint, RequiresRole requiresRole) + throws Throwable { + + RoleType[] roles = requiresRole.value(); + + if (roles.length == 0) { + throw new IllegalArgumentException("@RequiresRole must specify at least one role"); + } + + authService.requireRole(roles); + + return joinPoint.proceed(); + } +} diff --git a/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java b/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java index a20e2c49..20e80979 100644 --- a/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java +++ b/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java @@ -1,5 +1,8 @@ package com.wcc.platform.controller; +import com.wcc.platform.configuration.security.LogicalOperator; +import com.wcc.platform.configuration.security.RequiresPermission; +import com.wcc.platform.domain.auth.Permission; import com.wcc.platform.domain.platform.mentorship.ApplicationAcceptRequest; import com.wcc.platform.domain.platform.mentorship.ApplicationDeclineRequest; import com.wcc.platform.domain.platform.mentorship.ApplicationStatus; @@ -36,7 +39,7 @@ public class MentorshipApplicationController { private final MenteeWorkflowService applicationService; - + /** * API to get all applications submitted by a mentee for a specific cycle. * @@ -81,6 +84,9 @@ public ResponseEntity withdrawApplication( * @return List of applications */ @GetMapping("/mentors/{mentorId}/applications") + @RequiresPermission( + value = {Permission.MENTOR_APPLICATION_READ, Permission.MENTOR_APPROVE}, + operator = LogicalOperator.OR) @Operation(summary = "Get applications received by a mentor") @ResponseStatus(HttpStatus.OK) public ResponseEntity> getMentorApplications( From e7d1ec67b7d4e28f4dfa2ea6a8a06f6e71ad40e1 Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Tue, 27 Jan 2026 01:03:01 +0000 Subject: [PATCH 4/9] review changes --- .../configuration/TokenAuthFilter.java | 6 +-- .../security/AuthorizationAspect.java | 6 +-- .../configuration/security/RequiresRole.java | 2 + .../wcc/platform/domain/auth/UserAccount.java | 48 ++----------------- .../domain/platform/type/RoleType.java | 25 ---------- .../com/wcc/platform/service/AuthService.java | 25 ++-------- 6 files changed, 13 insertions(+), 99 deletions(-) diff --git a/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java b/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java index 532564c9..c9503252 100644 --- a/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java +++ b/src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java @@ -70,11 +70,7 @@ protected void doFilterInternal( final var authorities = List.of(new SimpleGrantedAuthority(user.getPrimaryRole().name())); final UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken( - user, // Principal is now UserAccount.User - null, - authorities // You can add authorities here if needed - ); + new UsernamePasswordAuthenticationToken(user, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication); } diff --git a/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java index 84789fe8..d2efcfbc 100644 --- a/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java +++ b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java @@ -3,6 +3,7 @@ import com.wcc.platform.domain.auth.Permission; import com.wcc.platform.domain.platform.type.RoleType; import com.wcc.platform.service.AuthService; +import lombok.AllArgsConstructor; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @@ -12,14 +13,11 @@ @Aspect @Component @Order(1) +@AllArgsConstructor public class AuthorizationAspect { private final AuthService authService; - public AuthorizationAspect(final AuthService authService) { - this.authService = authService; - } - @Around("@annotation(requiresPermission)") public Object checkPermission( ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { diff --git a/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java b/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java index 167f7901..86497023 100644 --- a/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java +++ b/src/main/java/com/wcc/platform/configuration/security/RequiresRole.java @@ -10,4 +10,6 @@ @Target({ElementType.METHOD, ElementType.TYPE}) public @interface RequiresRole { RoleType[] value(); + + LogicalOperator operator() default LogicalOperator.AND; } diff --git a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java index d68de228..05af4fb8 100644 --- a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java +++ b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java @@ -11,6 +11,7 @@ import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; +import org.springframework.util.CollectionUtils; /** Domain object representing an application user linked to a Member. */ @Data @@ -27,7 +28,7 @@ public class UserAccount { /** Get all permissions from all assigned roles in UserAccount. */ public Set getPermissions() { - if (roles == null || roles.isEmpty()) { + if (roles == null || CollectionUtils.isEmpty(roles)) { return Set.of(); } @@ -36,46 +37,6 @@ public Set getPermissions() { .collect(Collectors.toSet()); } - /* */ - /** Check if user has a specific permission in assigned roles. */ - /* - public boolean hasPermission(Permission permission) { - return getPermissions().contains(permission); - } - - */ - /** Check if user has any of the specified permissions. */ - /* - public boolean hasAnyPermission(Permission... permissions) { - Set userPermissions = getPermissions(); - return Arrays.stream(permissions).anyMatch(userPermissions::contains); - } - - */ - /** Check if user has all of the specified permissions. */ - /* - public boolean hasAllPermissions(Permission... permissions) { - Set userPermissions = getPermissions(); - return Arrays.stream(permissions).allMatch(userPermissions::contains); - } - - */ - /** Check if user has a specific role. */ - /* - public boolean hasRole(RoleType role) { - return roles != null && roles.contains(role); - } - - */ - /** Check if user has any of the specified roles. */ - /* - public boolean hasAnyRole(RoleType... rolesToCheck) { - if (roles == null) { - return false; - } - return Arrays.stream(rolesToCheck).anyMatch(roles::contains); - }*/ - /** * A record that encapsulates a User within the platform. * @@ -93,7 +54,7 @@ public record User(UserAccount userAccount, Member member) { * (since MENTOR has higher privileges). */ public RoleType getPrimaryRole() { - if (member == null || member.getMemberTypes() == null || member.getMemberTypes().isEmpty()) { + if (member == null || CollectionUtils.isEmpty(member.getMemberTypes())) { return RoleType.VIEWER; } return MemberTypeRoleMapper.getHighestRole(member.getMemberTypes()); @@ -107,7 +68,7 @@ public RoleType getPrimaryRole() { * CONTRIBUTOR roles. */ public Set getAllMemberRoles() { - if (member == null || member.getMemberTypes() == null) { + if (member == null || CollectionUtils.isEmpty(member.getMemberTypes())) { return Set.of(RoleType.VIEWER); } return MemberTypeRoleMapper.getRolesForMemberTypes(member.getMemberTypes()); @@ -138,7 +99,6 @@ public Set getAllPermissions() { MemberTypeRoleMapper.getAllPermissionsForMemberTypes(member.getMemberTypes())); } - // Add permissions from explicitly assigned roles permissions.addAll(userAccount.getPermissions()); return permissions; diff --git a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java index ff3f91a7..9ef15f94 100644 --- a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java +++ b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java @@ -56,31 +56,6 @@ public static RoleType fromId(final int typeId) { return VIEWER; } - /** Check if this role has a specific permission. */ - public boolean hasPermission(Permission permission) { - return permissions.contains(permission); - } - - /** Check if this role has any of the specified permissions. */ - public boolean hasAnyPermission(Permission... requiredPermissions) { - for (Permission permission : requiredPermissions) { - if (permissions.contains(permission)) { - return true; - } - } - return false; - } - - /** Check if this role has all of the specified permissions. */ - public boolean hasAllPermissions(Permission... requiredPermissions) { - for (Permission permission : requiredPermissions) { - if (!permissions.contains(permission)) { - return false; - } - } - return true; - } - @Override public String toString() { return description; diff --git a/src/main/java/com/wcc/platform/service/AuthService.java b/src/main/java/com/wcc/platform/service/AuthService.java index 1e4fc4b1..f1af53be 100644 --- a/src/main/java/com/wcc/platform/service/AuthService.java +++ b/src/main/java/com/wcc/platform/service/AuthService.java @@ -16,6 +16,7 @@ import java.util.Base64; import java.util.Optional; import java.util.Set; +import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; @@ -27,35 +28,17 @@ * authentication, token management, and member retrieval. */ @Service +@RequiredArgsConstructor public class AuthService { private static final SecureRandom RANDOM = new SecureRandom(); private final UserAccountRepository userAccountRepository; private final UserTokenRepository userTokenRepository; private final MemberRepository memberRepository; - private final int tokenTtlMinutes; private final PasswordEncoder passwordEncoder; - /** - * Constructor for the AuthService class. - * - * @param userAccountRepository the repository for managing user account entities - * @param userTokenRepository the repository for managing user token entities - * @param memberRepository the repository for managing member entities - * @param tokenTtlMinutes the time-to-live value for tokens, in minutes - */ - public AuthService( - final UserAccountRepository userAccountRepository, - final UserTokenRepository userTokenRepository, - final MemberRepository memberRepository, - final @Value("${security.token.ttl-minutes}") int tokenTtlMinutes, - final PasswordEncoder passwordEncoder) { - this.userAccountRepository = userAccountRepository; - this.userTokenRepository = userTokenRepository; - this.memberRepository = memberRepository; - this.tokenTtlMinutes = tokenTtlMinutes; - this.passwordEncoder = passwordEncoder; - } + @Value("${security.token.ttl-minutes}") + private int tokenTtlMinutes; public Optional findUserByEmail(final String email) { return userAccountRepository.findByEmail(email); From cef4458c9f2e5faef3740a27d3aa58657d57e0c3 Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Wed, 28 Jan 2026 21:55:06 +0000 Subject: [PATCH 5/9] removed unwanted methods --- .../wcc/platform/domain/auth/UserAccount.java | 32 ----- .../com/wcc/platform/service/AuthService.java | 116 ------------------ 2 files changed, 148 deletions(-) diff --git a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java index 05af4fb8..14ef732e 100644 --- a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java +++ b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java @@ -104,42 +104,10 @@ public Set getAllPermissions() { return permissions; } - /** - * Check if user has a specific permission. Checks permissions from both member types and - * assigned roles. - */ - public boolean hasPermission(Permission permission) { - return getAllPermissions().contains(permission); - } - - /** - * Check if user has a specific role. Checks both member-type-derived roles and explicitly - * assigned roles. - */ - public boolean hasRole(RoleType role) { - return getAllRoles().contains(role); - } - /** Check if user has any of the specified roles. */ public boolean hasAnyRole(RoleType... roles) { Set userRoles = getAllRoles(); return Arrays.stream(roles).anyMatch(userRoles::contains); } - - /** Check if user is a super admin (has DIRECTOR member type). */ - public boolean isSuperAdmin() { - if (member == null || member.getMemberTypes() == null) { - return false; - } - return MemberTypeRoleMapper.isSuperAdmin(member.getMemberTypes()); - } - - /** Check if user is an admin (has DIRECTOR or LEADER member type). */ - public boolean isAdmin() { - if (member == null || member.getMemberTypes() == null) { - return false; - } - return MemberTypeRoleMapper.isAdmin(member.getMemberTypes()); - } } } diff --git a/src/main/java/com/wcc/platform/service/AuthService.java b/src/main/java/com/wcc/platform/service/AuthService.java index f1af53be..cb9500d3 100644 --- a/src/main/java/com/wcc/platform/service/AuthService.java +++ b/src/main/java/com/wcc/platform/service/AuthService.java @@ -151,28 +151,6 @@ public Optional getUserWithMember(final Integer userId) { return memberOpt.map(member -> new UserAccount.User(userAccount, member)); } - /** - * Retrieves the complete User (UserAccount + Member) by email. This is used for authentication - * and RBAC. - * - * @param email the email of the user - * @return an {@code Optional} containing the user and member if found - */ - public Optional getUserWithMemberByEmail(final String email) { - Optional userAccountOpt = userAccountRepository.findByEmail(email); - if (userAccountOpt.isEmpty()) { - return Optional.empty(); - } - - UserAccount userAccount = userAccountOpt.get(); - if (userAccount.getMemberId() == null) { - return Optional.empty(); - } - - Optional memberOpt = memberRepository.findById(userAccount.getMemberId()); - return memberOpt.map(member -> new UserAccount.User(userAccount, member)); - } - /** * Authenticates a user based on the provided token and returns the complete User (UserAccount + * Member). This is the preferred method for RBAC-enabled authentication. @@ -215,46 +193,6 @@ public UserAccount.User getCurrentUser() { throw new ForbiddenException("Invalid authentication principal"); } - /** - * Get current user's permissions from all member types and assigned roles. - * - * @return set of all permissions the user has - */ - public Set getCurrentUserPermissions() { - return getCurrentUser().getAllPermissions(); - } - - /** - * Get current user's primary role (highest privilege role from member types). - * - * @return the primary role of the user - */ - public RoleType getCurrentUserPrimaryRole() { - return getCurrentUser().getPrimaryRole(); - } - - /** - * Get all roles the current user has (from member types and assigned roles). - * - * @return set of all roles - */ - public Set getCurrentUserRoles() { - return getCurrentUser().getAllRoles(); - } - - /** - * Require a specific permission. - * - * @param permission the required permission - * @throws ForbiddenException if user doesn't have the permission - */ - public void requirePermission(Permission permission) { - if (!getCurrentUser().hasPermission(permission)) { - throw new ForbiddenException( - String.format("Permission denied. Required: %s", permission.name())); - } - } - /** * Require any of the specified permissions (OR logic). * @@ -310,58 +248,4 @@ public void requireRole(RoleType... allowedRoles) { "Role denied. User roles: %s, Required any of: %s", user.getAllRoles(), Arrays.toString(allowedRoles))); } - - /** - * Check if current user has permission (non-throwing). - * - * @param permission the permission to check - * @return true if user has the permission, false otherwise - */ - public boolean hasPermission(Permission permission) { - try { - return getCurrentUser().hasPermission(permission); - } catch (ForbiddenException e) { - return false; - } - } - - /** - * Check if current user has role (non-throwing). - * - * @param role the role to check - * @return true if user has the role, false otherwise - */ - public boolean hasRole(RoleType role) { - try { - return getCurrentUser().hasRole(role); - } catch (ForbiddenException e) { - return false; - } - } - - /** - * Check if current user is a super admin. - * - * @return true if user is super admin, false otherwise - */ - public boolean isSuperAdmin() { - try { - return getCurrentUser().isSuperAdmin(); - } catch (ForbiddenException e) { - return false; - } - } - - /** - * Check if current user is an admin (SUPER_ADMIN or ADMIN). - * - * @return true if user is admin, false otherwise - */ - public boolean isAdmin() { - try { - return getCurrentUser().isAdmin(); - } catch (ForbiddenException e) { - return false; - } - } } From 3d2159865471a9cc8b6905e104a6b4242dd358f3 Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Wed, 28 Jan 2026 23:02:26 +0000 Subject: [PATCH 6/9] unit tests --- .../GlobalExceptionHandlerTest.java | 13 + .../platform/domain/auth/UserAccountTest.java | 322 ++++++++++ .../wcc/platform/service/AuthServiceTest.java | 592 ++++++++++++++++++ 3 files changed, 927 insertions(+) create mode 100644 src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java create mode 100644 src/test/java/com/wcc/platform/service/AuthServiceTest.java diff --git a/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java b/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java index f2c3c5e1..992f25c3 100644 --- a/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java +++ b/src/test/java/com/wcc/platform/configuration/GlobalExceptionHandlerTest.java @@ -5,9 +5,11 @@ import static org.mockito.Mockito.when; import static org.springframework.http.HttpStatus.BAD_REQUEST; import static org.springframework.http.HttpStatus.CONFLICT; +import static org.springframework.http.HttpStatus.FORBIDDEN; import com.wcc.platform.domain.exceptions.DuplicatedMemberException; import com.wcc.platform.domain.exceptions.ErrorDetails; +import com.wcc.platform.domain.exceptions.ForbiddenException; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -64,4 +66,15 @@ void shouldReturnNotAcceptableForMethodArgumentNotValidException() { assertEquals(BAD_REQUEST, response.getStatusCode()); assertEquals(expectation, response.getBody()); } + + @Test + void shouldReturnForbiddenForAccessDeniedException() { + var exception = new ForbiddenException("Error"); + + var response = globalExceptionHandler.handleForbiddenException(exception, webRequest); + + var expectation = new ErrorDetails(FORBIDDEN.value(), "Error", DETAILS); + assertEquals(FORBIDDEN, response.getStatusCode()); + assertEquals(expectation, response.getBody()); + } } diff --git a/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java b/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java new file mode 100644 index 00000000..b08ca420 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java @@ -0,0 +1,322 @@ +package com.wcc.platform.domain.auth; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.domain.platform.type.MemberType; +import com.wcc.platform.domain.platform.type.RoleType; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +class UserAccountTest { + + @Test + void testGetPermissions_withMultipleRoles_returnsAggregatedPermissions() { + UserAccount userAccount = + UserAccount.builder() + .id(1) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN, RoleType.MENTOR)) + .enabled(true) + .build(); + + Set permissions = userAccount.getPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + } + + @Test + void testGetPermissions_withSingleRole_returnsRolePermissions() { + UserAccount userAccount = + UserAccount.builder() + .id(1) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .enabled(true) + .build(); + + Set permissions = userAccount.getPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + assertEquals(RoleType.ADMIN.getPermissions(), permissions); + } + + @Test + void testGetPermissions_withNullRoles_returnsEmptySet() { + UserAccount userAccount = + UserAccount.builder().id(1).email("user@example.com").roles(null).enabled(true).build(); + + Set permissions = userAccount.getPermissions(); + + assertNotNull(permissions); + assertTrue(permissions.isEmpty()); + } + + @Test + void testGetPrimaryRole_memberWithSingleType_returnsCorrespondingRole() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + + assertEquals(RoleType.MENTOR, user.getPrimaryRole()); + } + + @Test + void testGetPrimaryRole_memberWithMultipleTypes_returnsHighestPrivilegeRole() { + // DIRECTOR is higher privilege than MENTEE + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.DIRECTOR, MemberType.MENTEE)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + + assertEquals(RoleType.SUPER_ADMIN, user.getPrimaryRole()); + } + + @Test + void testGetAllMemberRoles_memberWithSingleType_returnsSingleRole() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set roles = user.getAllMemberRoles(); + + assertEquals(1, roles.size()); + assertTrue(roles.contains(RoleType.MENTOR)); + } + + @Test + void testGetAllMemberRoles_memberWithMultipleTypes_returnsMultipleRoles() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR, MemberType.COLLABORATOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set roles = user.getAllMemberRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains(RoleType.MENTOR)); + assertTrue(roles.contains(RoleType.CONTRIBUTOR)); + } + + @Test + void testGetAllRoles_onlyMemberRoles_returnsOnlyMemberRoles() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set roles = user.getAllRoles(); + + assertEquals(1, roles.size()); + assertTrue(roles.contains(RoleType.MENTOR)); + } + + @Test + void testGetAllRoles_onlyUserRoles_returnsOnlyUserRoles() { + Member member = Member.builder().id(1L).fullName("John Doe").memberTypes(List.of()).build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set roles = user.getAllRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains(RoleType.ADMIN)); + } + + @Test + void testGetAllRoles_combinedMemberAndUserRoles_returnsAggregatedRoles() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set roles = user.getAllRoles(); + + assertEquals(2, roles.size()); + assertTrue(roles.contains(RoleType.MENTOR)); + assertTrue(roles.contains(RoleType.ADMIN)); + } + + @Test + void testGetAllPermissions_fromMemberTypes_includesMemberTypePermissions() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set permissions = user.getAllPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + } + + @Test + void testGetAllPermissions_fromUserRoles_includesUserRolePermissions() { + Member member = Member.builder().id(1L).fullName("John Doe").memberTypes(List.of()).build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set permissions = user.getAllPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + } + + @Test + void testGetAllPermissions_combinedPermissions_aggregatesFromBothSources() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + Set permissions = user.getAllPermissions(); + + assertNotNull(permissions); + assertFalse(permissions.isEmpty()); + // Should include permissions from both MENTOR role and ADMIN role + assertTrue(permissions.size() >= 2); + } + + @Test + void testHasAnyRole_userHasOneOfRequiredRoles_returnsTrue() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + + assertTrue(user.hasAnyRole(RoleType.ADMIN, RoleType.VIEWER)); + } + + @Test + void testHasAnyRole_userHasMultipleRequiredRoles_returnsTrue() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTOR)) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .memberId(1L) + .email("john@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + + assertTrue(user.hasAnyRole(RoleType.ADMIN, RoleType.MENTOR)); + } + + @Test + void testHasAnyRole_userHasNoneOfRequiredRoles_returnsFalse() { + Member member = + Member.builder() + .id(1L) + .fullName("John Doe") + .memberTypes(List.of(MemberType.MENTEE)) + .build(); + + UserAccount userAccount = + UserAccount.builder().id(1).memberId(1L).email("john@example.com").roles(List.of()).build(); + + UserAccount.User user = new UserAccount.User(userAccount, member); + + assertFalse(user.hasAnyRole(RoleType.ADMIN, RoleType.MENTOR)); + } +} diff --git a/src/test/java/com/wcc/platform/service/AuthServiceTest.java b/src/test/java/com/wcc/platform/service/AuthServiceTest.java new file mode 100644 index 00000000..aa63d2e0 --- /dev/null +++ b/src/test/java/com/wcc/platform/service/AuthServiceTest.java @@ -0,0 +1,592 @@ +package com.wcc.platform.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.auth.Permission; +import com.wcc.platform.domain.auth.UserAccount; +import com.wcc.platform.domain.auth.UserToken; +import com.wcc.platform.domain.exceptions.ForbiddenException; +import com.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.domain.platform.type.MemberType; +import com.wcc.platform.domain.platform.type.RoleType; +import com.wcc.platform.repository.MemberRepository; +import com.wcc.platform.repository.UserAccountRepository; +import com.wcc.platform.repository.UserTokenRepository; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.test.util.ReflectionTestUtils; + +class AuthServiceTest { + + @Mock private UserAccountRepository userAccountRepository; + @Mock private UserTokenRepository userTokenRepository; + @Mock private MemberRepository memberRepository; + @Mock private PasswordEncoder passwordEncoder; + @Mock private SecurityContext securityContext; + @Mock private Authentication authentication; + + private AuthService authService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + authService = + new AuthService( + userAccountRepository, userTokenRepository, memberRepository, passwordEncoder); + ReflectionTestUtils.setField(authService, "tokenTtlMinutes", 60); + } + + // ==================== findUserByEmail Tests ==================== + + @Test + void testFindUserByEmail_userExists_returnsUserAccount() { + String email = "user@example.com"; + UserAccount userAccount = + UserAccount.builder() + .id(1) + .email(email) + .memberId(1L) + .enabled(true) + .roles(List.of(RoleType.ADMIN)) + .build(); + + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.of(userAccount)); + + Optional result = authService.findUserByEmail(email); + + assertTrue(result.isPresent()); + assertEquals(email, result.get().getEmail()); + verify(userAccountRepository).findByEmail(email); + } + + @Test + void testFindUserByEmail_userNotFound_returnsEmpty() { + String email = "notfound@example.com"; + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.empty()); + + Optional result = authService.findUserByEmail(email); + + assertTrue(result.isEmpty()); + } + + // ==================== getMember Tests ==================== + + @Test + void testGetMember_memberExists_returnsMemberDto() { + Long memberId = 1L; + Member member = + Member.builder().id(memberId).fullName("John Doe").email("john@example.com").build(); + + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + var result = authService.getMember(memberId); + + assertNotNull(result); + assertEquals("John Doe", result.getFullName()); + } + + @Test + void testGetMember_memberNotFound_returnsNull() { + Long memberId = 999L; + when(memberRepository.findById(memberId)).thenReturn(Optional.empty()); + + var result = authService.getMember(memberId); + + assertNull(result); + } + + @Test + void testGetMember_nullId_returnsNull() { + var result = authService.getMember(null); + + assertNull(result); + verify(memberRepository, never()).findById(any()); + } + + // ==================== authenticateAndIssueToken Tests ==================== + + @Test + void testAuthenticateAndIssueToken_validCredentials_returnsToken() { + String email = "user@example.com"; + String password = "password123"; + String passwordHash = "hashed_password"; + + UserAccount userAccount = + UserAccount.builder() + .id(1) + .email(email) + .passwordHash(passwordHash) + .enabled(true) + .roles(List.of(RoleType.ADMIN)) + .build(); + + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.of(userAccount)); + when(passwordEncoder.matches(password, passwordHash)).thenReturn(true); + when(userTokenRepository.create(any(UserToken.class))) + .thenAnswer(invocation -> invocation.getArgument(0)); + + Optional result = authService.authenticateAndIssueToken(email, password); + + assertTrue(result.isPresent()); + assertEquals(1, result.get().getUserId()); + assertFalse(result.get().isRevoked()); + } + + @Test + void testAuthenticateAndIssueToken_userNotFound_returnsEmpty() { + String email = "notfound@example.com"; + String password = "password123"; + + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.empty()); + + Optional result = authService.authenticateAndIssueToken(email, password); + + assertTrue(result.isEmpty()); + } + + @Test + void testAuthenticateAndIssueToken_userDisabled_returnsEmpty() { + String email = "user@example.com"; + String password = "password123"; + + UserAccount userAccount = + UserAccount.builder().id(1).email(email).passwordHash("hash").enabled(false).build(); + + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.of(userAccount)); + + Optional result = authService.authenticateAndIssueToken(email, password); + + assertTrue(result.isEmpty()); + } + + @Test + void testAuthenticateAndIssueToken_invalidPassword_returnsEmpty() { + String email = "user@example.com"; + String password = "wrongpassword"; + String passwordHash = "hashed_password"; + + UserAccount userAccount = + UserAccount.builder().id(1).email(email).passwordHash(passwordHash).enabled(true).build(); + + when(userAccountRepository.findByEmail(email)).thenReturn(Optional.of(userAccount)); + when(passwordEncoder.matches(password, passwordHash)).thenReturn(false); + + Optional result = authService.authenticateAndIssueToken(email, password); + + assertTrue(result.isEmpty()); + } + + // ==================== authenticateByToken Tests ==================== + + @Test + void testAuthenticateByToken_validToken_returnsUserAccount() { + String token = "valid-token"; + Integer userId = 1; + + UserToken userToken = + UserToken.builder() + .token(token) + .userId(userId) + .issuedAt(OffsetDateTime.now()) + .expiresAt(OffsetDateTime.now().plusHours(1)) + .revoked(false) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .email("user@example.com") + .enabled(true) + .roles(List.of(RoleType.ADMIN)) + .build(); + + when(userTokenRepository.findValidByToken(eq(token), any(OffsetDateTime.class))) + .thenReturn(Optional.of(userToken)); + when(userAccountRepository.findById(userId)).thenReturn(Optional.of(userAccount)); + + Optional result = authService.authenticateByToken(token); + + assertTrue(result.isPresent()); + assertEquals("user@example.com", result.get().getEmail()); + } + + @Test + void testAuthenticateByToken_invalidToken_returnsEmpty() { + String token = "invalid-token"; + + when(userTokenRepository.findValidByToken(eq(token), any(OffsetDateTime.class))) + .thenReturn(Optional.empty()); + + Optional result = authService.authenticateByToken(token); + + assertTrue(result.isEmpty()); + } + + @Test + void testAuthenticateByToken_userNotFound_returnsEmpty() { + String token = "valid-token"; + Integer userId = 999; + + UserToken userToken = + UserToken.builder() + .token(token) + .userId(userId) + .issuedAt(OffsetDateTime.now()) + .expiresAt(OffsetDateTime.now().plusHours(1)) + .revoked(false) + .build(); + + when(userTokenRepository.findValidByToken(eq(token), any(OffsetDateTime.class))) + .thenReturn(Optional.of(userToken)); + when(userAccountRepository.findById(userId)).thenReturn(Optional.empty()); + + Optional result = authService.authenticateByToken(token); + + assertTrue(result.isEmpty()); + } + + // ==================== getUserWithMember Tests ==================== + + @Test + void testGetUserWithMember_userAndMemberExist_returnsUser() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = + Member.builder() + .id(memberId) + .fullName("John Doe") + .memberTypes(List.of(MemberType.LEADER)) + .build(); + + when(userAccountRepository.findById(userId)).thenReturn(Optional.of(userAccount)); + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + Optional result = authService.getUserWithMember(userId); + + assertTrue(result.isPresent()); + assertEquals("user@example.com", result.get().userAccount().getEmail()); + assertEquals("John Doe", result.get().member().getFullName()); + } + + @Test + void testGetUserWithMember_userHasNoMemberId_returnsEmpty() { + Integer userId = 1; + + UserAccount userAccount = UserAccount.builder().id(userId).memberId(null).build(); + + when(userAccountRepository.findById(userId)).thenReturn(Optional.of(userAccount)); + + Optional result = authService.getUserWithMember(userId); + + assertTrue(result.isEmpty()); + } + + // ==================== authenticateByTokenWithMember Tests ==================== + + @Test + void testAuthenticateByTokenWithMember_validToken_returnsUserWithMember() { + String token = "valid-token"; + Integer userId = 1; + Long memberId = 1L; + + UserToken userToken = + UserToken.builder() + .token(token) + .userId(userId) + .issuedAt(OffsetDateTime.now()) + .expiresAt(OffsetDateTime.now().plusHours(1)) + .revoked(false) + .build(); + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + + when(userTokenRepository.findValidByToken(eq(token), any())).thenReturn(Optional.of(userToken)); + when(userAccountRepository.findById(userId)).thenReturn(Optional.of(userAccount)); + when(memberRepository.findById(memberId)).thenReturn(Optional.of(member)); + + Optional result = authService.authenticateByTokenWithMember(token); + + assertTrue(result.isPresent()); + assertEquals("user@example.com", result.get().userAccount().getEmail()); + } + + @Test + void testAuthenticateByTokenWithMember_invalidToken_returnsEmpty() { + String token = "invalid-token"; + + when(userTokenRepository.findValidByToken(eq(token), any(OffsetDateTime.class))) + .thenReturn(Optional.empty()); + + Optional result = authService.authenticateByTokenWithMember(token); + + assertTrue(result.isEmpty()); + } + + // ==================== getCurrentUser Tests ==================== + + @Test + void testGetCurrentUser_validAuthentication_returnsUser() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + UserAccount.User result = authService.getCurrentUser(); + + assertEquals("user@example.com", result.userAccount().getEmail()); + } + + @Test + void testGetCurrentUser_notAuthenticated_throwsForbiddenException() { + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(null); + + assertThrows(ForbiddenException.class, () -> authService.getCurrentUser()); + } + + @Test + void testGetCurrentUser_invalidPrincipal_throwsForbiddenException() { + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn("invalid-principal"); + + assertThrows(ForbiddenException.class, () -> authService.getCurrentUser()); + } + + // ==================== requireAnyPermission Tests ==================== + + @Test + void testRequireAnyPermission_userHasPermission_succeeds() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + // ADMIN role has MENTOR_APPROVE permission + authService.requireAnyPermission(Permission.MENTOR_APPROVE); + + // Should not throw + } + + @Test + void testRequireAnyPermission_userLacksPermission_throwsForbiddenException() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.VIEWER)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + assertThrows( + ForbiddenException.class, + () -> authService.requireAnyPermission(Permission.MENTOR_APPROVE)); + } + + // ==================== requireAllPermissions Tests ==================== + + @Test + void testRequireAllPermissions_userHasAllPermissions_succeeds() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + // ADMIN has both permissions + authService.requireAllPermissions(Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE); + + // Should not throw + } + + @Test + void testRequireAllPermissions_userMissesOnePermission_throwsForbiddenException() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.VIEWER)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + assertThrows( + ForbiddenException.class, + () -> + authService.requireAllPermissions( + Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE)); + } + + // ==================== requireRole Tests ==================== + + @Test + void testRequireRole_userHasRequiredRole_succeeds() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.ADMIN)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + authService.requireRole(RoleType.ADMIN); + } + + @Test + void testRequireRole_userLacksRequiredRole_throwsForbiddenException() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.VIEWER)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + assertThrows(ForbiddenException.class, () -> authService.requireRole(RoleType.ADMIN)); + } + + @Test + void testRequireRole_userHasAnyOfMultipleRoles_succeeds() { + Integer userId = 1; + Long memberId = 1L; + + UserAccount userAccount = + UserAccount.builder() + .id(userId) + .memberId(memberId) + .email("user@example.com") + .roles(List.of(RoleType.MENTOR)) + .build(); + + Member member = Member.builder().id(memberId).fullName("John Doe").build(); + UserAccount.User user = new UserAccount.User(userAccount, member); + + SecurityContextHolder.setContext(securityContext); + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.isAuthenticated()).thenReturn(true); + when(authentication.getPrincipal()).thenReturn(user); + + authService.requireRole(RoleType.ADMIN, RoleType.MENTOR); + } +} From b216515ab97b6b62c9562667f5df3222f7119c5b Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Wed, 28 Jan 2026 23:30:06 +0000 Subject: [PATCH 7/9] fix failed unit tests --- .../configuration/TokenAuthFilterTest.java | 23 +++++++++++++------ .../wcc/platform/service/AuthServiceTest.java | 10 ++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java b/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java index bd156f82..3da50670 100644 --- a/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java +++ b/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java @@ -9,6 +9,8 @@ import static org.mockito.Mockito.when; import com.wcc.platform.domain.auth.UserAccount; +import com.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.domain.platform.type.MemberType; import com.wcc.platform.domain.platform.type.RoleType; import com.wcc.platform.service.AuthService; import jakarta.servlet.FilterChain; @@ -54,18 +56,25 @@ void givenValidBearerTokenWhenDoFilterInternalThenAuthenticationIsSet() UserAccount mockUser = UserAccount.builder().email("admin@wcc.dev").roles(List.of(RoleType.ADMIN)).build(); + Member member = + Member.builder() + .id(1L) + .fullName("Admin WCC") + .memberTypes(List.of(MemberType.LEADER)) + .build(); + UserAccount.User user = new UserAccount.User(mockUser, member); - when(authService.authenticateByToken(token)).thenReturn(Optional.of(mockUser)); + when(authService.authenticateByTokenWithMember(token)).thenReturn(Optional.of(user)); tokenAuthFilter.doFilterInternal(request, response, filterChain); var authentication = SecurityContextHolder.getContext().getAuthentication(); - verify(authService).authenticateByToken(token); + verify(authService).authenticateByTokenWithMember(token); verify(filterChain).doFilter(request, response); assertNotNull(authentication); assertTrue(authentication.isAuthenticated()); - assertEquals("admin@wcc.dev", authentication.getName()); + assertEquals(user, authentication.getPrincipal()); final var authorities = authentication.getAuthorities(); assertTrue(authorities.stream().anyMatch(a -> a.getAuthority().equals("ADMIN"))); } @@ -79,7 +88,7 @@ void givenMissingAuthorizationHeaderWhenDoFilterInternalThenNoAuthenticationAtte tokenAuthFilter.doFilterInternal(request, response, filterChain); - verify(authService, never()).authenticateByToken(anyString()); + verify(authService, never()).authenticateByTokenWithMember(anyString()); verify(filterChain).doFilter(request, response); assert SecurityContextHolder.getContext().getAuthentication() == null; } @@ -92,11 +101,11 @@ void givenInvalidBearerTokenWhenDoFilterInternalThenAuthenticationNotSet() throws ServletException, IOException { final String invalidToken = "invalidToken123"; when(request.getHeader("Authorization")).thenReturn("Bearer " + invalidToken); - when(authService.authenticateByToken(invalidToken)).thenReturn(Optional.empty()); + when(authService.authenticateByTokenWithMember(invalidToken)).thenReturn(Optional.empty()); tokenAuthFilter.doFilterInternal(request, response, filterChain); - verify(authService).authenticateByToken(invalidToken); + verify(authService).authenticateByTokenWithMember(invalidToken); verify(filterChain).doFilter(request, response); assert SecurityContextHolder.getContext().getAuthentication() == null; } @@ -111,7 +120,7 @@ void givenMalformedHeaderWithoutBearerWhenDoFilterInternalThenNoAuthenticationAt tokenAuthFilter.doFilterInternal(request, response, filterChain); - verify(authService, never()).authenticateByToken(anyString()); + verify(authService, never()).authenticateByTokenWithMember(anyString()); verify(filterChain).doFilter(request, response); assert SecurityContextHolder.getContext().getAuthentication() == null; } diff --git a/src/test/java/com/wcc/platform/service/AuthServiceTest.java b/src/test/java/com/wcc/platform/service/AuthServiceTest.java index aa63d2e0..07e82508 100644 --- a/src/test/java/com/wcc/platform/service/AuthServiceTest.java +++ b/src/test/java/com/wcc/platform/service/AuthServiceTest.java @@ -59,7 +59,7 @@ void setUp() { @Test void testFindUserByEmail_userExists_returnsUserAccount() { - String email = "user@example.com"; + var email = "user@example.com"; UserAccount userAccount = UserAccount.builder() .id(1) @@ -80,7 +80,7 @@ void testFindUserByEmail_userExists_returnsUserAccount() { @Test void testFindUserByEmail_userNotFound_returnsEmpty() { - String email = "notfound@example.com"; + var email = "notfound@example.com"; when(userAccountRepository.findByEmail(email)).thenReturn(Optional.empty()); Optional result = authService.findUserByEmail(email); @@ -348,7 +348,7 @@ void testAuthenticateByTokenWithMember_validToken_returnsUserWithMember() { @Test void testAuthenticateByTokenWithMember_invalidToken_returnsEmpty() { - String token = "invalid-token"; + var token = "invalid-token"; when(userTokenRepository.findValidByToken(eq(token), any(OffsetDateTime.class))) .thenReturn(Optional.empty()); @@ -362,8 +362,8 @@ void testAuthenticateByTokenWithMember_invalidToken_returnsEmpty() { @Test void testGetCurrentUser_validAuthentication_returnsUser() { - Integer userId = 1; - Long memberId = 1L; + var userId = 1; + var memberId = 1L; UserAccount userAccount = UserAccount.builder() From 3c2f3846447acaf18eba054ae575b18fb20b6f44 Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Thu, 29 Jan 2026 21:56:11 +0000 Subject: [PATCH 8/9] fixed PMD and some data --- .../configuration/GlobalExceptionHandler.java | 4 +- .../security/AuthorizationAspect.java | 9 +-- .../MentorshipApplicationController.java | 2 +- .../domain/auth/MemberTypeRoleMapper.java | 56 +++++++++---------- .../wcc/platform/domain/auth/Permission.java | 8 +-- .../wcc/platform/domain/auth/UserAccount.java | 10 ++-- .../domain/exceptions/ForbiddenException.java | 2 +- .../domain/platform/type/RoleType.java | 16 +++--- .../com/wcc/platform/service/AuthService.java | 31 +++++----- ...V23__20260120__update_user_auth_tables.sql | 24 +++++++- .../configuration/TokenAuthFilterTest.java | 2 +- .../domain/auth/MemberTypeRoleMapperTest.java | 17 +++--- .../platform/domain/auth/UserAccountTest.java | 6 +- 13 files changed, 101 insertions(+), 86 deletions(-) diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index 15ba0e8b..e85724e1 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -134,8 +134,8 @@ public ResponseEntity handleMethodArgumentNotValidException( /** Return 403 Forbidden for ForbiddenException. */ @ExceptionHandler(ForbiddenException.class) public ResponseEntity handleForbiddenException( - ForbiddenException ex, final WebRequest request) { - var errorResponse = + final ForbiddenException ex, final WebRequest request) { + final var errorResponse = new ErrorDetails( HttpStatus.FORBIDDEN.value(), ex.getMessage(), request.getDescription(false)); diff --git a/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java index d2efcfbc..4661267e 100644 --- a/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java +++ b/src/main/java/com/wcc/platform/configuration/security/AuthorizationAspect.java @@ -20,9 +20,10 @@ public class AuthorizationAspect { @Around("@annotation(requiresPermission)") public Object checkPermission( - ProceedingJoinPoint joinPoint, RequiresPermission requiresPermission) throws Throwable { + final ProceedingJoinPoint joinPoint, final RequiresPermission requiresPermission) + throws Throwable { - Permission[] permissions = requiresPermission.value(); + final Permission[] permissions = requiresPermission.value(); if (permissions.length == 0) { throw new IllegalArgumentException( @@ -39,10 +40,10 @@ public Object checkPermission( } @Around("@annotation(requiresRole)") - public Object checkRole(ProceedingJoinPoint joinPoint, RequiresRole requiresRole) + public Object checkRole(final ProceedingJoinPoint joinPoint, final RequiresRole requiresRole) throws Throwable { - RoleType[] roles = requiresRole.value(); + final RoleType[] roles = requiresRole.value(); if (roles.length == 0) { throw new IllegalArgumentException("@RequiresRole must specify at least one role"); diff --git a/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java b/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java index 20e80979..0db6b043 100644 --- a/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java +++ b/src/main/java/com/wcc/platform/controller/MentorshipApplicationController.java @@ -85,7 +85,7 @@ public ResponseEntity withdrawApplication( */ @GetMapping("/mentors/{mentorId}/applications") @RequiresPermission( - value = {Permission.MENTOR_APPLICATION_READ, Permission.MENTOR_APPROVE}, + value = {Permission.MENTOR_APPL_READ, Permission.MENTOR_APPROVE}, operator = LogicalOperator.OR) @Operation(summary = "Get applications received by a mentor") @ResponseStatus(HttpStatus.OK) diff --git a/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java index 7cf95f05..3f29ee2c 100644 --- a/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java +++ b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java @@ -8,12 +8,12 @@ import java.util.Set; import java.util.stream.Collectors; -public class MemberTypeRoleMapper { +public final class MemberTypeRoleMapper { - private static final Map MEMBER_TYPE_TO_ROLE_MAP = + private static final Map MEMBER_TYPE_TO_ROLE = Map.of( - MemberType.DIRECTOR, RoleType.SUPER_ADMIN, - MemberType.LEADER, RoleType.ADMIN, + MemberType.DIRECTOR, RoleType.ADMIN, + MemberType.LEADER, RoleType.LEADER, MemberType.MENTOR, RoleType.MENTOR, MemberType.MENTEE, RoleType.MENTEE, MemberType.COLLABORATOR, RoleType.CONTRIBUTOR, @@ -26,8 +26,8 @@ public class MemberTypeRoleMapper { // Role hierarchy: higher number = more privileged private static final Map ROLE_HIERARCHY = Map.of( - RoleType.SUPER_ADMIN, 100, - RoleType.ADMIN, 80, + RoleType.ADMIN, 100, + RoleType.LEADER, 80, RoleType.MENTOR, 60, RoleType.CONTRIBUTOR, 50, RoleType.MENTEE, 40, @@ -38,11 +38,11 @@ private MemberTypeRoleMapper() { } /** Get role for a single MemberType. */ - public static RoleType getRoleForMemberType(MemberType memberType) { + public static RoleType getRoleForMemberType(final MemberType memberType) { if (memberType == null) { throw new IllegalArgumentException("MemberType cannot be null"); } - return MEMBER_TYPE_TO_ROLE_MAP.getOrDefault(memberType, RoleType.VIEWER); + return MEMBER_TYPE_TO_ROLE.getOrDefault(memberType, RoleType.VIEWER); } /** @@ -51,7 +51,7 @@ public static RoleType getRoleForMemberType(MemberType memberType) { * @param memberTypes list of member types * @return set of all roles corresponding to the member types */ - public static Set getRolesForMemberTypes(List memberTypes) { + public static Set getRolesForMemberTypes(final List memberTypes) { if (memberTypes == null || memberTypes.isEmpty()) { return Set.of(RoleType.VIEWER); } @@ -68,7 +68,7 @@ public static Set getRolesForMemberTypes(List memberTypes) * @param memberTypes list of member types * @return the role with highest privilege level */ - public static RoleType getHighestRole(List memberTypes) { + public static RoleType getHighestRole(final List memberTypes) { if (memberTypes == null || memberTypes.isEmpty()) { return RoleType.VIEWER; } @@ -85,7 +85,8 @@ public static RoleType getHighestRole(List memberTypes) { * @param memberTypes list of member types * @return set of all unique permissions */ - public static Set getAllPermissionsForMemberTypes(List memberTypes) { + public static Set getAllPermissionsForMemberTypes( + final List memberTypes) { if (memberTypes == null || memberTypes.isEmpty()) { return RoleType.VIEWER.getPermissions(); } @@ -97,31 +98,24 @@ public static Set getAllPermissionsForMemberTypes(List m } /** Check if any of the member types maps to SUPER_ADMIN. */ - public static boolean isSuperAdmin(List memberTypes) { - if (memberTypes == null) { - return false; - } - return memberTypes.stream() - .anyMatch(type -> getRoleForMemberType(type) == RoleType.SUPER_ADMIN); + public static boolean isSuperAdmin(final List memberTypes) { + return memberTypes != null + && memberTypes.stream().anyMatch(type -> getRoleForMemberType(type) == RoleType.ADMIN); } /** Check if any of the member types maps to ADMIN or SUPER_ADMIN. */ - public static boolean isAdmin(List memberTypes) { - if (memberTypes == null) { - return false; - } - return memberTypes.stream() - .map(MemberTypeRoleMapper::getRoleForMemberType) - .anyMatch(role -> role == RoleType.SUPER_ADMIN || role == RoleType.ADMIN); + public static boolean isAdmin(final List memberTypes) { + return memberTypes != null + && memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .anyMatch(role -> role == RoleType.LEADER || role == RoleType.ADMIN); } /** Check if member has a specific role through any of their member types. */ - public static boolean hasRole(List memberTypes, RoleType targetRole) { - if (memberTypes == null || targetRole == null) { - return false; - } - return memberTypes.stream() - .map(MemberTypeRoleMapper::getRoleForMemberType) - .anyMatch(role -> role == targetRole); + public static boolean hasRole(final List memberTypes, final RoleType targetRole) { + return (memberTypes != null && targetRole != null) + && memberTypes.stream() + .map(MemberTypeRoleMapper::getRoleForMemberType) + .anyMatch(role -> role == targetRole); } } diff --git a/src/main/java/com/wcc/platform/domain/auth/Permission.java b/src/main/java/com/wcc/platform/domain/auth/Permission.java index e7035468..747b6f85 100644 --- a/src/main/java/com/wcc/platform/domain/auth/Permission.java +++ b/src/main/java/com/wcc/platform/domain/auth/Permission.java @@ -10,13 +10,13 @@ public enum Permission { USER_DELETE("user:delete", "Delete users"), // Mentor Operations - MENTOR_APPLICATION_READ("mentor:application:read", "View mentor applications"), - MENTOR_APPLICATION_WRITE("mentor:application:write", "Accept/decline mentee applications"), + MENTOR_APPL_READ("mentor:application:read", "View mentor applications"), + MENTOR_APPL_WRITE("mentor:application:write", "Accept/decline mentee applications"), MENTOR_PROFILE_UPDATE("mentor:profile:update", "Update mentor profile"), // Mentee Operations - MENTEE_APPLICATION_SUBMIT("mentee:application:submit", "Submit mentee applications"), - MENTEE_APPLICATION_READ("mentee:application:read", "View own application status"), + MENTEE_APPL_SUBMIT("mentee:application:submit", "Submit mentee applications"), + MENTEE_APPL_READ("mentee:application:read", "View own application status"), // Admin Operations MENTOR_APPROVE("admin:mentor:approve", "Approve/reject mentors"), diff --git a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java index 14ef732e..31942e18 100644 --- a/src/main/java/com/wcc/platform/domain/auth/UserAccount.java +++ b/src/main/java/com/wcc/platform/domain/auth/UserAccount.java @@ -78,8 +78,9 @@ public Set getAllMemberRoles() { * Get all roles including both: 1. Roles derived from member types 2. Roles explicitly assigned * in UserAccount */ + @SuppressWarnings("PMD.UseEnumCollections") public Set getAllRoles() { - Set allRoles = new HashSet<>(getAllMemberRoles()); + final Set allRoles = new HashSet<>(getAllMemberRoles()); if (userAccount.getRoles() != null) { allRoles.addAll(userAccount.getRoles()); } @@ -90,8 +91,9 @@ public Set getAllRoles() { * Get all permissions including those from: 1. All member types (union of permissions) 2. * Explicitly assigned roles in UserAccount */ + @SuppressWarnings("PMD.UseEnumCollections") public Set getAllPermissions() { - Set permissions = new HashSet<>(); + final Set permissions = new HashSet<>(); // Add permissions from member types if (member != null && member.getMemberTypes() != null) { @@ -105,8 +107,8 @@ public Set getAllPermissions() { } /** Check if user has any of the specified roles. */ - public boolean hasAnyRole(RoleType... roles) { - Set userRoles = getAllRoles(); + public boolean hasAnyRole(final RoleType... roles) { + final Set userRoles = getAllRoles(); return Arrays.stream(roles).anyMatch(userRoles::contains); } } diff --git a/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java b/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java index 009b03aa..9a222af0 100644 --- a/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java +++ b/src/main/java/com/wcc/platform/domain/exceptions/ForbiddenException.java @@ -2,7 +2,7 @@ public class ForbiddenException extends RuntimeException { - public ForbiddenException(String message) { + public ForbiddenException(final String message) { super(message); } } diff --git a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java index 9ef15f94..77e8d773 100644 --- a/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java +++ b/src/main/java/com/wcc/platform/domain/platform/type/RoleType.java @@ -9,27 +9,25 @@ @Getter @AllArgsConstructor public enum RoleType { - SUPER_ADMIN(1, "Platform Super Administrator", Set.of(Permission.values())), - ADMIN( + ADMIN(1, "Platform Administrator", Set.of(Permission.values())), + LEADER( 4, - "Platform Administrator", + "Platform Leader", Set.of( Permission.USER_READ, Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE, Permission.CYCLE_EMAIL_SEND, Permission.MATCH_MANAGE, - Permission.MENTOR_APPLICATION_READ)), + Permission.MENTOR_APPL_READ)), MENTEE( - 5, - "Mentee In Community", - Set.of(Permission.MENTEE_APPLICATION_SUBMIT, Permission.MENTEE_APPLICATION_READ)), + 5, "Mentee In Community", Set.of(Permission.MENTEE_APPL_SUBMIT, Permission.MENTEE_APPL_READ)), MENTOR( 6, "Mentor In Community", Set.of( - Permission.MENTOR_APPLICATION_READ, - Permission.MENTOR_APPLICATION_WRITE, + Permission.MENTOR_APPL_READ, + Permission.MENTOR_APPL_WRITE, Permission.MENTOR_PROFILE_UPDATE)), CONTRIBUTOR(2, "Contributor In Community", Set.of(Permission.USER_READ)), diff --git a/src/main/java/com/wcc/platform/service/AuthService.java b/src/main/java/com/wcc/platform/service/AuthService.java index cb9500d3..fbb7703d 100644 --- a/src/main/java/com/wcc/platform/service/AuthService.java +++ b/src/main/java/com/wcc/platform/service/AuthService.java @@ -29,6 +29,7 @@ */ @Service @RequiredArgsConstructor +@SuppressWarnings("PMD.TooManyMethods") public class AuthService { private static final SecureRandom RANDOM = new SecureRandom(); @@ -137,17 +138,17 @@ public Optional getUserWithMember(final Integer userId) { return Optional.empty(); } - Optional userAccountOpt = userAccountRepository.findById(userId); + final Optional userAccountOpt = userAccountRepository.findById(userId); if (userAccountOpt.isEmpty()) { return Optional.empty(); } - UserAccount userAccount = userAccountOpt.get(); + final UserAccount userAccount = userAccountOpt.get(); if (userAccount.getMemberId() == null) { return Optional.empty(); } - Optional memberOpt = memberRepository.findById(userAccount.getMemberId()); + final Optional memberOpt = memberRepository.findById(userAccount.getMemberId()); return memberOpt.map(member -> new UserAccount.User(userAccount, member)); } @@ -178,13 +179,13 @@ public Optional authenticateByTokenWithMember(final String tok * @throws ForbiddenException if user is not authenticated or principal is invalid */ public UserAccount.User getCurrentUser() { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authentication == null || !authentication.isAuthenticated()) { throw new ForbiddenException("User is not authenticated"); } - Object principal = authentication.getPrincipal(); + final Object principal = authentication.getPrincipal(); if (principal instanceof UserAccount.User) { return (UserAccount.User) principal; @@ -199,11 +200,11 @@ public UserAccount.User getCurrentUser() { * @param permissions the permissions (user needs at least one) * @throws ForbiddenException if user doesn't have any of the permissions */ - public void requireAnyPermission(Permission... permissions) { - UserAccount.User user = getCurrentUser(); - Set userPermissions = user.getAllPermissions(); + public void requireAnyPermission(final Permission... permissions) { + final UserAccount.User user = getCurrentUser(); + final Set userPermissions = user.getAllPermissions(); - boolean hasAny = Arrays.stream(permissions).anyMatch(userPermissions::contains); + final boolean hasAny = Arrays.stream(permissions).anyMatch(userPermissions::contains); if (!hasAny) { throw new ForbiddenException( @@ -217,11 +218,11 @@ public void requireAnyPermission(Permission... permissions) { * @param permissions the permissions (user needs all of them) * @throws ForbiddenException if user doesn't have all the permissions */ - public void requireAllPermissions(Permission... permissions) { - UserAccount.User user = getCurrentUser(); - Set userPermissions = user.getAllPermissions(); + public void requireAllPermissions(final Permission... permissions) { + final UserAccount.User user = getCurrentUser(); + final Set userPermissions = user.getAllPermissions(); - boolean hasAll = Arrays.stream(permissions).allMatch(userPermissions::contains); + final boolean hasAll = Arrays.stream(permissions).allMatch(userPermissions::contains); if (!hasAll) { throw new ForbiddenException( @@ -236,8 +237,8 @@ public void requireAllPermissions(Permission... permissions) { * @param allowedRoles the allowed roles (user needs one of them) * @throws ForbiddenException if user doesn't have any of the roles */ - public void requireRole(RoleType... allowedRoles) { - UserAccount.User user = getCurrentUser(); + public void requireRole(final RoleType... allowedRoles) { + final UserAccount.User user = getCurrentUser(); if (user.hasAnyRole(allowedRoles)) { return; diff --git a/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql b/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql index 71f95fe3..dee3abb9 100644 --- a/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql +++ b/src/main/resources/db/migration/V23__20260120__update_user_auth_tables.sql @@ -5,6 +5,26 @@ WHERE id = 2; -- Insert Role Types INSERT INTO role_types (id, name, description) -VALUES (5, 'MENTEE', 'Mentee In Community'), +VALUES (4, 'LEADER', 'Leader In Community'), + (5, 'MENTEE', 'Mentee In Community'), (6, 'MENTOR', 'Mentor In Community'), - (7, 'MEMBER', 'Member In Community'); \ No newline at end of file + (7, 'MEMBER', 'Member In Community'); + +-- Insert a member +INSERT INTO members (full_name, slack_name, position, company_name, email, city, country_id, + status_id, bio, years_experience, spoken_language) +values ('Sonali Goel', 'sonaligoel', 'Senior Software Engineer', 'Tesco Technology', + 'sonali.learn.ai@gmail.com', 'London', 234, 1, + 'Passionate about technology and community building.', 10, 'English, Hindi'); + +-- Insert user account for the new member +INSERT INTO user_accounts (member_id, email, password_hash, enabled) +VALUES ((SELECT id FROM members WHERE members.email = 'sonali.learn.ai@gmail.com'), + 'sonali.learn.ai@gmail.com', + '$argon2id$v=19$m=65536,t=3,p=1$49crq1CpGyILHW2LRfdpRg$mIaxgAa7ksupTF49pjkONlD2U3i48m2jmbeXeWvRJno', + TRUE); + +-- Link the new member to the LEADER role +INSERT INTO user_roles (user_id, role_id) +VALUES ((SELECT id FROM user_accounts WHERE email = 'sonali.learn.ai@gmail.com'), + (SELECT id FROM role_types WHERE name = 'LEADER')); \ No newline at end of file diff --git a/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java b/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java index 3da50670..5894b280 100644 --- a/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java +++ b/src/test/java/com/wcc/platform/configuration/TokenAuthFilterTest.java @@ -60,7 +60,7 @@ void givenValidBearerTokenWhenDoFilterInternalThenAuthenticationIsSet() Member.builder() .id(1L) .fullName("Admin WCC") - .memberTypes(List.of(MemberType.LEADER)) + .memberTypes(List.of(MemberType.DIRECTOR)) .build(); UserAccount.User user = new UserAccount.User(mockUser, member); diff --git a/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java b/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java index aada0789..cd0a1ff8 100644 --- a/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java +++ b/src/test/java/com/wcc/platform/domain/auth/MemberTypeRoleMapperTest.java @@ -15,9 +15,8 @@ class MemberTypeRoleMapperTest { @Test void getRoleForMemberType_returnsMappedRole() { - assertEquals( - RoleType.SUPER_ADMIN, MemberTypeRoleMapper.getRoleForMemberType(MemberType.DIRECTOR)); - assertEquals(RoleType.ADMIN, MemberTypeRoleMapper.getRoleForMemberType(MemberType.LEADER)); + assertEquals(RoleType.ADMIN, MemberTypeRoleMapper.getRoleForMemberType(MemberType.DIRECTOR)); + assertEquals(RoleType.LEADER, MemberTypeRoleMapper.getRoleForMemberType(MemberType.LEADER)); assertEquals(RoleType.MENTOR, MemberTypeRoleMapper.getRoleForMemberType(MemberType.MENTOR)); assertEquals(RoleType.MENTEE, MemberTypeRoleMapper.getRoleForMemberType(MemberType.MENTEE)); assertEquals( @@ -47,7 +46,7 @@ void getRolesForMemberTypes_collectsRoles() { Set roles = MemberTypeRoleMapper.getRolesForMemberTypes( List.of(MemberType.LEADER, MemberType.MENTEE, MemberType.SPEAKER)); - assertTrue(roles.contains(RoleType.ADMIN)); + assertTrue(roles.contains(RoleType.LEADER)); assertTrue(roles.contains(RoleType.MENTEE)); assertTrue(roles.contains(RoleType.CONTRIBUTOR)); } @@ -57,11 +56,11 @@ void getHighestRole_returnsMostPrivileged() { // Director maps to SUPER_ADMIN (highest), Member maps to VIEWER (low) RoleType highest = MemberTypeRoleMapper.getHighestRole(List.of(MemberType.MEMBER, MemberType.DIRECTOR)); - assertEquals(RoleType.SUPER_ADMIN, highest); + assertEquals(RoleType.ADMIN, highest); // Leader(Admin) vs Mentor -> ADMIN is higher in defined hierarchy highest = MemberTypeRoleMapper.getHighestRole(List.of(MemberType.MENTOR, MemberType.LEADER)); - assertEquals(RoleType.ADMIN, highest); + assertEquals(RoleType.LEADER, highest); } @Test @@ -101,9 +100,9 @@ void isAdmin_checksCorrectly() { void hasRole_checksCorrectly() { assertTrue( MemberTypeRoleMapper.hasRole( - List.of(MemberType.LEADER, MemberType.MEMBER), RoleType.ADMIN)); - assertFalse(MemberTypeRoleMapper.hasRole(List.of(MemberType.MEMBER), RoleType.ADMIN)); - assertFalse(MemberTypeRoleMapper.hasRole(null, RoleType.ADMIN)); + List.of(MemberType.LEADER, MemberType.MEMBER), RoleType.LEADER)); + assertFalse(MemberTypeRoleMapper.hasRole(List.of(MemberType.MEMBER), RoleType.LEADER)); + assertFalse(MemberTypeRoleMapper.hasRole(null, RoleType.LEADER)); assertFalse(MemberTypeRoleMapper.hasRole(List.of(MemberType.LEADER), null)); } } diff --git a/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java b/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java index b08ca420..de7d6059 100644 --- a/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java +++ b/src/test/java/com/wcc/platform/domain/auth/UserAccountTest.java @@ -90,7 +90,7 @@ void testGetPrimaryRole_memberWithMultipleTypes_returnsHighestPrivilegeRole() { UserAccount.User user = new UserAccount.User(userAccount, member); - assertEquals(RoleType.SUPER_ADMIN, user.getPrimaryRole()); + assertEquals(RoleType.ADMIN, user.getPrimaryRole()); } @Test @@ -160,14 +160,14 @@ void testGetAllRoles_onlyUserRoles_returnsOnlyUserRoles() { .id(1) .memberId(1L) .email("john@example.com") - .roles(List.of(RoleType.ADMIN)) + .roles(List.of(RoleType.LEADER)) .build(); UserAccount.User user = new UserAccount.User(userAccount, member); Set roles = user.getAllRoles(); assertEquals(2, roles.size()); - assertTrue(roles.contains(RoleType.ADMIN)); + assertTrue(roles.contains(RoleType.LEADER)); } @Test From d7bf7f92c95a7c239d34e465a8bb34299adaf6ef Mon Sep 17 00:00:00 2001 From: Sonali Goel Date: Thu, 29 Jan 2026 22:13:14 +0000 Subject: [PATCH 9/9] fixed PMD failure --- .../domain/auth/MemberTypeRoleMapper.java | 3 +- .../security/AuthorizationAspectTest.java | 120 ++++++++++++++++++ 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/wcc/platform/configuration/security/AuthorizationAspectTest.java diff --git a/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java index 3f29ee2c..1c4a152f 100644 --- a/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java +++ b/src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java @@ -113,7 +113,8 @@ public static boolean isAdmin(final List memberTypes) { /** Check if member has a specific role through any of their member types. */ public static boolean hasRole(final List memberTypes, final RoleType targetRole) { - return (memberTypes != null && targetRole != null) + return memberTypes != null + && targetRole != null && memberTypes.stream() .map(MemberTypeRoleMapper::getRoleForMemberType) .anyMatch(role -> role == targetRole); diff --git a/src/test/java/com/wcc/platform/configuration/security/AuthorizationAspectTest.java b/src/test/java/com/wcc/platform/configuration/security/AuthorizationAspectTest.java new file mode 100644 index 00000000..668b2cf4 --- /dev/null +++ b/src/test/java/com/wcc/platform/configuration/security/AuthorizationAspectTest.java @@ -0,0 +1,120 @@ +package com.wcc.platform.configuration.security; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.auth.Permission; +import com.wcc.platform.domain.exceptions.ForbiddenException; +import com.wcc.platform.domain.platform.type.RoleType; +import com.wcc.platform.service.AuthService; +import org.aspectj.lang.ProceedingJoinPoint; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class AuthorizationAspectTest { + + @Mock private AuthService authService; + @Mock private ProceedingJoinPoint joinPoint; + @Mock private RequiresPermission requiresPermission; + @Mock private RequiresRole requiresRole; + + private AuthorizationAspect authorizationAspect; + + @BeforeEach + void setUp() { + authorizationAspect = new AuthorizationAspect(authService); + } + + @Test + void testCheckPermission_andOperator_allPermissionsGranted_proceedsWithMethod() throws Throwable { + Permission[] permissions = {Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE}; + when(requiresPermission.value()).thenReturn(permissions); + when(requiresPermission.operator()).thenReturn(LogicalOperator.AND); + when(joinPoint.proceed()).thenReturn("success"); + + Object result = authorizationAspect.checkPermission(joinPoint, requiresPermission); + + assertEquals("success", result); + verify(authService).requireAllPermissions(permissions); + verify(joinPoint).proceed(); + } + + @Test + void testCheckPermission_andOperator_permissionDenied_throwsForbiddenException() + throws Throwable { + Permission[] permissions = {Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE}; + when(requiresPermission.value()).thenReturn(permissions); + when(requiresPermission.operator()).thenReturn(LogicalOperator.AND); + doThrow(new ForbiddenException("Permission denied")) + .when(authService) + .requireAllPermissions(permissions); + + assertThrows( + ForbiddenException.class, + () -> authorizationAspect.checkPermission(joinPoint, requiresPermission)); + + verify(authService).requireAllPermissions(permissions); + verify(joinPoint, never()).proceed(); + } + + @Test + void testCheckPermission_orOperator_anyPermissionGranted_proceedsWithMethod() throws Throwable { + Permission[] permissions = {Permission.MENTOR_APPROVE, Permission.MENTEE_APPROVE}; + when(requiresPermission.value()).thenReturn(permissions); + when(requiresPermission.operator()).thenReturn(LogicalOperator.OR); + when(joinPoint.proceed()).thenReturn("success"); + + Object result = authorizationAspect.checkPermission(joinPoint, requiresPermission); + + assertEquals("success", result); + verify(authService).requireAnyPermission(permissions); + verify(joinPoint).proceed(); + } + + @Test + void testCheckRole_singleRoleGranted_proceedsWithMethod() throws Throwable { + RoleType[] roles = {RoleType.ADMIN}; + when(requiresRole.value()).thenReturn(roles); + when(joinPoint.proceed()).thenReturn("success"); + + Object result = authorizationAspect.checkRole(joinPoint, requiresRole); + + assertEquals("success", result); + verify(authService).requireRole(roles); + verify(joinPoint).proceed(); + } + + @Test + void testCheckRole_multipleRolesGranted_proceedsWithMethod() throws Throwable { + RoleType[] roles = {RoleType.ADMIN, RoleType.LEADER, RoleType.MENTOR}; + when(requiresRole.value()).thenReturn(roles); + when(joinPoint.proceed()).thenReturn("success"); + + Object result = authorizationAspect.checkRole(joinPoint, requiresRole); + + assertEquals("success", result); + verify(authService).requireRole(roles); + verify(joinPoint).proceed(); + } + + @Test + void testCheckRole_roleDenied_throwsForbiddenException() throws Throwable { + RoleType[] roles = {RoleType.ADMIN}; + when(requiresRole.value()).thenReturn(roles); + doThrow(new ForbiddenException("Role not granted")).when(authService).requireRole(roles); + + assertThrows( + ForbiddenException.class, () -> authorizationAspect.checkRole(joinPoint, requiresRole)); + + verify(authService).requireRole(roles); + verify(joinPoint, never()).proceed(); + } +}