mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-22 05:54:24 +02:00
* s3api: persist single-identity IAM changes through the credential manager CreateAccessKey, DeleteAccessKey, UpdateAccessKey, PutUserPolicy, DeleteUserPolicy, SetUserStatus, UpdateUser and DeleteUser used to load the whole configuration, change one identity and then SaveConfiguration, rewriting and re-propagating every identity. They now write only the identity (and, for rename/delete, the groups and service accounts that referenced it) through the credential manager, which propagates that identity alone. Static identities and skipPersist keep the previous full-save/no-write contract. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * iamapi: persist single-identity IAM changes through the credential manager Same targeted-write change as the embedded IAM API for the standalone weed iam dispatcher: UpdateUser, DeleteUser, CreateAccessKey, DeleteAccessKey, UpdateAccessKey, PutUserPolicy and DeleteUserPolicy write one identity through the credential manager instead of saving the whole configuration. CreateAccessKey keeps the full save when the handler created a user the store does not know. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * credential/memory: re-key identity on UpdateUser rename UpdateUser stored the identity under the old username key even when identity.Name changed. Re-key it the way filer_etc does (delete old, save new) so the targeted rename path sees the moved record. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: cover targeted single-identity IAM writes Recording-store tests assert each rerouted action uses the single-identity credential store methods and never SaveConfiguration, in both the embedded S3 IAM dispatcher and the standalone iamapi dispatcher, plus the skipPersist and static-identity contracts and a memory-store rename case. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> * credential: keep not-found and rename-collision semantics in stores IamGrpcStore.DeleteAccessKey translates gRPC NotFound to ErrAccessKeyNotFound so a repeat delete stays the no-op the IAM handlers advertise. MemoryStore.UpdateUser rejects a rename onto an existing user instead of overwriting it, matching RenameUser. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
464 lines
12 KiB
Go
464 lines
12 KiB
Go
package memory
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"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) {
|
|
store.mu.RLock()
|
|
defer store.mu.RUnlock()
|
|
|
|
if !store.initialized {
|
|
return nil, fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
config := &iam_pb.S3ApiConfiguration{}
|
|
|
|
// Convert all users to identities
|
|
for _, user := range store.users {
|
|
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),
|
|
})
|
|
}
|
|
|
|
// Groups are written via CreateGroup / AddUserToGroup / AttachGroupPolicy
|
|
// (never via SaveConfiguration), so they live in store.groups regardless
|
|
// of the bulk-config path. Surface them here so consumers that reload
|
|
// through cm.LoadConfiguration see a faithful snapshot — without this,
|
|
// iam.groups would never repopulate on memory-store reloads and group
|
|
// policy evaluation would silently no-op.
|
|
for _, g := range store.groups {
|
|
config.Groups = append(config.Groups, cloneGroup(g))
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
func (store *MemoryStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
if !store.initialized {
|
|
return fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
// 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 {
|
|
store.users[identity.Name] = store.deepCopyIdentity(identity)
|
|
|
|
// Index access keys
|
|
for _, credential := range identity.Credentials {
|
|
store.accessKeys[credential.AccessKey] = identity.Name
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (store *MemoryStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
if !store.initialized {
|
|
return fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
if _, exists := store.users[identity.Name]; exists {
|
|
return credential.ErrUserAlreadyExists
|
|
}
|
|
|
|
// Check for duplicate access keys
|
|
for _, cred := range identity.Credentials {
|
|
if _, exists := store.accessKeys[cred.AccessKey]; exists {
|
|
return fmt.Errorf("access key %s already exists", cred.AccessKey)
|
|
}
|
|
}
|
|
|
|
// Deep copy to avoid mutation issues
|
|
identityCopy := store.deepCopyIdentity(identity)
|
|
store.users[identity.Name] = identityCopy
|
|
|
|
// Index access keys
|
|
for _, cred := range identity.Credentials {
|
|
store.accessKeys[cred.AccessKey] = identity.Name
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, 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 a deep copy to avoid mutation issues
|
|
return store.deepCopyIdentity(user), nil
|
|
}
|
|
|
|
func (store *MemoryStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
if !store.initialized {
|
|
return fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
existingUser, exists := store.users[username]
|
|
if !exists {
|
|
return credential.ErrUserNotFound
|
|
}
|
|
|
|
if identity.Name != username {
|
|
// Rename: the destination name must not already be taken, matching
|
|
// RenameUser's collision check.
|
|
if _, clash := store.users[identity.Name]; clash {
|
|
return credential.ErrUserAlreadyExists
|
|
}
|
|
}
|
|
|
|
// Remove old access keys from index
|
|
for _, cred := range existingUser.Credentials {
|
|
delete(store.accessKeys, cred.AccessKey)
|
|
}
|
|
|
|
// Check for duplicate access keys (excluding current user)
|
|
for _, cred := range identity.Credentials {
|
|
if existingUsername, exists := store.accessKeys[cred.AccessKey]; exists && existingUsername != username {
|
|
return fmt.Errorf("access key %s already exists", cred.AccessKey)
|
|
}
|
|
}
|
|
|
|
// Deep copy to avoid mutation issues
|
|
identityCopy := store.deepCopyIdentity(identity)
|
|
if identityCopy.Name != username {
|
|
// Rename: re-key the record, the way filer_etc does (delete old, save new).
|
|
delete(store.users, username)
|
|
}
|
|
store.users[identityCopy.Name] = identityCopy
|
|
|
|
// Re-index access keys
|
|
for _, cred := range identity.Credentials {
|
|
store.accessKeys[cred.AccessKey] = identityCopy.Name
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStore) DeleteUser(ctx context.Context, username 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
|
|
}
|
|
|
|
// Remove access keys from index
|
|
for _, cred := range user.Credentials {
|
|
delete(store.accessKeys, cred.AccessKey)
|
|
}
|
|
|
|
// Remove user
|
|
delete(store.users, username)
|
|
delete(store.inlinePolicies, username)
|
|
|
|
return nil
|
|
}
|
|
|
|
// RenameUser implements credential.UserRenamer for the memory store.
|
|
// Renames the user and re-points access-key and inline-policy indexes
|
|
// to the new name. The whole operation is performed under the store
|
|
// lock so it is atomic with respect to concurrent reads.
|
|
func (store *MemoryStore) RenameUser(ctx context.Context, oldName, newName string) error {
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
if !store.initialized {
|
|
return fmt.Errorf("store not initialized")
|
|
}
|
|
if oldName == newName {
|
|
return nil
|
|
}
|
|
|
|
user, exists := store.users[oldName]
|
|
if !exists {
|
|
return credential.ErrUserNotFound
|
|
}
|
|
if _, clash := store.users[newName]; clash {
|
|
return credential.ErrUserAlreadyExists
|
|
}
|
|
|
|
user.Name = newName
|
|
store.users[newName] = user
|
|
delete(store.users, oldName)
|
|
|
|
for _, cred := range user.Credentials {
|
|
store.accessKeys[cred.AccessKey] = newName
|
|
}
|
|
|
|
if policies, ok := store.inlinePolicies[oldName]; ok {
|
|
store.inlinePolicies[newName] = policies
|
|
delete(store.inlinePolicies, oldName)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStore) ListUsers(ctx context.Context) ([]string, error) {
|
|
store.mu.RLock()
|
|
defer store.mu.RUnlock()
|
|
|
|
if !store.initialized {
|
|
return nil, fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
var usernames []string
|
|
for username := range store.users {
|
|
usernames = append(usernames, username)
|
|
}
|
|
|
|
return usernames, nil
|
|
}
|
|
|
|
func (store *MemoryStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
|
|
store.mu.RLock()
|
|
defer store.mu.RUnlock()
|
|
|
|
if !store.initialized {
|
|
return nil, fmt.Errorf("store not initialized")
|
|
}
|
|
|
|
username, exists := store.accessKeys[accessKey]
|
|
if !exists {
|
|
return nil, credential.ErrAccessKeyNotFound
|
|
}
|
|
|
|
user, exists := store.users[username]
|
|
if !exists {
|
|
// This should not happen, but handle it gracefully
|
|
return nil, credential.ErrUserNotFound
|
|
}
|
|
|
|
// Return a deep copy to avoid mutation issues
|
|
return store.deepCopyIdentity(user), nil
|
|
}
|
|
|
|
func (store *MemoryStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) 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
|
|
}
|
|
|
|
// Check if access key already exists
|
|
if _, exists := store.accessKeys[cred.AccessKey]; exists {
|
|
return fmt.Errorf("access key %s already exists", cred.AccessKey)
|
|
}
|
|
|
|
// Add credential to user
|
|
user.Credentials = append(user.Credentials, &iam_pb.Credential{
|
|
AccessKey: cred.AccessKey,
|
|
SecretKey: cred.SecretKey,
|
|
})
|
|
|
|
// Index the access key
|
|
store.accessKeys[cred.AccessKey] = username
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *MemoryStore) DeleteAccessKey(ctx context.Context, username string, accessKey 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
|
|
}
|
|
|
|
// Find and remove the credential
|
|
var newCredentials []*iam_pb.Credential
|
|
found := false
|
|
for _, cred := range user.Credentials {
|
|
if cred.AccessKey == accessKey {
|
|
found = true
|
|
// Remove from access key index
|
|
delete(store.accessKeys, accessKey)
|
|
} else {
|
|
newCredentials = append(newCredentials, cred)
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return credential.ErrAccessKeyNotFound
|
|
}
|
|
|
|
user.Credentials = newCredentials
|
|
return nil
|
|
}
|
|
|
|
// deepCopyIdentity creates a deep copy of an identity to avoid mutation issues
|
|
func (store *MemoryStore) deepCopyIdentity(identity *iam_pb.Identity) *iam_pb.Identity {
|
|
if identity == nil {
|
|
return nil
|
|
}
|
|
|
|
// Use JSON marshaling/unmarshaling for deep copy
|
|
// This is simple and safe for protobuf messages
|
|
data, err := json.Marshal(identity)
|
|
if err != nil {
|
|
// Fallback to shallow copy if JSON fails
|
|
return &iam_pb.Identity{
|
|
Name: identity.Name,
|
|
Account: identity.Account,
|
|
Credentials: identity.Credentials,
|
|
Actions: identity.Actions,
|
|
PolicyNames: identity.PolicyNames,
|
|
}
|
|
}
|
|
|
|
var copy iam_pb.Identity
|
|
if err := json.Unmarshal(data, ©); err != nil {
|
|
// Fallback to shallow copy if JSON fails
|
|
return &iam_pb.Identity{
|
|
Name: identity.Name,
|
|
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
|
|
}
|