初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+438
View File
@@ -0,0 +1,438 @@
package oci
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identitydomains"
)
// defaultIdpRuleID 是 Default Identity Provider Policy 的预置规则固定 ID
// 其 return 中 SamlIDPs 数组决定登录页显示哪些 SAML IdP。
const defaultIdpRuleID = "DefaultIDPRule"
// samlMetadataPath 是 Identity Domain 的 SP SAML 元数据端点。
const samlMetadataPath = "/fed/v1/metadata"
// idpSchema 是 IdentityProvider 资源的 SCIM schema。
const idpSchema = "urn:ietf:params:scim:schemas:oracle:idcs:IdentityProvider"
// IdentityProviderInfo 是一个外部身份提供者的关键字段。
type IdentityProviderInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Enabled bool `json:"enabled"`
PartnerProviderID string `json:"partnerProviderId"`
JitEnabled bool `json:"jitEnabled"`
TimeCreated *time.Time `json:"timeCreated,omitempty"`
}
// CreateIdpInput 是创建 SAML IdP 的输入;零值即控制台默认行为:
// 名称 ID 格式「无」、SAML 断言名称 ID 映射到用户名、JIT 开启建用户不更新、
// 静态分配 Administrators 组(service 层负责把缺省字段填成这些默认值)。
type CreateIdpInput struct {
Name string // partnerName,必填
Metadata string // IdP metadata XML,必填
Description string
IconURL string // 登录页 logo,仅接受 http(s) 外链 URLIDCS 拒绝 data URI),空则不设置
// 映射用户身份
NameIDFormat string // 请求的名称 ID 格式,如 saml-none / saml-emailaddress
AssertionAttribute string // 非空时改用断言属性映射,值为断言中的属性名;空用 NameID
UserStoreAttribute string // 目标域用户属性:userName 或 emails.primary(主邮箱)
// JIT 预配
JitEnabled bool
JitCreate bool // JIT 时创建新用户
JitUpdate bool // JIT 时更新既有用户属性
JitAssignAdminGroup bool // 静态分配 Administrators 组
JitMapEmail bool // 属性映射额外映射主邮箱(域要求邮箱必填时需开启)
}
// ListIdentityProviders 实现 Client:列出域内 SAML 身份提供者。
func (c *RealClient) ListIdentityProviders(ctx context.Context, cred Credentials, region string) ([]IdentityProviderInfo, error) {
dc, err := c.domainsClient(ctx, cred, region)
if err != nil {
return nil, err
}
resp, err := dc.ListIdentityProviders(ctx, identitydomains.ListIdentityProvidersRequest{})
if err != nil {
return nil, fmt.Errorf("list identity providers: %w", err)
}
idps := make([]IdentityProviderInfo, 0, len(resp.Resources))
for _, idp := range resp.Resources {
if idp.Type != identitydomains.IdentityProviderTypeSaml {
continue
}
idps = append(idps, toIdpInfo(idp))
}
return idps, nil
}
// CreateSamlIdentityProvider 实现 Client:按输入创建禁用态 SAML IdP 并配置 JIT。
func (c *RealClient) CreateSamlIdentityProvider(ctx context.Context, cred Credentials, region string, in CreateIdpInput) (IdentityProviderInfo, error) {
dc, err := c.domainsClient(ctx, cred, region)
if err != nil {
return IdentityProviderInfo{}, err
}
var adminGroup *identitydomains.IdentityProviderJitUserProvAssignedGroups
if in.JitEnabled && in.JitAssignAdminGroup {
if adminGroup, err = adminGroupRef(ctx, dc); err != nil {
return IdentityProviderInfo{}, err
}
}
resp, err := dc.CreateIdentityProvider(ctx, identitydomains.CreateIdentityProviderRequest{
IdentityProvider: buildSamlIdp(in, adminGroup),
})
if err != nil {
return IdentityProviderInfo{}, fmt.Errorf("create identity provider %s: %w", in.Name, err)
}
if in.JitEnabled {
if err := ensureJitAttributeMappings(ctx, dc, resp.IdentityProvider, jitMappings(in)); err != nil {
return toIdpInfo(resp.IdentityProvider), err
}
}
return toIdpInfo(resp.IdentityProvider), nil
}
// buildSamlIdp 按输入组装 SAML IdP(创建为禁用态,启用走 activate 接口)。
func buildSamlIdp(in CreateIdpInput, adminGroup *identitydomains.IdentityProviderJitUserProvAssignedGroups) identitydomains.IdentityProvider {
enabled := false
boolFalse := false
idp := identitydomains.IdentityProvider{
Schemas: []string{idpSchema},
PartnerName: &in.Name,
Enabled: &enabled,
Type: identitydomains.IdentityProviderTypeSaml,
Metadata: &in.Metadata,
NameIdFormat: &in.NameIDFormat,
SignatureHashAlgorithm: identitydomains.IdentityProviderSignatureHashAlgorithm256,
IncludeSigningCertInSignature: &boolFalse,
UserMappingMethod: identitydomains.IdentityProviderUserMappingMethodNameidtouserattribute,
UserMappingStoreAttribute: &in.UserStoreAttribute,
JitUserProvEnabled: &in.JitEnabled,
JitUserProvCreateUserEnabled: &in.JitCreate,
JitUserProvAttributeUpdateEnabled: &in.JitUpdate,
JitUserProvGroupAssertionAttributeEnabled: &boolFalse,
JitUserProvGroupStaticListEnabled: &in.JitAssignAdminGroup,
JitUserProvGroupMappingMode: identitydomains.IdentityProviderJitUserProvGroupMappingModeImplicit,
}
if in.AssertionAttribute != "" {
idp.UserMappingMethod = identitydomains.IdentityProviderUserMappingMethodAssertionattributetouserattribute
idp.AssertionAttribute = &in.AssertionAttribute
}
if in.IconURL != "" {
idp.IconUrl = &in.IconURL
}
if in.Description != "" {
idp.Description = &in.Description
}
if adminGroup != nil {
idp.JitUserProvAssignedGroups = []identitydomains.IdentityProviderJitUserProvAssignedGroups{*adminGroup}
}
return idp
}
// adminGroupRef 查询域内 Administrators 组作为 JIT 静态分配目标;不存在时返回 nil。
func adminGroupRef(ctx context.Context, dc identitydomains.IdentityDomainsClient) (*identitydomains.IdentityProviderJitUserProvAssignedGroups, error) {
filter := fmt.Sprintf("displayName eq %q", administratorsGroupName)
count := 1
resp, err := dc.ListGroups(ctx, identitydomains.ListGroupsRequest{
Filter: &filter,
Attributes: common.String("id,displayName"),
Count: &count,
})
if err != nil {
return nil, fmt.Errorf("find Administrators group: %w", err)
}
if len(resp.Resources) == 0 || resp.Resources[0].Id == nil {
return nil, nil
}
return &identitydomains.IdentityProviderJitUserProvAssignedGroups{Value: resp.Resources[0].Id}, nil
}
// jitMappings 生成 JIT 属性映射:来源为 NameID(或指定断言属性),
// 固定映射 用户名 + 姓,按需追加主邮箱(emails[work].value)。
func jitMappings(in CreateIdpInput) []interface{} {
src := "$(assertion.fed.nameidvalue)"
if in.AssertionAttribute != "" {
src = "$(assertion." + in.AssertionAttribute + ")"
}
ms := []interface{}{
map[string]string{"managedObjectAttributeName": src, "idcsAttributeName": "userName"},
map[string]string{"managedObjectAttributeName": src, "idcsAttributeName": "name.familyName"},
}
if in.JitMapEmail {
ms = append(ms, map[string]string{"managedObjectAttributeName": src, "idcsAttributeName": "emails[work].value"})
}
return ms
}
// ensureJitAttributeMappings 把 IdP 自动生成的 JIT 属性映射替换为给定映射。
func ensureJitAttributeMappings(ctx context.Context, dc identitydomains.IdentityDomainsClient, idp identitydomains.IdentityProvider, mappings []interface{}) error {
ref := idp.JitUserProvAttributes
if ref == nil || ref.Value == nil {
got, err := dc.GetIdentityProvider(ctx, identitydomains.GetIdentityProviderRequest{IdentityProviderId: idp.Id})
if err != nil {
return fmt.Errorf("reload idp for jit attributes: %w", err)
}
ref = got.JitUserProvAttributes
}
if ref == nil || ref.Value == nil {
return fmt.Errorf("idp %s has no jit attribute mapping resource", deref(idp.Id))
}
var value interface{} = mappings
_, err := dc.PatchMappedAttribute(ctx, identitydomains.PatchMappedAttributeRequest{
MappedAttributeId: ref.Value,
PatchOp: identitydomains.PatchOp{
Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
Operations: []identitydomains.Operations{{
Op: identitydomains.OperationsOpReplace,
Path: common.String("attributeMappings"),
Value: &value,
}},
},
})
if err != nil {
return fmt.Errorf("patch jit attribute mappings: %w", err)
}
return nil
}
// SetIdentityProviderEnabled 实现 Client:启用/停用 IdP 并同步登录页显示。
// 顺序有讲究:启用后才能上登录页,停用前必须先从登录页移除。
func (c *RealClient) SetIdentityProviderEnabled(ctx context.Context, cred Credentials, region, idpID string, enabled bool) (IdentityProviderInfo, error) {
dc, err := c.domainsClient(ctx, cred, region)
if err != nil {
return IdentityProviderInfo{}, err
}
if !enabled {
if err := updateLoginPageIdps(ctx, dc, idpID, false); err != nil {
return IdentityProviderInfo{}, err
}
}
idp, err := patchIdpEnabled(ctx, dc, idpID, enabled)
if err != nil {
return IdentityProviderInfo{}, err
}
if enabled {
if err := updateLoginPageIdps(ctx, dc, idpID, true); err != nil {
return toIdpInfo(idp), err
}
}
return toIdpInfo(idp), nil
}
func patchIdpEnabled(ctx context.Context, dc identitydomains.IdentityDomainsClient, idpID string, enabled bool) (identitydomains.IdentityProvider, error) {
var value interface{} = enabled
resp, err := dc.PatchIdentityProvider(ctx, identitydomains.PatchIdentityProviderRequest{
IdentityProviderId: &idpID,
PatchOp: identitydomains.PatchOp{
Schemas: []string{scimPatchSchema},
Operations: []identitydomains.Operations{{
Op: identitydomains.OperationsOpReplace,
Path: common.String("enabled"),
Value: &value,
}},
},
})
if err != nil {
return identitydomains.IdentityProvider{}, fmt.Errorf("patch idp %s enabled=%v: %w", idpID, enabled, err)
}
return resp.IdentityProvider, nil
}
// DeleteIdentityProvider 实现 Client:删除 IdP;先移出登录页并停用。
func (c *RealClient) DeleteIdentityProvider(ctx context.Context, cred Credentials, region, idpID string) error {
dc, err := c.domainsClient(ctx, cred, region)
if err != nil {
return err
}
if err := updateLoginPageIdps(ctx, dc, idpID, false); err != nil {
return err
}
if _, err := patchIdpEnabled(ctx, dc, idpID, false); err != nil {
return err
}
if _, err := dc.DeleteIdentityProvider(ctx, identitydomains.DeleteIdentityProviderRequest{
IdentityProviderId: &idpID,
}); err != nil {
return fmt.Errorf("delete identity provider %s: %w", idpID, err)
}
return nil
}
// updateLoginPageIdps 修改预置 DefaultIDPRule 的 SamlIDPs 数组控制登录页显示。
func updateLoginPageIdps(ctx context.Context, dc identitydomains.IdentityDomainsClient, idpID string, show bool) error {
rule, err := dc.GetRule(ctx, identitydomains.GetRuleRequest{RuleId: common.String(defaultIdpRuleID)})
if err != nil {
return fmt.Errorf("get default idp rule: %w", err)
}
returns, changed, err := rebuildSamlIdpsReturn(rule.Return, idpID, show)
if err != nil || !changed {
return err
}
var value interface{} = returns
_, err = dc.PatchRule(ctx, identitydomains.PatchRuleRequest{
RuleId: common.String(defaultIdpRuleID),
PatchOp: identitydomains.PatchOp{
Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
Operations: []identitydomains.Operations{{
Op: identitydomains.OperationsOpReplace,
Path: common.String("return"),
Value: &value,
}},
},
})
if err != nil {
return fmt.Errorf("patch default idp rule: %w", err)
}
return nil
}
// rebuildSamlIdpsReturn 重建规则 return 数组,增删 SamlIDPs 中的目标 IdP。
func rebuildSamlIdpsReturn(items []identitydomains.RuleReturn, idpID string, show bool) ([]interface{}, bool, error) {
returns := make([]interface{}, 0, len(items))
changed := false
for _, item := range items {
name, value := deref(item.Name), deref(item.Value)
if name == "SamlIDPs" {
next, ok, err := toggleJSONList(value, idpID, show)
if err != nil {
return nil, false, fmt.Errorf("parse SamlIDPs %q: %w", value, err)
}
value, changed = next, ok
}
returns = append(returns, map[string]string{"name": name, "value": value})
}
return returns, changed, nil
}
// toggleJSONList 在 JSON 数组字符串中添加或移除一个元素,返回是否有变化。
func toggleJSONList(list, item string, add bool) (string, bool, error) {
var items []string
if list != "" {
if err := json.Unmarshal([]byte(list), &items); err != nil {
return "", false, err
}
}
next := make([]string, 0, len(items)+1)
found := false
for _, it := range items {
if it == item {
found = true
if !add {
continue
}
}
next = append(next, it)
}
if add && !found {
next = append(next, item)
}
if add == found {
return list, false, nil
}
b, err := json.Marshal(next)
return string(b), true, err
}
// DownloadDomainSamlMetadata 实现 Client:下载域的 SP SAML 元数据 XML。
// 域设置「访问签名证书」未开启时端点不可用,自动开启后重试
// (联邦配置要求 IdP 侧能读取该元数据,开启公开访问是官方标准做法)。
func (c *RealClient) DownloadDomainSamlMetadata(ctx context.Context, cred Credentials, region string) ([]byte, error) {
url, err := c.domainURL(ctx, cred, region)
if err != nil {
return nil, err
}
body, status, err := fetchSamlMetadata(ctx, url)
if err != nil {
return nil, err
}
if status == http.StatusOK {
return body, nil
}
if err := c.enableSigningCertPublicAccess(ctx, cred, region); err != nil {
return nil, err
}
body, status, err = fetchSamlMetadata(ctx, url)
if err != nil {
return nil, err
}
if status != http.StatusOK {
return nil, fmt.Errorf("download saml metadata: HTTP %d: %s", status, truncate(string(body), 200))
}
return body, nil
}
// fetchSamlMetadata 匿名请求元数据端点;该端点只支持公开访问,不接受 OCI 签名。
func fetchSamlMetadata(ctx context.Context, url string) ([]byte, int, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url+samlMetadataPath, nil)
if err != nil {
return nil, 0, fmt.Errorf("build metadata request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, 0, fmt.Errorf("download saml metadata: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, 0, fmt.Errorf("read saml metadata: %w", err)
}
return body, resp.StatusCode, nil
}
// enableSigningCertPublicAccess 开启域设置「访问签名证书」公开访问。
func (c *RealClient) enableSigningCertPublicAccess(ctx context.Context, cred Credentials, region string) error {
dc, err := c.domainsClient(ctx, cred, region)
if err != nil {
return err
}
var value interface{} = true
_, err = dc.PatchSetting(ctx, identitydomains.PatchSettingRequest{
SettingId: common.String("Settings"),
PatchOp: identitydomains.PatchOp{
Schemas: []string{scimPatchSchema},
Operations: []identitydomains.Operations{{
Op: identitydomains.OperationsOpReplace,
Path: common.String("signingCertPublicAccess"),
Value: &value,
}},
},
})
if err != nil {
return fmt.Errorf("enable signing cert public access: %w", err)
}
return nil
}
func truncate(s string, n int) string {
if len(s) <= n {
return s
}
return s[:n] + "..."
}
func toIdpInfo(idp identitydomains.IdentityProvider) IdentityProviderInfo {
info := IdentityProviderInfo{
ID: deref(idp.Id),
Name: deref(idp.PartnerName),
Type: string(idp.Type),
Enabled: idp.Enabled != nil && *idp.Enabled,
PartnerProviderID: deref(idp.PartnerProviderId),
JitEnabled: idp.JitUserProvEnabled != nil && *idp.JitUserProvEnabled,
}
if idp.Meta != nil && idp.Meta.Created != nil {
if t, err := time.Parse(time.RFC3339, *idp.Meta.Created); err == nil {
info.TimeCreated = &t
}
}
return info
}