初始提交:OCI 面板后端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,736 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/identity"
|
||||
"github.com/oracle/oci-go-sdk/v65/identitydomains"
|
||||
)
|
||||
|
||||
// TenantUser 是租户下的一个 IAM 用户。
|
||||
type TenantUser struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Email string `json:"email"`
|
||||
EmailVerified bool `json:"emailVerified"`
|
||||
MfaActivated bool `json:"mfaActivated"`
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
IsCurrentUser bool `json:"isCurrentUser"`
|
||||
TimeCreated *time.Time `json:"timeCreated"`
|
||||
LastLoginTime *time.Time `json:"lastLoginTime,omitempty"`
|
||||
}
|
||||
|
||||
// Oracle 预置的管理员角色与管理员组显示名。
|
||||
const (
|
||||
domainAdminRoleName = "Identity Domain Administrator"
|
||||
administratorsGroupName = "Administrators"
|
||||
)
|
||||
|
||||
// scimAppRolesAttr 是 SCIM 用户 appRoles 扩展属性的完整路径。
|
||||
const scimAppRolesAttr = "urn:ietf:params:scim:schemas:oracle:idcs:extension:user:User:appRoles"
|
||||
|
||||
// TenantUserDetail 是编辑表单需要的域档案与管理员状态;
|
||||
// InDomain 为 false 表示经典 IAM 用户,其余字段无意义。
|
||||
type TenantUserDetail struct {
|
||||
InDomain bool `json:"inDomain"`
|
||||
GivenName string `json:"givenName"`
|
||||
FamilyName string `json:"familyName"`
|
||||
IsDomainAdmin bool `json:"isDomainAdmin"`
|
||||
InAdminGroup bool `json:"inAdminGroup"`
|
||||
}
|
||||
|
||||
// GetTenantUserDetail 实现 Client:查用户域档案与管理员状态(编辑表单预填充用)。
|
||||
func (c *RealClient) GetTenantUserDetail(ctx context.Context, cred Credentials, homeRegion, userID string) (TenantUserDetail, error) {
|
||||
dc, err := c.domainsClient(ctx, cred, homeRegion)
|
||||
if err != nil {
|
||||
return TenantUserDetail{}, err
|
||||
}
|
||||
u, found, err := domainUserByOcid(ctx, dc, userID, "groups", scimAppRolesAttr)
|
||||
if err != nil || !found {
|
||||
return TenantUserDetail{}, err
|
||||
}
|
||||
return toTenantUserDetail(u), nil
|
||||
}
|
||||
|
||||
// toTenantUserDetail 提取姓名、管理员角色与管理员组成员状态。
|
||||
func toTenantUserDetail(u identitydomains.User) TenantUserDetail {
|
||||
d := TenantUserDetail{InDomain: true}
|
||||
if u.Name != nil {
|
||||
d.GivenName = deref(u.Name.GivenName)
|
||||
d.FamilyName = deref(u.Name.FamilyName)
|
||||
}
|
||||
for _, g := range u.Groups {
|
||||
if deref(g.Display) == administratorsGroupName {
|
||||
d.InAdminGroup = true
|
||||
}
|
||||
}
|
||||
if ext := u.UrnIetfParamsScimSchemasOracleIdcsExtensionUserUser; ext != nil {
|
||||
for _, r := range ext.AppRoles {
|
||||
if deref(r.Display) == domainAdminRoleName {
|
||||
d.IsDomainAdmin = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func (c *RealClient) identityClientAt(cred Credentials, region string) (identity.IdentityClient, error) {
|
||||
ic, err := identity.NewIdentityClientWithConfigurationProvider(provider(cred))
|
||||
if err != nil {
|
||||
return ic, fmt.Errorf("new identity client: %w", err)
|
||||
}
|
||||
applyProxy(&ic.BaseClient, cred)
|
||||
if region != "" {
|
||||
ic.SetRegion(normalizeRegion(region))
|
||||
}
|
||||
return ic, nil
|
||||
}
|
||||
|
||||
// ListTenantUsers 实现 Client:列出租户全部 IAM 用户。
|
||||
func (c *RealClient) ListTenantUsers(ctx context.Context, cred Credentials) ([]TenantUser, error) {
|
||||
ic, err := c.identityClientAt(cred, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var users []TenantUser
|
||||
var page *string
|
||||
for {
|
||||
resp, err := ic.ListUsers(ctx, identity.ListUsersRequest{CompartmentId: &cred.TenancyOCID, Page: page})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list users: %w", err)
|
||||
}
|
||||
for _, item := range resp.Items {
|
||||
users = append(users, toTenantUser(item, cred.UserOCID))
|
||||
}
|
||||
if resp.OpcNextPage == nil {
|
||||
return orEmpty(users), nil
|
||||
}
|
||||
page = resp.OpcNextPage
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTenantUserInput 是创建用户的输入;GivenName/FamilyName 走
|
||||
// Identity Domains SCIM(经典 IAM 无对应字段)。两个管理员选项同时勾选时
|
||||
// 先授身份域管理员角色,再加入 Administrators 组。
|
||||
type CreateTenantUserInput struct {
|
||||
Name string // 登录用户名
|
||||
Description string
|
||||
Email string
|
||||
GivenName string
|
||||
FamilyName string
|
||||
GrantDomainAdmin bool // 授予「身份域管理员」应用角色
|
||||
AddToAdminGroup bool // 加入 Administrators 组
|
||||
}
|
||||
|
||||
// CreateTenantUser 实现 Client:创建用户(须发往 home region)。
|
||||
// 优先 Identity Domains SCIM(支持姓名与管理员授权);SCIM 不可用时回退
|
||||
// 经典 IAM API,此时管理员选项无法执行、姓名并入描述。
|
||||
func (c *RealClient) CreateTenantUser(ctx context.Context, cred Credentials, homeRegion string, in CreateTenantUserInput) (TenantUser, error) {
|
||||
user, err := c.createUserViaDomain(ctx, cred, homeRegion, in)
|
||||
if err == nil {
|
||||
return user, nil
|
||||
}
|
||||
if in.GrantDomainAdmin || in.AddToAdminGroup {
|
||||
return TenantUser{}, fmt.Errorf("create user via identity domain: %w", err)
|
||||
}
|
||||
return c.createUserClassic(ctx, cred, homeRegion, in)
|
||||
}
|
||||
|
||||
// createUserClassic 走经典 IAM API 创建用户(姓名并入描述)。
|
||||
func (c *RealClient) createUserClassic(ctx context.Context, cred Credentials, homeRegion string, in CreateTenantUserInput) (TenantUser, error) {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return TenantUser{}, err
|
||||
}
|
||||
description := in.Description
|
||||
if description == "" {
|
||||
description = strings.TrimSpace(in.GivenName + " " + in.FamilyName)
|
||||
}
|
||||
details := identity.CreateUserDetails{
|
||||
CompartmentId: &cred.TenancyOCID,
|
||||
Name: &in.Name,
|
||||
Description: &description,
|
||||
}
|
||||
if in.Email != "" {
|
||||
details.Email = &in.Email
|
||||
}
|
||||
resp, err := ic.CreateUser(ctx, identity.CreateUserRequest{CreateUserDetails: details})
|
||||
if err != nil {
|
||||
return TenantUser{}, fmt.Errorf("create user %s: %w", in.Name, err)
|
||||
}
|
||||
return toTenantUser(resp.User, cred.UserOCID), nil
|
||||
}
|
||||
|
||||
// createUserViaDomain 通过 Identity Domains SCIM 创建用户并按选项授权。
|
||||
func (c *RealClient) createUserViaDomain(ctx context.Context, cred Credentials, homeRegion string, in CreateTenantUserInput) (TenantUser, error) {
|
||||
dc, err := c.domainsClient(ctx, cred, homeRegion)
|
||||
if err != nil {
|
||||
return TenantUser{}, err
|
||||
}
|
||||
resp, err := dc.CreateUser(ctx, identitydomains.CreateUserRequest{User: buildScimUser(in)})
|
||||
if err != nil {
|
||||
return TenantUser{}, fmt.Errorf("create domain user %s: %w", in.Name, err)
|
||||
}
|
||||
scimID := deref(resp.User.Id)
|
||||
// 同时勾选时约定先授身份域管理员,再加管理员组
|
||||
if in.GrantDomainAdmin {
|
||||
if err := grantDomainAdminRole(ctx, dc, scimID); err != nil {
|
||||
return toDomainTenantUser(resp.User), err
|
||||
}
|
||||
}
|
||||
if in.AddToAdminGroup {
|
||||
if err := addUserToAdminGroup(ctx, dc, scimID); err != nil {
|
||||
return toDomainTenantUser(resp.User), err
|
||||
}
|
||||
}
|
||||
return toDomainTenantUser(resp.User), nil
|
||||
}
|
||||
|
||||
func buildScimUser(in CreateTenantUserInput) identitydomains.User {
|
||||
u := identitydomains.User{
|
||||
Schemas: []string{"urn:ietf:params:scim:schemas:core:2.0:User"},
|
||||
UserName: &in.Name,
|
||||
Name: &identitydomains.UserName{
|
||||
GivenName: &in.GivenName,
|
||||
FamilyName: &in.FamilyName,
|
||||
},
|
||||
}
|
||||
if in.Description != "" {
|
||||
u.Description = &in.Description
|
||||
}
|
||||
if in.Email != "" {
|
||||
primary := true
|
||||
u.Emails = []identitydomains.UserEmails{{
|
||||
Value: &in.Email,
|
||||
Type: identitydomains.UserEmailsTypeWork,
|
||||
Primary: &primary,
|
||||
}}
|
||||
}
|
||||
return u
|
||||
}
|
||||
|
||||
// findDomainAdminRole 查找「Identity Domain Administrator」应用角色。
|
||||
func findDomainAdminRole(ctx context.Context, dc identitydomains.IdentityDomainsClient) (identitydomains.AppRole, error) {
|
||||
filter := fmt.Sprintf("displayName eq %q", domainAdminRoleName)
|
||||
count := 1
|
||||
roles, err := dc.ListAppRoles(ctx, identitydomains.ListAppRolesRequest{
|
||||
Filter: &filter,
|
||||
Attributes: common.String("id,displayName,app"),
|
||||
Count: &count,
|
||||
})
|
||||
if err != nil {
|
||||
return identitydomains.AppRole{}, fmt.Errorf("find domain administrator role: %w", err)
|
||||
}
|
||||
if len(roles.Resources) == 0 || roles.Resources[0].Id == nil || roles.Resources[0].App == nil {
|
||||
return identitydomains.AppRole{}, fmt.Errorf("identity domain administrator role not found")
|
||||
}
|
||||
return roles.Resources[0], nil
|
||||
}
|
||||
|
||||
// grantDomainAdminRole 授予「Identity Domain Administrator」应用角色。
|
||||
func grantDomainAdminRole(ctx context.Context, dc identitydomains.IdentityDomainsClient, userSCIMID string) error {
|
||||
role, err := findDomainAdminRole(ctx, dc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = dc.CreateGrant(ctx, identitydomains.CreateGrantRequest{
|
||||
Grant: identitydomains.Grant{
|
||||
Schemas: []string{"urn:ietf:params:scim:schemas:oracle:idcs:Grant"},
|
||||
GrantMechanism: identitydomains.GrantGrantMechanismAdministratorToUser,
|
||||
Grantee: &identitydomains.GrantGrantee{
|
||||
Type: identitydomains.GrantGranteeTypeUser,
|
||||
Value: &userSCIMID,
|
||||
},
|
||||
App: &identitydomains.GrantApp{Value: role.App.Value},
|
||||
Entitlement: &identitydomains.GrantEntitlement{
|
||||
AttributeName: common.String("appRoles"),
|
||||
AttributeValue: role.Id,
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("grant domain administrator role: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// revokeDomainAdminRole 撤销直接授予用户的「身份域管理员」角色;
|
||||
// 只删 ADMINISTRATOR_TO_USER 机制的授权,经组继承的不动。
|
||||
func revokeDomainAdminRole(ctx context.Context, dc identitydomains.IdentityDomainsClient, userSCIMID string) error {
|
||||
role, err := findDomainAdminRole(ctx, dc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filter := fmt.Sprintf("grantee.value eq %q and entitlement.attributeValue eq %q", userSCIMID, deref(role.Id))
|
||||
grants, err := dc.ListGrants(ctx, identitydomains.ListGrantsRequest{
|
||||
Filter: &filter,
|
||||
Attributes: common.String("id,grantMechanism"),
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("find domain administrator grants: %w", err)
|
||||
}
|
||||
for _, g := range grants.Resources {
|
||||
if g.GrantMechanism != identitydomains.GrantGrantMechanismAdministratorToUser {
|
||||
continue
|
||||
}
|
||||
if _, err := dc.DeleteGrant(ctx, identitydomains.DeleteGrantRequest{GrantId: g.Id}); err != nil {
|
||||
return fmt.Errorf("revoke domain administrator role: %w", err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addUserToAdminGroup 把用户加入 Administrators 组。
|
||||
func addUserToAdminGroup(ctx context.Context, dc identitydomains.IdentityDomainsClient, userSCIMID string) error {
|
||||
group, err := adminGroupRef(ctx, dc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if group == nil {
|
||||
return fmt.Errorf("administrators group not found")
|
||||
}
|
||||
var members interface{} = []map[string]string{{"value": userSCIMID, "type": "User"}}
|
||||
_, err = dc.PatchGroup(ctx, identitydomains.PatchGroupRequest{
|
||||
GroupId: group.Value,
|
||||
PatchOp: identitydomains.PatchOp{
|
||||
Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
|
||||
Operations: []identitydomains.Operations{{
|
||||
Op: identitydomains.OperationsOpAdd,
|
||||
Path: common.String("members"),
|
||||
Value: &members,
|
||||
}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("add user to administrators group: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// removeUserFromAdminGroup 把用户移出 Administrators 组。
|
||||
func removeUserFromAdminGroup(ctx context.Context, dc identitydomains.IdentityDomainsClient, userSCIMID string) error {
|
||||
group, err := adminGroupRef(ctx, dc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if group == nil {
|
||||
return fmt.Errorf("administrators group not found")
|
||||
}
|
||||
path := fmt.Sprintf("members[value eq %q]", userSCIMID)
|
||||
_, err = dc.PatchGroup(ctx, identitydomains.PatchGroupRequest{
|
||||
GroupId: group.Value,
|
||||
PatchOp: identitydomains.PatchOp{
|
||||
Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
|
||||
Operations: []identitydomains.Operations{{
|
||||
Op: identitydomains.OperationsOpRemove,
|
||||
Path: &path,
|
||||
}},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove user from administrators group: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// toDomainTenantUser 把 SCIM 用户转为列表通用形状(ID 用对应的 IAM OCID)。
|
||||
func toDomainTenantUser(u identitydomains.User) TenantUser {
|
||||
out := TenantUser{
|
||||
ID: deref(u.Ocid),
|
||||
Name: deref(u.UserName),
|
||||
Description: deref(u.Description),
|
||||
LifecycleState: "ACTIVE",
|
||||
}
|
||||
if out.ID == "" {
|
||||
out.ID = deref(u.Id)
|
||||
}
|
||||
for _, e := range u.Emails {
|
||||
if e.Primary != nil && *e.Primary {
|
||||
out.Email = deref(e.Value)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// UpdateTenantUserInput 是编辑用户资料的输入;nil 字段不修改。
|
||||
// 管理员字段为三态:nil 不动,true 授予/加入,false 撤销/移出。
|
||||
type UpdateTenantUserInput struct {
|
||||
Description *string
|
||||
Email *string
|
||||
GivenName *string
|
||||
FamilyName *string
|
||||
GrantDomainAdmin *bool // 「身份域管理员」应用角色
|
||||
AddToAdminGroup *bool // Administrators 组成员
|
||||
}
|
||||
|
||||
// UpdateTenantUser 实现 Client:编辑用户资料(须发往 home region)。
|
||||
// 优先 Identity Domains SCIM(支持姓名);用户不在域中时回退经典
|
||||
// IAM API(仅备注与邮箱,姓名变更报错)。
|
||||
func (c *RealClient) UpdateTenantUser(ctx context.Context, cred Credentials, homeRegion, userID string, in UpdateTenantUserInput) (TenantUser, error) {
|
||||
dc, err := c.domainsClient(ctx, cred, homeRegion)
|
||||
if err != nil {
|
||||
return TenantUser{}, err
|
||||
}
|
||||
domainUser, found, err := domainUserByOcid(ctx, dc, userID)
|
||||
if err != nil {
|
||||
return TenantUser{}, err
|
||||
}
|
||||
if found {
|
||||
user, err := patchDomainUser(ctx, dc, domainUser, in)
|
||||
if err != nil {
|
||||
return user, err
|
||||
}
|
||||
scimID := deref(domainUser.Id)
|
||||
if err := applyAdminGrants(ctx, dc, scimID, in); err != nil {
|
||||
return user, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
if in.GivenName != nil || in.FamilyName != nil {
|
||||
return TenantUser{}, fmt.Errorf("update user %s: 姓名仅身份域用户可修改(该用户不在身份域中)", userID)
|
||||
}
|
||||
if in.GrantDomainAdmin != nil || in.AddToAdminGroup != nil {
|
||||
return TenantUser{}, fmt.Errorf("update user %s: 管理员授权仅身份域用户可操作(该用户不在身份域中)", userID)
|
||||
}
|
||||
return c.updateUserClassic(ctx, cred, homeRegion, userID, in)
|
||||
}
|
||||
|
||||
// updateUserClassic 走经典 IAM API 更新备注与邮箱。
|
||||
func (c *RealClient) updateUserClassic(ctx context.Context, cred Credentials, homeRegion, userID string, in UpdateTenantUserInput) (TenantUser, error) {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return TenantUser{}, err
|
||||
}
|
||||
resp, err := ic.UpdateUser(ctx, identity.UpdateUserRequest{
|
||||
UserId: &userID,
|
||||
UpdateUserDetails: identity.UpdateUserDetails{Description: in.Description, Email: in.Email},
|
||||
})
|
||||
if err != nil {
|
||||
return TenantUser{}, fmt.Errorf("update user %s: %w", userID, err)
|
||||
}
|
||||
return toTenantUser(resp.User, cred.UserOCID), nil
|
||||
}
|
||||
|
||||
// patchDomainUser 以 SCIM Patch 修改域用户的指定字段。
|
||||
func patchDomainUser(ctx context.Context, dc identitydomains.IdentityDomainsClient, u identitydomains.User, in UpdateTenantUserInput) (TenantUser, error) {
|
||||
ops := buildUserPatchOps(u, in)
|
||||
if len(ops) == 0 {
|
||||
return toDomainTenantUser(u), nil
|
||||
}
|
||||
resp, err := dc.PatchUser(ctx, identitydomains.PatchUserRequest{
|
||||
UserId: u.Id,
|
||||
PatchOp: identitydomains.PatchOp{
|
||||
Schemas: []string{"urn:ietf:params:scim:api:messages:2.0:PatchOp"},
|
||||
Operations: ops,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return TenantUser{}, fmt.Errorf("patch domain user %s: %w", deref(u.UserName), err)
|
||||
}
|
||||
return toDomainTenantUser(resp.User), nil
|
||||
}
|
||||
|
||||
// applyAdminGrants 同步管理员状态:nil 不动,true 授予/加入,false 撤销/移出。
|
||||
func applyAdminGrants(ctx context.Context, dc identitydomains.IdentityDomainsClient, scimID string, in UpdateTenantUserInput) error {
|
||||
if in.GrantDomainAdmin != nil {
|
||||
apply := revokeDomainAdminRole
|
||||
if *in.GrantDomainAdmin {
|
||||
apply = grantDomainAdminRole
|
||||
}
|
||||
if err := apply(ctx, dc, scimID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if in.AddToAdminGroup != nil {
|
||||
apply := removeUserFromAdminGroup
|
||||
if *in.AddToAdminGroup {
|
||||
apply = addUserToAdminGroup
|
||||
}
|
||||
return apply(ctx, dc, scimID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildUserPatchOps(u identitydomains.User, in UpdateTenantUserInput) []identitydomains.Operations {
|
||||
var ops []identitydomains.Operations
|
||||
if in.GivenName != nil {
|
||||
ops = append(ops, replaceOp("name.givenName", *in.GivenName))
|
||||
}
|
||||
if in.FamilyName != nil {
|
||||
ops = append(ops, replaceOp("name.familyName", *in.FamilyName))
|
||||
}
|
||||
if in.Description != nil {
|
||||
ops = append(ops, replaceOp("description", *in.Description))
|
||||
}
|
||||
if in.Email != nil {
|
||||
ops = append(ops, emailPatchOp(u, *in.Email))
|
||||
}
|
||||
return ops
|
||||
}
|
||||
|
||||
// emailPatchOp 生成主邮箱修改操作:已有 work 邮箱按 filter 路径替换,
|
||||
// 没有则整体添加一条 primary work 邮箱(避免整组 replace 冲掉 recovery 邮箱)。
|
||||
func emailPatchOp(u identitydomains.User, email string) identitydomains.Operations {
|
||||
for _, e := range u.Emails {
|
||||
if e.Type == identitydomains.UserEmailsTypeWork {
|
||||
return replaceOp(`emails[type eq "work"].value`, email)
|
||||
}
|
||||
}
|
||||
var value interface{} = []map[string]interface{}{
|
||||
{"value": email, "type": "work", "primary": true},
|
||||
}
|
||||
return identitydomains.Operations{
|
||||
Op: identitydomains.OperationsOpAdd,
|
||||
Path: common.String("emails"),
|
||||
Value: &value,
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteTenantUser 实现 Client:删除 IAM 用户(须发往 home region)。
|
||||
func (c *RealClient) DeleteTenantUser(ctx context.Context, cred Credentials, homeRegion, userID string) error {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := ic.DeleteUser(ctx, identity.DeleteUserRequest{UserId: &userID}); err != nil {
|
||||
return fmt.Errorf("delete user %s: %w", userID, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetTenantUserPassword 实现 Client:重置用户控制台密码并返回一次性新密码。
|
||||
// 优先经典 IAM API;新式租户(用户只存在于 Identity Domain)自动回退 SCIM。
|
||||
func (c *RealClient) ResetTenantUserPassword(ctx context.Context, cred Credentials, homeRegion, userID string) (string, error) {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := ic.CreateOrResetUIPassword(ctx, identity.CreateOrResetUIPasswordRequest{UserId: &userID})
|
||||
if err == nil {
|
||||
return deref(resp.Password), nil
|
||||
}
|
||||
if !isClassicUnsupported(err) {
|
||||
return "", fmt.Errorf("reset ui password of %s: %w", userID, err)
|
||||
}
|
||||
return c.resetPasswordViaDomain(ctx, cred, homeRegion, userID)
|
||||
}
|
||||
|
||||
// resetPasswordViaDomain 通过 Identity Domains SCIM API 强设随机新密码。
|
||||
func (c *RealClient) resetPasswordViaDomain(ctx context.Context, cred Credentials, homeRegion, userID string) (string, error) {
|
||||
dc, err := c.domainsClient(ctx, cred, homeRegion)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
scimID, err := scimUserID(ctx, dc, userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
password, err := generatePassword()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bypass := true
|
||||
_, err = dc.PutUserPasswordChanger(ctx, identitydomains.PutUserPasswordChangerRequest{
|
||||
UserPasswordChangerId: &scimID,
|
||||
UserPasswordChanger: identitydomains.UserPasswordChanger{
|
||||
Schemas: []string{"urn:ietf:params:scim:schemas:oracle:idcs:UserPasswordChanger"},
|
||||
Password: &password,
|
||||
BypassNotification: &bypass,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// 身份域通常不允许凭据本人经管理通道改自己的密码
|
||||
if userID == cred.UserOCID {
|
||||
return "", fmt.Errorf("change password via identity domain(当前签名用户请在 OCI 控制台修改密码): %w", err)
|
||||
}
|
||||
return "", fmt.Errorf("change password via identity domain: %w", err)
|
||||
}
|
||||
return password, nil
|
||||
}
|
||||
|
||||
// DeleteTenantUserMfaDevices 实现 Client:清除用户全部 MFA。
|
||||
// 先删经典 TOTP 设备,再通过 Identity Domains 移除全部认证因子(尽力)。
|
||||
func (c *RealClient) DeleteTenantUserMfaDevices(ctx context.Context, cred Credentials, homeRegion, userID string) (int, error) {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
deleted, err := deleteClassicTotpDevices(ctx, ic, userID)
|
||||
if err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
if err := c.removeDomainFactors(ctx, cred, homeRegion, userID); err != nil {
|
||||
return deleted, err
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
func deleteClassicTotpDevices(ctx context.Context, ic identity.IdentityClient, userID string) (int, error) {
|
||||
resp, err := ic.ListMfaTotpDevices(ctx, identity.ListMfaTotpDevicesRequest{UserId: &userID})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("list mfa totp devices: %w", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, item := range resp.Items {
|
||||
_, err := ic.DeleteMfaTotpDevice(ctx, identity.DeleteMfaTotpDeviceRequest{
|
||||
UserId: &userID,
|
||||
MfaTotpDeviceId: item.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return deleted, fmt.Errorf("delete mfa totp device: %w", err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
// removeDomainFactors 通过 SCIM AuthenticationFactorsRemover 移除用户
|
||||
// 在 Identity Domain 注册的全部认证因子;用户不在域中时视为无事可做。
|
||||
func (c *RealClient) removeDomainFactors(ctx context.Context, cred Credentials, homeRegion, userID string) error {
|
||||
dc, err := c.domainsClient(ctx, cred, homeRegion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scimID, err := scimUserID(ctx, dc, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
_, err = dc.CreateAuthenticationFactorsRemover(ctx, identitydomains.CreateAuthenticationFactorsRemoverRequest{
|
||||
AuthenticationFactorsRemover: identitydomains.AuthenticationFactorsRemover{
|
||||
Schemas: []string{"urn:ietf:params:scim:schemas:oracle:idcs:AuthenticationFactorsRemover"},
|
||||
User: &identitydomains.AuthenticationFactorsRemoverUser{Value: &scimID},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("remove authentication factors: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTenantUserApiKeys 实现 Client:删除用户的 API Key。
|
||||
// 默认跳过当前配置正在使用的指纹以免面板失联,includeCurrent 为 true 时一并删除。
|
||||
func (c *RealClient) DeleteTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string, includeCurrent bool) (int, error) {
|
||||
ic, err := c.identityClientAt(cred, homeRegion)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
resp, err := ic.ListApiKeys(ctx, identity.ListApiKeysRequest{UserId: &userID})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("list api keys: %w", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, key := range resp.Items {
|
||||
fingerprint := deref(key.Fingerprint)
|
||||
if !includeCurrent && userID == cred.UserOCID && fingerprint == cred.Fingerprint {
|
||||
continue
|
||||
}
|
||||
_, err := ic.DeleteApiKey(ctx, identity.DeleteApiKeyRequest{UserId: &userID, Fingerprint: &fingerprint})
|
||||
if err != nil {
|
||||
return deleted, fmt.Errorf("delete api key %s: %w", fingerprint, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
|
||||
// scimUserID 用经典 OCID 在 Identity Domain 中解析 SCIM 用户 ID。
|
||||
func scimUserID(ctx context.Context, dc identitydomains.IdentityDomainsClient, classicOCID string) (string, error) {
|
||||
u, found, err := domainUserByOcid(ctx, dc, classicOCID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !found {
|
||||
return "", fmt.Errorf("user %s not found in identity domain", classicOCID)
|
||||
}
|
||||
return *u.Id, nil
|
||||
}
|
||||
|
||||
// domainUserByOcid 用经典 OCID 查域用户;不存在时 found 为 false 而非报错。
|
||||
// extraAttrs 用于按需追加 groups、appRoles 等重量级属性。
|
||||
func domainUserByOcid(ctx context.Context, dc identitydomains.IdentityDomainsClient, classicOCID string, extraAttrs ...string) (identitydomains.User, bool, error) {
|
||||
filter := fmt.Sprintf("ocid eq %q", classicOCID)
|
||||
attrs := strings.Join(append([]string{"id,ocid,userName,description,name,emails"}, extraAttrs...), ",")
|
||||
count := 1
|
||||
resp, err := dc.ListUsers(ctx, identitydomains.ListUsersRequest{
|
||||
Filter: &filter,
|
||||
Attributes: &attrs,
|
||||
Count: &count,
|
||||
})
|
||||
if err != nil {
|
||||
return identitydomains.User{}, false, fmt.Errorf("find domain user by ocid: %w", err)
|
||||
}
|
||||
if len(resp.Resources) == 0 || resp.Resources[0].Id == nil {
|
||||
return identitydomains.User{}, false, nil
|
||||
}
|
||||
return resp.Resources[0], true, nil
|
||||
}
|
||||
|
||||
// isClassicUnsupported 判断经典 IAM API 对该用户不适用(仅存在于 Identity Domain)。
|
||||
func isClassicUnsupported(err error) bool {
|
||||
var svcErr common.ServiceError
|
||||
if errors.As(err, &svcErr) {
|
||||
return svcErr.GetHTTPStatusCode() == 404 || svcErr.GetHTTPStatusCode() == 400
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// passwordCharsets 保证生成的密码覆盖四类字符,满足常见复杂度策略。
|
||||
var passwordCharsets = []string{
|
||||
"ABCDEFGHJKLMNPQRSTUVWXYZ",
|
||||
"abcdefghijkmnpqrstuvwxyz",
|
||||
"23456789",
|
||||
"#-_+!",
|
||||
}
|
||||
|
||||
// generatePassword 生成 16 位随机密码,每类字符至少出现一次。
|
||||
func generatePassword() (string, error) {
|
||||
const length = 16
|
||||
all := strings.Join(passwordCharsets, "")
|
||||
out := make([]byte, 0, length)
|
||||
for _, set := range passwordCharsets {
|
||||
ch, err := randomChar(set)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out = append(out, ch)
|
||||
}
|
||||
for len(out) < length {
|
||||
ch, err := randomChar(all)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
out = append(out, ch)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
func randomChar(set string) (byte, error) {
|
||||
n, err := rand.Int(rand.Reader, big.NewInt(int64(len(set))))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("generate password: %w", err)
|
||||
}
|
||||
return set[n.Int64()], nil
|
||||
}
|
||||
|
||||
func toTenantUser(u identity.User, currentUserOCID string) TenantUser {
|
||||
return TenantUser{
|
||||
ID: deref(u.Id),
|
||||
Name: deref(u.Name),
|
||||
Description: deref(u.Description),
|
||||
Email: deref(u.Email),
|
||||
EmailVerified: u.EmailVerified != nil && *u.EmailVerified,
|
||||
MfaActivated: u.IsMfaActivated != nil && *u.IsMfaActivated,
|
||||
LifecycleState: string(u.LifecycleState),
|
||||
IsCurrentUser: deref(u.Id) == currentUserOCID,
|
||||
TimeCreated: sdkTime(u.TimeCreated),
|
||||
LastLoginTime: sdkTime(u.LastSuccessfulLoginTime),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user