mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-05 02:41:49 +00:00
feat(auth): validate built-in adapter contracts
Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
parent
4f06e69224
commit
17da5d1e3f
13 changed files with 868 additions and 5 deletions
171
scripts/check-enterprise-identity-architecture.sh
Executable file
171
scripts/check-enterprise-identity-architecture.sh
Executable file
|
|
@ -0,0 +1,171 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repository_root="$(cd "$script_dir/.." && pwd)"
|
||||
|
||||
require_rg() {
|
||||
if ! command -v rg >/dev/null 2>&1; then
|
||||
echo "enterprise identity architecture check requires rg" >&2
|
||||
exit 2
|
||||
fi
|
||||
}
|
||||
|
||||
report_matches() {
|
||||
local label="$1"
|
||||
local directory="$2"
|
||||
local pattern="$3"
|
||||
local matches
|
||||
|
||||
if [[ ! -d "$directory" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
matches="$(rg -n --glob '*.java' --regexp "$pattern" "$directory" || true)"
|
||||
if [[ -z "$matches" ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[$label]" >&2
|
||||
echo "$matches" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
check_tree() {
|
||||
local root="$1"
|
||||
local violations=0
|
||||
local domain_root="$root/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain"
|
||||
local auth_root="$root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth"
|
||||
local protected_domain
|
||||
local protected_core
|
||||
local adapter_root
|
||||
|
||||
for protected_domain in organization directory entitlement; do
|
||||
report_matches \
|
||||
"domain-$protected_domain-forbidden-dependency" \
|
||||
"$domain_root/$protected_domain" \
|
||||
'^import com\.iflytek\.skillhub\.(auth|controller|compat|infra|repository|service|scim|oauth)\.' \
|
||||
|| violations=$((violations + 1))
|
||||
report_matches \
|
||||
"domain-$protected_domain-protocol-leak" \
|
||||
"$domain_root/$protected_domain" \
|
||||
'(?i)\b(oidc|oauth|saml|scim|ldap|dingtalk|okta|azure|github|gitlab)\b' \
|
||||
|| violations=$((violations + 1))
|
||||
done
|
||||
|
||||
for protected_core in federation/core identity/core connection/core; do
|
||||
report_matches \
|
||||
"identity-core-concrete-adapter-import" \
|
||||
"$auth_root/$protected_core" \
|
||||
'^import .*\.adapter\.' \
|
||||
|| violations=$((violations + 1))
|
||||
report_matches \
|
||||
"identity-core-provider-branch" \
|
||||
"$auth_root/$protected_core" \
|
||||
'(?i)\b(github|gitlab|dingtalk|okta|azure|keycloak|auth0)\b' \
|
||||
|| violations=$((violations + 1))
|
||||
done
|
||||
|
||||
for adapter_root in \
|
||||
"$auth_root/federation/adapter" \
|
||||
"$auth_root/connection/adapter"; do
|
||||
report_matches \
|
||||
"adapter-direct-state-write" \
|
||||
"$adapter_root" \
|
||||
'^import .*\.(UserAccountRepository|OrganizationMembershipRepository|NamespaceMemberRepository|NamespaceMemberGrantRepository);' \
|
||||
|| violations=$((violations + 1))
|
||||
report_matches \
|
||||
"adapter-runtime-code-loading" \
|
||||
"$adapter_root" \
|
||||
'(Class\.forName|URLClassLoader|ScriptEngineManager)' \
|
||||
|| violations=$((violations + 1))
|
||||
done
|
||||
|
||||
if (( violations > 0 )); then
|
||||
echo "enterprise identity architecture violations: $violations" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
self_test() {
|
||||
local fixture_root
|
||||
local violation_output
|
||||
fixture_root="$(mktemp -d -t skillhub-identity-architecture.XXXXXX)"
|
||||
|
||||
cleanup_fixture() {
|
||||
if [[ -n "${fixture_root:-}" && -d "$fixture_root" && "$fixture_root" == /tmp/skillhub-identity-architecture.* ]]; then
|
||||
rm -rf -- "$fixture_root"
|
||||
fi
|
||||
}
|
||||
trap cleanup_fixture EXIT
|
||||
|
||||
mkdir -p \
|
||||
"$fixture_root/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization" \
|
||||
"$fixture_root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/core" \
|
||||
"$fixture_root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/adapter"
|
||||
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.domain.organization;' \
|
||||
'public record Organization(String id) {}' \
|
||||
> "$fixture_root/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/Organization.java"
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.auth.identity.core;' \
|
||||
'public final class IdentityDecision {}' \
|
||||
> "$fixture_root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/core/IdentityDecision.java"
|
||||
|
||||
if ! check_tree "$fixture_root" >/dev/null 2>&1; then
|
||||
echo "architecture self-test rejected the approved fixture" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.domain.organization;' \
|
||||
'import com.iflytek.skillhub.auth.identity.IdentityBindingService;' \
|
||||
'public final class ForbiddenDomainDependency {}' \
|
||||
> "$fixture_root/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/ForbiddenDomainDependency.java"
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.auth.identity.core;' \
|
||||
'public final class ProviderBranch { String provider = "github"; }' \
|
||||
> "$fixture_root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/identity/core/ProviderBranch.java"
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.auth.federation.adapter;' \
|
||||
'import com.iflytek.skillhub.domain.user.UserAccountRepository;' \
|
||||
'public final class DirectStateWrite { void load() throws Exception { Class.forName("evil.Plugin"); } }' \
|
||||
> "$fixture_root/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/federation/adapter/DirectStateWrite.java"
|
||||
printf '%s\n' \
|
||||
'package com.iflytek.skillhub.domain.organization;' \
|
||||
'public final class ScimWirePayload { String protocol = "SCIM"; }' \
|
||||
> "$fixture_root/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/organization/ScimWirePayload.java"
|
||||
|
||||
if violation_output="$(check_tree "$fixture_root" 2>&1)"; then
|
||||
echo "architecture self-test accepted intentional violations" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for expected in \
|
||||
domain-organization-forbidden-dependency \
|
||||
domain-organization-protocol-leak \
|
||||
identity-core-provider-branch \
|
||||
adapter-direct-state-write \
|
||||
adapter-runtime-code-loading; do
|
||||
if [[ "$violation_output" != *"[$expected]"* ]]; then
|
||||
echo "architecture self-test did not detect $expected" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "enterprise identity architecture self-test passed"
|
||||
}
|
||||
|
||||
require_rg
|
||||
if [[ "${1:-}" == "--self-test" ]]; then
|
||||
self_test
|
||||
exit 0
|
||||
fi
|
||||
if [[ $# -ne 0 ]]; then
|
||||
echo "usage: $0 [--self-test]" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
check_tree "$repository_root"
|
||||
echo "enterprise identity architecture check passed"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
/** Platform-understood behavior that an adapter may safely advertise. */
|
||||
public enum AdapterCapability {
|
||||
IDENTITY_ASSERTION,
|
||||
VERIFIED_EMAIL_ASSERTION,
|
||||
PROFILE_ATTRIBUTE_ASSERTION,
|
||||
DIRECTORY_USERS,
|
||||
DIRECTORY_GROUPS,
|
||||
INCREMENTAL_RECONCILIATION,
|
||||
DEPROVISIONING
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
/** Explicit adapter contract version; only major changes may break persisted revisions. */
|
||||
public record AdapterContractVersion(int major, int minor) {
|
||||
|
||||
public AdapterContractVersion {
|
||||
if (major < 1) {
|
||||
throw new IllegalArgumentException("adapter contract major version must be positive");
|
||||
}
|
||||
if (minor < 0) {
|
||||
throw new IllegalArgumentException("adapter contract minor version must not be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
/** Persistable adapter identity and platform capabilities, without implementation class names. */
|
||||
public record AdapterDescriptor(
|
||||
AdapterKey adapterKey,
|
||||
AdapterContractVersion contractVersion,
|
||||
ConnectionKind connectionKind,
|
||||
int configSchemaVersion,
|
||||
Optional<InteractionModel> interactionModel,
|
||||
Set<AdapterCapability> capabilities
|
||||
) {
|
||||
|
||||
public AdapterDescriptor {
|
||||
Objects.requireNonNull(adapterKey, "adapterKey");
|
||||
Objects.requireNonNull(contractVersion, "contractVersion");
|
||||
Objects.requireNonNull(connectionKind, "connectionKind");
|
||||
if (configSchemaVersion < 1) {
|
||||
throw new IllegalArgumentException("config schema version must be positive");
|
||||
}
|
||||
interactionModel = Objects.requireNonNull(interactionModel, "interactionModel");
|
||||
capabilities = Set.copyOf(Objects.requireNonNull(capabilities, "capabilities"));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
/** Validated catalog of built-in descriptors keyed by stable adapter key, contract and schema version. */
|
||||
public final class AdapterDescriptorRegistry {
|
||||
|
||||
private static final Set<Integer> SUPPORTED_CONTRACT_MAJORS = Set.of(1);
|
||||
private static final Set<AdapterCapability> LOGIN_CAPABILITIES = Set.of(
|
||||
AdapterCapability.IDENTITY_ASSERTION,
|
||||
AdapterCapability.VERIFIED_EMAIL_ASSERTION,
|
||||
AdapterCapability.PROFILE_ATTRIBUTE_ASSERTION
|
||||
);
|
||||
private static final Set<AdapterCapability> DIRECTORY_CAPABILITIES = Set.of(
|
||||
AdapterCapability.DIRECTORY_USERS,
|
||||
AdapterCapability.DIRECTORY_GROUPS,
|
||||
AdapterCapability.INCREMENTAL_RECONCILIATION,
|
||||
AdapterCapability.DEPROVISIONING
|
||||
);
|
||||
|
||||
private final Map<RegistrationKey, AdapterDescriptor> descriptorsByKey;
|
||||
private final List<AdapterDescriptor> descriptors;
|
||||
|
||||
public AdapterDescriptorRegistry(Collection<AdapterDescriptor> descriptors) {
|
||||
Objects.requireNonNull(descriptors, "descriptors");
|
||||
List<AdapterDescriptor> ordered = new ArrayList<>(descriptors);
|
||||
ordered.forEach(descriptor -> Objects.requireNonNull(descriptor, "descriptor"));
|
||||
ordered.sort(Comparator
|
||||
.comparing((AdapterDescriptor descriptor) -> descriptor.adapterKey().value())
|
||||
.thenComparingInt(descriptor -> descriptor.contractVersion().major())
|
||||
.thenComparingInt(descriptor -> descriptor.contractVersion().minor())
|
||||
.thenComparingInt(AdapterDescriptor::configSchemaVersion));
|
||||
|
||||
Map<RegistrationKey, AdapterDescriptor> validated = new LinkedHashMap<>();
|
||||
for (AdapterDescriptor descriptor : ordered) {
|
||||
validate(descriptor);
|
||||
RegistrationKey key = RegistrationKey.from(descriptor);
|
||||
if (validated.putIfAbsent(key, descriptor) != null) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.DUPLICATE_REGISTRATION,
|
||||
descriptor,
|
||||
"duplicate adapter key, contract version and config schema version"
|
||||
);
|
||||
}
|
||||
}
|
||||
this.descriptorsByKey = Map.copyOf(validated);
|
||||
this.descriptors = List.copyOf(ordered);
|
||||
}
|
||||
|
||||
public AdapterDescriptor require(
|
||||
AdapterKey adapterKey,
|
||||
AdapterContractVersion contractVersion,
|
||||
int configSchemaVersion
|
||||
) {
|
||||
Objects.requireNonNull(adapterKey, "adapterKey");
|
||||
Objects.requireNonNull(contractVersion, "contractVersion");
|
||||
if (configSchemaVersion < 1) {
|
||||
throw new IllegalArgumentException("config schema version must be positive");
|
||||
}
|
||||
AdapterDescriptor descriptor = descriptorsByKey.get(new RegistrationKey(
|
||||
adapterKey,
|
||||
contractVersion,
|
||||
configSchemaVersion
|
||||
));
|
||||
if (descriptor == null) {
|
||||
boolean sameContract = descriptors.stream().anyMatch(candidate ->
|
||||
candidate.adapterKey().equals(adapterKey)
|
||||
&& candidate.contractVersion().equals(contractVersion));
|
||||
if (sameContract) {
|
||||
throw new AdapterRegistryException(
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONFIG_SCHEMA_VERSION,
|
||||
"Adapter is not registered for the requested config schema version"
|
||||
);
|
||||
}
|
||||
boolean sameKey = descriptors.stream().anyMatch(candidate ->
|
||||
candidate.adapterKey().equals(adapterKey));
|
||||
if (sameKey) {
|
||||
throw new AdapterRegistryException(
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONTRACT_VERSION,
|
||||
"Adapter is not registered for the requested contract version"
|
||||
);
|
||||
}
|
||||
throw new AdapterRegistryException(
|
||||
AdapterRegistryFailureReason.ADAPTER_NOT_REGISTERED,
|
||||
"Adapter is not registered for the requested key"
|
||||
);
|
||||
}
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
public List<AdapterDescriptor> descriptors() {
|
||||
return descriptors;
|
||||
}
|
||||
|
||||
private static void validate(AdapterDescriptor descriptor) {
|
||||
if (!SUPPORTED_CONTRACT_MAJORS.contains(descriptor.contractVersion().major())) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONTRACT_VERSION,
|
||||
descriptor,
|
||||
"unsupported adapter contract major"
|
||||
);
|
||||
}
|
||||
switch (descriptor.connectionKind()) {
|
||||
case LOGIN -> validateLogin(descriptor);
|
||||
case DIRECTORY -> validateDirectory(descriptor);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateLogin(AdapterDescriptor descriptor) {
|
||||
if (descriptor.interactionModel().isEmpty()) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.INVALID_INTERACTION_MODEL,
|
||||
descriptor,
|
||||
"login adapter must declare an interaction model"
|
||||
);
|
||||
}
|
||||
if (!LOGIN_CAPABILITIES.containsAll(descriptor.capabilities())) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.INVALID_CAPABILITY_SET,
|
||||
descriptor,
|
||||
"login adapter declares a non-login capability"
|
||||
);
|
||||
}
|
||||
if (!descriptor.capabilities().contains(AdapterCapability.IDENTITY_ASSERTION)) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.MISSING_REQUIRED_CAPABILITY,
|
||||
descriptor,
|
||||
"login adapter must emit an identity assertion"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static void validateDirectory(AdapterDescriptor descriptor) {
|
||||
if (descriptor.interactionModel().isPresent()) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.INVALID_INTERACTION_MODEL,
|
||||
descriptor,
|
||||
"directory adapter must not declare a login interaction model"
|
||||
);
|
||||
}
|
||||
if (!DIRECTORY_CAPABILITIES.containsAll(descriptor.capabilities())) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.INVALID_CAPABILITY_SET,
|
||||
descriptor,
|
||||
"directory adapter declares a non-directory capability"
|
||||
);
|
||||
}
|
||||
if (!descriptor.capabilities().contains(AdapterCapability.DIRECTORY_USERS)) {
|
||||
throw failure(
|
||||
AdapterRegistryFailureReason.MISSING_REQUIRED_CAPABILITY,
|
||||
descriptor,
|
||||
"directory capabilities require directory user provisioning"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private static AdapterRegistryException failure(
|
||||
AdapterRegistryFailureReason reason,
|
||||
AdapterDescriptor descriptor,
|
||||
String detail
|
||||
) {
|
||||
return new AdapterRegistryException(
|
||||
reason,
|
||||
detail + ": " + descriptor.adapterKey().value()
|
||||
);
|
||||
}
|
||||
|
||||
private record RegistrationKey(
|
||||
AdapterKey adapterKey,
|
||||
AdapterContractVersion contractVersion,
|
||||
int configSchemaVersion
|
||||
) {
|
||||
|
||||
private static RegistrationKey from(AdapterDescriptor descriptor) {
|
||||
return new RegistrationKey(
|
||||
descriptor.adapterKey(),
|
||||
descriptor.contractVersion(),
|
||||
descriptor.configSchemaVersion()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/** Deterministic startup or lookup failure for the built-in adapter registry. */
|
||||
public final class AdapterRegistryException extends IllegalStateException {
|
||||
|
||||
private final AdapterRegistryFailureReason reason;
|
||||
|
||||
public AdapterRegistryException(AdapterRegistryFailureReason reason, String message) {
|
||||
super(message);
|
||||
this.reason = Objects.requireNonNull(reason, "reason");
|
||||
}
|
||||
|
||||
public AdapterRegistryFailureReason reason() {
|
||||
return reason;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
/** Stable failure categories for adapter registration and lookup. */
|
||||
public enum AdapterRegistryFailureReason {
|
||||
DUPLICATE_REGISTRATION,
|
||||
UNSUPPORTED_CONTRACT_VERSION,
|
||||
UNSUPPORTED_CONFIG_SCHEMA_VERSION,
|
||||
INVALID_INTERACTION_MODEL,
|
||||
INVALID_CAPABILITY_SET,
|
||||
MISSING_REQUIRED_CAPABILITY,
|
||||
IMPLEMENTATION_MISMATCH,
|
||||
ADAPTER_NOT_REGISTERED
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
/** Independent connection planes understood by the platform registry. */
|
||||
public enum ConnectionKind {
|
||||
LOGIN,
|
||||
DIRECTORY
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package com.iflytek.skillhub.auth.federation.adapter;
|
||||
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterDescriptor;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterDescriptorRegistry;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterContractVersion;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterKey;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterRegistryException;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterRegistryFailureReason;
|
||||
import com.iflytek.skillhub.auth.connection.core.ConnectionKind;
|
||||
import com.iflytek.skillhub.auth.connection.core.InteractionModel;
|
||||
import com.iflytek.skillhub.auth.federation.core.RedirectAuthenticationAdapter;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/** Registry of reviewed redirect adapter instances assembled by the application build. */
|
||||
public final class BuiltInRedirectAuthenticationAdapterRegistry {
|
||||
|
||||
private final AdapterDescriptorRegistry descriptors;
|
||||
private final Map<RegistrationKey, RedirectAuthenticationAdapter<?>> adapters;
|
||||
|
||||
public BuiltInRedirectAuthenticationAdapterRegistry(
|
||||
Collection<? extends RedirectAuthenticationAdapter<?>> adapters
|
||||
) {
|
||||
Objects.requireNonNull(adapters, "adapters");
|
||||
this.descriptors = new AdapterDescriptorRegistry(
|
||||
adapters.stream().map(RedirectAuthenticationAdapter::descriptor).toList()
|
||||
);
|
||||
Map<RegistrationKey, RedirectAuthenticationAdapter<?>> validated = new LinkedHashMap<>();
|
||||
for (RedirectAuthenticationAdapter<?> adapter : adapters) {
|
||||
Objects.requireNonNull(adapter, "adapter");
|
||||
AdapterDescriptor descriptor = adapter.descriptor();
|
||||
if (descriptor.connectionKind() != ConnectionKind.LOGIN
|
||||
|| descriptor.interactionModel().orElse(null) != InteractionModel.REDIRECT) {
|
||||
throw new AdapterRegistryException(
|
||||
AdapterRegistryFailureReason.IMPLEMENTATION_MISMATCH,
|
||||
"Redirect adapter descriptor does not declare a redirect login interaction"
|
||||
);
|
||||
}
|
||||
Objects.requireNonNull(adapter.configType(), "adapter configType");
|
||||
validated.put(RegistrationKey.from(descriptor), adapter);
|
||||
}
|
||||
this.adapters = Map.copyOf(validated);
|
||||
}
|
||||
|
||||
public RedirectAuthenticationAdapter<?> require(
|
||||
AdapterKey adapterKey,
|
||||
AdapterContractVersion contractVersion,
|
||||
int configSchemaVersion
|
||||
) {
|
||||
descriptors.require(adapterKey, contractVersion, configSchemaVersion);
|
||||
return adapters.get(new RegistrationKey(adapterKey, contractVersion, configSchemaVersion));
|
||||
}
|
||||
|
||||
private record RegistrationKey(
|
||||
AdapterKey adapterKey,
|
||||
AdapterContractVersion contractVersion,
|
||||
int configSchemaVersion
|
||||
) {
|
||||
|
||||
private static RegistrationKey from(AdapterDescriptor descriptor) {
|
||||
return new RegistrationKey(
|
||||
descriptor.adapterKey(),
|
||||
descriptor.contractVersion(),
|
||||
descriptor.configSchemaVersion()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
package com.iflytek.skillhub.auth.federation.core;
|
||||
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterKey;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterDescriptor;
|
||||
import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeConfig;
|
||||
import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot;
|
||||
|
||||
/** Two-phase contract for redirect-based authentication adapters. */
|
||||
public interface RedirectAuthenticationAdapter<C extends LoginConnectionRuntimeConfig> {
|
||||
|
||||
AdapterKey adapterKey();
|
||||
AdapterDescriptor descriptor();
|
||||
|
||||
Class<C> configType();
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,243 @@
|
|||
package com.iflytek.skillhub.auth.connection.core;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class AdapterDescriptorRegistryTest {
|
||||
|
||||
@Test
|
||||
void require_resolvesVersionedBuiltInLoginAndDirectoryDescriptors() {
|
||||
AdapterDescriptor login = loginDescriptor(
|
||||
"oidc-standard",
|
||||
new AdapterContractVersion(1, 2),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(
|
||||
AdapterCapability.IDENTITY_ASSERTION,
|
||||
AdapterCapability.VERIFIED_EMAIL_ASSERTION
|
||||
)
|
||||
);
|
||||
AdapterDescriptor directory = new AdapterDescriptor(
|
||||
new AdapterKey("scim-directory"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.DIRECTORY,
|
||||
1,
|
||||
Optional.empty(),
|
||||
Set.of(
|
||||
AdapterCapability.DIRECTORY_USERS,
|
||||
AdapterCapability.DIRECTORY_GROUPS,
|
||||
AdapterCapability.DEPROVISIONING
|
||||
)
|
||||
);
|
||||
AdapterDescriptorRegistry registry = new AdapterDescriptorRegistry(List.of(login, directory));
|
||||
|
||||
assertThat(registry.require(new AdapterKey("oidc-standard"), new AdapterContractVersion(1, 2), 1))
|
||||
.isSameAs(login);
|
||||
assertThat(registry.require(new AdapterKey("scim-directory"), new AdapterContractVersion(1, 0), 1))
|
||||
.isSameAs(directory);
|
||||
assertThat(registry.descriptors()).containsExactly(login, directory);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsDuplicateAdapterKeyContractVersionAndConfigSchemaVersion() {
|
||||
AdapterDescriptor first = loginDescriptor(
|
||||
"duplicate-login",
|
||||
new AdapterContractVersion(1, 0),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
AdapterDescriptor second = loginDescriptor(
|
||||
"duplicate-login",
|
||||
new AdapterContractVersion(1, 0),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(second, first)),
|
||||
AdapterRegistryFailureReason.DUPLICATE_REGISTRATION
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_allowsSameAdapterKeyWithDifferentMinorOrConfigSchemaVersion() {
|
||||
AdapterDescriptor first = loginDescriptor(
|
||||
"versioned-login",
|
||||
new AdapterContractVersion(1, 0),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
AdapterDescriptor second = new AdapterDescriptor(
|
||||
new AdapterKey("versioned-login"),
|
||||
new AdapterContractVersion(1, 1),
|
||||
ConnectionKind.LOGIN,
|
||||
2,
|
||||
Optional.of(InteractionModel.REDIRECT),
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
|
||||
AdapterDescriptorRegistry registry = new AdapterDescriptorRegistry(List.of(second, first));
|
||||
|
||||
assertThat(registry.require(new AdapterKey("versioned-login"), new AdapterContractVersion(1, 0), 1))
|
||||
.isSameAs(first);
|
||||
assertThat(registry.require(new AdapterKey("versioned-login"), new AdapterContractVersion(1, 1), 2))
|
||||
.isSameAs(second);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsUnsupportedContractMajorVersion() {
|
||||
AdapterDescriptor future = loginDescriptor(
|
||||
"future-login",
|
||||
new AdapterContractVersion(2, 0),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(future)),
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONTRACT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsLoginWithoutInteractionModel() {
|
||||
AdapterDescriptor invalid = new AdapterDescriptor(
|
||||
new AdapterKey("missing-interaction"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.LOGIN,
|
||||
1,
|
||||
Optional.empty(),
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(invalid)),
|
||||
AdapterRegistryFailureReason.INVALID_INTERACTION_MODEL
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsDirectoryWithLoginInteractionModel() {
|
||||
AdapterDescriptor invalid = new AdapterDescriptor(
|
||||
new AdapterKey("directory-redirect"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.DIRECTORY,
|
||||
1,
|
||||
Optional.of(InteractionModel.REDIRECT),
|
||||
Set.of(AdapterCapability.DIRECTORY_USERS)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(invalid)),
|
||||
AdapterRegistryFailureReason.INVALID_INTERACTION_MODEL
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsCapabilitiesFromAnotherConnectionKind() {
|
||||
AdapterDescriptor invalid = loginDescriptor(
|
||||
"login-with-directory-write",
|
||||
new AdapterContractVersion(1, 0),
|
||||
InteractionModel.PASSIVE_ASSERTION,
|
||||
Set.of(
|
||||
AdapterCapability.IDENTITY_ASSERTION,
|
||||
AdapterCapability.DIRECTORY_USERS
|
||||
)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(invalid)),
|
||||
AdapterRegistryFailureReason.INVALID_CAPABILITY_SET
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsCapabilitiesWhoseDependenciesAreMissing() {
|
||||
AdapterDescriptor invalid = new AdapterDescriptor(
|
||||
new AdapterKey("groups-without-users"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.DIRECTORY,
|
||||
1,
|
||||
Optional.empty(),
|
||||
Set.of(AdapterCapability.DIRECTORY_GROUPS)
|
||||
);
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> new AdapterDescriptorRegistry(List.of(invalid)),
|
||||
AdapterRegistryFailureReason.MISSING_REQUIRED_CAPABILITY
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void require_failsClosedWhenRequestedContractMinorIsNotRegistered() {
|
||||
AdapterDescriptor descriptor = loginDescriptor(
|
||||
"oidc-standard",
|
||||
new AdapterContractVersion(1, 2),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
AdapterDescriptorRegistry registry = new AdapterDescriptorRegistry(List.of(descriptor));
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> registry.require(new AdapterKey("oidc-standard"), new AdapterContractVersion(1, 0), 1),
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONTRACT_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void require_failsClosedWhenRequestedConfigSchemaVersionIsNotRegistered() {
|
||||
AdapterDescriptor descriptor = loginDescriptor(
|
||||
"oidc-standard",
|
||||
new AdapterContractVersion(1, 2),
|
||||
InteractionModel.REDIRECT,
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
AdapterDescriptorRegistry registry = new AdapterDescriptorRegistry(List.of(descriptor));
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> registry.require(new AdapterKey("oidc-standard"), new AdapterContractVersion(1, 2), 2),
|
||||
AdapterRegistryFailureReason.UNSUPPORTED_CONFIG_SCHEMA_VERSION
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void require_failsClosedForUnknownStableKeyWithoutLoadingAClassName() {
|
||||
AdapterDescriptorRegistry registry = new AdapterDescriptorRegistry(List.of());
|
||||
|
||||
assertRegistryFailure(
|
||||
() -> registry.require(new AdapterKey("database-class-name"), new AdapterContractVersion(1, 0), 1),
|
||||
AdapterRegistryFailureReason.ADAPTER_NOT_REGISTERED
|
||||
);
|
||||
}
|
||||
|
||||
private static AdapterDescriptor loginDescriptor(
|
||||
String key,
|
||||
AdapterContractVersion contractVersion,
|
||||
InteractionModel interactionModel,
|
||||
Set<AdapterCapability> capabilities
|
||||
) {
|
||||
return new AdapterDescriptor(
|
||||
new AdapterKey(key),
|
||||
contractVersion,
|
||||
ConnectionKind.LOGIN,
|
||||
1,
|
||||
Optional.of(interactionModel),
|
||||
capabilities
|
||||
);
|
||||
}
|
||||
|
||||
private static void assertRegistryFailure(
|
||||
Runnable operation,
|
||||
AdapterRegistryFailureReason reason
|
||||
) {
|
||||
assertThatThrownBy(operation::run)
|
||||
.isInstanceOfSatisfying(
|
||||
AdapterRegistryException.class,
|
||||
failure -> assertThat(failure.reason()).isEqualTo(reason)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package com.iflytek.skillhub.auth.federation.adapter;
|
||||
|
||||
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.AdapterRegistryException;
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterRegistryFailureReason;
|
||||
import com.iflytek.skillhub.auth.connection.core.ConnectionKind;
|
||||
import com.iflytek.skillhub.auth.connection.core.InteractionModel;
|
||||
import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeConfig;
|
||||
import com.iflytek.skillhub.auth.connection.core.LoginConnectionRuntimeSnapshot;
|
||||
import com.iflytek.skillhub.auth.federation.core.IdentityAssertion;
|
||||
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.federation.core.RedirectStartResult;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class BuiltInRedirectAuthenticationAdapterRegistryTest {
|
||||
|
||||
@Test
|
||||
void require_returnsCompileTimeRegisteredAdapterByStableKeyAndMajorVersion() {
|
||||
TestRedirectAdapter adapter = new TestRedirectAdapter(descriptor(InteractionModel.REDIRECT));
|
||||
BuiltInRedirectAuthenticationAdapterRegistry registry =
|
||||
new BuiltInRedirectAuthenticationAdapterRegistry(List.of(adapter));
|
||||
|
||||
assertThat(registry.require(new AdapterKey("test-redirect"), new AdapterContractVersion(1, 0), 1))
|
||||
.isSameAs(adapter);
|
||||
}
|
||||
|
||||
@Test
|
||||
void constructor_rejectsDescriptorWhoseInteractionDoesNotMatchRedirectInterface() {
|
||||
TestRedirectAdapter adapter = new TestRedirectAdapter(descriptor(InteractionModel.CREDENTIAL));
|
||||
|
||||
assertThatThrownBy(() -> new BuiltInRedirectAuthenticationAdapterRegistry(List.of(adapter)))
|
||||
.isInstanceOfSatisfying(
|
||||
AdapterRegistryException.class,
|
||||
failure -> assertThat(failure.reason())
|
||||
.isEqualTo(AdapterRegistryFailureReason.IMPLEMENTATION_MISMATCH)
|
||||
);
|
||||
}
|
||||
|
||||
private static AdapterDescriptor descriptor(InteractionModel interactionModel) {
|
||||
return new AdapterDescriptor(
|
||||
new AdapterKey("test-redirect"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.LOGIN,
|
||||
1,
|
||||
Optional.of(interactionModel),
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
}
|
||||
|
||||
private record TestConfig() implements LoginConnectionRuntimeConfig {
|
||||
}
|
||||
|
||||
private record TestRedirectAdapter(AdapterDescriptor descriptor)
|
||||
implements RedirectAuthenticationAdapter<TestConfig> {
|
||||
|
||||
@Override
|
||||
public Class<TestConfig> configType() {
|
||||
return TestConfig.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RedirectStartResult start(
|
||||
LoginConnectionRuntimeSnapshot<TestConfig> connection,
|
||||
RedirectStartRequest request
|
||||
) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IdentityAssertion complete(
|
||||
LoginConnectionRuntimeSnapshot<TestConfig> connection,
|
||||
RedirectCompleteRequest request
|
||||
) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,11 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
import com.iflytek.skillhub.auth.connection.core.AdapterKey;
|
||||
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.ConnectionHandle;
|
||||
import com.iflytek.skillhub.auth.connection.core.ConnectionKind;
|
||||
import com.iflytek.skillhub.auth.connection.core.ConnectionUnavailableException;
|
||||
import com.iflytek.skillhub.auth.connection.core.EnterpriseConnectionRegistry;
|
||||
import com.iflytek.skillhub.auth.connection.core.InteractionModel;
|
||||
|
|
@ -89,7 +93,7 @@ class RedirectAuthenticationAdapterContractTest {
|
|||
);
|
||||
assertThat(RedirectAuthenticationAdapter.class.getDeclaredMethods())
|
||||
.extracting(Method::getName)
|
||||
.containsExactlyInAnyOrder("adapterKey", "configType", "start", "complete");
|
||||
.containsExactlyInAnyOrder("descriptor", "configType", "start", "complete");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -118,8 +122,15 @@ class RedirectAuthenticationAdapterContractTest {
|
|||
private static final class TestRedirectAdapter implements RedirectAuthenticationAdapter<TestRuntimeConfig> {
|
||||
|
||||
@Override
|
||||
public AdapterKey adapterKey() {
|
||||
return new AdapterKey("test-redirect");
|
||||
public AdapterDescriptor descriptor() {
|
||||
return new AdapterDescriptor(
|
||||
new AdapterKey("test-redirect"),
|
||||
new AdapterContractVersion(1, 0),
|
||||
ConnectionKind.LOGIN,
|
||||
1,
|
||||
Optional.of(InteractionModel.REDIRECT),
|
||||
Set.of(AdapterCapability.IDENTITY_ASSERTION)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue