package oci import ( "context" "errors" "fmt" "github.com/oracle/oci-go-sdk/v65/common" "github.com/oracle/oci-go-sdk/v65/identity" "github.com/oracle/oci-go-sdk/v65/identitydomains" ) // notificationSettingID 是 Identity Domain 通知设置的固定资源 ID。 const notificationSettingID = "NotificationSettings" // NotificationRecipients 是域通知收件人设置:test mode 开启后 // 域内全部通知只发给 recipients 列表。 type NotificationRecipients struct { Recipients []string `json:"recipients"` TestModeEnabled bool `json:"testModeEnabled"` } // PasswordPolicyInfo 是一条域密码策略的关键字段。 type PasswordPolicyInfo struct { ID string `json:"id"` Name string `json:"name"` Priority int `json:"priority"` PasswordStrength string `json:"passwordStrength"` PasswordExpiresAfter *int `json:"passwordExpiresAfter,omitempty"` MinLength *int `json:"minLength,omitempty"` NumPasswordsInHistory *int `json:"numPasswordsInHistory,omitempty"` } // UpdatePasswordPolicyInput 是密码策略修改项;nil 字段不修改。 // PasswordExpiresAfter 为 0 表示密码永不过期; // NumPasswordsInHistory 为 0 表示可复用以前的密码(不记历史)。 type UpdatePasswordPolicyInput struct { PasswordExpiresAfter *int MinLength *int NumPasswordsInHistory *int } // IdentitySettingInfo 是域身份设置里本面板关心的开关。 type IdentitySettingInfo struct { ID string `json:"id"` // 用户需要提供主电子邮件地址:开启后创建用户 / JIT 预配必须带邮箱 PrimaryEmailRequired bool `json:"primaryEmailRequired"` } // domainURL 解析租户 Default Identity Domain 的 endpoint URL。 func (c *RealClient) domainURL(ctx context.Context, cred Credentials, region string) (string, error) { ic, err := c.identityClientAt(cred, region) if err != nil { return "", err } resp, err := ic.ListDomains(ctx, identity.ListDomainsRequest{CompartmentId: &cred.TenancyOCID}) if err != nil { return "", fmt.Errorf("list identity domains: %w", err) } url := pickDomainURL(resp.Items) if url == "" { return "", fmt.Errorf("no active identity domain found") } return url, nil } // domainsClient 解析租户的 Default Identity Domain 并构造 SCIM 客户端。 func (c *RealClient) domainsClient(ctx context.Context, cred Credentials, region string) (identitydomains.IdentityDomainsClient, error) { url, err := c.domainURL(ctx, cred, region) if err != nil { return identitydomains.IdentityDomainsClient{}, err } dc, err := identitydomains.NewIdentityDomainsClientWithConfigurationProvider(provider(cred), url) if err != nil { return dc, fmt.Errorf("new identity domains client: %w", err) } applyProxy(&dc.BaseClient, cred) // IDCS 错误体是 SCIM 格式,改写后 SDK 才能解析出真实错误消息 dc.HTTPClient = scimErrorDispatcher{base: dc.HTTPClient} return dc, nil } // pickDomainURL 优先返回名为 Default 的域,否则取第一个 ACTIVE 的域。 func pickDomainURL(domains []identity.DomainSummary) string { fallback := "" for _, d := range domains { if d.LifecycleState != identity.DomainLifecycleStateActive { continue } if deref(d.DisplayName) == "Default" { return deref(d.Url) } if fallback == "" { fallback = deref(d.Url) } } return fallback } // GetNotificationRecipients 实现 Client:查询域通知收件人设置。 func (c *RealClient) GetNotificationRecipients(ctx context.Context, cred Credentials, region string) (NotificationRecipients, error) { dc, err := c.domainsClient(ctx, cred, region) if err != nil { return NotificationRecipients{}, err } setting, err := getNotificationSetting(ctx, dc) if err != nil { return NotificationRecipients{}, err } return NotificationRecipients{ Recipients: orEmpty(setting.TestRecipients), TestModeEnabled: setting.TestModeEnabled != nil && *setting.TestModeEnabled, }, nil } // UpdateNotificationRecipients 实现 Client:把域通知改为只发给指定收件人; // emails 为空时关闭 test mode 恢复默认发送。其余字段原样保留。 func (c *RealClient) UpdateNotificationRecipients(ctx context.Context, cred Credentials, region string, emails []string) (NotificationRecipients, error) { dc, err := c.domainsClient(ctx, cred, region) if err != nil { return NotificationRecipients{}, err } setting, err := getNotificationSetting(ctx, dc) if err != nil { return NotificationRecipients{}, err } enabled := len(emails) > 0 setting.TestRecipients = emails setting.TestModeEnabled = &enabled id := notificationSettingID resp, err := dc.PutNotificationSetting(ctx, identitydomains.PutNotificationSettingRequest{ NotificationSettingId: &id, NotificationSetting: setting, }) if err != nil { return NotificationRecipients{}, fmt.Errorf("put notification setting: %w", err) } return NotificationRecipients{ Recipients: orEmpty(resp.TestRecipients), TestModeEnabled: resp.TestModeEnabled != nil && *resp.TestModeEnabled, }, nil } func getNotificationSetting(ctx context.Context, dc identitydomains.IdentityDomainsClient) (identitydomains.NotificationSetting, error) { id := notificationSettingID resp, err := dc.GetNotificationSetting(ctx, identitydomains.GetNotificationSettingRequest{ NotificationSettingId: &id, }) if err != nil { return identitydomains.NotificationSetting{}, fmt.Errorf("get notification setting: %w", err) } return resp.NotificationSetting, nil } // ListPasswordPolicies 实现 Client:列出域内全部密码策略。 func (c *RealClient) ListPasswordPolicies(ctx context.Context, cred Credentials, region string) ([]PasswordPolicyInfo, error) { dc, err := c.domainsClient(ctx, cred, region) if err != nil { return nil, err } resp, err := dc.ListPasswordPolicies(ctx, identitydomains.ListPasswordPoliciesRequest{}) if err != nil { return nil, fmt.Errorf("list password policies: %w", err) } policies := make([]PasswordPolicyInfo, 0, len(resp.Resources)) for _, p := range resp.Resources { policies = append(policies, toPasswordPolicyInfo(p)) } return policies, nil } // UpdatePasswordPolicy 实现 Client:以 SCIM Patch 修改密码策略的指定字段。 // 仅 Custom 强度的策略可修改,Simple/Standard 内置策略 OCI 返回 403。 func (c *RealClient) UpdatePasswordPolicy(ctx context.Context, cred Credentials, region, policyID string, in UpdatePasswordPolicyInput) (PasswordPolicyInfo, error) { ops := buildPolicyPatchOps(in) if len(ops) == 0 { return PasswordPolicyInfo{}, fmt.Errorf("update password policy: nothing to update") } dc, err := c.domainsClient(ctx, cred, region) if err != nil { return PasswordPolicyInfo{}, err } resp, err := dc.PatchPasswordPolicy(ctx, identitydomains.PatchPasswordPolicyRequest{ PasswordPolicyId: &policyID, PatchOp: identitydomains.PatchOp{ Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, Operations: ops, }, }) if err != nil { if isForbidden(err) { return PasswordPolicyInfo{}, fmt.Errorf("patch password policy %s: only Custom-strength policies are editable: %w", policyID, err) } return PasswordPolicyInfo{}, fmt.Errorf("patch password policy %s: %w", policyID, err) } return toPasswordPolicyInfo(resp.PasswordPolicy), nil } // isForbidden 判断服务端返回 403(内置只读资源等场景)。 func isForbidden(err error) bool { var svcErr common.ServiceError return errors.As(err, &svcErr) && svcErr.GetHTTPStatusCode() == 403 } func buildPolicyPatchOps(in UpdatePasswordPolicyInput) []identitydomains.Operations { var ops []identitydomains.Operations if in.PasswordExpiresAfter != nil { ops = append(ops, intOrRemoveOp("passwordExpiresAfter", *in.PasswordExpiresAfter)) } if in.MinLength != nil { ops = append(ops, replaceOp("minLength", *in.MinLength)) } if in.NumPasswordsInHistory != nil { ops = append(ops, intOrRemoveOp("numPasswordsInHistory", *in.NumPasswordsInHistory)) } return ops } // intOrRemoveOp 值为 0 时移除属性(IDCS 语义 null = 永不过期 / 可复用密码; // numPasswordsInHistory 合法范围 1-500,replace 0 会被拒),否则 replace。 func intOrRemoveOp(path string, v int) identitydomains.Operations { if v == 0 { return removeOp(path) } return replaceOp(path, v) } func removeOp(path string) identitydomains.Operations { return identitydomains.Operations{ Op: identitydomains.OperationsOpRemove, Path: &path, } } // GetIdentitySetting 实现 Client:读取域身份设置(IdentitySettings 单例资源)。 func (c *RealClient) GetIdentitySetting(ctx context.Context, cred Credentials, region string) (IdentitySettingInfo, error) { dc, err := c.domainsClient(ctx, cred, region) if err != nil { return IdentitySettingInfo{}, err } resp, err := dc.ListIdentitySettings(ctx, identitydomains.ListIdentitySettingsRequest{}) if err != nil { return IdentitySettingInfo{}, fmt.Errorf("list identity settings: %w", err) } if len(resp.Resources) == 0 { return IdentitySettingInfo{}, fmt.Errorf("identity settings resource not found") } s := resp.Resources[0] info := IdentitySettingInfo{ID: deref(s.Id)} if s.PrimaryEmailRequired != nil { info.PrimaryEmailRequired = *s.PrimaryEmailRequired } return info, nil } // UpdateIdentitySetting 实现 Client:修改「用户需要提供主电子邮件地址」开关。 func (c *RealClient) UpdateIdentitySetting(ctx context.Context, cred Credentials, region string, primaryEmailRequired bool) (IdentitySettingInfo, error) { current, err := c.GetIdentitySetting(ctx, cred, region) if err != nil { return IdentitySettingInfo{}, err } dc, err := c.domainsClient(ctx, cred, region) if err != nil { return IdentitySettingInfo{}, err } resp, err := dc.PatchIdentitySetting(ctx, identitydomains.PatchIdentitySettingRequest{ IdentitySettingId: ¤t.ID, PatchOp: identitydomains.PatchOp{ Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"}, Operations: []identitydomains.Operations{replaceOp("primaryEmailRequired", primaryEmailRequired)}, }, }) if err != nil { return IdentitySettingInfo{}, fmt.Errorf("patch identity settings: %w", err) } info := IdentitySettingInfo{ID: deref(resp.IdentitySetting.Id)} if resp.IdentitySetting.PrimaryEmailRequired != nil { info.PrimaryEmailRequired = *resp.IdentitySetting.PrimaryEmailRequired } return info, nil } func replaceOp(path string, value interface{}) identitydomains.Operations { return identitydomains.Operations{ Op: identitydomains.OperationsOpReplace, Path: &path, Value: &value, } } func toPasswordPolicyInfo(p identitydomains.PasswordPolicy) PasswordPolicyInfo { info := PasswordPolicyInfo{ ID: deref(p.Id), Name: deref(p.Name), PasswordStrength: string(p.PasswordStrength), PasswordExpiresAfter: p.PasswordExpiresAfter, MinLength: p.MinLength, NumPasswordsInHistory: p.NumPasswordsInHistory, } if p.Priority != nil { info.Priority = *p.Priority } return info }