243 lines
8.3 KiB
Go
243 lines
8.3 KiB
Go
package oci
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/oracle/oci-go-sdk/v65/common"
|
|
"github.com/oracle/oci-go-sdk/v65/identitydomains"
|
|
)
|
|
|
|
// TestRealOCIFederation 验证 Federation 全链路:下载域元数据 → 创建 IdP
|
|
// (默认 JIT)→ 激活并上登录页 → 免 MFA 规则置顶 → 逐级回滚清理。
|
|
// 只操作本测试创建的资源,现有 IdP 与规则只做顺延断言。
|
|
func TestRealOCIFederation(t *testing.T) {
|
|
if os.Getenv("OCI_INTEGRATION_TEST") != "1" {
|
|
t.Skip("set OCI_INTEGRATION_TEST=1 to run real OCI tests")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
|
defer cancel()
|
|
cred := loadTestCredentials(t, "试用期")
|
|
client := NewClient()
|
|
region := cred.Region
|
|
|
|
metadata, err := client.DownloadDomainSamlMetadata(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("DownloadDomainSamlMetadata: %v", err)
|
|
}
|
|
if !strings.Contains(string(metadata), "EntityDescriptor") {
|
|
t.Fatalf("metadata is not SAML XML: %s", string(metadata[:min(len(metadata), 120)]))
|
|
}
|
|
t.Logf("domain saml metadata: %d bytes", len(metadata))
|
|
|
|
before, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("ListConsoleSignOnRules: %v", err)
|
|
}
|
|
logRules(t, "before", before)
|
|
|
|
idp := createTestIdp(ctx, t, client, cred, region)
|
|
verifyIdpDefaults(ctx, t, client, cred, region, idp.ID)
|
|
|
|
activated, err := client.SetIdentityProviderEnabled(ctx, cred, region, "", idp.ID, true)
|
|
if err != nil {
|
|
t.Fatalf("activate idp: %v", err)
|
|
}
|
|
if !activated.Enabled {
|
|
t.Error("idp not enabled after activate")
|
|
}
|
|
assertLoginPage(ctx, t, client, cred, region, idp.ID, true)
|
|
|
|
exemptionRoundTrip(ctx, t, client, cred, region, idp.ID, len(before))
|
|
|
|
deactivated, err := client.SetIdentityProviderEnabled(ctx, cred, region, "", idp.ID, false)
|
|
if err != nil {
|
|
t.Fatalf("deactivate idp: %v", err)
|
|
}
|
|
if deactivated.Enabled {
|
|
t.Error("idp still enabled after deactivate")
|
|
}
|
|
assertLoginPage(ctx, t, client, cred, region, idp.ID, false)
|
|
}
|
|
|
|
func createTestIdp(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region string) IdentityProviderInfo {
|
|
t.Helper()
|
|
metadata, err := os.ReadFile("../../test-idp.xml")
|
|
if err != nil {
|
|
t.Fatalf("read test-idp.xml: %v", err)
|
|
}
|
|
name := fmt.Sprintf("oci-portal-e2e-idp-%d", time.Now().Unix())
|
|
idp, err := client.CreateSamlIdentityProvider(ctx, cred, region, "", CreateIdpInput{
|
|
Name: name,
|
|
Metadata: string(metadata),
|
|
Description: "oci-portal e2e temporary idp",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("CreateSamlIdentityProvider: %v", err)
|
|
}
|
|
t.Logf("idp created: %s (%s) partner=%s", idp.Name, idp.ID, idp.PartnerProviderID)
|
|
if idp.Enabled {
|
|
t.Error("new idp should be disabled")
|
|
}
|
|
t.Cleanup(func() {
|
|
cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
|
defer ccancel()
|
|
if err := client.DeleteIdentityProvider(cctx, cred, region, "", idp.ID); err != nil {
|
|
t.Errorf("cleanup: delete idp: %v", err)
|
|
} else {
|
|
t.Log("cleanup: idp deleted")
|
|
}
|
|
})
|
|
return idp
|
|
}
|
|
|
|
// verifyIdpDefaults 回读 IdP 断言控制台默认值与 JIT 属性映射两条。
|
|
func verifyIdpDefaults(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string) {
|
|
t.Helper()
|
|
dc, err := client.domainsClient(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("domainsClient: %v", err)
|
|
}
|
|
got, err := dc.GetIdentityProvider(ctx, identitydomains.GetIdentityProviderRequest{IdentityProviderId: &idpID})
|
|
if err != nil {
|
|
t.Fatalf("get idp: %v", err)
|
|
}
|
|
if deref(got.NameIdFormat) != "saml-none" {
|
|
t.Errorf("nameIdFormat = %q, want saml-none", deref(got.NameIdFormat))
|
|
}
|
|
if got.UserMappingMethod != identitydomains.IdentityProviderUserMappingMethodNameidtouserattribute || deref(got.UserMappingStoreAttribute) != "userName" {
|
|
t.Errorf("user mapping = %s/%s, want NameIDToUserAttribute/userName", got.UserMappingMethod, deref(got.UserMappingStoreAttribute))
|
|
}
|
|
if got.JitUserProvEnabled == nil || !*got.JitUserProvEnabled || got.JitUserProvCreateUserEnabled == nil || !*got.JitUserProvCreateUserEnabled {
|
|
t.Error("jit enable/create should be true")
|
|
}
|
|
if got.JitUserProvAttributeUpdateEnabled != nil && *got.JitUserProvAttributeUpdateEnabled {
|
|
t.Error("jit attribute update should be false")
|
|
}
|
|
if len(got.JitUserProvAssignedGroups) != 1 || deref(got.JitUserProvAssignedGroups[0].Display) != "Administrators" {
|
|
t.Errorf("jit assigned groups = %+v, want [Administrators]", got.JitUserProvAssignedGroups)
|
|
}
|
|
verifyJitMappings(ctx, t, dc, got.JitUserProvAttributes)
|
|
}
|
|
|
|
func verifyJitMappings(ctx context.Context, t *testing.T, dc identitydomains.IdentityDomainsClient, ref *identitydomains.IdentityProviderJitUserProvAttributes) {
|
|
t.Helper()
|
|
if ref == nil || ref.Value == nil {
|
|
t.Error("idp has no jit attribute mapping resource")
|
|
return
|
|
}
|
|
ma, err := dc.GetMappedAttribute(ctx, identitydomains.GetMappedAttributeRequest{MappedAttributeId: ref.Value})
|
|
if err != nil {
|
|
t.Errorf("get mapped attribute: %v", err)
|
|
return
|
|
}
|
|
want := map[string]bool{"userName": false, "name.familyName": false}
|
|
for _, m := range ma.AttributeMappings {
|
|
if deref(m.ManagedObjectAttributeName) == "$(assertion.fed.nameidvalue)" {
|
|
want[deref(m.IdcsAttributeName)] = true
|
|
}
|
|
}
|
|
for attr, seen := range want {
|
|
if !seen {
|
|
t.Errorf("jit mapping NameID value → %s missing; got %+v", attr, ma.AttributeMappings)
|
|
}
|
|
}
|
|
}
|
|
|
|
// assertLoginPage 校验 DefaultIDPRule 的 SamlIDPs 是否包含目标 IdP。
|
|
func assertLoginPage(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string, want bool) {
|
|
t.Helper()
|
|
dc, err := client.domainsClient(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("domainsClient: %v", err)
|
|
}
|
|
rule, err := dc.GetRule(ctx, identitydomains.GetRuleRequest{RuleId: common.String(defaultIdpRuleID)})
|
|
if err != nil {
|
|
t.Fatalf("get default idp rule: %v", err)
|
|
}
|
|
for _, r := range rule.Return {
|
|
if deref(r.Name) != "SamlIDPs" {
|
|
continue
|
|
}
|
|
has := strings.Contains(deref(r.Value), idpID)
|
|
if has != want {
|
|
t.Errorf("login page SamlIDPs contains idp = %v, want %v (value=%s)", has, want, deref(r.Value))
|
|
}
|
|
return
|
|
}
|
|
t.Error("DefaultIDPRule has no SamlIDPs return")
|
|
}
|
|
|
|
// exemptionRoundTrip 创建免 MFA 规则断言置顶与顺延,删除后断言完全复位。
|
|
func exemptionRoundTrip(ctx context.Context, t *testing.T, client *RealClient, cred Credentials, region, idpID string, beforeCount int) {
|
|
t.Helper()
|
|
rule, err := client.CreateMfaExemptionRule(ctx, cred, region, "", idpID, "oci-portal-e2e-skip-mfa")
|
|
if err != nil {
|
|
t.Fatalf("CreateMfaExemptionRule: %v", err)
|
|
}
|
|
t.Logf("exemption rule created: %s (%s)", rule.Name, rule.ID)
|
|
deleted := false
|
|
defer func() {
|
|
if deleted {
|
|
return
|
|
}
|
|
if err := client.DeleteMfaExemptionRule(ctx, cred, region, "", rule.ID); err != nil {
|
|
t.Errorf("cleanup: delete exemption rule: %v", err)
|
|
}
|
|
}()
|
|
|
|
after, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("ListConsoleSignOnRules after create: %v", err)
|
|
}
|
|
logRules(t, "after-create", after)
|
|
if len(after) != beforeCount+1 {
|
|
t.Errorf("rule count = %d, want %d", len(after), beforeCount+1)
|
|
}
|
|
if after[0].ID != rule.ID || after[0].Sequence != 1 {
|
|
t.Errorf("top rule = %s seq=%d, want %s seq=1", after[0].ID, after[0].Sequence, rule.ID)
|
|
}
|
|
if after[0].AuthenticationFactor != "IDP" {
|
|
t.Errorf("authenticationFactor = %s, want IDP", after[0].AuthenticationFactor)
|
|
}
|
|
if !strings.Contains(after[0].ConditionValue, idpID) {
|
|
t.Errorf("condition value %q does not reference idp", after[0].ConditionValue)
|
|
}
|
|
|
|
if err := client.DeleteMfaExemptionRule(ctx, cred, region, "", rule.ID); err != nil {
|
|
t.Fatalf("DeleteMfaExemptionRule: %v", err)
|
|
}
|
|
deleted = true
|
|
restored, err := client.ListConsoleSignOnRules(ctx, cred, region, "")
|
|
if err != nil {
|
|
t.Fatalf("ListConsoleSignOnRules after delete: %v", err)
|
|
}
|
|
logRules(t, "after-delete", restored)
|
|
if len(restored) != beforeCount {
|
|
t.Errorf("rule count after delete = %d, want %d", len(restored), beforeCount)
|
|
}
|
|
for _, r := range restored {
|
|
if r.ID == rule.ID {
|
|
t.Error("exemption rule still present after delete")
|
|
}
|
|
}
|
|
}
|
|
|
|
func logRules(t *testing.T, tag string, rules []SignOnRuleInfo) {
|
|
t.Helper()
|
|
for _, r := range rules {
|
|
t.Logf("%s: seq=%d %s (%s) factor=%s", tag, r.Sequence, r.Name, r.ID, r.AuthenticationFactor)
|
|
}
|
|
}
|
|
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|