s3api: add AttachUserPolicy/DetachUserPolicy/ListAttachedUserPolicies (#8379)
* iam: add XML responses for managed user policy APIs * s3api: implement attach/detach/list attached user policies * s3api: add embedded IAM tests for managed user policies * iam: update CredentialStore interface and Manager for managed policies Updated the `CredentialStore` interface to include `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` methods. The `CredentialManager` was updated to delegate these calls to the store. Added common error variables for policy management. * iam: implement managed policy methods in MemoryStore Implemented `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` in the MemoryStore. Also ensured deep copying of identities includes PolicyNames. * iam: implement managed policy methods in PostgresStore Modified Postgres schema to include `policy_names` JSONB column in `users`. Implemented `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies`. Updated user CRUD operations to handle policy names persistence. * iam: implement managed policy methods in remaining stores Implemented user policy management in: - `FilerEtcStore` (partial implementation) - `IamGrpcStore` (delegated via GetUser/UpdateUser) - `PropagatingCredentialStore` (to broadcast updates) Ensures cluster-wide consistency for policy attachments. * s3api: refactor EmbeddedIamApi to use managed policy APIs - Refactored `AttachUserPolicy`, `DetachUserPolicy`, and `ListAttachedUserPolicies` to use `e.credentialManager` directly. - Fixed a critical error suppression bug in `ExecuteAction` that always returned success even on failure. - Implemented robust error matching using string comparison fallbacks. - Improved consistency by reloading configuration after policy changes. * s3api: update and refine IAM integration tests - Updated tests to use a real `MemoryStore`-backed `CredentialManager`. - Refined test configuration synchronization using `sync.Once` and manual deep-copying to prevent state corruption. - Improved `extractEmbeddedIamErrorCodeAndMessage` to handle more XML formats robustly. - Adjusted test expectations to match current AWS IAM behavior. * fix compilation * visibility * ensure 10 policies * reload * add integration tests * Guard raft command registration * Allow IAM actions in policy tests * Validate gRPC policy attachments * Revert Validate gRPC policy attachments * Tighten gRPC policy attach/detach * Improve IAM managed policy handling * Improve managed policy filters
This commit is contained in:
@@ -20,7 +20,7 @@ type FilerAddressSetter interface {
|
||||
|
||||
// CredentialManager manages user credentials using a configurable store
|
||||
type CredentialManager struct {
|
||||
store CredentialStore
|
||||
Store CredentialStore
|
||||
}
|
||||
|
||||
// NewCredentialManager creates a new credential manager with the specified store
|
||||
@@ -46,128 +46,128 @@ func NewCredentialManager(storeName CredentialStoreTypeName, configuration util.
|
||||
}
|
||||
|
||||
return &CredentialManager{
|
||||
store: store,
|
||||
Store: store,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (cm *CredentialManager) SetMasterClient(masterClient *wdclient.MasterClient, grpcDialOption grpc.DialOption) {
|
||||
cm.store = NewPropagatingCredentialStore(cm.store, masterClient, grpcDialOption)
|
||||
cm.Store = NewPropagatingCredentialStore(cm.Store, masterClient, grpcDialOption)
|
||||
}
|
||||
|
||||
// SetFilerAddressFunc sets the function to get the current filer address
|
||||
func (cm *CredentialManager) SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption) {
|
||||
if s, ok := cm.store.(FilerAddressSetter); ok {
|
||||
if s, ok := cm.Store.(FilerAddressSetter); ok {
|
||||
s.SetFilerAddressFunc(getFiler, grpcDialOption)
|
||||
}
|
||||
}
|
||||
|
||||
// GetStore returns the underlying credential store
|
||||
func (cm *CredentialManager) GetStore() CredentialStore {
|
||||
return cm.store
|
||||
return cm.Store
|
||||
}
|
||||
|
||||
// GetStoreName returns the name of the underlying credential store
|
||||
func (cm *CredentialManager) GetStoreName() string {
|
||||
if cm.store != nil {
|
||||
return string(cm.store.GetName())
|
||||
if cm.Store != nil {
|
||||
return string(cm.Store.GetName())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the S3 API configuration
|
||||
func (cm *CredentialManager) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
|
||||
return cm.store.LoadConfiguration(ctx)
|
||||
return cm.Store.LoadConfiguration(ctx)
|
||||
}
|
||||
|
||||
// SaveConfiguration saves the S3 API configuration
|
||||
func (cm *CredentialManager) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
|
||||
return cm.store.SaveConfiguration(ctx, config)
|
||||
return cm.Store.SaveConfiguration(ctx, config)
|
||||
}
|
||||
|
||||
// CreateUser creates a new user
|
||||
func (cm *CredentialManager) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
|
||||
return cm.store.CreateUser(ctx, identity)
|
||||
return cm.Store.CreateUser(ctx, identity)
|
||||
}
|
||||
|
||||
// GetUser retrieves a user by username
|
||||
func (cm *CredentialManager) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) {
|
||||
return cm.store.GetUser(ctx, username)
|
||||
return cm.Store.GetUser(ctx, username)
|
||||
}
|
||||
|
||||
// UpdateUser updates an existing user
|
||||
func (cm *CredentialManager) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
|
||||
return cm.store.UpdateUser(ctx, username, identity)
|
||||
return cm.Store.UpdateUser(ctx, username, identity)
|
||||
}
|
||||
|
||||
// DeleteUser removes a user
|
||||
func (cm *CredentialManager) DeleteUser(ctx context.Context, username string) error {
|
||||
return cm.store.DeleteUser(ctx, username)
|
||||
return cm.Store.DeleteUser(ctx, username)
|
||||
}
|
||||
|
||||
// ListUsers returns all usernames
|
||||
func (cm *CredentialManager) ListUsers(ctx context.Context) ([]string, error) {
|
||||
return cm.store.ListUsers(ctx)
|
||||
return cm.Store.ListUsers(ctx)
|
||||
}
|
||||
|
||||
// GetUserByAccessKey retrieves a user by access key
|
||||
func (cm *CredentialManager) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
|
||||
return cm.store.GetUserByAccessKey(ctx, accessKey)
|
||||
return cm.Store.GetUserByAccessKey(ctx, accessKey)
|
||||
}
|
||||
|
||||
// CreateAccessKey creates a new access key for a user
|
||||
func (cm *CredentialManager) CreateAccessKey(ctx context.Context, username string, credential *iam_pb.Credential) error {
|
||||
return cm.store.CreateAccessKey(ctx, username, credential)
|
||||
return cm.Store.CreateAccessKey(ctx, username, credential)
|
||||
}
|
||||
|
||||
// DeleteAccessKey removes an access key for a user
|
||||
func (cm *CredentialManager) DeleteAccessKey(ctx context.Context, username string, accessKey string) error {
|
||||
return cm.store.DeleteAccessKey(ctx, username, accessKey)
|
||||
return cm.Store.DeleteAccessKey(ctx, username, accessKey)
|
||||
}
|
||||
|
||||
// GetPolicies returns all policies
|
||||
func (cm *CredentialManager) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
|
||||
return cm.store.GetPolicies(ctx)
|
||||
return cm.Store.GetPolicies(ctx)
|
||||
}
|
||||
|
||||
// PutPolicy creates or updates a policy
|
||||
func (cm *CredentialManager) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||
return cm.store.PutPolicy(ctx, name, document)
|
||||
return cm.Store.PutPolicy(ctx, name, document)
|
||||
}
|
||||
|
||||
// DeletePolicy removes a policy
|
||||
func (cm *CredentialManager) DeletePolicy(ctx context.Context, name string) error {
|
||||
return cm.store.DeletePolicy(ctx, name)
|
||||
return cm.Store.DeletePolicy(ctx, name)
|
||||
}
|
||||
|
||||
// GetPolicy retrieves a policy by name
|
||||
func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
|
||||
return cm.store.GetPolicy(ctx, name)
|
||||
return cm.Store.GetPolicy(ctx, name)
|
||||
}
|
||||
|
||||
// CreatePolicy creates a new policy (if supported by the store)
|
||||
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||
// Check if the store implements PolicyManager interface with CreatePolicy
|
||||
if policyStore, ok := cm.store.(PolicyManager); ok {
|
||||
if policyStore, ok := cm.Store.(PolicyManager); ok {
|
||||
return policyStore.CreatePolicy(ctx, name, document)
|
||||
}
|
||||
// Fallback to PutPolicy for stores that only implement CredentialStore
|
||||
return cm.store.PutPolicy(ctx, name, document)
|
||||
return cm.Store.PutPolicy(ctx, name, document)
|
||||
}
|
||||
|
||||
// UpdatePolicy updates an existing policy (if supported by the store)
|
||||
func (cm *CredentialManager) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||
// Check if the store implements PolicyManager interface with UpdatePolicy
|
||||
if policyStore, ok := cm.store.(PolicyManager); ok {
|
||||
if policyStore, ok := cm.Store.(PolicyManager); ok {
|
||||
return policyStore.UpdatePolicy(ctx, name, document)
|
||||
}
|
||||
// Fallback to PutPolicy for stores that only implement CredentialStore
|
||||
return cm.store.PutPolicy(ctx, name, document)
|
||||
return cm.Store.PutPolicy(ctx, name, document)
|
||||
}
|
||||
|
||||
// Shutdown performs cleanup
|
||||
func (cm *CredentialManager) Shutdown() {
|
||||
if cm.store != nil {
|
||||
cm.store.Shutdown()
|
||||
if cm.Store != nil {
|
||||
cm.Store.Shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,25 +194,40 @@ func GetAvailableStores() []CredentialStoreTypeName {
|
||||
|
||||
// CreateServiceAccount creates a new service account
|
||||
func (cm *CredentialManager) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
|
||||
return cm.store.CreateServiceAccount(ctx, sa)
|
||||
return cm.Store.CreateServiceAccount(ctx, sa)
|
||||
}
|
||||
|
||||
// UpdateServiceAccount updates an existing service account
|
||||
func (cm *CredentialManager) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
|
||||
return cm.store.UpdateServiceAccount(ctx, id, sa)
|
||||
return cm.Store.UpdateServiceAccount(ctx, id, sa)
|
||||
}
|
||||
|
||||
// DeleteServiceAccount removes a service account
|
||||
func (cm *CredentialManager) DeleteServiceAccount(ctx context.Context, id string) error {
|
||||
return cm.store.DeleteServiceAccount(ctx, id)
|
||||
return cm.Store.DeleteServiceAccount(ctx, id)
|
||||
}
|
||||
|
||||
// GetServiceAccount retrieves a service account by ID
|
||||
func (cm *CredentialManager) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
|
||||
return cm.store.GetServiceAccount(ctx, id)
|
||||
return cm.Store.GetServiceAccount(ctx, id)
|
||||
}
|
||||
|
||||
// ListServiceAccounts returns all service accounts
|
||||
func (cm *CredentialManager) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
|
||||
return cm.store.ListServiceAccounts(ctx)
|
||||
return cm.Store.ListServiceAccounts(ctx)
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user
|
||||
func (cm *CredentialManager) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
return cm.Store.AttachUserPolicy(ctx, username, policyName)
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (cm *CredentialManager) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
return cm.Store.DetachUserPolicy(ctx, username, policyName)
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (cm *CredentialManager) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
return cm.Store.ListAttachedUserPolicies(ctx, username)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ var (
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrAccessKeyNotFound = errors.New("access key not found")
|
||||
ErrServiceAccountNotFound = errors.New("service account not found")
|
||||
ErrPolicyNotFound = errors.New("policy not found")
|
||||
ErrPolicyAlreadyAttached = errors.New("policy already attached")
|
||||
ErrPolicyNotAttached = errors.New("policy not attached to user")
|
||||
)
|
||||
|
||||
// CredentialStoreTypeName represents the type name of a credential store
|
||||
@@ -81,6 +84,14 @@ type CredentialStore interface {
|
||||
ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error)
|
||||
GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error)
|
||||
|
||||
// User Policy Attachment Management
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
AttachUserPolicy(ctx context.Context, username string, policyName string) error
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
DetachUserPolicy(ctx context.Context, username string, policyName string) error
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error)
|
||||
|
||||
// Shutdown performs cleanup when the store is being shut down
|
||||
Shutdown()
|
||||
}
|
||||
|
||||
@@ -464,3 +464,65 @@ func listEntries(ctx context.Context, client filer_pb.SeaweedFilerClient, dir st
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
func (store *FilerEtcStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
// Get user
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify policy exists
|
||||
policy, err := store.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return credential.ErrPolicyNotFound
|
||||
}
|
||||
|
||||
// Check if already attached
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
return credential.ErrPolicyAlreadyAttached
|
||||
}
|
||||
}
|
||||
|
||||
identity.PolicyNames = append(identity.PolicyNames, policyName)
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (store *FilerEtcStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
var newPolicies []string
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
found = true
|
||||
} else {
|
||||
newPolicies = append(newPolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return credential.ErrPolicyNotAttached
|
||||
}
|
||||
|
||||
identity.PolicyNames = newPolicies
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (store *FilerEtcStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return identity.PolicyNames, nil
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package grpc
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
)
|
||||
|
||||
@@ -118,3 +119,66 @@ func (store *IamGrpcStore) DeleteAccessKey(ctx context.Context, username string,
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
func (store *IamGrpcStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
// Get current user
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify policy exists
|
||||
policy, err := store.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return credential.ErrPolicyNotFound
|
||||
}
|
||||
|
||||
// Check if already attached
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
// Already attached - return success (idempotent)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
identity.PolicyNames = append(identity.PolicyNames, policyName)
|
||||
return store.UpdateUser(ctx, username, identity)
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (store *IamGrpcStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
var newPolicies []string
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
found = true
|
||||
} else {
|
||||
newPolicies = append(newPolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return credential.ErrPolicyNotAttached
|
||||
}
|
||||
|
||||
identity.PolicyNames = newPolicies
|
||||
return store.UpdateUser(ctx, username, identity)
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (store *IamGrpcStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return identity.PolicyNames, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
||||
)
|
||||
|
||||
func (store *MemoryStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
|
||||
@@ -21,9 +22,16 @@ func (store *MemoryStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiC
|
||||
|
||||
// Convert all users to identities
|
||||
for _, user := range store.users {
|
||||
// Deep copy the identity to avoid mutation issues
|
||||
identityCopy := store.deepCopyIdentity(user)
|
||||
config.Identities = append(config.Identities, identityCopy)
|
||||
config.Identities = append(config.Identities, store.deepCopyIdentity(user))
|
||||
}
|
||||
|
||||
// Add all policies
|
||||
for name, doc := range store.policies {
|
||||
content, _ := json.Marshal(doc)
|
||||
config.Policies = append(config.Policies, &iam_pb.Policy{
|
||||
Name: name,
|
||||
Content: string(content),
|
||||
})
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -40,12 +48,11 @@ func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.
|
||||
// Clear existing data
|
||||
store.users = make(map[string]*iam_pb.Identity)
|
||||
store.accessKeys = make(map[string]string)
|
||||
store.policies = make(map[string]policy_engine.PolicyDocument)
|
||||
|
||||
// Add all identities
|
||||
for _, identity := range config.Identities {
|
||||
// Deep copy to avoid mutation issues
|
||||
identityCopy := store.deepCopyIdentity(identity)
|
||||
store.users[identity.Name] = identityCopy
|
||||
store.users[identity.Name] = store.deepCopyIdentity(identity)
|
||||
|
||||
// Index access keys
|
||||
for _, credential := range identity.Credentials {
|
||||
@@ -53,6 +60,14 @@ func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.
|
||||
}
|
||||
}
|
||||
|
||||
// Add all policies
|
||||
for _, policy := range config.Policies {
|
||||
var doc policy_engine.PolicyDocument
|
||||
if err := json.Unmarshal([]byte(policy.Content), &doc); err == nil {
|
||||
store.policies[policy.Name] = doc
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -284,6 +299,7 @@ func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Id
|
||||
Account: identity.Account,
|
||||
Credentials: identity.Credentials,
|
||||
Actions: identity.Actions,
|
||||
PolicyNames: identity.PolicyNames,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,8 +311,91 @@ func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Id
|
||||
Account: identity.Account,
|
||||
Credentials: identity.Credentials,
|
||||
Actions: identity.Actions,
|
||||
PolicyNames: identity.PolicyNames,
|
||||
}
|
||||
}
|
||||
|
||||
return ©
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
func (store *MemoryStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
if !store.initialized {
|
||||
return fmt.Errorf("store not initialized")
|
||||
}
|
||||
|
||||
user, exists := store.users[username]
|
||||
if !exists {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
|
||||
// Verify policy exists
|
||||
if _, exists := store.policies[policyName]; !exists {
|
||||
return credential.ErrPolicyNotFound
|
||||
}
|
||||
|
||||
// Check if already attached
|
||||
for _, p := range user.PolicyNames {
|
||||
if p == policyName {
|
||||
return credential.ErrPolicyAlreadyAttached
|
||||
}
|
||||
}
|
||||
|
||||
user.PolicyNames = append(user.PolicyNames, policyName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (store *MemoryStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
if !store.initialized {
|
||||
return fmt.Errorf("store not initialized")
|
||||
}
|
||||
|
||||
user, exists := store.users[username]
|
||||
if !exists {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
|
||||
found := false
|
||||
var newPolicies []string
|
||||
for _, p := range user.PolicyNames {
|
||||
if p == policyName {
|
||||
found = true
|
||||
} else {
|
||||
newPolicies = append(newPolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return credential.ErrPolicyNotAttached
|
||||
}
|
||||
|
||||
user.PolicyNames = newPolicies
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (store *MemoryStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
store.mu.RLock()
|
||||
defer store.mu.RUnlock()
|
||||
|
||||
if !store.initialized {
|
||||
return nil, fmt.Errorf("store not initialized")
|
||||
}
|
||||
|
||||
user, exists := store.users[username]
|
||||
if !exists {
|
||||
return nil, credential.ErrUserNotFound
|
||||
}
|
||||
|
||||
// Return copy to prevent mutation
|
||||
result := make([]string, len(user.PolicyNames))
|
||||
copy(result, user.PolicyNames)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
|
||||
config := &iam_pb.S3ApiConfiguration{}
|
||||
|
||||
// Query all users
|
||||
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users")
|
||||
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions, policy_names FROM users")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to query users: %w", err)
|
||||
}
|
||||
@@ -26,9 +26,9 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
|
||||
|
||||
for rows.Next() {
|
||||
var username, email string
|
||||
var accountDataJSON, actionsJSON []byte
|
||||
var accountDataJSON, actionsJSON, policyNamesJSON []byte
|
||||
|
||||
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil {
|
||||
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON, &policyNamesJSON); err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user row: %w", err)
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
|
||||
}
|
||||
}
|
||||
|
||||
// Parse policy names
|
||||
if len(policyNamesJSON) > 0 {
|
||||
if err := json.Unmarshal(policyNamesJSON, &identity.PolicyNames); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal policy names for user %s: %v", username, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Query credentials for this user
|
||||
credRows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username)
|
||||
if err != nil {
|
||||
@@ -116,10 +123,19 @@ func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_p
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal policy names
|
||||
var policyNamesJSON []byte
|
||||
if identity.PolicyNames != nil {
|
||||
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal policy names for user %s: %v", identity.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert user
|
||||
_, err := tx.ExecContext(ctx,
|
||||
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)",
|
||||
identity.Name, "", accountDataJSON, actionsJSON)
|
||||
"INSERT INTO users (username, email, account_data, actions, policy_names) VALUES ($1, $2, $3, $4, $5)",
|
||||
identity.Name, "", accountDataJSON, actionsJSON, policyNamesJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert user %s: %v", identity.Name, err)
|
||||
}
|
||||
@@ -178,10 +194,19 @@ func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Ide
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal policy names
|
||||
var policyNamesJSON []byte
|
||||
if identity.PolicyNames != nil {
|
||||
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal policy names: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert user
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)",
|
||||
identity.Name, "", accountDataJSON, actionsJSON)
|
||||
"INSERT INTO users (username, email, account_data, actions, policy_names) VALUES ($1, $2, $3, $4, $5)",
|
||||
identity.Name, "", accountDataJSON, actionsJSON, policyNamesJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to insert user: %w", err)
|
||||
}
|
||||
@@ -205,11 +230,11 @@ func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_
|
||||
}
|
||||
|
||||
var email string
|
||||
var accountDataJSON, actionsJSON []byte
|
||||
var accountDataJSON, actionsJSON, policyNamesJSON []byte
|
||||
|
||||
err := store.db.QueryRowContext(ctx,
|
||||
"SELECT email, account_data, actions FROM users WHERE username = $1",
|
||||
username).Scan(&email, &accountDataJSON, &actionsJSON)
|
||||
"SELECT email, account_data, actions, policy_names FROM users WHERE username = $1",
|
||||
username).Scan(&email, &accountDataJSON, &actionsJSON, &policyNamesJSON)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, credential.ErrUserNotFound
|
||||
@@ -235,6 +260,13 @@ func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_
|
||||
}
|
||||
}
|
||||
|
||||
// Parse policy names
|
||||
if len(policyNamesJSON) > 0 {
|
||||
if err := json.Unmarshal(policyNamesJSON, &identity.PolicyNames); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal policy names: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Query credentials
|
||||
rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username)
|
||||
if err != nil {
|
||||
@@ -297,10 +329,19 @@ func (store *PostgresStore) UpdateUser(ctx context.Context, username string, ide
|
||||
}
|
||||
}
|
||||
|
||||
// Marshal policy names
|
||||
var policyNamesJSON []byte
|
||||
if identity.PolicyNames != nil {
|
||||
policyNamesJSON, err = json.Marshal(identity.PolicyNames)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal policy names: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Update user
|
||||
_, err = tx.ExecContext(ctx,
|
||||
"UPDATE users SET email = $2, account_data = $3, actions = $4, updated_at = CURRENT_TIMESTAMP WHERE username = $1",
|
||||
username, "", accountDataJSON, actionsJSON)
|
||||
"UPDATE users SET email = $2, account_data = $3, actions = $4, policy_names = $5, updated_at = CURRENT_TIMESTAMP WHERE username = $1",
|
||||
username, "", accountDataJSON, actionsJSON, policyNamesJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
@@ -444,3 +485,81 @@ func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
func (store *PostgresStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
if !store.configured {
|
||||
return fmt.Errorf("store not configured")
|
||||
}
|
||||
|
||||
// Get user
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify policy exists
|
||||
policy, err := store.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return credential.ErrPolicyNotFound
|
||||
}
|
||||
|
||||
// Check if already attached
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
return credential.ErrPolicyAlreadyAttached
|
||||
}
|
||||
}
|
||||
|
||||
// Append policy name and update
|
||||
identity.PolicyNames = append(identity.PolicyNames, policyName)
|
||||
return store.UpdateUser(ctx, username, identity)
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (store *PostgresStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
if !store.configured {
|
||||
return fmt.Errorf("store not configured")
|
||||
}
|
||||
|
||||
// Get user
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find and remove policy
|
||||
found := false
|
||||
var newPolicyNames []string
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
found = true
|
||||
} else {
|
||||
newPolicyNames = append(newPolicyNames, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return credential.ErrPolicyNotAttached
|
||||
}
|
||||
|
||||
identity.PolicyNames = newPolicyNames
|
||||
return store.UpdateUser(ctx, username, identity)
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (store *PostgresStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
if !store.configured {
|
||||
return nil, fmt.Errorf("store not configured")
|
||||
}
|
||||
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return identity.PolicyNames, nil
|
||||
}
|
||||
|
||||
@@ -93,12 +93,18 @@ func (store *PostgresStore) createTables() error {
|
||||
email VARCHAR(255),
|
||||
account_data JSONB,
|
||||
actions JSONB,
|
||||
policy_names JSONB DEFAULT '[]',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
||||
`
|
||||
|
||||
// Migration: Add policy_names column if it doesn't exist (for existing installations)
|
||||
addPolicyNamesColumn := `
|
||||
ALTER TABLE users ADD COLUMN IF NOT EXISTS policy_names JSONB DEFAULT '[]';
|
||||
`
|
||||
|
||||
// Create credentials table
|
||||
credentialsTable := `
|
||||
CREATE TABLE IF NOT EXISTS credentials (
|
||||
@@ -139,6 +145,11 @@ func (store *PostgresStore) createTables() error {
|
||||
return fmt.Errorf("failed to create users table: %w", err)
|
||||
}
|
||||
|
||||
// Run migration to add policy_names column for existing installations
|
||||
if _, err := store.db.Exec(addPolicyNamesColumn); err != nil {
|
||||
return fmt.Errorf("failed to add policy_names column: %w", err)
|
||||
}
|
||||
|
||||
if _, err := store.db.Exec(credentialsTable); err != nil {
|
||||
return fmt.Errorf("failed to create credentials table: %w", err)
|
||||
}
|
||||
|
||||
@@ -91,6 +91,46 @@ func (s *PropagatingCredentialStore) propagateChange(ctx context.Context, fn fun
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func (s *PropagatingCredentialStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
glog.V(4).Infof("IAM: PropagatingCredentialStore.AttachUserPolicy %s -> %s", username, policyName)
|
||||
if err := s.CredentialStore.AttachUserPolicy(ctx, username, policyName); err != nil {
|
||||
return err
|
||||
}
|
||||
// Fetch updated identity to propagate
|
||||
identity, err := s.CredentialStore.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
glog.Warningf("failed to get user %s after attaching policy: %v", username, err)
|
||||
return nil
|
||||
}
|
||||
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
|
||||
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PropagatingCredentialStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
glog.V(4).Infof("IAM: PropagatingCredentialStore.DetachUserPolicy %s -> %s", username, policyName)
|
||||
if err := s.CredentialStore.DetachUserPolicy(ctx, username, policyName); err != nil {
|
||||
return err
|
||||
}
|
||||
// Fetch updated identity to propagate
|
||||
identity, err := s.CredentialStore.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
glog.Warningf("failed to get user %s after detaching policy: %v", username, err)
|
||||
return nil
|
||||
}
|
||||
s.propagateChange(ctx, func(tx context.Context, client s3_pb.SeaweedS3IamCacheClient) error {
|
||||
_, err := client.PutIdentity(tx, &iam_pb.PutIdentityRequest{Identity: identity})
|
||||
return err
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PropagatingCredentialStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
return s.CredentialStore.ListAttachedUserPolicies(ctx, username)
|
||||
}
|
||||
|
||||
func (s *PropagatingCredentialStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
|
||||
glog.V(4).Infof("IAM: PropagatingCredentialStore.CreateUser %s", identity.Name)
|
||||
if err := s.CredentialStore.CreateUser(ctx, identity); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user