mirror of
https://github.com/versity/versitygw.git
synced 2026-09-19 22:44:28 +00:00
feat: add IAM Role CRUD
Adds `CreateRole`, `GetRole`, `ListRoles`, `DeleteRole`, and `UpdateAssumeRolePolicy` to the standalone IAM service, following the same controller/storage patterns established for users. Both the internal filesystem/S3-backed store and the Vault-backed store implement the new `Storer` methods, with role-specific indexing and lookup helpers mirroring the existing user ones. Role creation requires a trust policy, passed as `AssumeRolePolicyDocument`. A trust policy is a distinct kind of IAM policy document that governs who (or what) is allowed to assume a role, rather than what actions the role itself is permitted to perform. Its grammar is effectively the inverse of an identity policy: `Principal` is required, `Action`/`NotAction` values must carry the `sts:` prefix, and `Resource`/`NotResource` are forbidden. This is implemented in `iamapi/policy/trust.go` as a new validation path alongside the existing identity-policy validation, and is reused by `UpdateAssumeRolePolicy` when replacing a role's trust policy. Also fixes user name uniqueness enforcement to be case-insensitive, matching AWS IAM behavior, and applies the same case-insensitive handling to role names. The internal store now maintains lowercase name indexes for both users and roles, and the Vault store resolves the canonical stored key via a case-insensitive list-and-compare fallback since Vault's KV paths are case-sensitive.
This commit is contained in:
+246
-25
@@ -54,12 +54,24 @@ type iamConfig struct {
|
||||
// AccessKeyIndex maps an access key id to the username that owns it,
|
||||
// so GetAccessKeyLastUsed can resolve a key without scanning every user.
|
||||
AccessKeyIndex map[string]string `json:"accessKeyIndex"`
|
||||
// UserNameIndex maps a lowercased user name to the canonical (as-created)
|
||||
// stored user name, so lookups can enforce AWS's case-insensitive
|
||||
// uniqueness while still preserving the original casing in conf.Users's
|
||||
// key and the stored User.UserName.
|
||||
UserNameIndex map[string]string `json:"userNameIndex"`
|
||||
|
||||
Roles map[string]types.Role `json:"roles"`
|
||||
// RoleNameIndex is UserNameIndex's counterpart for roles.
|
||||
RoleNameIndex map[string]string `json:"roleNameIndex"`
|
||||
}
|
||||
|
||||
func defaultIAMConfig() iamConfig {
|
||||
return iamConfig{
|
||||
Users: map[string]types.User{},
|
||||
AccessKeyIndex: map[string]string{},
|
||||
UserNameIndex: map[string]string{},
|
||||
Roles: map[string]types.Role{},
|
||||
RoleNameIndex: map[string]string{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +82,49 @@ func normalizeIAMConfig(conf *iamConfig) {
|
||||
if conf.AccessKeyIndex == nil {
|
||||
conf.AccessKeyIndex = make(map[string]string)
|
||||
}
|
||||
if conf.UserNameIndex == nil {
|
||||
conf.UserNameIndex = make(map[string]string)
|
||||
}
|
||||
for name := range conf.Users {
|
||||
key := strings.ToLower(name)
|
||||
if _, ok := conf.UserNameIndex[key]; !ok {
|
||||
conf.UserNameIndex[key] = name
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Roles == nil {
|
||||
conf.Roles = make(map[string]types.Role)
|
||||
}
|
||||
if conf.RoleNameIndex == nil {
|
||||
conf.RoleNameIndex = make(map[string]string)
|
||||
}
|
||||
for name := range conf.Roles {
|
||||
key := strings.ToLower(name)
|
||||
if _, ok := conf.RoleNameIndex[key]; !ok {
|
||||
conf.RoleNameIndex[key] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lookupUser resolves name to the canonical stored user name and entry,
|
||||
// case-insensitively, via conf.UserNameIndex.
|
||||
func lookupUser(conf iamConfig, name string) (string, types.User, bool) {
|
||||
canonical, ok := conf.UserNameIndex[strings.ToLower(name)]
|
||||
if !ok {
|
||||
return "", types.User{}, false
|
||||
}
|
||||
user, ok := conf.Users[canonical]
|
||||
return canonical, user, ok
|
||||
}
|
||||
|
||||
// lookupRole is lookupUser's counterpart for roles.
|
||||
func lookupRole(conf iamConfig, name string) (string, types.Role, bool) {
|
||||
canonical, ok := conf.RoleNameIndex[strings.ToLower(name)]
|
||||
if !ok {
|
||||
return "", types.Role{}, false
|
||||
}
|
||||
role, ok := conf.Roles[canonical]
|
||||
return canonical, role, ok
|
||||
}
|
||||
|
||||
func (s *InternalStore) CreateUser(_ context.Context, user types.User) (*types.User, error) {
|
||||
@@ -82,7 +137,8 @@ func (s *InternalStore) CreateUser(_ context.Context, user types.User) (*types.U
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if _, ok := conf.Users[user.UserName]; ok {
|
||||
key := strings.ToLower(user.UserName)
|
||||
if _, ok := conf.UserNameIndex[key]; ok {
|
||||
return nil, iamerr.EntityAlreadyExistsUser(user.UserName)
|
||||
}
|
||||
for _, existing := range conf.Users {
|
||||
@@ -92,6 +148,7 @@ func (s *InternalStore) CreateUser(_ context.Context, user types.User) (*types.U
|
||||
}
|
||||
|
||||
conf.Users[user.UserName] = user
|
||||
conf.UserNameIndex[key] = user.UserName
|
||||
return json.Marshal(conf)
|
||||
}); err != nil {
|
||||
return nil, unwrapAPIError(err)
|
||||
@@ -110,7 +167,7 @@ func (s *InternalStore) DeleteUser(_ context.Context, username string) error {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[username]
|
||||
canonical, user, ok := lookupUser(conf, username)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(username)
|
||||
}
|
||||
@@ -121,7 +178,8 @@ func (s *InternalStore) DeleteUser(_ context.Context, username string) error {
|
||||
return nil, iamerr.GetAPIError(iamerr.ErrDeleteConflict)
|
||||
}
|
||||
|
||||
delete(conf.Users, username)
|
||||
delete(conf.Users, canonical)
|
||||
delete(conf.UserNameIndex, strings.ToLower(canonical))
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
@@ -136,7 +194,7 @@ func (s *InternalStore) GetUser(_ context.Context, username string) (*types.User
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[username]
|
||||
_, user, ok := lookupUser(conf, username)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(username)
|
||||
}
|
||||
@@ -204,7 +262,7 @@ func (s *InternalStore) UpdateUser(_ context.Context, input UpdateUserInput) (*t
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
canonical, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -213,8 +271,8 @@ func (s *InternalStore) UpdateUser(_ context.Context, input UpdateUserInput) (*t
|
||||
if input.NewUserName != "" {
|
||||
finalName = input.NewUserName
|
||||
}
|
||||
if finalName != input.UserName {
|
||||
if _, ok := conf.Users[finalName]; ok {
|
||||
if !strings.EqualFold(finalName, canonical) {
|
||||
if _, ok := conf.UserNameIndex[strings.ToLower(finalName)]; ok {
|
||||
return nil, iamerr.EntityAlreadyExistsUser(finalName)
|
||||
}
|
||||
}
|
||||
@@ -229,13 +287,15 @@ func (s *InternalStore) UpdateUser(_ context.Context, input UpdateUserInput) (*t
|
||||
user.Arn = input.NewArn
|
||||
}
|
||||
|
||||
if user.UserName != input.UserName {
|
||||
delete(conf.Users, input.UserName)
|
||||
if user.UserName != canonical {
|
||||
delete(conf.Users, canonical)
|
||||
delete(conf.UserNameIndex, strings.ToLower(canonical))
|
||||
for _, key := range user.AccessKeys {
|
||||
conf.AccessKeyIndex[key.AccessKeyId] = user.UserName
|
||||
}
|
||||
}
|
||||
conf.Users[user.UserName] = user
|
||||
conf.UserNameIndex[strings.ToLower(user.UserName)] = user.UserName
|
||||
updated = user
|
||||
|
||||
return json.Marshal(conf)
|
||||
@@ -257,7 +317,7 @@ func (s *InternalStore) CreateAccessKey(_ context.Context, input CreateAccessKey
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
canonical, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -274,11 +334,11 @@ func (s *InternalStore) CreateAccessKey(_ context.Context, input CreateAccessKey
|
||||
Status: input.Status,
|
||||
CreateDate: input.CreateDate,
|
||||
})
|
||||
conf.Users[input.UserName] = user
|
||||
conf.AccessKeyIndex[input.AccessKeyID] = input.UserName
|
||||
conf.Users[canonical] = user
|
||||
conf.AccessKeyIndex[input.AccessKeyID] = canonical
|
||||
|
||||
created = types.AccessKey{
|
||||
UserName: input.UserName,
|
||||
UserName: canonical,
|
||||
AccessKeyId: input.AccessKeyID,
|
||||
Status: input.Status,
|
||||
SecretAccessKey: input.SecretAccessKey,
|
||||
@@ -303,7 +363,7 @@ func (s *InternalStore) UpdateAccessKey(_ context.Context, input UpdateAccessKey
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
canonical, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -320,7 +380,7 @@ func (s *InternalStore) UpdateAccessKey(_ context.Context, input UpdateAccessKey
|
||||
return nil, iamerr.NoSuchEntityAccessKey(input.AccessKeyID)
|
||||
}
|
||||
|
||||
conf.Users[input.UserName] = user
|
||||
conf.Users[canonical] = user
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
@@ -336,7 +396,7 @@ func (s *InternalStore) DeleteAccessKey(_ context.Context, username, accessKeyID
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[username]
|
||||
canonical, user, ok := lookupUser(conf, username)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(username)
|
||||
}
|
||||
@@ -353,7 +413,7 @@ func (s *InternalStore) DeleteAccessKey(_ context.Context, username, accessKeyID
|
||||
}
|
||||
|
||||
user.AccessKeys = slices.Delete(user.AccessKeys, idx, idx+1)
|
||||
conf.Users[username] = user
|
||||
conf.Users[canonical] = user
|
||||
delete(conf.AccessKeyIndex, accessKeyID)
|
||||
|
||||
return json.Marshal(conf)
|
||||
@@ -402,7 +462,7 @@ func (s *InternalStore) ListAccessKeys(_ context.Context, input ListAccessKeysIn
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
canonical, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -410,7 +470,7 @@ func (s *InternalStore) ListAccessKeys(_ context.Context, input ListAccessKeysIn
|
||||
keys := make([]types.AccessKeyMetadata, 0, len(user.AccessKeys))
|
||||
for _, key := range user.AccessKeys {
|
||||
keys = append(keys, types.AccessKeyMetadata{
|
||||
UserName: input.UserName,
|
||||
UserName: canonical,
|
||||
AccessKeyId: key.AccessKeyId,
|
||||
Status: key.Status,
|
||||
CreateDate: key.CreateDate,
|
||||
@@ -459,7 +519,7 @@ func (s *InternalStore) PutUserPolicy(_ context.Context, input PutUserPolicyInpu
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
canonical, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -490,7 +550,7 @@ func (s *InternalStore) PutUserPolicy(_ context.Context, input PutUserPolicyInpu
|
||||
})
|
||||
}
|
||||
|
||||
conf.Users[input.UserName] = user
|
||||
conf.Users[canonical] = user
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
@@ -505,7 +565,7 @@ func (s *InternalStore) GetUserPolicy(_ context.Context, userName, policyName st
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[userName]
|
||||
_, user, ok := lookupUser(conf, userName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(userName)
|
||||
}
|
||||
@@ -530,7 +590,7 @@ func (s *InternalStore) DeleteUserPolicy(_ context.Context, userName, policyName
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[userName]
|
||||
canonical, user, ok := lookupUser(conf, userName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(userName)
|
||||
}
|
||||
@@ -547,7 +607,7 @@ func (s *InternalStore) DeleteUserPolicy(_ context.Context, userName, policyName
|
||||
}
|
||||
|
||||
user.Policies.Inline = slices.Delete(user.Policies.Inline, idx, idx+1)
|
||||
conf.Users[userName] = user
|
||||
conf.Users[canonical] = user
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
@@ -562,7 +622,7 @@ func (s *InternalStore) ListUserPolicies(_ context.Context, input ListUserPolici
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
_, user, ok := lookupUser(conf, input.UserName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
@@ -602,6 +662,160 @@ func (s *InternalStore) ListUserPolicies(_ context.Context, input ListUserPolici
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) CreateRole(_ context.Context, role types.Role) (*types.Role, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
role.EnsureRoleLastUsed()
|
||||
|
||||
if err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := strings.ToLower(role.RoleName)
|
||||
if _, ok := conf.RoleNameIndex[key]; ok {
|
||||
return nil, iamerr.EntityAlreadyExistsRole(role.RoleName)
|
||||
}
|
||||
for _, existing := range conf.Roles {
|
||||
if existing.RoleID == role.RoleID {
|
||||
return nil, ErrRoleIDAlreadyExists
|
||||
}
|
||||
}
|
||||
|
||||
conf.Roles[role.RoleName] = role
|
||||
conf.RoleNameIndex[key] = role.RoleName
|
||||
return json.Marshal(conf)
|
||||
}); err != nil {
|
||||
return nil, unwrapAPIError(err)
|
||||
}
|
||||
|
||||
return cloneRole(role), nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) GetRole(_ context.Context, roleName string) (*types.Role, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
|
||||
return cloneRole(role), nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) ListRoles(_ context.Context, input ListRolesInput) (*ListRolesOutput, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
roles := make([]types.Role, 0, len(conf.Roles))
|
||||
for _, role := range conf.Roles {
|
||||
if input.PathPrefix != "" && !strings.HasPrefix(role.Path, input.PathPrefix) {
|
||||
continue
|
||||
}
|
||||
// ListRoles entries omit RoleLastUsed even though it's persisted —
|
||||
// matches the documented list/get field asymmetry.
|
||||
role.RoleLastUsed = nil
|
||||
roles = append(roles, role)
|
||||
}
|
||||
sort.Slice(roles, func(i, j int) bool {
|
||||
return roles[i].RoleName < roles[j].RoleName
|
||||
})
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(roles)
|
||||
for i, role := range roles {
|
||||
if role.RoleName == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
roles = roles[start:]
|
||||
|
||||
limit := len(roles)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListRolesOutput{
|
||||
Roles: make([]types.Role, limit),
|
||||
}
|
||||
copy(out.Roles, roles[:limit])
|
||||
if limit < len(roles) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.Roles[limit-1].RoleName
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) DeleteRole(_ context.Context, roleName string) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
if len(role.Policies.Inline) > 0 {
|
||||
return nil, iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies)
|
||||
}
|
||||
|
||||
delete(conf.Roles, canonical)
|
||||
delete(conf.RoleNameIndex, strings.ToLower(canonical))
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) UpdateAssumeRolePolicy(_ context.Context, input UpdateAssumeRolePolicyInput) (*types.Role, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
var updated types.Role
|
||||
if err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, role, ok := lookupRole(conf, input.RoleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(input.RoleName)
|
||||
}
|
||||
|
||||
role.AssumeRolePolicyDocument = input.PolicyDocument
|
||||
conf.Roles[canonical] = role
|
||||
updated = role
|
||||
|
||||
return json.Marshal(conf)
|
||||
}); err != nil {
|
||||
return nil, unwrapAPIError(err)
|
||||
}
|
||||
|
||||
return cloneRole(updated), nil
|
||||
}
|
||||
|
||||
func cloneUser(user types.User) *types.User {
|
||||
cloned := user
|
||||
cloned.Tags = slices.Clone(user.Tags)
|
||||
@@ -609,3 +823,10 @@ func cloneUser(user types.User) *types.User {
|
||||
cloned.Policies.Inline = slices.Clone(user.Policies.Inline)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
func cloneRole(role types.Role) *types.Role {
|
||||
cloned := role
|
||||
cloned.Tags = slices.Clone(role.Tags)
|
||||
cloned.Policies.Inline = slices.Clone(role.Policies.Inline)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ const MaxInlinePolicyBytesPerUser = 2048
|
||||
var (
|
||||
ErrUserIDAlreadyExists = errors.New("iamapi: user id already exists")
|
||||
ErrAccessKeyIDAlreadyExists = errors.New("iamapi: access key id already exists")
|
||||
ErrRoleIDAlreadyExists = errors.New("iamapi: role id already exists")
|
||||
)
|
||||
|
||||
type ListUsersInput struct {
|
||||
@@ -108,6 +109,23 @@ type ListUserPoliciesOutput struct {
|
||||
Marker string
|
||||
}
|
||||
|
||||
type ListRolesInput struct {
|
||||
PathPrefix string
|
||||
Marker string
|
||||
MaxItems int32
|
||||
}
|
||||
|
||||
type ListRolesOutput struct {
|
||||
Roles []types.Role
|
||||
IsTruncated bool
|
||||
Marker string
|
||||
}
|
||||
|
||||
type UpdateAssumeRolePolicyInput struct {
|
||||
RoleName string
|
||||
PolicyDocument string
|
||||
}
|
||||
|
||||
// Storer is the IAM API storage backend contract.
|
||||
type Storer interface {
|
||||
CreateUser(ctx context.Context, user types.User) (*types.User, error)
|
||||
@@ -126,6 +144,12 @@ type Storer interface {
|
||||
GetUserPolicy(ctx context.Context, userName, policyName string) (*types.PolicyEntry, error)
|
||||
DeleteUserPolicy(ctx context.Context, userName, policyName string) error
|
||||
ListUserPolicies(ctx context.Context, input ListUserPoliciesInput) (*ListUserPoliciesOutput, error)
|
||||
|
||||
CreateRole(ctx context.Context, role types.Role) (*types.Role, error)
|
||||
GetRole(ctx context.Context, roleName string) (*types.Role, error)
|
||||
ListRoles(ctx context.Context, input ListRolesInput) (*ListRolesOutput, error)
|
||||
DeleteRole(ctx context.Context, roleName string) error
|
||||
UpdateAssumeRolePolicy(ctx context.Context, input UpdateAssumeRolePolicyInput) (*types.Role, error)
|
||||
}
|
||||
|
||||
func unwrapAPIError(err error) error {
|
||||
|
||||
@@ -221,3 +221,166 @@ func TestInternalStoreUserCRUDAndPagination(t *testing.T) {
|
||||
t.Fatalf("DeleteUser missing err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalStoreUserNameCaseInsensitive(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, err := NewInternal(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatalf("NewInternal: %v", err)
|
||||
}
|
||||
|
||||
if _, err := store.CreateUser(ctx, types.User{UserName: "alice", UserID: "AIDA11111111111111111"}); err != nil {
|
||||
t.Fatalf("CreateUser: %v", err)
|
||||
}
|
||||
if _, err := store.CreateUser(ctx, types.User{UserName: "ALICE", UserID: "AIDA22222222222222222"}); !errors.Is(err, iamerr.EntityAlreadyExistsUser("ALICE")) {
|
||||
t.Fatalf("CreateUser case-variant duplicate err = %v, want EntityAlreadyExists", err)
|
||||
}
|
||||
|
||||
got, err := store.GetUser(ctx, "ALICE")
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser case-insensitive lookup: %v", err)
|
||||
}
|
||||
if got.UserName != "alice" {
|
||||
t.Fatalf("GetUser case-insensitive lookup = %#v, want canonical casing preserved", got)
|
||||
}
|
||||
|
||||
if err := store.DeleteUser(ctx, "ALICE"); err != nil {
|
||||
t.Fatalf("DeleteUser case-insensitive lookup: %v", err)
|
||||
}
|
||||
if _, err := store.GetUser(ctx, "alice"); !errors.Is(err, iamerr.NoSuchEntityUser("alice")) {
|
||||
t.Fatalf("GetUser after case-insensitive delete err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalStoreRoleCRUDAndPagination(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
dir := t.TempDir()
|
||||
store, err := NewInternal(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("NewInternal: %v", err)
|
||||
}
|
||||
|
||||
created := time.Date(2026, 7, 11, 18, 0, 0, 0, time.UTC)
|
||||
roles := []types.Role{
|
||||
{
|
||||
Path: "/engineering/",
|
||||
RoleName: "alice-role",
|
||||
RoleID: "AROA22222222222222222",
|
||||
Arn: "arn:aws:iam::000000000000:role/engineering/alice-role",
|
||||
CreateDate: created,
|
||||
AssumeRolePolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`,
|
||||
MaxSessionDuration: 3600,
|
||||
Tags: []types.Tag{
|
||||
{Key: "env", Value: "test"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Path: "/engineering/platform/",
|
||||
RoleName: "bob-role",
|
||||
RoleID: "AROA33333333333333333",
|
||||
Arn: "arn:aws:iam::000000000000:role/engineering/platform/bob-role",
|
||||
CreateDate: created.Add(time.Second),
|
||||
AssumeRolePolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`,
|
||||
MaxSessionDuration: 3600,
|
||||
},
|
||||
{
|
||||
Path: "/ops/",
|
||||
RoleName: "carol-role",
|
||||
RoleID: "AROA44444444444444444",
|
||||
Arn: "arn:aws:iam::000000000000:role/ops/carol-role",
|
||||
CreateDate: created.Add(2 * time.Second),
|
||||
AssumeRolePolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"AWS":"*"},"Action":"sts:AssumeRole"}]}`,
|
||||
MaxSessionDuration: 3600,
|
||||
},
|
||||
}
|
||||
for _, role := range roles {
|
||||
created, err := store.CreateRole(ctx, role)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRole(%s): %v", role.RoleName, err)
|
||||
}
|
||||
if created.RoleLastUsed == nil {
|
||||
t.Fatalf("CreateRole(%s) RoleLastUsed = nil, want non-nil empty element", role.RoleName)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := store.CreateRole(ctx, roles[0]); !errors.Is(err, iamerr.EntityAlreadyExistsRole("alice-role")) {
|
||||
t.Fatalf("CreateRole duplicate err = %v, want EntityAlreadyExists", err)
|
||||
}
|
||||
if _, err := store.CreateRole(ctx, types.Role{RoleName: "ALICE-ROLE", RoleID: "AROA55555555555555555"}); !errors.Is(err, iamerr.EntityAlreadyExistsRole("ALICE-ROLE")) {
|
||||
t.Fatalf("CreateRole case-variant duplicate err = %v, want EntityAlreadyExists", err)
|
||||
}
|
||||
duplicateID := roles[2]
|
||||
duplicateID.RoleName = "dave-role"
|
||||
if _, err := store.CreateRole(ctx, duplicateID); !errors.Is(err, ErrRoleIDAlreadyExists) {
|
||||
t.Fatalf("CreateRole duplicate id err = %v, want ErrRoleIDAlreadyExists", err)
|
||||
}
|
||||
|
||||
got, err := store.GetRole(ctx, "ALICE-ROLE")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRole: %v", err)
|
||||
}
|
||||
if got.RoleName != "alice-role" || got.RoleID != roles[0].RoleID {
|
||||
t.Fatalf("GetRole = %#v, want alice-role with stable id and preserved casing", got)
|
||||
}
|
||||
if !reflect.DeepEqual(got.Tags, roles[0].Tags) {
|
||||
t.Fatalf("GetRole tags = %#v, want %#v", got.Tags, roles[0].Tags)
|
||||
}
|
||||
if got.RoleLastUsed == nil {
|
||||
t.Fatal("GetRole RoleLastUsed = nil, want non-nil empty element")
|
||||
}
|
||||
|
||||
page1, err := store.ListRoles(ctx, ListRolesInput{PathPrefix: "/engineering/", MaxItems: 1})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoles page1: %v", err)
|
||||
}
|
||||
if len(page1.Roles) != 1 || page1.Roles[0].RoleName != "alice-role" || !page1.IsTruncated || page1.Marker != "alice-role" {
|
||||
t.Fatalf("page1 = %#v, want truncated alice-role page", page1)
|
||||
}
|
||||
if page1.Roles[0].RoleLastUsed != nil {
|
||||
t.Fatalf("ListRoles RoleLastUsed = %#v, want nil (list/get asymmetry)", page1.Roles[0].RoleLastUsed)
|
||||
}
|
||||
|
||||
page2, err := store.ListRoles(ctx, ListRolesInput{PathPrefix: "/engineering/", Marker: page1.Marker, MaxItems: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoles page2: %v", err)
|
||||
}
|
||||
if len(page2.Roles) != 1 || page2.Roles[0].RoleName != "bob-role" || page2.IsTruncated {
|
||||
t.Fatalf("page2 = %#v, want final bob-role page", page2)
|
||||
}
|
||||
|
||||
updatedRole, err := store.UpdateAssumeRolePolicy(ctx, UpdateAssumeRolePolicyInput{
|
||||
RoleName: "alice-role",
|
||||
PolicyDocument: `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sts.amazonaws.com"},"Action":"sts:AssumeRole"}]}`,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateAssumeRolePolicy: %v", err)
|
||||
}
|
||||
if updatedRole.AssumeRolePolicyDocument != `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sts.amazonaws.com"},"Action":"sts:AssumeRole"}]}` {
|
||||
t.Fatalf("UpdateAssumeRolePolicy result = %#v", updatedRole)
|
||||
}
|
||||
if updatedRole.RoleID != roles[0].RoleID {
|
||||
t.Fatalf("UpdateAssumeRolePolicy identity changed: %#v", updatedRole)
|
||||
}
|
||||
if _, err := store.UpdateAssumeRolePolicy(ctx, UpdateAssumeRolePolicyInput{RoleName: "missing-role", PolicyDocument: "{}"}); !errors.Is(err, iamerr.NoSuchEntityRole("missing-role")) {
|
||||
t.Fatalf("UpdateAssumeRolePolicy missing role err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
|
||||
reopened, err := NewInternal(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("reopen NewInternal: %v", err)
|
||||
}
|
||||
reopenedRole, err := reopened.GetRole(ctx, "alice-role")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRole after reopen: %v", err)
|
||||
}
|
||||
if reopenedRole.AssumeRolePolicyDocument != updatedRole.AssumeRolePolicyDocument {
|
||||
t.Fatalf("reopened AssumeRolePolicyDocument = %q, want %q", reopenedRole.AssumeRolePolicyDocument, updatedRole.AssumeRolePolicyDocument)
|
||||
}
|
||||
|
||||
if err := reopened.DeleteRole(ctx, "carol-role"); err != nil {
|
||||
t.Fatalf("DeleteRole: %v", err)
|
||||
}
|
||||
if err := reopened.DeleteRole(ctx, "carol-role"); !errors.Is(err, iamerr.NoSuchEntityRole("carol-role")) {
|
||||
t.Fatalf("DeleteRole missing err = %v, want NoSuchEntity", err)
|
||||
}
|
||||
}
|
||||
|
||||
+320
-6
@@ -191,7 +191,45 @@ func (s *VaultStore) reAuthIfNeeded(err error) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// findUserKey resolves name to the exact stored KV path segment (the
|
||||
// original UserName casing used at creation), case-insensitively, by
|
||||
// listing the users under secretStoragePath and comparing with EqualFold.
|
||||
// AWS enforces case-insensitive UserName uniqueness but Vault's KV paths
|
||||
// are plain case-sensitive strings, so a list+compare fallback is needed —
|
||||
// KV has no native case-insensitive lookup. ok is false both when nothing
|
||||
// matches and (harmlessly) when the prefix has no children at all.
|
||||
func (s *VaultStore) findUserKey(name string) (string, bool, error) {
|
||||
resp, err := s.client.Secrets.KvV2List(context.Background(), s.secretStoragePath, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return "", false, nil
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return "", false, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2List(context.Background(), s.secretStoragePath, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
}
|
||||
for _, key := range resp.Data.Keys {
|
||||
if strings.EqualFold(key, name) {
|
||||
return key, true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) CreateUser(_ context.Context, user types.User) (*types.User, error) {
|
||||
if _, ok, err := s.findUserKey(user.UserName); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return nil, iamerr.EntityAlreadyExistsUser(user.UserName)
|
||||
}
|
||||
|
||||
userMap, err := userToVaultMap(user)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serialize user: %w", err)
|
||||
@@ -239,11 +277,19 @@ func (s *VaultStore) DeleteUser(ctx context.Context, username string) error {
|
||||
if len(user.AccessKeys) > 0 {
|
||||
return iamerr.GetAPIError(iamerr.ErrDeleteConflict)
|
||||
}
|
||||
return s.deleteByPath(username)
|
||||
return s.deleteByPath(user.UserName)
|
||||
}
|
||||
|
||||
func (s *VaultStore) GetUser(_ context.Context, username string) (*types.User, error) {
|
||||
path := s.secretStoragePath + "/" + username
|
||||
canonical, ok, err := s.findUserKey(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(username)
|
||||
}
|
||||
|
||||
path := s.secretStoragePath + "/" + canonical
|
||||
resp, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
@@ -261,7 +307,7 @@ func (s *VaultStore) GetUser(_ context.Context, username string) (*types.User, e
|
||||
}
|
||||
}
|
||||
|
||||
user, err := parseVaultUser(resp.Data.Data, username)
|
||||
user, err := parseVaultUser(resp.Data.Data, canonical)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -340,13 +386,14 @@ func (s *VaultStore) UpdateUser(ctx context.Context, input UpdateUserInput) (*ty
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
originalName := user.UserName
|
||||
|
||||
finalName := user.UserName
|
||||
if input.NewUserName != "" {
|
||||
finalName = input.NewUserName
|
||||
}
|
||||
|
||||
if finalName != input.UserName {
|
||||
if !strings.EqualFold(finalName, originalName) {
|
||||
existing, err := s.GetUser(ctx, finalName)
|
||||
if err != nil && !errors.Is(err, iamerr.NoSuchEntityUser(finalName)) {
|
||||
return nil, err
|
||||
@@ -366,12 +413,12 @@ func (s *VaultStore) UpdateUser(ctx context.Context, input UpdateUserInput) (*ty
|
||||
user.Arn = input.NewArn
|
||||
}
|
||||
|
||||
if user.UserName != input.UserName {
|
||||
if user.UserName != originalName {
|
||||
// Create at new path first to detect conflicts before deleting the old entry.
|
||||
if _, err := s.CreateUser(ctx, *user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.deleteByPath(input.UserName); err != nil {
|
||||
if err := s.deleteByPath(originalName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if _, err := s.replaceUser(ctx, *user); err != nil {
|
||||
@@ -689,6 +736,273 @@ func (s *VaultStore) deleteByPath(username string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// rolesPath is the KV prefix under which roles are stored, kept distinct
|
||||
// from secretStoragePath (which holds users) so listing one entity kind
|
||||
// never has to filter out the other's keys.
|
||||
func (s *VaultStore) rolesPath() string {
|
||||
return s.secretStoragePath + "/roles"
|
||||
}
|
||||
|
||||
// findRoleKey is findUserKey's counterpart for roles.
|
||||
func (s *VaultStore) findRoleKey(name string) (string, bool, error) {
|
||||
resp, err := s.client.Secrets.KvV2List(context.Background(), s.rolesPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return "", false, nil
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return "", false, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2List(context.Background(), s.rolesPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return "", false, nil
|
||||
}
|
||||
return "", false, err
|
||||
}
|
||||
}
|
||||
for _, key := range resp.Data.Keys {
|
||||
if strings.EqualFold(key, name) {
|
||||
return key, true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) CreateRole(_ context.Context, role types.Role) (*types.Role, error) {
|
||||
if _, ok, err := s.findRoleKey(role.RoleName); err != nil {
|
||||
return nil, err
|
||||
} else if ok {
|
||||
return nil, iamerr.EntityAlreadyExistsRole(role.RoleName)
|
||||
}
|
||||
|
||||
role.EnsureRoleLastUsed()
|
||||
|
||||
roleMap, err := roleToVaultMap(role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("serialize role: %w", err)
|
||||
}
|
||||
|
||||
path := s.rolesPath() + "/" + role.RoleName
|
||||
req := schema.KvV2WriteRequest{
|
||||
Data: map[string]any{role.RoleName: roleMap},
|
||||
Options: map[string]any{
|
||||
"cas": 0,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = s.client.Secrets.KvV2Write(context.Background(), path, req, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "check-and-set") {
|
||||
return nil, iamerr.EntityAlreadyExistsRole(role.RoleName)
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
// retry once after re-auth
|
||||
_, err = s.client.Secrets.KvV2Write(context.Background(), path, req, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "check-and-set") {
|
||||
return nil, iamerr.EntityAlreadyExistsRole(role.RoleName)
|
||||
}
|
||||
if vault.IsErrorStatus(err, http.StatusForbidden) {
|
||||
return nil, fmt.Errorf("vault 403 permission denied on path %q. check KV mount path and policy. original: %w", path, err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return cloneRole(role), nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) GetRole(_ context.Context, roleName string) (*types.Role, error) {
|
||||
canonical, ok, err := s.findRoleKey(roleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
|
||||
path := s.rolesPath() + "/" + canonical
|
||||
resp, err := s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return nil, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2Read(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
role, err := parseVaultRole(resp.Data.Data, canonical)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cloneRole(role), nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) ListRoles(ctx context.Context, input ListRolesInput) (*ListRolesOutput, error) {
|
||||
resp, err := s.client.Secrets.KvV2List(context.Background(), s.rolesPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return &ListRolesOutput{Roles: []types.Role{}}, nil
|
||||
}
|
||||
reauthErr := s.reAuthIfNeeded(err)
|
||||
if reauthErr != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return &ListRolesOutput{Roles: []types.Role{}}, nil
|
||||
}
|
||||
return nil, reauthErr
|
||||
}
|
||||
resp, err = s.client.Secrets.KvV2List(context.Background(), s.rolesPath(), s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if vault.IsErrorStatus(err, http.StatusNotFound) {
|
||||
return &ListRolesOutput{Roles: []types.Role{}}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
roles := make([]types.Role, 0, len(resp.Data.Keys))
|
||||
for _, key := range resp.Data.Keys {
|
||||
role, err := s.GetRole(ctx, key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if input.PathPrefix != "" && !strings.HasPrefix(role.Path, input.PathPrefix) {
|
||||
continue
|
||||
}
|
||||
// ListRoles entries omit RoleLastUsed even though GetRole (reused
|
||||
// above to fetch each entry) attaches it — matches the documented
|
||||
// list/get field asymmetry.
|
||||
role.RoleLastUsed = nil
|
||||
roles = append(roles, *role)
|
||||
}
|
||||
|
||||
sort.Slice(roles, func(i, j int) bool {
|
||||
return roles[i].RoleName < roles[j].RoleName
|
||||
})
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(roles)
|
||||
for i, role := range roles {
|
||||
if role.RoleName == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
roles = roles[start:]
|
||||
|
||||
limit := len(roles)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListRolesOutput{
|
||||
Roles: make([]types.Role, limit),
|
||||
}
|
||||
copy(out.Roles, roles[:limit])
|
||||
if limit < len(roles) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.Roles[limit-1].RoleName
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) DeleteRole(ctx context.Context, roleName string) error {
|
||||
role, err := s.GetRole(ctx, roleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(role.Policies.Inline) > 0 {
|
||||
return iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies)
|
||||
}
|
||||
return s.deleteRoleByPath(role.RoleName)
|
||||
}
|
||||
|
||||
func (s *VaultStore) UpdateAssumeRolePolicy(ctx context.Context, input UpdateAssumeRolePolicyInput) (*types.Role, error) {
|
||||
role, err := s.GetRole(ctx, input.RoleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
role.AssumeRolePolicyDocument = input.PolicyDocument
|
||||
|
||||
return s.replaceRole(ctx, *role)
|
||||
}
|
||||
|
||||
// replaceRole overwrites the stored document for role.RoleName by deleting
|
||||
// all existing versions and recreating with CAS=0.
|
||||
func (s *VaultStore) replaceRole(ctx context.Context, role types.Role) (*types.Role, error) {
|
||||
if err := s.deleteRoleByPath(role.RoleName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.CreateRole(ctx, role)
|
||||
}
|
||||
|
||||
// deleteRoleByPath permanently removes a role secret and all its versions
|
||||
// without checking for existence first.
|
||||
func (s *VaultStore) deleteRoleByPath(roleName string) error {
|
||||
path := s.rolesPath() + "/" + roleName
|
||||
_, err := s.client.Secrets.KvV2DeleteMetadataAndAllVersions(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
if reauthErr := s.reAuthIfNeeded(err); reauthErr != nil {
|
||||
return reauthErr
|
||||
}
|
||||
_, err = s.client.Secrets.KvV2DeleteMetadataAndAllVersions(context.Background(), path, s.kvReqOpts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errInvalidVaultRole = errors.New("invalid role entry in vault secrets engine")
|
||||
|
||||
// roleToVaultMap is userToVaultMap's counterpart for roles.
|
||||
func roleToVaultMap(role types.Role) (map[string]any, error) {
|
||||
b, err := json.Marshal(role)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// parseVaultRole reconstructs a Role from the raw map[string]any that vault
|
||||
// returns. The outer key is the role name.
|
||||
func parseVaultRole(data map[string]any, roleName string) (types.Role, error) {
|
||||
raw, ok := data[roleName]
|
||||
if !ok {
|
||||
return types.Role{}, errInvalidVaultRole
|
||||
}
|
||||
roleMap, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return types.Role{}, errInvalidVaultRole
|
||||
}
|
||||
b, err := json.Marshal(roleMap)
|
||||
if err != nil {
|
||||
return types.Role{}, fmt.Errorf("re-marshal vault role: %w", err)
|
||||
}
|
||||
var role types.Role
|
||||
if err := json.Unmarshal(b, &role); err != nil {
|
||||
return types.Role{}, fmt.Errorf("unmarshal vault role: %w", err)
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
|
||||
var errInvalidVaultUser = errors.New("invalid user entry in vault secrets engine")
|
||||
|
||||
// userToVaultMap round-trips User through JSON to produce a map[string]any
|
||||
|
||||
Reference in New Issue
Block a user