mirror of
https://github.com/versity/versitygw.git
synced 2026-09-26 09:54:49 +00:00
feat: add IAM user inline policy CRUD
Add support for AWS-compatible inline identity-based policies on IAM users, implementing the `PutUserPolicy`, `GetUserPolicy`, `DeleteUserPolicy`, and `ListUserPolicies` actions on both the internal and Vault storage backends. - iamapi/policy is a new package that parses and validates policy documents against IAM's parameter-level constraints (max length, allowed charset) and policy grammar (Version, Effect, mutually exclusive Action/NotAction and Resource/NotResource, vendor-prefixed actions, ARN-shaped resources, no Principal/NotPrincipal, unique Sids). - `PutUserPolicy` creates or replaces a named inline policy on a user, enforcing a 2048-byte aggregate quota across all of a user's inline policies (MaxInlinePolicyBytesPerUser), matching the AWS IAM quota. - `GetUserPolicy` returns a policy's document RFC 3986 percent-encoded, matching how real IAM encodes the PolicyDocument response element. - `DeleteUserPolicy` removes a named inline policy from a user. - `ListUserPolicies` returns a paginated, sorted list of a user's inline policy names, honoring Marker/MaxItems like the other IAM list APIs. - `DeleteUser` is now rejected with a DeleteConflict error if the user still has inline policies attached, mirroring the existing access-key delete-conflict behavior.
This commit is contained in:
@@ -21,6 +21,7 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/versity/versitygw/iamapi/iamerr"
|
||||
"github.com/versity/versitygw/iamapi/types"
|
||||
@@ -113,6 +114,9 @@ func (s *InternalStore) DeleteUser(_ context.Context, username string) error {
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(username)
|
||||
}
|
||||
if len(user.Policies.Inline) > 0 {
|
||||
return nil, iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies)
|
||||
}
|
||||
if len(user.AccessKeys) > 0 {
|
||||
return nil, iamerr.GetAPIError(iamerr.ErrDeleteConflict)
|
||||
}
|
||||
@@ -445,9 +449,163 @@ func (s *InternalStore) ListAccessKeys(_ context.Context, input ListAccessKeysIn
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *InternalStore) PutUserPolicy(_ context.Context, input PutUserPolicyInput) 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
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
newTotal := len(input.PolicyDocument)
|
||||
replaceAt := -1
|
||||
for i, p := range user.Policies.Inline {
|
||||
if p.PolicyName == input.PolicyName {
|
||||
replaceAt = i
|
||||
continue
|
||||
}
|
||||
newTotal += len(p.PolicyDocument)
|
||||
}
|
||||
if newTotal > MaxInlinePolicyBytesPerUser {
|
||||
return nil, iamerr.InlinePolicyQuotaExceeded("user", input.UserName, MaxInlinePolicyBytesPerUser)
|
||||
}
|
||||
|
||||
if replaceAt >= 0 {
|
||||
user.Policies.Inline[replaceAt].PolicyDocument = input.PolicyDocument
|
||||
user.Policies.Inline[replaceAt].UpdateDate = now
|
||||
} else {
|
||||
user.Policies.Inline = append(user.Policies.Inline, types.PolicyEntry{
|
||||
PolicyName: input.PolicyName,
|
||||
PolicyDocument: input.PolicyDocument,
|
||||
CreateDate: now,
|
||||
UpdateDate: now,
|
||||
})
|
||||
}
|
||||
|
||||
conf.Users[input.UserName] = user
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) GetUserPolicy(_ context.Context, userName, policyName string) (*types.PolicyEntry, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[userName]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(userName)
|
||||
}
|
||||
|
||||
for _, p := range user.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
cloned := p
|
||||
return &cloned, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, iamerr.NoSuchEntityUserPolicy(userName, policyName)
|
||||
}
|
||||
|
||||
func (s *InternalStore) DeleteUserPolicy(_ context.Context, userName, policyName 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
|
||||
}
|
||||
|
||||
user, ok := conf.Users[userName]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(userName)
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, p := range user.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return nil, iamerr.NoSuchEntityUserPolicy(userName, policyName)
|
||||
}
|
||||
|
||||
user.Policies.Inline = slices.Delete(user.Policies.Inline, idx, idx+1)
|
||||
conf.Users[userName] = user
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) ListUserPolicies(_ context.Context, input ListUserPoliciesInput) (*ListUserPoliciesOutput, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, ok := conf.Users[input.UserName]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityUser(input.UserName)
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(user.Policies.Inline))
|
||||
for _, p := range user.Policies.Inline {
|
||||
names = append(names, p.PolicyName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(names)
|
||||
for i, name := range names {
|
||||
if name == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
names = names[start:]
|
||||
|
||||
limit := len(names)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListUserPoliciesOutput{
|
||||
PolicyNames: make([]string, limit),
|
||||
}
|
||||
copy(out.PolicyNames, names[:limit])
|
||||
if limit < len(names) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.PolicyNames[limit-1]
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func cloneUser(user types.User) *types.User {
|
||||
cloned := user
|
||||
cloned.Tags = slices.Clone(user.Tags)
|
||||
cloned.AccessKeys = slices.Clone(user.AccessKeys)
|
||||
cloned.Policies.Inline = slices.Clone(user.Policies.Inline)
|
||||
return &cloned
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ import (
|
||||
// user may hold at once, matching the AWS IAM quota.
|
||||
const MaxAccessKeysPerUser = 2
|
||||
|
||||
// MaxInlinePolicyBytesPerUser is the maximum aggregate size, in bytes, of
|
||||
// all of a single IAM user's inline policy documents combined
|
||||
const MaxInlinePolicyBytesPerUser = 2048
|
||||
|
||||
var (
|
||||
ErrUserIDAlreadyExists = errors.New("iamapi: user id already exists")
|
||||
ErrAccessKeyIDAlreadyExists = errors.New("iamapi: access key id already exists")
|
||||
@@ -86,6 +90,24 @@ type GetAccessKeyLastUsedOutput struct {
|
||||
Region string
|
||||
}
|
||||
|
||||
type PutUserPolicyInput struct {
|
||||
UserName string
|
||||
PolicyName string
|
||||
PolicyDocument string
|
||||
}
|
||||
|
||||
type ListUserPoliciesInput struct {
|
||||
UserName string
|
||||
Marker string
|
||||
MaxItems int32
|
||||
}
|
||||
|
||||
type ListUserPoliciesOutput struct {
|
||||
PolicyNames []string
|
||||
IsTruncated bool
|
||||
Marker string
|
||||
}
|
||||
|
||||
// Storer is the IAM API storage backend contract.
|
||||
type Storer interface {
|
||||
CreateUser(ctx context.Context, user types.User) (*types.User, error)
|
||||
@@ -99,6 +121,11 @@ type Storer interface {
|
||||
DeleteAccessKey(ctx context.Context, username, accessKeyID string) error
|
||||
GetAccessKeyLastUsed(ctx context.Context, accessKeyID string) (*GetAccessKeyLastUsedOutput, error)
|
||||
ListAccessKeys(ctx context.Context, input ListAccessKeysInput) (*ListAccessKeysOutput, error)
|
||||
|
||||
PutUserPolicy(ctx context.Context, input PutUserPolicyInput) error
|
||||
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)
|
||||
}
|
||||
|
||||
func unwrapAPIError(err error) error {
|
||||
|
||||
@@ -233,6 +233,9 @@ func (s *VaultStore) DeleteUser(ctx context.Context, username string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(user.Policies.Inline) > 0 {
|
||||
return iamerr.GetAPIError(iamerr.ErrDeleteConflictPolicies)
|
||||
}
|
||||
if len(user.AccessKeys) > 0 {
|
||||
return iamerr.GetAPIError(iamerr.ErrDeleteConflict)
|
||||
}
|
||||
@@ -553,6 +556,122 @@ func (s *VaultStore) ListAccessKeys(ctx context.Context, input ListAccessKeysInp
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *VaultStore) PutUserPolicy(ctx context.Context, input PutUserPolicyInput) error {
|
||||
user, err := s.GetUser(ctx, input.UserName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
newTotal := len(input.PolicyDocument)
|
||||
replaceAt := -1
|
||||
for i, p := range user.Policies.Inline {
|
||||
if p.PolicyName == input.PolicyName {
|
||||
replaceAt = i
|
||||
continue
|
||||
}
|
||||
newTotal += len(p.PolicyDocument)
|
||||
}
|
||||
if newTotal > MaxInlinePolicyBytesPerUser {
|
||||
return iamerr.InlinePolicyQuotaExceeded("user", input.UserName, MaxInlinePolicyBytesPerUser)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Truncate(time.Second)
|
||||
if replaceAt >= 0 {
|
||||
user.Policies.Inline[replaceAt].PolicyDocument = input.PolicyDocument
|
||||
user.Policies.Inline[replaceAt].UpdateDate = now
|
||||
} else {
|
||||
user.Policies.Inline = append(user.Policies.Inline, types.PolicyEntry{
|
||||
PolicyName: input.PolicyName,
|
||||
PolicyDocument: input.PolicyDocument,
|
||||
CreateDate: now,
|
||||
UpdateDate: now,
|
||||
})
|
||||
}
|
||||
|
||||
_, err = s.replaceUser(ctx, *user)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *VaultStore) GetUserPolicy(ctx context.Context, userName, policyName string) (*types.PolicyEntry, error) {
|
||||
user, err := s.GetUser(ctx, userName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, p := range user.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
cloned := p
|
||||
return &cloned, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, iamerr.NoSuchEntityUserPolicy(userName, policyName)
|
||||
}
|
||||
|
||||
func (s *VaultStore) DeleteUserPolicy(ctx context.Context, userName, policyName string) error {
|
||||
user, err := s.GetUser(ctx, userName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
idx := -1
|
||||
for i, p := range user.Policies.Inline {
|
||||
if p.PolicyName == policyName {
|
||||
idx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if idx == -1 {
|
||||
return iamerr.NoSuchEntityUserPolicy(userName, policyName)
|
||||
}
|
||||
|
||||
user.Policies.Inline = slices.Delete(user.Policies.Inline, idx, idx+1)
|
||||
|
||||
_, err = s.replaceUser(ctx, *user)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *VaultStore) ListUserPolicies(ctx context.Context, input ListUserPoliciesInput) (*ListUserPoliciesOutput, error) {
|
||||
user, err := s.GetUser(ctx, input.UserName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(user.Policies.Inline))
|
||||
for _, p := range user.Policies.Inline {
|
||||
names = append(names, p.PolicyName)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
start := 0
|
||||
if input.Marker != "" {
|
||||
start = len(names)
|
||||
for i, name := range names {
|
||||
if name == input.Marker {
|
||||
start = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
names = names[start:]
|
||||
|
||||
limit := len(names)
|
||||
if input.MaxItems > 0 && int(input.MaxItems) < limit {
|
||||
limit = int(input.MaxItems)
|
||||
}
|
||||
|
||||
out := &ListUserPoliciesOutput{
|
||||
PolicyNames: make([]string, limit),
|
||||
}
|
||||
copy(out.PolicyNames, names[:limit])
|
||||
if limit < len(names) {
|
||||
out.IsTruncated = true
|
||||
out.Marker = out.PolicyNames[limit-1]
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// deleteByPath permanently removes a secret and all its versions without
|
||||
// checking for existence first.
|
||||
func (s *VaultStore) deleteByPath(username string) error {
|
||||
|
||||
Reference in New Issue
Block a user