Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -129,4 +130,15 @@ public ResponseEntity<ErrorDetails> 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<ErrorDetails> handleForbiddenException(
ForbiddenException ex, final WebRequest request) {
var errorResponse =
new ErrorDetails(
HttpStatus.FORBIDDEN.value(), ex.getMessage(), request.getDescription(false));

return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}
}
28 changes: 13 additions & 15 deletions src/main/java/com/wcc/platform/configuration/TokenAuthFilter.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -61,21 +64,16 @@ 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<User> 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, null, authorities);

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}

filterChain.doFilter(request, response);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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 lombok.AllArgsConstructor;
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)
@AllArgsConstructor
public class AuthorizationAspect {

private final 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();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.wcc.platform.configuration.security;

public enum LogicalOperator {
AND,
OR
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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();

LogicalOperator operator() default LogicalOperator.AND;
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -36,7 +39,7 @@
public class MentorshipApplicationController {

private final MenteeWorkflowService applicationService;

/**
* API to get all applications submitted by a mentee for a specific cycle.
*
Expand Down Expand Up @@ -81,6 +84,9 @@ public ResponseEntity<MenteeApplication> 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<List<MenteeApplication>> getMentorApplications(
Expand Down
127 changes: 127 additions & 0 deletions src/main/java/com/wcc/platform/domain/auth/MemberTypeRoleMapper.java
Original file line number Diff line number Diff line change
@@ -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<MemberType, RoleType> 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<RoleType, Integer> 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<RoleType> getRolesForMemberTypes(List<MemberType> 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<MemberType> 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<Permission> getAllPermissionsForMemberTypes(List<MemberType> 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<MemberType> 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<MemberType> 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<MemberType> memberTypes, RoleType targetRole) {
if (memberTypes == null || targetRole == null) {
return false;
}
return memberTypes.stream()
.map(MemberTypeRoleMapper::getRoleForMemberType)
.anyMatch(role -> role == targetRole);
}
}
29 changes: 29 additions & 0 deletions src/main/java/com/wcc/platform/domain/auth/Permission.java
Original file line number Diff line number Diff line change
@@ -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;
}
Loading
Loading