package oci import ( "context" "crypto/rand" "errors" "fmt" "math/big" "slices" "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" // adminGroupNames 是租户管理员组候选名(按优先序):原生域为 Administrators, // IDCS 迁移域(idcs foundation,如 OracleIdentityCloudService)为 OCI_Administrators; // 迁移域另有 IDCS_Administrators 属身份域管理范畴,由上面的应用角色概念覆盖,不算在内。 var adminGroupNames = []string{"Administrators", "OCI_Administrators"} // isAdminGroupName 判断组显示名是否为租户管理员组。 func isAdminGroupName(name string) bool { return slices.Contains(adminGroupNames, name) } // 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, domainID, userID string) (TenantUserDetail, error) { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) 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 isAdminGroupName(deref(g.Display)) { 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 用户。 // domainID 非空时经 Identity Domains SCIM 只列该域用户; // 为空时走经典 IAM(跨域拍平视图,兼容无域老租户)。 func (c *RealClient) ListTenantUsers(ctx context.Context, cred Credentials, homeRegion, domainID string) ([]TenantUser, error) { if domainID != "" { return c.listDomainUsers(ctx, cred, homeRegion, domainID) } 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 } } // scimListAttrs 是按域列用户时请求的属性集:列表字段 + MFA 与最近登录扩展。 const scimListAttrs = "id,ocid,userName,description,emails,active,meta.created," + "urn:ietf:params:scim:schemas:oracle:idcs:extension:mfa:User:mfaStatus," + "urn:ietf:params:scim:schemas:oracle:idcs:extension:userState:User:lastSuccessfulLoginDate" // maxScimUserPages 限制按域列用户的翻页数(每页 200,上限 5000 人)。 const maxScimUserPages = 25 // listDomainUsers 经 SCIM 按 startIndex 翻页列出域内全部用户。 func (c *RealClient) listDomainUsers(ctx context.Context, cred Credentials, homeRegion, domainID string) ([]TenantUser, error) { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) if err != nil { return nil, err } var users []TenantUser attrs := scimListAttrs count := 200 start := 1 for range maxScimUserPages { req := identitydomains.ListUsersRequest{Attributes: &attrs, Count: &count, StartIndex: &start} resp, err := dc.ListUsers(ctx, req) if err != nil { return nil, fmt.Errorf("list domain users: %w", err) } for _, u := range resp.Resources { users = append(users, toDomainListUser(u, cred.UserOCID)) } start += len(resp.Resources) if resp.TotalResults == nil || start > *resp.TotalResults || len(resp.Resources) == 0 { return orEmpty(users), nil } } return orEmpty(users), nil } // toDomainListUser 把 SCIM 用户映射为列表 DTO;ID 用 IAM OCID // 保持与经典路径同一 id 语义(IsCurrentUser 比对、后续操作路由)。 func toDomainListUser(u identitydomains.User, currentUserOCID string) TenantUser { out := toDomainTenantUser(u) out.IsCurrentUser = out.ID == currentUserOCID if u.Active != nil && !*u.Active { out.LifecycleState = "INACTIVE" } if u.Meta != nil { out.TimeCreated = parseScimTime(u.Meta.Created) } if ext := u.UrnIetfParamsScimSchemasOracleIdcsExtensionMfaUser; ext != nil { out.MfaActivated = ext.MfaStatus == identitydomains.ExtensionMfaUserMfaStatusEnrolled } if ext := u.UrnIetfParamsScimSchemasOracleIdcsExtensionUserStateUser; ext != nil { out.LastLoginTime = parseScimTime(ext.LastSuccessfulLoginDate) } for _, e := range u.Emails { if e.Primary != nil && *e.Primary { out.EmailVerified = e.Verified != nil && *e.Verified } } return out } // parseScimTime 解析 SCIM 的 RFC3339 时间串;空值或格式异常返回 nil。 func parseScimTime(s *string) *time.Time { if s == nil || *s == "" { return nil } t, err := time.Parse(time.RFC3339, *s) if err != nil { return nil } return &t } // 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,此时管理员选项无法执行、姓名并入描述。 // domainID 非空表示用户显式选择了目标域,SCIM 失败不再回退(避免建错域)。 func (c *RealClient) CreateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID string, in CreateTenantUserInput) (TenantUser, error) { user, err := c.createUserViaDomain(ctx, cred, homeRegion, domainID, in) if err == nil { return user, nil } if domainID != "" || 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, domainID string, in CreateTenantUserInput) (TenantUser, error) { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) 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(仅备注与邮箱,姓名变更报错);domainID 非空时不回退。 func (c *RealClient) UpdateTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string, in UpdateTenantUserInput) (TenantUser, error) { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) 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 domainID != "" { return TenantUser{}, fmt.Errorf("update user %s: user not found in identity domain", userID) } 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)。 // domainID 非空时经该域 SCIM 删除(经典 API 对非默认域用户不生效)。 func (c *RealClient) DeleteTenantUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) error { if domainID != "" { return c.deleteDomainUser(ctx, cred, homeRegion, domainID, userID) } 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 } // deleteDomainUser 经 SCIM 强制删除域用户(forceDelete 连带其授权与组成员关系)。 func (c *RealClient) deleteDomainUser(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) error { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) if err != nil { return err } scimID, err := scimUserID(ctx, dc, userID) if err != nil { return err } force := true _, err = dc.DeleteUser(ctx, identitydomains.DeleteUserRequest{UserId: &scimID, ForceDelete: &force}) if err != nil { return fmt.Errorf("delete domain user %s: %w", userID, err) } return nil } // ResetTenantUserPassword 实现 Client:重置用户控制台密码并返回一次性新密码。 // domainID 非空直走该域 SCIM;为空优先经典 IAM API, // 新式租户(用户只存在于 Identity Domain)自动回退 SCIM。 func (c *RealClient) ResetTenantUserPassword(ctx context.Context, cred Credentials, homeRegion, domainID, userID string) (string, error) { if domainID != "" { return c.resetPasswordViaDomain(ctx, cred, homeRegion, domainID, userID) } 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, domainID, userID string) (string, error) { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) 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, domainID, userID string) (int, error) { // 显式指定域时用户的认证因子只在域内,经典 TOTP 清理不适用 if domainID != "" { return 0, c.removeDomainFactors(ctx, cred, homeRegion, domainID, userID) } 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, domainID, userID string) error { dc, err := c.domainsClient(ctx, cred, homeRegion, domainID) 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 } // TenantUserApiKey 是用户名下的一把 API 签名 key。 type TenantUserApiKey struct { Fingerprint string `json:"fingerprint"` TimeCreated *time.Time `json:"timeCreated"` // IsCurrent 表示该 key 正被当前配置用于签名请求 IsCurrent bool `json:"isCurrent"` } // ListTenantUserApiKeys 实现 Client:列出用户的 API 签名 key。 func (c *RealClient) ListTenantUserApiKeys(ctx context.Context, cred Credentials, homeRegion, userID string) ([]TenantUserApiKey, error) { ic, err := c.identityClientAt(cred, homeRegion) if err != nil { return nil, err } resp, err := ic.ListApiKeys(ctx, identity.ListApiKeysRequest{UserId: &userID}) if err != nil { return nil, fmt.Errorf("list api keys: %w", err) } keys := make([]TenantUserApiKey, 0, len(resp.Items)) for _, item := range resp.Items { k := TenantUserApiKey{Fingerprint: deref(item.Fingerprint)} if item.TimeCreated != nil { t := item.TimeCreated.Time k.TimeCreated = &t } k.IsCurrent = userID == cred.UserOCID && k.Fingerprint == cred.Fingerprint keys = append(keys, k) } return keys, nil } // UploadTenantUserApiKey 实现 Client:为用户上传 RSA 公钥,返回 OCI 回填的指纹。 func (c *RealClient) UploadTenantUserApiKey(ctx context.Context, cred Credentials, homeRegion, userID, publicKeyPEM string) (string, error) { ic, err := c.identityClientAt(cred, homeRegion) if err != nil { return "", err } resp, err := ic.UploadApiKey(ctx, identity.UploadApiKeyRequest{ UserId: &userID, CreateApiKeyDetails: identity.CreateApiKeyDetails{Key: &publicKeyPEM}, }) if err != nil { return "", fmt.Errorf("upload api key: %w", err) } return deref(resp.Fingerprint), nil } // DeleteTenantUserApiKey 实现 Client:按指纹删除用户的单把 API key。 func (c *RealClient) DeleteTenantUserApiKey(ctx context.Context, cred Credentials, homeRegion, userID, fingerprint string) error { ic, err := c.identityClientAt(cred, homeRegion) if err != nil { return err } _, err = ic.DeleteApiKey(ctx, identity.DeleteApiKeyRequest{UserId: &userID, Fingerprint: &fingerprint}) if err != nil { return fmt.Errorf("delete api key %s: %w", fingerprint, err) } return 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), } }