初始提交:OCI 面板后端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:04 +08:00
commit b9a3e97e84
168 changed files with 31794 additions and 0 deletions
+736
View File
@@ -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),
}
}