feat(identity): finalize enterprise login control plane

This commit is contained in:
XiaoSeS 2026-09-22 14:47:19 +08:00
parent cceb8ce0d3
commit f27974efa6
28 changed files with 1971 additions and 375 deletions

View file

@ -9,9 +9,7 @@ public record LoginConnectionCreateRequest(
@NotBlank @Size(max = 128) String displayName,
@NotBlank @Size(max = 64) String adapterKey,
@NotEmpty Map<String, Object> 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=<redacted>, clientSecret="
+ (clientSecret == null ? "none" : "<redacted>")
+ ", verifiedEmailCorrelationEnabled=" + verifiedEmailCorrelationEnabled
+ ", jitProvisioningEnabled=" + jitProvisioningEnabled + "]";
+ (clientSecret == null ? "none" : "<redacted>") + "]";
}
}

View file

@ -6,16 +6,12 @@ import java.util.Map;
public record LoginConnectionRevisionCreateRequest(
@NotEmpty Map<String, Object> 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=<redacted>"
+ ", clientSecret=" + (clientSecret == null ? "unchanged" : "<redacted>")
+ ", verifiedEmailCorrelationEnabled=" + verifiedEmailCorrelationEnabled
+ ", jitProvisioningEnabled=" + jitProvisioningEnabled + "]";
+ ", clientSecret=" + (clientSecret == null ? "unchanged" : "<redacted>") + "]";
}
}

View file

@ -9,8 +9,6 @@ public record LoginConnectionRevisionResponse(
String adapterContractVersion,
int configSchemaVersion,
Map<String, Object> configuration,
boolean verifiedEmailCorrelationEnabled,
boolean jitProvisioningEnabled,
Instant createdAt
) {
}

View file

@ -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) {

View file

@ -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"));

View file

@ -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())

View file

@ -93,9 +93,6 @@ class LoginConnectionLifecyclePersistenceTest {
@Autowired
private LoginConnectionLifecycleService lifecycleService;
@Autowired
private IdentityOperationRepository identityOperationRepository;
@Autowired
private EntityManager entityManager;

View file

@ -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",

View file

@ -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<DecoderKey, LoginConnectionRuntimeSnapshotDecoder> decoders;
public BuiltInLoginConnectionRuntimeSnapshotMaterializer(
Collection<? extends LoginConnectionRuntimeSnapshotDecoder> decoders
) {
Objects.requireNonNull(decoders, "decoders");
Map<DecoderKey, LoginConnectionRuntimeSnapshotDecoder> 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) {
}
}

View file

@ -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<LoginConnection> findAllByOrganizationId(String organizationId) {
return delegate.findAllByOrganizationIdOrderByCreatedAtDescIdDesc(
requireText(organizationId, "organizationId")
);
}
@Override
@Transactional(readOnly = true)
public Optional<LoginConnection> findByOrganizationIdAndId(String organizationId, String id) {

View file

@ -18,6 +18,14 @@ public class JpaLoginConnectionRevisionRepositoryAdapter
this.delegate = Objects.requireNonNull(delegate, "delegate");
}
@Override
@Transactional(readOnly = true)
public Optional<StoredLoginConnectionRevision> findLatestByConnectionId(String connectionId) {
return delegate.findFirstByConnectionIdOrderByRevisionDescIdDesc(
requireText(connectionId, "connectionId")
);
}
@Override
@Transactional(readOnly = true)
public Optional<StoredLoginConnectionRevision> findByConnectionIdAndId(

View file

@ -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<LoginConnection> findAllByOrganizationId(String organizationId);
Optional<LoginConnection> findByOrganizationIdAndId(String organizationId, String id);
/** Serializes high-risk control-plane mutations such as Secret rotation. */

View file

@ -5,6 +5,8 @@ import java.util.Optional;
/** Connection-scoped persistence port for immutable Login Connection revisions. */
public interface LoginConnectionRevisionRepository {
Optional<StoredLoginConnectionRevision> findLatestByConnectionId(String connectionId);
Optional<StoredLoginConnectionRevision> findByConnectionIdAndId(
String connectionId,
String revisionId

View file

@ -7,6 +7,10 @@ import org.springframework.data.jpa.repository.JpaRepository;
interface LoginConnectionRevisionSpringDataRepository
extends JpaRepository<StoredLoginConnectionRevision, String> {
Optional<StoredLoginConnectionRevision> findFirstByConnectionIdOrderByRevisionDescIdDesc(
String connectionId
);
Optional<StoredLoginConnectionRevision> findByConnectionIdAndId(
String connectionId,
String id

View file

@ -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);
}

View file

@ -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<LoginConnection, String> {
List<LoginConnection> findAllByOrganizationIdOrderByCreatedAtDescIdDesc(
String organizationId
);
Optional<LoginConnection> findByOrganizationIdAndId(String organizationId, String id);
@Lock(LockModeType.PESSIMISTIC_WRITE)

View file

@ -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<String> scopes = scopes(values.get("scopes"));
AdapterDescriptor descriptor = OidcRedirectAuthenticationAdapter.supportedDescriptor();
AdapterDescriptor descriptor = supportedDescriptor();
Map<String, Object> 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<OidcLoginConnectionRuntimeConfig> 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
)
);
}
}

View file

@ -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<String> 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<String> 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;
}
}

View file

@ -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<String> 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<String> 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<AdapterCapability> 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<AdapterCapability> parseCapabilities(String json) {
JsonNode value = parse(json);
if (!value.isArray()) {
throw new ConnectionUnavailableException();
}
Set<AdapterCapability> 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<String> fieldNames(JsonNode object) {
Set<String> 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])
);
}
}

View file

@ -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 {

View file

@ -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<String> 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<String> getPrivateHostAllowlist() {
return privateHostAllowlist;
}
public void setPrivateHostAllowlist(Set<String> privateHostAllowlist) {
Objects.requireNonNull(privateHostAllowlist, "privateHostAllowlist");
LinkedHashSet<String> 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");
}
}
}

View file

@ -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<LoginConnectionRuntimeSnapshotDecoder> 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<LoginConnectionControlAdapter> 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<RedirectAuthenticationAdapter<?>> 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<LoginConnectionRuntimeSnapshotDecoder> 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<LoginConnectionControlAdapter> adapters
) {
return new LoginConnectionControlAdapterRegistry(adapters);
}
}

View file

@ -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<String> keys = List.of();
public String getActiveKeyId() {
return activeKeyId;
}
public void setActiveKeyId(String activeKeyId) {
this.activeKeyId = activeKeyId == null ? "" : activeKeyId.trim();
}
public List<String> getKeys() {
return keys;
}
public void setKeys(List<String> 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<String, byte[]> 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<SecretEnvelopeKey> 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=<redacted>, keys=<redacted>]";
}
}

View file

@ -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<OidcLoginConnectionRuntimeConfig> 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<OidcLoginConnectionRuntimeConfig> 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()
))
);
}
}

View file

@ -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
)
),

View file

@ -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<OrganizationRole> 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,

View file

@ -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() {

File diff suppressed because it is too large Load diff