-
Notifications
You must be signed in to change notification settings - Fork 0
feat(core): add jit principal generation #105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
RVANDO12
wants to merge
23
commits into
main
Choose a base branch
from
feat/right-management
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
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 624e10f
feat(core): copilot review 1 and soner qube coverage
RVANDO12 be7e530
feat(core): fix sonar issue
RVANDO12 3827709
feat(core): fix precommit
RVANDO12 e2e2a19
feat(core): fix last issue
RVANDO12 3c2966f
feat(core): fix parallel deployment
RVANDO12 4617e96
feat(core): fix parallel deployment - precommit issue
RVANDO12 1624c2c
feat(core): human check fix
RVANDO12 f61294c
feat(core): human check fix
RVANDO12 7326cca
Merge branch 'main' into feat/right-management
RVANDO12 d01d30e
Merge branch 'main' into feat/right-management
RVANDO12 cf680d3
Merge branch 'main' into feat/right-management
RVANDO12 9a1283e
Merge branch 'main' into feat/right-management
RVANDO12 819b995
Merge branch 'main' into feat/right-management
RVANDO12 8c2c431
feat(core): modify how to manage auth link to principal management
RVANDO12 9731c40
feat(core): fix sonar issue
RVANDO12 13e9f10
feat(core): fix vale issue
RVANDO12 b934ca8
Merge branch 'main' into feat/right-management
RVANDO12 8ac62d3
feat(core): fix vale issue
RVANDO12 00e8857
feat(core): first reveiw fix
RVANDO12 f3ccb83
feat(core): first reveiw fix
RVANDO12 4c0bdaf
feat(core): first reveiw fix
RVANDO12 8ca575d
feat(core): refacto sonar qube suggestion
RVANDO12 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
19 changes: 19 additions & 0 deletions
19
...in/java/com/decathlon/idp_core/domain/exception/principal/PrincipalCreationException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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)); | ||
| } | ||
| } |
20 changes: 20 additions & 0 deletions
20
...in/java/com/decathlon/idp_core/domain/exception/principal/PrincipalNotFoundException.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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."); | ||
| } | ||
| } |
26 changes: 26 additions & 0 deletions
26
src/main/java/com/decathlon/idp_core/domain/model/principal/PrincipalInfo.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
10 changes: 10 additions & 0 deletions
10
src/main/java/com/decathlon/idp_core/domain/model/principal/PrincipalKind.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
...in/java/com/decathlon/idp_core/domain/service/principal/PrincipalProvisioningService.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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())); | ||
| } | ||
| } | ||
|
|
||
| /// 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)); | ||
| } | ||
| } | ||
82 changes: 82 additions & 0 deletions
82
...n/java/com/decathlon/idp_core/infrastructure/adapters/api/auth/JitProvisioningFilter.java
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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) { | ||
|
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) { | ||
|
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("/"); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.