Skip to content

Commit 9a724e9

Browse files
sync permissions from M2M
1 parent 717f96a commit 9a724e9

File tree

9 files changed

+268
-45
lines changed

9 files changed

+268
-45
lines changed

.igrpstudio/shared/dto/PermissionDTO.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"name": "departmentCode",
6262
"objectType": "java",
6363
"type": "string",
64-
"required": true,
64+
"required": false,
6565
"before": false,
6666
"after": false,
6767
"positive": false,

src/main/java/cv/igrp/platform/access_management/m2m/domain/service/PermissionSyncService.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
import cv.igrp.platform.access_management.shared.application.dto.PermissionDTO;
66
import cv.igrp.platform.access_management.shared.domain.exceptions.IgrpResponseStatusException;
77
import cv.igrp.platform.access_management.shared.infrastructure.persistence.entity.PermissionEntity;
8+
import cv.igrp.platform.access_management.shared.infrastructure.persistence.entity.ResourceEntity;
89
import cv.igrp.platform.access_management.shared.infrastructure.persistence.repository.PermissionEntityRepository;
10+
import cv.igrp.platform.access_management.shared.infrastructure.persistence.repository.ResourceEntityRepository;
11+
import cv.igrp.platform.access_management.shared.security.AuthenticationHelper;
912
import org.apache.commons.codec.digest.DigestUtils;
1013
import org.slf4j.Logger;
1114
import org.slf4j.LoggerFactory;
@@ -26,11 +29,15 @@ public class PermissionSyncService {
2629

2730
private static final Logger LOGGER = LoggerFactory.getLogger(PermissionSyncService.class);
2831

32+
private final AuthenticationHelper authenticationHelper;
2933
private final PermissionEntityRepository permissionRepository;
34+
private final ResourceEntityRepository resourceEntityRepository;
3035
private final ObjectMapper objectMapper;
3136

32-
public PermissionSyncService(PermissionEntityRepository permissionRepository, ObjectMapper objectMapper) {
37+
public PermissionSyncService(PermissionEntityRepository permissionRepository, ResourceEntityRepository resourceEntityRepository, ObjectMapper objectMapper, AuthenticationHelper authenticationHelper) {
38+
this.authenticationHelper = authenticationHelper;
3339
this.permissionRepository = permissionRepository;
40+
this.resourceEntityRepository = resourceEntityRepository;
3441
this.objectMapper = objectMapper;
3542
}
3643

@@ -68,8 +75,15 @@ public void synchronizePermissions(List<PermissionDTO> permissions) {
6875
);
6976
}
7077

71-
// Get all existing permissions
72-
List<PermissionEntity> existingPermissions = permissionRepository.findAll();
78+
// Get all existing permissions for the current resource
79+
ResourceEntity resource = resourceEntityRepository.findByNameAndStatusNot(authenticationHelper.getPreferredUsername(), Status.DELETED)
80+
.orElseThrow(() -> IgrpResponseStatusException.notFound("Resource not found", "Resource with name: " + authenticationHelper.getPreferredUsername() + " not found."));
81+
82+
Set<ResourceEntity> resourceEntities = new HashSet<>();
83+
resourceEntities.add(resource);
84+
85+
List<PermissionEntity> existingPermissions = permissionRepository.findAllByResourcesAndStatusNot(resourceEntities, Status.DELETED);
86+
7387
Map<String, PermissionEntity> existingByName = existingPermissions.stream()
7488
.collect(Collectors.toMap(
7589
p -> p.getName().toLowerCase(Locale.ROOT),
@@ -91,7 +105,9 @@ public void synchronizePermissions(List<PermissionDTO> permissions) {
91105
newPerm.setName(dto.getName());
92106
newPerm.setDescription(dto.getDescription());
93107
newPerm.setStatus(dto.getStatus() != null ? dto.getStatus() : Status.ACTIVE);
94-
permissionRepository.save(newPerm);
108+
PermissionEntity savedPerm = permissionRepository.save(newPerm);
109+
resource.getPermissions().add(savedPerm);
110+
resourceEntityRepository.save(resource);
95111
LOGGER.info("[PermissionSync] Created new permission '{}'", dto.getName());
96112
} else {
97113
// Check for difference using structural hash
@@ -116,7 +132,8 @@ public void synchronizePermissions(List<PermissionDTO> permissions) {
116132

117133
if (!toDelete.isEmpty()) {
118134
for (PermissionEntity perm : toDelete) {
119-
permissionRepository.delete(perm);
135+
perm.setStatus(Status.DELETED);
136+
permissionRepository.save(perm);
120137
LOGGER.info("[PermissionSync] Deleted permission '{}'", perm.getName());
121138
}
122139
}

src/main/java/cv/igrp/platform/access_management/permission/application/commands/CreatePermissionCommandHandler.java

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,18 @@ public CreatePermissionCommandHandler(PermissionEntityRepository repository, Dep
8383
@Transactional
8484
public ResponseEntity<PermissionDTO> handle(CreatePermissionCommand command) {
8585
PermissionDTO request = command.getPermissiondto();
86-
DepartmentEntity foundDepartment = departmentRepository.findByCodeAndStatusNot(command.getPermissiondto().getDepartmentCode(), DepartmentStatus.DELETED)
87-
.orElseThrow(() -> {
88-
log.warn("Department with code {} not found.", command.getPermissiondto().getDepartmentCode());
89-
return IgrpResponseStatusException.of(
90-
HttpStatus.NOT_FOUND, "Create Permission", "Department with code: " + command.getPermissiondto().getDepartmentCode() + " not found."
91-
);
92-
});
86+
DepartmentEntity foundDepartment = null;
87+
88+
if(command.getPermissiondto().getDepartmentCode() != null && !command.getPermissiondto().getDepartmentCode().isEmpty()) {
89+
log.info("Finding Department with code: {}", command.getPermissiondto().getDepartmentCode());
90+
foundDepartment = departmentRepository.findByCodeAndStatusNot(command.getPermissiondto().getDepartmentCode(), DepartmentStatus.DELETED)
91+
.orElseThrow(() -> {
92+
log.warn("Department with code {} not found.", command.getPermissiondto().getDepartmentCode());
93+
return IgrpResponseStatusException.of(
94+
HttpStatus.NOT_FOUND, "Create Permission", "Department with code: " + command.getPermissiondto().getDepartmentCode() + " not found."
95+
);
96+
});
97+
}
9398

9499
command.getPermissiondto().setName(command.getPermissiondto().getName());
95100

src/main/java/cv/igrp/platform/access_management/shared/application/dto/PermissionDTO.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ public class PermissionDTO {
2222

2323

2424
private Integer id ;
25-
@NotBlank(message = "The field <name> is required")
26-
@Size(max = 60, message = "The field length <name> cannot be more than 60 characters")
25+
@NotBlank(message = "The field <name> is required")
26+
@Size(max = 60, message = "The field length <name> cannot be more than 60 characters")
2727
@Pattern(message = "Invalid value format for field <name>.", regexp = "^[A-Za-z0-9_-]+$")
2828

2929
private String name ;
@@ -33,7 +33,7 @@ public class PermissionDTO {
3333

3434

3535
private Status status ;
36-
@NotBlank(message = "The field <departmentCode> is required")
36+
3737

3838
private String departmentCode ;
3939

src/main/java/cv/igrp/platform/access_management/shared/infrastructure/persistence/repository/PermissionEntityRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import cv.igrp.platform.access_management.shared.application.constants.Status;
44
import cv.igrp.platform.access_management.shared.infrastructure.persistence.entity.MenuEntryEntity;
55
import cv.igrp.platform.access_management.shared.infrastructure.persistence.entity.PermissionEntity;
6+
import cv.igrp.platform.access_management.shared.infrastructure.persistence.entity.ResourceEntity;
67
import org.springframework.data.jpa.repository.Query;
78
import org.springframework.data.repository.query.Param;
89
import org.springframework.stereotype.Repository;
@@ -11,6 +12,7 @@
1112

1213
import java.util.List;
1314
import java.util.Optional;
15+
import java.util.Set;
1416

1517
import org.springframework.data.repository.history.RevisionRepository;
1618

@@ -82,5 +84,6 @@ AND p.id NOT IN (
8284
""")
8385
List<PermissionEntity> findAvailablePermissionsForRole(@Param("name") String name);
8486

87+
List<PermissionEntity> findAllByResourcesAndStatusNot(Set<ResourceEntity> resources, Status status);
8588

8689
}

src/main/java/cv/igrp/platform/access_management/shared/security/AuthenticationHelper.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import org.springframework.security.core.Authentication;
44
import org.springframework.security.core.context.SecurityContextHolder;
5+
import org.springframework.security.core.userdetails.User;
56
import org.springframework.security.oauth2.jwt.Jwt;
67
import org.springframework.stereotype.Component;
78

@@ -13,17 +14,33 @@
1314
public class AuthenticationHelper {
1415

1516
/**
16-
* Retrieves the preferred username from the JWT token in the security context.
17+
* Retrieves the username from the current SecurityContext.
18+
* <p>
19+
* Supports both JWT-based authentication (standard OAuth2 users)
20+
* and machine-to-machine (M2M) authentication
1721
*
18-
* @return preferred username
19-
* @throws IllegalStateException if the JWT token is not found in the security context
22+
* @return the username or client ID depending on the authentication type
23+
* @throws IllegalStateException if no authentication is found
2024
*/
2125
public String getPreferredUsername() {
2226
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
23-
if (authentication != null && authentication.getPrincipal() instanceof Jwt jwt) {
27+
28+
if (authentication == null) {
29+
throw new IllegalStateException("No authentication found in security context");
30+
}
31+
32+
// Case 1: JWT (OAuth2 user)
33+
if (authentication.getPrincipal() instanceof Jwt jwt) {
2434
return jwt.getClaimAsString("preferred_username");
2535
}
26-
throw new IllegalStateException("JWT token not found in security context");
36+
37+
// Case 2: M2M authentication (UsernamePasswordAuthenticationToken)
38+
if (authentication.getPrincipal() instanceof User user) {
39+
return user.getUsername();
40+
}
41+
42+
// Case 3: Fallback (any other type)
43+
return authentication.getName();
2744
}
2845

2946
/**

src/main/java/cv/igrp/platform/access_management/shared/security/BasicAuthSecurityConfiguration.java

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,32 @@
11
package cv.igrp.platform.access_management.shared.security;
22

3+
import jakarta.servlet.FilterChain;
4+
import jakarta.servlet.ServletException;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import org.slf4j.Logger;
8+
import org.slf4j.LoggerFactory;
9+
import org.springframework.beans.factory.annotation.Value;
310
import org.springframework.context.annotation.Bean;
411
import org.springframework.context.annotation.Configuration;
512
import org.springframework.context.annotation.Profile;
13+
import org.springframework.core.annotation.Order;
14+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
615
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
716
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
817
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
918
import org.springframework.security.config.http.SessionCreationPolicy;
19+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
20+
import org.springframework.security.core.context.SecurityContextHolder;
21+
import org.springframework.security.core.userdetails.User;
1022
import org.springframework.security.web.SecurityFilterChain;
23+
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
24+
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
1125
import org.springframework.web.cors.CorsConfiguration;
26+
import org.springframework.web.filter.OncePerRequestFilter;
27+
28+
import java.io.IOException;
29+
import java.util.Collections;
1230

1331
import static org.springframework.security.config.Customizer.withDefaults;
1432

@@ -17,14 +35,83 @@
1735
@Profile("basic-auth") // Only active if 'basic-auth' profile is enabled
1836
public class BasicAuthSecurityConfiguration {
1937

38+
private static final Logger log = LoggerFactory.getLogger(BasicAuthSecurityConfiguration.class);
39+
40+
@Value("${igrp.access.m2m.sync-token:}")
41+
private String machineAuthToken;
42+
43+
/**
44+
* Filter that authenticates M2M requests using a static token header.
45+
*/
46+
private class MachineAuthFilter extends OncePerRequestFilter {
47+
@Override
48+
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
49+
throws ServletException, IOException {
50+
51+
if (!request.getRequestURI().startsWith("/api/m2m/")) {
52+
filterChain.doFilter(request, response);
53+
return;
54+
}
55+
56+
String client = request.getHeader("X-Machine-Service-ID");
57+
String header = request.getHeader("X-Machine-Auth-Token");
58+
59+
if (header == null || !header.equals(machineAuthToken)) {
60+
log.warn("[M2M] Unauthorized access: missing or invalid authentication");
61+
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
62+
response.getWriter().write("Unauthorized: Invalid or missing machine-to-machine authentication token.");
63+
return;
64+
}
65+
66+
var authority = new SimpleGrantedAuthority("ROLE_M2M");
67+
var principal = new User(
68+
(client != null && !client.isBlank()) ? client : "m2m-client",
69+
"N/A",
70+
Collections.singletonList(authority)
71+
);
72+
var authentication = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
73+
74+
var context = SecurityContextHolder.createEmptyContext();
75+
context.setAuthentication(authentication);
76+
SecurityContextHolder.setContext(context);
77+
78+
// Persist the context
79+
new RequestAttributeSecurityContextRepository().saveContext(context, request, response);
80+
81+
log.info("[M2M] Authenticated machine client: {}", principal.getUsername());
82+
83+
filterChain.doFilter(request, response);
84+
}
85+
}
86+
87+
// --- Security chain 1: machine-to-machine endpoints ---
88+
@Bean
89+
@Order(1)
90+
public SecurityFilterChain m2mSecurityFilterChain(HttpSecurity http) throws Exception {
91+
var securityContextRepository = new RequestAttributeSecurityContextRepository();
92+
93+
http.securityMatcher("/api/m2m/**")
94+
.csrf(AbstractHttpConfigurer::disable)
95+
.securityContext(ctx -> ctx
96+
.securityContextRepository(securityContextRepository)
97+
)
98+
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
99+
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
100+
.addFilterBefore(new BasicAuthSecurityConfiguration.MachineAuthFilter(), UsernamePasswordAuthenticationFilter.class);
101+
102+
return http.build();
103+
}
104+
105+
// --- Security chain 2: Basic Auth for other endpoints ---
20106
@Bean
107+
@Order(2)
21108
public SecurityFilterChain basicAuthFilterChain(HttpSecurity http) throws Exception {
22109

23110
http.csrf(AbstractHttpConfigurer::disable);
24111

25112
http.authorizeHttpRequests(auth -> auth
26113
.requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html",
27-
"/swagger-resources/**", "/webjars/**", "/actuator/**").permitAll()
114+
"/swagger-resources/**", "/webjars/**", "/actuator/**", "/api/m2m/**").permitAll()
28115
.anyRequest().authenticated()
29116
)
30117
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

0 commit comments

Comments
 (0)