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}) }