Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
e3c612e
feat(core): add jit principal generation
RVANDO12 Jul 30, 2026
624e10f
feat(core): copilot review 1 and soner qube coverage
RVANDO12 Jul 30, 2026
be7e530
feat(core): fix sonar issue
RVANDO12 Jul 30, 2026
3827709
feat(core): fix precommit
RVANDO12 Jul 30, 2026
e2e2a19
feat(core): fix last issue
RVANDO12 Jul 30, 2026
3c2966f
feat(core): fix parallel deployment
RVANDO12 Jul 31, 2026
4617e96
feat(core): fix parallel deployment - precommit issue
RVANDO12 Jul 31, 2026
1624c2c
feat(core): human check fix
RVANDO12 Jul 31, 2026
f61294c
feat(core): human check fix
RVANDO12 Jul 31, 2026
7326cca
Merge branch 'main' into feat/right-management
RVANDO12 Aug 3, 2026
d01d30e
Merge branch 'main' into feat/right-management
RVANDO12 Aug 3, 2026
cf680d3
Merge branch 'main' into feat/right-management
RVANDO12 Aug 4, 2026
9a1283e
Merge branch 'main' into feat/right-management
RVANDO12 Aug 6, 2026
819b995
Merge branch 'main' into feat/right-management
RVANDO12 Aug 10, 2026
8c2c431
feat(core): modify how to manage auth link to principal management
RVANDO12 Aug 11, 2026
9731c40
feat(core): fix sonar issue
RVANDO12 Aug 11, 2026
13e9f10
feat(core): fix vale issue
RVANDO12 Aug 11, 2026
b934ca8
Merge branch 'main' into feat/right-management
RVANDO12 Aug 11, 2026
8ac62d3
feat(core): fix vale issue
RVANDO12 Aug 11, 2026
00e8857
feat(core): first reveiw fix
RVANDO12 Aug 13, 2026
f3ccb83
feat(core): first reveiw fix
RVANDO12 Aug 13, 2026
4c0bdaf
feat(core): first reveiw fix
RVANDO12 Aug 13, 2026
8ca575d
feat(core): refacto sonar qube suggestion
RVANDO12 Aug 13, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ public class ValidationMessages {
public static final String ENTITY_VALIDATION_FAILED = "Entity validation failed: ";
public static final String ENTITY_DELETION_BLOCKED = "Cannot delete entity '%s' (template: '%s') because it is referenced by required relations in the following entities: %s. Please update the relation definitions to make them optional or remove the required constraint before deleting this entity.";

// Principal provisioning validation messages
public static final String PRINCIPAL_CREATION_FAILED = "Failed to create principal with identifier '%s'. The principal provider is currently unavailable. The system will automatically retry provisioning on the next request.";

// Helper method to construct rules incompatibility message
public static String rulesAreIncompatible(String rule1, String rule2) {
return PROPERTY_RULES_MUTUALLY_EXCLUSIVE.replace("{rule1}", rule1).replace("{rule2}", rule2);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.decathlon.idp_core.domain.exception.principal;

import static com.decathlon.idp_core.domain.constant.ValidationMessages.PRINCIPAL_CREATION_FAILED;

/// Custom exception indicating that principal creation failed due to a transient provider issue.
///
/// **Business purpose:** Represents a transient failure in principal provisioning that does
/// not block IDP Core usage. The system will automatically retry creation on the next request
/// when the principal is accessed (Just-In-Time provisioning retry strategy).
public class PrincipalCreationException extends RuntimeException {

/// Constructs a new exception with the principal identifier.
///
/// @param principalIdentifier the identifier of the principal that failed to be
/// created
public PrincipalCreationException(String principalIdentifier) {
super(String.format(PRINCIPAL_CREATION_FAILED, principalIdentifier));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.decathlon.idp_core.domain.exception.principal;

/// Custom exception indicating that a principal was not found in the system.
///
/// **Business purpose:** Represents the business rule violation when attempting
/// to access a principal that doesn't exist in the catalog. This exception is
/// thrown to signal that the requested principal could not be located, which may
/// indicate a failure in Just-In-Time (JIT) provisioning or an invalid identifier.
public class PrincipalNotFoundException extends RuntimeException {

/// Custom exception for principal not found scenarios.
///
/// **Design rationale:** Specific exception enables tailored HTTP status
/// mapping
/// in ApiExceptionHandler (404 instead of generic 500).
public PrincipalNotFoundException(String identifier) {
super("Principal with identifier '" + identifier
+ "' not found in catalog. JIT provisioning may have failed.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.decathlon.idp_core.domain.model.principal;

import java.util.List;
import java.util.Map;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

/// Domain model representing the identity and attributes of an authenticated principal.
///
/// **Business invariants:**
/// - [identifier] must be unique and non-blank (subject for humans, client_id for services)
/// - [kind] determines how claims are interpreted
/// - [name] provides a human-readable label for both humans and service accounts
///
/// **Ubiquitous language:** A Principal is any authenticated actor (human or machine)
/// that can interact with the IDP-Core API. This model carries the essential identity
/// information extracted from the authentication context.
public record PrincipalInfo(@NotBlank String identifier, @NotNull PrincipalKind kind,
@NotBlank String name, Map<String, String> attributes, List<String> groups) {

public PrincipalInfo {
attributes = attributes != null ? Map.copyOf(attributes) : Map.of();
groups = groups != null ? List.copyOf(groups) : List.of();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.decathlon.idp_core.domain.model.principal;

/// Enumeration defining the type of principal (actor) in the system.
///
/// **Business meaning:**
/// - HUMAN: A human user authenticated via OAuth2/OIDC
/// - SERVICE_ACCOUNT: A machine client (webhook, API connector, service token)
public enum PrincipalKind {
HUMAN, SERVICE_ACCOUNT
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,7 @@ PaginatedResult<Entity> search(SearchFilterNode filter, String query,

void deleteByTemplateIdentifierAndIdentifier(String templateIdentifier, String entityIdentifier);

List<Entity> findAllByTemplateIdentifierAndIdentifierIn(String templateIdentifier,
List<String> identifiers);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package com.decathlon.idp_core.domain.service.principal;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.decathlon.idp_core.domain.exception.entity.EntityAlreadyExistsException;
import com.decathlon.idp_core.domain.exception.principal.PrincipalCreationException;
import com.decathlon.idp_core.domain.model.entity.Entity;
import com.decathlon.idp_core.domain.model.entity.Property;
import com.decathlon.idp_core.domain.model.entity.Relation;
import com.decathlon.idp_core.domain.model.principal.PrincipalInfo;
import com.decathlon.idp_core.domain.port.EntityRepositoryPort;

import lombok.RequiredArgsConstructor;

/// Domain service orchestrating Just-In-Time (JIT) provisioning of Principal entities.
///
/// **Business purpose:** Ensures every authenticated principal (human or service account)
/// has a corresponding Entity in the catalog with template_identifier="principal".
/// This enables unified identity management and audit tracking across the platform.
///
/// **Key responsibilities:**
/// - Create Principal entities on first authentication
/// - Map authentication claims to entity properties and relations
/// - Maintain referential integrity with team/group entities
///
/// **Design rationale:** Separates authentication (infrastructure concern) from
/// identity management (business concern). The authentication layer extracts PrincipalInfo,
/// this service persists it as domain entities.
@Service
@RequiredArgsConstructor
public class PrincipalProvisioningService {

private static final String PRINCIPAL_TEMPLATE_IDENTIFIER = "principal";

private final EntityRepositoryPort entityRepository;

/// Provisions a Principal entity based on authentication information.
///
/// **Contract:** Performs JIT provisioning:
/// - If principal does not exist: Creates new Principal entity
/// - If principal exists: Returns it without modification (updates handled by
/// separate endpoint)
/// - Returns the provisioned Principal entity
///
/// **Thread-safety:** Uses database constraints to handle concurrent first-time
/// logins.
/// If two requests race to create the same principal, one will succeed and the
/// other
/// will return the existing principal.
///
/// @param principalInfo extracted authentication information
/// @return the provisioned Principal entity
@Transactional
public Entity provisionPrincipal(PrincipalInfo principalInfo) {
Optional<Entity> existingPrincipal = entityRepository.findByTemplateIdentifierAndIdentifier(
PRINCIPAL_TEMPLATE_IDENTIFIER, principalInfo.identifier());

if (existingPrincipal.isPresent()) {
return existingPrincipal.get();
}

return createNewPrincipal(principalInfo);
}

/// Retrieves a Principal entity by its identifier.
///
/// **Contract:** Returns the Principal entity if it exists, empty otherwise.
///
/// @param identifier unique principal identifier
/// @return optional containing the principal entity
@Transactional(readOnly = true)
public Optional<Entity> getPrincipal(String identifier) {
return entityRepository.findByTemplateIdentifierAndIdentifier(PRINCIPAL_TEMPLATE_IDENTIFIER,
identifier);
}

/// Creates a new Principal entity in the catalog.
///
/// **Business logic:** Maps PrincipalInfo to entity properties and relations.
/// If a concurrent creation occurs (race condition during JIT provisioning),
/// the existing entity is returned instead.
///
/// @param principalInfo extracted authentication information
/// @return the newly created or existing Principal entity
private Entity createNewPrincipal(PrincipalInfo principalInfo) {
Entity newPrincipal = new Entity(null, PRINCIPAL_TEMPLATE_IDENTIFIER, principalInfo.name(),
principalInfo.identifier(), buildProperties(principalInfo), buildRelations(principalInfo));
try {
return entityRepository.save(newPrincipal);
} catch (EntityAlreadyExistsException _) {
return entityRepository
.findByTemplateIdentifierAndIdentifier(PRINCIPAL_TEMPLATE_IDENTIFIER,
principalInfo.identifier())
.orElseThrow(() -> new PrincipalCreationException(
"Principal concurrent creation detected but subsequent read failed for identifier: "
+ principalInfo.identifier()));
}
Comment thread
RVANDO12 marked this conversation as resolved.
}

/// Builds properties for the principal entity based on the provided
/// PrincipalInfo.
///
/// **Business logic:** Includes the principal kind and any non-blank
/// attributes.
///
/// @param principalInfo the principal information containing attributes
/// @return list of properties for the principal entity
private List<Property> buildProperties(PrincipalInfo principalInfo) {
List<Property> properties = new ArrayList<>();
properties.add(new Property(null, "kind", principalInfo.kind().name()));

principalInfo.attributes().forEach((key, value) -> {
if (value != null && !value.isBlank()) {
properties.add(new Property(null, key, value));
}
});

return properties;
}

/// Builds relations for the principal based on group memberships.
///
/// **Business logic:** Only includes relations to existing teams. If a group
/// does not correspond to a team entity, it is ignored.
///
/// @param principalInfo the principal information containing group memberships
/// @return list of relations to existing teams
private List<Relation> buildRelations(PrincipalInfo principalInfo) {
if (principalInfo.groups().isEmpty()) {
return List.of();
}

// Batch query to find all existing teams in one DB round-trip
List<String> validGroups = entityRepository
.findAllByTemplateIdentifierAndIdentifierIn("team", principalInfo.groups()).stream()
.map(Entity::identifier).toList();

if (validGroups.isEmpty()) {
return List.of();
}

return List.of(new Relation(null, "member_of", "team", validGroups));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.decathlon.idp_core.infrastructure.adapters.api.auth;

import java.io.IOException;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.springframework.lang.NonNull;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import com.decathlon.idp_core.domain.model.principal.PrincipalInfo;
import com.decathlon.idp_core.domain.service.principal.PrincipalProvisioningService;
import com.decathlon.idp_core.infrastructure.adapters.api.principal.PrincipalExtractor;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

/// Infrastructure filter that triggers Just-In-Time (JIT) provisioning of principals.
///
/// **Purpose:** Intercepts every authenticated request to ensure the authenticated
/// principal has a corresponding Entity in the catalog. This enables:
/// - Automatic user onboarding (no manual account creation)
/// - Real-time profile synchronization from identity provider
/// - Unified identity management for humans and service accounts
///
/// **Design rationale:** Positioned after Spring Security authentication but before
/// controllers, ensuring JIT provisioning happens transparently for all API endpoints.
/// Failures are logged but don't block the request (fail-open for availability).
@Slf4j
@Component
@RequiredArgsConstructor
public class JitProvisioningFilter extends OncePerRequestFilter {

private final PrincipalExtractor principalExtractor;
private final PrincipalProvisioningService provisioningService;

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response, @NonNull FilterChain filterChain)
throws ServletException, IOException {

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null && authentication.isAuthenticated()
&& !isAnonymous(authentication)) {
provisionPrincipalSafely(authentication);
}

filterChain.doFilter(request, response);
}

private void provisionPrincipalSafely(Authentication authentication) {
Comment thread
RVANDO12 marked this conversation as resolved.
try {
PrincipalInfo principalInfo = principalExtractor.extractPrincipalInfo(authentication);
provisioningService.provisionPrincipal(principalInfo);

log.debug("JIT provisioning successful for principal: {} (kind: {})",
principalInfo.identifier(), principalInfo.kind());
} catch (Exception e) {
// Log error but don't block the request - fail-open for availability
// The principal may not have a catalog entry, but can still authenticate
log.warn("JIT provisioning failed for authenticated principal: {}", e.getMessage(), e);
}
}

private boolean isAnonymous(Authentication authentication) {
return authentication instanceof org.springframework.security.authentication.AnonymousAuthenticationToken;
}

@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) {
Comment thread
RVANDO12 marked this conversation as resolved.
String path = request.getRequestURI();
// Skip JIT provisioning for public endpoints
return path.startsWith("/actuator/") || path.startsWith("/swagger-ui/")
|| path.startsWith("/v3/api-docs/") || path.equals("/");
}
}
Loading
Loading