diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionCreateRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionCreateRequest.java index ffe89973..737b7cba 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionCreateRequest.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionCreateRequest.java @@ -9,9 +9,7 @@ public record LoginConnectionCreateRequest( @NotBlank @Size(max = 128) String displayName, @NotBlank @Size(max = 64) String adapterKey, @NotEmpty Map configuration, - @Size(min = 1, max = 4096) String clientSecret, - boolean verifiedEmailCorrelationEnabled, - boolean jitProvisioningEnabled + @Size(min = 1, max = 4096) String clientSecret ) { @Override @@ -19,8 +17,6 @@ public record LoginConnectionCreateRequest( return "LoginConnectionCreateRequest[displayName=" + displayName + ", adapterKey=" + adapterKey + ", configuration=, clientSecret=" - + (clientSecret == null ? "none" : "") - + ", verifiedEmailCorrelationEnabled=" + verifiedEmailCorrelationEnabled - + ", jitProvisioningEnabled=" + jitProvisioningEnabled + "]"; + + (clientSecret == null ? "none" : "") + "]"; } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionCreateRequest.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionCreateRequest.java index 36cf2aba..0897184f 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionCreateRequest.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionCreateRequest.java @@ -6,16 +6,12 @@ import java.util.Map; public record LoginConnectionRevisionCreateRequest( @NotEmpty Map configuration, - @Size(min = 1, max = 4096) String clientSecret, - boolean verifiedEmailCorrelationEnabled, - boolean jitProvisioningEnabled + @Size(min = 1, max = 4096) String clientSecret ) { @Override public String toString() { return "LoginConnectionRevisionCreateRequest[configuration=" - + ", clientSecret=" + (clientSecret == null ? "unchanged" : "") - + ", verifiedEmailCorrelationEnabled=" + verifiedEmailCorrelationEnabled - + ", jitProvisioningEnabled=" + jitProvisioningEnabled + "]"; + + ", clientSecret=" + (clientSecret == null ? "unchanged" : "") + "]"; } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionResponse.java index 9479c0ad..d1ef1a21 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/LoginConnectionRevisionResponse.java @@ -9,8 +9,6 @@ public record LoginConnectionRevisionResponse( String adapterContractVersion, int configSchemaVersion, Map configuration, - boolean verifiedEmailCorrelationEnabled, - boolean jitProvisioningEnabled, Instant createdAt ) { } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/EnterpriseConnectionQueryRepository.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/EnterpriseConnectionQueryRepository.java index 49e4cbca..13e91784 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/EnterpriseConnectionQueryRepository.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/repository/EnterpriseConnectionQueryRepository.java @@ -147,8 +147,6 @@ public class EnterpriseConnectionQueryRepository { revision.getAdapterContractVersion(), revision.getConfigSchemaVersion(), Map.copyOf(objectMapper.readValue(revision.getTypedConfig(), CONFIG_TYPE)), - revision.correlationPolicy().verifiedEmailCorrelationEnabled(), - revision.correlationPolicy().jitProvisioningEnabled(), revision.getCreatedAt() ); } catch (Exception invalidStoredConfiguration) { diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/EnterpriseConnectionAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/EnterpriseConnectionAppService.java index 53daad90..5f4a2967 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/EnterpriseConnectionAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/EnterpriseConnectionAppService.java @@ -15,7 +15,6 @@ import com.iflytek.skillhub.auth.connection.core.AdapterKey; import com.iflytek.skillhub.auth.connection.secret.LoginConnectionSecretService; import com.iflytek.skillhub.auth.connection.secret.SecretMaterial; import com.iflytek.skillhub.auth.connection.secret.SecretPurpose; -import com.iflytek.skillhub.auth.federation.core.IdentityCorrelationPolicySettings; import com.iflytek.skillhub.auth.operation.IdentityOperation; import com.iflytek.skillhub.auth.operation.IdentityOperationRepository; import com.iflytek.skillhub.domain.audit.AuditLogService; @@ -144,7 +143,6 @@ public class EnterpriseConnectionAppService { 1, prepared, secretVersion, - policy(request.verifiedEmailCorrelationEnabled(), request.jitProvisioningEnabled()), actorUserId, now )); @@ -207,7 +205,6 @@ public class EnterpriseConnectionAppService { previous.getRevision() + 1, prepared, secretVersion, - policy(request.verifiedEmailCorrelationEnabled(), request.jitProvisioningEnabled()), actorUserId, now )); @@ -349,7 +346,6 @@ public class EnterpriseConnectionAppService { long revisionNumber, PreparedLoginConnectionRevision prepared, Long secretVersion, - IdentityCorrelationPolicySettings correlationPolicy, String actorUserId, Instant now ) { @@ -365,7 +361,6 @@ public class EnterpriseConnectionAppService { prepared.capabilitiesJson(), prepared.typedConfigJson(), secretVersion, - correlationPolicy, actorUserId, now ); @@ -401,17 +396,6 @@ public class EnterpriseConnectionAppService { )); } - private static IdentityCorrelationPolicySettings policy( - boolean verifiedEmail, - boolean jit - ) { - try { - return new IdentityCorrelationPolicySettings(verifiedEmail, jit); - } catch (IllegalArgumentException invalid) { - throw new DomainBadRequestException("error.loginConnection.correlation.invalid"); - } - } - private LoginConnection requireConnection(String organizationId, String connectionId) { return connections.findByOrganizationIdAndId(organizationId, connectionId) .orElseThrow(() -> new DomainNotFoundException("error.loginConnection.notFound")); diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/OrganizationAdminControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/OrganizationAdminControllerTest.java index 600a05fb..a810ba97 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/OrganizationAdminControllerTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/OrganizationAdminControllerTest.java @@ -195,8 +195,6 @@ class OrganizationAdminControllerTest { "clientId", "skillhub", "scopes", List.of("openid") ), - false, - false, NOW ), new LoginConnectionSecretSummaryResponse(true, NOW, null), @@ -217,12 +215,10 @@ class OrganizationAdminControllerTest { "adapterKey": "oidc", "configuration": { "issuer": "https://id.example.com", - "clientId": "skillhub", - "scopes": ["openid"] + "clientId": "skillhub", + "scopes": ["openid"] }, - "clientSecret": "never-echo-this-secret", - "verifiedEmailCorrelationEnabled": false, - "jitProvisioningEnabled": false + "clientSecret": "never-echo-this-secret" } """)) .andExpect(status().isOk()) diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/LoginConnectionLifecyclePersistenceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/LoginConnectionLifecyclePersistenceTest.java index 5e6f8278..1cc222b8 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/LoginConnectionLifecyclePersistenceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/LoginConnectionLifecyclePersistenceTest.java @@ -93,9 +93,6 @@ class LoginConnectionLifecyclePersistenceTest { @Autowired private LoginConnectionLifecycleService lifecycleService; - @Autowired - private IdentityOperationRepository identityOperationRepository; - @Autowired private EntityManager entityManager; diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/OrganizationPersistenceTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/OrganizationPersistenceTest.java index 3d5a4956..9f03b32c 100644 --- a/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/OrganizationPersistenceTest.java +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/repository/OrganizationPersistenceTest.java @@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.iflytek.skillhub.domain.organization.MembershipSourceType; import com.iflytek.skillhub.domain.organization.Organization; +import com.iflytek.skillhub.domain.organization.OrganizationAccessGuard; import com.iflytek.skillhub.domain.organization.OrganizationAuthorizationPolicy; import com.iflytek.skillhub.domain.organization.OrganizationAuthorizationService; import com.iflytek.skillhub.domain.organization.OrganizationDomain; @@ -63,6 +64,7 @@ import org.testcontainers.junit.jupiter.Testcontainers; JpaOrganizationMembershipRepositoryAdapter.class, JpaOrganizationDomainRepositoryAdapter.class, JpaOrganizationRoleBindingRepositoryAdapter.class, + OrganizationAccessGuard.class, OrganizationAuthorizationPolicy.class, OrganizationAuthorizationService.class, OrganizationRoleBindingService.class @@ -362,7 +364,7 @@ class OrganizationPersistenceTest { assertThat(membershipRepository.findCurrentByOrganizationIdAndSource( organization.getId(), - MembershipSourceType.DIRECTORY, + MembershipSourceType.JIT, "directory-user-replacement" )).get().extracting(OrganizationMembership::getId).isEqualTo(replacement.getId()); assertThat(entityManager.find(OrganizationMembership.class, history.getId()).getStatus()) @@ -630,7 +632,7 @@ class OrganizationPersistenceTest { ) { OrganizationMembership membership = OrganizationMembership.provisioned( organizationId, - MembershipSourceType.DIRECTORY, + MembershipSourceType.JIT, sourceId, userId, userId + "@example.com", diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/BuiltInLoginConnectionRuntimeSnapshotMaterializer.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/BuiltInLoginConnectionRuntimeSnapshotMaterializer.java new file mode 100644 index 00000000..93e00aa2 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/BuiltInLoginConnectionRuntimeSnapshotMaterializer.java @@ -0,0 +1,63 @@ +package com.iflytek.skillhub.auth.connection.control; + +import com.iflytek.skillhub.auth.connection.core.AdapterKey; +import com.iflytek.skillhub.auth.connection.core.ConnectionUnavailableException; +import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** Dispatches persisted revisions to reviewed, contract-versioned Adapter decoders. */ +public final class BuiltInLoginConnectionRuntimeSnapshotMaterializer + implements LoginConnectionRuntimeSnapshotMaterializer { + + private final Map decoders; + + public BuiltInLoginConnectionRuntimeSnapshotMaterializer( + Collection decoders + ) { + Objects.requireNonNull(decoders, "decoders"); + Map indexed = new LinkedHashMap<>(); + for (LoginConnectionRuntimeSnapshotDecoder decoder : decoders) { + Objects.requireNonNull(decoder, "decoder"); + if (decoder.contractMajor() < 1) { + throw new IllegalArgumentException("Decoder contract major must be positive"); + } + DecoderKey key = new DecoderKey(decoder.adapterKey(), decoder.contractMajor()); + if (indexed.putIfAbsent(key, decoder) != null) { + throw new IllegalArgumentException("Duplicate Login Connection runtime decoder"); + } + } + this.decoders = Map.copyOf(indexed); + } + + @Override + public LoginConnectionRuntimeSnapshot materialize( + LoginConnectionRevisionSnapshot revision + ) { + Objects.requireNonNull(revision, "revision"); + LoginConnectionRuntimeSnapshotDecoder decoder = decoders.get( + new DecoderKey(revision.adapterKey(), parseContractMajor(revision.adapterContractVersion())) + ); + if (decoder == null) { + throw new ConnectionUnavailableException(); + } + return decoder.decode(revision); + } + + private static int parseContractMajor(String contractVersion) { + int separator = contractVersion.indexOf('.'); + if (separator < 1 || separator != contractVersion.lastIndexOf('.')) { + throw new ConnectionUnavailableException(); + } + try { + return Integer.parseInt(contractVersion.substring(0, separator)); + } catch (NumberFormatException invalid) { + throw new ConnectionUnavailableException(); + } + } + + private record DecoderKey(AdapterKey adapterKey, int contractMajor) { + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRepositoryAdapter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRepositoryAdapter.java index a1e7932f..c9467396 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRepositoryAdapter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRepositoryAdapter.java @@ -1,6 +1,7 @@ package com.iflytek.skillhub.auth.connection.control; import com.iflytek.skillhub.auth.connection.core.ConnectionHandle; +import java.util.List; import java.util.Objects; import java.util.Optional; import org.springframework.stereotype.Repository; @@ -16,6 +17,14 @@ public class JpaLoginConnectionRepositoryAdapter implements LoginConnectionRepos this.delegate = Objects.requireNonNull(delegate, "delegate"); } + @Override + @Transactional(readOnly = true) + public List findAllByOrganizationId(String organizationId) { + return delegate.findAllByOrganizationIdOrderByCreatedAtDescIdDesc( + requireText(organizationId, "organizationId") + ); + } + @Override @Transactional(readOnly = true) public Optional findByOrganizationIdAndId(String organizationId, String id) { diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRevisionRepositoryAdapter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRevisionRepositoryAdapter.java index b34bc956..90be0434 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRevisionRepositoryAdapter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/JpaLoginConnectionRevisionRepositoryAdapter.java @@ -18,6 +18,14 @@ public class JpaLoginConnectionRevisionRepositoryAdapter this.delegate = Objects.requireNonNull(delegate, "delegate"); } + @Override + @Transactional(readOnly = true) + public Optional findLatestByConnectionId(String connectionId) { + return delegate.findFirstByConnectionIdOrderByRevisionDescIdDesc( + requireText(connectionId, "connectionId") + ); + } + @Override @Transactional(readOnly = true) public Optional findByConnectionIdAndId( diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRepository.java index 37cf0a15..0383cabd 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRepository.java @@ -1,11 +1,14 @@ package com.iflytek.skillhub.auth.connection.control; import com.iflytek.skillhub.auth.connection.core.ConnectionHandle; +import java.util.List; import java.util.Optional; /** Organization-scoped persistence port for Login Connection lifecycle state. */ public interface LoginConnectionRepository { + List findAllByOrganizationId(String organizationId); + Optional findByOrganizationIdAndId(String organizationId, String id); /** Serializes high-risk control-plane mutations such as Secret rotation. */ diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionRepository.java index a606beb5..f49ab4ed 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionRepository.java @@ -5,6 +5,8 @@ import java.util.Optional; /** Connection-scoped persistence port for immutable Login Connection revisions. */ public interface LoginConnectionRevisionRepository { + Optional findLatestByConnectionId(String connectionId); + Optional findByConnectionIdAndId( String connectionId, String revisionId diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionSpringDataRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionSpringDataRepository.java index 44d9845b..a6be0986 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionSpringDataRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRevisionSpringDataRepository.java @@ -7,6 +7,10 @@ import org.springframework.data.jpa.repository.JpaRepository; interface LoginConnectionRevisionSpringDataRepository extends JpaRepository { + Optional findFirstByConnectionIdOrderByRevisionDescIdDesc( + String connectionId + ); + Optional findByConnectionIdAndId( String connectionId, String id diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRuntimeSnapshotDecoder.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRuntimeSnapshotDecoder.java new file mode 100644 index 00000000..5dedd4e4 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionRuntimeSnapshotDecoder.java @@ -0,0 +1,14 @@ +package com.iflytek.skillhub.auth.connection.control; + +import com.iflytek.skillhub.auth.connection.core.AdapterKey; +import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot; + +/** Adapter-owned decoder for one persisted Login Connection contract major. */ +public interface LoginConnectionRuntimeSnapshotDecoder { + + AdapterKey adapterKey(); + + int contractMajor(); + + LoginConnectionRuntimeSnapshot decode(LoginConnectionRevisionSnapshot revision); +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionSpringDataRepository.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionSpringDataRepository.java index 60981018..c482ed3c 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionSpringDataRepository.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/control/LoginConnectionSpringDataRepository.java @@ -1,5 +1,6 @@ package com.iflytek.skillhub.auth.connection.control; +import java.util.List; import java.util.Optional; import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,6 +11,10 @@ import org.springframework.data.repository.query.Param; /** Internal Spring Data delegate; callers use the tenant-scoped repository port. */ interface LoginConnectionSpringDataRepository extends JpaRepository { + List findAllByOrganizationIdOrderByCreatedAtDescIdDesc( + String organizationId + ); + Optional findByOrganizationIdAndId(String organizationId, String id); @Lock(LockModeType.PESSIMISTIC_WRITE) diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapter.java index 724af256..1923fa75 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapter.java @@ -11,7 +11,10 @@ import com.iflytek.skillhub.auth.connection.control.LoginConnectionTestProbe; import com.iflytek.skillhub.auth.connection.control.PreparedLoginConnectionRevision; import com.iflytek.skillhub.auth.connection.core.AdapterDescriptor; import com.iflytek.skillhub.auth.connection.core.AdapterKey; +import com.iflytek.skillhub.auth.connection.core.AdapterContractVersion; +import com.iflytek.skillhub.auth.connection.core.ConnectionKind; import com.iflytek.skillhub.auth.connection.core.ConnectionUnavailableException; +import com.iflytek.skillhub.auth.connection.core.InteractionModel; import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot; import com.iflytek.skillhub.auth.connection.secret.SecretMaterial; import com.iflytek.skillhub.auth.connection.secret.SecretMaterialResolver; @@ -77,7 +80,7 @@ public final class OidcLoginConnectionControlAdapter implements LoginConnectionC OidcIssuer issuer = new OidcIssuer(requiredString(values, "issuer")); String clientId = requiredString(values, "clientId"); List scopes = scopes(values.get("scopes")); - AdapterDescriptor descriptor = OidcRedirectAuthenticationAdapter.supportedDescriptor(); + AdapterDescriptor descriptor = supportedDescriptor(); Map persisted = new LinkedHashMap<>(); persisted.put("issuer", issuer.value()); persisted.put("clientId", clientId); @@ -104,7 +107,7 @@ public final class OidcLoginConnectionControlAdapter implements LoginConnectionC return candidate -> { LoginConnectionRuntimeSnapshot runtime = requireOidcRuntime(candidate); - var reference = runtime.secretReference().orElseThrow(() -> failure( + var reference = candidate.secretReference().orElseThrow(() -> failure( LoginConnectionTestFailureReason.CREDENTIAL )); try (SecretMaterial ignored = secrets.resolve( @@ -206,4 +209,19 @@ public final class OidcLoginConnectionControlAdapter implements LoginConnectionC ) { return new LoginConnectionTestException(reason); } + + private static AdapterDescriptor supportedDescriptor() { + return new AdapterDescriptor( + OidcLoginConnectionRuntimeConfig.ADAPTER_KEY, + new AdapterContractVersion(1, 0), + ConnectionKind.LOGIN, + 1, + Optional.of(InteractionModel.REDIRECT), + Set.of( + com.iflytek.skillhub.auth.connection.core.AdapterCapability.IDENTITY_ASSERTION, + com.iflytek.skillhub.auth.connection.core.AdapterCapability.VERIFIED_EMAIL_ASSERTION, + com.iflytek.skillhub.auth.connection.core.AdapterCapability.PROFILE_ATTRIBUTE_ASSERTION + ) + ); + } } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeConfig.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeConfig.java new file mode 100644 index 00000000..25fa2d8e --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeConfig.java @@ -0,0 +1,65 @@ +package com.iflytek.skillhub.auth.connection.oidc; + +import com.iflytek.skillhub.auth.connection.core.AdapterKey; +import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeConfig; +import com.iflytek.skillhub.auth.federation.core.SubjectType; +import java.util.Collections; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +/** Immutable non-secret OIDC client configuration pinned to one connection revision. */ +public record OidcLoginConnectionRuntimeConfig( + OidcIssuer issuer, + String clientId, + Set scopes +) implements LoginConnectionRuntimeConfig { + + public static final AdapterKey ADAPTER_KEY = new AdapterKey("oidc-standard"); + public static final SubjectType SUBJECT_TYPE = new SubjectType("oidc-sub"); + private static final int MAX_SCOPES = 16; + private static final int MAX_SCOPE_LENGTH = 128; + + public OidcLoginConnectionRuntimeConfig { + issuer = Objects.requireNonNull(issuer, "issuer"); + clientId = requireClientId(clientId); + TreeSet normalizedScopes = new TreeSet<>(); + normalizedScopes.add("openid"); + for (String scope : Objects.requireNonNull(scopes, "scopes")) { + normalizedScopes.add(requireScope(scope)); + } + if (normalizedScopes.size() > MAX_SCOPES) { + throw new IllegalArgumentException("OIDC scope count exceeds the safe limit"); + } + scopes = Collections.unmodifiableSet(normalizedScopes); + } + + String scopeValue() { + return String.join(" ", scopes); + } + + private static String requireClientId(String value) { + Objects.requireNonNull(value, "clientId"); + if (value.isBlank() + || !value.equals(value.trim()) + || value.length() > 256 + || value.codePoints().anyMatch(Character::isISOControl)) { + throw new IllegalArgumentException("OIDC client id is invalid"); + } + return value; + } + + private static String requireScope(String value) { + Objects.requireNonNull(value, "scope"); + if (value.isEmpty() || value.length() > MAX_SCOPE_LENGTH) { + throw new IllegalArgumentException("OIDC scope is invalid"); + } + boolean invalid = value.chars().anyMatch(character -> + character < 0x21 || character > 0x7e || character == '"' || character == '\\' + ); + if (invalid) { + throw new IllegalArgumentException("OIDC scope is invalid"); + } + return value; + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeSnapshotMaterializer.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeSnapshotMaterializer.java new file mode 100644 index 00000000..98f355c5 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionRuntimeSnapshotMaterializer.java @@ -0,0 +1,182 @@ +package com.iflytek.skillhub.auth.connection.oidc; + +import com.fasterxml.jackson.core.JsonFactory; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.StreamReadConstraints; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iflytek.skillhub.auth.connection.control.LoginConnectionRevisionSnapshot; +import com.iflytek.skillhub.auth.connection.control.LoginConnectionRuntimeSnapshotDecoder; +import com.iflytek.skillhub.auth.connection.core.AdapterCapability; +import com.iflytek.skillhub.auth.connection.core.AdapterContractVersion; +import com.iflytek.skillhub.auth.connection.core.AdapterDescriptor; +import com.iflytek.skillhub.auth.connection.core.AdapterKey; +import com.iflytek.skillhub.auth.connection.core.ConnectionKind; +import com.iflytek.skillhub.auth.connection.core.ConnectionUnavailableException; +import com.iflytek.skillhub.auth.connection.core.InteractionModel; +import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot; +import java.util.LinkedHashSet; +import java.util.Optional; +import java.util.Set; + +/** Strictly decodes immutable OIDC revisions; unknown or incompatible fields fail closed. */ +public final class OidcLoginConnectionRuntimeSnapshotMaterializer + implements LoginConnectionRuntimeSnapshotDecoder { + + private static final int MAX_JSON_BYTES = 16 * 1024; + private static final Set CONFIG_FIELDS = Set.of("issuer", "clientId", "scopes"); + + private final ObjectMapper objectMapper; + + public OidcLoginConnectionRuntimeSnapshotMaterializer() { + JsonFactory factory = JsonFactory.builder() + .streamReadConstraints(StreamReadConstraints.builder() + .maxNestingDepth(8) + .maxStringLength(8 * 1024) + .maxNumberLength(32) + .build()) + .build(); + factory.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + this.objectMapper = new ObjectMapper(factory); + } + + static AdapterDescriptor supportedDescriptor() { + return new AdapterDescriptor( + OidcLoginConnectionRuntimeConfig.ADAPTER_KEY, + new AdapterContractVersion(1, 0), + ConnectionKind.LOGIN, + 1, + Optional.of(InteractionModel.REDIRECT), + Set.of( + AdapterCapability.IDENTITY_ASSERTION, + AdapterCapability.VERIFIED_EMAIL_ASSERTION, + AdapterCapability.PROFILE_ATTRIBUTE_ASSERTION + ) + ); + } + + @Override + public AdapterKey adapterKey() { + return OidcLoginConnectionRuntimeConfig.ADAPTER_KEY; + } + + @Override + public int contractMajor() { + return supportedDescriptor().contractVersion().major(); + } + + @Override + public LoginConnectionRuntimeSnapshot decode( + LoginConnectionRevisionSnapshot revision + ) { + try { + AdapterDescriptor descriptor = supportedDescriptor(); + requireCompatible(revision, descriptor); + JsonNode config = parseObject(revision.typedConfigJson()); + if (!fieldNames(config).equals(CONFIG_FIELDS)) { + throw new ConnectionUnavailableException(); + } + Set scopes = new LinkedHashSet<>(); + JsonNode scopeValues = config.get("scopes"); + if (scopeValues == null || !scopeValues.isArray()) { + throw new ConnectionUnavailableException(); + } + scopeValues.forEach(scope -> { + if (!scope.isTextual()) { + throw new ConnectionUnavailableException(); + } + scopes.add(scope.textValue()); + }); + return new LoginConnectionRuntimeSnapshot<>( + revision.organizationId(), + revision.connectionId(), + revision.publicHandle(), + revision.revision(), + descriptor, + new OidcLoginConnectionRuntimeConfig( + new OidcIssuer(requiredText(config, "issuer")), + requiredText(config, "clientId"), + scopes + ) + ); + } catch (ConnectionUnavailableException unavailable) { + throw unavailable; + } catch (RuntimeException invalidRevision) { + throw new ConnectionUnavailableException(); + } + } + + private void requireCompatible( + LoginConnectionRevisionSnapshot revision, + AdapterDescriptor descriptor + ) { + AdapterContractVersion contract = parseContract(revision.adapterContractVersion()); + Set capabilities = parseCapabilities(revision.capabilitiesJson()); + if (!revision.adapterKey().equals(descriptor.adapterKey()) + || contract.major() != descriptor.contractVersion().major() + || contract.minor() > descriptor.contractVersion().minor() + || revision.configSchemaVersion() != descriptor.configSchemaVersion() + || !capabilities.equals(descriptor.capabilities())) { + throw new ConnectionUnavailableException(); + } + } + + private Set parseCapabilities(String json) { + JsonNode value = parse(json); + if (!value.isArray()) { + throw new ConnectionUnavailableException(); + } + Set capabilities = new LinkedHashSet<>(); + value.forEach(capability -> { + if (!capability.isTextual()) { + throw new ConnectionUnavailableException(); + } + capabilities.add(AdapterCapability.valueOf(capability.textValue())); + }); + return Set.copyOf(capabilities); + } + + private JsonNode parseObject(String json) { + JsonNode value = parse(json); + if (!value.isObject()) { + throw new ConnectionUnavailableException(); + } + return value; + } + + private JsonNode parse(String json) { + if (json.getBytes(java.nio.charset.StandardCharsets.UTF_8).length > MAX_JSON_BYTES) { + throw new ConnectionUnavailableException(); + } + try { + return objectMapper.readTree(json); + } catch (java.io.IOException invalidJson) { + throw new ConnectionUnavailableException(); + } + } + + private static Set fieldNames(JsonNode object) { + Set fields = new LinkedHashSet<>(); + object.fieldNames().forEachRemaining(fields::add); + return Set.copyOf(fields); + } + + private static String requiredText(JsonNode object, String field) { + JsonNode value = object.get(field); + if (value == null || !value.isTextual()) { + throw new ConnectionUnavailableException(); + } + return value.textValue(); + } + + private static AdapterContractVersion parseContract(String value) { + String[] parts = value.split("\\.", -1); + if (parts.length != 2) { + throw new ConnectionUnavailableException(); + } + return new AdapterContractVersion( + Integer.parseInt(parts[0]), + Integer.parseInt(parts[1]) + ); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseIdentityCoreConfiguration.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseIdentityCoreConfiguration.java index a78ed123..866109d4 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseIdentityCoreConfiguration.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseIdentityCoreConfiguration.java @@ -14,7 +14,9 @@ import org.springframework.context.annotation.Configuration; @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties({ IdentityCoreProperties.class, - EnterpriseIdentityRolloutProperties.class + EnterpriseIdentityRolloutProperties.class, + EnterpriseOidcProperties.class, + EnterpriseSecretEnvelopeProperties.class }) public class EnterpriseIdentityCoreConfiguration { diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcProperties.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcProperties.java new file mode 100644 index 00000000..d15e66b4 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcProperties.java @@ -0,0 +1,67 @@ +package com.iflytek.skillhub.auth.federation.config; + +import java.net.URI; +import java.util.LinkedHashSet; +import java.util.Locale; +import java.util.Objects; +import java.util.Set; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Conservative transport and outbound-network controls for dynamic enterprise OIDC. */ +@ConfigurationProperties(prefix = "skillhub.enterprise.oidc") +public class EnterpriseOidcProperties { + + private boolean enabled; + private String publicBaseUri = ""; + private Set privateHostAllowlist = Set.of(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getPublicBaseUri() { + return publicBaseUri; + } + + public void setPublicBaseUri(String publicBaseUri) { + this.publicBaseUri = Objects.requireNonNull(publicBaseUri, "publicBaseUri").trim(); + } + + public Set getPrivateHostAllowlist() { + return privateHostAllowlist; + } + + public void setPrivateHostAllowlist(Set privateHostAllowlist) { + Objects.requireNonNull(privateHostAllowlist, "privateHostAllowlist"); + LinkedHashSet normalized = new LinkedHashSet<>(); + for (String host : privateHostAllowlist) { + Objects.requireNonNull(host, "privateHostAllowlist entry"); + String value = host.trim().toLowerCase(Locale.ROOT); + if (value.isEmpty()) { + throw new IllegalArgumentException("privateHostAllowlist must not contain blanks"); + } + normalized.add(value); + } + this.privateHostAllowlist = Set.copyOf(normalized); + } + + public void validateEnabledConfiguration() { + URI base; + try { + base = URI.create(publicBaseUri); + } catch (IllegalArgumentException invalid) { + throw new IllegalStateException("Enterprise OIDC public base URI is invalid"); + } + if (!"https".equals(base.getScheme()) + || base.getHost() == null + || base.getRawUserInfo() != null + || base.getRawQuery() != null + || base.getRawFragment() != null) { + throw new IllegalStateException("Enterprise OIDC public base URI must use HTTPS"); + } + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcRuntimeConfiguration.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcRuntimeConfiguration.java index 5999fe85..728ffbfd 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcRuntimeConfiguration.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseOidcRuntimeConfiguration.java @@ -1,310 +1,121 @@ package com.iflytek.skillhub.auth.federation.config; -import com.fasterxml.jackson.databind.ObjectMapper; import com.iflytek.skillhub.auth.connection.control.BuiltInLoginConnectionRuntimeSnapshotMaterializer; -import com.iflytek.skillhub.auth.connection.control.LoginConnectionRepository; -import com.iflytek.skillhub.auth.connection.control.LoginConnectionRevisionRepository; import com.iflytek.skillhub.auth.connection.control.LoginConnectionControlAdapter; import com.iflytek.skillhub.auth.connection.control.LoginConnectionControlAdapterRegistry; import com.iflytek.skillhub.auth.connection.control.LoginConnectionRuntimeSnapshotDecoder; import com.iflytek.skillhub.auth.connection.control.LoginConnectionRuntimeSnapshotMaterializer; -import com.iflytek.skillhub.auth.connection.control.PersistentEnterpriseConnectionRegistry; -import com.iflytek.skillhub.auth.connection.core.ConnectionHandle; -import com.iflytek.skillhub.auth.connection.core.ConnectionUnavailableException; -import com.iflytek.skillhub.auth.connection.core.EnterpriseConnectionContinuationRegistry; import com.iflytek.skillhub.auth.connection.oidc.DefaultOidcMetadataSource; import com.iflytek.skillhub.auth.connection.oidc.JdkOidcSingleHopHttpTransport; -import com.iflytek.skillhub.auth.connection.oidc.OidcAuthorizationTransactionService; import com.iflytek.skillhub.auth.connection.oidc.OidcConnectionMetadataProbe; -import com.iflytek.skillhub.auth.connection.oidc.OidcAuthorizationTransactionStore; import com.iflytek.skillhub.auth.connection.oidc.OidcHostAddressResolver; -import com.iflytek.skillhub.auth.connection.oidc.OidcIdTokenValidator; -import com.iflytek.skillhub.auth.connection.oidc.OidcLoginConnectionRuntimeSnapshotMaterializer; import com.iflytek.skillhub.auth.connection.oidc.OidcLoginConnectionControlAdapter; +import com.iflytek.skillhub.auth.connection.oidc.OidcLoginConnectionRuntimeSnapshotMaterializer; import com.iflytek.skillhub.auth.connection.oidc.OidcMetadataManager; import com.iflytek.skillhub.auth.connection.oidc.OidcOutboundTargetPolicy; -import com.iflytek.skillhub.auth.connection.oidc.OidcRedirectAuthenticationAdapter; -import com.iflytek.skillhub.auth.connection.oidc.OidcTokenEndpointClient; -import com.iflytek.skillhub.auth.connection.oidc.RedisOidcAuthorizationTransactionStore; import com.iflytek.skillhub.auth.connection.oidc.SecureOidcDocumentClient; import com.iflytek.skillhub.auth.connection.secret.AesGcmSecretEnvelopeCipher; import com.iflytek.skillhub.auth.connection.secret.InMemorySecretEnvelopeKeyring; import com.iflytek.skillhub.auth.connection.secret.LoginConnectionSecretRepository; import com.iflytek.skillhub.auth.connection.secret.LoginConnectionSecretService; -import com.iflytek.skillhub.auth.federation.EnterpriseRedirectLoginGateway; -import com.iflytek.skillhub.auth.federation.EnterpriseRedirectLoginService; -import com.iflytek.skillhub.auth.federation.adapter.BuiltInRedirectAuthenticationAdapterRegistry; import com.iflytek.skillhub.auth.federation.adapter.RemoteIdentityIoExecutor; -import com.iflytek.skillhub.auth.federation.association.EnterpriseIdentityAssociationService; -import com.iflytek.skillhub.auth.federation.association.ExternalIdentityRepository; -import com.iflytek.skillhub.auth.federation.association.PreProvisionedLoginSubjectRepository; -import com.iflytek.skillhub.auth.federation.core.IdentityCoreActivation; -import com.iflytek.skillhub.auth.federation.core.RedirectAuthenticationAdapter; -import com.iflytek.skillhub.auth.federation.core.RedirectCompleteRequest; -import com.iflytek.skillhub.auth.federation.core.RedirectStartRequest; -import com.iflytek.skillhub.auth.rbac.RbacService; -import com.iflytek.skillhub.auth.session.EnterpriseBrowserSessionService; -import com.iflytek.skillhub.domain.namespace.GlobalNamespaceMembershipService; -import com.iflytek.skillhub.domain.organization.OrganizationDomainRepository; -import com.iflytek.skillhub.domain.organization.OrganizationMembershipRepository; -import com.iflytek.skillhub.domain.organization.OrganizationRepository; -import com.iflytek.skillhub.domain.user.UserAccountRepository; -import jakarta.servlet.http.HttpServletRequest; import java.time.Clock; import java.util.List; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.StringRedisTemplate; -import org.springframework.transaction.PlatformTransactionManager; -/** Production composition root for the first built-in dynamic enterprise login Adapter. */ +/** R1-B composition root for enterprise OIDC control-plane management only. */ @Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(name = "skillhub.enterprise.oidc.enabled", havingValue = "true") public class EnterpriseOidcRuntimeConfiguration { + @Bean(destroyMethod = "close") + InMemorySecretEnvelopeKeyring enterpriseSecretEnvelopeKeyring( + EnterpriseSecretEnvelopeProperties properties + ) { + return properties.createKeyring(); + } + @Bean - @ConditionalOnProperty( - name = "skillhub.enterprise.oidc.enabled", - havingValue = "false", - matchIfMissing = true - ) - EnterpriseRedirectLoginGateway disabledEnterpriseRedirectLoginGateway() { - return new DisabledEnterpriseRedirectLoginGateway(); + LoginConnectionSecretService loginConnectionSecretService( + com.iflytek.skillhub.auth.connection.control.LoginConnectionRepository connections, + LoginConnectionSecretRepository secrets, + InMemorySecretEnvelopeKeyring keyring + ) { + return new LoginConnectionSecretService( + connections, + secrets, + new AesGcmSecretEnvelopeCipher(keyring) + ); } - @Configuration(proxyBeanMethods = false) - @ConditionalOnProperty(name = "skillhub.enterprise.oidc.enabled", havingValue = "true") - static class EnabledRuntime { - - @Bean(destroyMethod = "close") - InMemorySecretEnvelopeKeyring enterpriseSecretEnvelopeKeyring( - EnterpriseSecretEnvelopeProperties properties - ) { - return properties.createKeyring(); - } - - @Bean - LoginConnectionSecretService loginConnectionSecretService( - LoginConnectionRepository connections, - LoginConnectionSecretRepository secrets, - InMemorySecretEnvelopeKeyring keyring - ) { - return new LoginConnectionSecretService( - connections, - secrets, - new AesGcmSecretEnvelopeCipher(keyring) - ); - } - - @Bean - OidcOutboundTargetPolicy oidcOutboundTargetPolicy(EnterpriseOidcProperties properties) { - properties.validateEnabledConfiguration(); - return new OidcOutboundTargetPolicy( - OidcHostAddressResolver.system(), - properties.getPrivateHostAllowlist() - ); - } - - @Bean - JdkOidcSingleHopHttpTransport oidcHttpTransport() { - return new JdkOidcSingleHopHttpTransport(); - } - - @Bean - OidcMetadataManager oidcMetadataManager( - OidcOutboundTargetPolicy targetPolicy, - JdkOidcSingleHopHttpTransport transport, - RemoteIdentityIoExecutor remoteIdentityIo - ) { - SecureOidcDocumentClient documents = new SecureOidcDocumentClient( - targetPolicy, - transport - ); - return new OidcMetadataManager( - new DefaultOidcMetadataSource(documents, targetPolicy), - remoteIdentityIo - ); - } - - @Bean - OidcAuthorizationTransactionStore oidcAuthorizationTransactionStore( - StringRedisTemplate redis, - ObjectMapper objectMapper - ) { - return new RedisOidcAuthorizationTransactionStore(redis, objectMapper); - } - - @Bean - OidcAuthorizationTransactionService oidcAuthorizationTransactionService( - OidcMetadataManager metadata, - OidcAuthorizationTransactionStore transactions - ) { - return new OidcAuthorizationTransactionService(metadata, transactions); - } - - @Bean - OidcLoginConnectionRuntimeSnapshotMaterializer oidcRuntimeSnapshotDecoder() { - return new OidcLoginConnectionRuntimeSnapshotMaterializer(); - } - - @Bean - LoginConnectionRuntimeSnapshotMaterializer loginConnectionRuntimeSnapshotMaterializer( - List decoders - ) { - return new BuiltInLoginConnectionRuntimeSnapshotMaterializer(decoders); - } - - @Bean - OidcConnectionMetadataProbe oidcConnectionMetadataProbe(OidcMetadataManager metadata) { - return (key, issuer, occurredAt) -> metadata.get(key, issuer, occurredAt); - } - - @Bean - OidcLoginConnectionControlAdapter oidcLoginConnectionControlAdapter( - LoginConnectionRuntimeSnapshotMaterializer materializer, - LoginConnectionSecretService secrets, - OidcConnectionMetadataProbe metadata, - Clock clock - ) { - return new OidcLoginConnectionControlAdapter( - materializer, - secrets, - metadata, - clock - ); - } - - @Bean - LoginConnectionControlAdapterRegistry loginConnectionControlAdapterRegistry( - List adapters - ) { - return new LoginConnectionControlAdapterRegistry(adapters); - } - - @Bean - EnterpriseConnectionContinuationRegistry enterpriseConnectionRegistry( - LoginConnectionRepository connections, - LoginConnectionRevisionRepository revisions, - LoginConnectionRuntimeSnapshotMaterializer materializer, - OrganizationRepository organizations - ) { - return new PersistentEnterpriseConnectionRegistry( - connections, - revisions, - materializer, - organizations - ); - } - - @Bean - OidcRedirectAuthenticationAdapter oidcRedirectAuthenticationAdapter( - OidcAuthorizationTransactionService transactions, - OidcMetadataManager metadata, - OidcOutboundTargetPolicy targetPolicy, - JdkOidcSingleHopHttpTransport transport, - LoginConnectionSecretService secrets, - RemoteIdentityIoExecutor remoteIdentityIo, - EnterpriseConnectionContinuationRegistry connections, - Clock clock - ) { - OidcTokenEndpointClient tokenEndpoint = new OidcTokenEndpointClient( - targetPolicy, - transport, - secrets, - remoteIdentityIo - ); - return new OidcRedirectAuthenticationAdapter( - transactions, - metadata, - tokenEndpoint, - new OidcIdTokenValidator(metadata), - connections, - clock - ); - } - - @Bean - BuiltInRedirectAuthenticationAdapterRegistry redirectAuthenticationAdapterRegistry( - List> adapters - ) { - return new BuiltInRedirectAuthenticationAdapterRegistry(adapters); - } - - @Bean - EnterpriseIdentityAssociationService enterpriseIdentityAssociationService( - IdentityCoreActivation activation, - ExternalIdentityRepository externalIdentities, - PreProvisionedLoginSubjectRepository subjects, - OrganizationDomainRepository domains, - OrganizationMembershipRepository memberships, - OrganizationRepository organizations, - UserAccountRepository accounts, - GlobalNamespaceMembershipService globalMemberships, - ApplicationEventPublisher events, - PlatformTransactionManager transactionManager, - Clock clock - ) { - return new EnterpriseIdentityAssociationService( - activation, - externalIdentities, - subjects, - domains, - memberships, - organizations, - accounts, - globalMemberships, - events, - transactionManager, - clock - ); - } - - @Bean - EnterpriseRedirectLoginGateway enterpriseRedirectLoginGateway( - EnterpriseConnectionContinuationRegistry connections, - BuiltInRedirectAuthenticationAdapterRegistry adapters, - EnterpriseIdentityAssociationService associations, - UserAccountRepository accounts, - RbacService rbac, - EnterpriseBrowserSessionService sessions, - EnterpriseIdentityRolloutProperties rollout - ) { - return new EnterpriseRedirectLoginService( - connections, - adapters, - associations, - accounts, - rbac, - sessions, - rollout - ); - } + @Bean + OidcOutboundTargetPolicy oidcOutboundTargetPolicy(EnterpriseOidcProperties properties) { + properties.validateEnabledConfiguration(); + return new OidcOutboundTargetPolicy( + OidcHostAddressResolver.system(), + properties.getPrivateHostAllowlist() + ); } - private static final class DisabledEnterpriseRedirectLoginGateway - implements EnterpriseRedirectLoginGateway { + @Bean + JdkOidcSingleHopHttpTransport oidcHttpTransport() { + return new JdkOidcSingleHopHttpTransport(); + } - @Override - public boolean isAvailable(ConnectionHandle handle) { - return false; - } + @Bean + OidcMetadataManager oidcMetadataManager( + OidcOutboundTargetPolicy targetPolicy, + JdkOidcSingleHopHttpTransport transport, + RemoteIdentityIoExecutor remoteIdentityIo + ) { + SecureOidcDocumentClient documents = new SecureOidcDocumentClient( + targetPolicy, + transport + ); + return new OidcMetadataManager( + new DefaultOidcMetadataSource(documents, targetPolicy), + remoteIdentityIo + ); + } - @Override - public com.iflytek.skillhub.auth.federation.core.RedirectStartResult start( - ConnectionHandle handle, - RedirectStartRequest request - ) { - throw new ConnectionUnavailableException(); - } + @Bean + OidcLoginConnectionRuntimeSnapshotMaterializer oidcRuntimeSnapshotDecoder() { + return new OidcLoginConnectionRuntimeSnapshotMaterializer(); + } - @Override - public LoginCompletion complete( - ConnectionHandle handle, - RedirectCompleteRequest request, - HttpServletRequest httpRequest - ) { - throw new ConnectionUnavailableException(); - } + @Bean + LoginConnectionRuntimeSnapshotMaterializer loginConnectionRuntimeSnapshotMaterializer( + List decoders + ) { + return new BuiltInLoginConnectionRuntimeSnapshotMaterializer(decoders); + } + + @Bean + OidcConnectionMetadataProbe oidcConnectionMetadataProbe(OidcMetadataManager metadata) { + return (key, issuer, occurredAt) -> metadata.get(key, issuer, occurredAt); + } + + @Bean + OidcLoginConnectionControlAdapter oidcLoginConnectionControlAdapter( + LoginConnectionRuntimeSnapshotMaterializer materializer, + LoginConnectionSecretService secrets, + OidcConnectionMetadataProbe metadata, + Clock clock + ) { + return new OidcLoginConnectionControlAdapter( + materializer, + secrets, + metadata, + clock + ); + } + + @Bean + LoginConnectionControlAdapterRegistry loginConnectionControlAdapterRegistry( + List adapters + ) { + return new LoginConnectionControlAdapterRegistry(adapters); } } diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseSecretEnvelopeProperties.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseSecretEnvelopeProperties.java new file mode 100644 index 00000000..0056bd14 --- /dev/null +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/config/EnterpriseSecretEnvelopeProperties.java @@ -0,0 +1,93 @@ +package com.iflytek.skillhub.auth.federation.config; + +import com.iflytek.skillhub.auth.connection.secret.InMemorySecretEnvelopeKeyring; +import com.iflytek.skillhub.auth.connection.secret.SecretEnvelopeKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Operator-owned keyring configuration; key material is never rendered by this type. */ +@ConfigurationProperties(prefix = "skillhub.enterprise.secret-envelope") +public class EnterpriseSecretEnvelopeProperties { + + private String activeKeyId = ""; + private List keys = List.of(); + + public String getActiveKeyId() { + return activeKeyId; + } + + public void setActiveKeyId(String activeKeyId) { + this.activeKeyId = activeKeyId == null ? "" : activeKeyId.trim(); + } + + public List getKeys() { + return keys; + } + + public void setKeys(List keys) { + this.keys = keys == null ? List.of() : List.copyOf(keys); + } + + InMemorySecretEnvelopeKeyring createKeyring() { + if (activeKeyId.isBlank()) { + throw new IllegalStateException("Enterprise Secret envelope active key id is required"); + } + Map decoded = new LinkedHashMap<>(); + byte[] activeBytes = null; + try { + for (String entry : keys) { + int separator = entry == null ? -1 : entry.indexOf(':'); + if (separator < 3 || separator == entry.length() - 1) { + throw invalidKeyring(); + } + String keyId = entry.substring(0, separator).trim(); + byte[] key = Base64.getDecoder().decode(entry.substring(separator + 1).trim()); + if (decoded.putIfAbsent(keyId, key) != null) { + Arrays.fill(key, (byte) 0); + throw invalidKeyring(); + } + } + activeBytes = decoded.remove(activeKeyId); + if (activeBytes == null) { + throw invalidKeyring(); + } + SecretEnvelopeKey active = null; + List previous = new ArrayList<>(); + try { + active = new SecretEnvelopeKey(activeKeyId, activeBytes); + decoded.forEach((keyId, bytes) -> + previous.add(new SecretEnvelopeKey(keyId, bytes))); + return new InMemorySecretEnvelopeKeyring( + active, + previous.toArray(SecretEnvelopeKey[]::new) + ); + } finally { + if (active != null) { + active.close(); + } + previous.forEach(SecretEnvelopeKey::close); + } + } catch (IllegalArgumentException invalid) { + throw invalidKeyring(); + } finally { + if (activeBytes != null) { + Arrays.fill(activeBytes, (byte) 0); + } + decoded.values().forEach(bytes -> Arrays.fill(bytes, (byte) 0)); + } + } + + private static IllegalStateException invalidKeyring() { + return new IllegalStateException("Enterprise Secret envelope keyring is invalid"); + } + + @Override + public String toString() { + return "EnterpriseSecretEnvelopeProperties[activeKeyId=, keys=]"; + } +} diff --git a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapterTest.java b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapterTest.java index 7926ebda..2514c50c 100644 --- a/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapterTest.java +++ b/server/skillhub-auth/src/test/java/com/iflytek/skillhub/auth/connection/oidc/OidcLoginConnectionControlAdapterTest.java @@ -46,7 +46,7 @@ class OidcLoginConnectionControlAdapterTest { ); assertThat(prepared.adapterDescriptor()).isEqualTo( - OidcRedirectAuthenticationAdapter.supportedDescriptor() + OidcLoginConnectionRuntimeSnapshotMaterializer.supportedDescriptor() ); assertThat(prepared.typedConfigJson()).contains( "https://id.example.com", "skillhub", "openid", "email" @@ -82,7 +82,7 @@ class OidcLoginConnectionControlAdapterTest { LoginConnectionRuntimeSnapshot runtime = runtime(); doReturn(runtime).when(materializer).materialize(revision); given(secrets.resolve( - runtime.secretReference().orElseThrow(), + revision.secretReference().orElseThrow(), SecretPurpose.LOGIN_CLIENT_SECRET, NOW )).willReturn(SecretMaterial.copyOf("top-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8))); @@ -101,7 +101,7 @@ class OidcLoginConnectionControlAdapterTest { LoginConnectionRuntimeSnapshot runtime = runtime(); doReturn(runtime).when(materializer).materialize(revision); given(secrets.resolve( - runtime.secretReference().orElseThrow(), + revision.secretReference().orElseThrow(), SecretPurpose.LOGIN_CLIENT_SECRET, NOW )).willReturn(SecretMaterial.copyOf("top-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8))); @@ -137,10 +137,7 @@ class OidcLoginConnectionControlAdapterTest { "connection-1", new ConnectionHandle("login-test-handle"), 1, - OidcRedirectAuthenticationAdapter.supportedDescriptor(), - Optional.of(new com.iflytek.skillhub.auth.connection.secret.SecretReference( - "org-1", "connection-1", SecretPurpose.LOGIN_CLIENT_SECRET, 1 - )), + OidcLoginConnectionRuntimeSnapshotMaterializer.supportedDescriptor(), new OidcLoginConnectionRuntimeConfig( new OidcIssuer("https://id.example.com"), "skillhub", @@ -163,8 +160,7 @@ class OidcLoginConnectionControlAdapterTest { "{\"issuer\":\"https://id.example.com\",\"clientId\":\"skillhub\",\"scopes\":[\"openid\"]}", Optional.of(new com.iflytek.skillhub.auth.connection.secret.SecretReference( "org-1", "connection-1", SecretPurpose.LOGIN_CLIENT_SECRET, 1 - )), - com.iflytek.skillhub.auth.federation.core.IdentityCorrelationPolicySettings.disabled() + )) ); } } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicy.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicy.java index 0b6f5513..e1ed4c5b 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicy.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicy.java @@ -25,8 +25,6 @@ public class OrganizationAuthorizationPolicy { Set.of( OrganizationRole.ORG_OWNER, OrganizationRole.IDENTITY_ADMIN, - OrganizationRole.LOGIN_SECRET_ADMIN, - OrganizationRole.MEMBER_ADMIN, OrganizationRole.ORG_AUDITOR ) ), @@ -44,15 +42,6 @@ public class OrganizationAuthorizationPolicy { OrganizationRole.ORG_OWNER, OrganizationRole.IDENTITY_ADMIN, OrganizationRole.LOGIN_SECRET_ADMIN, - OrganizationRole.MEMBER_ADMIN, - OrganizationRole.ORG_AUDITOR - ) - ), - Map.entry( - OrganizationAdministrativeAction.VIEW_LOGIN_CONNECTIONS, - Set.of( - OrganizationRole.ORG_OWNER, - OrganizationRole.IDENTITY_ADMIN, OrganizationRole.ORG_AUDITOR ) ), diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicyTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicyTest.java index 4a08adcb..187546ef 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicyTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationAuthorizationPolicyTest.java @@ -35,7 +35,7 @@ class OrganizationAuthorizationPolicyTest { OrganizationAdministrativeAction.VIEW_MEMBERS, Set.of( OrganizationRole.ORG_OWNER, - OrganizationRole.DIRECTORY_ADMIN, + OrganizationRole.MEMBER_ADMIN, OrganizationRole.ORG_AUDITOR ) ), @@ -44,6 +44,7 @@ class OrganizationAuthorizationPolicyTest { Set.of( OrganizationRole.ORG_OWNER, OrganizationRole.IDENTITY_ADMIN, + OrganizationRole.LOGIN_SECRET_ADMIN, OrganizationRole.ORG_AUDITOR ) ), @@ -65,35 +66,15 @@ class OrganizationAuthorizationPolicyTest { ), Map.entry( OrganizationAdministrativeAction.ROTATE_LOGIN_SECRETS, - Set.of(OrganizationRole.IDENTITY_ADMIN) + Set.of(OrganizationRole.LOGIN_SECRET_ADMIN) ), Map.entry( OrganizationAdministrativeAction.RESOLVE_IDENTITY_CONFLICTS, Set.of(OrganizationRole.IDENTITY_ADMIN) ), - Map.entry( - OrganizationAdministrativeAction.MANAGE_DIRECTORY_CONNECTIONS, - Set.of(OrganizationRole.DIRECTORY_ADMIN) - ), - Map.entry( - OrganizationAdministrativeAction.ROTATE_DIRECTORY_CREDENTIALS, - Set.of(OrganizationRole.DIRECTORY_ADMIN) - ), Map.entry( OrganizationAdministrativeAction.MANAGE_MEMBERS, - Set.of(OrganizationRole.DIRECTORY_ADMIN) - ), - Map.entry( - OrganizationAdministrativeAction.RUN_DIRECTORY_SYNCHRONIZATION, - Set.of(OrganizationRole.DIRECTORY_ADMIN) - ), - Map.entry( - OrganizationAdministrativeAction.MANAGE_ENTITLEMENT_MAPPINGS, - Set.of(OrganizationRole.ENTITLEMENT_ADMIN) - ), - Map.entry( - OrganizationAdministrativeAction.VIEW_MAPPING_IMPACT, - Set.of(OrganizationRole.ENTITLEMENT_ADMIN) + Set.of(OrganizationRole.MEMBER_ADMIN) ), Map.entry( OrganizationAdministrativeAction.VIEW_AUDIT, @@ -119,7 +100,7 @@ class OrganizationAuthorizationPolicyTest { void combinedRolesReceiveOnlyTheUnionOfTheirExplicitActions() { Set roles = Set.of( OrganizationRole.IDENTITY_ADMIN, - OrganizationRole.ENTITLEMENT_ADMIN + OrganizationRole.LOGIN_SECRET_ADMIN ); assertThat(policy.isAllowed( @@ -128,7 +109,7 @@ class OrganizationAuthorizationPolicyTest { )).isTrue(); assertThat(policy.isAllowed( roles, - OrganizationAdministrativeAction.MANAGE_ENTITLEMENT_MAPPINGS + OrganizationAdministrativeAction.ROTATE_LOGIN_SECRETS )).isTrue(); assertThat(policy.isAllowed( roles, diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationMembershipServiceTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationMembershipServiceTest.java index e9ec48ba..5a2c2e8f 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationMembershipServiceTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/organization/OrganizationMembershipServiceTest.java @@ -39,6 +39,9 @@ class OrganizationMembershipServiceTest { @Mock private UserAccountRepository userAccountRepository; + @Mock + private OrganizationAccessGuard accessGuard; + private OrganizationMembershipService service; @BeforeEach @@ -49,7 +52,7 @@ class OrganizationMembershipServiceTest { roleBindingRepository, userAccountRepository, new OrganizationAuthorizationService( - membershipRepository, + accessGuard, roleBindingRepository, new OrganizationAuthorizationPolicy() ) @@ -175,14 +178,10 @@ class OrganizationMembershipServiceTest { } private void givenDirectoryAdministrator() { - given(membershipRepository.findCurrentByOrganizationIdAndUserId( - ORGANIZATION_ID, - ACTOR_ID - )).willReturn(Optional.of(activeMembership(ACTOR_ID))); given(roleBindingRepository.findActiveByOrganizationIdAndUserId( ORGANIZATION_ID, ACTOR_ID - )).willReturn(List.of(binding(ACTOR_ID, OrganizationRole.DIRECTORY_ADMIN))); + )).willReturn(List.of(binding(ACTOR_ID, OrganizationRole.MEMBER_ADMIN))); } private Organization organization() { diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts index d756bff0..7be8bdf3 100644 --- a/web/src/api/generated/schema.d.ts +++ b/web/src/api/generated/schema.d.ts @@ -1996,6 +1996,278 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/organizations/{organizationId}/suspend": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["suspend"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/role-bindings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listRoleBindings"]; + put?: never; + post: operations["grantRole"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/reactivate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["reactivate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/members": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listMembers_2"]; + put?: never; + post: operations["addMember_2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/members/{membershipId}/suspend": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["suspendMember"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/members/{membershipId}/reactivate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["reactivateMember"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/members/{membershipId}/deprovision": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["deprovisionMember"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["list_5"]; + put?: never; + post: operations["create_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}/suspend": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["suspend_1"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}/revisions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["createRevision"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}/revisions/{revisionId}/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["testRevision"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}/revisions/{revisionId}/activate": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["activate"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}/disable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["disable"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/domains": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listDomains"]; + put?: never; + post: operations["issueDomainChallenge"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/domains/{domainId}/verify": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["verifyDomain"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/domains/{domainId}/disable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["disableDomain"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/decommission": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post: operations["decommission"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/device/authorize": { parameters: { query?: never; @@ -2380,6 +2652,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/admin/organizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["list_7"]; + put?: never; + post: operations["create_2"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/namespaces/{slug}/unfreeze": { parameters: { query?: never; @@ -2435,9 +2723,9 @@ export interface paths { path?: never; cookie?: never; }; - get: operations["listMembers_2"]; + get: operations["listMembers_3"]; put?: never; - post: operations["addMember_2"]; + post: operations["addMember_3"]; delete?: never; options?: never; head?: never; @@ -4336,6 +4624,54 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/organizations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["listMine"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/v1/organizations/{organizationId}/login-connections/{connectionId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get: operations["get_1"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/health": { parameters: { query?: never; @@ -4471,7 +4807,7 @@ export interface paths { path?: never; cookie?: never; }; - get: operations["list_5"]; + get: operations["list_6"]; put?: never; post?: never; delete?: never; @@ -4802,6 +5138,22 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/organizations/{organizationId}/role-bindings/{bindingId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete: operations["revokeRole"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/api/v1/admin/namespaces/{slug}/members/{userId}": { parameters: { query?: never; @@ -5568,6 +5920,233 @@ export interface components { ClawHubDeleteResponse: { ok?: boolean; }; + ApiResponseOrganizationResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["OrganizationResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + OrganizationResponse: { + id?: string; + slug?: string; + displayName?: string; + /** @enum {string} */ + status?: "ACTIVE" | "SUSPENDED" | "DECOMMISSIONED"; + /** Format: int64 */ + authorityVersion?: number; + createdBy?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + callerRoles?: ("ORG_OWNER" | "IDENTITY_ADMIN" | "LOGIN_SECRET_ADMIN" | "MEMBER_ADMIN" | "ORG_AUDITOR")[]; + }; + OrganizationRoleBindingCreateRequest: { + userId: string; + /** @enum {string} */ + role: "ORG_OWNER" | "IDENTITY_ADMIN" | "LOGIN_SECRET_ADMIN" | "MEMBER_ADMIN" | "ORG_AUDITOR"; + }; + ApiResponseOrganizationRoleBindingResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["OrganizationRoleBindingResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + OrganizationRoleBindingResponse: { + id?: string; + organizationId?: string; + userId?: string; + accountDisplayName?: string; + accountEmail?: string; + /** @enum {string} */ + role?: "ORG_OWNER" | "IDENTITY_ADMIN" | "LOGIN_SECRET_ADMIN" | "MEMBER_ADMIN" | "ORG_AUDITOR"; + /** @enum {string} */ + status?: "ACTIVE" | "REVOKED"; + createdBy?: string; + revokedBy?: string; + /** Format: date-time */ + revokedAt?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + OrganizationMemberCreateRequest: { + userId: string; + }; + ApiResponseOrganizationMemberResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["OrganizationMemberResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + OrganizationMemberResponse: { + id?: string; + organizationId?: string; + userId?: string; + /** @enum {string} */ + status?: "INVITED" | "PROVISIONED" | "ACTIVE" | "SUSPENDED" | "DEPROVISIONED"; + /** @enum {string} */ + sourceType?: "INVITATION" | "MANUAL" | "JIT"; + sourceId?: string; + organizationDisplayName?: string; + organizationEmail?: string; + accountDisplayName?: string; + accountEmail?: string; + department?: string; + employeeNumber?: string; + /** Format: int64 */ + authorityVersion?: number; + /** Format: date-time */ + activatedAt?: string; + /** Format: date-time */ + suspendedAt?: string; + /** Format: date-time */ + deprovisionedAt?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + LoginConnectionCreateRequest: { + displayName: string; + adapterKey: string; + configuration: { + [key: string]: Record; + }; + clientSecret?: string; + }; + ApiResponseLoginConnectionResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["LoginConnectionResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + LoginConnectionHealthResponse: { + status?: string; + failureReason?: string; + /** Format: date-time */ + checkedAt?: string; + }; + LoginConnectionResponse: { + id?: string; + publicHandle?: string; + displayName?: string; + adapterKey?: string; + status?: string; + activeRevisionId?: string; + lastTestedRevisionId?: string; + latestRevision?: components["schemas"]["LoginConnectionRevisionResponse"]; + secret?: components["schemas"]["LoginConnectionSecretSummaryResponse"]; + health?: components["schemas"]["LoginConnectionHealthResponse"]; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; + LoginConnectionRevisionResponse: { + id?: string; + /** Format: int64 */ + revision?: number; + adapterContractVersion?: string; + /** Format: int32 */ + configSchemaVersion?: number; + configuration?: { + [key: string]: Record; + }; + /** Format: date-time */ + createdAt?: string; + }; + LoginConnectionSecretSummaryResponse: { + configured?: boolean; + /** Format: date-time */ + updatedAt?: string; + /** Format: date-time */ + previousValidUntil?: string; + }; + ConfirmedConnectionActionRequest: { + confirmed?: boolean; + }; + LoginConnectionRevisionCreateRequest: { + configuration: { + [key: string]: Record; + }; + clientSecret?: string; + }; + ApiResponseLoginConnectionTestResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["LoginConnectionTestResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + LoginConnectionTestResponse: { + success?: boolean; + revisionId?: string; + failureReason?: string; + /** Format: date-time */ + checkedAt?: string; + }; + OrganizationDomainCreateRequest: { + domain: string; + }; + ApiResponseOrganizationDomainChallengeResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["OrganizationDomainChallengeResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + OrganizationDomainChallengeResponse: { + domainId?: string; + domain?: string; + /** @enum {string} */ + method?: "DNS_TXT"; + recordName?: string; + recordValue?: string; + }; + ApiResponseOrganizationDomainResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["OrganizationDomainResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + OrganizationDomainResponse: { + id?: string; + organizationId?: string; + domain?: string; + /** @enum {string} */ + status?: "PENDING" | "VERIFIED" | "DISABLED"; + /** @enum {string} */ + verificationMethod?: "DNS_TXT"; + /** Format: date-time */ + verifiedAt?: string; + /** Format: date-time */ + lastCheckedAt?: string; + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + updatedAt?: string; + }; AuthorizeRequest: { userCode?: string; }; @@ -5720,6 +6299,11 @@ export interface components { id?: number; status?: string; }; + OrganizationCreateRequest: { + slug: string; + displayName: string; + initialOwnerUserId: string; + }; AdminNamespaceDetailResponse: { /** Format: int64 */ id?: number; @@ -7066,6 +7650,87 @@ export interface components { match?: components["schemas"]["VersionInfo"]; latestVersion?: components["schemas"]["VersionInfo"]; }; + ApiResponsePageResponseOrganizationResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["PageResponseOrganizationResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + PageResponseOrganizationResponse: { + items?: components["schemas"]["OrganizationResponse"][]; + /** Format: int64 */ + total?: number; + /** Format: int32 */ + page?: number; + /** Format: int32 */ + size?: number; + }; + ApiResponsePageResponseOrganizationRoleBindingResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["PageResponseOrganizationRoleBindingResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + PageResponseOrganizationRoleBindingResponse: { + items?: components["schemas"]["OrganizationRoleBindingResponse"][]; + /** Format: int64 */ + total?: number; + /** Format: int32 */ + page?: number; + /** Format: int32 */ + size?: number; + }; + ApiResponsePageResponseOrganizationMemberResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["PageResponseOrganizationMemberResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + PageResponseOrganizationMemberResponse: { + items?: components["schemas"]["OrganizationMemberResponse"][]; + /** Format: int64 */ + total?: number; + /** Format: int32 */ + page?: number; + /** Format: int32 */ + size?: number; + }; + ApiResponseListLoginConnectionResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["LoginConnectionResponse"][]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + ApiResponsePageResponseOrganizationDomainResponse: { + /** Format: int32 */ + code?: number; + msg?: string; + data?: components["schemas"]["PageResponseOrganizationDomainResponse"]; + /** Format: date-time */ + timestamp?: string; + requestId?: string; + }; + PageResponseOrganizationDomainResponse: { + items?: components["schemas"]["OrganizationDomainResponse"][]; + /** Format: int64 */ + total?: number; + /** Format: int32 */ + page?: number; + /** Format: int32 */ + size?: number; + }; ApiResponseListAuthProviderResponse: { /** Format: int32 */ code?: number; @@ -11301,6 +11966,521 @@ export interface operations { }; }; }; + suspend: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationResponse"]; + }; + }; + }; + }; + listRoleBindings: { + parameters: { + query?: { + page?: number; + size?: number; + }; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponsePageResponseOrganizationRoleBindingResponse"]; + }; + }; + }; + }; + grantRole: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationRoleBindingCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationRoleBindingResponse"]; + }; + }; + }; + }; + reactivate: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationResponse"]; + }; + }; + }; + }; + listMembers_2: { + parameters: { + query?: { + page?: number; + size?: number; + }; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponsePageResponseOrganizationMemberResponse"]; + }; + }; + }; + }; + addMember_2: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationMemberCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationMemberResponse"]; + }; + }; + }; + }; + suspendMember: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + membershipId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationMemberResponse"]; + }; + }; + }; + }; + reactivateMember: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + membershipId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationMemberResponse"]; + }; + }; + }; + }; + deprovisionMember: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + membershipId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationMemberResponse"]; + }; + }; + }; + }; + list_5: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseListLoginConnectionResponse"]; + }; + }; + }; + }; + create_1: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginConnectionCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; + suspend_1: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConfirmedConnectionActionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; + createRevision: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["LoginConnectionRevisionCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; + testRevision: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + revisionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionTestResponse"]; + }; + }; + }; + }; + activate: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + revisionId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConfirmedConnectionActionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; + disable: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ConfirmedConnectionActionRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; + listDomains: { + parameters: { + query?: { + page?: number; + size?: number; + }; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponsePageResponseOrganizationDomainResponse"]; + }; + }; + }; + }; + issueDomainChallenge: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationDomainCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationDomainChallengeResponse"]; + }; + }; + }; + }; + verifyDomain: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + domainId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationDomainResponse"]; + }; + }; + }; + }; + disableDomain: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + domainId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationDomainResponse"]; + }; + }; + }; + }; + decommission: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationResponse"]; + }; + }; + }; + }; authorizeDevice: { parameters: { query?: never; @@ -11867,6 +13047,53 @@ export interface operations { }; }; }; + list_7: { + parameters: { + query?: { + page?: number; + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponsePageResponseOrganizationResponse"]; + }; + }; + }; + }; + create_2: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OrganizationCreateRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationResponse"]; + }; + }; + }; + }; unfreezeNamespace_2: { parameters: { query?: never; @@ -11937,7 +13164,7 @@ export interface operations { }; }; }; - listMembers_2: { + listMembers_3: { parameters: { query?: { page?: number; @@ -11962,7 +13189,7 @@ export interface operations { }; }; }; - addMember_2: { + addMember_3: { parameters: { query?: never; header?: never; @@ -14934,6 +16161,74 @@ export interface operations { }; }; }; + listMine: { + parameters: { + query?: { + page?: number; + size?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponsePageResponseOrganizationResponse"]; + }; + }; + }; + }; + get: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationResponse"]; + }; + }; + }; + }; + get_1: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + connectionId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseLoginConnectionResponse"]; + }; + }; + }; + }; health: { parameters: { query?: never; @@ -15110,7 +16405,7 @@ export interface operations { }; }; }; - list_5: { + list_6: { parameters: { query?: { status?: string; @@ -15599,6 +16894,29 @@ export interface operations { }; }; }; + revokeRole: { + parameters: { + query?: never; + header?: never; + path: { + organizationId: string; + bindingId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "*/*": components["schemas"]["ApiResponseOrganizationRoleBindingResponse"]; + }; + }; + }; + }; removeMember_2: { parameters: { query?: never;