Files
seaweedFS/weed/admin/dash/user_management.go
Chris Lu a3717cd4b5 fix(admin): show anonymous user in Object Store Users UI (#8671)
The anonymous identity was explicitly filtered out of the user listing,
making it invisible in the admin console. Users could not view or edit
its permissions. Attempting to recreate it failed with "already exists".

Remove the anonymous skip in GetObjectStoreUsers so it appears like any
other identity. Add a guard in DeleteObjectStoreUser to prevent deletion
of the anonymous system identity, which would break unauthenticated S3
access.

Fixes #8466

Co-authored-by: Copilot <copilot@github.com>
2026-03-16 19:54:57 -07:00

464 lines
13 KiB
Go

package dash
import (
"context"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/credential"
"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")
}
// Prevent deletion of the anonymous identity — it is a system identity
// used for unauthenticated S3 access. Removing it would break anonymous
// request handling in the IAM layer.
if username == "anonymous" {
return fmt.Errorf("cannot delete the system identity 'anonymous'")
}
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
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,
}
// 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
if req.AccessKey != "" && (len(req.AccessKey) < 4 || len(req.AccessKey) > 128) {
return nil, fmt.Errorf("access key must be between 4 and 128 characters: %w", ErrInvalidInput)
}
if req.SecretKey != "" && (len(req.SecretKey) < 8 || len(req.SecretKey) > 128) {
return nil, fmt.Errorf("secret key must be between 8 and 128 characters: %w", ErrInvalidInput)
}
// Use provided keys or generate new ones
accessKey := req.AccessKey
if accessKey == "" {
accessKey = generateAccessKey()
}
secretKey := req.SecretKey
if secretKey == "" {
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 {
// Generate 40-character secret key (AWS standard)
b := make([]byte, 30) // 30 bytes = 40 characters in base64
rand.Read(b)
return base64.StdEncoding.EncodeToString(b)
}
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
}