mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-12 17:40:43 +02:00
* fix(s3): preserve exact policy document in embedded IAM PutUserPolicy/GetUserPolicy (#9008) The embedded IAM implementation (used when IAM requests go through the S3 gateway) discarded the original policy document on PutUserPolicy, storing only the lossy ident.Actions representation. GetUserPolicy then reconstructed the document from these coarse-grained actions, producing wildcard-expanded actions (s3:GetObject → s3:Get*), duplicates, and collapsed resources (array → single string). PR #9009 fixed this in the standalone IAM server (weed/iamapi/) but the embedded IAM (weed/s3api/) — which is the code path most users hit — had the same bugs. Changes: - Add InlinePolicyStore optional interface to credential store, with implementations for FilerEtcStore (uses existing PoliciesCollection), MemoryStore, and PropagatingCredentialStore. - Embedded IAM PutUserPolicy now persists the original policy document via CredentialManager.PutUserInlinePolicy for lossless round-trips. - Embedded IAM GetUserPolicy first tries the stored inline policy; only falls back to lossy reconstruction from ident.Actions when no stored document exists (e.g. policies created before this fix). - Fix the fallback reconstruction: add action deduplication and preserve resource paths verbatim (no more spurious /* appending). - Update DeleteUserPolicy/ListUserPolicies to use stored inline policies. * fix(s3): address PR review feedback for embedded IAM inline policies - Validate PolicyName is non-empty in PutUserPolicy and DeleteUserPolicy - Add recomputeActions() to aggregate ident.Actions from ALL stored inline policies on put/delete, fixing the issue where a second PutUserPolicy would overwrite the first policy's enforcement - Log errors from GetUserInlinePolicy in the GetUserPolicy fallback instead of silently ignoring them - Add initialization guards to MemoryStore GetUserInlinePolicy and ListUserInlinePolicies for consistency with other read methods * fix(s3): make inline policy persistence fatal and propagate recompute errors Address second round of review feedback: - recomputeActions() now returns ([]string, error) so callers can distinguish store failures from "no stored policies" and abort the mutation on transient errors instead of silently falling back. - PutUserInlinePolicy and DeleteUserInlinePolicy failures are now fatal: the API call returns ServiceFailure instead of logging and continuing, keeping ident.Actions and stored policy state in sync. * chore: gofmt weed/s3api/iceberg/handlers_oauth.go Pre-existing formatting issue from #9017; fixes S3 Tables Format Check CI.
417 lines
15 KiB
Go
417 lines
15 KiB
Go
package credential
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
"github.com/seaweedfs/seaweedfs/weed/wdclient"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/protobuf/encoding/protojson"
|
|
)
|
|
|
|
// FilerAddressSetter is an interface for credential stores that need a dynamic filer address
|
|
type FilerAddressSetter interface {
|
|
SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption)
|
|
}
|
|
|
|
// CredentialManager manages user credentials using a configurable store
|
|
type CredentialManager struct {
|
|
Store CredentialStore
|
|
// staticMu protects staticIdentities and staticNames, which are written
|
|
// by SetStaticIdentities (startup + config reload) and read concurrently
|
|
// by LoadConfiguration, SaveConfiguration, GetStaticUsernames, and IsStaticIdentity.
|
|
staticMu sync.RWMutex
|
|
// staticIdentities holds identities loaded from a static config file (-s3.config).
|
|
// These are included in LoadConfiguration so that listing operations
|
|
// return all configured identities, not just dynamic ones from the store.
|
|
staticIdentities []*iam_pb.Identity
|
|
staticNames map[string]bool
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (cm *CredentialManager) SetMasterClient(masterClient *wdclient.MasterClient, grpcDialOption grpc.DialOption) {
|
|
cm.Store = NewPropagatingCredentialStore(cm.Store, masterClient, grpcDialOption)
|
|
}
|
|
|
|
// SetFilerAddressFunc sets the function to get the current filer address
|
|
func (cm *CredentialManager) SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption) {
|
|
if s, ok := cm.Store.(FilerAddressSetter); ok {
|
|
s.SetFilerAddressFunc(getFiler, grpcDialOption)
|
|
}
|
|
}
|
|
|
|
// GetStore returns the underlying credential store
|
|
func (cm *CredentialManager) GetStore() CredentialStore {
|
|
return cm.Store
|
|
}
|
|
|
|
// GetStoreName returns the name of the underlying credential store
|
|
func (cm *CredentialManager) GetStoreName() string {
|
|
if cm.Store != nil {
|
|
return string(cm.Store.GetName())
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// SetStaticIdentities registers identities loaded from a static config file.
|
|
// These identities are included in LoadConfiguration and ListUsers results
|
|
// but are never persisted to the dynamic store.
|
|
func (cm *CredentialManager) SetStaticIdentities(identities []*iam_pb.Identity) {
|
|
filtered := make([]*iam_pb.Identity, 0, len(identities))
|
|
names := make(map[string]bool, len(identities))
|
|
for _, ident := range identities {
|
|
if ident != nil {
|
|
filtered = append(filtered, ident)
|
|
names[ident.Name] = true
|
|
}
|
|
}
|
|
cm.staticMu.Lock()
|
|
cm.staticIdentities = filtered
|
|
cm.staticNames = names
|
|
cm.staticMu.Unlock()
|
|
}
|
|
|
|
// IsStaticIdentity returns true if the named identity was loaded from static config.
|
|
func (cm *CredentialManager) IsStaticIdentity(name string) bool {
|
|
cm.staticMu.RLock()
|
|
defer cm.staticMu.RUnlock()
|
|
return cm.staticNames[name]
|
|
}
|
|
|
|
// GetStaticIdentity returns the protobuf identity for a static user, or nil.
|
|
func (cm *CredentialManager) GetStaticIdentity(name string) *iam_pb.Identity {
|
|
cm.staticMu.RLock()
|
|
defer cm.staticMu.RUnlock()
|
|
for _, ident := range cm.staticIdentities {
|
|
if ident.Name == name {
|
|
return ident
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetStaticUsernames returns the names of all static identities.
|
|
func (cm *CredentialManager) GetStaticUsernames() []string {
|
|
cm.staticMu.RLock()
|
|
defer cm.staticMu.RUnlock()
|
|
names := make([]string, 0, len(cm.staticIdentities))
|
|
for _, ident := range cm.staticIdentities {
|
|
names = append(names, ident.Name)
|
|
}
|
|
return names
|
|
}
|
|
|
|
// LoadConfiguration loads the S3 API configuration from the store and merges
|
|
// in any static identities so that listing operations show all users.
|
|
func (cm *CredentialManager) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
|
|
config, err := cm.Store.LoadConfiguration(ctx)
|
|
if err != nil {
|
|
return config, err
|
|
}
|
|
// Merge static identities that are not already in the dynamic config
|
|
cm.staticMu.RLock()
|
|
staticIdents := cm.staticIdentities
|
|
cm.staticMu.RUnlock()
|
|
if len(staticIdents) > 0 {
|
|
dynamicNames := make(map[string]bool, len(config.Identities))
|
|
for _, ident := range config.Identities {
|
|
dynamicNames[ident.Name] = true
|
|
}
|
|
for _, si := range staticIdents {
|
|
if !dynamicNames[si.Name] {
|
|
config.Identities = append(config.Identities, si)
|
|
}
|
|
}
|
|
}
|
|
return config, nil
|
|
}
|
|
|
|
// SaveConfiguration saves the S3 API configuration.
|
|
// Static identities are filtered out before saving to the store.
|
|
// The caller's config is not mutated.
|
|
func (cm *CredentialManager) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
|
|
cm.staticMu.RLock()
|
|
staticNames := cm.staticNames
|
|
cm.staticMu.RUnlock()
|
|
if len(staticNames) > 0 {
|
|
var dynamicOnly []*iam_pb.Identity
|
|
for _, ident := range config.Identities {
|
|
if !staticNames[ident.Name] {
|
|
dynamicOnly = append(dynamicOnly, ident)
|
|
}
|
|
}
|
|
configCopy := *config
|
|
configCopy.Identities = dynamicOnly
|
|
return cm.Store.SaveConfiguration(ctx, &configCopy)
|
|
}
|
|
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 usernames from the dynamic store via cm.Store.ListUsers.
|
|
// On store error the error is returned directly without merging static entries.
|
|
// Static identities (cm.staticIdentities) are NOT included here because
|
|
// internal callers (e.g. DeletePolicy) look up each user in the store and
|
|
// would fail on non-existent static entries. External callers that need the
|
|
// full list should merge GetStaticUsernames separately.
|
|
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)
|
|
}
|
|
|
|
// GetPolicies returns all policies
|
|
func (cm *CredentialManager) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
|
|
return cm.Store.GetPolicies(ctx)
|
|
}
|
|
|
|
// PutPolicy creates or updates a policy
|
|
func (cm *CredentialManager) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
|
return cm.Store.PutPolicy(ctx, name, document)
|
|
}
|
|
|
|
// DeletePolicy removes a policy
|
|
func (cm *CredentialManager) DeletePolicy(ctx context.Context, name string) error {
|
|
return cm.Store.DeletePolicy(ctx, name)
|
|
}
|
|
|
|
// GetPolicy retrieves a policy by name
|
|
func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
|
|
return cm.Store.GetPolicy(ctx, name)
|
|
}
|
|
|
|
// ListPolicyNames returns the names of all policies
|
|
func (cm *CredentialManager) ListPolicyNames(ctx context.Context) ([]string, error) {
|
|
return cm.Store.ListPolicyNames(ctx)
|
|
}
|
|
|
|
// CreatePolicy creates a new policy (if supported by the store)
|
|
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
|
// Check if the store implements PolicyManager interface with CreatePolicy
|
|
if policyStore, ok := cm.Store.(PolicyManager); ok {
|
|
return policyStore.CreatePolicy(ctx, name, document)
|
|
}
|
|
// Fallback to PutPolicy for stores that only implement CredentialStore
|
|
return cm.Store.PutPolicy(ctx, name, document)
|
|
}
|
|
|
|
// UpdatePolicy updates an existing policy (if supported by the store)
|
|
func (cm *CredentialManager) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
|
// Check if the store implements PolicyManager interface with UpdatePolicy
|
|
if policyStore, ok := cm.Store.(PolicyManager); ok {
|
|
return policyStore.UpdatePolicy(ctx, name, document)
|
|
}
|
|
// Fallback to PutPolicy for stores that only implement CredentialStore
|
|
return cm.Store.PutPolicy(ctx, name, document)
|
|
}
|
|
|
|
// PutUserInlinePolicy stores a per-user inline policy document.
|
|
// Returns nil without error if the underlying store does not support inline policies.
|
|
func (cm *CredentialManager) PutUserInlinePolicy(ctx context.Context, userName, policyName string, document policy_engine.PolicyDocument) error {
|
|
if store, ok := cm.Store.(InlinePolicyStore); ok {
|
|
return store.PutUserInlinePolicy(ctx, userName, policyName, document)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetUserInlinePolicy retrieves a per-user inline policy document.
|
|
// Returns nil without error if the underlying store does not support inline policies.
|
|
func (cm *CredentialManager) GetUserInlinePolicy(ctx context.Context, userName, policyName string) (*policy_engine.PolicyDocument, error) {
|
|
if store, ok := cm.Store.(InlinePolicyStore); ok {
|
|
return store.GetUserInlinePolicy(ctx, userName, policyName)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// DeleteUserInlinePolicy removes a per-user inline policy document.
|
|
func (cm *CredentialManager) DeleteUserInlinePolicy(ctx context.Context, userName, policyName string) error {
|
|
if store, ok := cm.Store.(InlinePolicyStore); ok {
|
|
return store.DeleteUserInlinePolicy(ctx, userName, policyName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListUserInlinePolicies returns the names of all inline policies for a user.
|
|
func (cm *CredentialManager) ListUserInlinePolicies(ctx context.Context, userName string) ([]string, error) {
|
|
if store, ok := cm.Store.(InlinePolicyStore); ok {
|
|
return store.ListUserInlinePolicies(ctx, userName)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
// LoadS3ConfigFile reads a static S3 identity config file and registers
|
|
// the identities so they appear in LoadConfiguration and listing results.
|
|
func (cm *CredentialManager) LoadS3ConfigFile(path string) error {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("read %s: %w", path, err)
|
|
}
|
|
config := &iam_pb.S3ApiConfiguration{}
|
|
opts := protojson.UnmarshalOptions{DiscardUnknown: true, AllowPartial: true}
|
|
if err := opts.Unmarshal(content, config); err != nil {
|
|
return fmt.Errorf("parse %s: %w", path, err)
|
|
}
|
|
for _, ident := range config.Identities {
|
|
ident.IsStatic = true
|
|
}
|
|
cm.SetStaticIdentities(config.Identities)
|
|
glog.V(1).Infof("Loaded %d static identities from %s", len(config.Identities), path)
|
|
return nil
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// CreateServiceAccount creates a new service account
|
|
func (cm *CredentialManager) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
|
|
return cm.Store.CreateServiceAccount(ctx, sa)
|
|
}
|
|
|
|
// UpdateServiceAccount updates an existing service account
|
|
func (cm *CredentialManager) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
|
|
return cm.Store.UpdateServiceAccount(ctx, id, sa)
|
|
}
|
|
|
|
// DeleteServiceAccount removes a service account
|
|
func (cm *CredentialManager) DeleteServiceAccount(ctx context.Context, id string) error {
|
|
return cm.Store.DeleteServiceAccount(ctx, id)
|
|
}
|
|
|
|
// GetServiceAccount retrieves a service account by ID
|
|
func (cm *CredentialManager) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
|
|
return cm.Store.GetServiceAccount(ctx, id)
|
|
}
|
|
|
|
// ListServiceAccounts returns all service accounts
|
|
func (cm *CredentialManager) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
|
|
return cm.Store.ListServiceAccounts(ctx)
|
|
}
|
|
|
|
// AttachUserPolicy attaches a managed policy to a user
|
|
func (cm *CredentialManager) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
|
return cm.Store.AttachUserPolicy(ctx, username, policyName)
|
|
}
|
|
|
|
// DetachUserPolicy detaches a managed policy from a user
|
|
func (cm *CredentialManager) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
|
return cm.Store.DetachUserPolicy(ctx, username, policyName)
|
|
}
|
|
|
|
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
|
func (cm *CredentialManager) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
|
return cm.Store.ListAttachedUserPolicies(ctx, username)
|
|
}
|
|
|
|
// Group Management
|
|
|
|
func (cm *CredentialManager) CreateGroup(ctx context.Context, group *iam_pb.Group) error {
|
|
return cm.Store.CreateGroup(ctx, group)
|
|
}
|
|
|
|
func (cm *CredentialManager) GetGroup(ctx context.Context, groupName string) (*iam_pb.Group, error) {
|
|
return cm.Store.GetGroup(ctx, groupName)
|
|
}
|
|
|
|
func (cm *CredentialManager) DeleteGroup(ctx context.Context, groupName string) error {
|
|
return cm.Store.DeleteGroup(ctx, groupName)
|
|
}
|
|
|
|
func (cm *CredentialManager) ListGroups(ctx context.Context) ([]string, error) {
|
|
return cm.Store.ListGroups(ctx)
|
|
}
|
|
|
|
func (cm *CredentialManager) UpdateGroup(ctx context.Context, group *iam_pb.Group) error {
|
|
return cm.Store.UpdateGroup(ctx, group)
|
|
}
|