280 lines
10 KiB
Go
280 lines
10 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"
|
|
|
|
// signOnConsentSchema 是修改 Oracle 预置 OCI Console sign-on 策略的知情同意扩展 schema。
|
|
const signOnConsentSchema = "urn:ietf:params:scim:schemas:oracle:idcs:extension:ociconsolesignonpolicyconsent:Policy"
|
|
|
|
// 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
|
|
var consent interface{} = true
|
|
var justification interface{} = "MFA Configured in External IDP"
|
|
_, err := dc.PatchPolicy(ctx, identitydomains.PatchPolicyRequest{
|
|
PolicyId: common.String(ociConsolePolicyID),
|
|
PatchOp: identitydomains.PatchOp{
|
|
Schemas: []string{scimPatchSchema},
|
|
// 改动 Oracle 预置策略必须附带知情同意,否则 400 Missing required attribute(s): consent
|
|
Operations: []identitydomains.Operations{
|
|
{Op: identitydomains.OperationsOpReplace, Path: common.String("rules"), Value: &value},
|
|
{Op: identitydomains.OperationsOpAdd, Path: common.String(signOnConsentSchema + ":consent"), Value: &consent},
|
|
{Op: identitydomains.OperationsOpAdd, Path: common.String(signOnConsentSchema + ":justification"), Value: &justification},
|
|
},
|
|
},
|
|
})
|
|
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})
|
|
}
|