Browse Source
Add credential storage (#6938)
Add credential storage (#6938)
* add credential store interface * load credential.toml * lint * create credentialManager with explicit store type * add type name * InitializeCredentialManager * remove unused functions * fix missing import * fix import * fix nil configurationtesting-sdx-generation
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 3652 additions and 284 deletions
-
48weed/admin/dash/admin_server.go
-
319weed/admin/dash/user_management.go
-
9weed/command/iam.go
-
3weed/command/s3.go
-
11weed/command/scaffold.go
-
55weed/command/scaffold/credential.toml
-
3weed/command/scaffold/example.go
-
182weed/credential/README.md
-
133weed/credential/config_loader.go
-
125weed/credential/credential_manager.go
-
91weed/credential/credential_store.go
-
353weed/credential/credential_test.go
-
235weed/credential/filer_etc/filer_etc_store.go
-
373weed/credential/memory/memory_store.go
-
315weed/credential/memory/memory_store_test.go
-
221weed/credential/migration.go
-
570weed/credential/postgres/postgres_store.go
-
557weed/credential/sqlite/sqlite_store.go
-
122weed/credential/test/integration_test.go
-
59weed/iamapi/iamapi_server.go
-
62weed/s3api/auth_credentials.go
-
8weed/s3api/s3api_put_object_helper_test.go
-
18weed/s3api/s3api_server.go
@ -0,0 +1,55 @@ |
|||||
|
# Put this file to one of the location, with descending priority |
||||
|
# ./credential.toml |
||||
|
# $HOME/.seaweedfs/credential.toml |
||||
|
# /etc/seaweedfs/credential.toml |
||||
|
# this file is read by S3 API and IAM API servers |
||||
|
|
||||
|
# Choose one of the credential stores below |
||||
|
# Only one store can be enabled at a time |
||||
|
|
||||
|
# Filer-based credential store (default, uses existing filer storage) |
||||
|
[credential.filer_etc] |
||||
|
enabled = true |
||||
|
# filer address and grpc_dial_option will be automatically configured by the server |
||||
|
|
||||
|
# SQLite credential store (recommended for single-node deployments) |
||||
|
[credential.sqlite] |
||||
|
enabled = false |
||||
|
file = "/var/lib/seaweedfs/credentials.db" |
||||
|
# Optional: table name prefix (default: "sw_") |
||||
|
table_prefix = "sw_" |
||||
|
|
||||
|
# PostgreSQL credential store (recommended for multi-node deployments) |
||||
|
[credential.postgres] |
||||
|
enabled = false |
||||
|
hostname = "localhost" |
||||
|
port = 5432 |
||||
|
username = "seaweedfs" |
||||
|
password = "your_password" |
||||
|
database = "seaweedfs" |
||||
|
schema = "public" |
||||
|
sslmode = "disable" |
||||
|
# Optional: table name prefix (default: "sw_") |
||||
|
table_prefix = "sw_" |
||||
|
# Connection pool settings |
||||
|
connection_max_idle = 10 |
||||
|
connection_max_open = 100 |
||||
|
connection_max_lifetime_seconds = 3600 |
||||
|
|
||||
|
# Memory credential store (for testing only, data is lost on restart) |
||||
|
[credential.memory] |
||||
|
enabled = false |
||||
|
|
||||
|
# Environment variable overrides: |
||||
|
# Any configuration value can be overridden by environment variables |
||||
|
# Rules: |
||||
|
# * Prefix with "WEED_CREDENTIAL_" |
||||
|
# * Convert to uppercase |
||||
|
# * Replace '.' with '_' |
||||
|
# |
||||
|
# Examples: |
||||
|
# export WEED_CREDENTIAL_POSTGRES_PASSWORD=secret |
||||
|
# export WEED_CREDENTIAL_SQLITE_FILE=/custom/path/credentials.db |
||||
|
# export WEED_CREDENTIAL_POSTGRES_HOSTNAME=db.example.com |
||||
|
# export WEED_CREDENTIAL_FILER_ETC_ENABLED=true |
||||
|
# export WEED_CREDENTIAL_SQLITE_ENABLED=false |
@ -0,0 +1,182 @@ |
|||||
|
# Credential Store Integration |
||||
|
|
||||
|
This document shows how the credential store has been integrated into SeaweedFS's S3 API and IAM API components. |
||||
|
|
||||
|
## Quick Start |
||||
|
|
||||
|
1. **Generate credential configuration:** |
||||
|
```bash |
||||
|
weed scaffold -config=credential -output=. |
||||
|
``` |
||||
|
|
||||
|
2. **Edit credential.toml** to enable your preferred store (filer_etc is enabled by default) |
||||
|
|
||||
|
3. **Start S3 API server** - it will automatically load credential.toml: |
||||
|
```bash |
||||
|
weed s3 -filer=localhost:8888 |
||||
|
``` |
||||
|
|
||||
|
## Integration Overview |
||||
|
|
||||
|
The credential store provides a pluggable backend for storing S3 identities and credentials, supporting: |
||||
|
- **Filer-based storage** (filer_etc) - Uses existing filer storage (default) |
||||
|
- **SQLite** - Local database storage |
||||
|
- **PostgreSQL** - Shared database for multiple servers |
||||
|
- **Memory** - In-memory storage for testing |
||||
|
|
||||
|
## Configuration |
||||
|
|
||||
|
### Using credential.toml |
||||
|
|
||||
|
Generate the configuration template: |
||||
|
```bash |
||||
|
weed scaffold -config=credential |
||||
|
``` |
||||
|
|
||||
|
This creates a `credential.toml` file with all available options. The filer_etc store is enabled by default: |
||||
|
|
||||
|
```toml |
||||
|
# Filer-based credential store (default, uses existing filer storage) |
||||
|
[credential.filer_etc] |
||||
|
enabled = true |
||||
|
|
||||
|
# SQLite credential store (recommended for single-node deployments) |
||||
|
[credential.sqlite] |
||||
|
enabled = false |
||||
|
file = "/var/lib/seaweedfs/credentials.db" |
||||
|
|
||||
|
# PostgreSQL credential store (recommended for multi-node deployments) |
||||
|
[credential.postgres] |
||||
|
enabled = false |
||||
|
hostname = "localhost" |
||||
|
port = 5432 |
||||
|
username = "seaweedfs" |
||||
|
password = "your_password" |
||||
|
database = "seaweedfs" |
||||
|
|
||||
|
# Memory credential store (for testing only, data is lost on restart) |
||||
|
[credential.memory] |
||||
|
enabled = false |
||||
|
``` |
||||
|
|
||||
|
The credential.toml file is automatically loaded from these locations (in priority order): |
||||
|
- `./credential.toml` |
||||
|
- `$HOME/.seaweedfs/credential.toml` |
||||
|
- `/etc/seaweedfs/credential.toml` |
||||
|
|
||||
|
### Server Configuration |
||||
|
|
||||
|
Both S3 API and IAM API servers automatically load credential.toml during startup. No additional configuration is required. |
||||
|
|
||||
|
## Usage Examples |
||||
|
|
||||
|
### Filer-based Store (Default) |
||||
|
|
||||
|
```toml |
||||
|
[credential.filer_etc] |
||||
|
enabled = true |
||||
|
``` |
||||
|
|
||||
|
This uses the existing filer storage and is compatible with current deployments. |
||||
|
|
||||
|
### SQLite Store |
||||
|
|
||||
|
```toml |
||||
|
[credential.sqlite] |
||||
|
enabled = true |
||||
|
file = "/var/lib/seaweedfs/credentials.db" |
||||
|
table_prefix = "sw_" |
||||
|
``` |
||||
|
|
||||
|
### PostgreSQL Store |
||||
|
|
||||
|
```toml |
||||
|
[credential.postgres] |
||||
|
enabled = true |
||||
|
hostname = "localhost" |
||||
|
port = 5432 |
||||
|
username = "seaweedfs" |
||||
|
password = "your_password" |
||||
|
database = "seaweedfs" |
||||
|
schema = "public" |
||||
|
sslmode = "disable" |
||||
|
table_prefix = "sw_" |
||||
|
connection_max_idle = 10 |
||||
|
connection_max_open = 100 |
||||
|
connection_max_lifetime_seconds = 3600 |
||||
|
``` |
||||
|
|
||||
|
### Memory Store (Testing) |
||||
|
|
||||
|
```toml |
||||
|
[credential.memory] |
||||
|
enabled = true |
||||
|
``` |
||||
|
|
||||
|
## Environment Variables |
||||
|
|
||||
|
All credential configuration can be overridden with environment variables: |
||||
|
|
||||
|
```bash |
||||
|
# Override PostgreSQL password |
||||
|
export WEED_CREDENTIAL_POSTGRES_PASSWORD=secret |
||||
|
|
||||
|
# Override SQLite file path |
||||
|
export WEED_CREDENTIAL_SQLITE_FILE=/custom/path/credentials.db |
||||
|
|
||||
|
# Override PostgreSQL hostname |
||||
|
export WEED_CREDENTIAL_POSTGRES_HOSTNAME=db.example.com |
||||
|
|
||||
|
# Enable/disable stores |
||||
|
export WEED_CREDENTIAL_FILER_ETC_ENABLED=true |
||||
|
export WEED_CREDENTIAL_SQLITE_ENABLED=false |
||||
|
``` |
||||
|
|
||||
|
Rules: |
||||
|
- Prefix with `WEED_CREDENTIAL_` |
||||
|
- Convert to uppercase |
||||
|
- Replace `.` with `_` |
||||
|
|
||||
|
## Implementation Details |
||||
|
|
||||
|
Components automatically load credential configuration during startup: |
||||
|
|
||||
|
```go |
||||
|
// Server initialization |
||||
|
if credConfig, err := credential.LoadCredentialConfiguration(); err == nil && credConfig != nil { |
||||
|
credentialManager, err := credential.NewCredentialManager( |
||||
|
credConfig.Store, |
||||
|
credConfig.Config, |
||||
|
credConfig.Prefix, |
||||
|
) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to initialize credential manager: %v", err) |
||||
|
} |
||||
|
// Use credential manager for operations |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
## Benefits |
||||
|
|
||||
|
1. **Easy Configuration** - Generate template with `weed scaffold -config=credential` |
||||
|
2. **Pluggable Storage** - Switch between filer_etc, SQLite, PostgreSQL without code changes |
||||
|
3. **Backward Compatibility** - Filer-based storage works with existing deployments |
||||
|
4. **Scalability** - Database stores support multiple concurrent servers |
||||
|
5. **Performance** - Database access can be faster than file-based storage |
||||
|
6. **Testing** - Memory store simplifies unit testing |
||||
|
7. **Environment Override** - All settings can be overridden with environment variables |
||||
|
|
||||
|
## Error Handling |
||||
|
|
||||
|
When a credential store is configured, it must initialize successfully or the server will fail to start: |
||||
|
|
||||
|
```go |
||||
|
if credConfig != nil { |
||||
|
credentialManager, err = credential.NewCredentialManager(...) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to initialize credential manager: %v", err) |
||||
|
} |
||||
|
} |
||||
|
``` |
||||
|
|
||||
|
This ensures explicit configuration - if you configure a credential store, it must work properly. |
@ -0,0 +1,133 @@ |
|||||
|
package credential |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
// CredentialConfig represents the credential configuration from credential.toml
|
||||
|
type CredentialConfig struct { |
||||
|
Store string |
||||
|
Config util.Configuration |
||||
|
Prefix string |
||||
|
} |
||||
|
|
||||
|
// LoadCredentialConfiguration loads credential configuration from credential.toml
|
||||
|
// Returns the store type, configuration, and prefix for credential management
|
||||
|
func LoadCredentialConfiguration() (*CredentialConfig, error) { |
||||
|
// Try to load credential.toml configuration
|
||||
|
loaded := util.LoadConfiguration("credential", false) |
||||
|
if !loaded { |
||||
|
glog.V(1).Info("No credential.toml found, credential store disabled") |
||||
|
return nil, nil |
||||
|
} |
||||
|
|
||||
|
viper := util.GetViper() |
||||
|
|
||||
|
// Find which credential store is enabled
|
||||
|
var enabledStore string |
||||
|
var storePrefix string |
||||
|
|
||||
|
// Get available store types from registered stores
|
||||
|
storeTypes := GetAvailableStores() |
||||
|
for _, storeType := range storeTypes { |
||||
|
key := fmt.Sprintf("credential.%s.enabled", string(storeType)) |
||||
|
if viper.GetBool(key) { |
||||
|
if enabledStore != "" { |
||||
|
return nil, fmt.Errorf("multiple credential stores enabled: %s and %s. Only one store can be enabled", enabledStore, string(storeType)) |
||||
|
} |
||||
|
enabledStore = string(storeType) |
||||
|
storePrefix = fmt.Sprintf("credential.%s.", string(storeType)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if enabledStore == "" { |
||||
|
glog.V(1).Info("No credential store enabled in credential.toml") |
||||
|
return nil, nil |
||||
|
} |
||||
|
|
||||
|
glog.V(0).Infof("Loaded credential configuration: store=%s", enabledStore) |
||||
|
|
||||
|
return &CredentialConfig{ |
||||
|
Store: enabledStore, |
||||
|
Config: viper, |
||||
|
Prefix: storePrefix, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// GetCredentialStoreConfig extracts credential store configuration from command line flags
|
||||
|
// This is used when credential store is configured via command line instead of credential.toml
|
||||
|
func GetCredentialStoreConfig(store string, config util.Configuration, prefix string) *CredentialConfig { |
||||
|
if store == "" { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
return &CredentialConfig{ |
||||
|
Store: store, |
||||
|
Config: config, |
||||
|
Prefix: prefix, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// MergeCredentialConfig merges command line credential config with credential.toml config
|
||||
|
// Command line flags take priority over credential.toml
|
||||
|
func MergeCredentialConfig(cmdLineStore string, cmdLineConfig util.Configuration, cmdLinePrefix string) (*CredentialConfig, error) { |
||||
|
// If command line credential store is specified, use it
|
||||
|
if cmdLineStore != "" { |
||||
|
glog.V(0).Infof("Using command line credential configuration: store=%s", cmdLineStore) |
||||
|
return GetCredentialStoreConfig(cmdLineStore, cmdLineConfig, cmdLinePrefix), nil |
||||
|
} |
||||
|
|
||||
|
// Otherwise, try to load from credential.toml
|
||||
|
config, err := LoadCredentialConfiguration() |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
|
||||
|
if config == nil { |
||||
|
glog.V(1).Info("No credential store configured") |
||||
|
} |
||||
|
|
||||
|
return config, nil |
||||
|
} |
||||
|
|
||||
|
// NewCredentialManagerWithDefaults creates a credential manager with fallback to defaults
|
||||
|
// If explicitStore is provided, it will be used regardless of credential.toml
|
||||
|
// If explicitStore is empty, it tries credential.toml first, then defaults to "filer_etc"
|
||||
|
func NewCredentialManagerWithDefaults(explicitStore CredentialStoreTypeName) (*CredentialManager, error) { |
||||
|
var storeName CredentialStoreTypeName |
||||
|
var config util.Configuration |
||||
|
var prefix string |
||||
|
|
||||
|
// If explicit store is provided, use it
|
||||
|
if explicitStore != "" { |
||||
|
storeName = explicitStore |
||||
|
config = nil |
||||
|
prefix = "" |
||||
|
glog.V(0).Infof("Using explicit credential store: %s", storeName) |
||||
|
} else { |
||||
|
// Try to load from credential.toml first
|
||||
|
if credConfig, err := LoadCredentialConfiguration(); err == nil && credConfig != nil { |
||||
|
storeName = CredentialStoreTypeName(credConfig.Store) |
||||
|
config = credConfig.Config |
||||
|
prefix = credConfig.Prefix |
||||
|
glog.V(0).Infof("Loaded credential configuration from credential.toml: store=%s", storeName) |
||||
|
} else { |
||||
|
// Default to filer_etc store
|
||||
|
storeName = StoreTypeFilerEtc |
||||
|
config = nil |
||||
|
prefix = "" |
||||
|
glog.V(1).Info("No credential.toml found, defaulting to filer_etc store") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Create the credential manager
|
||||
|
credentialManager, err := NewCredentialManager(storeName, config, prefix) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to initialize credential manager with store '%s': %v", storeName, err) |
||||
|
} |
||||
|
|
||||
|
return credentialManager, nil |
||||
|
} |
@ -0,0 +1,125 @@ |
|||||
|
package credential |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
// CredentialManager manages user credentials using a configurable store
|
||||
|
type CredentialManager struct { |
||||
|
store CredentialStore |
||||
|
} |
||||
|
|
||||
|
// NewCredentialManager creates a new credential manager with the specified store
|
||||
|
func NewCredentialManager(storeName CredentialStoreTypeName, configuration util.Configuration, prefix string) (*CredentialManager, error) { |
||||
|
var store CredentialStore |
||||
|
|
||||
|
// Find the requested store implementation
|
||||
|
for _, s := range Stores { |
||||
|
if s.GetName() == storeName { |
||||
|
store = s |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if store == nil { |
||||
|
return nil, fmt.Errorf("credential store '%s' not found. Available stores: %s", |
||||
|
storeName, getAvailableStores()) |
||||
|
} |
||||
|
|
||||
|
// Initialize the store
|
||||
|
if err := store.Initialize(configuration, prefix); err != nil { |
||||
|
return nil, fmt.Errorf("failed to initialize credential store '%s': %v", storeName, err) |
||||
|
} |
||||
|
|
||||
|
return &CredentialManager{ |
||||
|
store: store, |
||||
|
}, nil |
||||
|
} |
||||
|
|
||||
|
// GetStore returns the underlying credential store
|
||||
|
func (cm *CredentialManager) GetStore() CredentialStore { |
||||
|
return cm.store |
||||
|
} |
||||
|
|
||||
|
// LoadConfiguration loads the S3 API configuration
|
||||
|
func (cm *CredentialManager) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// CreateUser creates a new user
|
||||
|
func (cm *CredentialManager) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
} |
||||
|
|
||||
|
// DeleteUser removes a user
|
||||
|
func (cm *CredentialManager) DeleteUser(ctx context.Context, username string) error { |
||||
|
return cm.store.DeleteUser(ctx, username) |
||||
|
} |
||||
|
|
||||
|
// ListUsers returns all usernames
|
||||
|
func (cm *CredentialManager) ListUsers(ctx context.Context) ([]string, error) { |
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
} |
||||
|
|
||||
|
// Shutdown performs cleanup
|
||||
|
func (cm *CredentialManager) Shutdown() { |
||||
|
if cm.store != nil { |
||||
|
cm.store.Shutdown() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// getAvailableStores returns a comma-separated list of available store names
|
||||
|
func getAvailableStores() string { |
||||
|
var storeNames []string |
||||
|
for _, store := range Stores { |
||||
|
storeNames = append(storeNames, string(store.GetName())) |
||||
|
} |
||||
|
return strings.Join(storeNames, ", ") |
||||
|
} |
||||
|
|
||||
|
// GetAvailableStores returns a list of available credential store names
|
||||
|
func GetAvailableStores() []CredentialStoreTypeName { |
||||
|
var storeNames []CredentialStoreTypeName |
||||
|
for _, store := range Stores { |
||||
|
storeNames = append(storeNames, store.GetName()) |
||||
|
} |
||||
|
if storeNames == nil { |
||||
|
return []CredentialStoreTypeName{} |
||||
|
} |
||||
|
return storeNames |
||||
|
} |
@ -0,0 +1,91 @@ |
|||||
|
package credential |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"errors" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
var ( |
||||
|
ErrUserNotFound = errors.New("user not found") |
||||
|
ErrUserAlreadyExists = errors.New("user already exists") |
||||
|
ErrAccessKeyNotFound = errors.New("access key not found") |
||||
|
) |
||||
|
|
||||
|
// CredentialStoreTypeName represents the type name of a credential store
|
||||
|
type CredentialStoreTypeName string |
||||
|
|
||||
|
// Credential store name constants
|
||||
|
const ( |
||||
|
StoreTypeMemory CredentialStoreTypeName = "memory" |
||||
|
StoreTypeFilerEtc CredentialStoreTypeName = "filer_etc" |
||||
|
StoreTypePostgres CredentialStoreTypeName = "postgres" |
||||
|
StoreTypeSQLite CredentialStoreTypeName = "sqlite" |
||||
|
) |
||||
|
|
||||
|
// CredentialStore defines the interface for user credential storage and retrieval
|
||||
|
type CredentialStore interface { |
||||
|
// GetName returns the name of the credential store implementation
|
||||
|
GetName() CredentialStoreTypeName |
||||
|
|
||||
|
// Initialize initializes the credential store with configuration
|
||||
|
Initialize(configuration util.Configuration, prefix string) error |
||||
|
|
||||
|
// LoadConfiguration loads the entire S3 API configuration
|
||||
|
LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) |
||||
|
|
||||
|
// SaveConfiguration saves the entire S3 API configuration
|
||||
|
SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error |
||||
|
|
||||
|
// CreateUser creates a new user with the given identity
|
||||
|
CreateUser(ctx context.Context, identity *iam_pb.Identity) error |
||||
|
|
||||
|
// GetUser retrieves a user by username
|
||||
|
GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) |
||||
|
|
||||
|
// UpdateUser updates an existing user
|
||||
|
UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error |
||||
|
|
||||
|
// DeleteUser removes a user by username
|
||||
|
DeleteUser(ctx context.Context, username string) error |
||||
|
|
||||
|
// ListUsers returns all usernames
|
||||
|
ListUsers(ctx context.Context) ([]string, error) |
||||
|
|
||||
|
// GetUserByAccessKey retrieves a user by access key
|
||||
|
GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) |
||||
|
|
||||
|
// CreateAccessKey creates a new access key for a user
|
||||
|
CreateAccessKey(ctx context.Context, username string, credential *iam_pb.Credential) error |
||||
|
|
||||
|
// DeleteAccessKey removes an access key for a user
|
||||
|
DeleteAccessKey(ctx context.Context, username string, accessKey string) error |
||||
|
|
||||
|
// Shutdown performs cleanup when the store is being shut down
|
||||
|
Shutdown() |
||||
|
} |
||||
|
|
||||
|
// AccessKeyInfo represents access key information with metadata
|
||||
|
type AccessKeyInfo struct { |
||||
|
AccessKey string `json:"accessKey"` |
||||
|
SecretKey string `json:"secretKey"` |
||||
|
Username string `json:"username"` |
||||
|
CreatedAt time.Time `json:"createdAt"` |
||||
|
} |
||||
|
|
||||
|
// UserCredentials represents a user's credentials and metadata
|
||||
|
type UserCredentials struct { |
||||
|
Username string `json:"username"` |
||||
|
Email string `json:"email"` |
||||
|
Account *iam_pb.Account `json:"account,omitempty"` |
||||
|
Credentials []*iam_pb.Credential `json:"credentials"` |
||||
|
Actions []string `json:"actions"` |
||||
|
CreatedAt time.Time `json:"createdAt"` |
||||
|
UpdatedAt time.Time `json:"updatedAt"` |
||||
|
} |
||||
|
|
||||
|
// Stores holds all available credential store implementations
|
||||
|
var Stores []CredentialStore |
@ -0,0 +1,353 @@ |
|||||
|
package credential |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
func TestCredentialStoreInterface(t *testing.T) { |
||||
|
// Note: This test may fail if run without importing store packages
|
||||
|
// For full integration testing, see the test/ package
|
||||
|
if len(Stores) == 0 { |
||||
|
t.Skip("No credential stores registered - this is expected when testing the base package without store imports") |
||||
|
} |
||||
|
|
||||
|
// Check that expected stores are available
|
||||
|
storeNames := GetAvailableStores() |
||||
|
expectedStores := []string{string(StoreTypeFilerEtc), string(StoreTypeMemory)} |
||||
|
|
||||
|
// Add SQLite and PostgreSQL if they're available (build tags dependent)
|
||||
|
for _, storeName := range storeNames { |
||||
|
found := false |
||||
|
for _, expected := range append(expectedStores, string(StoreTypeSQLite), string(StoreTypePostgres)) { |
||||
|
if string(storeName) == expected { |
||||
|
found = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !found { |
||||
|
t.Errorf("Unexpected store found: %s", storeName) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Test that filer_etc store is always available
|
||||
|
filerEtcStoreFound := false |
||||
|
memoryStoreFound := false |
||||
|
for _, storeName := range storeNames { |
||||
|
if string(storeName) == string(StoreTypeFilerEtc) { |
||||
|
filerEtcStoreFound = true |
||||
|
} |
||||
|
if string(storeName) == string(StoreTypeMemory) { |
||||
|
memoryStoreFound = true |
||||
|
} |
||||
|
} |
||||
|
if !filerEtcStoreFound { |
||||
|
t.Error("FilerEtc store should always be available") |
||||
|
} |
||||
|
if !memoryStoreFound { |
||||
|
t.Error("Memory store should always be available") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestCredentialManagerCreation(t *testing.T) { |
||||
|
config := util.GetViper() |
||||
|
|
||||
|
// Test creating credential manager with invalid store
|
||||
|
_, err := NewCredentialManager(CredentialStoreTypeName("nonexistent"), config, "test.") |
||||
|
if err == nil { |
||||
|
t.Error("Expected error for nonexistent store") |
||||
|
} |
||||
|
|
||||
|
// Skip store-specific tests if no stores are registered
|
||||
|
if len(Stores) == 0 { |
||||
|
t.Skip("No credential stores registered - skipping store-specific tests") |
||||
|
} |
||||
|
|
||||
|
// Test creating credential manager with available stores
|
||||
|
availableStores := GetAvailableStores() |
||||
|
if len(availableStores) == 0 { |
||||
|
t.Skip("No stores available for testing") |
||||
|
} |
||||
|
|
||||
|
// Test with the first available store
|
||||
|
storeName := availableStores[0] |
||||
|
cm, err := NewCredentialManager(storeName, config, "test.") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create credential manager with store %s: %v", storeName, err) |
||||
|
} |
||||
|
if cm == nil { |
||||
|
t.Error("Credential manager should not be nil") |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
// Test that the store is of the correct type
|
||||
|
if cm.GetStore().GetName() != storeName { |
||||
|
t.Errorf("Expected %s store, got %s", storeName, cm.GetStore().GetName()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestCredentialInterface(t *testing.T) { |
||||
|
// Skip if no stores are registered
|
||||
|
if len(Stores) == 0 { |
||||
|
t.Skip("No credential stores registered - for full testing see test/ package") |
||||
|
} |
||||
|
|
||||
|
// Test the interface with the first available store
|
||||
|
availableStores := GetAvailableStores() |
||||
|
if len(availableStores) == 0 { |
||||
|
t.Skip("No stores available for testing") |
||||
|
} |
||||
|
|
||||
|
testCredentialInterfaceWithStore(t, availableStores[0]) |
||||
|
} |
||||
|
|
||||
|
func testCredentialInterfaceWithStore(t *testing.T, storeName CredentialStoreTypeName) { |
||||
|
// Create a test identity
|
||||
|
testIdentity := &iam_pb.Identity{ |
||||
|
Name: "testuser", |
||||
|
Actions: []string{"Read", "Write"}, |
||||
|
Account: &iam_pb.Account{ |
||||
|
Id: "123456789012", |
||||
|
DisplayName: "Test User", |
||||
|
EmailAddress: "test@example.com", |
||||
|
}, |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "AKIAIOSFODNN7EXAMPLE", |
||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Test the interface methods exist (compile-time check)
|
||||
|
config := util.GetViper() |
||||
|
cm, err := NewCredentialManager(storeName, config, "test.") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create credential manager: %v", err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Test LoadConfiguration
|
||||
|
_, err = cm.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("LoadConfiguration failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test CreateUser
|
||||
|
err = cm.CreateUser(ctx, testIdentity) |
||||
|
if err != nil { |
||||
|
t.Fatalf("CreateUser failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test GetUser
|
||||
|
user, err := cm.GetUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("GetUser failed: %v", err) |
||||
|
} |
||||
|
if user.Name != "testuser" { |
||||
|
t.Errorf("Expected user name 'testuser', got %s", user.Name) |
||||
|
} |
||||
|
|
||||
|
// Test ListUsers
|
||||
|
users, err := cm.ListUsers(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("ListUsers failed: %v", err) |
||||
|
} |
||||
|
if len(users) != 1 || users[0] != "testuser" { |
||||
|
t.Errorf("Expected ['testuser'], got %v", users) |
||||
|
} |
||||
|
|
||||
|
// Test GetUserByAccessKey
|
||||
|
userByKey, err := cm.GetUserByAccessKey(ctx, "AKIAIOSFODNN7EXAMPLE") |
||||
|
if err != nil { |
||||
|
t.Fatalf("GetUserByAccessKey failed: %v", err) |
||||
|
} |
||||
|
if userByKey.Name != "testuser" { |
||||
|
t.Errorf("Expected user name 'testuser', got %s", userByKey.Name) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestCredentialManagerIntegration(t *testing.T) { |
||||
|
// Skip if no stores are registered
|
||||
|
if len(Stores) == 0 { |
||||
|
t.Skip("No credential stores registered - for full testing see test/ package") |
||||
|
} |
||||
|
|
||||
|
// Test with the first available store
|
||||
|
availableStores := GetAvailableStores() |
||||
|
if len(availableStores) == 0 { |
||||
|
t.Skip("No stores available for testing") |
||||
|
} |
||||
|
|
||||
|
storeName := availableStores[0] |
||||
|
config := util.GetViper() |
||||
|
cm, err := NewCredentialManager(storeName, config, "test.") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create credential manager: %v", err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Test complete workflow
|
||||
|
user1 := &iam_pb.Identity{ |
||||
|
Name: "user1", |
||||
|
Actions: []string{"Read"}, |
||||
|
Account: &iam_pb.Account{ |
||||
|
Id: "111111111111", |
||||
|
DisplayName: "User One", |
||||
|
EmailAddress: "user1@example.com", |
||||
|
}, |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "AKIAUSER1", |
||||
|
SecretKey: "secret1", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
user2 := &iam_pb.Identity{ |
||||
|
Name: "user2", |
||||
|
Actions: []string{"Write"}, |
||||
|
Account: &iam_pb.Account{ |
||||
|
Id: "222222222222", |
||||
|
DisplayName: "User Two", |
||||
|
EmailAddress: "user2@example.com", |
||||
|
}, |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "AKIAUSER2", |
||||
|
SecretKey: "secret2", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Create users
|
||||
|
err = cm.CreateUser(ctx, user1) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create user1: %v", err) |
||||
|
} |
||||
|
|
||||
|
err = cm.CreateUser(ctx, user2) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create user2: %v", err) |
||||
|
} |
||||
|
|
||||
|
// List users
|
||||
|
users, err := cm.ListUsers(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to list users: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(users) != 2 { |
||||
|
t.Errorf("Expected 2 users, got %d", len(users)) |
||||
|
} |
||||
|
|
||||
|
// Test access key lookup
|
||||
|
foundUser, err := cm.GetUserByAccessKey(ctx, "AKIAUSER1") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get user by access key: %v", err) |
||||
|
} |
||||
|
if foundUser.Name != "user1" { |
||||
|
t.Errorf("Expected user1, got %s", foundUser.Name) |
||||
|
} |
||||
|
|
||||
|
// Delete user
|
||||
|
err = cm.DeleteUser(ctx, "user1") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to delete user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user is deleted
|
||||
|
_, err = cm.GetUser(ctx, "user1") |
||||
|
if err != ErrUserNotFound { |
||||
|
t.Errorf("Expected ErrUserNotFound, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Clean up
|
||||
|
err = cm.DeleteUser(ctx, "user2") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to delete user2: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestErrorTypes tests that the custom error types are defined correctly
|
||||
|
func TestErrorTypes(t *testing.T) { |
||||
|
// Test that error types are defined
|
||||
|
if ErrUserNotFound == nil { |
||||
|
t.Error("ErrUserNotFound should be defined") |
||||
|
} |
||||
|
if ErrUserAlreadyExists == nil { |
||||
|
t.Error("ErrUserAlreadyExists should be defined") |
||||
|
} |
||||
|
if ErrAccessKeyNotFound == nil { |
||||
|
t.Error("ErrAccessKeyNotFound should be defined") |
||||
|
} |
||||
|
|
||||
|
// Test error messages
|
||||
|
if ErrUserNotFound.Error() != "user not found" { |
||||
|
t.Errorf("Expected 'user not found', got '%s'", ErrUserNotFound.Error()) |
||||
|
} |
||||
|
if ErrUserAlreadyExists.Error() != "user already exists" { |
||||
|
t.Errorf("Expected 'user already exists', got '%s'", ErrUserAlreadyExists.Error()) |
||||
|
} |
||||
|
if ErrAccessKeyNotFound.Error() != "access key not found" { |
||||
|
t.Errorf("Expected 'access key not found', got '%s'", ErrAccessKeyNotFound.Error()) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// TestGetAvailableStores tests the store discovery function
|
||||
|
func TestGetAvailableStores(t *testing.T) { |
||||
|
stores := GetAvailableStores() |
||||
|
if len(stores) == 0 { |
||||
|
t.Skip("No stores available for testing") |
||||
|
} |
||||
|
|
||||
|
// Convert to strings for comparison
|
||||
|
storeNames := make([]string, len(stores)) |
||||
|
for i, store := range stores { |
||||
|
storeNames[i] = string(store) |
||||
|
} |
||||
|
|
||||
|
t.Logf("Available stores: %v (count: %d)", storeNames, len(storeNames)) |
||||
|
|
||||
|
// We expect at least memory and filer_etc stores to be available
|
||||
|
expectedStores := []string{string(StoreTypeFilerEtc), string(StoreTypeMemory)} |
||||
|
|
||||
|
// Add SQLite and PostgreSQL if they're available (build tags dependent)
|
||||
|
for _, storeName := range storeNames { |
||||
|
found := false |
||||
|
for _, expected := range append(expectedStores, string(StoreTypeSQLite), string(StoreTypePostgres)) { |
||||
|
if storeName == expected { |
||||
|
found = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !found { |
||||
|
t.Errorf("Unexpected store found: %s", storeName) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Test that filer_etc store is always available
|
||||
|
filerEtcStoreFound := false |
||||
|
memoryStoreFound := false |
||||
|
for _, storeName := range storeNames { |
||||
|
if storeName == string(StoreTypeFilerEtc) { |
||||
|
filerEtcStoreFound = true |
||||
|
} |
||||
|
if storeName == string(StoreTypeMemory) { |
||||
|
memoryStoreFound = true |
||||
|
} |
||||
|
} |
||||
|
if !filerEtcStoreFound { |
||||
|
t.Error("FilerEtc store should always be available") |
||||
|
} |
||||
|
if !memoryStoreFound { |
||||
|
t.Error("Memory store should always be available") |
||||
|
} |
||||
|
} |
@ -0,0 +1,235 @@ |
|||||
|
package filer_etc |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"context" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/filer" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
"google.golang.org/grpc" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
credential.Stores = append(credential.Stores, &FilerEtcStore{}) |
||||
|
} |
||||
|
|
||||
|
// FilerEtcStore implements CredentialStore using SeaweedFS filer for storage
|
||||
|
type FilerEtcStore struct { |
||||
|
filerGrpcAddress string |
||||
|
grpcDialOption grpc.DialOption |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) GetName() credential.CredentialStoreTypeName { |
||||
|
return credential.StoreTypeFilerEtc |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) Initialize(configuration util.Configuration, prefix string) error { |
||||
|
// Handle nil configuration gracefully
|
||||
|
if configuration != nil { |
||||
|
store.filerGrpcAddress = configuration.GetString(prefix + "filer") |
||||
|
// TODO: Initialize grpcDialOption based on configuration
|
||||
|
} |
||||
|
// Note: filerGrpcAddress can be set later via SetFilerClient method
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// SetFilerClient sets the filer client details for the file store
|
||||
|
func (store *FilerEtcStore) SetFilerClient(filerAddress string, grpcDialOption grpc.DialOption) { |
||||
|
store.filerGrpcAddress = filerAddress |
||||
|
store.grpcDialOption = grpcDialOption |
||||
|
} |
||||
|
|
||||
|
// withFilerClient executes a function with a filer client
|
||||
|
func (store *FilerEtcStore) withFilerClient(fn func(client filer_pb.SeaweedFilerClient) error) error { |
||||
|
if store.filerGrpcAddress == "" { |
||||
|
return fmt.Errorf("filer address not configured") |
||||
|
} |
||||
|
|
||||
|
// Use the pb.WithGrpcFilerClient helper similar to existing code
|
||||
|
return pb.WithGrpcFilerClient(false, 0, pb.ServerAddress(store.filerGrpcAddress), store.grpcDialOption, fn) |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
||||
|
s3cfg := &iam_pb.S3ApiConfiguration{} |
||||
|
|
||||
|
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
||||
|
var buf bytes.Buffer |
||||
|
if err := filer.ReadEntry(nil, client, filer.IamConfigDirectory, filer.IamIdentityFile, &buf); err != nil { |
||||
|
if err != filer_pb.ErrNotFound { |
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
if buf.Len() > 0 { |
||||
|
return filer.ParseS3ConfigurationFromBytes(buf.Bytes(), s3cfg) |
||||
|
} |
||||
|
return nil |
||||
|
}) |
||||
|
|
||||
|
return s3cfg, err |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
||||
|
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error { |
||||
|
var buf bytes.Buffer |
||||
|
if err := filer.ProtoToText(&buf, config); err != nil { |
||||
|
return fmt.Errorf("failed to marshal configuration: %v", err) |
||||
|
} |
||||
|
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamIdentityFile, buf.Bytes()) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
||||
|
// Load existing configuration
|
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Check if user already exists
|
||||
|
for _, existingIdentity := range config.Identities { |
||||
|
if existingIdentity.Name == identity.Name { |
||||
|
return credential.ErrUserAlreadyExists |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Add new identity
|
||||
|
config.Identities = append(config.Identities, identity) |
||||
|
|
||||
|
// Save configuration
|
||||
|
return store.SaveConfiguration(ctx, config) |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
for _, identity := range config.Identities { |
||||
|
if identity.Name == username { |
||||
|
return identity, nil |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Find and update the user
|
||||
|
for i, existingIdentity := range config.Identities { |
||||
|
if existingIdentity.Name == username { |
||||
|
config.Identities[i] = identity |
||||
|
return store.SaveConfiguration(ctx, config) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) DeleteUser(ctx context.Context, username string) error { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Find and remove the user
|
||||
|
for i, identity := range config.Identities { |
||||
|
if identity.Name == username { |
||||
|
config.Identities = append(config.Identities[:i], config.Identities[i+1:]...) |
||||
|
return store.SaveConfiguration(ctx, config) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
var usernames []string |
||||
|
for _, identity := range config.Identities { |
||||
|
usernames = append(usernames, identity.Name) |
||||
|
} |
||||
|
|
||||
|
return usernames, nil |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
for _, identity := range config.Identities { |
||||
|
for _, credential := range identity.Credentials { |
||||
|
if credential.AccessKey == accessKey { |
||||
|
return identity, nil |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return nil, credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Find the user and add the credential
|
||||
|
for _, identity := range config.Identities { |
||||
|
if identity.Name == username { |
||||
|
// Check if access key already exists
|
||||
|
for _, existingCred := range identity.Credentials { |
||||
|
if existingCred.AccessKey == cred.AccessKey { |
||||
|
return fmt.Errorf("access key %s already exists", cred.AccessKey) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
identity.Credentials = append(identity.Credentials, cred) |
||||
|
return store.SaveConfiguration(ctx, config) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
||||
|
config, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Find the user and remove the credential
|
||||
|
for _, identity := range config.Identities { |
||||
|
if identity.Name == username { |
||||
|
for i, cred := range identity.Credentials { |
||||
|
if cred.AccessKey == accessKey { |
||||
|
identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...) |
||||
|
return store.SaveConfiguration(ctx, config) |
||||
|
} |
||||
|
} |
||||
|
return credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
func (store *FilerEtcStore) Shutdown() { |
||||
|
// No cleanup needed for file store
|
||||
|
} |
@ -0,0 +1,373 @@ |
|||||
|
package memory |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"sync" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
credential.Stores = append(credential.Stores, &MemoryStore{}) |
||||
|
} |
||||
|
|
||||
|
// MemoryStore implements CredentialStore using in-memory storage
|
||||
|
// This is primarily intended for testing purposes
|
||||
|
type MemoryStore struct { |
||||
|
mu sync.RWMutex |
||||
|
users map[string]*iam_pb.Identity // username -> identity
|
||||
|
accessKeys map[string]string // access_key -> username
|
||||
|
initialized bool |
||||
|
} |
||||
|
|
||||
|
func (store *MemoryStore) GetName() credential.CredentialStoreTypeName { |
||||
|
return credential.StoreTypeMemory |
||||
|
} |
||||
|
|
||||
|
func (store *MemoryStore) Initialize(configuration util.Configuration, prefix string) error { |
||||
|
store.mu.Lock() |
||||
|
defer store.mu.Unlock() |
||||
|
|
||||
|
if store.initialized { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
store.users = make(map[string]*iam_pb.Identity) |
||||
|
store.accessKeys = make(map[string]string) |
||||
|
store.initialized = true |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
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 { |
||||
|
// Deep copy the identity to avoid mutation issues
|
||||
|
identityCopy := store.deepCopyIdentity(user) |
||||
|
config.Identities = append(config.Identities, identityCopy) |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
// Add all identities
|
||||
|
for _, identity := range config.Identities { |
||||
|
// Deep copy to avoid mutation issues
|
||||
|
identityCopy := store.deepCopyIdentity(identity) |
||||
|
store.users[identity.Name] = identityCopy |
||||
|
|
||||
|
// Index access keys
|
||||
|
for _, credential := range identity.Credentials { |
||||
|
store.accessKeys[credential.AccessKey] = identity.Name |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
// 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) |
||||
|
store.users[username] = identityCopy |
||||
|
|
||||
|
// Re-index access keys
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
store.accessKeys[cred.AccessKey] = username |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
func (store *MemoryStore) Shutdown() { |
||||
|
store.mu.Lock() |
||||
|
defer store.mu.Unlock() |
||||
|
|
||||
|
// Clear all data
|
||||
|
store.users = nil |
||||
|
store.accessKeys = nil |
||||
|
store.initialized = false |
||||
|
} |
||||
|
|
||||
|
// 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, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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, |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return © |
||||
|
} |
||||
|
|
||||
|
// Reset clears all data in the store (useful for testing)
|
||||
|
func (store *MemoryStore) Reset() { |
||||
|
store.mu.Lock() |
||||
|
defer store.mu.Unlock() |
||||
|
|
||||
|
if store.initialized { |
||||
|
store.users = make(map[string]*iam_pb.Identity) |
||||
|
store.accessKeys = make(map[string]string) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// GetUserCount returns the number of users in the store (useful for testing)
|
||||
|
func (store *MemoryStore) GetUserCount() int { |
||||
|
store.mu.RLock() |
||||
|
defer store.mu.RUnlock() |
||||
|
|
||||
|
return len(store.users) |
||||
|
} |
||||
|
|
||||
|
// GetAccessKeyCount returns the number of access keys in the store (useful for testing)
|
||||
|
func (store *MemoryStore) GetAccessKeyCount() int { |
||||
|
store.mu.RLock() |
||||
|
defer store.mu.RUnlock() |
||||
|
|
||||
|
return len(store.accessKeys) |
||||
|
} |
@ -0,0 +1,315 @@ |
|||||
|
package memory |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
func TestMemoryStore(t *testing.T) { |
||||
|
store := &MemoryStore{} |
||||
|
|
||||
|
// Test initialization
|
||||
|
config := util.GetViper() |
||||
|
if err := store.Initialize(config, "credential."); err != nil { |
||||
|
t.Fatalf("Failed to initialize store: %v", err) |
||||
|
} |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Test creating a user
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: "testuser", |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "access123", |
||||
|
SecretKey: "secret123", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
if err := store.CreateUser(ctx, identity); err != nil { |
||||
|
t.Fatalf("Failed to create user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test getting user
|
||||
|
retrievedUser, err := store.GetUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get user: %v", err) |
||||
|
} |
||||
|
|
||||
|
if retrievedUser.Name != "testuser" { |
||||
|
t.Errorf("Expected username 'testuser', got '%s'", retrievedUser.Name) |
||||
|
} |
||||
|
|
||||
|
if len(retrievedUser.Credentials) != 1 { |
||||
|
t.Errorf("Expected 1 credential, got %d", len(retrievedUser.Credentials)) |
||||
|
} |
||||
|
|
||||
|
// Test getting user by access key
|
||||
|
userByAccessKey, err := store.GetUserByAccessKey(ctx, "access123") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get user by access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
if userByAccessKey.Name != "testuser" { |
||||
|
t.Errorf("Expected username 'testuser', got '%s'", userByAccessKey.Name) |
||||
|
} |
||||
|
|
||||
|
// Test listing users
|
||||
|
users, err := store.ListUsers(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to list users: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(users) != 1 || users[0] != "testuser" { |
||||
|
t.Errorf("Expected ['testuser'], got %v", users) |
||||
|
} |
||||
|
|
||||
|
// Test creating access key
|
||||
|
newCred := &iam_pb.Credential{ |
||||
|
AccessKey: "access456", |
||||
|
SecretKey: "secret456", |
||||
|
} |
||||
|
|
||||
|
if err := store.CreateAccessKey(ctx, "testuser", newCred); err != nil { |
||||
|
t.Fatalf("Failed to create access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user now has 2 credentials
|
||||
|
updatedUser, err := store.GetUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get updated user: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(updatedUser.Credentials) != 2 { |
||||
|
t.Errorf("Expected 2 credentials, got %d", len(updatedUser.Credentials)) |
||||
|
} |
||||
|
|
||||
|
// Test deleting access key
|
||||
|
if err := store.DeleteAccessKey(ctx, "testuser", "access456"); err != nil { |
||||
|
t.Fatalf("Failed to delete access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user now has 1 credential again
|
||||
|
finalUser, err := store.GetUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get final user: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(finalUser.Credentials) != 1 { |
||||
|
t.Errorf("Expected 1 credential, got %d", len(finalUser.Credentials)) |
||||
|
} |
||||
|
|
||||
|
// Test deleting user
|
||||
|
if err := store.DeleteUser(ctx, "testuser"); err != nil { |
||||
|
t.Fatalf("Failed to delete user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user is gone
|
||||
|
_, err = store.GetUser(ctx, "testuser") |
||||
|
if err != credential.ErrUserNotFound { |
||||
|
t.Errorf("Expected ErrUserNotFound, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test error cases
|
||||
|
if err := store.CreateUser(ctx, identity); err != nil { |
||||
|
t.Fatalf("Failed to create user for error tests: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Try to create duplicate user
|
||||
|
if err := store.CreateUser(ctx, identity); err != credential.ErrUserAlreadyExists { |
||||
|
t.Errorf("Expected ErrUserAlreadyExists, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Try to get non-existent user
|
||||
|
_, err = store.GetUser(ctx, "nonexistent") |
||||
|
if err != credential.ErrUserNotFound { |
||||
|
t.Errorf("Expected ErrUserNotFound, got %v", err) |
||||
|
} |
||||
|
|
||||
|
// Try to get user by non-existent access key
|
||||
|
_, err = store.GetUserByAccessKey(ctx, "nonexistent") |
||||
|
if err != credential.ErrAccessKeyNotFound { |
||||
|
t.Errorf("Expected ErrAccessKeyNotFound, got %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMemoryStoreConcurrency(t *testing.T) { |
||||
|
store := &MemoryStore{} |
||||
|
config := util.GetViper() |
||||
|
if err := store.Initialize(config, "credential."); err != nil { |
||||
|
t.Fatalf("Failed to initialize store: %v", err) |
||||
|
} |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Test concurrent access
|
||||
|
done := make(chan bool, 10) |
||||
|
for i := 0; i < 10; i++ { |
||||
|
go func(i int) { |
||||
|
defer func() { done <- true }() |
||||
|
|
||||
|
username := fmt.Sprintf("user%d", i) |
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: username, |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: fmt.Sprintf("access%d", i), |
||||
|
SecretKey: fmt.Sprintf("secret%d", i), |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
if err := store.CreateUser(ctx, identity); err != nil { |
||||
|
t.Errorf("Failed to create user %s: %v", username, err) |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if _, err := store.GetUser(ctx, username); err != nil { |
||||
|
t.Errorf("Failed to get user %s: %v", username, err) |
||||
|
return |
||||
|
} |
||||
|
}(i) |
||||
|
} |
||||
|
|
||||
|
// Wait for all goroutines to complete
|
||||
|
for i := 0; i < 10; i++ { |
||||
|
<-done |
||||
|
} |
||||
|
|
||||
|
// Verify all users were created
|
||||
|
users, err := store.ListUsers(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to list users: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(users) != 10 { |
||||
|
t.Errorf("Expected 10 users, got %d", len(users)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMemoryStoreReset(t *testing.T) { |
||||
|
store := &MemoryStore{} |
||||
|
config := util.GetViper() |
||||
|
if err := store.Initialize(config, "credential."); err != nil { |
||||
|
t.Fatalf("Failed to initialize store: %v", err) |
||||
|
} |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create a user
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: "testuser", |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "access123", |
||||
|
SecretKey: "secret123", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
if err := store.CreateUser(ctx, identity); err != nil { |
||||
|
t.Fatalf("Failed to create user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user exists
|
||||
|
if store.GetUserCount() != 1 { |
||||
|
t.Errorf("Expected 1 user, got %d", store.GetUserCount()) |
||||
|
} |
||||
|
|
||||
|
if store.GetAccessKeyCount() != 1 { |
||||
|
t.Errorf("Expected 1 access key, got %d", store.GetAccessKeyCount()) |
||||
|
} |
||||
|
|
||||
|
// Reset the store
|
||||
|
store.Reset() |
||||
|
|
||||
|
// Verify store is empty
|
||||
|
if store.GetUserCount() != 0 { |
||||
|
t.Errorf("Expected 0 users after reset, got %d", store.GetUserCount()) |
||||
|
} |
||||
|
|
||||
|
if store.GetAccessKeyCount() != 0 { |
||||
|
t.Errorf("Expected 0 access keys after reset, got %d", store.GetAccessKeyCount()) |
||||
|
} |
||||
|
|
||||
|
// Verify user is gone
|
||||
|
_, err := store.GetUser(ctx, "testuser") |
||||
|
if err != credential.ErrUserNotFound { |
||||
|
t.Errorf("Expected ErrUserNotFound after reset, got %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func TestMemoryStoreConfigurationSaveLoad(t *testing.T) { |
||||
|
store := &MemoryStore{} |
||||
|
config := util.GetViper() |
||||
|
if err := store.Initialize(config, "credential."); err != nil { |
||||
|
t.Fatalf("Failed to initialize store: %v", err) |
||||
|
} |
||||
|
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create initial configuration
|
||||
|
originalConfig := &iam_pb.S3ApiConfiguration{ |
||||
|
Identities: []*iam_pb.Identity{ |
||||
|
{ |
||||
|
Name: "user1", |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "access1", |
||||
|
SecretKey: "secret1", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
Name: "user2", |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "access2", |
||||
|
SecretKey: "secret2", |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Save configuration
|
||||
|
if err := store.SaveConfiguration(ctx, originalConfig); err != nil { |
||||
|
t.Fatalf("Failed to save configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Load configuration
|
||||
|
loadedConfig, err := store.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify configuration matches
|
||||
|
if len(loadedConfig.Identities) != 2 { |
||||
|
t.Errorf("Expected 2 identities, got %d", len(loadedConfig.Identities)) |
||||
|
} |
||||
|
|
||||
|
// Check users exist
|
||||
|
user1, err := store.GetUser(ctx, "user1") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get user1: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(user1.Credentials) != 1 || user1.Credentials[0].AccessKey != "access1" { |
||||
|
t.Errorf("User1 credentials not correct: %+v", user1.Credentials) |
||||
|
} |
||||
|
|
||||
|
user2, err := store.GetUser(ctx, "user2") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to get user2: %v", err) |
||||
|
} |
||||
|
|
||||
|
if len(user2.Credentials) != 1 || user2.Credentials[0].AccessKey != "access2" { |
||||
|
t.Errorf("User2 credentials not correct: %+v", user2.Credentials) |
||||
|
} |
||||
|
} |
@ -0,0 +1,221 @@ |
|||||
|
package credential |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"fmt" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/glog" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
) |
||||
|
|
||||
|
// MigrateCredentials migrates credentials from one store to another
|
||||
|
func MigrateCredentials(fromStoreName, toStoreName CredentialStoreTypeName, configuration util.Configuration, fromPrefix, toPrefix string) error { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create source credential manager
|
||||
|
fromCM, err := NewCredentialManager(fromStoreName, configuration, fromPrefix) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to create source credential manager (%s): %v", fromStoreName, err) |
||||
|
} |
||||
|
defer fromCM.Shutdown() |
||||
|
|
||||
|
// Create destination credential manager
|
||||
|
toCM, err := NewCredentialManager(toStoreName, configuration, toPrefix) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to create destination credential manager (%s): %v", toStoreName, err) |
||||
|
} |
||||
|
defer toCM.Shutdown() |
||||
|
|
||||
|
// Load configuration from source
|
||||
|
glog.Infof("Loading configuration from %s store...", fromStoreName) |
||||
|
config, err := fromCM.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration from source store: %v", err) |
||||
|
} |
||||
|
|
||||
|
if config == nil || len(config.Identities) == 0 { |
||||
|
glog.Info("No identities found in source store") |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
glog.Infof("Found %d identities in source store", len(config.Identities)) |
||||
|
|
||||
|
// Migrate each identity
|
||||
|
var migrated, failed int |
||||
|
for _, identity := range config.Identities { |
||||
|
glog.V(1).Infof("Migrating user: %s", identity.Name) |
||||
|
|
||||
|
// Check if user already exists in destination
|
||||
|
existingUser, err := toCM.GetUser(ctx, identity.Name) |
||||
|
if err != nil && err != ErrUserNotFound { |
||||
|
glog.Errorf("Failed to check if user %s exists in destination: %v", identity.Name, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if existingUser != nil { |
||||
|
glog.Warningf("User %s already exists in destination store, skipping", identity.Name) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Create user in destination
|
||||
|
err = toCM.CreateUser(ctx, identity) |
||||
|
if err != nil { |
||||
|
glog.Errorf("Failed to create user %s in destination store: %v", identity.Name, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
migrated++ |
||||
|
glog.V(1).Infof("Successfully migrated user: %s", identity.Name) |
||||
|
} |
||||
|
|
||||
|
glog.Infof("Migration completed: %d migrated, %d failed", migrated, failed) |
||||
|
|
||||
|
if failed > 0 { |
||||
|
return fmt.Errorf("migration completed with %d failures", failed) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// ExportCredentials exports credentials from a store to a configuration
|
||||
|
func ExportCredentials(storeName CredentialStoreTypeName, configuration util.Configuration, prefix string) (*iam_pb.S3ApiConfiguration, error) { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create credential manager
|
||||
|
cm, err := NewCredentialManager(storeName, configuration, prefix) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to create credential manager (%s): %v", storeName, err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
// Load configuration
|
||||
|
config, err := cm.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
return config, nil |
||||
|
} |
||||
|
|
||||
|
// ImportCredentials imports credentials from a configuration to a store
|
||||
|
func ImportCredentials(storeName CredentialStoreTypeName, configuration util.Configuration, prefix string, config *iam_pb.S3ApiConfiguration) error { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create credential manager
|
||||
|
cm, err := NewCredentialManager(storeName, configuration, prefix) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to create credential manager (%s): %v", storeName, err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
// Import each identity
|
||||
|
var imported, failed int |
||||
|
for _, identity := range config.Identities { |
||||
|
glog.V(1).Infof("Importing user: %s", identity.Name) |
||||
|
|
||||
|
// Check if user already exists
|
||||
|
existingUser, err := cm.GetUser(ctx, identity.Name) |
||||
|
if err != nil && err != ErrUserNotFound { |
||||
|
glog.Errorf("Failed to check if user %s exists: %v", identity.Name, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if existingUser != nil { |
||||
|
glog.Warningf("User %s already exists, skipping", identity.Name) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Create user
|
||||
|
err = cm.CreateUser(ctx, identity) |
||||
|
if err != nil { |
||||
|
glog.Errorf("Failed to create user %s: %v", identity.Name, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
imported++ |
||||
|
glog.V(1).Infof("Successfully imported user: %s", identity.Name) |
||||
|
} |
||||
|
|
||||
|
glog.Infof("Import completed: %d imported, %d failed", imported, failed) |
||||
|
|
||||
|
if failed > 0 { |
||||
|
return fmt.Errorf("import completed with %d failures", failed) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
// ValidateCredentials validates that all credentials in a store are accessible
|
||||
|
func ValidateCredentials(storeName CredentialStoreTypeName, configuration util.Configuration, prefix string) error { |
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create credential manager
|
||||
|
cm, err := NewCredentialManager(storeName, configuration, prefix) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to create credential manager (%s): %v", storeName, err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
// Load configuration
|
||||
|
config, err := cm.LoadConfiguration(ctx) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to load configuration: %v", err) |
||||
|
} |
||||
|
|
||||
|
if config == nil || len(config.Identities) == 0 { |
||||
|
glog.Info("No identities found in store") |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
glog.Infof("Validating %d identities...", len(config.Identities)) |
||||
|
|
||||
|
// Validate each identity
|
||||
|
var validated, failed int |
||||
|
for _, identity := range config.Identities { |
||||
|
// Check if user can be retrieved
|
||||
|
user, err := cm.GetUser(ctx, identity.Name) |
||||
|
if err != nil { |
||||
|
glog.Errorf("Failed to retrieve user %s: %v", identity.Name, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if user == nil { |
||||
|
glog.Errorf("User %s not found", identity.Name) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
// Validate access keys
|
||||
|
for _, credential := range identity.Credentials { |
||||
|
accessKeyUser, err := cm.GetUserByAccessKey(ctx, credential.AccessKey) |
||||
|
if err != nil { |
||||
|
glog.Errorf("Failed to retrieve user by access key %s: %v", credential.AccessKey, err) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
if accessKeyUser == nil || accessKeyUser.Name != identity.Name { |
||||
|
glog.Errorf("Access key %s does not map to correct user %s", credential.AccessKey, identity.Name) |
||||
|
failed++ |
||||
|
continue |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
validated++ |
||||
|
glog.V(1).Infof("Successfully validated user: %s", identity.Name) |
||||
|
} |
||||
|
|
||||
|
glog.Infof("Validation completed: %d validated, %d failed", validated, failed) |
||||
|
|
||||
|
if failed > 0 { |
||||
|
return fmt.Errorf("validation completed with %d failures", failed) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
@ -0,0 +1,570 @@ |
|||||
|
package postgres |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"database/sql" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"time" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
|
||||
|
_ "github.com/lib/pq" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
credential.Stores = append(credential.Stores, &PostgresStore{}) |
||||
|
} |
||||
|
|
||||
|
// PostgresStore implements CredentialStore using PostgreSQL
|
||||
|
type PostgresStore struct { |
||||
|
db *sql.DB |
||||
|
configured bool |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) GetName() credential.CredentialStoreTypeName { |
||||
|
return credential.StoreTypePostgres |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) Initialize(configuration util.Configuration, prefix string) error { |
||||
|
if store.configured { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
hostname := configuration.GetString(prefix + "hostname") |
||||
|
port := configuration.GetInt(prefix + "port") |
||||
|
username := configuration.GetString(prefix + "username") |
||||
|
password := configuration.GetString(prefix + "password") |
||||
|
database := configuration.GetString(prefix + "database") |
||||
|
schema := configuration.GetString(prefix + "schema") |
||||
|
sslmode := configuration.GetString(prefix + "sslmode") |
||||
|
|
||||
|
// Set defaults
|
||||
|
if hostname == "" { |
||||
|
hostname = "localhost" |
||||
|
} |
||||
|
if port == 0 { |
||||
|
port = 5432 |
||||
|
} |
||||
|
if schema == "" { |
||||
|
schema = "public" |
||||
|
} |
||||
|
if sslmode == "" { |
||||
|
sslmode = "disable" |
||||
|
} |
||||
|
|
||||
|
// Build connection string
|
||||
|
connStr := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s search_path=%s", |
||||
|
hostname, port, username, password, database, sslmode, schema) |
||||
|
|
||||
|
db, err := sql.Open("postgres", connStr) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to open database: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test connection
|
||||
|
if err := db.Ping(); err != nil { |
||||
|
db.Close() |
||||
|
return fmt.Errorf("failed to ping database: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Set connection pool settings
|
||||
|
db.SetMaxOpenConns(25) |
||||
|
db.SetMaxIdleConns(5) |
||||
|
db.SetConnMaxLifetime(5 * time.Minute) |
||||
|
|
||||
|
store.db = db |
||||
|
|
||||
|
// Create tables if they don't exist
|
||||
|
if err := store.createTables(); err != nil { |
||||
|
db.Close() |
||||
|
return fmt.Errorf("failed to create tables: %v", err) |
||||
|
} |
||||
|
|
||||
|
store.configured = true |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) createTables() error { |
||||
|
// Create users table
|
||||
|
usersTable := ` |
||||
|
CREATE TABLE IF NOT EXISTS users ( |
||||
|
username VARCHAR(255) PRIMARY KEY, |
||||
|
email VARCHAR(255), |
||||
|
account_data JSONB, |
||||
|
actions JSONB, |
||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
||||
|
); |
||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); |
||||
|
` |
||||
|
|
||||
|
// Create credentials table
|
||||
|
credentialsTable := ` |
||||
|
CREATE TABLE IF NOT EXISTS credentials ( |
||||
|
id SERIAL PRIMARY KEY, |
||||
|
username VARCHAR(255) REFERENCES users(username) ON DELETE CASCADE, |
||||
|
access_key VARCHAR(255) UNIQUE NOT NULL, |
||||
|
secret_key VARCHAR(255) NOT NULL, |
||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP |
||||
|
); |
||||
|
CREATE INDEX IF NOT EXISTS idx_credentials_username ON credentials(username); |
||||
|
CREATE INDEX IF NOT EXISTS idx_credentials_access_key ON credentials(access_key); |
||||
|
` |
||||
|
|
||||
|
// Execute table creation
|
||||
|
if _, err := store.db.Exec(usersTable); err != nil { |
||||
|
return fmt.Errorf("failed to create users table: %v", err) |
||||
|
} |
||||
|
|
||||
|
if _, err := store.db.Exec(credentialsTable); err != nil { |
||||
|
return fmt.Errorf("failed to create credentials table: %v", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
config := &iam_pb.S3ApiConfiguration{} |
||||
|
|
||||
|
// Query all users
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users") |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query users: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
for rows.Next() { |
||||
|
var username, email string |
||||
|
var accountDataJSON, actionsJSON []byte |
||||
|
|
||||
|
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan user row: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: username, |
||||
|
} |
||||
|
|
||||
|
// Parse account data
|
||||
|
if len(accountDataJSON) > 0 { |
||||
|
if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal account data for user %s: %v", username, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Parse actions
|
||||
|
if len(actionsJSON) > 0 { |
||||
|
if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal actions 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 { |
||||
|
return nil, fmt.Errorf("failed to query credentials for user %s: %v", username, err) |
||||
|
} |
||||
|
|
||||
|
for credRows.Next() { |
||||
|
var accessKey, secretKey string |
||||
|
if err := credRows.Scan(&accessKey, &secretKey); err != nil { |
||||
|
credRows.Close() |
||||
|
return nil, fmt.Errorf("failed to scan credential row for user %s: %v", username, err) |
||||
|
} |
||||
|
|
||||
|
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
||||
|
AccessKey: accessKey, |
||||
|
SecretKey: secretKey, |
||||
|
}) |
||||
|
} |
||||
|
credRows.Close() |
||||
|
|
||||
|
config.Identities = append(config.Identities, identity) |
||||
|
} |
||||
|
|
||||
|
return config, nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Clear existing data
|
||||
|
if _, err := tx.ExecContext(ctx, "DELETE FROM credentials"); err != nil { |
||||
|
return fmt.Errorf("failed to clear credentials: %v", err) |
||||
|
} |
||||
|
if _, err := tx.ExecContext(ctx, "DELETE FROM users"); err != nil { |
||||
|
return fmt.Errorf("failed to clear users: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert all identities
|
||||
|
for _, identity := range config.Identities { |
||||
|
// Marshal account data
|
||||
|
var accountDataJSON []byte |
||||
|
if identity.Account != nil { |
||||
|
accountDataJSON, err = json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data for user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON []byte |
||||
|
if identity.Actions != nil { |
||||
|
actionsJSON, err = json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions 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) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
|
||||
|
// Insert credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err := tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
||||
|
identity.Name, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential for user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Check if user already exists
|
||||
|
var count int |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", identity.Name).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count > 0 { |
||||
|
return credential.ErrUserAlreadyExists |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Marshal account data
|
||||
|
var accountDataJSON []byte |
||||
|
if identity.Account != nil { |
||||
|
accountDataJSON, err = json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON []byte |
||||
|
if identity.Actions != nil { |
||||
|
actionsJSON, err = json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Insert user
|
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO users (username, email, account_data, actions) VALUES ($1, $2, $3, $4)", |
||||
|
identity.Name, "", accountDataJSON, actionsJSON) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
||||
|
identity.Name, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
var email string |
||||
|
var accountDataJSON, actionsJSON []byte |
||||
|
|
||||
|
err := store.db.QueryRowContext(ctx, |
||||
|
"SELECT email, account_data, actions FROM users WHERE username = $1", |
||||
|
username).Scan(&email, &accountDataJSON, &actionsJSON) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, credential.ErrUserNotFound |
||||
|
} |
||||
|
return nil, fmt.Errorf("failed to query user: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: username, |
||||
|
} |
||||
|
|
||||
|
// Parse account data
|
||||
|
if len(accountDataJSON) > 0 { |
||||
|
if err := json.Unmarshal(accountDataJSON, &identity.Account); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal account data: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Parse actions
|
||||
|
if len(actionsJSON) > 0 { |
||||
|
if err := json.Unmarshal(actionsJSON, &identity.Actions); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal actions: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Query credentials
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = $1", username) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query credentials: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
for rows.Next() { |
||||
|
var accessKey, secretKey string |
||||
|
if err := rows.Scan(&accessKey, &secretKey); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan credential: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
||||
|
AccessKey: accessKey, |
||||
|
SecretKey: secretKey, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return identity, nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
// Marshal account data
|
||||
|
var accountDataJSON []byte |
||||
|
if identity.Account != nil { |
||||
|
accountDataJSON, err = json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON []byte |
||||
|
if identity.Actions != nil { |
||||
|
actionsJSON, err = json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions: %v", 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) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to update user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Delete existing credentials
|
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM credentials WHERE username = $1", username) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete existing credentials: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert new credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
||||
|
username, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) DeleteUser(ctx context.Context, username string) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
result, err := store.db.ExecContext(ctx, "DELETE FROM users WHERE username = $1", username) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete user: %v", err) |
||||
|
} |
||||
|
|
||||
|
rowsAffected, err := result.RowsAffected() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to get rows affected: %v", err) |
||||
|
} |
||||
|
|
||||
|
if rowsAffected == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) ListUsers(ctx context.Context) ([]string, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT username FROM users ORDER BY username") |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query users: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
var usernames []string |
||||
|
for rows.Next() { |
||||
|
var username string |
||||
|
if err := rows.Scan(&username); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan username: %v", err) |
||||
|
} |
||||
|
usernames = append(usernames, username) |
||||
|
} |
||||
|
|
||||
|
return usernames, nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
var username string |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT username FROM credentials WHERE access_key = $1", accessKey).Scan(&username) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
return nil, fmt.Errorf("failed to query access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
return store.GetUser(ctx, username) |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
// Insert credential
|
||||
|
_, err = store.db.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES ($1, $2, $3)", |
||||
|
username, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
result, err := store.db.ExecContext(ctx, |
||||
|
"DELETE FROM credentials WHERE username = $1 AND access_key = $2", |
||||
|
username, accessKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
rowsAffected, err := result.RowsAffected() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to get rows affected: %v", err) |
||||
|
} |
||||
|
|
||||
|
if rowsAffected == 0 { |
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err = store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = $1", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
return credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *PostgresStore) Shutdown() { |
||||
|
if store.db != nil { |
||||
|
store.db.Close() |
||||
|
store.db = nil |
||||
|
} |
||||
|
store.configured = false |
||||
|
} |
@ -0,0 +1,557 @@ |
|||||
|
package sqlite |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"database/sql" |
||||
|
"encoding/json" |
||||
|
"fmt" |
||||
|
"os" |
||||
|
"path/filepath" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
|
||||
|
_ "modernc.org/sqlite" |
||||
|
) |
||||
|
|
||||
|
func init() { |
||||
|
credential.Stores = append(credential.Stores, &SqliteStore{}) |
||||
|
} |
||||
|
|
||||
|
// SqliteStore implements CredentialStore using SQLite
|
||||
|
type SqliteStore struct { |
||||
|
db *sql.DB |
||||
|
configured bool |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) GetName() credential.CredentialStoreTypeName { |
||||
|
return credential.StoreTypeSQLite |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) Initialize(configuration util.Configuration, prefix string) error { |
||||
|
if store.configured { |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
dbFile := configuration.GetString(prefix + "dbFile") |
||||
|
if dbFile == "" { |
||||
|
dbFile = "seaweedfs_credentials.db" |
||||
|
} |
||||
|
|
||||
|
// Create directory if it doesn't exist
|
||||
|
dir := filepath.Dir(dbFile) |
||||
|
if dir != "." { |
||||
|
if err := os.MkdirAll(dir, 0755); err != nil { |
||||
|
return fmt.Errorf("failed to create directory %s: %v", dir, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
db, err := sql.Open("sqlite", dbFile) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to open database: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test connection
|
||||
|
if err := db.Ping(); err != nil { |
||||
|
db.Close() |
||||
|
return fmt.Errorf("failed to ping database: %v", err) |
||||
|
} |
||||
|
|
||||
|
store.db = db |
||||
|
|
||||
|
// Create tables if they don't exist
|
||||
|
if err := store.createTables(); err != nil { |
||||
|
db.Close() |
||||
|
return fmt.Errorf("failed to create tables: %v", err) |
||||
|
} |
||||
|
|
||||
|
store.configured = true |
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) createTables() error { |
||||
|
// Create users table
|
||||
|
usersTable := ` |
||||
|
CREATE TABLE IF NOT EXISTS users ( |
||||
|
username TEXT PRIMARY KEY, |
||||
|
email TEXT, |
||||
|
account_data TEXT, |
||||
|
actions TEXT, |
||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP, |
||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP |
||||
|
); |
||||
|
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); |
||||
|
` |
||||
|
|
||||
|
// Create credentials table
|
||||
|
credentialsTable := ` |
||||
|
CREATE TABLE IF NOT EXISTS credentials ( |
||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT, |
||||
|
username TEXT REFERENCES users(username) ON DELETE CASCADE, |
||||
|
access_key TEXT UNIQUE NOT NULL, |
||||
|
secret_key TEXT NOT NULL, |
||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP |
||||
|
); |
||||
|
CREATE INDEX IF NOT EXISTS idx_credentials_username ON credentials(username); |
||||
|
CREATE INDEX IF NOT EXISTS idx_credentials_access_key ON credentials(access_key); |
||||
|
` |
||||
|
|
||||
|
// Execute table creation
|
||||
|
if _, err := store.db.Exec(usersTable); err != nil { |
||||
|
return fmt.Errorf("failed to create users table: %v", err) |
||||
|
} |
||||
|
|
||||
|
if _, err := store.db.Exec(credentialsTable); err != nil { |
||||
|
return fmt.Errorf("failed to create credentials table: %v", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
config := &iam_pb.S3ApiConfiguration{} |
||||
|
|
||||
|
// Query all users
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT username, email, account_data, actions FROM users") |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query users: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
for rows.Next() { |
||||
|
var username, email, accountDataJSON, actionsJSON string |
||||
|
|
||||
|
if err := rows.Scan(&username, &email, &accountDataJSON, &actionsJSON); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan user row: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: username, |
||||
|
} |
||||
|
|
||||
|
// Parse account data
|
||||
|
if accountDataJSON != "" { |
||||
|
if err := json.Unmarshal([]byte(accountDataJSON), &identity.Account); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal account data for user %s: %v", username, err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Parse actions
|
||||
|
if actionsJSON != "" { |
||||
|
if err := json.Unmarshal([]byte(actionsJSON), &identity.Actions); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal actions 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 = ?", username) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query credentials for user %s: %v", username, err) |
||||
|
} |
||||
|
|
||||
|
for credRows.Next() { |
||||
|
var accessKey, secretKey string |
||||
|
if err := credRows.Scan(&accessKey, &secretKey); err != nil { |
||||
|
credRows.Close() |
||||
|
return nil, fmt.Errorf("failed to scan credential row for user %s: %v", username, err) |
||||
|
} |
||||
|
|
||||
|
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
||||
|
AccessKey: accessKey, |
||||
|
SecretKey: secretKey, |
||||
|
}) |
||||
|
} |
||||
|
credRows.Close() |
||||
|
|
||||
|
config.Identities = append(config.Identities, identity) |
||||
|
} |
||||
|
|
||||
|
return config, nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Clear existing data
|
||||
|
if _, err := tx.ExecContext(ctx, "DELETE FROM credentials"); err != nil { |
||||
|
return fmt.Errorf("failed to clear credentials: %v", err) |
||||
|
} |
||||
|
if _, err := tx.ExecContext(ctx, "DELETE FROM users"); err != nil { |
||||
|
return fmt.Errorf("failed to clear users: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert all identities
|
||||
|
for _, identity := range config.Identities { |
||||
|
// Marshal account data
|
||||
|
var accountDataJSON string |
||||
|
if identity.Account != nil { |
||||
|
data, err := json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data for user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
accountDataJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON string |
||||
|
if identity.Actions != nil { |
||||
|
data, err := json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions for user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
actionsJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Insert user
|
||||
|
_, err := tx.ExecContext(ctx, |
||||
|
"INSERT INTO users (username, email, account_data, actions) VALUES (?, ?, ?, ?)", |
||||
|
identity.Name, "", accountDataJSON, actionsJSON) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
|
||||
|
// Insert credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err := tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES (?, ?, ?)", |
||||
|
identity.Name, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential for user %s: %v", identity.Name, err) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Check if user already exists
|
||||
|
var count int |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = ?", identity.Name).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count > 0 { |
||||
|
return credential.ErrUserAlreadyExists |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Marshal account data
|
||||
|
var accountDataJSON string |
||||
|
if identity.Account != nil { |
||||
|
data, err := json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data: %v", err) |
||||
|
} |
||||
|
accountDataJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON string |
||||
|
if identity.Actions != nil { |
||||
|
data, err := json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions: %v", err) |
||||
|
} |
||||
|
actionsJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Insert user
|
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO users (username, email, account_data, actions) VALUES (?, ?, ?, ?)", |
||||
|
identity.Name, "", accountDataJSON, actionsJSON) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES (?, ?, ?)", |
||||
|
identity.Name, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
var email, accountDataJSON, actionsJSON string |
||||
|
|
||||
|
err := store.db.QueryRowContext(ctx, |
||||
|
"SELECT email, account_data, actions FROM users WHERE username = ?", |
||||
|
username).Scan(&email, &accountDataJSON, &actionsJSON) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, credential.ErrUserNotFound |
||||
|
} |
||||
|
return nil, fmt.Errorf("failed to query user: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity := &iam_pb.Identity{ |
||||
|
Name: username, |
||||
|
} |
||||
|
|
||||
|
// Parse account data
|
||||
|
if accountDataJSON != "" { |
||||
|
if err := json.Unmarshal([]byte(accountDataJSON), &identity.Account); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal account data: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Parse actions
|
||||
|
if actionsJSON != "" { |
||||
|
if err := json.Unmarshal([]byte(actionsJSON), &identity.Actions); err != nil { |
||||
|
return nil, fmt.Errorf("failed to unmarshal actions: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Query credentials
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT access_key, secret_key FROM credentials WHERE username = ?", username) |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query credentials: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
for rows.Next() { |
||||
|
var accessKey, secretKey string |
||||
|
if err := rows.Scan(&accessKey, &secretKey); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan credential: %v", err) |
||||
|
} |
||||
|
|
||||
|
identity.Credentials = append(identity.Credentials, &iam_pb.Credential{ |
||||
|
AccessKey: accessKey, |
||||
|
SecretKey: secretKey, |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return identity, nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Start transaction
|
||||
|
tx, err := store.db.BeginTx(ctx, nil) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to begin transaction: %v", err) |
||||
|
} |
||||
|
defer tx.Rollback() |
||||
|
|
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err = tx.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
// Marshal account data
|
||||
|
var accountDataJSON string |
||||
|
if identity.Account != nil { |
||||
|
data, err := json.Marshal(identity.Account) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal account data: %v", err) |
||||
|
} |
||||
|
accountDataJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Marshal actions
|
||||
|
var actionsJSON string |
||||
|
if identity.Actions != nil { |
||||
|
data, err := json.Marshal(identity.Actions) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to marshal actions: %v", err) |
||||
|
} |
||||
|
actionsJSON = string(data) |
||||
|
} |
||||
|
|
||||
|
// Update user
|
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"UPDATE users SET email = ?, account_data = ?, actions = ?, updated_at = CURRENT_TIMESTAMP WHERE username = ?", |
||||
|
"", accountDataJSON, actionsJSON, username) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to update user: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Delete existing credentials
|
||||
|
_, err = tx.ExecContext(ctx, "DELETE FROM credentials WHERE username = ?", username) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete existing credentials: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Insert new credentials
|
||||
|
for _, cred := range identity.Credentials { |
||||
|
_, err = tx.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES (?, ?, ?)", |
||||
|
username, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return tx.Commit() |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) DeleteUser(ctx context.Context, username string) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
result, err := store.db.ExecContext(ctx, "DELETE FROM users WHERE username = ?", username) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete user: %v", err) |
||||
|
} |
||||
|
|
||||
|
rowsAffected, err := result.RowsAffected() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to get rows affected: %v", err) |
||||
|
} |
||||
|
|
||||
|
if rowsAffected == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) ListUsers(ctx context.Context) ([]string, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
rows, err := store.db.QueryContext(ctx, "SELECT username FROM users ORDER BY username") |
||||
|
if err != nil { |
||||
|
return nil, fmt.Errorf("failed to query users: %v", err) |
||||
|
} |
||||
|
defer rows.Close() |
||||
|
|
||||
|
var usernames []string |
||||
|
for rows.Next() { |
||||
|
var username string |
||||
|
if err := rows.Scan(&username); err != nil { |
||||
|
return nil, fmt.Errorf("failed to scan username: %v", err) |
||||
|
} |
||||
|
usernames = append(usernames, username) |
||||
|
} |
||||
|
|
||||
|
return usernames, nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) { |
||||
|
if !store.configured { |
||||
|
return nil, fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
var username string |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT username FROM credentials WHERE access_key = ?", accessKey).Scan(&username) |
||||
|
if err != nil { |
||||
|
if err == sql.ErrNoRows { |
||||
|
return nil, credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
return nil, fmt.Errorf("failed to query access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
return store.GetUser(ctx, username) |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err := store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
|
||||
|
// Insert credential
|
||||
|
_, err = store.db.ExecContext(ctx, |
||||
|
"INSERT INTO credentials (username, access_key, secret_key) VALUES (?, ?, ?)", |
||||
|
username, cred.AccessKey, cred.SecretKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to insert credential: %v", err) |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error { |
||||
|
if !store.configured { |
||||
|
return fmt.Errorf("store not configured") |
||||
|
} |
||||
|
|
||||
|
result, err := store.db.ExecContext(ctx, |
||||
|
"DELETE FROM credentials WHERE username = ? AND access_key = ?", |
||||
|
username, accessKey) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to delete access key: %v", err) |
||||
|
} |
||||
|
|
||||
|
rowsAffected, err := result.RowsAffected() |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to get rows affected: %v", err) |
||||
|
} |
||||
|
|
||||
|
if rowsAffected == 0 { |
||||
|
// Check if user exists
|
||||
|
var count int |
||||
|
err = store.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE username = ?", username).Scan(&count) |
||||
|
if err != nil { |
||||
|
return fmt.Errorf("failed to check user existence: %v", err) |
||||
|
} |
||||
|
if count == 0 { |
||||
|
return credential.ErrUserNotFound |
||||
|
} |
||||
|
return credential.ErrAccessKeyNotFound |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func (store *SqliteStore) Shutdown() { |
||||
|
if store.db != nil { |
||||
|
store.db.Close() |
||||
|
store.db = nil |
||||
|
} |
||||
|
store.configured = false |
||||
|
} |
@ -0,0 +1,122 @@ |
|||||
|
package test |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"testing" |
||||
|
|
||||
|
"github.com/seaweedfs/seaweedfs/weed/credential" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb" |
||||
|
"github.com/seaweedfs/seaweedfs/weed/util" |
||||
|
|
||||
|
// Import all store implementations to register them
|
||||
|
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc" |
||||
|
_ "github.com/seaweedfs/seaweedfs/weed/credential/memory" |
||||
|
_ "github.com/seaweedfs/seaweedfs/weed/credential/postgres" |
||||
|
_ "github.com/seaweedfs/seaweedfs/weed/credential/sqlite" |
||||
|
) |
||||
|
|
||||
|
func TestStoreRegistration(t *testing.T) { |
||||
|
// Test that stores are registered
|
||||
|
storeNames := credential.GetAvailableStores() |
||||
|
if len(storeNames) == 0 { |
||||
|
t.Fatal("No credential stores registered") |
||||
|
} |
||||
|
|
||||
|
expectedStores := []string{string(credential.StoreTypeFilerEtc), string(credential.StoreTypeMemory), string(credential.StoreTypeSQLite), string(credential.StoreTypePostgres)} |
||||
|
|
||||
|
// Verify all expected stores are present
|
||||
|
for _, expected := range expectedStores { |
||||
|
found := false |
||||
|
for _, storeName := range storeNames { |
||||
|
if string(storeName) == expected { |
||||
|
found = true |
||||
|
break |
||||
|
} |
||||
|
} |
||||
|
if !found { |
||||
|
t.Errorf("Expected store not found: %s", expected) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
t.Logf("Available stores: %v", storeNames) |
||||
|
} |
||||
|
|
||||
|
func TestMemoryStoreIntegration(t *testing.T) { |
||||
|
// Test creating credential manager with memory store
|
||||
|
config := util.GetViper() |
||||
|
cm, err := credential.NewCredentialManager(credential.StoreTypeMemory, config, "test.") |
||||
|
if err != nil { |
||||
|
t.Fatalf("Failed to create memory credential manager: %v", err) |
||||
|
} |
||||
|
defer cm.Shutdown() |
||||
|
|
||||
|
// Test that the store is of the correct type
|
||||
|
if cm.GetStore().GetName() != credential.StoreTypeMemory { |
||||
|
t.Errorf("Expected memory store, got %s", cm.GetStore().GetName()) |
||||
|
} |
||||
|
|
||||
|
// Test basic operations
|
||||
|
ctx := context.Background() |
||||
|
|
||||
|
// Create test user
|
||||
|
testUser := &iam_pb.Identity{ |
||||
|
Name: "testuser", |
||||
|
Actions: []string{"Read", "Write"}, |
||||
|
Account: &iam_pb.Account{ |
||||
|
Id: "123456789012", |
||||
|
DisplayName: "Test User", |
||||
|
EmailAddress: "test@example.com", |
||||
|
}, |
||||
|
Credentials: []*iam_pb.Credential{ |
||||
|
{ |
||||
|
AccessKey: "AKIAIOSFODNN7EXAMPLE", |
||||
|
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", |
||||
|
}, |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
// Test CreateUser
|
||||
|
err = cm.CreateUser(ctx, testUser) |
||||
|
if err != nil { |
||||
|
t.Fatalf("CreateUser failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Test GetUser
|
||||
|
user, err := cm.GetUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("GetUser failed: %v", err) |
||||
|
} |
||||
|
if user.Name != "testuser" { |
||||
|
t.Errorf("Expected user name 'testuser', got %s", user.Name) |
||||
|
} |
||||
|
|
||||
|
// Test ListUsers
|
||||
|
users, err := cm.ListUsers(ctx) |
||||
|
if err != nil { |
||||
|
t.Fatalf("ListUsers failed: %v", err) |
||||
|
} |
||||
|
if len(users) != 1 || users[0] != "testuser" { |
||||
|
t.Errorf("Expected ['testuser'], got %v", users) |
||||
|
} |
||||
|
|
||||
|
// Test GetUserByAccessKey
|
||||
|
userByKey, err := cm.GetUserByAccessKey(ctx, "AKIAIOSFODNN7EXAMPLE") |
||||
|
if err != nil { |
||||
|
t.Fatalf("GetUserByAccessKey failed: %v", err) |
||||
|
} |
||||
|
if userByKey.Name != "testuser" { |
||||
|
t.Errorf("Expected user name 'testuser', got %s", userByKey.Name) |
||||
|
} |
||||
|
|
||||
|
// Test DeleteUser
|
||||
|
err = cm.DeleteUser(ctx, "testuser") |
||||
|
if err != nil { |
||||
|
t.Fatalf("DeleteUser failed: %v", err) |
||||
|
} |
||||
|
|
||||
|
// Verify user was deleted
|
||||
|
_, err = cm.GetUser(ctx, "testuser") |
||||
|
if err != credential.ErrUserNotFound { |
||||
|
t.Errorf("Expected ErrUserNotFound, got %v", err) |
||||
|
} |
||||
|
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue