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 }