mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-07-28 19:13:17 +00:00
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.
480 lines
14 KiB
Go
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
|
|
}
|