Browse Source
Merge branch 'origin/master' into master
Merge branch 'origin/master' into master
Resolved merge conflicts in: - weed/admin/static/js/modal-alerts.js: Adopted incoming improvements and HTML support. - weed/admin/view/app/collection_details.templ: Switched to showAlert info type. - weed/admin/view/app/file_browser.templ: Used descriptive delete message. - weed/admin/view/app/maintenance_workers.templ: Used encoding and headers in pauseWorker. - weed/admin/view/app/object_store_users.templ: Restored accidentally deleted delete functions and used encodeURIComponent. - weed/admin/view/app/policies.templ: Standardized on showAlert and descriptive confirmations. Regenerated all templ files.pull/8128/head
72 changed files with 3365 additions and 2116 deletions
-
2.github/workflows/e2e.yml
-
6.github/workflows/ec-integration.yml
-
2.github/workflows/go.yml
-
20go.mod
-
36go.sum
-
59weed/admin/dash/policies_management.go
-
53weed/admin/dash/service_account_helpers.go
-
373weed/admin/dash/service_account_management.go
-
55weed/admin/static/js/iam-utils.js
-
130weed/admin/static/js/modal-alerts.js
-
58weed/admin/view/app/admin_templ.go
-
14weed/admin/view/app/cluster_brokers_templ.go
-
46weed/admin/view/app/cluster_collections_templ.go
-
54weed/admin/view/app/cluster_ec_shards.templ
-
158weed/admin/view/app/cluster_ec_shards_templ.go
-
6weed/admin/view/app/cluster_ec_volumes.templ
-
72weed/admin/view/app/cluster_ec_volumes_templ.go
-
18weed/admin/view/app/cluster_filers_templ.go
-
18weed/admin/view/app/cluster_masters_templ.go
-
84weed/admin/view/app/cluster_volume_servers_templ.go
-
92weed/admin/view/app/cluster_volumes_templ.go
-
4weed/admin/view/app/collection_details.templ
-
54weed/admin/view/app/collection_details_templ.go
-
44weed/admin/view/app/ec_volume_details_templ.go
-
9weed/admin/view/app/file_browser.templ
-
58weed/admin/view/app/file_browser_templ.go
-
18weed/admin/view/app/maintenance_config_schema.templ
-
50weed/admin/view/app/maintenance_config_schema_templ.go
-
30weed/admin/view/app/maintenance_config_templ.go
-
60weed/admin/view/app/maintenance_queue_templ.go
-
26weed/admin/view/app/maintenance_workers.templ
-
42weed/admin/view/app/maintenance_workers_templ.go
-
66weed/admin/view/app/object_store_users.templ
-
24weed/admin/view/app/object_store_users_templ.go
-
40weed/admin/view/app/policies.templ
-
24weed/admin/view/app/policies_templ.go
-
50weed/admin/view/app/s3_buckets_templ.go
-
9weed/admin/view/app/service_accounts.templ
-
30weed/admin/view/app/service_accounts_templ.go
-
22weed/admin/view/app/subscribers_templ.go
-
72weed/admin/view/app/task_config_schema_templ.go
-
16weed/admin/view/app/task_config_templ.go
-
6weed/admin/view/app/task_config_templ_templ.go
-
16weed/admin/view/app/task_detail.templ
-
140weed/admin/view/app/task_detail_templ.go
-
94weed/admin/view/app/topic_details_templ.go
-
20weed/admin/view/app/topics_templ.go
-
84weed/admin/view/app/volume_details_templ.go
-
14weed/admin/view/components/config_sections_templ.go
-
102weed/admin/view/components/form_fields_templ.go
-
1weed/admin/view/layout/layout.templ
-
44weed/admin/view/layout/layout_templ.go
-
1weed/command/imports.go
-
5weed/command/scaffold/credential.toml
-
45weed/credential/credential_manager.go
-
24weed/credential/credential_store.go
-
210weed/credential/filer_etc/filer_etc_identity.go
-
224weed/credential/filer_etc/filer_etc_policy.go
-
206weed/credential/filer_etc/filer_etc_service_account.go
-
498weed/credential/filer_multiple/filer_multiple_store.go
-
16weed/credential/grpc/grpc_policy.go
-
78weed/credential/grpc/grpc_service_account.go
-
85weed/credential/memory/memory_service_account.go
-
29weed/credential/memory/memory_store.go
-
65weed/credential/memory/memory_store_test.go
-
173weed/credential/postgres/postgres_service_account.go
-
15weed/credential/postgres/postgres_store.go
-
28weed/credential/validation.go
-
56weed/pb/iam.proto
-
703weed/pb/iam_pb/iam.pb.go
-
258weed/pb/iam_pb/iam_grpc.pb.go
-
137weed/server/filer_server_handlers_iam_grpc.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 |
|||
} |
|||
@ -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?'); |
|||
} |
|||
58
weed/admin/view/app/file_browser_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
50
weed/admin/view/app/maintenance_config_schema_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
42
weed/admin/view/app/maintenance_workers_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
24
weed/admin/view/app/object_store_users_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
24
weed/admin/view/app/policies_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
30
weed/admin/view/app/service_accounts_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
140
weed/admin/view/app/task_detail_templ.go
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -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 |
|||
} |
|||
@ -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) |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
} |
|||
@ -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 |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue