初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,242 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user