Files
oci-portal/internal/oci/signon.go
T
Wang Defa dbba1f4905
CI / test (push) Successful in 30s
Release / release (push) Successful in 49s
发布 0.1.0:通知渠道、告警规则、令牌版本与安全加固
2026-07-10 17:38:34 +08:00

274 lines
9.4 KiB
Go

package oci
import (
"context"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/oracle/oci-go-sdk/v65/common"
"github.com/oracle/oci-go-sdk/v65/identitydomains"
)
// ociConsolePolicyID 是预置 sign-on 策略「Security Policy for OCI Console」的固定 ID。
const ociConsolePolicyID = "OciConsolePolicy"
// scimPatchSchema 是 SCIM PatchOp 的 schema。
const scimPatchSchema = "urn:ietf:params:scim:api:messages:2.0:PatchOp"
// SignOnRuleInfo 是 sign-on 策略中一条规则的关键字段。
type SignOnRuleInfo struct {
ID string `json:"id"`
Name string `json:"name"`
Sequence int `json:"sequence"`
AuthenticationFactor string `json:"authenticationFactor"`
ConditionAttribute string `json:"conditionAttribute,omitempty"`
ConditionValue string `json:"conditionValue,omitempty"`
BuiltIn bool `json:"builtIn"`
}
// ListConsoleSignOnRules 实现 Client:按优先级列出 OCI Console sign-on 策略的规则。
func (c *RealClient) ListConsoleSignOnRules(ctx context.Context, cred Credentials, region, domainID string) ([]SignOnRuleInfo, error) {
dc, err := c.domainsClient(ctx, cred, region, domainID)
if err != nil {
return nil, err
}
refs, err := consolePolicyRules(ctx, dc)
if err != nil {
return nil, err
}
rules := make([]SignOnRuleInfo, 0, len(refs))
for _, ref := range refs {
info, err := loadSignOnRule(ctx, dc, deref(ref.Value), ref.Sequence)
if err != nil {
return nil, err
}
rules = append(rules, info)
}
sort.Slice(rules, func(i, j int) bool { return rules[i].Sequence < rules[j].Sequence })
return rules, nil
}
func consolePolicyRules(ctx context.Context, dc identitydomains.IdentityDomainsClient) ([]identitydomains.PolicyRules, error) {
resp, err := dc.GetPolicy(ctx, identitydomains.GetPolicyRequest{
PolicyId: common.String(ociConsolePolicyID),
Attributes: common.String("id,name,rules"),
})
if err != nil {
return nil, fmt.Errorf("get console sign-on policy: %w", err)
}
return resp.Rules, nil
}
func loadSignOnRule(ctx context.Context, dc identitydomains.IdentityDomainsClient, ruleID string, seq *int) (SignOnRuleInfo, error) {
resp, err := dc.GetRule(ctx, identitydomains.GetRuleRequest{RuleId: &ruleID})
if err != nil {
return SignOnRuleInfo{}, fmt.Errorf("get rule %s: %w", ruleID, err)
}
info := SignOnRuleInfo{
ID: ruleID,
Name: deref(resp.Name),
BuiltIn: strings.HasPrefix(ruleID, "OciConsole"),
}
if seq != nil {
info.Sequence = *seq
}
for _, r := range resp.Return {
if deref(r.Name) == "authenticationFactor" {
info.AuthenticationFactor = deref(r.Value)
}
}
fillRuleCondition(ctx, dc, resp.Rule, &info)
return info, nil
}
// fillRuleCondition 尽力回填规则条件(仅解析直接引用 Condition 的规则)。
func fillRuleCondition(ctx context.Context, dc identitydomains.IdentityDomainsClient, rule identitydomains.Rule, info *SignOnRuleInfo) {
cg := rule.ConditionGroup
if cg == nil || cg.Type != identitydomains.RuleConditionGroupTypeCondition || cg.Value == nil {
return
}
resp, err := dc.GetCondition(ctx, identitydomains.GetConditionRequest{ConditionId: cg.Value})
if err != nil {
return
}
info.ConditionAttribute = deref(resp.AttributeName)
info.ConditionValue = deref(resp.AttributeValue)
}
// CreateMfaExemptionRule 实现 Client:为指定 IdP 创建免 MFA sign-on 规则并置顶。
// 规则语义与控制台一致:subject.authenticatedBy in [idpID] → authenticationFactor=IDP。
func (c *RealClient) CreateMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, idpID, ruleName string) (SignOnRuleInfo, error) {
dc, err := c.domainsClient(ctx, cred, region, domainID)
if err != nil {
return SignOnRuleInfo{}, err
}
condID, err := createIdpCondition(ctx, dc, idpID, ruleName)
if err != nil {
return SignOnRuleInfo{}, err
}
ruleID, err := createExemptionRule(ctx, dc, condID, ruleName)
if err != nil {
deleteConditionQuiet(ctx, dc, condID)
return SignOnRuleInfo{}, err
}
if err := prependRuleToPolicy(ctx, dc, ruleID); err != nil {
deleteRuleQuiet(ctx, dc, ruleID)
deleteConditionQuiet(ctx, dc, condID)
return SignOnRuleInfo{}, err
}
return SignOnRuleInfo{
ID: ruleID, Name: ruleName, Sequence: 1,
AuthenticationFactor: "IDP",
ConditionAttribute: "subject.authenticatedBy",
ConditionValue: jsonList(idpID),
}, nil
}
func jsonList(items ...string) string {
b, _ := json.Marshal(items)
return string(b)
}
func createIdpCondition(ctx context.Context, dc identitydomains.IdentityDomainsClient, idpID, name string) (string, error) {
value := jsonList(idpID)
resp, err := dc.CreateCondition(ctx, identitydomains.CreateConditionRequest{
Condition: identitydomains.Condition{
Schemas: []string{"urn:ietf:params:scim:schemas:oracle:idcs:Condition"},
Name: common.String(name + "-condition"),
AttributeName: common.String("subject.authenticatedBy"),
Operator: identitydomains.ConditionOperatorIn,
AttributeValue: &value,
},
})
if err != nil {
return "", fmt.Errorf("create idp condition: %w", err)
}
return deref(resp.Id), nil
}
func createExemptionRule(ctx context.Context, dc identitydomains.IdentityDomainsClient, condID, name string) (string, error) {
resp, err := dc.CreateRule(ctx, identitydomains.CreateRuleRequest{
Rule: identitydomains.Rule{
Schemas: []string{"urn:ietf:params:scim:schemas:oracle:idcs:Rule"},
Name: &name,
PolicyType: &identitydomains.RulePolicyType{Value: common.String("SignOn")},
ConditionGroup: &identitydomains.RuleConditionGroup{
Type: identitydomains.RuleConditionGroupTypeCondition,
Value: &condID,
},
Return: []identitydomains.RuleReturn{
{Name: common.String("effect"), Value: common.String("ALLOW")},
{Name: common.String("reAuthenticate"), Value: common.String("false")},
{Name: common.String("authenticationFactor"), Value: common.String("IDP")},
},
},
})
if err != nil {
return "", fmt.Errorf("create exemption rule: %w", err)
}
return deref(resp.Id), nil
}
// prependRuleToPolicy 把规则插到策略首位,其余规则优先级顺延。
func prependRuleToPolicy(ctx context.Context, dc identitydomains.IdentityDomainsClient, ruleID string) error {
existing, err := consolePolicyRules(ctx, dc)
if err != nil {
return err
}
rules := []interface{}{map[string]interface{}{"value": ruleID, "sequence": 1}}
for i, r := range sortedRules(existing) {
rules = append(rules, map[string]interface{}{"value": deref(r.Value), "sequence": i + 2})
}
return patchPolicyRules(ctx, dc, rules)
}
// removeRuleFromPolicy 从策略移除规则,其余规则优先级从 1 重排。
func removeRuleFromPolicy(ctx context.Context, dc identitydomains.IdentityDomainsClient, ruleID string) error {
existing, err := consolePolicyRules(ctx, dc)
if err != nil {
return err
}
rules := make([]interface{}, 0, len(existing))
for _, r := range sortedRules(existing) {
if deref(r.Value) == ruleID {
continue
}
rules = append(rules, map[string]interface{}{"value": deref(r.Value), "sequence": len(rules) + 1})
}
if len(rules) == len(existing) {
return nil
}
return patchPolicyRules(ctx, dc, rules)
}
func sortedRules(rules []identitydomains.PolicyRules) []identitydomains.PolicyRules {
out := append([]identitydomains.PolicyRules(nil), rules...)
sort.Slice(out, func(i, j int) bool {
si, sj := 0, 0
if out[i].Sequence != nil {
si = *out[i].Sequence
}
if out[j].Sequence != nil {
sj = *out[j].Sequence
}
return si < sj
})
return out
}
func patchPolicyRules(ctx context.Context, dc identitydomains.IdentityDomainsClient, rules []interface{}) error {
var value interface{} = rules
_, err := dc.PatchPolicy(ctx, identitydomains.PatchPolicyRequest{
PolicyId: common.String(ociConsolePolicyID),
PatchOp: identitydomains.PatchOp{
Schemas: []string{scimPatchSchema},
Operations: []identitydomains.Operations{{
Op: identitydomains.OperationsOpReplace,
Path: common.String("rules"),
Value: &value,
}},
},
})
if err != nil {
return fmt.Errorf("patch console sign-on policy rules: %w", err)
}
return nil
}
// DeleteMfaExemptionRule 实现 Client:删除免 MFA 规则并连带清理其条件。
// 拒绝删除 Oracle 预置规则。
func (c *RealClient) DeleteMfaExemptionRule(ctx context.Context, cred Credentials, region, domainID, ruleID string) error {
if strings.HasPrefix(ruleID, "OciConsole") {
return fmt.Errorf("delete sign-on rule: %s is a built-in rule and cannot be deleted", ruleID)
}
dc, err := c.domainsClient(ctx, cred, region, domainID)
if err != nil {
return err
}
rule, err := dc.GetRule(ctx, identitydomains.GetRuleRequest{RuleId: &ruleID})
if err != nil {
return fmt.Errorf("get rule %s: %w", ruleID, err)
}
if err := removeRuleFromPolicy(ctx, dc, ruleID); err != nil {
return err
}
if _, err := dc.DeleteRule(ctx, identitydomains.DeleteRuleRequest{RuleId: &ruleID}); err != nil {
return fmt.Errorf("delete rule %s: %w", ruleID, err)
}
cg := rule.ConditionGroup
if cg != nil && cg.Type == identitydomains.RuleConditionGroupTypeCondition && cg.Value != nil {
deleteConditionQuiet(ctx, dc, *cg.Value)
}
return nil
}
func deleteRuleQuiet(ctx context.Context, dc identitydomains.IdentityDomainsClient, ruleID string) {
_, _ = dc.DeleteRule(ctx, identitydomains.DeleteRuleRequest{RuleId: &ruleID})
}
func deleteConditionQuiet(ctx context.Context, dc identitydomains.IdentityDomainsClient, condID string) {
_, _ = dc.DeleteCondition(ctx, identitydomains.DeleteConditionRequest{ConditionId: &condID})
}