Browse Source

migrate IAM policies to multi-file storage (#8114)

* Add IAM gRPC service definition

- Add GetConfiguration/PutConfiguration for config management
- Add CreateUser/GetUser/UpdateUser/DeleteUser/ListUsers for user management
- Add CreateAccessKey/DeleteAccessKey/GetUserByAccessKey for access key management
- Methods mirror existing IAM HTTP API functionality

* Add IAM gRPC handlers on filer server

- Implement IamGrpcServer with CredentialManager integration
- Handle configuration get/put operations
- Handle user CRUD operations
- Handle access key create/delete operations
- All methods delegate to CredentialManager for actual storage

* Wire IAM gRPC service to filer server

- Add CredentialManager field to FilerOption and FilerServer
- Import credential store implementations in filer command
- Initialize CredentialManager from credential.toml if available
- Register IAM gRPC service on filer gRPC server
- Enable credential management via gRPC alongside existing filer services

* Regenerate IAM protobuf with gRPC service methods

* fix: compilation error in DeleteUser

* fix: address code review comments for IAM migration

* feat: migrate policies to multi-file layout and fix identity duplicated content

* refactor: remove configuration.json and migrate Service Accounts to multi-file layout

* refactor: standardize Service Accounts as distinct store entities and fix Admin Server persistence

* config: set ServiceAccountsDirectory to /etc/iam/service_accounts

* Fix Chrome dialog auto-dismiss with Bootstrap modals

- Add modal-alerts.js library with Bootstrap modal replacements
- Replace all 15 confirm() calls with showConfirm/showDeleteConfirm
- Auto-override window.alert() for all alert() calls
- Fixes Chrome 132+ aggressively blocking native dialogs

* Upgrade Bootstrap from 5.3.2 to 5.3.8

* Fix syntax error in object_store_users.templ - remove duplicate closing braces

* create policy

* display errors

* migrate to multi-file policies

* address PR feedback: use showDeleteConfirm and showErrorMessage in policies.templ, refine migration check

* Update policies_templ.go

* add service account to iam grpc

* iam: fix potential path traversal in policy names by validating name pattern

* iam: add GetServiceAccountByAccessKey to CredentialStore interface

* iam: implement service account support for PostgresStore

Includes full CRUD operations and efficient lookup by access key.

* iam: implement GetServiceAccountByAccessKey for filer_etc, grpc, and memory stores

Provides efficient lookup of service accounts by access key where possible,
with linear scan fallbacks for file-based stores.

* iam: remove filer_multiple support

Deleted its implementation and references in imports, scaffold config,
and core interface constants. Redundant with filer_etc.

* clear comment

* dash: robustify service account construction

- Guard against nil sa.Credential when constructing responses
- Fix Expiration logic to only set if > 0, avoiding Unix epoch 1970
- Ensure consistency across Get, Create, and Update handlers

* credential/filer_etc: improve error propagation in configuration handlers

- Return error from loadServiceAccountsFromMultiFile to callers
- Ensure listEntries errors in SaveConfiguration (cleanup logic) are
  propagated unless they are "not found" failures.
- Fixes potential silent failures during IAM configuration sync.

* credential/filer_etc: add existence check to CreateServiceAccount

Ensures consistency with other stores by preventing accidental overwrite
of existing service accounts during creation.

* credential/memory: improve store robustness and Reset logic

- Enforce ID immutability in UpdateServiceAccount to prevent orphans
- Update Reset() to also clear the policies map, ensuring full state
  cleanup for tests.

* dash: improve service account robustness and policy docs

- Wrap parent user lookup errors to preserve context
- Strictly validate Status field in UpdateServiceAccount
- Add deprecation comments to legacy policy management methods

* credential/filer_etc: protect against path traversal in service accounts

Implemented ID validation (alphanumeric, underscores, hyphens) and applied
it to Get, Save, and Delete operations to ensure no directory traversal
via saId.json filenames.

* credential/postgres: improve robustness and cleanup comments

- Removed brainstorming comments in GetServiceAccountByAccessKey
- Added missing rows.Err() check during iteration
- Properly propagate Scan and Unmarshal errors instead of swallowing them

* admin: unify UI alerts and confirmations using Bootstrap modals

- Updated modal-alerts.js with improved automated alert type detection
- Replaced native alert() and confirm() with showAlert(), showConfirm(),
  and showDeleteConfirm() across various Templ components
- Improved UX for delete operations by providing better context and styling
- Ensured consistent error reporting across IAM and Maintenance views

* admin: additional UI consistency fixes for alerts and confirmations

- Replaced native alert() and confirm() with Bootstrap modals in:
  - EC volumes (repair flow)
  - Collection details (repair flow)
  - File browser (properties and delete)
  - Maintenance config schema (save and reset)
- Improved delete confirmation in file browser with item context
- Ensured consistent success/error/info styling for all feedbacks

* make

* iam: add GetServiceAccountByAccessKey RPC and update GetConfiguration

* iam: implement GetServiceAccountByAccessKey on server and client

* iam: centralize policy and service account validation

* iam: optimize MemoryStore service account lookups with indexing

* iam: fix postgres service_accounts table and optimize lookups

* admin: refactor modal alerts and clean up dashboard logic

* admin: fix EC shards table layout mismatch

* admin: URL-encode IAM path parameters for safety

* admin: implement pauseWorker logic in maintenance view

* iam: add rows.Err() check to postgres ListServiceAccounts

* iam: standardize ErrServiceAccountNotFound across credential stores

* iam: map ErrServiceAccountNotFound to codes.NotFound in DeleteServiceAccount

* iam: refine service account store logic, errors and schema

* iam: add validation to GetServiceAccountByAccessKey

* admin: refine modal titles and ensure URL safety

* admin: address bot review comments for alerts and async usage

* iam: fix syntax error by restoring missing function declaration

* [FilerEtcStore] improve error handling in CreateServiceAccount

Refine error handling to provide clearer messages when checking for
existing service accounts.

* [PostgresStore] add nil guards and validation to service account methods

Ensure input parameters are not nil and required IDs are present
to prevent runtime panics and ensure data integrity.

* [JS] add shared IAM utility script

Consolidate common IAM operations like deleteUser and deleteAccessKey
into a shared utility script for better maintainability.

* [View] include shared IAM utilities in layout

Include iam-utils.js in the main layout to make IAM functions
available across all administrative pages.

* [View] refactor IAM logic and restore async in EC Shards view

Remove redundant local IAM functions and ensure that delete
confirmation callbacks are properly marked as async.

* [View] consolidate IAM logic in Object Store Users view

Remove redundant local definitions of deleteUser and deleteAccessKey,
relying on the shared utilities instead.

* [View] update generated templ files for UI consistency

* credential/postgres: remove redundant name column from service_accounts table

The id is already used as the unique identifier and was being copied to the name column.
This removes the name column from the schema and updates the INSERT/UPDATE queries.

* credential/filer_etc: improve logging for policy migration failures

Added Errorf log if AtomicRenameEntry fails during migration to ensure visibility of common failure points.

* credential: allow uppercase characters in service account ID username

Updated ServiceAccountIdPattern to allow [A-Za-z0-9_-]+ for the username component,
matching the actual service account creation logic which uses the parent user name directly.

* Update object_store_users_templ.go

* admin: fix ec_shards pagination to handle numeric page arguments

Updated goToPage in cluster_ec_shards.templ to accept either an Event
or a numeric page argument. This prevents errors when goToPage(1)
is called directly. Corrected both the .templ source and generated Go code.

* credential/filer_etc: improve service account storage robustness

Added nil guard to saveServiceAccount, updated GetServiceAccount
to return ErrServiceAccountNotFound for empty data, and improved
deleteServiceAccount to handle response-level Filer errors.
pull/8128/head
Chris Lu 4 days ago
committed by GitHub
parent
commit
5a7c74feac
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 59
      weed/admin/dash/policies_management.go
  2. 53
      weed/admin/dash/service_account_helpers.go
  3. 373
      weed/admin/dash/service_account_management.go
  4. 6
      weed/admin/static/css/bootstrap.min.css
  5. 1
      weed/admin/static/css/bootstrap.min.css.map
  6. 6
      weed/admin/static/js/bootstrap.bundle.min.js
  7. 1
      weed/admin/static/js/bootstrap.bundle.min.js.map
  8. 55
      weed/admin/static/js/iam-utils.js
  9. 331
      weed/admin/static/js/modal-alerts.js
  10. 58
      weed/admin/view/app/cluster_ec_shards.templ
  11. 126
      weed/admin/view/app/cluster_ec_shards_templ.go
  12. 10
      weed/admin/view/app/cluster_ec_volumes.templ
  13. 2
      weed/admin/view/app/cluster_ec_volumes_templ.go
  14. 8
      weed/admin/view/app/collection_details.templ
  15. 2
      weed/admin/view/app/collection_details_templ.go
  16. 9
      weed/admin/view/app/file_browser.templ
  17. 2
      weed/admin/view/app/file_browser_templ.go
  18. 4
      weed/admin/view/app/maintenance_config.templ
  19. 22
      weed/admin/view/app/maintenance_config_schema.templ
  20. 50
      weed/admin/view/app/maintenance_config_schema_templ.go
  21. 2
      weed/admin/view/app/maintenance_config_templ.go
  22. 19
      weed/admin/view/app/maintenance_workers.templ
  23. 2
      weed/admin/view/app/maintenance_workers_templ.go
  24. 103
      weed/admin/view/app/object_store_users.templ
  25. 2
      weed/admin/view/app/object_store_users_templ.go
  26. 44
      weed/admin/view/app/policies.templ
  27. 2
      weed/admin/view/app/policies_templ.go
  28. 13
      weed/admin/view/app/service_accounts.templ
  29. 2
      weed/admin/view/app/service_accounts_templ.go
  30. 4
      weed/admin/view/app/task_config.templ
  31. 4
      weed/admin/view/app/task_config_schema.templ
  32. 2
      weed/admin/view/app/task_config_schema_templ.go
  33. 2
      weed/admin/view/app/task_config_templ.go
  34. 4
      weed/admin/view/app/task_config_templ.templ
  35. 2
      weed/admin/view/app/task_config_templ_templ.go
  36. 20
      weed/admin/view/app/task_detail.templ
  37. 2
      weed/admin/view/app/task_detail_templ.go
  38. 3
      weed/admin/view/layout/layout.templ
  39. 8
      weed/admin/view/layout/layout_templ.go
  40. 1
      weed/command/imports.go
  41. 5
      weed/command/scaffold/credential.toml
  42. 45
      weed/credential/credential_manager.go
  43. 24
      weed/credential/credential_store.go
  44. 210
      weed/credential/filer_etc/filer_etc_identity.go
  45. 224
      weed/credential/filer_etc/filer_etc_policy.go
  46. 206
      weed/credential/filer_etc/filer_etc_service_account.go
  47. 498
      weed/credential/filer_multiple/filer_multiple_store.go
  48. 16
      weed/credential/grpc/grpc_policy.go
  49. 78
      weed/credential/grpc/grpc_service_account.go
  50. 85
      weed/credential/memory/memory_service_account.go
  51. 29
      weed/credential/memory/memory_store.go
  52. 65
      weed/credential/memory/memory_store_test.go
  53. 173
      weed/credential/postgres/postgres_service_account.go
  54. 15
      weed/credential/postgres/postgres_store.go
  55. 28
      weed/credential/validation.go
  56. 56
      weed/pb/iam.proto
  57. 703
      weed/pb/iam_pb/iam.pb.go
  58. 258
      weed/pb/iam_pb/iam_grpc.pb.go
  59. 137
      weed/server/filer_server_handlers_iam_grpc.go

59
weed/admin/dash/policies_management.go

@ -56,72 +56,33 @@ func NewCredentialStorePolicyManager(credentialManager *credential.CredentialMan
}
// GetPolicies retrieves all IAM policies via credential store
// Deprecated: This method delegates to the credential manager and will be moved/removed in a future release.
func (cspm *CredentialStorePolicyManager) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
// Get policies from credential store
// We'll use the credential store to access the filer indirectly
// Since policies are stored separately, we need to access the underlying store
store := cspm.credentialManager.GetStore()
glog.V(1).Infof("Getting policies from credential store: %T", store)
// Check if the store supports policy management
if policyStore, ok := store.(credential.PolicyManager); ok {
glog.V(1).Infof("Store supports policy management, calling GetPolicies")
policies, err := policyStore.GetPolicies(ctx)
if err != nil {
glog.Errorf("Error getting policies from store: %v", err)
return nil, err
}
glog.V(1).Infof("Got %d policies from store", len(policies))
return policies, nil
} else {
// Fallback: use empty policies for stores that don't support policies
glog.V(1).Infof("Credential store doesn't support policy management, returning empty policies")
return make(map[string]policy_engine.PolicyDocument), nil
}
return cspm.credentialManager.GetPolicies(ctx)
}
// CreatePolicy creates a new IAM policy via credential store
// Deprecated: This method delegates to the credential manager and will be moved/removed in a future release.
func (cspm *CredentialStorePolicyManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
store := cspm.credentialManager.GetStore()
if policyStore, ok := store.(credential.PolicyManager); ok {
return policyStore.CreatePolicy(ctx, name, document)
}
return fmt.Errorf("credential store doesn't support policy creation")
return cspm.credentialManager.CreatePolicy(ctx, name, document)
}
// UpdatePolicy updates an existing IAM policy via credential store
// Deprecated: This method delegates to the credential manager and will be moved/removed in a future release.
func (cspm *CredentialStorePolicyManager) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
store := cspm.credentialManager.GetStore()
if policyStore, ok := store.(credential.PolicyManager); ok {
return policyStore.UpdatePolicy(ctx, name, document)
}
return fmt.Errorf("credential store doesn't support policy updates")
return cspm.credentialManager.UpdatePolicy(ctx, name, document)
}
// DeletePolicy deletes an IAM policy via credential store
// Deprecated: This method delegates to the credential manager and will be moved/removed in a future release.
func (cspm *CredentialStorePolicyManager) DeletePolicy(ctx context.Context, name string) error {
store := cspm.credentialManager.GetStore()
if policyStore, ok := store.(credential.PolicyManager); ok {
return policyStore.DeletePolicy(ctx, name)
}
return fmt.Errorf("credential store doesn't support policy deletion")
return cspm.credentialManager.DeletePolicy(ctx, name)
}
// GetPolicy retrieves a specific IAM policy via credential store
// Deprecated: This method delegates to the credential manager and will be moved/removed in a future release.
func (cspm *CredentialStorePolicyManager) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
store := cspm.credentialManager.GetStore()
if policyStore, ok := store.(credential.PolicyManager); ok {
return policyStore.GetPolicy(ctx, name)
}
return nil, fmt.Errorf("credential store doesn't support policy retrieval")
return cspm.credentialManager.GetPolicy(ctx, name)
}
// AdminServer policy management methods using credential.PolicyManager

53
weed/admin/dash/service_account_helpers.go

@ -1,53 +0,0 @@
package dash
import (
"fmt"
"strings"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
// identityToServiceAccount converts an IAM identity to a ServiceAccount struct
// This helper reduces code duplication across GetServiceAccounts, GetServiceAccountDetails,
// UpdateServiceAccount, and GetServiceAccountByAccessKey
func identityToServiceAccount(identity *iam_pb.Identity) (*ServiceAccount, error) {
if identity == nil {
return nil, fmt.Errorf("identity cannot be nil")
}
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
return nil, fmt.Errorf("not a service account: %s", identity.GetName())
}
parts := strings.SplitN(identity.GetName(), ":", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid service account ID format")
}
sa := &ServiceAccount{
ID: identity.GetName(),
ParentUser: parts[1],
Status: StatusActive,
CreateDate: getCreationDate(identity.GetActions()),
Expiration: getExpiration(identity.GetActions()),
}
// Get description from account display name
if identity.Account != nil {
sa.Description = identity.Account.GetDisplayName()
}
// Get access key from credentials
if len(identity.Credentials) > 0 {
sa.AccessKeyId = identity.Credentials[0].GetAccessKey()
}
// Check if disabled
for _, action := range identity.GetActions() {
if action == disabledAction {
sa.Status = StatusInactive
break
}
}
return sa, nil
}

373
weed/admin/dash/service_account_management.go

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
"time"
@ -18,140 +17,57 @@ var (
)
const (
createdAtActionPrefix = "createdAt:"
expirationActionPrefix = "expiresAt:"
disabledAction = "__disabled__"
serviceAccountPrefix = "sa:"
accessKeyPrefix = "ABIA" // Service account access keys use ABIA prefix
serviceAccountPrefix = "sa:"
accessKeyPrefix = "ABIA" // Service account access keys use ABIA prefix
// Status constants
StatusActive = "Active"
StatusInactive = "Inactive"
)
// Helper functions for managing creation timestamps in actions
func getCreationDate(actions []string) time.Time {
for _, action := range actions {
if strings.HasPrefix(action, createdAtActionPrefix) {
timestampStr := strings.TrimPrefix(action, createdAtActionPrefix)
if timestamp, err := strconv.ParseInt(timestampStr, 10, 64); err == nil {
return time.Unix(timestamp, 0)
}
}
}
return time.Time{} // Return zero time for legacy service accounts without stored creation date
}
func setCreationDate(actions []string, createDate time.Time) []string {
// Remove any existing createdAt action
filtered := make([]string, 0, len(actions)+1)
for _, action := range actions {
if !strings.HasPrefix(action, createdAtActionPrefix) {
filtered = append(filtered, action)
}
}
// Add new createdAt action
filtered = append(filtered, fmt.Sprintf("%s%d", createdAtActionPrefix, createDate.Unix()))
return filtered
}
// Helper functions for managing expiration timestamps in actions
func getExpiration(actions []string) time.Time {
for _, action := range actions {
if strings.HasPrefix(action, expirationActionPrefix) {
timestampStr := strings.TrimPrefix(action, expirationActionPrefix)
if timestamp, err := strconv.ParseInt(timestampStr, 10, 64); err == nil {
return time.Unix(timestamp, 0)
}
}
}
return time.Time{} // No expiration set
}
func setExpiration(actions []string, expiration time.Time) []string {
// Remove any existing expiration action
filtered := make([]string, 0, len(actions)+1)
for _, action := range actions {
if !strings.HasPrefix(action, expirationActionPrefix) {
filtered = append(filtered, action)
}
}
// Add new expiration action if not zero
if !expiration.IsZero() {
filtered = append(filtered, fmt.Sprintf("%s%d", expirationActionPrefix, expiration.Unix()))
}
return filtered
}
// GetServiceAccounts returns all service accounts, optionally filtered by parent user
// NOTE: Service accounts are stored as special identities with "sa:" prefix
func (s *AdminServer) GetServiceAccounts(ctx context.Context, parentUser string) ([]ServiceAccount, error) {
if s.credentialManager == nil {
return nil, fmt.Errorf("credential manager not available")
}
// Load the current configuration to find service account identities
config, err := s.credentialManager.LoadConfiguration(ctx)
pbAccounts, err := s.credentialManager.ListServiceAccounts(ctx)
if err != nil {
return nil, fmt.Errorf("failed to load configuration: %w", err)
return nil, fmt.Errorf("failed to list service accounts: %w", err)
}
var accounts []ServiceAccount
// Service accounts are stored as identities with "sa:" prefix in their name
// Format: "sa:<parent_user>:<uuid>"
for _, identity := range config.GetIdentities() {
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
continue
}
parts := strings.SplitN(identity.GetName(), ":", 3)
if len(parts) < 3 {
for _, sa := range pbAccounts {
if sa == nil {
continue
}
parent := parts[1]
saId := identity.GetName()
// Filter by parent user if specified
if parentUser != "" && parent != parentUser {
if parentUser != "" && sa.ParentUser != parentUser {
continue
}
// Extract description from account display name if available
description := ""
status := StatusActive
if identity.Account != nil {
description = identity.Account.GetDisplayName()
if sa.Disabled {
status = StatusInactive
}
// Get access key from credentials
accessKey := ""
if len(identity.Credentials) > 0 {
accessKey = identity.Credentials[0].GetAccessKey()
// Service accounts use ABIA prefix
if !strings.HasPrefix(accessKey, accessKeyPrefix) {
continue // Not a service account
}
account := ServiceAccount{
ID: sa.Id,
ParentUser: sa.ParentUser,
Description: sa.Description,
Status: status,
CreateDate: time.Unix(sa.CreatedAt, 0),
}
// Check if disabled (stored in actions)
for _, action := range identity.GetActions() {
if action == disabledAction {
status = StatusInactive
break
}
if sa.Expiration > 0 {
account.Expiration = time.Unix(sa.Expiration, 0)
}
accounts = append(accounts, ServiceAccount{
ID: saId,
ParentUser: parent,
Description: description,
AccessKeyId: accessKey,
Status: status,
CreateDate: getCreationDate(identity.GetActions()),
Expiration: getExpiration(identity.GetActions()),
})
if sa.Credential != nil {
account.AccessKeyId = sa.Credential.AccessKey
}
accounts = append(accounts, account)
}
return accounts, nil
@ -163,43 +79,33 @@ func (s *AdminServer) GetServiceAccountDetails(ctx context.Context, id string) (
return nil, fmt.Errorf("credential manager not available")
}
// Get the identity
identity, err := s.credentialManager.GetUser(ctx, id)
sa, err := s.credentialManager.GetServiceAccount(ctx, id)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
return nil, fmt.Errorf("failed to get service account: %w", err)
}
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
return nil, fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
if sa == nil {
return nil, ErrServiceAccountNotFound
}
parts := strings.SplitN(identity.GetName(), ":", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid service account ID format")
status := StatusActive
if sa.Disabled {
status = StatusInactive
}
account := &ServiceAccount{
ID: id,
ParentUser: parts[1],
Status: StatusActive,
CreateDate: getCreationDate(identity.GetActions()),
Expiration: getExpiration(identity.GetActions()),
ID: sa.Id,
ParentUser: sa.ParentUser,
Description: sa.Description,
Status: status,
CreateDate: time.Unix(sa.CreatedAt, 0),
}
if identity.Account != nil {
account.Description = identity.Account.GetDisplayName()
if sa.Expiration > 0 {
account.Expiration = time.Unix(sa.Expiration, 0)
}
if len(identity.Credentials) > 0 {
account.AccessKeyId = identity.Credentials[0].GetAccessKey()
}
// Check if disabled
for _, action := range identity.GetActions() {
if action == disabledAction {
account.Status = StatusInactive
break
}
if sa.Credential != nil {
account.AccessKeyId = sa.Credential.AccessKey
}
return account, nil
@ -212,55 +118,48 @@ func (s *AdminServer) CreateServiceAccount(ctx context.Context, req CreateServic
}
// Validate parent user exists
_, err := s.credentialManager.GetUser(ctx, req.ParentUser)
if err != nil {
return nil, fmt.Errorf("parent user not found: %s", req.ParentUser)
if _, err := s.credentialManager.GetUser(ctx, req.ParentUser); err != nil {
return nil, fmt.Errorf("parent user lookup failed for %s: %w", req.ParentUser, err)
}
// Generate unique ID and credentials
uuid := generateAccountId()
// Maintain consistent ID format: sa:<parent>:<uuid>
saId := fmt.Sprintf("sa:%s:%s", req.ParentUser, uuid)
accessKey := accessKeyPrefix + generateAccessKey()[len(accessKeyPrefix):] // Use ABIA prefix for service accounts
accessKey := accessKeyPrefix + generateAccessKey()[len(accessKeyPrefix):]
secretKey := generateSecretKey()
// Create the service account as a special identity
now := time.Now()
// Parse expiration if provided
var expiration time.Time
sa := &iam_pb.ServiceAccount{
Id: saId,
ParentUser: req.ParentUser,
Description: req.Description,
Credential: &iam_pb.Credential{
AccessKey: accessKey,
SecretKey: secretKey,
Status: StatusActive,
},
CreatedAt: now.Unix(),
Disabled: false,
}
if req.Expiration != "" {
var err error
expiration, err = time.Parse(time.RFC3339, req.Expiration)
exp, err := time.Parse(time.RFC3339, req.Expiration)
if err != nil {
return nil, fmt.Errorf("invalid expiration format: %w", err)
}
sa.Expiration = exp.Unix()
}
identity := &iam_pb.Identity{
Name: saId,
Account: &iam_pb.Account{
Id: uuid,
DisplayName: req.Description,
},
Credentials: []*iam_pb.Credential{
{
AccessKey: accessKey,
SecretKey: secretKey,
},
},
// Store creation date and expiration in actions
Actions: setExpiration(setCreationDate([]string{}, now), expiration),
}
// Create the service account
err = s.credentialManager.CreateUser(ctx, identity)
if err != nil {
if err := s.credentialManager.CreateServiceAccount(ctx, sa); err != nil {
return nil, fmt.Errorf("failed to create service account: %w", err)
}
glog.V(1).Infof("Created service account %s for user %s", saId, req.ParentUser)
return &ServiceAccount{
resp := &ServiceAccount{
ID: saId,
ParentUser: req.ParentUser,
Description: req.Description,
@ -268,8 +167,13 @@ func (s *AdminServer) CreateServiceAccount(ctx context.Context, req CreateServic
SecretAccessKey: secretKey, // Only returned on creation
Status: StatusActive,
CreateDate: now,
Expiration: expiration,
}, nil
}
if sa.Expiration > 0 {
resp.Expiration = time.Unix(sa.Expiration, 0)
}
return resp, nil
}
// UpdateServiceAccount updates an existing service account
@ -278,85 +182,67 @@ func (s *AdminServer) UpdateServiceAccount(ctx context.Context, id string, req U
return nil, fmt.Errorf("credential manager not available")
}
// Get existing identity
identity, err := s.credentialManager.GetUser(ctx, id)
sa, err := s.credentialManager.GetServiceAccount(ctx, id)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
return nil, fmt.Errorf("failed to get service account: %w", err)
}
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
return nil, fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
if sa == nil {
return nil, ErrServiceAccountNotFound
}
// Update description if provided
if req.Description != "" {
if identity.Account == nil {
identity.Account = &iam_pb.Account{}
}
identity.Account.DisplayName = req.Description
sa.Description = req.Description
}
// Update status by adding/removing disabled action
if req.Status != "" {
// Remove existing disabled marker
newActions := make([]string, 0, len(identity.Actions))
for _, action := range identity.Actions {
if action != disabledAction {
newActions = append(newActions, action)
}
}
// Add disabled action if setting to Inactive
if req.Status == StatusInactive {
newActions = append(newActions, disabledAction)
switch req.Status {
case StatusInactive:
sa.Disabled = true
case StatusActive:
sa.Disabled = false
default:
return nil, fmt.Errorf("invalid status value: %s (must be %s or %s)", req.Status, StatusActive, StatusInactive)
}
identity.Actions = newActions
}
// Update expiration if provided
if req.Expiration != "" {
var expiration time.Time
var err error
expiration, err = time.Parse(time.RFC3339, req.Expiration)
exp, err := time.Parse(time.RFC3339, req.Expiration)
if err != nil {
return nil, fmt.Errorf("invalid expiration format: %w", err)
}
identity.Actions = setExpiration(identity.Actions, expiration)
sa.Expiration = exp.Unix()
}
// Update the identity
err = s.credentialManager.UpdateUser(ctx, id, identity)
if err != nil {
if err := s.credentialManager.UpdateServiceAccount(ctx, id, sa); err != nil {
return nil, fmt.Errorf("failed to update service account: %w", err)
}
glog.V(1).Infof("Updated service account %s", id)
// Build response
parts := strings.SplitN(id, ":", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid service account ID format")
status := StatusActive
if sa.Disabled {
status = StatusInactive
}
result := &ServiceAccount{
ID: id,
ParentUser: parts[1],
Description: identity.Account.GetDisplayName(),
Status: StatusActive,
CreateDate: getCreationDate(identity.Actions),
accessKeyId := ""
if sa.Credential != nil {
accessKeyId = sa.Credential.AccessKey
}
if len(identity.Credentials) > 0 {
result.AccessKeyId = identity.Credentials[0].GetAccessKey()
resp := &ServiceAccount{
ID: sa.Id,
ParentUser: sa.ParentUser,
Description: sa.Description,
Status: status,
CreateDate: time.Unix(sa.CreatedAt, 0),
AccessKeyId: accessKeyId,
}
for _, action := range identity.Actions {
if action == disabledAction {
result.Status = StatusInactive
break
}
if sa.Expiration > 0 {
resp.Expiration = time.Unix(sa.Expiration, 0)
}
return result, nil
return resp, nil
}
// DeleteServiceAccount deletes a service account
@ -365,19 +251,16 @@ func (s *AdminServer) DeleteServiceAccount(ctx context.Context, id string) error
return fmt.Errorf("credential manager not available")
}
// Verify it's a service account
identity, err := s.credentialManager.GetUser(ctx, id)
// Verify existence
sa, err := s.credentialManager.GetServiceAccount(ctx, id)
if err != nil {
return fmt.Errorf("%w: %s", ErrServiceAccountNotFound, id)
return fmt.Errorf("failed to check service account: %w", err)
}
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
return fmt.Errorf("%w: not a service account: %s", ErrServiceAccountNotFound, id)
if sa == nil {
return ErrServiceAccountNotFound
}
// Delete the identity
err = s.credentialManager.DeleteUser(ctx, id)
if err != nil {
if err := s.credentialManager.DeleteServiceAccount(ctx, id); err != nil {
return fmt.Errorf("failed to delete service account: %w", err)
}
@ -395,40 +278,30 @@ func (s *AdminServer) GetServiceAccountByAccessKey(ctx context.Context, accessKe
return nil, fmt.Errorf("credential manager not available")
}
// Find identity by access key
identity, err := s.credentialManager.GetUserByAccessKey(ctx, accessKey)
// Efficient lookup is now supported by the interface and optimized stores
sa, err := s.credentialManager.GetStore().GetServiceAccountByAccessKey(ctx, accessKey)
if err != nil {
return nil, fmt.Errorf("service account not found for access key: %s", accessKey)
return nil, fmt.Errorf("failed to find service account: %w", err)
}
if !strings.HasPrefix(identity.GetName(), serviceAccountPrefix) {
return nil, fmt.Errorf("not a service account")
status := StatusActive
if sa.Disabled {
status = StatusInactive
}
parts := strings.SplitN(identity.GetName(), ":", 3)
if len(parts) < 3 {
return nil, fmt.Errorf("invalid service account ID format")
accessKeyId := ""
if sa.Credential != nil {
accessKeyId = sa.Credential.AccessKey
}
account := &ServiceAccount{
ID: identity.GetName(),
ParentUser: parts[1],
AccessKeyId: accessKey,
Status: StatusActive,
CreateDate: getCreationDate(identity.GetActions()),
Expiration: getExpiration(identity.GetActions()),
resp := &ServiceAccount{
ID: sa.Id,
ParentUser: sa.ParentUser,
Description: sa.Description,
AccessKeyId: accessKeyId,
Status: status,
CreateDate: time.Unix(sa.CreatedAt, 0),
}
if identity.Account != nil {
account.Description = identity.Account.GetDisplayName()
if sa.Expiration > 0 {
resp.Expiration = time.Unix(sa.Expiration, 0)
}
for _, action := range identity.GetActions() {
if action == disabledAction {
account.Status = StatusInactive
break
}
}
return account, nil
return resp, nil
}

6
weed/admin/static/css/bootstrap.min.css
File diff suppressed because it is too large
View File

1
weed/admin/static/css/bootstrap.min.css.map
File diff suppressed because it is too large
View File

6
weed/admin/static/js/bootstrap.bundle.min.js
File diff suppressed because it is too large
View File

1
weed/admin/static/js/bootstrap.bundle.min.js.map
File diff suppressed because it is too large
View File

55
weed/admin/static/js/iam-utils.js

@ -0,0 +1,55 @@
/**
* Shared IAM utility functions for the SeaweedFS Admin Dashboard.
*/
// Delete user function
async function deleteUser(username) {
showDeleteConfirm(username, async function () {
try {
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`, {
method: 'DELETE'
});
if (response.ok) {
showAlert('User deleted successfully', 'success');
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json().catch(() => ({}));
showAlert('Failed to delete user: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error deleting user:', error);
showAlert('Failed to delete user: ' + error.message, 'error');
}
}, 'Are you sure you want to delete this user? This action cannot be undone.');
}
// Delete access key function
async function deleteAccessKey(username, accessKey) {
showDeleteConfirm(accessKey, async function () {
try {
const encodedUsername = encodeURIComponent(username);
const encodedAccessKey = encodeURIComponent(accessKey);
const response = await fetch(`/api/users/${encodedUsername}/access-keys/${encodedAccessKey}`, {
method: 'DELETE'
});
if (response.ok) {
showAlert('Access key deleted successfully', 'success');
// If refreshAccessKeysList exists (in object_store_users.templ), use it
if (typeof refreshAccessKeysList === 'function') {
refreshAccessKeysList(username);
} else {
setTimeout(() => window.location.reload(), 1000);
}
} else {
const error = await response.json().catch(() => ({}));
showAlert('Failed to delete access key: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error deleting access key:', error);
showAlert('Failed to delete access key: ' + error.message, 'error');
}
}, 'Are you sure you want to delete this access key?');
}

331
weed/admin/static/js/modal-alerts.js

@ -0,0 +1,331 @@
/**
* Modal Alerts - Bootstrap Modal replacement for native alert() and confirm()
* Fixes Chrome auto-dismiss issue with native dialogs
*
* Usage:
* showAlert('Message', 'success');
* showConfirm('Delete this?', function() { });
*/
(function () {
'use strict';
// Create and inject modal HTML into page if not already present
function ensureModalsExist() {
if (document.getElementById('globalAlertModal')) {
return; // Already exists
}
const modalsHTML = `
<!-- Global Alert Modal -->
<div class="modal fade" id="globalAlertModal" tabindex="-1" aria-labelledby="globalAlertModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" id="globalAlertModalHeader">
<h5 class="modal-title" id="globalAlertModalLabel">
<i class="fas fa-info-circle me-2" id="globalAlertModalIcon"></i>
<span id="globalAlertModalTitle">Notice</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="globalAlertModalBody">
<!-- Message will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>
<!-- Global Confirm Modal -->
<div class="modal fade" id="globalConfirmModal" tabindex="-1" aria-labelledby="globalConfirmModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-warning">
<h5 class="modal-title" id="globalConfirmModalLabel">
<i class="fas fa-question-circle me-2"></i><span id="globalConfirmModalTitleText">Confirm Action</span>
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="globalConfirmModalBody">
<!-- Message will be inserted here -->
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" id="globalConfirmCancelBtn">Cancel</button>
<button type="button" class="btn btn-primary" id="globalConfirmOkBtn">OK</button>
</div>
</div>
</div>
</div>
<!-- Global Delete Confirm Modal -->
<div class="modal fade" id="globalDeleteModal" tabindex="-1" aria-labelledby="globalDeleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="globalDeleteModalLabel">
<i class="fas fa-exclamation-triangle me-2"></i>Confirm Delete
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-2" id="globalDeleteModalMessage">Are you sure you want to delete this item?</p>
<p class="mb-0"><strong id="globalDeleteModalItemName"></strong></p>
<p class="text-muted small mt-2 mb-0">This action cannot be undone.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="globalDeleteConfirmBtn">
<i class="fas fa-trash me-1"></i>Delete
</button>
</div>
</div>
</div>
</div>
`;
// Inject modals at end of body
document.body.insertAdjacentHTML('beforeend', modalsHTML);
}
/**
* Show an alert message using Bootstrap modal
* @param {string} message - The message to display
* @param {string|object} typeOrOptions - Type ('success', 'error', 'warning', 'info') or options object
* @param {string} title - Optional custom title
*/
window.showAlert = function (message, typeOrOptions, title) {
ensureModalsExist();
let type = 'info';
let isHtml = false;
if (typeof typeOrOptions === 'object' && typeOrOptions !== null) {
type = typeOrOptions.type || 'info';
isHtml = typeOrOptions.isHtml || false;
title = typeOrOptions.title || title;
} else if (typeof typeOrOptions === 'string') {
type = typeOrOptions;
}
const modal = document.getElementById('globalAlertModal');
const header = document.getElementById('globalAlertModalHeader');
const titleEl = document.getElementById('globalAlertModalTitle');
const bodyEl = document.getElementById('globalAlertModalBody');
const iconEl = document.getElementById('globalAlertModalIcon');
// Configuration for different types
const types = {
'success': {
title: 'Success',
icon: 'fa-check-circle',
headerClass: 'bg-success text-white',
btnClose: 'btn-close-white'
},
'error': {
title: 'Error',
icon: 'fa-exclamation-triangle',
headerClass: 'bg-danger text-white',
btnClose: 'btn-close-white'
},
'warning': {
title: 'Warning',
icon: 'fa-exclamation-circle',
headerClass: 'bg-warning text-dark',
btnClose: ''
},
'info': {
title: 'Notice',
icon: 'fa-info-circle',
headerClass: 'bg-info text-white',
btnClose: 'btn-close-white'
}
};
const config = types[type] || types['info'];
// Update header styling
header.className = 'modal-header ' + config.headerClass;
const closeBtn = header.querySelector('.btn-close');
closeBtn.className = 'btn-close ' + config.btnClose;
// Update icon
iconEl.className = 'fas ' + config.icon + ' me-2';
// Update title
titleEl.textContent = title || config.title;
// Update body - support HTML or text
if (isHtml || message.includes('<p>') || message.includes('<ul>')) {
bodyEl.innerHTML = message;
} else {
bodyEl.innerHTML = '<p class="mb-0">' + escapeHtml(message) + '</p>';
}
// Show modal
const bsModal = new bootstrap.Modal(modal);
bsModal.show();
};
/**
* Show a confirmation dialog using Bootstrap modal
* @param {string} message - The confirmation message
* @param {function} onConfirm - Callback function if user confirms
* @param {function|object} onCancelOrOptions - Optional callback or options object
* @param {string} title - Optional custom title
*/
window.showConfirm = function (message, onConfirm, onCancelOrOptions, title) {
ensureModalsExist();
let onCancel = null;
let isHtml = false;
if (typeof onCancelOrOptions === 'object' && onCancelOrOptions !== null) {
onCancel = onCancelOrOptions.onCancel;
isHtml = onCancelOrOptions.isHtml || false;
title = onCancelOrOptions.title || null;
} else {
onCancel = onCancelOrOptions;
}
const modalEl = document.getElementById('globalConfirmModal');
const bodyEl = document.getElementById('globalConfirmModalBody');
const titleEl = document.getElementById('globalConfirmModalTitleText');
const okBtn = document.getElementById('globalConfirmOkBtn');
const cancelBtn = document.getElementById('globalConfirmCancelBtn');
// Set title
if (title) {
titleEl.textContent = title;
} else {
titleEl.textContent = 'Confirm Action';
}
// Set message
if (isHtml || message.includes('<p>') || message.includes('<ul>')) {
bodyEl.innerHTML = message;
} else {
bodyEl.innerHTML = '<p class="mb-0">' + escapeHtml(message) + '</p>';
}
// Remove old event listeners by cloning buttons
const newOkBtn = okBtn.cloneNode(true);
const newCancelBtn = cancelBtn.cloneNode(true);
okBtn.parentNode.replaceChild(newOkBtn, okBtn);
cancelBtn.parentNode.replaceChild(newCancelBtn, cancelBtn);
const modal = new bootstrap.Modal(modalEl);
// Add event listeners
newOkBtn.addEventListener('click', function () {
modal.hide();
if (typeof onConfirm === 'function') {
onConfirm();
}
});
newCancelBtn.addEventListener('click', function () {
modal.hide();
if (typeof onCancel === 'function') {
onCancel();
}
});
modal.show();
};
/**
* Show a delete confirmation dialog
* @param {string} itemName - Name of the item to delete
* @param {function} onConfirm - Callback function if user confirms deletion
* @param {string} message - Optional custom message (default: "Are you sure you want to delete this item?")
*/
window.showDeleteConfirm = function (itemName, onConfirm, message) {
ensureModalsExist();
const modalEl = document.getElementById('globalDeleteModal');
const messageEl = document.getElementById('globalDeleteModalMessage');
const itemNameEl = document.getElementById('globalDeleteModalItemName');
const confirmBtn = document.getElementById('globalDeleteConfirmBtn');
// Set custom message if provided
if (message) {
messageEl.textContent = message;
} else {
messageEl.textContent = 'Are you sure you want to delete this item?';
}
// Set item name
itemNameEl.textContent = itemName;
// Remove old event listener by cloning button
const newConfirmBtn = confirmBtn.cloneNode(true);
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
const modal = new bootstrap.Modal(modalEl);
// Add new event listener
newConfirmBtn.addEventListener('click', function () {
modal.hide();
if (typeof onConfirm === 'function') {
onConfirm();
}
});
modal.show();
};
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function (m) { return map[m]; });
}
// Auto-initialize on DOMContentLoaded
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensureModalsExist);
} else {
ensureModalsExist();
}
/**
* AUTOMATIC OVERRIDE of native alert()
* This makes ALL existing alert() calls automatically use Bootstrap modals
*/
window.alert = function (message) {
// Auto-detect message type from content
let type = 'info';
const msg = String(message || '');
const msgLower = msg.toLowerCase();
// Refined type inference to avoid false positives
if (msgLower.includes('success') || msgLower.includes('successfully') || msgLower.includes('created') || msgLower.includes('updated') || msgLower.includes('saved')) {
// Avoid "not successful"
if (!msgLower.includes('not success')) {
type = 'success';
}
}
if (type === 'info') {
if (msgLower.includes('error') || msgLower.includes('failed') || msgLower.includes('invalid') || msgLower.includes('exception')) {
type = 'error';
} else if (msgLower.includes('warning') || msgLower.includes('required') || msgLower.includes('attention')) {
type = 'warning';
}
}
showAlert(msg, type);
};
console.log('Modal Alerts library loaded - native alert() overridden');
console.log('For confirm(), use showConfirm() or showDeleteConfirm() instead of native confirm()');
})();

58
weed/admin/view/app/cluster_ec_shards.templ

@ -145,20 +145,6 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
</a>
</th>
}
<th>
<a href="#" onclick="sortBy('server')" class="text-dark text-decoration-none">
Server
if data.SortBy == "server" {
if data.SortOrder == "asc" {
<i class="fas fa-sort-up ms-1"></i>
} else {
<i class="fas fa-sort-down ms-1"></i>
}
} else {
<i class="fas fa-sort ms-1 text-muted"></i>
}
</a>
</th>
if data.ShowDataCenterColumn {
<th>
<a href="#" onclick="sortBy('datacenter')" class="text-dark text-decoration-none">
@ -175,6 +161,20 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
</a>
</th>
}
<th>
<a href="#" onclick="sortBy('server')" class="text-dark text-decoration-none">
Server
if data.SortBy == "server" {
if data.SortOrder == "asc" {
<i class="fas fa-sort-up ms-1"></i>
} else {
<i class="fas fa-sort-down ms-1"></i>
}
} else {
<i class="fas fa-sort ms-1 text-muted"></i>
}
</a>
</th>
if data.ShowRackColumn {
<th>
<a href="#" onclick="sortBy('rack')" class="text-dark text-decoration-none">
@ -215,14 +215,14 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
}
</td>
}
<td>
<code class="small">{shard.Server}</code>
</td>
if data.ShowDataCenterColumn {
<td>
<span class="badge bg-outline-primary">{shard.DataCenter}</span>
</td>
}
<td>
<code class="small">{shard.Server}</code>
</td>
if data.ShowRackColumn {
<td>
<span class="badge bg-outline-secondary">{shard.Rack}</span>
@ -341,10 +341,15 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
});
}
function goToPage(event) {
// Get data from the link element (not any child elements)
const link = event.target.closest('a');
const page = link.getAttribute('data-page');
function goToPage(arg) {
let page;
if (typeof arg === 'number' || typeof arg === 'string') {
page = arg;
} else {
// Get data from the link element (not any child elements)
const link = arg.target.closest('a');
page = link.getAttribute('data-page');
}
updateUrl({ page: page });
}
@ -387,7 +392,7 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
// Get data from the button element (not the icon inside it)
const button = event.target.closest('button');
const volumeId = button.getAttribute('data-volume-id');
if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {
showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {
fetch(`/api/storage/volumes/${volumeId}/repair`, {
method: 'POST',
headers: {
@ -397,17 +402,18 @@ templ ClusterEcShards(data dash.ClusterEcShardsData) {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Repair initiated successfully');
showAlert('Repair initiated successfully', 'success');
location.reload();
} else {
alert('Failed to initiate repair: ' + data.error);
showAlert('Failed to initiate repair: ' + data.error, 'error');
}
})
.catch(error => {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'error');
});
}
});
}
</script>
}

126
weed/admin/view/app/cluster_ec_shards_templ.go

@ -5,11 +5,11 @@ package app
//lint:file-ignore SA4006 This context is only used if a nested component is present.
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
import (
"fmt"
"github.com/a-h/templ"
templruntime "github.com/a-h/templ/runtime"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
)
@ -218,60 +218,60 @@ func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<th><a href=\"#\" onclick=\"sortBy('server')\" class=\"text-dark text-decoration-none\">Server ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.SortBy == "server" {
if data.SortOrder == "asc" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<i class=\"fas fa-sort-up ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<i class=\"fas fa-sort-down ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<i class=\"fas fa-sort ms-1 text-muted\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</a></th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowDataCenterColumn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<th><a href=\"#\" onclick=\"sortBy('datacenter')\" class=\"text-dark text-decoration-none\">Data Center ")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<th><a href=\"#\" onclick=\"sortBy('datacenter')\" class=\"text-dark text-decoration-none\">Data Center ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.SortBy == "datacenter" {
if data.SortOrder == "asc" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<i class=\"fas fa-sort-up ms-1\"></i>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "<i class=\"fas fa-sort-up ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<i class=\"fas fa-sort-down ms-1\"></i>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "<i class=\"fas fa-sort-down ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<i class=\"fas fa-sort ms-1 text-muted\"></i>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<i class=\"fas fa-sort ms-1 text-muted\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</a></th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "<th><a href=\"#\" onclick=\"sortBy('server')\" class=\"text-dark text-decoration-none\">Server ")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.SortBy == "server" {
if data.SortOrder == "asc" {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "<i class=\"fas fa-sort-up ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<i class=\"fas fa-sort-down ms-1\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a></th>")
} else {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<i class=\"fas fa-sort ms-1 text-muted\"></i>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</a></th>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowRackColumn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "<th><a href=\"#\" onclick=\"sortBy('rack')\" class=\"text-dark text-decoration-none\">Rack ")
if templ_7745c5c3_Err != nil {
@ -356,42 +356,42 @@ func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<td><code class=\"small\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Server)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 219, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</code></td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowDataCenterColumn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<td><span class=\"badge bg-outline-primary\">")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "<td><span class=\"badge bg-outline-primary\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(shard.DataCenter)
var templ_7745c5c3_Var9 string
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(shard.DataCenter)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 223, Col: 88}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 220, Col: 88}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</span></td>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</span></td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "<td><code class=\"small\">")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
var templ_7745c5c3_Var10 string
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(shard.Server)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 224, Col: 61}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "</code></td>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
if data.ShowRackColumn {
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "<td><span class=\"badge bg-outline-secondary\">")
if templ_7745c5c3_Err != nil {
@ -663,7 +663,7 @@ func ClusterEcShards(data dash.ClusterEcShardsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = \"{data.SortBy}\";\n const currentOrder = \"{data.SortOrder}\";\n let newOrder = 'asc';\n \n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sortBy: field,\n sortOrder: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n // Get data from the link element (not any child elements)\n const link = event.target.closest('a');\n const page = link.getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n updateUrl({ pageSize: pageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key]) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function exportEcShards() {\n const url = new URL('/api/storage/ec-shards/export', window.location.origin);\n const params = new URLSearchParams(window.location.search);\n params.forEach((value, key) => {\n url.searchParams.set(key, value);\n });\n window.open(url.toString(), '_blank');\n }\n\n function showShardDetails(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n \n // Navigate to the EC volume details page\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {\n fetch(`/api/storage/volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + data.error);\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = \"{data.SortBy}\";\n const currentOrder = \"{data.SortOrder}\";\n let newOrder = 'asc';\n \n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sortBy: field,\n sortOrder: newOrder,\n page: 1\n });\n }\n\n function goToPage(arg) {\n let page;\n if (typeof arg === 'number' || typeof arg === 'string') {\n page = arg;\n } else {\n // Get data from the link element (not any child elements)\n const link = arg.target.closest('a');\n page = link.getAttribute('data-page');\n }\n updateUrl({ page: page });\n }\n\n function changePageSize() {\n const pageSize = document.getElementById('pageSizeSelect').value;\n updateUrl({ pageSize: pageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key]) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function exportEcShards() {\n const url = new URL('/api/storage/ec-shards/export', window.location.origin);\n const params = new URLSearchParams(window.location.search);\n params.forEach((value, key) => {\n url.searchParams.set(key, value);\n });\n window.open(url.toString(), '_blank');\n }\n\n function showShardDetails(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n \n // Navigate to the EC volume details page\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n // Get data from the button element (not the icon inside it)\n const button = event.target.closest('button');\n const volumeId = button.getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {\n fetch(`/api/storage/volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n showAlert('Repair initiated successfully', 'success');\n location.reload();\n } else {\n showAlert('Failed to initiate repair: ' + data.error, 'error');\n }\n })\n .catch(error => {\n showAlert('Error: ' + error.message, 'error');\n });\n });\n }\n\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -700,7 +700,7 @@ func displayShardDistribution(shard dash.EcShardWithInfo, allShards []dash.EcSha
var templ_7745c5c3_Var24 string
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(calculateDistributionSummary(shard.VolumeID, allShards))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 418, Col: 65}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 419, Col: 65}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
if templ_7745c5c3_Err != nil {
@ -750,7 +750,7 @@ func displayVolumeStatus(shard dash.EcShardWithInfo) templ.Component {
var templ_7745c5c3_Var26 string
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(shard.MissingShards)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 428, Col: 129}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 429, Col: 129}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
if templ_7745c5c3_Err != nil {
@ -768,7 +768,7 @@ func displayVolumeStatus(shard dash.EcShardWithInfo) templ.Component {
var templ_7745c5c3_Var27 string
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(shard.MissingShards)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 430, Col: 145}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 431, Col: 145}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
if templ_7745c5c3_Err != nil {
@ -786,7 +786,7 @@ func displayVolumeStatus(shard dash.EcShardWithInfo) templ.Component {
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(shard.MissingShards)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 432, Col: 138}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 433, Col: 138}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@ -804,7 +804,7 @@ func displayVolumeStatus(shard dash.EcShardWithInfo) templ.Component {
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(shard.MissingShards)))
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 434, Col: 137}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/app/cluster_ec_shards.templ`, Line: 435, Col: 137}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {

10
weed/admin/view/app/cluster_ec_volumes.templ

@ -378,7 +378,7 @@ templ ClusterEcVolumes(data dash.ClusterEcVolumesData) {
function repairVolume(event) {
const volumeId = event.target.closest('button').getAttribute('data-volume-id');
if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {
showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {
fetch(`/api/storage/ec-volumes/${volumeId}/repair`, {
method: 'POST',
headers: {
@ -393,16 +393,16 @@ templ ClusterEcVolumes(data dash.ClusterEcVolumesData) {
})
.then(data => {
if (data && data.success) {
alert('Repair initiated successfully');
showAlert('Repair initiated successfully', 'success');
location.reload();
} else {
alert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'));
showAlert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'), 'error');
}
})
.catch(error => {
alert('Error: ' + error.message);
showAlert('Error: ' + error.message, 'error');
});
}
});
}
</script>
}

2
weed/admin/view/app/cluster_ec_volumes_templ.go

@ -757,7 +757,7 @@ func ClusterEcVolumes(data dash.ClusterEcVolumesData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = new URLSearchParams(window.location.search).get('sort_by');\n const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sort_by: field,\n sort_order: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n event.preventDefault();\n const page = event.target.closest('a').getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize(newPageSize) {\n updateUrl({ page_size: newPageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key] != null) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function showVolumeDetails(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n if (confirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`)) {\n fetch(`/api/storage/ec-volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n if (data && data.success) {\n alert('Repair initiated successfully');\n location.reload();\n } else {\n alert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "<!-- JavaScript --><script>\n function sortBy(field) {\n const currentSort = new URLSearchParams(window.location.search).get('sort_by');\n const currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n \n let newOrder = 'asc';\n if (currentSort === field && currentOrder === 'asc') {\n newOrder = 'desc';\n }\n \n updateUrl({\n sort_by: field,\n sort_order: newOrder,\n page: 1\n });\n }\n\n function goToPage(event) {\n event.preventDefault();\n const page = event.target.closest('a').getAttribute('data-page');\n updateUrl({ page: page });\n }\n\n function changePageSize(newPageSize) {\n updateUrl({ page_size: newPageSize, page: 1 });\n }\n\n function updateUrl(params) {\n const url = new URL(window.location);\n Object.keys(params).forEach(key => {\n if (params[key] != null) {\n url.searchParams.set(key, params[key]);\n } else {\n url.searchParams.delete(key);\n }\n });\n window.location.href = url.toString();\n }\n\n function showVolumeDetails(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n window.location.href = `/storage/ec-volumes/${volumeId}`;\n }\n\n function repairVolume(event) {\n const volumeId = event.target.closest('button').getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for volume ${volumeId}?`, function() {\n fetch(`/api/storage/ec-volumes/${volumeId}/repair`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n }\n })\n .then(response => {\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n return response.json();\n })\n .then(data => {\n if (data && data.success) {\n showAlert('Repair initiated successfully', 'success');\n location.reload();\n } else {\n showAlert('Failed to initiate repair: ' + (data && data.error ? data.error : 'Unknown error'), 'error');\n }\n })\n .catch(error => {\n showAlert('Error: ' + error.message, 'error');\n });\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

8
weed/admin/view/app/collection_details.templ

@ -372,10 +372,10 @@ templ CollectionDetails(data dash.CollectionDetailsData) {
// Repair EC Volume
function repairEcVolume(event) {
const volumeId = event.target.closest('button').getAttribute('data-volume-id');
if (confirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`)) {
showConfirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`, function() {
// TODO: Implement repair functionality
alert('Repair functionality will be implemented soon.');
}
showAlert('Repair functionality will be implemented soon.', 'info');
});
}
</script>
}
}

2
weed/admin/view/app/collection_details_templ.go

@ -575,7 +575,7 @@ func CollectionDetails(data dash.CollectionDetailsData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<script>\n\t\t// Sorting functionality\n\t\tfunction sortBy(field) {\n\t\t\tconst currentSort = new URLSearchParams(window.location.search).get('sort_by');\n\t\t\tconst currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n\t\t\t\n\t\t\tlet newOrder = 'asc';\n\t\t\tif (currentSort === field && currentOrder === 'asc') {\n\t\t\t\tnewOrder = 'desc';\n\t\t\t}\n\t\t\t\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('sort_by', field);\n\t\t\turl.searchParams.set('sort_order', newOrder);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Pagination functionality\n\t\tfunction goToPage(event) {\n\t\t\tevent.preventDefault();\n\t\t\tconst page = event.target.closest('a').getAttribute('data-page');\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page', page);\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Page size functionality\n\t\tfunction changePageSize(newPageSize) {\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page_size', newPageSize);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page when changing page size\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Volume details\n\t\tfunction showVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tconst server = event.target.closest('button').getAttribute('data-server');\n\t\t\twindow.location.href = `/storage/volumes/${volumeId}/${server}`;\n\t\t}\n\n\t\t// EC Volume details\n\t\tfunction showEcVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\twindow.location.href = `/storage/ec-volumes/${volumeId}`;\n\t\t}\n\n\t\t// Repair EC Volume\n\t\tfunction repairEcVolume(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tif (confirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`)) {\n\t\t\t\t// TODO: Implement repair functionality\n\t\t\t\talert('Repair functionality will be implemented soon.');\n\t\t\t}\n\t\t}\n\t</script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "<script>\n\t\t// Sorting functionality\n\t\tfunction sortBy(field) {\n\t\t\tconst currentSort = new URLSearchParams(window.location.search).get('sort_by');\n\t\t\tconst currentOrder = new URLSearchParams(window.location.search).get('sort_order') || 'asc';\n\t\t\t\n\t\t\tlet newOrder = 'asc';\n\t\t\tif (currentSort === field && currentOrder === 'asc') {\n\t\t\t\tnewOrder = 'desc';\n\t\t\t}\n\t\t\t\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('sort_by', field);\n\t\t\turl.searchParams.set('sort_order', newOrder);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Pagination functionality\n\t\tfunction goToPage(event) {\n\t\t\tevent.preventDefault();\n\t\t\tconst page = event.target.closest('a').getAttribute('data-page');\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page', page);\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Page size functionality\n\t\tfunction changePageSize(newPageSize) {\n\t\t\tconst url = new URL(window.location);\n\t\t\turl.searchParams.set('page_size', newPageSize);\n\t\t\turl.searchParams.set('page', '1'); // Reset to first page when changing page size\n\t\t\twindow.location.href = url.toString();\n\t\t}\n\n\t\t// Volume details\n\t\tfunction showVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\tconst server = event.target.closest('button').getAttribute('data-server');\n\t\t\twindow.location.href = `/storage/volumes/${volumeId}/${server}`;\n\t\t}\n\n\t\t// EC Volume details\n\t\tfunction showEcVolumeDetails(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n\t\t\twindow.location.href = `/storage/ec-volumes/${volumeId}`;\n\t\t}\n\n\t\t// Repair EC Volume\n\t\tfunction repairEcVolume(event) {\n\t\t\tconst volumeId = event.target.closest('button').getAttribute('data-volume-id');\n showConfirm(`Are you sure you want to repair missing shards for EC volume ${volumeId}?`, function() {\n\t\t\t\t// TODO: Implement repair functionality\n\t\t\t\tshowAlert('Repair functionality will be implemented soon.', 'info');\n\t\t\t});\n\t\t}\n\t</script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

9
weed/admin/view/app/file_browser.templ

@ -365,9 +365,10 @@ templ FileBrowser(data dash.FileBrowserData) {
showFileProperties(path);
break;
case 'delete':
if (confirm('Are you sure you want to delete "' + path + '"?')) {
const fileName = path.split('/').pop();
showDeleteConfirm(fileName, function() {
deleteFile(path);
}
}, `Are you sure you want to delete "${fileName}"? This action cannot be undone.`);
break;
}
});
@ -395,14 +396,14 @@ templ FileBrowser(data dash.FileBrowserData) {
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error loading file properties: ' + data.error);
showAlert('Error loading file properties: ' + data.error, 'error');
} else {
displayFileProperties(data);
}
})
.catch(error => {
console.error('Error fetching file properties:', error);
alert('Error loading file properties: ' + error.message);
showAlert('Error loading file properties: ' + error.message, 'error');
});
}

2
weed/admin/view/app/file_browser_templ.go
File diff suppressed because it is too large
View File

4
weed/admin/view/app/maintenance_config.templ

@ -251,7 +251,7 @@ templ MaintenanceConfig(data *maintenance.MaintenanceConfigData) {
}
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
// Reset form to defaults (matching DefaultMaintenanceConfig values)
document.getElementById('enabled').checked = false;
document.getElementById('scanInterval').value = '30';
@ -261,7 +261,7 @@ templ MaintenanceConfig(data *maintenance.MaintenanceConfigData) {
document.getElementById('maxRetries').value = '3';
document.getElementById('retryDelay').value = '15';
document.getElementById('taskRetention').value = '7';
}
});
}
</script>
}

22
weed/admin/view/app/maintenance_config_schema.templ

@ -157,8 +157,10 @@ templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *m
})
.then(response => {
if (response.status === 401) {
alert('Authentication required. Please log in first.');
window.location.href = '/login';
showAlert('Authentication required. Please log in first.', 'warning');
setTimeout(() => {
window.location.href = '/login';
}, 2000);
return;
}
return response.json();
@ -166,20 +168,20 @@ templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *m
.then(data => {
if (!data) return; // Skip if redirected to login
if (data.success) {
alert('Configuration saved successfully!');
showAlert('Configuration saved successfully!', 'success');
location.reload();
} else {
alert('Error saving configuration: ' + (data.error || 'Unknown error'));
showAlert('Error saving configuration: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error saving configuration: ' + error.message);
showAlert('Error saving configuration: ' + error.message, 'error');
});
}
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
fetch('/maintenance/config/defaults', {
method: 'POST',
headers: {
@ -189,17 +191,17 @@ templ MaintenanceConfigSchema(data *maintenance.MaintenanceConfigData, schema *m
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Configuration reset to defaults!');
showAlert('Configuration reset to defaults!', 'success');
location.reload();
} else {
alert('Error resetting configuration: ' + (data.error || 'Unknown error'));
showAlert('Error resetting configuration: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error resetting configuration: ' + error.message);
showAlert('Error resetting configuration: ' + error.message, 'error');
});
}
});
}
</script>
}

50
weed/admin/view/app/maintenance_config_schema_templ.go
File diff suppressed because it is too large
View File

2
weed/admin/view/app/maintenance_config_templ.go

@ -273,7 +273,7 @@ func MaintenanceConfig(data *maintenance.MaintenanceConfigData) templ.Component
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div></div></div></div></div></div></div></div><script>\n function saveConfiguration() {\n // First, get current configuration to preserve existing values\n fetch('/api/maintenance/config')\n .then(response => response.json())\n .then(currentConfig => {\n // Update only the fields from the form\n const updatedConfig = {\n ...currentConfig.config, // Preserve existing config\n enabled: document.getElementById('enabled').checked,\n scan_interval_seconds: parseInt(document.getElementById('scanInterval').value) * 60, // Convert to seconds\n worker_timeout_seconds: parseInt(document.getElementById('workerTimeout').value) * 60, // Convert to seconds\n task_timeout_seconds: parseInt(document.getElementById('taskTimeout').value) * 3600, // Convert to seconds\n retry_delay_seconds: parseInt(document.getElementById('retryDelay').value) * 60, // Convert to seconds\n max_retries: parseInt(document.getElementById('maxRetries').value),\n task_retention_seconds: parseInt(document.getElementById('taskRetention').value) * 24 * 3600, // Convert to seconds\n policy: {\n ...currentConfig.config.policy, // Preserve existing policy\n global_max_concurrent: parseInt(document.getElementById('globalMaxConcurrent').value)\n }\n };\n\n // Send the updated configuration\n return fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updatedConfig)\n });\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration saved successfully');\n location.reload(); // Reload to show updated values\n } else {\n alert('Failed to save configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {\n // Reset form to defaults (matching DefaultMaintenanceConfig values)\n document.getElementById('enabled').checked = false;\n document.getElementById('scanInterval').value = '30';\n document.getElementById('workerTimeout').value = '5';\n document.getElementById('taskTimeout').value = '2';\n document.getElementById('globalMaxConcurrent').value = '4';\n document.getElementById('maxRetries').value = '3';\n document.getElementById('retryDelay').value = '15';\n document.getElementById('taskRetention').value = '7';\n }\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "</p></div></div></div></div></div></div></div></div><script>\n function saveConfiguration() {\n // First, get current configuration to preserve existing values\n fetch('/api/maintenance/config')\n .then(response => response.json())\n .then(currentConfig => {\n // Update only the fields from the form\n const updatedConfig = {\n ...currentConfig.config, // Preserve existing config\n enabled: document.getElementById('enabled').checked,\n scan_interval_seconds: parseInt(document.getElementById('scanInterval').value) * 60, // Convert to seconds\n worker_timeout_seconds: parseInt(document.getElementById('workerTimeout').value) * 60, // Convert to seconds\n task_timeout_seconds: parseInt(document.getElementById('taskTimeout').value) * 3600, // Convert to seconds\n retry_delay_seconds: parseInt(document.getElementById('retryDelay').value) * 60, // Convert to seconds\n max_retries: parseInt(document.getElementById('maxRetries').value),\n task_retention_seconds: parseInt(document.getElementById('taskRetention').value) * 24 * 3600, // Convert to seconds\n policy: {\n ...currentConfig.config.policy, // Preserve existing policy\n global_max_concurrent: parseInt(document.getElementById('globalMaxConcurrent').value)\n }\n };\n\n // Send the updated configuration\n return fetch('/api/maintenance/config', {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(updatedConfig)\n });\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration saved successfully');\n location.reload(); // Reload to show updated values\n } else {\n alert('Failed to save configuration: ' + (data.error || 'Unknown error'));\n }\n })\n .catch(error => {\n alert('Error: ' + error.message);\n });\n }\n\n function resetToDefaults() {\n showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {\n // Reset form to defaults (matching DefaultMaintenanceConfig values)\n document.getElementById('enabled').checked = false;\n document.getElementById('scanInterval').value = '30';\n document.getElementById('workerTimeout').value = '5';\n document.getElementById('taskTimeout').value = '2';\n document.getElementById('globalMaxConcurrent').value = '4';\n document.getElementById('maxRetries').value = '3';\n document.getElementById('retryDelay').value = '15';\n document.getElementById('taskRetention').value = '7';\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

19
weed/admin/view/app/maintenance_workers.templ

@ -247,7 +247,8 @@ templ MaintenanceWorkers(data *dash.MaintenanceWorkersData) {
var modal = new bootstrap.Modal(document.getElementById('workerDetailsModal'));
// Load worker details
fetch('/api/maintenance/workers/' + workerID)
const encodedWorkerId = encodeURIComponent(workerID);
fetch('/api/maintenance/workers/' + encodedWorkerId)
.then(response => response.json())
.then(data => {
const content = document.getElementById('workerDetailsContent');
@ -302,23 +303,27 @@ templ MaintenanceWorkers(data *dash.MaintenanceWorkersData) {
function pauseWorker(event) {
const workerID = event.target.closest('button').getAttribute('data-worker-id');
if (confirm('Are you sure you want to pause this worker?')) {
fetch('/api/maintenance/workers/' + workerID + '/pause', {
method: 'POST'
showConfirm(`Are you sure you want to pause worker ${workerID}?`, function() {
const encodedWorkerId = encodeURIComponent(workerID);
fetch('/api/maintenance/workers/' + encodedWorkerId + '/pause', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
location.reload();
} else {
alert('Failed to pause worker: ' + data.error);
showAlert('Failed to pause worker: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error pausing worker:', error);
alert('Failed to pause worker');
showAlert('Failed to pause worker', 'error');
});
}
});
}
function formatDuration(nanoseconds) {

2
weed/admin/view/app/maintenance_workers_templ.go
File diff suppressed because it is too large
View File

103
weed/admin/view/app/object_store_users.templ

@ -516,11 +516,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
} else {
const error = await response.json().catch(() => ({}));
showErrorMessage('Failed to load policies: ' + (error.error || 'Unknown error'));
showAlert('Failed to load policies: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error loading policies:', error);
showErrorMessage('Failed to load policies: ' + error.message);
showAlert('Failed to load policies: ' + error.message, 'error');
}
}
@ -691,25 +691,27 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Show user details modal
async function showUserDetails(username) {
try {
const response = await fetch(`/api/users/${username}`);
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`);
if (response.ok) {
const user = await response.json();
document.getElementById('userDetailsContent').innerHTML = createUserDetailsContent(user);
const modal = new bootstrap.Modal(document.getElementById('userDetailsModal'));
modal.show();
} else {
showErrorMessage('Failed to load user details');
showAlert('Failed to load user details', 'error');
}
} catch (error) {
console.error('Error loading user details:', error);
showErrorMessage('Failed to load user details');
showAlert('Failed to load user details', 'error');
}
}
// Edit user function
async function editUser(username) {
try {
const response = await fetch(`/api/users/${username}`);
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`);
if (response.ok) {
const user = await response.json();
@ -773,18 +775,19 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
} else {
showErrorMessage('Failed to load user details');
showAlert('Failed to load user details', 'error');
}
} catch (error) {
console.error('Error loading user:', error);
showErrorMessage('Failed to load user details');
showAlert('Failed to load user details', 'error');
}
}
// Manage access keys function
async function manageAccessKeys(username) {
try {
const response = await fetch(`/api/users/${username}`);
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`);
if (response.ok) {
const user = await response.json();
document.getElementById('accessKeysUsername').textContent = username;
@ -792,35 +795,14 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
const modal = new bootstrap.Modal(document.getElementById('accessKeysModal'));
modal.show();
} else {
showErrorMessage('Failed to load access keys');
showAlert('Failed to load access keys', 'error');
}
} catch (error) {
console.error('Error loading access keys:', error);
showErrorMessage('Failed to load access keys');
showAlert('Failed to load access keys', 'error');
}
}
// Delete user function
async function deleteUser(username) {
if (confirm(`Are you sure you want to delete user "${username}"? This action cannot be undone.`)) {
try {
const response = await fetch(`/api/users/${username}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccessMessage('User deleted successfully');
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to delete user: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting user:', error);
showErrorMessage('Failed to delete user: ' + error.message);
}
}
}
// Handle create user form submission
async function handleCreateUser() {
@ -863,11 +845,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to create user: ' + (error.error || 'Unknown error'));
showAlert('Failed to create user: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error creating user:', error);
showErrorMessage('Failed to create user: ' + error.message);
showAlert('Failed to create user: ' + error.message, 'error');
}
}
@ -876,7 +858,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
async function handleUpdateUser() {
const username = document.getElementById('editUsername').value;
if (!username) {
showErrorMessage('Username is required');
showAlert('Username is required', 'error');
return;
}
@ -885,13 +867,13 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Validate that permissions are not empty
if (!allActions || allActions.length === 0) {
showErrorMessage('At least one permission must be selected');
showAlert('At least one permission must be selected', 'error');
return;
}
// Check for null (validation failure from buildBucketPermissionsNew)
if (allActions === null) {
showErrorMessage('Please select at least one bucket when using specific bucket permissions');
showAlert('Please select at least one bucket when using specific bucket permissions', 'error');
return;
}
@ -902,7 +884,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
};
try {
const response = await fetch(`/api/users/${username}`, {
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
@ -919,11 +902,11 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
setTimeout(() => window.location.reload(), 1000);
} else {
const error = await response.json();
showErrorMessage('Failed to update user: ' + (error.error || 'Unknown error'));
showAlert('Failed to update user: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error updating user:', error);
showErrorMessage('Failed to update user: ' + error.message);
showAlert('Failed to update user: ' + error.message, 'error');
}
}
@ -1028,7 +1011,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
// Refresh access keys list content
async function refreshAccessKeysList(username) {
try {
const response = await fetch(`/api/users/${username}`);
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}`);
if (response.ok) {
const user = await response.json();
document.getElementById('accessKeysContent').innerHTML = createAccessKeysContent(user);
@ -1055,12 +1039,12 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to update access key status: ' + (error.error || 'Unknown error'));
showAlert('Failed to update access key status: ' + (error.error || 'Unknown error'), 'error');
refreshAccessKeysList(username);
}
} catch (error) {
console.error('Error updating access key status:', error);
showErrorMessage('Failed to update access key status: ' + error.message);
showAlert('Failed to update access key status: ' + error.message, 'error');
refreshAccessKeysList(username);
}
}
@ -1070,7 +1054,8 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
const username = document.getElementById('accessKeysUsername').textContent;
try {
const response = await fetch(`/api/users/${username}/access-keys`, {
const encodedUsername = encodeURIComponent(username);
const response = await fetch(`/api/users/${encodedUsername}/access-keys`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -1092,37 +1077,14 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to create access key: ' + (error.error || 'Unknown error'));
showAlert('Failed to create access key: ' + (error.error || 'Unknown error'), 'error');
}
} catch (error) {
console.error('Error creating access key:', error);
showErrorMessage('Failed to create access key: ' + error.message);
showAlert('Failed to create access key: ' + error.message, 'error');
}
}
// Delete access key
async function deleteAccessKey(username, accessKey) {
if (confirm('Are you sure you want to delete this access key?')) {
try {
const response = await fetch(`/api/users/${username}/access-keys/${accessKey}`, {
method: 'DELETE'
});
if (response.ok) {
showSuccessMessage('Access key deleted successfully');
// Refresh access keys display
refreshAccessKeysList(username);
} else {
const error = await response.json();
showErrorMessage('Failed to delete access key: ' + (error.error || 'Unknown error'));
}
} catch (error) {
console.error('Error deleting access key:', error);
showErrorMessage('Failed to delete access key: ' + error.message);
}
}
}
// Utility functions
@ -1132,8 +1094,7 @@ templ ObjectStoreUsers(data dash.ObjectStoreUsersData) {
}
function showErrorMessage(message) {
// Simple implementation - could be enhanced with toast notifications
alert('Error: ' + message);
showAlert(message, 'error');
}
function escapeHtml(text) {

2
weed/admin/view/app/object_store_users_templ.go
File diff suppressed because it is too large
View File

44
weed/admin/view/app/policies.templ

@ -351,7 +351,7 @@ templ Policies(data dash.PoliciesData) {
const policyDocumentText = formData.get('document');
if (!policyName || !policyDocumentText) {
alert('Please fill in all required fields');
showAlert('Please fill in all required fields', 'warning');
return;
}
@ -359,7 +359,7 @@ templ Policies(data dash.PoliciesData) {
try {
policyDocument = JSON.parse(policyDocumentText);
} catch (e) {
alert('Invalid JSON in policy document: ' + e.message);
showAlert('Invalid JSON in policy document: ' + e.message, 'error');
return;
}
@ -378,17 +378,17 @@ templ Policies(data dash.PoliciesData) {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy created successfully!');
showAlert('Policy created successfully!', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('createPolicyModal'));
if (modal) modal.hide();
location.reload(); // Refresh the page to show the new policy
} else {
alert('Error creating policy: ' + (data.error || 'Unknown error'));
showAlert('Error creating policy: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error creating policy: ' + error.message);
showAlert('Error creating policy: ' + error.message, 'error');
});
}
@ -507,7 +507,7 @@ templ Policies(data dash.PoliciesData) {
})
.catch(error => {
console.error('Error:', error);
alert('Error loading policy for editing: ' + error.message);
showAlert('Error loading policy for editing: ' + error.message, 'error');
const editModal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
if (editModal) editModal.hide();
});
@ -518,7 +518,7 @@ templ Policies(data dash.PoliciesData) {
const policyDocumentText = document.getElementById('editPolicyDocument').value;
if (!policyName || !policyDocumentText) {
alert('Please fill in all required fields');
showAlert('Please fill in all required fields', 'warning');
return;
}
@ -526,7 +526,7 @@ templ Policies(data dash.PoliciesData) {
try {
policyDocument = JSON.parse(policyDocumentText);
} catch (e) {
alert('Invalid JSON in policy document: ' + e.message);
showAlert('Invalid JSON in policy document: ' + e.message, 'error');
return;
}
@ -544,17 +544,17 @@ templ Policies(data dash.PoliciesData) {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy updated successfully!');
showAlert('Policy updated successfully!', 'success');
const modal = bootstrap.Modal.getInstance(document.getElementById('editPolicyModal'));
if (modal) modal.hide();
location.reload(); // Refresh the page to show the updated policy
} else {
alert('Error updating policy: ' + (data.error || 'Unknown error'));
showAlert('Error updating policy: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating policy: ' + error.message);
showAlert('Error updating policy: ' + error.message, 'error');
});
}
@ -610,7 +610,7 @@ templ Policies(data dash.PoliciesData) {
function validatePolicyJSON(policyText) {
if (!policyText) {
alert('Please enter a policy document first');
showAlert('Please enter a policy document first', 'warning');
return;
}
@ -619,40 +619,40 @@ templ Policies(data dash.PoliciesData) {
// Basic validation
if (!policy.Version) {
alert('Policy must have a Version field');
showAlert('Policy must have a Version field', 'error');
return;
}
if (!policy.Statement || !Array.isArray(policy.Statement)) {
alert('Policy must have a Statement array');
showAlert('Policy must have a Statement array', 'error');
return;
}
alert('Policy document is valid JSON!');
showAlert('Policy document is valid JSON!', 'success');
} catch (e) {
alert('Invalid JSON: ' + e.message);
showAlert('Invalid JSON: ' + e.message, 'error');
}
}
function deletePolicy(policyName) {
if (confirm('Are you sure you want to delete policy "' + policyName + '"?')) {
showDeleteConfirm(policyName, function() {
fetch('/api/object-store/policies/' + encodeURIComponent(policyName), {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Policy deleted successfully!');
location.reload(); // Refresh the page
showAlert('Policy deleted successfully!', 'success');
location.reload();
} else {
alert('Error deleting policy: ' + (data.error || 'Unknown error'));
showAlert('Error deleting policy: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error deleting policy: ' + error.message);
showAlert('Error deleting policy: ' + error.message, 'error');
});
}
});
}
</script>
}

2
weed/admin/view/app/policies_templ.go
File diff suppressed because it is too large
View File

13
weed/admin/view/app/service_accounts.templ

@ -335,7 +335,8 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
async function showSADetails(id) {
try {
const response = await fetch(`/api/service-accounts/${id}`);
const encodedId = encodeURIComponent(id);
const response = await fetch(`/api/service-accounts/${encodedId}`);
if (response.ok) {
const sa = await response.json();
document.getElementById('saDetailsContent').innerHTML = createSADetailsContent(sa);
@ -353,7 +354,8 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
async function toggleSAStatus(id, currentStatus) {
const newStatus = currentStatus === 'Active' ? 'Inactive' : 'Active';
try {
const response = await fetch(`/api/service-accounts/${id}`, {
const encodedId = encodeURIComponent(id);
const response = await fetch(`/api/service-accounts/${encodedId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ status: newStatus })
@ -373,9 +375,10 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
}
async function deleteSA(id) {
if (confirm('Are you sure you want to delete this service account? This action cannot be undone.')) {
showDeleteConfirm(id, async function() {
try {
const response = await fetch(`/api/service-accounts/${id}`, {
const encodedId = encodeURIComponent(id);
const response = await fetch(`/api/service-accounts/${encodedId}`, {
method: 'DELETE'
});
@ -390,7 +393,7 @@ templ ServiceAccounts(data dash.ServiceAccountsData) {
console.error('Error deleting service account:', error);
showErrorMessage('Failed to delete: ' + error.message);
}
}
}, 'Are you sure you want to delete this service account? This action cannot be undone.');
}
async function handleCreateServiceAccount() {

2
weed/admin/view/app/service_accounts_templ.go
File diff suppressed because it is too large
View File

4
weed/admin/view/app/task_config.templ

@ -104,13 +104,13 @@ templ TaskConfig(data *maintenance.TaskConfigData) {
<script>
function resetForm() {
if (confirm('Are you sure you want to reset all settings to their default values?')) {
showConfirm('Are you sure you want to reset all settings to their default values?', function() {
// Find all form inputs and reset them
const form = document.querySelector('form');
if (form) {
form.reset();
}
}
});
}
// Auto-save form data to localStorage for recovery

4
weed/admin/view/app/task_config_schema.templ

@ -127,7 +127,7 @@ templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfi
<script>
function resetToDefaults() {
if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {
showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {
// Reset form fields to their default values
const form = document.getElementById('taskConfigForm');
const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};
@ -154,7 +154,7 @@ templ TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfi
}
}
});
}
});
}
function convertSecondsToTaskIntervalValueUnit(totalSeconds) {

2
weed/admin/view/app/task_config_schema_templ.go

@ -170,7 +170,7 @@ func TaskConfigSchema(data *maintenance.TaskConfigData, schema *tasks.TaskConfig
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div></div></div></div><script>\n function resetToDefaults() {\n if (confirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.')) {\n // Reset form fields to their default values\n const form = document.getElementById('taskConfigForm');\n const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};\n \n Object.keys(schemaFields).forEach(fieldName => {\n const field = schemaFields[fieldName];\n const element = document.getElementById(fieldName);\n \n if (element && field.default_value !== undefined) {\n if (field.input_type === 'checkbox') {\n element.checked = field.default_value;\n } else if (field.input_type === 'interval') {\n // Handle interval fields with value and unit\n const valueElement = document.getElementById(fieldName + '_value');\n const unitElement = document.getElementById(fieldName + '_unit');\n if (valueElement && unitElement && field.default_value) {\n const defaultSeconds = field.default_value;\n const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);\n valueElement.value = value;\n unitElement.value = unit;\n }\n } else {\n element.value = field.default_value;\n }\n }\n });\n }\n }\n\n function convertSecondsToTaskIntervalValueUnit(totalSeconds) {\n if (totalSeconds === 0) {\n return { value: 0, unit: 'minutes' };\n }\n\n // Check if it's evenly divisible by days\n if (totalSeconds % (24 * 3600) === 0) {\n return { value: totalSeconds / (24 * 3600), unit: 'days' };\n }\n\n // Check if it's evenly divisible by hours\n if (totalSeconds % 3600 === 0) {\n return { value: totalSeconds / 3600, unit: 'hours' };\n }\n\n // Default to minutes\n return { value: totalSeconds / 60, unit: 'minutes' };\n }\n\n // Store schema data for JavaScript access (moved to after div is created)\n </script><!-- Hidden element to store schema data --><div data-task-schema=\"")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</div></div></div></div></div></div><script>\n function resetToDefaults() {\n showConfirm('Are you sure you want to reset to default configuration? This will overwrite your current settings.', function() {\n // Reset form fields to their default values\n const form = document.getElementById('taskConfigForm');\n const schemaFields = window.taskConfigSchema ? window.taskConfigSchema.fields : {};\n \n Object.keys(schemaFields).forEach(fieldName => {\n const field = schemaFields[fieldName];\n const element = document.getElementById(fieldName);\n \n if (element && field.default_value !== undefined) {\n if (field.input_type === 'checkbox') {\n element.checked = field.default_value;\n } else if (field.input_type === 'interval') {\n // Handle interval fields with value and unit\n const valueElement = document.getElementById(fieldName + '_value');\n const unitElement = document.getElementById(fieldName + '_unit');\n if (valueElement && unitElement && field.default_value) {\n const defaultSeconds = field.default_value;\n const { value, unit } = convertSecondsToTaskIntervalValueUnit(defaultSeconds);\n valueElement.value = value;\n unitElement.value = unit;\n }\n } else {\n element.value = field.default_value;\n }\n }\n });\n });\n }\n\n function convertSecondsToTaskIntervalValueUnit(totalSeconds) {\n if (totalSeconds === 0) {\n return { value: 0, unit: 'minutes' };\n }\n\n // Check if it's evenly divisible by days\n if (totalSeconds % (24 * 3600) === 0) {\n return { value: totalSeconds / (24 * 3600), unit: 'days' };\n }\n\n // Check if it's evenly divisible by hours\n if (totalSeconds % 3600 === 0) {\n return { value: totalSeconds / 3600, unit: 'hours' };\n }\n\n // Default to minutes\n return { value: totalSeconds / 60, unit: 'minutes' };\n }\n\n // Store schema data for JavaScript access (moved to after div is created)\n </script><!-- Hidden element to store schema data --><div data-task-schema=\"")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

2
weed/admin/view/app/task_config_templ.go

@ -163,7 +163,7 @@ func TaskConfig(data *maintenance.TaskConfigData) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div></div></div></div></div></div></div><script>\n function resetForm() {\n if (confirm('Are you sure you want to reset all settings to their default values?')) {\n // Find all form inputs and reset them\n const form = document.querySelector('form');\n if (form) {\n form.reset();\n }\n }\n }\n\n // Auto-save form data to localStorage for recovery\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n const taskType = '{string(data.TaskType)}';\n const storageKey = 'taskConfig_' + taskType;\n\n // Load saved data\n const savedData = localStorage.getItem(storageKey);\n if (savedData) {\n try {\n const data = JSON.parse(savedData);\n Object.keys(data).forEach(key => {\n const input = form.querySelector(`[name=\"${key}\"]`);\n if (input) {\n if (input.type === 'checkbox') {\n input.checked = data[key];\n } else {\n input.value = data[key];\n }\n }\n });\n } catch (e) {\n console.warn('Failed to load saved configuration:', e);\n }\n }\n\n // Save data on input change\n form.addEventListener('input', function() {\n const formData = new FormData(form);\n const data = {};\n for (let [key, value] of formData.entries()) {\n data[key] = value;\n }\n localStorage.setItem(storageKey, JSON.stringify(data));\n });\n\n // Clear saved data on successful submit\n form.addEventListener('submit', function() {\n localStorage.removeItem(storageKey);\n });\n }\n });\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</p></div></div></div></div></div></div></div><script>\n function resetForm() {\n showConfirm('Are you sure you want to reset all settings to their default values?', function() {\n // Find all form inputs and reset them\n const form = document.querySelector('form');\n if (form) {\n form.reset();\n }\n });\n }\n\n // Auto-save form data to localStorage for recovery\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n const taskType = '{string(data.TaskType)}';\n const storageKey = 'taskConfig_' + taskType;\n\n // Load saved data\n const savedData = localStorage.getItem(storageKey);\n if (savedData) {\n try {\n const data = JSON.parse(savedData);\n Object.keys(data).forEach(key => {\n const input = form.querySelector(`[name=\"${key}\"]`);\n if (input) {\n if (input.type === 'checkbox') {\n input.checked = data[key];\n } else {\n input.value = data[key];\n }\n }\n });\n } catch (e) {\n console.warn('Failed to load saved configuration:', e);\n }\n }\n\n // Save data on input change\n form.addEventListener('input', function() {\n const formData = new FormData(form);\n const data = {};\n for (let [key, value] of formData.entries()) {\n data[key] = value;\n }\n localStorage.setItem(storageKey, JSON.stringify(data));\n });\n\n // Clear saved data on successful submit\n form.addEventListener('submit', function() {\n localStorage.removeItem(storageKey);\n });\n }\n });\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

4
weed/admin/view/app/task_config_templ.templ

@ -121,9 +121,9 @@ templ TaskConfigTempl(data *TaskConfigTemplData) {
// Reset form function
function resetForm() {
if (confirm('Are you sure you want to reset all changes?')) {
showConfirm('Are you sure you want to reset all changes?', function() {
location.reload();
}
});
}
// Test configuration function

2
weed/admin/view/app/task_config_templ_templ.go

@ -101,7 +101,7 @@ func TaskConfigTempl(data *TaskConfigTemplData) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Form actions --><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-outline-secondary ms-2\" onclick=\"resetForm()\"><i class=\"fas fa-undo me-1\"></i> Reset</button></div><div><button type=\"button\" class=\"btn btn-outline-info\" onclick=\"testConfiguration()\"><i class=\"fas fa-play me-1\"></i> Test Configuration</button></div></div></div></div></div></div></form></div><script>\n // Form validation\n (function() {\n 'use strict';\n window.addEventListener('load', function() {\n var forms = document.getElementsByClassName('needs-validation');\n var validation = Array.prototype.filter.call(forms, function(form) {\n form.addEventListener('submit', function(event) {\n if (form.checkValidity() === false) {\n event.preventDefault();\n event.stopPropagation();\n }\n form.classList.add('was-validated');\n }, false);\n });\n }, false);\n })();\n\n // Auto-save functionality\n let autoSaveTimeout;\n function autoSave() {\n clearTimeout(autoSaveTimeout);\n autoSaveTimeout = setTimeout(function() {\n const formData = new FormData(document.querySelector('form'));\n localStorage.setItem('task_config_' + '{data.TaskType}', JSON.stringify(Object.fromEntries(formData)));\n }, 1000);\n }\n\n // Add auto-save listeners to all form inputs\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n form.addEventListener('input', autoSave);\n form.addEventListener('change', autoSave);\n }\n });\n\n // Reset form function\n function resetForm() {\n if (confirm('Are you sure you want to reset all changes?')) {\n location.reload();\n }\n }\n\n // Test configuration function\n function testConfiguration() {\n const formData = new FormData(document.querySelector('form'));\n \n // Show loading state\n const testBtn = document.querySelector('button[onclick=\"testConfiguration()\"]');\n const originalContent = testBtn.innerHTML;\n testBtn.innerHTML = '<i class=\"fas fa-spinner fa-spin me-1\"></i>Testing...';\n testBtn.disabled = true;\n \n fetch('/maintenance/config/{data.TaskType}/test', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration test successful!');\n } else {\n alert('Configuration test failed: ' + data.error);\n }\n })\n .catch(error => {\n alert('Test failed: ' + error);\n })\n .finally(() => {\n testBtn.innerHTML = originalContent;\n testBtn.disabled = false;\n });\n }\n </script>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<!-- Form actions --><div class=\"row\"><div class=\"col-12\"><div class=\"card\"><div class=\"card-body\"><div class=\"d-flex justify-content-between\"><div><button type=\"submit\" class=\"btn btn-primary\"><i class=\"fas fa-save me-1\"></i> Save Configuration</button> <button type=\"button\" class=\"btn btn-outline-secondary ms-2\" onclick=\"resetForm()\"><i class=\"fas fa-undo me-1\"></i> Reset</button></div><div><button type=\"button\" class=\"btn btn-outline-info\" onclick=\"testConfiguration()\"><i class=\"fas fa-play me-1\"></i> Test Configuration</button></div></div></div></div></div></div></form></div><script>\n // Form validation\n (function() {\n 'use strict';\n window.addEventListener('load', function() {\n var forms = document.getElementsByClassName('needs-validation');\n var validation = Array.prototype.filter.call(forms, function(form) {\n form.addEventListener('submit', function(event) {\n if (form.checkValidity() === false) {\n event.preventDefault();\n event.stopPropagation();\n }\n form.classList.add('was-validated');\n }, false);\n });\n }, false);\n })();\n\n // Auto-save functionality\n let autoSaveTimeout;\n function autoSave() {\n clearTimeout(autoSaveTimeout);\n autoSaveTimeout = setTimeout(function() {\n const formData = new FormData(document.querySelector('form'));\n localStorage.setItem('task_config_' + '{data.TaskType}', JSON.stringify(Object.fromEntries(formData)));\n }, 1000);\n }\n\n // Add auto-save listeners to all form inputs\n document.addEventListener('DOMContentLoaded', function() {\n const form = document.querySelector('form');\n if (form) {\n form.addEventListener('input', autoSave);\n form.addEventListener('change', autoSave);\n }\n });\n\n // Reset form function\n function resetForm() {\n showConfirm('Are you sure you want to reset all changes?', function() {\n location.reload();\n });\n }\n\n // Test configuration function\n function testConfiguration() {\n const formData = new FormData(document.querySelector('form'));\n \n // Show loading state\n const testBtn = document.querySelector('button[onclick=\"testConfiguration()\"]');\n const originalContent = testBtn.innerHTML;\n testBtn.innerHTML = '<i class=\"fas fa-spinner fa-spin me-1\"></i>Testing...';\n testBtn.disabled = true;\n \n fetch('/maintenance/config/{data.TaskType}/test', {\n method: 'POST',\n body: formData\n })\n .then(response => response.json())\n .then(data => {\n if (data.success) {\n alert('Configuration test successful!');\n } else {\n alert('Configuration test failed: ' + data.error);\n }\n })\n .catch(error => {\n alert('Test failed: ' + error);\n })\n .finally(() => {\n testBtn.innerHTML = originalContent;\n testBtn.disabled = false;\n });\n }\n </script>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}

20
weed/admin/view/app/task_detail.templ

@ -992,7 +992,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
function downloadTaskLogs() {
if (!currentTaskId || !currentWorkerId) {
alert('No task logs to download');
showAlert('No task logs to download', 'info');
return;
}
@ -1003,7 +1003,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
.then(response => response.json())
.then(data => {
if (data.error) {
alert('Error downloading logs: ' + data.error);
showAlert('Error downloading logs: ' + data.error, 'error');
return;
}
@ -1051,12 +1051,12 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
URL.revokeObjectURL(url);
})
.catch(error => {
alert('Error downloading logs: ' + error.message);
showAlert('Error downloading logs: ' + error.message, 'error');
});
}
function cancelTask(taskId) {
if (confirm('Are you sure you want to cancel this task?')) {
showConfirm('Are you sure you want to cancel this task?', function() {
fetch(`/api/maintenance/tasks/${taskId}/cancel`, {
method: 'POST',
headers: {
@ -1066,17 +1066,17 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
.then(response => response.json())
.then(data => {
if (data.success) {
alert('Task cancelled successfully');
showAlert('Task cancelled successfully', 'success');
location.reload();
} else {
alert('Error cancelling task: ' + data.error);
showAlert('Error cancelling task: ' + data.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
alert('Error cancelling task');
showAlert('Error cancelling task', 'error');
});
}
});
}
function refreshTaskLogs(taskId) {
@ -1087,7 +1087,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
})
.catch(error => {
console.error('Error:', error);
alert('Error refreshing logs');
showAlert('Error refreshing logs', 'error');
});
}
@ -1106,7 +1106,7 @@ templ TaskDetail(data *maintenance.TaskDetailData) {
})
.catch(error => {
console.error('Error:', error);
alert('Error exporting task detail');
showAlert('Error exporting task detail', 'error');
});
}

2
weed/admin/view/app/task_detail_templ.go
File diff suppressed because it is too large
View File

3
weed/admin/view/layout/layout.templ

@ -357,8 +357,11 @@ templ Layout(c *gin.Context, content templ.Component) {
<!-- Bootstrap JS -->
<script src="/static/js/bootstrap.bundle.min.js"></script>
<!-- Modal Alerts JS (replaces native alert/confirm) -->
<script src="/static/js/modal-alerts.js"></script>
<!-- Custom JS -->
<script src="/static/js/admin.js"></script>
<script src="/static/js/iam-utils.js"></script>
</body>
</html>
}

8
weed/admin/view/layout/layout_templ.go

@ -517,7 +517,7 @@ func Layout(c *gin.Context, content templ.Component) templ.Component {
return templ_7745c5c3_Err
}
}
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script></body></html>")
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</small></div></footer><!-- Bootstrap JS --><script src=\"/static/js/bootstrap.bundle.min.js\"></script><!-- Modal Alerts JS (replaces native alert/confirm) --><script src=\"/static/js/modal-alerts.js\"></script><!-- Custom JS --><script src=\"/static/js/admin.js\"></script><script src=\"/static/js/iam-utils.js\"></script></body></html>")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@ -553,7 +553,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var28 string
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 371, Col: 17}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 374, Col: 17}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
if templ_7745c5c3_Err != nil {
@ -566,7 +566,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var29 string
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(title)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 385, Col: 57}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 388, Col: 57}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
if templ_7745c5c3_Err != nil {
@ -584,7 +584,7 @@ func LoginForm(c *gin.Context, title string, errorMessage string) templ.Componen
var templ_7745c5c3_Var30 string
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
if templ_7745c5c3_Err != nil {
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 392, Col: 45}
return templ.Error{Err: templ_7745c5c3_Err, FileName: `view/layout/layout.templ`, Line: 395, Col: 45}
}
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
if templ_7745c5c3_Err != nil {

1
weed/command/imports.go

@ -36,5 +36,4 @@ import (
_ "github.com/seaweedfs/seaweedfs/weed/filer/ydb"
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_etc"
_ "github.com/seaweedfs/seaweedfs/weed/credential/filer_multiple"
)

5
weed/command/scaffold/credential.toml

@ -12,11 +12,6 @@
enabled = true
# filer address and grpc_dial_option will be automatically configured by the server
# Multi-file credential store (stores each user/policy in a separate file)
[credential.filer_multiple]
enabled = false
# filer address and grpc_dial_option will be automatically configured by the server
# PostgreSQL credential store (recommended for multi-node deployments)
[credential.postgres]

45
weed/credential/credential_manager.go

@ -125,6 +125,26 @@ func (cm *CredentialManager) GetPolicy(ctx context.Context, name string) (*polic
return cm.store.GetPolicy(ctx, name)
}
// CreatePolicy creates a new policy (if supported by the store)
func (cm *CredentialManager) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
// Check if the store implements PolicyManager interface with CreatePolicy
if policyStore, ok := cm.store.(PolicyManager); ok {
return policyStore.CreatePolicy(ctx, name, document)
}
// Fallback to PutPolicy for stores that only implement CredentialStore
return cm.store.PutPolicy(ctx, name, document)
}
// UpdatePolicy updates an existing policy (if supported by the store)
func (cm *CredentialManager) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
// Check if the store implements PolicyManager interface with UpdatePolicy
if policyStore, ok := cm.store.(PolicyManager); ok {
return policyStore.UpdatePolicy(ctx, name, document)
}
// Fallback to PutPolicy for stores that only implement CredentialStore
return cm.store.PutPolicy(ctx, name, document)
}
// Shutdown performs cleanup
func (cm *CredentialManager) Shutdown() {
if cm.store != nil {
@ -152,3 +172,28 @@ func GetAvailableStores() []CredentialStoreTypeName {
}
return storeNames
}
// CreateServiceAccount creates a new service account
func (cm *CredentialManager) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
return cm.store.CreateServiceAccount(ctx, sa)
}
// UpdateServiceAccount updates an existing service account
func (cm *CredentialManager) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
return cm.store.UpdateServiceAccount(ctx, id, sa)
}
// DeleteServiceAccount removes a service account
func (cm *CredentialManager) DeleteServiceAccount(ctx context.Context, id string) error {
return cm.store.DeleteServiceAccount(ctx, id)
}
// GetServiceAccount retrieves a service account by ID
func (cm *CredentialManager) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
return cm.store.GetServiceAccount(ctx, id)
}
// ListServiceAccounts returns all service accounts
func (cm *CredentialManager) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
return cm.store.ListServiceAccounts(ctx)
}

24
weed/credential/credential_store.go

@ -11,9 +11,10 @@ import (
)
var (
ErrUserNotFound = errors.New("user not found")
ErrUserAlreadyExists = errors.New("user already exists")
ErrAccessKeyNotFound = errors.New("access key not found")
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")
)
// CredentialStoreTypeName represents the type name of a credential store
@ -21,11 +22,10 @@ type CredentialStoreTypeName string
// Credential store name constants
const (
StoreTypeMemory CredentialStoreTypeName = "memory"
StoreTypeFilerEtc CredentialStoreTypeName = "filer_etc"
StoreTypeFilerMultiple CredentialStoreTypeName = "filer_multiple"
StoreTypePostgres CredentialStoreTypeName = "postgres"
StoreTypeGrpc CredentialStoreTypeName = "grpc"
StoreTypeMemory CredentialStoreTypeName = "memory"
StoreTypeFilerEtc CredentialStoreTypeName = "filer_etc"
StoreTypePostgres CredentialStoreTypeName = "postgres"
StoreTypeGrpc CredentialStoreTypeName = "grpc"
)
// CredentialStore defines the interface for user credential storage and retrieval
@ -73,6 +73,14 @@ type CredentialStore interface {
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)
// Shutdown performs cleanup when the store is being shut down
Shutdown()
}

210
weed/credential/filer_etc/filer_etc_identity.go

@ -1,10 +1,10 @@
package filer_etc
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/filer"
@ -14,10 +14,10 @@ import (
)
const (
IamIdentitiesDirectory = "identities"
IamConfigurationFile = "configuration.json"
IamLegacyIdentityFile = "identity.json"
IamLegacyIdentityOldFile = "identity.json.old"
IamIdentitiesDirectory = "identities"
IamServiceAccountsDirectory = "service_accounts"
IamLegacyIdentityFile = "identity.json"
IamLegacyIdentityOldFile = "identity.json.old"
)
func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
@ -35,13 +35,17 @@ func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3Ap
}
}
// 2. Load from multi-file structure (high priority, overrides legacy)
// This will merge identities into s3cfg
// 2. Load from multi-file structure (high priority, overrides legacy details)
if _, err := store.loadFromMultiFile(ctx, s3cfg); err != nil {
return s3cfg, err
}
// 3. Perform migration if we loaded legacy config
// 3. Load service accounts
if err := store.loadServiceAccountsFromMultiFile(ctx, s3cfg); err != nil {
return s3cfg, fmt.Errorf("failed to load service accounts: %w", err)
}
// 4. Perform migration if we loaded legacy config
// This ensures that all identities (including legacy ones) are written to individual files
// and the legacy file is renamed.
if foundLegacy {
@ -72,8 +76,11 @@ func (store *FilerEtcStore) loadFromMultiFile(ctx context.Context, s3cfg *iam_pb
dir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
// If directory doesn't exist, it's not multi-file yet
return nil
if err == filer_pb.ErrNotFound {
// If directory doesn't exist, it's not multi-file yet
return nil
}
return err
}
for _, entry := range entries {
@ -117,20 +124,6 @@ func (store *FilerEtcStore) loadFromMultiFile(ctx context.Context, s3cfg *iam_pb
return false, err
}
// 2. Load configuration.json (Accounts, etc.)
content, found, err := store.readInsideFiler(filer.IamConfigDirectory, IamConfigurationFile)
if err != nil {
return false, err
}
if found && len(content) > 0 {
tempCfg := &iam_pb.S3ApiConfiguration{}
if err := filer.ParseS3ConfigurationFromBytes(content, tempCfg); err == nil {
// Overwrite accounts from configuration.json (high priority)
s3cfg.Accounts = tempCfg.Accounts
}
return true, nil
}
return hasIdentities, nil
}
@ -144,27 +137,16 @@ func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, s3cfg *iam_p
}
}
// 2. Save rest of configuration
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// Create config with only accounts
cleanCfg := &iam_pb.S3ApiConfiguration{
Accounts: s3cfg.Accounts,
}
var buf bytes.Buffer
if err := filer.ProtoToText(&buf, cleanCfg); err != nil {
// 2. Save all service accounts
for _, sa := range s3cfg.ServiceAccounts {
if err := store.saveServiceAccount(ctx, sa); err != nil {
return err
}
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, IamConfigurationFile, buf.Bytes())
}); err != nil {
return err
}
// 3. Rename legacy file
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// We use atomic rename if possible, but Filer 'AtomicRenameEntry' exists in filer_pb
// util.JoinPath(filer.IamConfigDirectory, IamLegacyIdentityFile)
_, err := client.AtomicRenameEntry(context.Background(), &filer_pb.AtomicRenameEntryRequest{
_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
OldDirectory: filer.IamConfigDirectory,
OldName: IamLegacyIdentityFile,
NewDirectory: filer.IamConfigDirectory,
@ -182,31 +164,22 @@ func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_p
}
}
// 2. Save configuration file (accounts)
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
cleanCfg := &iam_pb.S3ApiConfiguration{
Accounts: config.Accounts,
}
var buf bytes.Buffer
if err := filer.ProtoToText(&buf, cleanCfg); err != nil {
// 2. Save all service accounts
for _, sa := range config.ServiceAccounts {
if err := store.saveServiceAccount(ctx, sa); err != nil {
return err
}
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, IamConfigurationFile, buf.Bytes())
})
if err != nil {
return err
}
// 3. Cleanup removed identities (Full Sync)
// Get list of existing identity files
// Compare with config.Identities
// Delete unknown ones
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
return nil // Should exist by now
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
validNames := make(map[string]bool)
@ -217,7 +190,7 @@ func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_p
for _, entry := range entries {
if !entry.IsDirectory && !validNames[entry.Name] {
// Delete obsolete identity file
if _, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
if _, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: dir,
Name: entry.Name,
}); err != nil {
@ -226,7 +199,42 @@ func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_p
}
}
return nil
})
}); err != nil {
return err
}
// 4. Cleanup removed service accounts (Full Sync)
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamServiceAccountsDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
validNames := make(map[string]bool)
for _, sa := range config.ServiceAccounts {
validNames[sa.Id+".json"] = true
}
for _, entry := range entries {
if !entry.IsDirectory && !validNames[entry.Name] {
if _, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: dir,
Name: entry.Name,
}); err != nil {
glog.Warningf("Failed to delete obsolete service account file %s: %v", entry.Name, err)
}
}
}
return nil
}); err != nil {
return err
}
return nil
}
func (store *FilerEtcStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
@ -274,16 +282,20 @@ func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, ide
}
func (store *FilerEtcStore) DeleteUser(ctx context.Context, username string) error {
// Verify existence first to return ErrUserNotFound if applicable
if _, err := store.GetUser(ctx, username); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.DeleteEntry(context.Background(), &filer_pb.DeleteEntryRequest{
_, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: filer.IamConfigDirectory + "/" + IamIdentitiesDirectory,
Name: username + ".json",
})
if err != nil {
// Map specific gRPC error to ErrUserNotFound if possible, but DeleteEntry usually returns success even if not found
// unless strict. 'credential.ErrUserNotFound' is expected by caller?
// The caller `DeleteUser` in handlers usually explicitly checks `ErrUserNotFound`.
// Ideally we verify existence first?
if strings.Contains(err.Error(), filer_pb.ErrNotFound.Error()) {
return credential.ErrUserNotFound
}
return err
}
return nil
@ -295,6 +307,9 @@ func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) {
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
entries, err := listEntries(ctx, client, filer.IamConfigDirectory+"/"+IamIdentitiesDirectory)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
for _, entry := range entries {
@ -307,29 +322,66 @@ func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) {
return usernames, err
}
// Access Key methods still need to operate on the identity object
// We can reuse GetUser / UpdateUser logic to avoid duplicating file IO code here,
// or implement optimized read-modify-write.
// Reusing GetUser/saveIdentity is cleanest.
func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
// This is inefficient in multi-file: requires scanning all files.
// Assuming number of users is not huge.
// For huge number of users, we'd need an index.
// Optimized: Iterate over identity files directly instead of loading full config.
// This avoids triggering migration side effects.
var foundIdentity *iam_pb.Identity
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
// If not found, check legacy file? No, optimization requested to avoid side effects.
// If migration hasn't run, this will return empty/not found.
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
for _, entry := range entries {
if entry.IsDirectory || !strings.HasSuffix(entry.Name, ".json") {
continue
}
// Read file content
var content []byte
if len(entry.Content) > 0 {
content = entry.Content
} else {
c, err := filer.ReadInsideFiler(client, dir, entry.Name)
if err != nil {
continue
}
content = c
}
if len(content) > 0 {
identity := &iam_pb.Identity{}
if err := json.Unmarshal(content, identity); err != nil {
continue
}
for _, cred := range identity.Credentials {
if cred.AccessKey == accessKey {
foundIdentity = identity
return nil // Found match, stop iteration
}
}
}
}
return nil
})
s3cfg, err := store.LoadConfiguration(ctx)
if err != nil {
return nil, err
}
for _, identity := range s3cfg.Identities {
for _, credential := range identity.Credentials {
if credential.AccessKey == accessKey {
// Return the specific identity
return identity, nil
}
}
if foundIdentity != nil {
return foundIdentity, nil
}
return nil, credential.ErrAccessKeyNotFound
}

224
weed/credential/filer_etc/filer_etc_policy.go

@ -3,22 +3,31 @@ package filer_etc
import (
"context"
"encoding/json"
"strings"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
)
const (
IamPoliciesDirectory = "policies"
IamLegacyPoliciesOldFile = "policies.json.old"
)
type PoliciesCollection struct {
Policies map[string]policy_engine.PolicyDocument `json:"policies"`
}
func validatePolicyName(name string) error {
return credential.ValidatePolicyName(name)
}
// GetPolicies retrieves all IAM policies from the filer
func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
policiesCollection := &PoliciesCollection{
Policies: make(map[string]policy_engine.PolicyDocument),
}
policies := make(map[string]policy_engine.PolicyDocument)
// Check if filer client is configured (with mutex protection)
store.mu.RLock()
@ -27,75 +36,142 @@ func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_
if !configured {
glog.V(1).Infof("Filer client not configured for policy retrieval, returning empty policies")
// Return empty policies if filer client is not configured
return policiesCollection.Policies, nil
return policies, nil
}
glog.V(2).Infof("Loading IAM policies from %s/%s (using current active filer)",
filer.IamConfigDirectory, filer.IamPoliciesFile)
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// Use ReadInsideFiler instead of ReadEntry since policies.json is small
// and stored inline. ReadEntry requires a master client for chunked files,
// but ReadInsideFiler only reads inline content.
content, err := filer.ReadInsideFiler(client, filer.IamConfigDirectory, filer.IamPoliciesFile)
// 1. Load from legacy single file (low priority)
content, foundLegacy, err := store.readInsideFiler(filer.IamConfigDirectory, filer.IamPoliciesFile)
if err != nil {
return nil, err
}
if foundLegacy && len(content) > 0 {
policiesCollection := &PoliciesCollection{
Policies: make(map[string]policy_engine.PolicyDocument),
}
if err := json.Unmarshal(content, policiesCollection); err != nil {
glog.Errorf("Failed to parse legacy IAM policies from %s/%s: %v",
filer.IamConfigDirectory, filer.IamPoliciesFile, err)
} else {
for name, policy := range policiesCollection.Policies {
policies[name] = policy
}
}
}
// 2. Load from multi-file structure (high priority, overrides legacy)
if err := store.loadPoliciesFromMultiFile(ctx, policies); err != nil {
return nil, err
}
// 3. Perform migration if we loaded legacy config
if foundLegacy {
if err := store.migratePoliciesToMultiFile(ctx, policies); err != nil {
glog.Errorf("Failed to migrate IAM policies to multi-file layout: %v", err)
return policies, err
}
}
return policies, nil
}
func (store *FilerEtcStore) loadPoliciesFromMultiFile(ctx context.Context, policies map[string]policy_engine.PolicyDocument) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
if err == filer_pb.ErrNotFound {
glog.V(1).Infof("Policies file not found at %s/%s, returning empty policies",
filer.IamConfigDirectory, filer.IamPoliciesFile)
// If file doesn't exist, return empty collection
return nil
}
glog.Errorf("Failed to read IAM policies file from %s/%s: %v",
filer.IamConfigDirectory, filer.IamPoliciesFile, err)
return err
}
if len(content) == 0 {
glog.V(2).Infof("IAM policies file at %s/%s is empty",
filer.IamConfigDirectory, filer.IamPoliciesFile)
return nil
}
for _, entry := range entries {
if entry.IsDirectory {
continue
}
glog.V(2).Infof("Read %d bytes from %s/%s",
len(content), filer.IamConfigDirectory, filer.IamPoliciesFile)
var content []byte
if len(entry.Content) > 0 {
content = entry.Content
} else {
c, err := filer.ReadInsideFiler(client, dir, entry.Name)
if err != nil {
glog.Warningf("Failed to read policy file %s: %v", entry.Name, err)
continue
}
content = c
}
if err := json.Unmarshal(content, policiesCollection); err != nil {
glog.Errorf("Failed to parse IAM policies from %s/%s: %v",
filer.IamConfigDirectory, filer.IamPoliciesFile, err)
return err
if len(content) > 0 {
var policy policy_engine.PolicyDocument
if err := json.Unmarshal(content, &policy); err != nil {
glog.Warningf("Failed to unmarshal policy %s: %v", entry.Name, err)
continue
}
// The file name is "policyName.json"
policyName := entry.Name
if len(policyName) > 5 && policyName[len(policyName)-5:] == ".json" {
policyName = policyName[:len(policyName)-5]
policies[policyName] = policy
}
}
}
glog.V(1).Infof("Successfully loaded %d IAM policies", len(policiesCollection.Policies))
return nil
})
}
if err != nil {
return nil, err
}
func (store *FilerEtcStore) migratePoliciesToMultiFile(ctx context.Context, policies map[string]policy_engine.PolicyDocument) error {
glog.Infof("Migrating IAM policies to multi-file layout...")
// Log policy names for debugging
if glog.V(2) && len(policiesCollection.Policies) > 0 {
for policyName := range policiesCollection.Policies {
glog.V(2).Infof(" Policy: %s", policyName)
// 1. Save all policies to individual files
for name, policy := range policies {
if err := store.savePolicy(ctx, name, policy); err != nil {
return err
}
}
return policiesCollection.Policies, nil
// 2. Rename legacy file
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
OldDirectory: filer.IamConfigDirectory,
OldName: filer.IamPoliciesFile,
NewDirectory: filer.IamConfigDirectory,
NewName: IamLegacyPoliciesOldFile,
})
if err != nil {
glog.Errorf("Failed to rename legacy IAM policies file %s/%s to %s: %v",
filer.IamConfigDirectory, filer.IamPoliciesFile, IamLegacyPoliciesOldFile, err)
}
return err
})
}
func (store *FilerEtcStore) savePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
if err := validatePolicyName(name); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
data, err := json.Marshal(document)
if err != nil {
return err
}
return filer.SaveInsideFiler(client, filer.IamConfigDirectory+"/"+IamPoliciesDirectory, name+".json", data)
})
}
// CreatePolicy creates a new IAM policy in the filer
func (store *FilerEtcStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.updatePolicies(ctx, func(policies map[string]policy_engine.PolicyDocument) {
policies[name] = document
})
return store.savePolicy(ctx, name, document)
}
// UpdatePolicy updates an existing IAM policy in the filer
func (store *FilerEtcStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.updatePolicies(ctx, func(policies map[string]policy_engine.PolicyDocument) {
policies[name] = document
})
return store.savePolicy(ctx, name, document)
}
// PutPolicy creates or updates an IAM policy in the filer
@ -105,46 +181,56 @@ func (store *FilerEtcStore) PutPolicy(ctx context.Context, name string, document
// DeletePolicy deletes an IAM policy from the filer
func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error {
return store.updatePolicies(ctx, func(policies map[string]policy_engine.PolicyDocument) {
delete(policies, name)
})
}
// updatePolicies is a helper method to update policies atomically
func (store *FilerEtcStore) updatePolicies(ctx context.Context, updateFunc func(map[string]policy_engine.PolicyDocument)) error {
// Load existing policies
policies, err := store.GetPolicies(ctx)
if err != nil {
return err
}
// Apply update
updateFunc(policies)
// Save back to filer
policiesCollection := &PoliciesCollection{
Policies: policies,
}
data, err := json.Marshal(policiesCollection)
if err != nil {
if err := validatePolicyName(name); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
return filer.SaveInsideFiler(client, filer.IamConfigDirectory, filer.IamPoliciesFile, data)
_, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: filer.IamConfigDirectory + "/" + IamPoliciesDirectory,
Name: name + ".json",
})
if err != nil && !strings.Contains(err.Error(), filer_pb.ErrNotFound.Error()) {
return err
}
return nil
})
}
// GetPolicy retrieves a specific IAM policy by name from the filer
func (store *FilerEtcStore) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
if err := validatePolicyName(name); err != nil {
return nil, err
}
var policy *policy_engine.PolicyDocument
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
data, err := filer.ReadInsideFiler(client, filer.IamConfigDirectory+"/"+IamPoliciesDirectory, name+".json")
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
if len(data) == 0 {
return nil
}
policy = &policy_engine.PolicyDocument{}
return json.Unmarshal(data, policy)
})
if policy != nil {
return policy, err
}
// fallback to full list if single file read fails (e.g. before migration completes or if partially migrated)
// Although migration should happen on first GetPolicies call.
policies, err := store.GetPolicies(ctx)
if err != nil {
return nil, err
}
if policy, exists := policies[name]; exists {
return &policy, nil
if p, exists := policies[name]; exists {
return &p, nil
}
return nil, nil // Policy not found

206
weed/credential/filer_etc/filer_etc_service_account.go

@ -0,0 +1,206 @@
package filer_etc
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
func validateServiceAccountId(id string) error {
return credential.ValidateServiceAccountId(id)
}
func (store *FilerEtcStore) loadServiceAccountsFromMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamServiceAccountsDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
for _, entry := range entries {
if entry.IsDirectory {
continue
}
var content []byte
if len(entry.Content) > 0 {
content = entry.Content
} else {
c, err := filer.ReadInsideFiler(client, dir, entry.Name)
if err != nil {
glog.Warningf("Failed to read service account file %s: %v", entry.Name, err)
continue
}
content = c
}
if len(content) > 0 {
sa := &iam_pb.ServiceAccount{}
if err := json.Unmarshal(content, sa); err != nil {
glog.Warningf("Failed to unmarshal service account %s: %v", entry.Name, err)
continue
}
s3cfg.ServiceAccounts = append(s3cfg.ServiceAccounts, sa)
}
}
return nil
})
}
func (store *FilerEtcStore) saveServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
if sa == nil {
return fmt.Errorf("service account is nil")
}
if err := validateServiceAccountId(sa.Id); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
data, err := json.Marshal(sa)
if err != nil {
return err
}
return filer.SaveInsideFiler(client, filer.IamConfigDirectory+"/"+IamServiceAccountsDirectory, sa.Id+".json", data)
})
}
func (store *FilerEtcStore) deleteServiceAccount(ctx context.Context, saId string) error {
if err := validateServiceAccountId(saId); err != nil {
return err
}
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
resp, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
Directory: filer.IamConfigDirectory + "/" + IamServiceAccountsDirectory,
Name: saId + ".json",
})
if err != nil {
if strings.Contains(err.Error(), filer_pb.ErrNotFound.Error()) {
return credential.ErrServiceAccountNotFound
}
return err
}
if resp != nil && resp.Error != "" {
if strings.Contains(resp.Error, filer_pb.ErrNotFound.Error()) {
return credential.ErrServiceAccountNotFound
}
return fmt.Errorf("delete service account %s: %s", saId, resp.Error)
}
return nil
})
}
func (store *FilerEtcStore) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
existing, err := store.GetServiceAccount(ctx, sa.Id)
if err != nil {
if !errors.Is(err, credential.ErrServiceAccountNotFound) {
return err
}
} else if existing != nil {
return fmt.Errorf("service account %s already exists", sa.Id)
}
return store.saveServiceAccount(ctx, sa)
}
func (store *FilerEtcStore) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
if sa.Id != id {
return fmt.Errorf("service account ID mismatch")
}
_, err := store.GetServiceAccount(ctx, id)
if err != nil {
return err
}
return store.saveServiceAccount(ctx, sa)
}
func (store *FilerEtcStore) DeleteServiceAccount(ctx context.Context, id string) error {
return store.deleteServiceAccount(ctx, id)
}
func (store *FilerEtcStore) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
if err := validateServiceAccountId(id); err != nil {
return nil, err
}
var sa *iam_pb.ServiceAccount
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
data, err := filer.ReadInsideFiler(client, filer.IamConfigDirectory+"/"+IamServiceAccountsDirectory, id+".json")
if err != nil {
if err == filer_pb.ErrNotFound {
return credential.ErrServiceAccountNotFound
}
return err
}
if len(data) == 0 {
return credential.ErrServiceAccountNotFound
}
sa = &iam_pb.ServiceAccount{}
return json.Unmarshal(data, sa)
})
return sa, err
}
func (store *FilerEtcStore) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
var accounts []*iam_pb.ServiceAccount
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
dir := filer.IamConfigDirectory + "/" + IamServiceAccountsDirectory
entries, err := listEntries(ctx, client, dir)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
for _, entry := range entries {
if entry.IsDirectory {
continue
}
var content []byte
if len(entry.Content) > 0 {
content = entry.Content
} else {
c, err := filer.ReadInsideFiler(client, dir, entry.Name)
if err != nil {
glog.Warningf("Failed to read service account file %s: %v", entry.Name, err)
continue
}
content = c
}
if len(content) > 0 {
sa := &iam_pb.ServiceAccount{}
if err := json.Unmarshal(content, sa); err != nil {
glog.Warningf("Failed to unmarshal service account %s: %v", entry.Name, err)
continue
}
accounts = append(accounts, sa)
}
}
return nil
})
return accounts, err
}
func (store *FilerEtcStore) GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error) {
accounts, err := store.ListServiceAccounts(ctx)
if err != nil {
return nil, err
}
for _, sa := range accounts {
if sa.Credential != nil && sa.Credential.AccessKey == accessKey {
return sa, nil
}
}
return nil, credential.ErrAccessKeyNotFound
}

498
weed/credential/filer_multiple/filer_multiple_store.go

@ -1,498 +0,0 @@
package filer_multiple
import (
"context"
"encoding/json"
"fmt"
"strings"
"sync"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc"
)
const (
IdentitiesDirectory = "/etc/seaweedfs/identities"
PoliciesDirectory = "/etc/seaweedfs/policies"
)
func init() {
credential.Stores = append(credential.Stores, &FilerMultipleStore{})
}
// FilerMultipleStore implements CredentialStore using SeaweedFS filer for storage
// storing each identity in a separate file
type FilerMultipleStore struct {
filerAddressFunc func() pb.ServerAddress // Function to get current active filer
grpcDialOption grpc.DialOption
mu sync.RWMutex // Protects filerAddressFunc and grpcDialOption
}
func (store *FilerMultipleStore) GetName() credential.CredentialStoreTypeName {
return credential.StoreTypeFilerMultiple
}
func (store *FilerMultipleStore) Initialize(configuration util.Configuration, prefix string) error {
// Handle nil configuration gracefully
if configuration != nil {
filerAddr := configuration.GetString(prefix + "filer")
if filerAddr != "" {
// Static configuration - use fixed address
store.mu.Lock()
store.filerAddressFunc = func() pb.ServerAddress {
return pb.ServerAddress(filerAddr)
}
store.mu.Unlock()
}
}
// Note: filerAddressFunc can be set later via SetFilerAddressFunc method
return nil
}
// SetFilerAddressFunc sets a function that returns the current active filer address
// This enables high availability by using the currently active filer
func (store *FilerMultipleStore) SetFilerAddressFunc(getFiler func() pb.ServerAddress, grpcDialOption grpc.DialOption) {
store.mu.Lock()
defer store.mu.Unlock()
store.filerAddressFunc = getFiler
store.grpcDialOption = grpcDialOption
}
// withFilerClient executes a function with a filer client
func (store *FilerMultipleStore) withFilerClient(fn func(client filer_pb.SeaweedFilerClient) error) error {
store.mu.RLock()
if store.filerAddressFunc == nil {
store.mu.RUnlock()
return fmt.Errorf("filer_multiple: filer not yet available - please wait for filer discovery to complete and try again")
}
filerAddress := store.filerAddressFunc()
dialOption := store.grpcDialOption
store.mu.RUnlock()
if filerAddress == "" {
return fmt.Errorf("filer_multiple: no filer discovered yet - please ensure a filer is running and accessible")
}
// Use the pb.WithGrpcFilerClient helper similar to existing code
return pb.WithGrpcFilerClient(false, 0, filerAddress, dialOption, fn)
}
func (store *FilerMultipleStore) Shutdown() {
// No cleanup needed for file store
}
func (store *FilerMultipleStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
s3cfg := &iam_pb.S3ApiConfiguration{}
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// List and process all identity files in the directory using streaming callback
return filer_pb.SeaweedList(ctx, client, IdentitiesDirectory, "", func(entry *filer_pb.Entry, isLast bool) error {
if entry.IsDirectory || !strings.HasSuffix(entry.Name, ".json") {
return nil
}
content, err := filer.ReadInsideFiler(client, IdentitiesDirectory, entry.Name)
if err != nil {
glog.Warningf("Failed to read identity file %s: %v", entry.Name, err)
return nil // Continue with next file
}
identity := &iam_pb.Identity{}
if err := json.Unmarshal(content, identity); err != nil {
glog.Warningf("Failed to parse identity file %s: %v", entry.Name, err)
return nil // Continue with next file
}
s3cfg.Identities = append(s3cfg.Identities, identity)
return nil
}, "", false, 10000)
})
if err != nil {
// If listing failed because directory doesn't exist, treat as empty config
if err == filer_pb.ErrNotFound {
return s3cfg, nil
}
return s3cfg, err
}
return s3cfg, nil
}
func (store *FilerMultipleStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
// This operation is expensive for multiple files mode as it would overwrite everything
// But we implement it for interface compliance.
// We will write each identity to a separate file and remove stale files.
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// 1. List existing identity files
existingFileNames := make(map[string]bool)
err := filer_pb.SeaweedList(ctx, client, IdentitiesDirectory, "", func(entry *filer_pb.Entry, isLast bool) error {
if !entry.IsDirectory && strings.HasSuffix(entry.Name, ".json") {
existingFileNames[entry.Name] = true
}
return nil
}, "", false, 10000)
if err != nil && err != filer_pb.ErrNotFound {
return fmt.Errorf("failed to list existing identities: %w", err)
}
// 2. Build a set of identity keys present in the provided config
newKeys := make(map[string]bool)
for _, identity := range config.Identities {
newKeys[identity.Name+".json"] = true
}
// 3. Write/overwrite each identity using saveIdentity
for _, identity := range config.Identities {
if err := store.saveIdentity(ctx, client, identity); err != nil {
return err
}
}
// 4. Delete any existing files whose identity key is not in the new set
for filename := range existingFileNames {
if !newKeys[filename] {
err := filer_pb.DoRemove(ctx, client, IdentitiesDirectory, filename, false, false, false, false, nil)
if err != nil && err != filer_pb.ErrNotFound {
glog.Warningf("failed to remove stale identity file %s: %v", filename, err)
}
}
}
return nil
})
}
func (store *FilerMultipleStore) saveIdentity(ctx context.Context, client filer_pb.SeaweedFilerClient, identity *iam_pb.Identity) error {
data, err := json.Marshal(identity)
if err != nil {
return fmt.Errorf("failed to marshal identity %s: %w", identity.Name, err)
}
filename := identity.Name + ".json"
return filer.SaveInsideFiler(client, IdentitiesDirectory, filename, data)
}
func (store *FilerMultipleStore) CreateUser(ctx context.Context, identity *iam_pb.Identity) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := identity.Name + ".json"
// Check if exists
exists, err := store.exists(ctx, client, IdentitiesDirectory, filename)
if err != nil {
return err
}
if exists {
return credential.ErrUserAlreadyExists
}
return store.saveIdentity(ctx, client, identity)
})
}
func (store *FilerMultipleStore) exists(ctx context.Context, client filer_pb.SeaweedFilerClient, dir, name string) (bool, error) {
request := &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
}
resp, err := filer_pb.LookupEntry(ctx, client, request)
if err != nil {
if err == filer_pb.ErrNotFound {
return false, nil
}
return false, err
}
return resp.Entry != nil, nil
}
func (store *FilerMultipleStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) {
var identity *iam_pb.Identity
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := username + ".json"
content, err := filer.ReadInsideFiler(client, IdentitiesDirectory, filename)
if err != nil {
if err == filer_pb.ErrNotFound {
return credential.ErrUserNotFound
}
return err
}
identity = &iam_pb.Identity{}
if err := json.Unmarshal(content, identity); err != nil {
return fmt.Errorf("failed to parse identity: %w", err)
}
return nil
})
return identity, err
}
func (store *FilerMultipleStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := username + ".json"
// Check if exists
exists, err := store.exists(ctx, client, IdentitiesDirectory, filename)
if err != nil {
return err
}
if !exists {
return credential.ErrUserNotFound
}
// If username changed (renamed), we need to create new file and then delete old one
if identity.Name != username {
// Check if the new username already exists to prevent overwrites
newFilename := identity.Name + ".json"
exists, err := store.exists(ctx, client, IdentitiesDirectory, newFilename)
if err != nil {
return err
}
if exists {
return fmt.Errorf("user %s already exists", identity.Name)
}
// Create new identity file FIRST
if err := store.saveIdentity(ctx, client, identity); err != nil {
return err
}
// Delete old user file SECOND
err = filer_pb.DoRemove(ctx, client, IdentitiesDirectory, filename, false, false, false, false, nil)
if err != nil && err != filer_pb.ErrNotFound {
// Rollback: try to remove the newly created file if deleting the old one failed
if errRollback := filer_pb.DoRemove(ctx, client, IdentitiesDirectory, newFilename, false, false, false, false, nil); errRollback != nil {
glog.Errorf("Rollback of creating %s failed after failing to remove %s: %v", newFilename, filename, errRollback)
}
return fmt.Errorf("failed to remove old identity file %s: %w", filename, err)
}
return nil
}
return store.saveIdentity(ctx, client, identity)
})
}
func (store *FilerMultipleStore) DeleteUser(ctx context.Context, username string) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := username + ".json"
err := filer_pb.DoRemove(ctx, client, IdentitiesDirectory, filename, false, false, false, false, nil)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
return nil
})
}
func (store *FilerMultipleStore) ListUsers(ctx context.Context) ([]string, error) {
var usernames []string
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
err := filer_pb.SeaweedList(ctx, client, IdentitiesDirectory, "", func(entry *filer_pb.Entry, isLast bool) error {
if !entry.IsDirectory && strings.HasSuffix(entry.Name, ".json") {
name := strings.TrimSuffix(entry.Name, ".json")
usernames = append(usernames, name)
}
return nil
}, "", false, 10000)
if err != nil {
if err == filer_pb.ErrNotFound {
// Treat as empty if directory not found
return nil
}
return err
}
return nil
})
return usernames, err
}
func (store *FilerMultipleStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
// This is inefficient in file store without index.
// We must iterate all users.
config, err := store.LoadConfiguration(ctx)
if err != nil {
return nil, err
}
for _, identity := range config.Identities {
for _, credential := range identity.Credentials {
if credential.AccessKey == accessKey {
return identity, nil
}
}
}
return nil, credential.ErrAccessKeyNotFound
}
func (store *FilerMultipleStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error {
identity, err := store.GetUser(ctx, username)
if err != nil {
return err
}
// Check duplicates
for _, existing := range identity.Credentials {
if existing.AccessKey == cred.AccessKey {
return fmt.Errorf("access key already exists")
}
}
identity.Credentials = append(identity.Credentials, cred)
return store.UpdateUser(ctx, username, identity)
}
func (store *FilerMultipleStore) DeleteAccessKey(ctx context.Context, username string, accessKey string) error {
identity, err := store.GetUser(ctx, username)
if err != nil {
return err
}
found := false
for i, cred := range identity.Credentials {
if cred.AccessKey == accessKey {
identity.Credentials = append(identity.Credentials[:i], identity.Credentials[i+1:]...)
found = true
break
}
}
if !found {
return credential.ErrAccessKeyNotFound
}
return store.UpdateUser(ctx, username, identity)
}
// PolicyManager implementation
func (store *FilerMultipleStore) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
policies := make(map[string]policy_engine.PolicyDocument)
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
return filer_pb.SeaweedList(ctx, client, PoliciesDirectory, "", func(entry *filer_pb.Entry, isLast bool) error {
if entry.IsDirectory || !strings.HasSuffix(entry.Name, ".json") {
return nil
}
content, err := filer.ReadInsideFiler(client, PoliciesDirectory, entry.Name)
if err != nil {
glog.Warningf("Failed to read policy file %s: %v", entry.Name, err)
return nil
}
var policy policy_engine.PolicyDocument
if err := json.Unmarshal(content, &policy); err != nil {
glog.Warningf("Failed to parse policy file %s: %v", entry.Name, err)
return nil
}
name := strings.TrimSuffix(entry.Name, ".json")
policies[name] = policy
return nil
}, "", false, 10000)
})
if err != nil {
if err == filer_pb.ErrNotFound {
return policies, nil
}
return nil, err
}
return policies, nil
}
func (store *FilerMultipleStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := name + ".json"
exists, err := store.exists(ctx, client, PoliciesDirectory, filename)
if err != nil {
return err
}
if exists {
return fmt.Errorf("policy %s already exists", name)
}
return store.savePolicy(ctx, client, name, document)
})
}
func (store *FilerMultipleStore) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
// We can just overwrite. The distinction between Create and Update in filer_multiple was just checking existence.
// Put implies "create or replace".
return store.savePolicy(ctx, client, name, document)
})
}
func (store *FilerMultipleStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := name + ".json"
exists, err := store.exists(ctx, client, PoliciesDirectory, filename)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("policy %s not found", name)
}
return store.savePolicy(ctx, client, name, document)
})
}
func (store *FilerMultipleStore) DeletePolicy(ctx context.Context, name string) error {
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := name + ".json"
err := filer_pb.DoRemove(ctx, client, PoliciesDirectory, filename, false, false, false, false, nil)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
return nil
})
}
func (store *FilerMultipleStore) GetPolicy(ctx context.Context, name string) (*policy_engine.PolicyDocument, error) {
var policy *policy_engine.PolicyDocument
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
filename := name + ".json"
content, err := filer.ReadInsideFiler(client, PoliciesDirectory, filename)
if err != nil {
if err == filer_pb.ErrNotFound {
return nil
}
return err
}
policy = &policy_engine.PolicyDocument{}
if err := json.Unmarshal(content, policy); err != nil {
return fmt.Errorf("failed to parse policy: %w", err)
}
return nil
})
return policy, err
}
func (store *FilerMultipleStore) savePolicy(ctx context.Context, client filer_pb.SeaweedFilerClient, name string, document policy_engine.PolicyDocument) error {
data, err := json.Marshal(document)
if err != nil {
return fmt.Errorf("failed to marshal policy %s: %w", name, err)
}
filename := name + ".json"
return filer.SaveInsideFiler(client, PoliciesDirectory, filename, data)
}

16
weed/credential/grpc/grpc_policy.go

@ -7,6 +7,8 @@ import (
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
"github.com/seaweedfs/seaweedfs/weed/s3api/policy_engine"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func (store *IamGrpcStore) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
@ -63,7 +65,21 @@ func (store *IamGrpcStore) GetPolicy(ctx context.Context, name string) (*policy_
return json.Unmarshal([]byte(resp.Content), &doc)
})
if err != nil {
// If policy not found, return nil instead of error (consistent with other stores)
if st, ok := status.FromError(err); ok && st.Code() == codes.NotFound {
return nil, nil
}
return nil, err
}
return &doc, nil
}
// CreatePolicy creates a new policy (delegates to PutPolicy)
func (store *IamGrpcStore) CreatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.PutPolicy(ctx, name, document)
}
// UpdatePolicy updates an existing policy (delegates to PutPolicy)
func (store *IamGrpcStore) UpdatePolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
return store.PutPolicy(ctx, name, document)
}

78
weed/credential/grpc/grpc_service_account.go

@ -0,0 +1,78 @@
package grpc
import (
"context"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
func (store *IamGrpcStore) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
return store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
_, err := client.CreateServiceAccount(ctx, &iam_pb.CreateServiceAccountRequest{
ServiceAccount: sa,
})
return err
})
}
func (store *IamGrpcStore) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
return store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
_, err := client.UpdateServiceAccount(ctx, &iam_pb.UpdateServiceAccountRequest{
Id: id,
ServiceAccount: sa,
})
return err
})
}
func (store *IamGrpcStore) DeleteServiceAccount(ctx context.Context, id string) error {
return store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
_, err := client.DeleteServiceAccount(ctx, &iam_pb.DeleteServiceAccountRequest{
Id: id,
})
return err
})
}
func (store *IamGrpcStore) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
var sa *iam_pb.ServiceAccount
err := store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
resp, err := client.GetServiceAccount(ctx, &iam_pb.GetServiceAccountRequest{
Id: id,
})
if err != nil {
return err
}
sa = resp.ServiceAccount
return nil
})
return sa, err
}
func (store *IamGrpcStore) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
var accounts []*iam_pb.ServiceAccount
err := store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
resp, err := client.ListServiceAccounts(ctx, &iam_pb.ListServiceAccountsRequest{})
if err != nil {
return err
}
accounts = resp.ServiceAccounts
return nil
})
return accounts, err
}
func (store *IamGrpcStore) GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error) {
var sa *iam_pb.ServiceAccount
err := store.withIamClient(func(client iam_pb.SeaweedIdentityAccessManagementClient) error {
resp, err := client.GetServiceAccountByAccessKey(ctx, &iam_pb.GetServiceAccountByAccessKeyRequest{
AccessKey: accessKey,
})
if err != nil {
return err
}
sa = resp.ServiceAccount
return nil
})
return sa, err
}

85
weed/credential/memory/memory_service_account.go

@ -0,0 +1,85 @@
package memory
import (
"context"
"fmt"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
func (store *MemoryStore) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
store.mu.Lock()
defer store.mu.Unlock()
if _, exists := store.serviceAccounts[sa.Id]; exists {
return fmt.Errorf("service account already exists")
}
store.serviceAccounts[sa.Id] = sa
if sa.Credential != nil && sa.Credential.AccessKey != "" {
store.serviceAccountAccessKeys[sa.Credential.AccessKey] = sa.Id
}
return nil
}
func (store *MemoryStore) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
store.mu.Lock()
defer store.mu.Unlock()
_, exists := store.serviceAccounts[id]
if !exists {
return credential.ErrServiceAccountNotFound
}
if sa.Id != id {
return fmt.Errorf("service account ID mismatch")
}
// Update access key index: remove any existing keys for this SA
for k, v := range store.serviceAccountAccessKeys {
if v == id {
delete(store.serviceAccountAccessKeys, k)
}
}
store.serviceAccounts[id] = sa
if sa.Credential != nil && sa.Credential.AccessKey != "" {
store.serviceAccountAccessKeys[sa.Credential.AccessKey] = sa.Id
}
return nil
}
func (store *MemoryStore) DeleteServiceAccount(ctx context.Context, id string) error {
store.mu.Lock()
defer store.mu.Unlock()
if sa, ok := store.serviceAccounts[id]; ok {
if sa.Credential != nil && sa.Credential.AccessKey != "" {
delete(store.serviceAccountAccessKeys, sa.Credential.AccessKey)
}
delete(store.serviceAccounts, id)
return nil
}
return credential.ErrServiceAccountNotFound
}
func (store *MemoryStore) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
store.mu.RLock()
defer store.mu.RUnlock()
if sa, exists := store.serviceAccounts[id]; exists {
return sa, nil
}
return nil, credential.ErrServiceAccountNotFound
}
func (store *MemoryStore) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
store.mu.RLock()
defer store.mu.RUnlock()
var accounts []*iam_pb.ServiceAccount
for _, sa := range store.serviceAccounts {
accounts = append(accounts, sa)
}
return accounts, nil
}

29
weed/credential/memory/memory_store.go

@ -1,6 +1,7 @@
package memory
import (
"context"
"sync"
"github.com/seaweedfs/seaweedfs/weed/credential"
@ -16,11 +17,13 @@ func init() {
// MemoryStore implements CredentialStore using in-memory storage
// This is primarily intended for testing purposes
type MemoryStore struct {
mu sync.RWMutex
users map[string]*iam_pb.Identity // username -> identity
accessKeys map[string]string // access_key -> username
policies map[string]policy_engine.PolicyDocument // policy_name -> policy_document
initialized bool
mu sync.RWMutex
users map[string]*iam_pb.Identity // username -> identity
accessKeys map[string]string // access_key -> username
serviceAccounts map[string]*iam_pb.ServiceAccount // id -> service_account
serviceAccountAccessKeys map[string]string // access_key -> id
policies map[string]policy_engine.PolicyDocument // policy_name -> policy_document
initialized bool
}
func (store *MemoryStore) GetName() credential.CredentialStoreTypeName {
@ -37,6 +40,8 @@ func (store *MemoryStore) Initialize(configuration util.Configuration, prefix st
store.users = make(map[string]*iam_pb.Identity)
store.accessKeys = make(map[string]string)
store.serviceAccounts = make(map[string]*iam_pb.ServiceAccount)
store.serviceAccountAccessKeys = make(map[string]string)
store.policies = make(map[string]policy_engine.PolicyDocument)
store.initialized = true
@ -49,6 +54,8 @@ func (store *MemoryStore) Shutdown() {
store.users = nil
store.accessKeys = nil
store.serviceAccounts = nil
store.serviceAccountAccessKeys = nil
store.policies = nil
store.initialized = false
}
@ -61,6 +68,9 @@ func (store *MemoryStore) Reset() {
if store.initialized {
store.users = make(map[string]*iam_pb.Identity)
store.accessKeys = make(map[string]string)
store.serviceAccounts = make(map[string]*iam_pb.ServiceAccount)
store.serviceAccountAccessKeys = make(map[string]string)
store.policies = make(map[string]policy_engine.PolicyDocument)
}
}
@ -79,3 +89,12 @@ func (store *MemoryStore) GetAccessKeyCount() int {
return len(store.accessKeys)
}
func (store *MemoryStore) GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error) {
store.mu.RLock()
defer store.mu.RUnlock()
if id, ok := store.serviceAccountAccessKeys[accessKey]; ok {
return store.serviceAccounts[id], nil
}
return nil, credential.ErrAccessKeyNotFound
}

65
weed/credential/memory/memory_store_test.go

@ -313,3 +313,68 @@ func TestMemoryStoreConfigurationSaveLoad(t *testing.T) {
t.Errorf("User2 credentials not correct: %+v", user2.Credentials)
}
}
func TestMemoryStoreServiceAccountByAccessKey(t *testing.T) {
store := &MemoryStore{}
config := util.GetViper()
if err := store.Initialize(config, "credential."); err != nil {
t.Fatalf("Failed to initialize store: %v", err)
}
ctx := context.Background()
// 1. Create service account
sa := &iam_pb.ServiceAccount{
Id: "sa-test-1",
ParentUser: "user1",
Credential: &iam_pb.Credential{
AccessKey: "ACCESS-KEY-1",
SecretKey: "SECRET-KEY-1",
},
}
if err := store.CreateServiceAccount(ctx, sa); err != nil {
t.Fatalf("Failed to create service account: %v", err)
}
// 2. Lookup by access key
found, err := store.GetServiceAccountByAccessKey(ctx, "ACCESS-KEY-1")
if err != nil {
t.Fatalf("Failed to lookup by access key: %v", err)
}
if found.Id != "sa-test-1" {
t.Errorf("Expected sa-test-1, got %s", found.Id)
}
// 3. Update with new access key
sa.Credential.AccessKey = "ACCESS-KEY-2"
if err := store.UpdateServiceAccount(ctx, sa.Id, sa); err != nil {
t.Fatalf("Failed to update service account: %v", err)
}
// Verify old key is gone
_, err = store.GetServiceAccountByAccessKey(ctx, "ACCESS-KEY-1")
if err != credential.ErrAccessKeyNotFound {
t.Errorf("Expected ErrAccessKeyNotFound for old key, got %v", err)
}
// Verify new key works
found, err = store.GetServiceAccountByAccessKey(ctx, "ACCESS-KEY-2")
if err != nil {
t.Fatalf("Failed to lookup by new access key: %v", err)
}
if found.Id != "sa-test-1" {
t.Errorf("Expected sa-test-1, got %s", found.Id)
}
// 4. Delete service account
if err := store.DeleteServiceAccount(ctx, sa.Id); err != nil {
t.Fatalf("Failed to delete service account: %v", err)
}
// Verify key is gone
_, err = store.GetServiceAccountByAccessKey(ctx, "ACCESS-KEY-2")
if err != credential.ErrAccessKeyNotFound {
t.Errorf("Expected ErrAccessKeyNotFound after delete, got %v", err)
}
}

173
weed/credential/postgres/postgres_service_account.go

@ -0,0 +1,173 @@
package postgres
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"github.com/seaweedfs/seaweedfs/weed/credential"
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
)
func (store *PostgresStore) CreateServiceAccount(ctx context.Context, sa *iam_pb.ServiceAccount) error {
if sa == nil {
return fmt.Errorf("service account is nil")
}
if sa.Id == "" {
return fmt.Errorf("service account ID is required")
}
if !store.configured {
return fmt.Errorf("store not configured")
}
data, err := json.Marshal(sa)
if err != nil {
return fmt.Errorf("failed to marshal service account: %w", err)
}
accessKey := ""
if sa.Credential != nil {
accessKey = sa.Credential.AccessKey
}
_, err = store.db.ExecContext(ctx,
"INSERT INTO service_accounts (id, access_key, content) VALUES ($1, $2, $3)",
sa.Id, accessKey, data)
if err != nil {
return fmt.Errorf("failed to insert service account: %w", err)
}
return nil
}
func (store *PostgresStore) UpdateServiceAccount(ctx context.Context, id string, sa *iam_pb.ServiceAccount) error {
if sa == nil {
return fmt.Errorf("service account is nil")
}
if id == "" {
return fmt.Errorf("service account ID is required")
}
if sa.Id != id {
return fmt.Errorf("service account ID mismatch")
}
data, err := json.Marshal(sa)
if err != nil {
return fmt.Errorf("failed to marshal service account: %w", err)
}
accessKey := ""
if sa.Credential != nil {
accessKey = sa.Credential.AccessKey
}
result, err := store.db.ExecContext(ctx,
"UPDATE service_accounts SET access_key = $2, content = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $1",
id, accessKey, data)
if err != nil {
return fmt.Errorf("failed to update service account: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return credential.ErrServiceAccountNotFound
}
return nil
}
func (store *PostgresStore) DeleteServiceAccount(ctx context.Context, id string) error {
if !store.configured {
return fmt.Errorf("store not configured")
}
result, err := store.db.ExecContext(ctx, "DELETE FROM service_accounts WHERE id = $1", id)
if err != nil {
return fmt.Errorf("failed to delete service account: %w", err)
}
rows, err := result.RowsAffected()
if err != nil {
return err
}
if rows == 0 {
return credential.ErrServiceAccountNotFound
}
return nil
}
func (store *PostgresStore) GetServiceAccount(ctx context.Context, id string) (*iam_pb.ServiceAccount, error) {
if !store.configured {
return nil, fmt.Errorf("store not configured")
}
var content []byte
err := store.db.QueryRowContext(ctx, "SELECT content FROM service_accounts WHERE id = $1", id).Scan(&content)
if err != nil {
if err == sql.ErrNoRows {
return nil, credential.ErrServiceAccountNotFound
}
return nil, fmt.Errorf("failed to get service account: %w", err)
}
sa := &iam_pb.ServiceAccount{}
if err := json.Unmarshal(content, sa); err != nil {
return nil, fmt.Errorf("failed to unmarshal service account: %w", err)
}
return sa, nil
}
func (store *PostgresStore) ListServiceAccounts(ctx context.Context) ([]*iam_pb.ServiceAccount, error) {
if !store.configured {
return nil, fmt.Errorf("store not configured")
}
rows, err := store.db.QueryContext(ctx, "SELECT content FROM service_accounts")
if err != nil {
return nil, fmt.Errorf("failed to list service accounts: %w", err)
}
defer rows.Close()
var accounts []*iam_pb.ServiceAccount
for rows.Next() {
var content []byte
if err := rows.Scan(&content); err != nil {
return nil, fmt.Errorf("failed to scan service account: %w", err)
}
sa := &iam_pb.ServiceAccount{}
if err := json.Unmarshal(content, sa); err != nil {
return nil, fmt.Errorf("failed to unmarshal service account: %w", err)
}
accounts = append(accounts, sa)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating service accounts: %w", err)
}
return accounts, nil
}
func (store *PostgresStore) GetServiceAccountByAccessKey(ctx context.Context, accessKey string) (*iam_pb.ServiceAccount, error) {
if !store.configured {
return nil, fmt.Errorf("store not configured")
}
var content []byte
err := store.db.QueryRowContext(ctx, "SELECT content FROM service_accounts WHERE access_key = $1", accessKey).Scan(&content)
if err != nil {
if err == sql.ErrNoRows {
return nil, credential.ErrAccessKeyNotFound
}
return nil, fmt.Errorf("failed to query service account by access key: %w", err)
}
sa := &iam_pb.ServiceAccount{}
if err := json.Unmarshal(content, sa); err != nil {
return nil, fmt.Errorf("failed to unmarshal service account: %w", err)
}
return sa, nil
}

15
weed/credential/postgres/postgres_store.go

@ -123,6 +123,17 @@ func (store *PostgresStore) createTables() error {
CREATE INDEX IF NOT EXISTS idx_policies_name ON policies(name);
`
// Create service_accounts table
serviceAccountsTable := `
CREATE TABLE IF NOT EXISTS service_accounts (
id VARCHAR(255) PRIMARY KEY,
access_key VARCHAR(255) UNIQUE,
content JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
`
// Execute table creation
if _, err := store.db.Exec(usersTable); err != nil {
return fmt.Errorf("failed to create users table: %w", err)
@ -136,6 +147,10 @@ func (store *PostgresStore) createTables() error {
return fmt.Errorf("failed to create policies table: %w", err)
}
if _, err := store.db.Exec(serviceAccountsTable); err != nil {
return fmt.Errorf("failed to create service_accounts table: %w", err)
}
return nil
}

28
weed/credential/validation.go

@ -0,0 +1,28 @@
package credential
import (
"fmt"
"regexp"
)
var (
PolicyNamePattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
ServiceAccountIdPattern = regexp.MustCompile(`^sa:[A-Za-z0-9_-]+:[a-z0-9-]+$`)
)
func ValidatePolicyName(name string) error {
if !PolicyNamePattern.MatchString(name) {
return fmt.Errorf("invalid policy name: %s", name)
}
return nil
}
func ValidateServiceAccountId(id string) error {
if id == "" {
return fmt.Errorf("service account ID cannot be empty")
}
if !ServiceAccountIdPattern.MatchString(id) {
return fmt.Errorf("invalid service account ID: %s (expected format sa:<user>:<uuid>)", id)
}
return nil
}

56
weed/pb/iam.proto

@ -30,6 +30,14 @@ service SeaweedIdentityAccessManagement {
rpc GetPolicy (GetPolicyRequest) returns (GetPolicyResponse);
rpc ListPolicies (ListPoliciesRequest) returns (ListPoliciesResponse);
rpc DeletePolicy (DeletePolicyRequest) returns (DeletePolicyResponse);
// Service Account Management
rpc CreateServiceAccount (CreateServiceAccountRequest) returns (CreateServiceAccountResponse);
rpc UpdateServiceAccount (UpdateServiceAccountRequest) returns (UpdateServiceAccountResponse);
rpc DeleteServiceAccount (DeleteServiceAccountRequest) returns (DeleteServiceAccountResponse);
rpc GetServiceAccount (GetServiceAccountRequest) returns (GetServiceAccountResponse);
rpc ListServiceAccounts (ListServiceAccountsRequest) returns (ListServiceAccountsResponse);
rpc GetServiceAccountByAccessKey (GetServiceAccountByAccessKeyRequest) returns (GetServiceAccountByAccessKeyResponse);
}
//////////////////////////////////////////////////
@ -196,3 +204,51 @@ message Policy {
string name = 1;
string content = 2; // JSON content of the policy
}
//////////////////////////////////////////////////
// Service Account Messages
message CreateServiceAccountRequest {
ServiceAccount service_account = 1;
}
message CreateServiceAccountResponse {
}
message UpdateServiceAccountRequest {
string id = 1;
ServiceAccount service_account = 2;
}
message UpdateServiceAccountResponse {
}
message DeleteServiceAccountRequest {
string id = 1;
}
message DeleteServiceAccountResponse {
}
message GetServiceAccountRequest {
string id = 1;
}
message GetServiceAccountResponse {
ServiceAccount service_account = 1;
}
message ListServiceAccountsRequest {
}
message ListServiceAccountsResponse {
repeated ServiceAccount service_accounts = 1;
}
message GetServiceAccountByAccessKeyRequest {
string access_key = 1;
}
message GetServiceAccountByAccessKeyResponse {
ServiceAccount service_account = 1;
}

703
weed/pb/iam_pb/iam.pb.go

@ -1647,6 +1647,510 @@ func (x *Policy) GetContent() string {
return ""
}
type CreateServiceAccountRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceAccount *ServiceAccount `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateServiceAccountRequest) Reset() {
*x = CreateServiceAccountRequest{}
mi := &file_iam_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateServiceAccountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateServiceAccountRequest) ProtoMessage() {}
func (x *CreateServiceAccountRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateServiceAccountRequest.ProtoReflect.Descriptor instead.
func (*CreateServiceAccountRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{34}
}
func (x *CreateServiceAccountRequest) GetServiceAccount() *ServiceAccount {
if x != nil {
return x.ServiceAccount
}
return nil
}
type CreateServiceAccountResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *CreateServiceAccountResponse) Reset() {
*x = CreateServiceAccountResponse{}
mi := &file_iam_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *CreateServiceAccountResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*CreateServiceAccountResponse) ProtoMessage() {}
func (x *CreateServiceAccountResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use CreateServiceAccountResponse.ProtoReflect.Descriptor instead.
func (*CreateServiceAccountResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{35}
}
type UpdateServiceAccountRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
ServiceAccount *ServiceAccount `protobuf:"bytes,2,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateServiceAccountRequest) Reset() {
*x = UpdateServiceAccountRequest{}
mi := &file_iam_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateServiceAccountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateServiceAccountRequest) ProtoMessage() {}
func (x *UpdateServiceAccountRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateServiceAccountRequest.ProtoReflect.Descriptor instead.
func (*UpdateServiceAccountRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{36}
}
func (x *UpdateServiceAccountRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *UpdateServiceAccountRequest) GetServiceAccount() *ServiceAccount {
if x != nil {
return x.ServiceAccount
}
return nil
}
type UpdateServiceAccountResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *UpdateServiceAccountResponse) Reset() {
*x = UpdateServiceAccountResponse{}
mi := &file_iam_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *UpdateServiceAccountResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*UpdateServiceAccountResponse) ProtoMessage() {}
func (x *UpdateServiceAccountResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use UpdateServiceAccountResponse.ProtoReflect.Descriptor instead.
func (*UpdateServiceAccountResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{37}
}
type DeleteServiceAccountRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteServiceAccountRequest) Reset() {
*x = DeleteServiceAccountRequest{}
mi := &file_iam_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteServiceAccountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteServiceAccountRequest) ProtoMessage() {}
func (x *DeleteServiceAccountRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteServiceAccountRequest.ProtoReflect.Descriptor instead.
func (*DeleteServiceAccountRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{38}
}
func (x *DeleteServiceAccountRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type DeleteServiceAccountResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *DeleteServiceAccountResponse) Reset() {
*x = DeleteServiceAccountResponse{}
mi := &file_iam_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *DeleteServiceAccountResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*DeleteServiceAccountResponse) ProtoMessage() {}
func (x *DeleteServiceAccountResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use DeleteServiceAccountResponse.ProtoReflect.Descriptor instead.
func (*DeleteServiceAccountResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{39}
}
type GetServiceAccountRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetServiceAccountRequest) Reset() {
*x = GetServiceAccountRequest{}
mi := &file_iam_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetServiceAccountRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetServiceAccountRequest) ProtoMessage() {}
func (x *GetServiceAccountRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetServiceAccountRequest.ProtoReflect.Descriptor instead.
func (*GetServiceAccountRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{40}
}
func (x *GetServiceAccountRequest) GetId() string {
if x != nil {
return x.Id
}
return ""
}
type GetServiceAccountResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceAccount *ServiceAccount `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetServiceAccountResponse) Reset() {
*x = GetServiceAccountResponse{}
mi := &file_iam_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetServiceAccountResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetServiceAccountResponse) ProtoMessage() {}
func (x *GetServiceAccountResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetServiceAccountResponse.ProtoReflect.Descriptor instead.
func (*GetServiceAccountResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{41}
}
func (x *GetServiceAccountResponse) GetServiceAccount() *ServiceAccount {
if x != nil {
return x.ServiceAccount
}
return nil
}
type ListServiceAccountsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListServiceAccountsRequest) Reset() {
*x = ListServiceAccountsRequest{}
mi := &file_iam_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListServiceAccountsRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListServiceAccountsRequest) ProtoMessage() {}
func (x *ListServiceAccountsRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListServiceAccountsRequest.ProtoReflect.Descriptor instead.
func (*ListServiceAccountsRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{42}
}
type ListServiceAccountsResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceAccounts []*ServiceAccount `protobuf:"bytes,1,rep,name=service_accounts,json=serviceAccounts,proto3" json:"service_accounts,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ListServiceAccountsResponse) Reset() {
*x = ListServiceAccountsResponse{}
mi := &file_iam_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ListServiceAccountsResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ListServiceAccountsResponse) ProtoMessage() {}
func (x *ListServiceAccountsResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ListServiceAccountsResponse.ProtoReflect.Descriptor instead.
func (*ListServiceAccountsResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{43}
}
func (x *ListServiceAccountsResponse) GetServiceAccounts() []*ServiceAccount {
if x != nil {
return x.ServiceAccounts
}
return nil
}
type GetServiceAccountByAccessKeyRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
AccessKey string `protobuf:"bytes,1,opt,name=access_key,json=accessKey,proto3" json:"access_key,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetServiceAccountByAccessKeyRequest) Reset() {
*x = GetServiceAccountByAccessKeyRequest{}
mi := &file_iam_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetServiceAccountByAccessKeyRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetServiceAccountByAccessKeyRequest) ProtoMessage() {}
func (x *GetServiceAccountByAccessKeyRequest) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[44]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetServiceAccountByAccessKeyRequest.ProtoReflect.Descriptor instead.
func (*GetServiceAccountByAccessKeyRequest) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{44}
}
func (x *GetServiceAccountByAccessKeyRequest) GetAccessKey() string {
if x != nil {
return x.AccessKey
}
return ""
}
type GetServiceAccountByAccessKeyResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServiceAccount *ServiceAccount `protobuf:"bytes,1,opt,name=service_account,json=serviceAccount,proto3" json:"service_account,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *GetServiceAccountByAccessKeyResponse) Reset() {
*x = GetServiceAccountByAccessKeyResponse{}
mi := &file_iam_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *GetServiceAccountByAccessKeyResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*GetServiceAccountByAccessKeyResponse) ProtoMessage() {}
func (x *GetServiceAccountByAccessKeyResponse) ProtoReflect() protoreflect.Message {
mi := &file_iam_proto_msgTypes[45]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use GetServiceAccountByAccessKeyResponse.ProtoReflect.Descriptor instead.
func (*GetServiceAccountByAccessKeyResponse) Descriptor() ([]byte, []int) {
return file_iam_proto_rawDescGZIP(), []int{45}
}
func (x *GetServiceAccountByAccessKeyResponse) GetServiceAccount() *ServiceAccount {
if x != nil {
return x.ServiceAccount
}
return nil
}
var File_iam_proto protoreflect.FileDescriptor
const file_iam_proto_rawDesc = "" +
@ -1751,7 +2255,29 @@ const file_iam_proto_rawDesc = "" +
"\x14DeletePolicyResponse\"6\n" +
"\x06Policy\x12\x12\n" +
"\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" +
"\acontent\x18\x02 \x01(\tR\acontent2\xbb\b\n" +
"\acontent\x18\x02 \x01(\tR\acontent\"^\n" +
"\x1bCreateServiceAccountRequest\x12?\n" +
"\x0fservice_account\x18\x01 \x01(\v2\x16.iam_pb.ServiceAccountR\x0eserviceAccount\"\x1e\n" +
"\x1cCreateServiceAccountResponse\"n\n" +
"\x1bUpdateServiceAccountRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12?\n" +
"\x0fservice_account\x18\x02 \x01(\v2\x16.iam_pb.ServiceAccountR\x0eserviceAccount\"\x1e\n" +
"\x1cUpdateServiceAccountResponse\"-\n" +
"\x1bDeleteServiceAccountRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\x1e\n" +
"\x1cDeleteServiceAccountResponse\"*\n" +
"\x18GetServiceAccountRequest\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\"\\\n" +
"\x19GetServiceAccountResponse\x12?\n" +
"\x0fservice_account\x18\x01 \x01(\v2\x16.iam_pb.ServiceAccountR\x0eserviceAccount\"\x1c\n" +
"\x1aListServiceAccountsRequest\"`\n" +
"\x1bListServiceAccountsResponse\x12A\n" +
"\x10service_accounts\x18\x01 \x03(\v2\x16.iam_pb.ServiceAccountR\x0fserviceAccounts\"D\n" +
"#GetServiceAccountByAccessKeyRequest\x12\x1d\n" +
"\n" +
"access_key\x18\x01 \x01(\tR\taccessKey\"g\n" +
"$GetServiceAccountByAccessKeyResponse\x12?\n" +
"\x0fservice_account\x18\x01 \x01(\v2\x16.iam_pb.ServiceAccountR\x0eserviceAccount2\x99\r\n" +
"\x1fSeaweedIdentityAccessManagement\x12U\n" +
"\x10GetConfiguration\x12\x1f.iam_pb.GetConfigurationRequest\x1a .iam_pb.GetConfigurationResponse\x12U\n" +
"\x10PutConfiguration\x12\x1f.iam_pb.PutConfigurationRequest\x1a .iam_pb.PutConfigurationResponse\x12C\n" +
@ -1769,7 +2295,13 @@ const file_iam_proto_rawDesc = "" +
"\tPutPolicy\x12\x18.iam_pb.PutPolicyRequest\x1a\x19.iam_pb.PutPolicyResponse\x12@\n" +
"\tGetPolicy\x12\x18.iam_pb.GetPolicyRequest\x1a\x19.iam_pb.GetPolicyResponse\x12I\n" +
"\fListPolicies\x12\x1b.iam_pb.ListPoliciesRequest\x1a\x1c.iam_pb.ListPoliciesResponse\x12I\n" +
"\fDeletePolicy\x12\x1b.iam_pb.DeletePolicyRequest\x1a\x1c.iam_pb.DeletePolicyResponseBK\n" +
"\fDeletePolicy\x12\x1b.iam_pb.DeletePolicyRequest\x1a\x1c.iam_pb.DeletePolicyResponse\x12a\n" +
"\x14CreateServiceAccount\x12#.iam_pb.CreateServiceAccountRequest\x1a$.iam_pb.CreateServiceAccountResponse\x12a\n" +
"\x14UpdateServiceAccount\x12#.iam_pb.UpdateServiceAccountRequest\x1a$.iam_pb.UpdateServiceAccountResponse\x12a\n" +
"\x14DeleteServiceAccount\x12#.iam_pb.DeleteServiceAccountRequest\x1a$.iam_pb.DeleteServiceAccountResponse\x12X\n" +
"\x11GetServiceAccount\x12 .iam_pb.GetServiceAccountRequest\x1a!.iam_pb.GetServiceAccountResponse\x12^\n" +
"\x13ListServiceAccounts\x12\".iam_pb.ListServiceAccountsRequest\x1a#.iam_pb.ListServiceAccountsResponse\x12y\n" +
"\x1cGetServiceAccountByAccessKey\x12+.iam_pb.GetServiceAccountByAccessKeyRequest\x1a,.iam_pb.GetServiceAccountByAccessKeyResponseBK\n" +
"\x10seaweedfs.clientB\bIamProtoZ-github.com/seaweedfs/seaweedfs/weed/pb/iam_pbb\x06proto3"
var (
@ -1784,42 +2316,54 @@ func file_iam_proto_rawDescGZIP() []byte {
return file_iam_proto_rawDescData
}
var file_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 34)
var file_iam_proto_msgTypes = make([]protoimpl.MessageInfo, 46)
var file_iam_proto_goTypes = []any{
(*GetConfigurationRequest)(nil), // 0: iam_pb.GetConfigurationRequest
(*GetConfigurationResponse)(nil), // 1: iam_pb.GetConfigurationResponse
(*PutConfigurationRequest)(nil), // 2: iam_pb.PutConfigurationRequest
(*PutConfigurationResponse)(nil), // 3: iam_pb.PutConfigurationResponse
(*CreateUserRequest)(nil), // 4: iam_pb.CreateUserRequest
(*CreateUserResponse)(nil), // 5: iam_pb.CreateUserResponse
(*GetUserRequest)(nil), // 6: iam_pb.GetUserRequest
(*GetUserResponse)(nil), // 7: iam_pb.GetUserResponse
(*UpdateUserRequest)(nil), // 8: iam_pb.UpdateUserRequest
(*UpdateUserResponse)(nil), // 9: iam_pb.UpdateUserResponse
(*DeleteUserRequest)(nil), // 10: iam_pb.DeleteUserRequest
(*DeleteUserResponse)(nil), // 11: iam_pb.DeleteUserResponse
(*ListUsersRequest)(nil), // 12: iam_pb.ListUsersRequest
(*ListUsersResponse)(nil), // 13: iam_pb.ListUsersResponse
(*CreateAccessKeyRequest)(nil), // 14: iam_pb.CreateAccessKeyRequest
(*CreateAccessKeyResponse)(nil), // 15: iam_pb.CreateAccessKeyResponse
(*DeleteAccessKeyRequest)(nil), // 16: iam_pb.DeleteAccessKeyRequest
(*DeleteAccessKeyResponse)(nil), // 17: iam_pb.DeleteAccessKeyResponse
(*GetUserByAccessKeyRequest)(nil), // 18: iam_pb.GetUserByAccessKeyRequest
(*GetUserByAccessKeyResponse)(nil), // 19: iam_pb.GetUserByAccessKeyResponse
(*S3ApiConfiguration)(nil), // 20: iam_pb.S3ApiConfiguration
(*Identity)(nil), // 21: iam_pb.Identity
(*Credential)(nil), // 22: iam_pb.Credential
(*Account)(nil), // 23: iam_pb.Account
(*ServiceAccount)(nil), // 24: iam_pb.ServiceAccount
(*PutPolicyRequest)(nil), // 25: iam_pb.PutPolicyRequest
(*PutPolicyResponse)(nil), // 26: iam_pb.PutPolicyResponse
(*GetPolicyRequest)(nil), // 27: iam_pb.GetPolicyRequest
(*GetPolicyResponse)(nil), // 28: iam_pb.GetPolicyResponse
(*ListPoliciesRequest)(nil), // 29: iam_pb.ListPoliciesRequest
(*ListPoliciesResponse)(nil), // 30: iam_pb.ListPoliciesResponse
(*DeletePolicyRequest)(nil), // 31: iam_pb.DeletePolicyRequest
(*DeletePolicyResponse)(nil), // 32: iam_pb.DeletePolicyResponse
(*Policy)(nil), // 33: iam_pb.Policy
(*GetConfigurationRequest)(nil), // 0: iam_pb.GetConfigurationRequest
(*GetConfigurationResponse)(nil), // 1: iam_pb.GetConfigurationResponse
(*PutConfigurationRequest)(nil), // 2: iam_pb.PutConfigurationRequest
(*PutConfigurationResponse)(nil), // 3: iam_pb.PutConfigurationResponse
(*CreateUserRequest)(nil), // 4: iam_pb.CreateUserRequest
(*CreateUserResponse)(nil), // 5: iam_pb.CreateUserResponse
(*GetUserRequest)(nil), // 6: iam_pb.GetUserRequest
(*GetUserResponse)(nil), // 7: iam_pb.GetUserResponse
(*UpdateUserRequest)(nil), // 8: iam_pb.UpdateUserRequest
(*UpdateUserResponse)(nil), // 9: iam_pb.UpdateUserResponse
(*DeleteUserRequest)(nil), // 10: iam_pb.DeleteUserRequest
(*DeleteUserResponse)(nil), // 11: iam_pb.DeleteUserResponse
(*ListUsersRequest)(nil), // 12: iam_pb.ListUsersRequest
(*ListUsersResponse)(nil), // 13: iam_pb.ListUsersResponse
(*CreateAccessKeyRequest)(nil), // 14: iam_pb.CreateAccessKeyRequest
(*CreateAccessKeyResponse)(nil), // 15: iam_pb.CreateAccessKeyResponse
(*DeleteAccessKeyRequest)(nil), // 16: iam_pb.DeleteAccessKeyRequest
(*DeleteAccessKeyResponse)(nil), // 17: iam_pb.DeleteAccessKeyResponse
(*GetUserByAccessKeyRequest)(nil), // 18: iam_pb.GetUserByAccessKeyRequest
(*GetUserByAccessKeyResponse)(nil), // 19: iam_pb.GetUserByAccessKeyResponse
(*S3ApiConfiguration)(nil), // 20: iam_pb.S3ApiConfiguration
(*Identity)(nil), // 21: iam_pb.Identity
(*Credential)(nil), // 22: iam_pb.Credential
(*Account)(nil), // 23: iam_pb.Account
(*ServiceAccount)(nil), // 24: iam_pb.ServiceAccount
(*PutPolicyRequest)(nil), // 25: iam_pb.PutPolicyRequest
(*PutPolicyResponse)(nil), // 26: iam_pb.PutPolicyResponse
(*GetPolicyRequest)(nil), // 27: iam_pb.GetPolicyRequest
(*GetPolicyResponse)(nil), // 28: iam_pb.GetPolicyResponse
(*ListPoliciesRequest)(nil), // 29: iam_pb.ListPoliciesRequest
(*ListPoliciesResponse)(nil), // 30: iam_pb.ListPoliciesResponse
(*DeletePolicyRequest)(nil), // 31: iam_pb.DeletePolicyRequest
(*DeletePolicyResponse)(nil), // 32: iam_pb.DeletePolicyResponse
(*Policy)(nil), // 33: iam_pb.Policy
(*CreateServiceAccountRequest)(nil), // 34: iam_pb.CreateServiceAccountRequest
(*CreateServiceAccountResponse)(nil), // 35: iam_pb.CreateServiceAccountResponse
(*UpdateServiceAccountRequest)(nil), // 36: iam_pb.UpdateServiceAccountRequest
(*UpdateServiceAccountResponse)(nil), // 37: iam_pb.UpdateServiceAccountResponse
(*DeleteServiceAccountRequest)(nil), // 38: iam_pb.DeleteServiceAccountRequest
(*DeleteServiceAccountResponse)(nil), // 39: iam_pb.DeleteServiceAccountResponse
(*GetServiceAccountRequest)(nil), // 40: iam_pb.GetServiceAccountRequest
(*GetServiceAccountResponse)(nil), // 41: iam_pb.GetServiceAccountResponse
(*ListServiceAccountsRequest)(nil), // 42: iam_pb.ListServiceAccountsRequest
(*ListServiceAccountsResponse)(nil), // 43: iam_pb.ListServiceAccountsResponse
(*GetServiceAccountByAccessKeyRequest)(nil), // 44: iam_pb.GetServiceAccountByAccessKeyRequest
(*GetServiceAccountByAccessKeyResponse)(nil), // 45: iam_pb.GetServiceAccountByAccessKeyResponse
}
var file_iam_proto_depIdxs = []int32{
20, // 0: iam_pb.GetConfigurationResponse.configuration:type_name -> iam_pb.S3ApiConfiguration
@ -1837,39 +2381,56 @@ var file_iam_proto_depIdxs = []int32{
23, // 12: iam_pb.Identity.account:type_name -> iam_pb.Account
22, // 13: iam_pb.ServiceAccount.credential:type_name -> iam_pb.Credential
33, // 14: iam_pb.ListPoliciesResponse.policies:type_name -> iam_pb.Policy
0, // 15: iam_pb.SeaweedIdentityAccessManagement.GetConfiguration:input_type -> iam_pb.GetConfigurationRequest
2, // 16: iam_pb.SeaweedIdentityAccessManagement.PutConfiguration:input_type -> iam_pb.PutConfigurationRequest
4, // 17: iam_pb.SeaweedIdentityAccessManagement.CreateUser:input_type -> iam_pb.CreateUserRequest
6, // 18: iam_pb.SeaweedIdentityAccessManagement.GetUser:input_type -> iam_pb.GetUserRequest
8, // 19: iam_pb.SeaweedIdentityAccessManagement.UpdateUser:input_type -> iam_pb.UpdateUserRequest
10, // 20: iam_pb.SeaweedIdentityAccessManagement.DeleteUser:input_type -> iam_pb.DeleteUserRequest
12, // 21: iam_pb.SeaweedIdentityAccessManagement.ListUsers:input_type -> iam_pb.ListUsersRequest
14, // 22: iam_pb.SeaweedIdentityAccessManagement.CreateAccessKey:input_type -> iam_pb.CreateAccessKeyRequest
16, // 23: iam_pb.SeaweedIdentityAccessManagement.DeleteAccessKey:input_type -> iam_pb.DeleteAccessKeyRequest
18, // 24: iam_pb.SeaweedIdentityAccessManagement.GetUserByAccessKey:input_type -> iam_pb.GetUserByAccessKeyRequest
25, // 25: iam_pb.SeaweedIdentityAccessManagement.PutPolicy:input_type -> iam_pb.PutPolicyRequest
27, // 26: iam_pb.SeaweedIdentityAccessManagement.GetPolicy:input_type -> iam_pb.GetPolicyRequest
29, // 27: iam_pb.SeaweedIdentityAccessManagement.ListPolicies:input_type -> iam_pb.ListPoliciesRequest
31, // 28: iam_pb.SeaweedIdentityAccessManagement.DeletePolicy:input_type -> iam_pb.DeletePolicyRequest
1, // 29: iam_pb.SeaweedIdentityAccessManagement.GetConfiguration:output_type -> iam_pb.GetConfigurationResponse
3, // 30: iam_pb.SeaweedIdentityAccessManagement.PutConfiguration:output_type -> iam_pb.PutConfigurationResponse
5, // 31: iam_pb.SeaweedIdentityAccessManagement.CreateUser:output_type -> iam_pb.CreateUserResponse
7, // 32: iam_pb.SeaweedIdentityAccessManagement.GetUser:output_type -> iam_pb.GetUserResponse
9, // 33: iam_pb.SeaweedIdentityAccessManagement.UpdateUser:output_type -> iam_pb.UpdateUserResponse
11, // 34: iam_pb.SeaweedIdentityAccessManagement.DeleteUser:output_type -> iam_pb.DeleteUserResponse
13, // 35: iam_pb.SeaweedIdentityAccessManagement.ListUsers:output_type -> iam_pb.ListUsersResponse
15, // 36: iam_pb.SeaweedIdentityAccessManagement.CreateAccessKey:output_type -> iam_pb.CreateAccessKeyResponse
17, // 37: iam_pb.SeaweedIdentityAccessManagement.DeleteAccessKey:output_type -> iam_pb.DeleteAccessKeyResponse
19, // 38: iam_pb.SeaweedIdentityAccessManagement.GetUserByAccessKey:output_type -> iam_pb.GetUserByAccessKeyResponse
26, // 39: iam_pb.SeaweedIdentityAccessManagement.PutPolicy:output_type -> iam_pb.PutPolicyResponse
28, // 40: iam_pb.SeaweedIdentityAccessManagement.GetPolicy:output_type -> iam_pb.GetPolicyResponse
30, // 41: iam_pb.SeaweedIdentityAccessManagement.ListPolicies:output_type -> iam_pb.ListPoliciesResponse
32, // 42: iam_pb.SeaweedIdentityAccessManagement.DeletePolicy:output_type -> iam_pb.DeletePolicyResponse
29, // [29:43] is the sub-list for method output_type
15, // [15:29] is the sub-list for method input_type
15, // [15:15] is the sub-list for extension type_name
15, // [15:15] is the sub-list for extension extendee
0, // [0:15] is the sub-list for field type_name
24, // 15: iam_pb.CreateServiceAccountRequest.service_account:type_name -> iam_pb.ServiceAccount
24, // 16: iam_pb.UpdateServiceAccountRequest.service_account:type_name -> iam_pb.ServiceAccount
24, // 17: iam_pb.GetServiceAccountResponse.service_account:type_name -> iam_pb.ServiceAccount
24, // 18: iam_pb.ListServiceAccountsResponse.service_accounts:type_name -> iam_pb.ServiceAccount
24, // 19: iam_pb.GetServiceAccountByAccessKeyResponse.service_account:type_name -> iam_pb.ServiceAccount
0, // 20: iam_pb.SeaweedIdentityAccessManagement.GetConfiguration:input_type -> iam_pb.GetConfigurationRequest
2, // 21: iam_pb.SeaweedIdentityAccessManagement.PutConfiguration:input_type -> iam_pb.PutConfigurationRequest
4, // 22: iam_pb.SeaweedIdentityAccessManagement.CreateUser:input_type -> iam_pb.CreateUserRequest
6, // 23: iam_pb.SeaweedIdentityAccessManagement.GetUser:input_type -> iam_pb.GetUserRequest
8, // 24: iam_pb.SeaweedIdentityAccessManagement.UpdateUser:input_type -> iam_pb.UpdateUserRequest
10, // 25: iam_pb.SeaweedIdentityAccessManagement.DeleteUser:input_type -> iam_pb.DeleteUserRequest
12, // 26: iam_pb.SeaweedIdentityAccessManagement.ListUsers:input_type -> iam_pb.ListUsersRequest
14, // 27: iam_pb.SeaweedIdentityAccessManagement.CreateAccessKey:input_type -> iam_pb.CreateAccessKeyRequest
16, // 28: iam_pb.SeaweedIdentityAccessManagement.DeleteAccessKey:input_type -> iam_pb.DeleteAccessKeyRequest
18, // 29: iam_pb.SeaweedIdentityAccessManagement.GetUserByAccessKey:input_type -> iam_pb.GetUserByAccessKeyRequest
25, // 30: iam_pb.SeaweedIdentityAccessManagement.PutPolicy:input_type -> iam_pb.PutPolicyRequest
27, // 31: iam_pb.SeaweedIdentityAccessManagement.GetPolicy:input_type -> iam_pb.GetPolicyRequest
29, // 32: iam_pb.SeaweedIdentityAccessManagement.ListPolicies:input_type -> iam_pb.ListPoliciesRequest
31, // 33: iam_pb.SeaweedIdentityAccessManagement.DeletePolicy:input_type -> iam_pb.DeletePolicyRequest
34, // 34: iam_pb.SeaweedIdentityAccessManagement.CreateServiceAccount:input_type -> iam_pb.CreateServiceAccountRequest
36, // 35: iam_pb.SeaweedIdentityAccessManagement.UpdateServiceAccount:input_type -> iam_pb.UpdateServiceAccountRequest
38, // 36: iam_pb.SeaweedIdentityAccessManagement.DeleteServiceAccount:input_type -> iam_pb.DeleteServiceAccountRequest
40, // 37: iam_pb.SeaweedIdentityAccessManagement.GetServiceAccount:input_type -> iam_pb.GetServiceAccountRequest
42, // 38: iam_pb.SeaweedIdentityAccessManagement.ListServiceAccounts:input_type -> iam_pb.ListServiceAccountsRequest
44, // 39: iam_pb.SeaweedIdentityAccessManagement.GetServiceAccountByAccessKey:input_type -> iam_pb.GetServiceAccountByAccessKeyRequest
1, // 40: iam_pb.SeaweedIdentityAccessManagement.GetConfiguration:output_type -> iam_pb.GetConfigurationResponse
3, // 41: iam_pb.SeaweedIdentityAccessManagement.PutConfiguration:output_type -> iam_pb.PutConfigurationResponse
5, // 42: iam_pb.SeaweedIdentityAccessManagement.CreateUser:output_type -> iam_pb.CreateUserResponse
7, // 43: iam_pb.SeaweedIdentityAccessManagement.GetUser:output_type -> iam_pb.GetUserResponse
9, // 44: iam_pb.SeaweedIdentityAccessManagement.UpdateUser:output_type -> iam_pb.UpdateUserResponse
11, // 45: iam_pb.SeaweedIdentityAccessManagement.DeleteUser:output_type -> iam_pb.DeleteUserResponse
13, // 46: iam_pb.SeaweedIdentityAccessManagement.ListUsers:output_type -> iam_pb.ListUsersResponse
15, // 47: iam_pb.SeaweedIdentityAccessManagement.CreateAccessKey:output_type -> iam_pb.CreateAccessKeyResponse
17, // 48: iam_pb.SeaweedIdentityAccessManagement.DeleteAccessKey:output_type -> iam_pb.DeleteAccessKeyResponse
19, // 49: iam_pb.SeaweedIdentityAccessManagement.GetUserByAccessKey:output_type -> iam_pb.GetUserByAccessKeyResponse
26, // 50: iam_pb.SeaweedIdentityAccessManagement.PutPolicy:output_type -> iam_pb.PutPolicyResponse
28, // 51: iam_pb.SeaweedIdentityAccessManagement.GetPolicy:output_type -> iam_pb.GetPolicyResponse
30, // 52: iam_pb.SeaweedIdentityAccessManagement.ListPolicies:output_type -> iam_pb.ListPoliciesResponse
32, // 53: iam_pb.SeaweedIdentityAccessManagement.DeletePolicy:output_type -> iam_pb.DeletePolicyResponse
35, // 54: iam_pb.SeaweedIdentityAccessManagement.CreateServiceAccount:output_type -> iam_pb.CreateServiceAccountResponse
37, // 55: iam_pb.SeaweedIdentityAccessManagement.UpdateServiceAccount:output_type -> iam_pb.UpdateServiceAccountResponse
39, // 56: iam_pb.SeaweedIdentityAccessManagement.DeleteServiceAccount:output_type -> iam_pb.DeleteServiceAccountResponse
41, // 57: iam_pb.SeaweedIdentityAccessManagement.GetServiceAccount:output_type -> iam_pb.GetServiceAccountResponse
43, // 58: iam_pb.SeaweedIdentityAccessManagement.ListServiceAccounts:output_type -> iam_pb.ListServiceAccountsResponse
45, // 59: iam_pb.SeaweedIdentityAccessManagement.GetServiceAccountByAccessKey:output_type -> iam_pb.GetServiceAccountByAccessKeyResponse
40, // [40:60] is the sub-list for method output_type
20, // [20:40] is the sub-list for method input_type
20, // [20:20] is the sub-list for extension type_name
20, // [20:20] is the sub-list for extension extendee
0, // [0:20] is the sub-list for field type_name
}
func init() { file_iam_proto_init() }
@ -1883,7 +2444,7 @@ func file_iam_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_iam_proto_rawDesc), len(file_iam_proto_rawDesc)),
NumEnums: 0,
NumMessages: 34,
NumMessages: 46,
NumExtensions: 0,
NumServices: 1,
},

258
weed/pb/iam_pb/iam_grpc.pb.go

@ -19,20 +19,26 @@ import (
const _ = grpc.SupportPackageIsVersion9
const (
SeaweedIdentityAccessManagement_GetConfiguration_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetConfiguration"
SeaweedIdentityAccessManagement_PutConfiguration_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/PutConfiguration"
SeaweedIdentityAccessManagement_CreateUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/CreateUser"
SeaweedIdentityAccessManagement_GetUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetUser"
SeaweedIdentityAccessManagement_UpdateUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/UpdateUser"
SeaweedIdentityAccessManagement_DeleteUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeleteUser"
SeaweedIdentityAccessManagement_ListUsers_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/ListUsers"
SeaweedIdentityAccessManagement_CreateAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/CreateAccessKey"
SeaweedIdentityAccessManagement_DeleteAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeleteAccessKey"
SeaweedIdentityAccessManagement_GetUserByAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetUserByAccessKey"
SeaweedIdentityAccessManagement_PutPolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/PutPolicy"
SeaweedIdentityAccessManagement_GetPolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetPolicy"
SeaweedIdentityAccessManagement_ListPolicies_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/ListPolicies"
SeaweedIdentityAccessManagement_DeletePolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeletePolicy"
SeaweedIdentityAccessManagement_GetConfiguration_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetConfiguration"
SeaweedIdentityAccessManagement_PutConfiguration_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/PutConfiguration"
SeaweedIdentityAccessManagement_CreateUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/CreateUser"
SeaweedIdentityAccessManagement_GetUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetUser"
SeaweedIdentityAccessManagement_UpdateUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/UpdateUser"
SeaweedIdentityAccessManagement_DeleteUser_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeleteUser"
SeaweedIdentityAccessManagement_ListUsers_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/ListUsers"
SeaweedIdentityAccessManagement_CreateAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/CreateAccessKey"
SeaweedIdentityAccessManagement_DeleteAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeleteAccessKey"
SeaweedIdentityAccessManagement_GetUserByAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetUserByAccessKey"
SeaweedIdentityAccessManagement_PutPolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/PutPolicy"
SeaweedIdentityAccessManagement_GetPolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetPolicy"
SeaweedIdentityAccessManagement_ListPolicies_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/ListPolicies"
SeaweedIdentityAccessManagement_DeletePolicy_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeletePolicy"
SeaweedIdentityAccessManagement_CreateServiceAccount_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/CreateServiceAccount"
SeaweedIdentityAccessManagement_UpdateServiceAccount_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/UpdateServiceAccount"
SeaweedIdentityAccessManagement_DeleteServiceAccount_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/DeleteServiceAccount"
SeaweedIdentityAccessManagement_GetServiceAccount_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetServiceAccount"
SeaweedIdentityAccessManagement_ListServiceAccounts_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/ListServiceAccounts"
SeaweedIdentityAccessManagement_GetServiceAccountByAccessKey_FullMethodName = "/iam_pb.SeaweedIdentityAccessManagement/GetServiceAccountByAccessKey"
)
// SeaweedIdentityAccessManagementClient is the client API for SeaweedIdentityAccessManagement service.
@ -57,6 +63,13 @@ type SeaweedIdentityAccessManagementClient interface {
GetPolicy(ctx context.Context, in *GetPolicyRequest, opts ...grpc.CallOption) (*GetPolicyResponse, error)
ListPolicies(ctx context.Context, in *ListPoliciesRequest, opts ...grpc.CallOption) (*ListPoliciesResponse, error)
DeletePolicy(ctx context.Context, in *DeletePolicyRequest, opts ...grpc.CallOption) (*DeletePolicyResponse, error)
// Service Account Management
CreateServiceAccount(ctx context.Context, in *CreateServiceAccountRequest, opts ...grpc.CallOption) (*CreateServiceAccountResponse, error)
UpdateServiceAccount(ctx context.Context, in *UpdateServiceAccountRequest, opts ...grpc.CallOption) (*UpdateServiceAccountResponse, error)
DeleteServiceAccount(ctx context.Context, in *DeleteServiceAccountRequest, opts ...grpc.CallOption) (*DeleteServiceAccountResponse, error)
GetServiceAccount(ctx context.Context, in *GetServiceAccountRequest, opts ...grpc.CallOption) (*GetServiceAccountResponse, error)
ListServiceAccounts(ctx context.Context, in *ListServiceAccountsRequest, opts ...grpc.CallOption) (*ListServiceAccountsResponse, error)
GetServiceAccountByAccessKey(ctx context.Context, in *GetServiceAccountByAccessKeyRequest, opts ...grpc.CallOption) (*GetServiceAccountByAccessKeyResponse, error)
}
type seaweedIdentityAccessManagementClient struct {
@ -207,6 +220,66 @@ func (c *seaweedIdentityAccessManagementClient) DeletePolicy(ctx context.Context
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) CreateServiceAccount(ctx context.Context, in *CreateServiceAccountRequest, opts ...grpc.CallOption) (*CreateServiceAccountResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(CreateServiceAccountResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_CreateServiceAccount_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) UpdateServiceAccount(ctx context.Context, in *UpdateServiceAccountRequest, opts ...grpc.CallOption) (*UpdateServiceAccountResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(UpdateServiceAccountResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_UpdateServiceAccount_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) DeleteServiceAccount(ctx context.Context, in *DeleteServiceAccountRequest, opts ...grpc.CallOption) (*DeleteServiceAccountResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(DeleteServiceAccountResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_DeleteServiceAccount_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) GetServiceAccount(ctx context.Context, in *GetServiceAccountRequest, opts ...grpc.CallOption) (*GetServiceAccountResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetServiceAccountResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_GetServiceAccount_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) ListServiceAccounts(ctx context.Context, in *ListServiceAccountsRequest, opts ...grpc.CallOption) (*ListServiceAccountsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListServiceAccountsResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_ListServiceAccounts_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *seaweedIdentityAccessManagementClient) GetServiceAccountByAccessKey(ctx context.Context, in *GetServiceAccountByAccessKeyRequest, opts ...grpc.CallOption) (*GetServiceAccountByAccessKeyResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetServiceAccountByAccessKeyResponse)
err := c.cc.Invoke(ctx, SeaweedIdentityAccessManagement_GetServiceAccountByAccessKey_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// SeaweedIdentityAccessManagementServer is the server API for SeaweedIdentityAccessManagement service.
// All implementations must embed UnimplementedSeaweedIdentityAccessManagementServer
// for forward compatibility.
@ -229,6 +302,13 @@ type SeaweedIdentityAccessManagementServer interface {
GetPolicy(context.Context, *GetPolicyRequest) (*GetPolicyResponse, error)
ListPolicies(context.Context, *ListPoliciesRequest) (*ListPoliciesResponse, error)
DeletePolicy(context.Context, *DeletePolicyRequest) (*DeletePolicyResponse, error)
// Service Account Management
CreateServiceAccount(context.Context, *CreateServiceAccountRequest) (*CreateServiceAccountResponse, error)
UpdateServiceAccount(context.Context, *UpdateServiceAccountRequest) (*UpdateServiceAccountResponse, error)
DeleteServiceAccount(context.Context, *DeleteServiceAccountRequest) (*DeleteServiceAccountResponse, error)
GetServiceAccount(context.Context, *GetServiceAccountRequest) (*GetServiceAccountResponse, error)
ListServiceAccounts(context.Context, *ListServiceAccountsRequest) (*ListServiceAccountsResponse, error)
GetServiceAccountByAccessKey(context.Context, *GetServiceAccountByAccessKeyRequest) (*GetServiceAccountByAccessKeyResponse, error)
mustEmbedUnimplementedSeaweedIdentityAccessManagementServer()
}
@ -281,6 +361,24 @@ func (UnimplementedSeaweedIdentityAccessManagementServer) ListPolicies(context.C
func (UnimplementedSeaweedIdentityAccessManagementServer) DeletePolicy(context.Context, *DeletePolicyRequest) (*DeletePolicyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeletePolicy not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) CreateServiceAccount(context.Context, *CreateServiceAccountRequest) (*CreateServiceAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateServiceAccount not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) UpdateServiceAccount(context.Context, *UpdateServiceAccountRequest) (*UpdateServiceAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateServiceAccount not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) DeleteServiceAccount(context.Context, *DeleteServiceAccountRequest) (*DeleteServiceAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteServiceAccount not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) GetServiceAccount(context.Context, *GetServiceAccountRequest) (*GetServiceAccountResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetServiceAccount not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) ListServiceAccounts(context.Context, *ListServiceAccountsRequest) (*ListServiceAccountsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListServiceAccounts not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) GetServiceAccountByAccessKey(context.Context, *GetServiceAccountByAccessKeyRequest) (*GetServiceAccountByAccessKeyResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetServiceAccountByAccessKey not implemented")
}
func (UnimplementedSeaweedIdentityAccessManagementServer) mustEmbedUnimplementedSeaweedIdentityAccessManagementServer() {
}
func (UnimplementedSeaweedIdentityAccessManagementServer) testEmbeddedByValue() {}
@ -555,6 +653,114 @@ func _SeaweedIdentityAccessManagement_DeletePolicy_Handler(srv interface{}, ctx
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_CreateServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(CreateServiceAccountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).CreateServiceAccount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_CreateServiceAccount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).CreateServiceAccount(ctx, req.(*CreateServiceAccountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_UpdateServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateServiceAccountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).UpdateServiceAccount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_UpdateServiceAccount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).UpdateServiceAccount(ctx, req.(*UpdateServiceAccountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_DeleteServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteServiceAccountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).DeleteServiceAccount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_DeleteServiceAccount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).DeleteServiceAccount(ctx, req.(*DeleteServiceAccountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_GetServiceAccount_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetServiceAccountRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).GetServiceAccount(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_GetServiceAccount_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).GetServiceAccount(ctx, req.(*GetServiceAccountRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_ListServiceAccounts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListServiceAccountsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).ListServiceAccounts(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_ListServiceAccounts_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).ListServiceAccounts(ctx, req.(*ListServiceAccountsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _SeaweedIdentityAccessManagement_GetServiceAccountByAccessKey_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetServiceAccountByAccessKeyRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(SeaweedIdentityAccessManagementServer).GetServiceAccountByAccessKey(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: SeaweedIdentityAccessManagement_GetServiceAccountByAccessKey_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(SeaweedIdentityAccessManagementServer).GetServiceAccountByAccessKey(ctx, req.(*GetServiceAccountByAccessKeyRequest))
}
return interceptor(ctx, in, info, handler)
}
// SeaweedIdentityAccessManagement_ServiceDesc is the grpc.ServiceDesc for SeaweedIdentityAccessManagement service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -618,6 +824,30 @@ var SeaweedIdentityAccessManagement_ServiceDesc = grpc.ServiceDesc{
MethodName: "DeletePolicy",
Handler: _SeaweedIdentityAccessManagement_DeletePolicy_Handler,
},
{
MethodName: "CreateServiceAccount",
Handler: _SeaweedIdentityAccessManagement_CreateServiceAccount_Handler,
},
{
MethodName: "UpdateServiceAccount",
Handler: _SeaweedIdentityAccessManagement_UpdateServiceAccount_Handler,
},
{
MethodName: "DeleteServiceAccount",
Handler: _SeaweedIdentityAccessManagement_DeleteServiceAccount_Handler,
},
{
MethodName: "GetServiceAccount",
Handler: _SeaweedIdentityAccessManagement_GetServiceAccount_Handler,
},
{
MethodName: "ListServiceAccounts",
Handler: _SeaweedIdentityAccessManagement_ListServiceAccounts_Handler,
},
{
MethodName: "GetServiceAccountByAccessKey",
Handler: _SeaweedIdentityAccessManagement_GetServiceAccountByAccessKey_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "iam.proto",

137
weed/server/filer_server_handlers_iam_grpc.go

@ -144,10 +144,6 @@ func (s *IamGrpcServer) DeleteUser(ctx context.Context, req *iam_pb.DeleteUserRe
err := s.credentialManager.DeleteUser(ctx, req.Username)
if err != nil {
if err == credential.ErrUserNotFound {
// Deleting a non-existent user is generally considered a success or Not Found depending on semantics
// In S3 API, usually idempotent. But for Admin API, often 404.
// Here we return NotFound to let client decide, but traditionally delete is idempotent.
// However, if we want strict status codes:
return nil, status.Errorf(codes.NotFound, "user %s not found", req.Username)
}
glog.Errorf("Failed to delete user %s: %v", req.Username, err)
@ -256,6 +252,9 @@ func (s *IamGrpcServer) PutPolicy(ctx context.Context, req *iam_pb.PutPolicyRequ
if req.Name == "" {
return nil, status.Errorf(codes.InvalidArgument, "policy name is required")
}
if err := credential.ValidatePolicyName(req.Name); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Content == "" {
return nil, status.Errorf(codes.InvalidArgument, "policy content is required")
}
@ -349,3 +348,133 @@ func (s *IamGrpcServer) DeletePolicy(ctx context.Context, req *iam_pb.DeletePoli
return &iam_pb.DeletePolicyResponse{}, nil
}
//////////////////////////////////////////////////
// Service Account Management
func (s *IamGrpcServer) CreateServiceAccount(ctx context.Context, req *iam_pb.CreateServiceAccountRequest) (*iam_pb.CreateServiceAccountResponse, error) {
if req == nil || req.ServiceAccount == nil {
return nil, status.Errorf(codes.InvalidArgument, "service account is required")
}
if err := credential.ValidateServiceAccountId(req.ServiceAccount.Id); err != nil {
return nil, status.Errorf(codes.InvalidArgument, "%v", err)
}
glog.V(4).Infof("CreateServiceAccount: %s", req.ServiceAccount.Id)
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
err := s.credentialManager.CreateServiceAccount(ctx, req.ServiceAccount)
if err != nil {
glog.Errorf("Failed to create service account %s: %v", req.ServiceAccount.Id, err)
return nil, status.Errorf(codes.Internal, "failed to create service account: %v", err)
}
return &iam_pb.CreateServiceAccountResponse{}, nil
}
func (s *IamGrpcServer) UpdateServiceAccount(ctx context.Context, req *iam_pb.UpdateServiceAccountRequest) (*iam_pb.UpdateServiceAccountResponse, error) {
if req == nil || req.ServiceAccount == nil {
return nil, status.Errorf(codes.InvalidArgument, "service account is required")
}
glog.V(4).Infof("UpdateServiceAccount: %s", req.Id)
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
err := s.credentialManager.UpdateServiceAccount(ctx, req.Id, req.ServiceAccount)
if err != nil {
glog.Errorf("Failed to update service account %s: %v", req.Id, err)
return nil, status.Errorf(codes.Internal, "failed to update service account: %v", err)
}
return &iam_pb.UpdateServiceAccountResponse{}, nil
}
func (s *IamGrpcServer) DeleteServiceAccount(ctx context.Context, req *iam_pb.DeleteServiceAccountRequest) (*iam_pb.DeleteServiceAccountResponse, error) {
glog.V(4).Infof("DeleteServiceAccount: %s", req.Id)
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
err := s.credentialManager.DeleteServiceAccount(ctx, req.Id)
if err != nil {
if err == credential.ErrServiceAccountNotFound {
return nil, status.Errorf(codes.NotFound, "service account %s not found", req.Id)
}
glog.Errorf("Failed to delete service account %s: %v", req.Id, err)
return nil, status.Errorf(codes.Internal, "failed to delete service account: %v", err)
}
return &iam_pb.DeleteServiceAccountResponse{}, nil
}
func (s *IamGrpcServer) GetServiceAccount(ctx context.Context, req *iam_pb.GetServiceAccountRequest) (*iam_pb.GetServiceAccountResponse, error) {
glog.V(4).Infof("GetServiceAccount: %s", req.Id)
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
sa, err := s.credentialManager.GetServiceAccount(ctx, req.Id)
if err != nil {
glog.Errorf("Failed to get service account %s: %v", req.Id, err)
return nil, status.Errorf(codes.Internal, "failed to get service account: %v", err)
}
if sa == nil {
return nil, status.Errorf(codes.NotFound, "service account %s not found", req.Id)
}
return &iam_pb.GetServiceAccountResponse{
ServiceAccount: sa,
}, nil
}
func (s *IamGrpcServer) ListServiceAccounts(ctx context.Context, req *iam_pb.ListServiceAccountsRequest) (*iam_pb.ListServiceAccountsResponse, error) {
glog.V(4).Infof("ListServiceAccounts")
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
accounts, err := s.credentialManager.ListServiceAccounts(ctx)
if err != nil {
glog.Errorf("Failed to list service accounts: %v", err)
return nil, status.Errorf(codes.Internal, "failed to list service accounts: %v", err)
}
return &iam_pb.ListServiceAccountsResponse{
ServiceAccounts: accounts,
}, nil
}
func (s *IamGrpcServer) GetServiceAccountByAccessKey(ctx context.Context, req *iam_pb.GetServiceAccountByAccessKeyRequest) (*iam_pb.GetServiceAccountByAccessKeyResponse, error) {
if req == nil {
return nil, status.Errorf(codes.InvalidArgument, "request is required")
}
glog.V(4).Infof("GetServiceAccountByAccessKey: %s", req.AccessKey)
if req.AccessKey == "" {
return nil, status.Errorf(codes.InvalidArgument, "access key is required")
}
if s.credentialManager == nil {
return nil, status.Errorf(codes.FailedPrecondition, "credential manager is not configured")
}
sa, err := s.credentialManager.GetStore().GetServiceAccountByAccessKey(ctx, req.AccessKey)
if err != nil {
if err == credential.ErrAccessKeyNotFound {
return nil, status.Errorf(codes.NotFound, "access key %s not found", req.AccessKey)
}
glog.Errorf("Failed to get service account by access key %s: %v", req.AccessKey, err)
return nil, status.Errorf(codes.Internal, "failed to get service account: %v", err)
}
return &iam_pb.GetServiceAccountByAccessKeyResponse{
ServiceAccount: sa,
}, nil
}
Loading…
Cancel
Save