* admin: remove misleading "secret key only shown once" warning
The access key details modal already allows viewing both the access key
and secret key at any time, so the warning about the secret key only
being displayed once is incorrect and misleading.
* admin: allow specifying custom access key and secret key
Add optional access_key and secret_key fields to the create access key
API. When provided, the specified keys are used instead of generating
random ones. The UI now shows a form with optional fields when creating
a new key, with a note that leaving them blank auto-generates keys.
* admin: check access key uniqueness before creating
Access keys must be globally unique across all users since S3 auth
looks them up in a single global map. Add an explicit check using
GetUserByAccessKey before creating, so the user gets a clear error
("access key is already in use") rather than a generic store error.
* Update object_store_users_templ.go
* admin: address review feedback for access key creation
Handler:
- Use decodeJSONBody/newJSONMaxReader instead of raw json.Decode to
enforce request size limits and handle malformed JSON properly
- Return 409 Conflict for duplicate access keys, 400 Bad Request for
validation errors, instead of generic 500
Backend:
- Validate access key length (4-128 chars) and secret key length
(8-128 chars) when user-provided
Frontend:
- Extract resetCreateKeyForm() helper to avoid duplicated cleanup logic
- Wire resetCreateKeyForm to accessKeysModal hidden.bs.modal event so
form state is always cleared when modal is dismissed
- Change secret key input to type="password" with a visibility toggle
* admin: guard against nil request and handle GetUserByAccessKey errors
- Add nil check for the CreateAccessKeyRequest pointer before
dereferencing, defaulting to an empty request (auto-generate both
keys).
- Handle non-"not found" errors from GetUserByAccessKey explicitly
instead of silently proceeding, so store errors (e.g. db connection
failures) surface rather than being swallowed.
* Update object_store_users_templ.go
* admin: fix access key uniqueness check with gRPC store
GetUserByAccessKey returns a gRPC NotFound status error (not the
sentinel credential.ErrAccessKeyNotFound) when using the gRPC store,
causing the uniqueness check to fail with a spurious error.
Treat the lookup as best-effort: only reject when a user is found
(err == nil). Any error (not-found via any store, connectivity issues)
falls through to the store's own CreateAccessKey which enforces
uniqueness definitively.
* admin: fix error handling and input validation for access key creation
Backend:
- Remove access key value from the duplicate-key error message to avoid
logging the caller-supplied identifier.
Handler:
- Handle empty POST body (io.EOF) as a valid request that auto-generates
both keys, instead of rejecting it as malformed JSON.
- Return 404 for "not found" errors (e.g. non-existent user) instead of
collapsing them into a 500.
Frontend:
- Add minlength/maxlength attributes matching backend constraints
(access key 4-128, secret key 8-128).
- Call reportValidity() before submitting so invalid lengths are caught
client-side without a round trip.
* admin: use sentinel errors and fix GetUserByAccessKey error handling
Backend (user_management.go):
- Define sentinel errors (ErrAccessKeyInUse, ErrUserNotFound,
ErrInvalidInput) and wrap them in returned errors so callers can use
errors.Is.
- Handle GetUserByAccessKey errors properly: check the sentinel
credential.ErrAccessKeyNotFound first, then fall back to string
matching for stores (gRPC) that return non-sentinel not-found errors.
Surface unexpected errors instead of silently proceeding.
Handler (user_handlers.go):
- Replace fragile strings.Contains error matching with errors.Is
against the new dash sentinels.
Frontend (object_store_users.templ):
- Add double-submit guard (isCreatingKey flag + button disabling) to
prevent duplicate access key creation requests.
457 lines
12 KiB
Go
457 lines
12 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")
|
|
}
|
|
|
|
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
|
|
}
|