Files
oci-portal/internal/oci/domainsettings.go
T

305 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-500replace 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: &current.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
}