Files
seaweedfs/weed/credential/credential_store.go
T
Chris LuandGitHub ff6f9fd90a iam: honor configured credential store for IAM API policies and propagate to S3 caches (fixes #9518) (#9522)
* iamapi: route managed policies through credential manager (fixes #9518)

CreatePolicy via the IAM API wrote straight to the filer
/etc/iam/policies.json, ignoring any non-filer credential store. When
credential.postgres was configured, policies created via the IAM API
landed only in the filer while the Admin UI wrote to postgres,
producing a split-brain where ListPolicies/GetPolicy never saw the
Admin UI's policies and vice versa.

GetPolicies/PutPolicies on IamS3ApiConfigure now load managed policies
from credentialManager and persist Create/Update/Delete as a delta
against the store. Inline user/group policies still live in the legacy
policies.json file (no credential-store API for them yet). Pre-existing
managed policies in the legacy file are merged on read so deployments
don't lose data, and re-persisted to the store on the next write so
the legacy file is drained over time.

* credential: route IAM API inline policies through credential manager

Extends the #9518 fix to user-inline and group-inline policies so the
IAM API never writes the legacy /etc/iam/policies.json bundle directly.
The previous patch only routed managed policies; this one finishes the
job for the other two policy types.

- Add GroupInlinePolicyStore + GroupInlinePoliciesLoader optional
  interfaces, mirroring the existing user-inline ones, and matching
  Put/Get/Delete/List/LoadAll wrappers on CredentialManager.
- Implement group-inline storage in memory (new map), filer_etc (new
  field on PoliciesCollection, reusing the legacy file under policyMu),
  and postgres (new group_inline_policies table with ON DELETE CASCADE
  off the groups FK).
- Wire the new methods through PropagatingCredentialStore so wrapped
  stores still delegate correctly.
- IamS3ApiConfigure.PutPolicies now applies managed + user-inline +
  group-inline as deltas through the credential manager; the legacy
  /etc/iam/policies.json file is never written when a credential
  manager is wired up. GetPolicies still reads the legacy bundle once
  as a fallback so unmigrated data is picked up and re-persisted into
  the store on the next write.

* credential: propagate SaveConfiguration writes to running S3 caches

Postgres (and any non-filer) credential stores never fired the S3 IAM
cache invalidation path on bulk identity / group updates. The
PropagatingCredentialStore had explicit Put/Remove handlers for
single-entity calls (CreateUser, PutPolicy, etc.) but inherited
SaveConfiguration unchanged from the embedded store, so the bulk path
the IAM API takes at the end of every handler was silent. Inline-policy
changes recompute identity.Actions and persist via SaveConfiguration,
so until restart the cached Actions on each S3 server stayed stale and
authorization decisions used the pre-change view.

Override SaveConfiguration to snapshot the prior user / group lists,
delegate the save, then fan out PutIdentity / PutGroup for what's in
the new config and RemoveIdentity / RemoveGroup for what got pruned.
Reuses the existing SeaweedS3IamCache RPCs, no protobuf changes.

* iamapi: drain legacy policies.json after authoritative credential-store writes

Review pointed out a resurrection bug: GetPolicies still reads
/etc/iam/policies.json as a one-way migration fallback, but PutPolicies
in the credential-manager path never wrote that file, so legacy-only
entries reappeared on the next read even after the IAM API "deleted"
them. PutPolicies now overwrites the bundle with an empty {} after a
successful credential-store write, unless the store is filer_etc
(which owns the bundle as its own inline-policy backing — clearing it
would wipe filer_etc's data). Also wraps the filer read, JSON
unmarshal, and marshal errors with context per the other review
comments.
2026-05-17 13:15:27 -07:00

188 lines
8.6 KiB
Go

package credential
import (
"context"
"errors"
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"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")
ErrServiceAccountNotFound = errors.New("service account not found")
ErrPolicyNotFound = errors.New("policy not found")
ErrPolicyAlreadyAttached = errors.New("policy already attached")
ErrPolicyNotAttached = errors.New("policy not attached to user")
ErrGroupNotFound = errors.New("group not found")
ErrGroupAlreadyExists = errors.New("group already exists")
ErrGroupNotEmpty = errors.New("group is not empty")
ErrUserNotInGroup = errors.New("user is not a member of the group")
)
// 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"
StoreTypeGrpc CredentialStoreTypeName = "grpc"
)
// 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
// Policy Management
GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error)
// ListPolicyNames returns the names of all policies
ListPolicyNames(ctx context.Context) ([]string, error)
// PutPolicy creates or replaces a policy document.
PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
DeletePolicy(ctx context.Context, name string) error
GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error)
// Service Account Management
CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error
UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error
DeleteServiceAccount(ctx context.Context, id string) error
GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error)
ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error)
GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error)
// User Policy Attachment Management
// AttachUserPolicy attaches a managed policy to a user by policy name
AttachUserPolicy(ctx context.Context, username string, policyName string) error
// DetachUserPolicy detaches a managed policy from a user
DetachUserPolicy(ctx context.Context, username string, policyName string) error
// ListAttachedUserPolicies returns the list of policy names attached to a user
ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error)
// Group Management
CreateGroup(ctx context.Context, group *iam_pb.Group) error
GetGroup(ctx context.Context, groupName string) (*iam_pb.Group, error)
DeleteGroup(ctx context.Context, groupName string) error
ListGroups(ctx context.Context) ([]string, error)
UpdateGroup(ctx context.Context, group *iam_pb.Group) 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"`
}
// PolicyManager interface for managing IAM policies
type PolicyManager interface {
GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error)
CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error
DeletePolicy(ctx context.Context, name string) error
GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error)
}
// InlinePolicyStore is an optional interface for credential stores that support
// per-user inline policy storage. Stores that implement this interface preserve
// the exact policy document submitted via PutUserPolicy, enabling lossless
// round-trips through GetUserPolicy.
type InlinePolicyStore interface {
PutUserInlinePolicy(ctx context.Context, userName, policyName string, document policy_engine.PolicyDocument) error
GetUserInlinePolicy(ctx context.Context, userName, policyName string) (*policy_engine.PolicyDocument, error)
DeleteUserInlinePolicy(ctx context.Context, userName, policyName string) error
ListUserInlinePolicies(ctx context.Context, userName string) ([]string, error)
}
// GroupInlinePolicyStore is an optional interface for credential stores that
// support per-group inline policy storage. Mirrors InlinePolicyStore so the
// IAM API can persist group-attached inline policies without writing a
// shared filer file.
type GroupInlinePolicyStore interface {
PutGroupInlinePolicy(ctx context.Context, groupName, policyName string, document policy_engine.PolicyDocument) error
GetGroupInlinePolicy(ctx context.Context, groupName, policyName string) (*policy_engine.PolicyDocument, error)
DeleteGroupInlinePolicy(ctx context.Context, groupName, policyName string) error
ListGroupInlinePolicies(ctx context.Context, groupName string) ([]string, error)
}
// InlinePoliciesLoader is an optional interface for credential stores that can
// bulk-load every user inline policy in a single round trip.
type InlinePoliciesLoader interface {
LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error)
}
// GroupInlinePoliciesLoader is an optional interface for credential stores
// that can bulk-load every group inline policy in a single round trip.
type GroupInlinePoliciesLoader interface {
LoadGroupInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error)
}
// UserRenamer is an optional interface for credential stores that can
// atomically rename a user along with all rows that reference the old
// username (credentials, inline policies, etc). Backends with referential
// integrity (e.g. PostgreSQL with ON DELETE CASCADE foreign keys on
// user_inline_policies) MUST implement this so the IAM rename path can
// move the dependents within a single transaction; copying them via
// per-row Put / Get / Delete calls would violate the FK at statement
// time because the new user row does not yet exist.
type UserRenamer interface {
RenameUser(ctx context.Context, oldName, newName string) error
}
// Stores holds all available credential store implementations
var Stores []CredentialStore