Files
seaweedfs/weed/admin/dash/user_management.go
T
Minsoo KimandGitHub 638f6ff433 admin: surface user inline policies in object store user details (#10013)
GetObjectStoreUserDetails only returned identity.PolicyNames (attached
managed policies) and omitted per-user inline policies. Inline policies are
stored separately from the identity record and are authoritative at S3
enforcement time (they take precedence over the legacy Actions list), so an
operator could not see what actually governed a user's access via the admin
API/UI.

Include inline policy names (via credentialManager.ListUserInlinePolicies) in
the returned PolicyNames. Adds a unit test using the memory credential store.
2026-06-18 22:20:56 -07:00

480 lines
14 KiB
Go

package dash
import (
"context"
"crypto/rand"
"errors"
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/iam"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
var (
ErrAccessKeyInUse = errors.New("access key already in use")
ErrUserNotFound = errors.New("user not found")
ErrInvalidInput = errors.New("invalid input")
)
// CreateObjectStoreUser creates a new user using the credential manager
func (s *AdminServer) CreateObjectStoreUser(req CreateUserRequest) (*ObjectStoreUser, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Create new identity
newIdentity := &iam_pb.Identity{
Name: req.Username,
Actions: req.Actions,
PolicyNames: req.PolicyNames,
}
// Add account if email is provided
if req.Email != "" {
newIdentity.Account = &iam_pb.Account{
Id: generateAccountId(),
DisplayName: req.Username,
EmailAddress: req.Email,
}
}
// Generate access key if requested
var accessKey, secretKey string
if req.GenerateKey {
accessKey = generateAccessKey()
secretKey = generateSecretKey()
newIdentity.Credentials = []*iam_pb.Credential{
{
AccessKey: accessKey,
SecretKey: secretKey,
},
}
}
// Create user using credential manager
err := s.credentialManager.CreateUser(ctx, newIdentity)
if err != nil {
if err == credential.ErrUserAlreadyExists {
return nil, fmt.Errorf("user %s already exists", req.Username)
}
return nil, fmt.Errorf("failed to create user: %w", err)
}
// Return created user
user := &ObjectStoreUser{
Username: req.Username,
Email: req.Email,
AccessKey: accessKey,
SecretKey: secretKey,
Permissions: req.Actions,
PolicyNames: req.PolicyNames,
}
return user, nil
}
// UpdateObjectStoreUser updates an existing user
func (s *AdminServer) UpdateObjectStoreUser(username string, req UpdateUserRequest) (*ObjectStoreUser, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Get existing user
identity, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return nil, fmt.Errorf("user %s not found", username)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
// Create updated identity
updatedIdentity := &iam_pb.Identity{
Name: identity.Name,
Account: identity.Account,
Credentials: identity.Credentials,
Actions: identity.Actions,
PolicyNames: identity.PolicyNames,
}
// Update actions if provided
if req.Actions != nil {
updatedIdentity.Actions = req.Actions
}
// Always update policy names when present in request (even if empty to allow clearing)
if req.PolicyNames != nil {
updatedIdentity.PolicyNames = req.PolicyNames
}
// Update email if provided
if req.Email != "" {
if updatedIdentity.Account == nil {
updatedIdentity.Account = &iam_pb.Account{
Id: generateAccountId(),
DisplayName: username,
}
}
updatedIdentity.Account.EmailAddress = req.Email
}
// Update user using credential manager
err = s.credentialManager.UpdateUser(ctx, username, updatedIdentity)
if err != nil {
return nil, fmt.Errorf("failed to update user: %w", err)
}
// Return updated user
user := &ObjectStoreUser{
Username: username,
Email: req.Email,
Permissions: updatedIdentity.Actions,
PolicyNames: updatedIdentity.PolicyNames,
}
// Get first access key for display
if len(updatedIdentity.Credentials) > 0 {
user.AccessKey = updatedIdentity.Credentials[0].AccessKey
user.SecretKey = updatedIdentity.Credentials[0].SecretKey
}
return user, nil
}
// DeleteObjectStoreUser deletes a user using the credential manager
func (s *AdminServer) DeleteObjectStoreUser(username string) error {
if s.credentialManager == nil {
return fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Delete user using credential manager
err := s.credentialManager.DeleteUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return fmt.Errorf("user %s not found", username)
}
return fmt.Errorf("failed to delete user: %w", err)
}
return nil
}
// GetObjectStoreUserDetails returns detailed information about a user
func (s *AdminServer) GetObjectStoreUserDetails(username string) (*UserDetails, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Get user using credential manager (resolves static users via filer gRPC)
identity, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return nil, fmt.Errorf("user %s not found", username)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
details := &UserDetails{
Username: username,
Actions: identity.Actions,
PolicyNames: identity.PolicyNames,
}
// Inline user policies are stored separately from the identity record and
// are authoritative at enforcement time (they take precedence over the
// legacy Actions list). Surface their names alongside attached policies so
// the admin reflects the user's actual permissions rather than only the
// (potentially overridden) Actions.
if inlinePolicyNames, err := s.credentialManager.ListUserInlinePolicies(ctx, username); err != nil {
glog.Warningf("failed to list inline policies for user %s: %v", username, err)
} else if len(inlinePolicyNames) > 0 {
details.PolicyNames = append(details.PolicyNames, inlinePolicyNames...)
}
// Set email from account if available
if identity.Account != nil {
details.Email = identity.Account.EmailAddress
}
// Look up groups the user belongs to
groupNames, err := s.credentialManager.ListGroups(ctx)
if err != nil {
return nil, fmt.Errorf("failed to list groups: %w", err)
}
for _, gName := range groupNames {
g, err := s.credentialManager.GetGroup(ctx, gName)
if err != nil {
return nil, fmt.Errorf("failed to get group %s: %w", gName, err)
}
for _, member := range g.Members {
if member == username {
details.Groups = append(details.Groups, gName)
break
}
}
}
// Convert credentials to access key info
for _, cred := range identity.Credentials {
details.AccessKeys = append(details.AccessKeys, AccessKeyInfo{
AccessKey: cred.AccessKey,
SecretKey: cred.SecretKey,
Status: cred.Status,
CreatedAt: time.Now().AddDate(0, -1, 0), // Mock creation date
})
}
return details, nil
}
// CreateAccessKey creates a new access key for a user
func (s *AdminServer) CreateAccessKey(username string, req *CreateAccessKeyRequest) (*AccessKeyInfo, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Check if user exists
_, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return nil, fmt.Errorf("user %s: %w", username, ErrUserNotFound)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
if req == nil {
req = &CreateAccessKeyRequest{}
}
// Validate provided keys (shared with the IAM API and embedded IAM paths).
if req.AccessKey != "" {
if err := iam.ValidateCallerSuppliedAccessKeyId(req.AccessKey); err != nil {
return nil, fmt.Errorf("%s: %w", err.Error(), ErrInvalidInput)
}
}
if req.SecretKey != "" {
if err := iam.ValidateCallerSuppliedSecretAccessKey(req.SecretKey); err != nil {
return nil, fmt.Errorf("%s: %w", err.Error(), ErrInvalidInput)
}
}
// Enforce the both-or-none rule to match the IAM API and embedded IAM
// paths — silently generating the missing half lets a caller end up
// with a credential they did not fully choose.
if (req.AccessKey != "") != (req.SecretKey != "") {
return nil, fmt.Errorf("access key and secret key must be supplied together: %w", ErrInvalidInput)
}
// Use provided keys or generate new ones
accessKey := req.AccessKey
secretKey := req.SecretKey
if accessKey == "" {
accessKey = generateAccessKey()
secretKey = generateSecretKey()
}
// Verify access key is globally unique
existingUser, err := s.credentialManager.GetUserByAccessKey(ctx, accessKey)
if existingUser != nil {
return nil, ErrAccessKeyInUse
}
if err != nil && !errors.Is(err, credential.ErrAccessKeyNotFound) && !isNotFoundError(err) {
return nil, fmt.Errorf("failed to check access key uniqueness: %w", err)
}
credential := &iam_pb.Credential{
AccessKey: accessKey,
SecretKey: secretKey,
Status: AccessKeyStatusActive,
}
// Create access key using credential manager
err = s.credentialManager.CreateAccessKey(ctx, username, credential)
if err != nil {
return nil, fmt.Errorf("failed to create access key: %w", err)
}
return &AccessKeyInfo{
AccessKey: accessKey,
SecretKey: secretKey,
Status: AccessKeyStatusActive,
CreatedAt: time.Now(),
}, nil
}
// DeleteAccessKey deletes an access key for a user
func (s *AdminServer) DeleteAccessKey(username, accessKeyId string) error {
if s.credentialManager == nil {
return fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Delete access key using credential manager
err := s.credentialManager.DeleteAccessKey(ctx, username, accessKeyId)
if err != nil {
if err == credential.ErrUserNotFound {
return fmt.Errorf("user %s not found", username)
}
if err == credential.ErrAccessKeyNotFound {
return fmt.Errorf("access key %s not found for user %s", accessKeyId, username)
}
return fmt.Errorf("failed to delete access key: %w", err)
}
return nil
}
// UpdateAccessKeyStatus updates the status of an access key for a user
func (s *AdminServer) UpdateAccessKeyStatus(username, accessKeyId, status string) error {
if s.credentialManager == nil {
return fmt.Errorf("credential manager not available")
}
// Validate status against allowed values
if status != AccessKeyStatusActive && status != AccessKeyStatusInactive {
return fmt.Errorf("invalid status '%s': must be '%s' or '%s'", status, AccessKeyStatusActive, AccessKeyStatusInactive)
}
ctx := context.Background()
// Get user using credential manager
identity, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return fmt.Errorf("user %s not found", username)
}
return fmt.Errorf("failed to get user: %w", err)
}
// Find and update the access key status
found := false
for _, cred := range identity.Credentials {
if cred.AccessKey == accessKeyId {
cred.Status = status
found = true
break
}
}
if !found {
return fmt.Errorf("access key %s not found for user %s", accessKeyId, username)
}
// Update user using credential manager
err = s.credentialManager.UpdateUser(ctx, username, identity)
if err != nil {
return fmt.Errorf("failed to update user access key status: %w", err)
}
return nil
}
// GetUserPolicies returns the policies for a user (actions)
func (s *AdminServer) GetUserPolicies(username string) ([]string, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Get user using credential manager
identity, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return nil, fmt.Errorf("user %s not found", username)
}
return nil, fmt.Errorf("failed to get user: %w", err)
}
return identity.Actions, nil
}
// UpdateUserPolicies updates the policies (actions) for a user
func (s *AdminServer) UpdateUserPolicies(username string, actions []string) error {
if s.credentialManager == nil {
return fmt.Errorf("credential manager not available")
}
ctx := context.Background()
// Get existing user
identity, err := s.credentialManager.GetUser(ctx, username)
if err != nil {
if err == credential.ErrUserNotFound {
return fmt.Errorf("user %s not found", username)
}
return fmt.Errorf("failed to get user: %w", err)
}
// Create updated identity with new actions
updatedIdentity := &iam_pb.Identity{
Name: identity.Name,
Account: identity.Account,
Credentials: identity.Credentials,
Actions: actions,
PolicyNames: identity.PolicyNames,
}
// Update user using credential manager
err = s.credentialManager.UpdateUser(ctx, username, updatedIdentity)
if err != nil {
return fmt.Errorf("failed to update user policies: %w", err)
}
return nil
}
// isNotFoundError checks for "not found" in the error message as a fallback
// for stores (e.g. gRPC) that don't return the credential.ErrAccessKeyNotFound sentinel.
func isNotFoundError(err error) bool {
return err != nil && strings.Contains(strings.ToLower(err.Error()), "not found")
}
// Helper functions for generating keys and IDs
func generateAccessKey() string {
// Generate 20-character access key (AWS standard)
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
b := make([]byte, 20)
for i := range b {
b[i] = charset[randomInt(len(charset))]
}
return string(b)
}
func generateSecretKey() string {
// Use the IAM helper to generate URL-safe secret keys (no +, / characters)
// that won't break S3 signature authentication
key, err := iam.GenerateSecretAccessKey()
if err != nil {
panic(fmt.Sprintf("failed to generate secret key: %v", err))
}
return key
}
func generateAccountId() string {
// Generate 12-digit account ID
b := make([]byte, 4)
rand.Read(b)
val := (uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]))
return fmt.Sprintf("%012d", val)
}
func randomInt(max int) int {
b := make([]byte, 1)
rand.Read(b)
return int(b[0]) % max
}