snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
189
weed/credential/filer_etc/filer_etc_group.go
Normal file
189
weed/credential/filer_etc/filer_etc_group.go
Normal file
@@ -0,0 +1,189 @@
|
||||
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/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
|
||||
)
|
||||
|
||||
const IamGroupsDirectory = "groups"
|
||||
|
||||
func (store *FilerEtcStore) loadGroupsFromMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
dir := filer.IamConfigDirectory + "/" + IamGroupsDirectory
|
||||
entries, err := listEntries(ctx, client, dir)
|
||||
if err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDirectory {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if len(entry.Content) > 0 {
|
||||
content = entry.Content
|
||||
} else {
|
||||
c, err := filer.ReadInsideFiler(ctx, client, dir, entry.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read group file %s: %w", entry.Name, err)
|
||||
}
|
||||
content = c
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("group file %s is empty", entry.Name)
|
||||
}
|
||||
g := &iam_pb.Group{}
|
||||
if err := json.Unmarshal(content, g); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal group %s: %w", entry.Name, err)
|
||||
}
|
||||
if g.Name == "" {
|
||||
return fmt.Errorf("group file %s has empty name", entry.Name)
|
||||
}
|
||||
// Merge: overwrite existing group with same name or append
|
||||
found := false
|
||||
for i, existing := range s3cfg.Groups {
|
||||
if existing.Name == g.Name {
|
||||
s3cfg.Groups[i] = g
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
s3cfg.Groups = append(s3cfg.Groups, g)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) saveGroup(ctx context.Context, group *iam_pb.Group) error {
|
||||
if group == nil {
|
||||
return fmt.Errorf("group is nil")
|
||||
}
|
||||
group.Name = strings.TrimSpace(group.Name)
|
||||
if group.Name == "" {
|
||||
return fmt.Errorf("group name is required")
|
||||
}
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := json.MarshalIndent(group, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return filer.SaveInsideFiler(context.Background(), client, filer.IamConfigDirectory+"/"+IamGroupsDirectory, group.Name+".json", data)
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) deleteGroupFile(ctx context.Context, groupName string) error {
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
resp, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
|
||||
Directory: filer.IamConfigDirectory + "/" + IamGroupsDirectory,
|
||||
Name: groupName + ".json",
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), filer_pb.ErrNotFound.Error()) {
|
||||
return credential.ErrGroupNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Error != "" {
|
||||
if strings.Contains(resp.Error, filer_pb.ErrNotFound.Error()) {
|
||||
return credential.ErrGroupNotFound
|
||||
}
|
||||
return fmt.Errorf("delete group %s: %s", groupName, resp.Error)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) CreateGroup(ctx context.Context, group *iam_pb.Group) error {
|
||||
if group != nil {
|
||||
group.Name = strings.TrimSpace(group.Name)
|
||||
}
|
||||
if group == nil || group.Name == "" {
|
||||
return fmt.Errorf("group name is required")
|
||||
}
|
||||
existing, err := store.GetGroup(ctx, group.Name)
|
||||
if err != nil {
|
||||
if !errors.Is(err, credential.ErrGroupNotFound) {
|
||||
return err
|
||||
}
|
||||
} else if existing != nil {
|
||||
return credential.ErrGroupAlreadyExists
|
||||
}
|
||||
return store.saveGroup(ctx, group)
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) GetGroup(ctx context.Context, groupName string) (*iam_pb.Group, error) {
|
||||
var group *iam_pb.Group
|
||||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := filer.ReadInsideFiler(ctx, client, filer.IamConfigDirectory+"/"+IamGroupsDirectory, groupName+".json")
|
||||
if err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return credential.ErrGroupNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return credential.ErrGroupNotFound
|
||||
}
|
||||
group = &iam_pb.Group{}
|
||||
return json.Unmarshal(data, group)
|
||||
})
|
||||
return group, err
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) DeleteGroup(ctx context.Context, groupName string) error {
|
||||
if _, err := store.GetGroup(ctx, groupName); err != nil {
|
||||
return err
|
||||
}
|
||||
return store.deleteGroupFile(ctx, groupName)
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) ListGroups(ctx context.Context) ([]string, error) {
|
||||
var names []string
|
||||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
entries, err := listEntries(ctx, client, filer.IamConfigDirectory+"/"+IamGroupsDirectory)
|
||||
if err != nil {
|
||||
if errors.Is(err, filer_pb.ErrNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDirectory && strings.HasSuffix(entry.Name, ".json") {
|
||||
names = append(names, strings.TrimSuffix(entry.Name, ".json"))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) UpdateGroup(ctx context.Context, group *iam_pb.Group) error {
|
||||
if group != nil {
|
||||
group.Name = strings.TrimSpace(group.Name)
|
||||
}
|
||||
if group == nil || group.Name == "" {
|
||||
return fmt.Errorf("group name is required")
|
||||
}
|
||||
if _, err := store.GetGroup(ctx, group.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
return store.saveGroup(ctx, group)
|
||||
}
|
||||
580
weed/credential/filer_etc/filer_etc_identity.go
Normal file
580
weed/credential/filer_etc/filer_etc_identity.go
Normal file
@@ -0,0 +1,580 @@
|
||||
package filer_etc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
IamIdentitiesDirectory = "identities"
|
||||
IamServiceAccountsDirectory = "service_accounts"
|
||||
IamLegacyIdentityFile = "identity.json"
|
||||
IamLegacyIdentityOldFile = "identity.json.old"
|
||||
)
|
||||
|
||||
func (store *FilerEtcStore) LoadConfiguration(ctx context.Context) (*iam_pb.S3ApiConfiguration, error) {
|
||||
s3cfg := &iam_pb.S3ApiConfiguration{}
|
||||
|
||||
// 1. Load from legacy single file (low priority)
|
||||
content, foundLegacy, err := store.readInsideFiler(ctx, filer.IamConfigDirectory, IamLegacyIdentityFile)
|
||||
if err != nil {
|
||||
return s3cfg, err
|
||||
}
|
||||
if foundLegacy && len(content) > 0 {
|
||||
if err := filer.ParseS3ConfigurationFromBytes(content, s3cfg); err != nil {
|
||||
glog.Errorf("Failed to parse legacy IAM configuration: %v", err)
|
||||
return s3cfg, err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load from multi-file structure (high priority, overrides legacy details)
|
||||
if _, err := store.loadFromMultiFile(ctx, s3cfg); err != nil {
|
||||
return s3cfg, err
|
||||
}
|
||||
|
||||
// 3. Load service accounts
|
||||
if err := store.loadServiceAccountsFromMultiFile(ctx, s3cfg); err != nil {
|
||||
return s3cfg, fmt.Errorf("failed to load service accounts: %w", err)
|
||||
}
|
||||
|
||||
// 3b. Load groups
|
||||
if err := store.loadGroupsFromMultiFile(ctx, s3cfg); err != nil {
|
||||
return s3cfg, fmt.Errorf("failed to load groups: %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 {
|
||||
if err := store.migrateToMultiFile(ctx, s3cfg); err != nil {
|
||||
glog.Errorf("Failed to migrate IAM configuration to multi-file layout: %v", err)
|
||||
return s3cfg, nil
|
||||
}
|
||||
}
|
||||
|
||||
return s3cfg, nil
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) loadFromMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) (bool, error) {
|
||||
var hasIdentities bool
|
||||
|
||||
// Helper to find existing identity index
|
||||
findIdentity := func(name string) int {
|
||||
for i, identity := range s3cfg.Identities {
|
||||
if identity.Name == name {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// 1. List identities
|
||||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
dir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
entries, err := listEntries(ctx, client, dir)
|
||||
if err != 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 {
|
||||
if entry.IsDirectory {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
hasIdentities = true
|
||||
|
||||
var content []byte
|
||||
if len(entry.Content) > 0 {
|
||||
content = entry.Content
|
||||
} else {
|
||||
c, err := filer.ReadInsideFiler(ctx, client, dir, entry.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read identity file %s: %w", entry.Name, err)
|
||||
}
|
||||
content = c
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("identity file %s is empty", entry.Name)
|
||||
}
|
||||
identity := &iam_pb.Identity{}
|
||||
if err := json.Unmarshal(content, identity); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal identity %s: %w", entry.Name, err)
|
||||
}
|
||||
if identity.Name == "" {
|
||||
return fmt.Errorf("identity file %s has empty name", entry.Name)
|
||||
}
|
||||
|
||||
// Merge logic: Overwrite existing or Append
|
||||
idx := findIdentity(identity.Name)
|
||||
if idx != -1 {
|
||||
s3cfg.Identities[idx] = identity
|
||||
} else {
|
||||
s3cfg.Identities = append(s3cfg.Identities, identity)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return hasIdentities, nil
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) migrateToMultiFile(ctx context.Context, s3cfg *iam_pb.S3ApiConfiguration) error {
|
||||
glog.Infof("Migrating IAM configuration to multi-file layout...")
|
||||
|
||||
// 1. Save all identities
|
||||
for _, identity := range s3cfg.Identities {
|
||||
if err := store.saveIdentity(ctx, identity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Save all service accounts
|
||||
for _, sa := range s3cfg.ServiceAccounts {
|
||||
if err := store.saveServiceAccount(ctx, sa); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Save all groups
|
||||
for _, g := range s3cfg.Groups {
|
||||
if err := store.saveGroup(ctx, g); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Rename legacy file
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
_, err := client.AtomicRenameEntry(ctx, &filer_pb.AtomicRenameEntryRequest{
|
||||
OldDirectory: filer.IamConfigDirectory,
|
||||
OldName: IamLegacyIdentityFile,
|
||||
NewDirectory: filer.IamConfigDirectory,
|
||||
NewName: IamLegacyIdentityOldFile,
|
||||
})
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) SaveConfiguration(ctx context.Context, config *iam_pb.S3ApiConfiguration) error {
|
||||
// 1. Save all identities
|
||||
for _, identity := range config.Identities {
|
||||
if err := store.saveIdentity(ctx, identity); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Save all service accounts
|
||||
for _, sa := range config.ServiceAccounts {
|
||||
if err := store.saveServiceAccount(ctx, sa); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 2b. Save all groups
|
||||
for _, g := range config.Groups {
|
||||
if err := store.saveGroup(ctx, g); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Cleanup removed identities (Full Sync)
|
||||
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
dir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
entries, err := listEntries(ctx, client, dir)
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
validNames := make(map[string]bool)
|
||||
for _, id := range config.Identities {
|
||||
validNames[id.Name+".json"] = true
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDirectory && !validNames[entry.Name] {
|
||||
// Delete obsolete identity file
|
||||
if err := filer_pb.DoRemove(ctx, client, dir, entry.Name, false, false, false, false, nil); err != nil {
|
||||
glog.Warningf("Failed to delete obsolete identity file %s: %v", entry.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
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 := filer_pb.DoRemove(ctx, client, dir, entry.Name, false, false, false, false, nil); err != nil {
|
||||
glog.Warningf("Failed to delete obsolete service account file %s: %v", entry.Name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Cleanup removed groups (Full Sync)
|
||||
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
dir := filer.IamConfigDirectory + "/" + IamGroupsDirectory
|
||||
entries, err := listEntries(ctx, client, dir)
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
validNames := make(map[string]bool)
|
||||
for _, g := range config.Groups {
|
||||
validNames[g.Name+".json"] = true
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if !entry.IsDirectory && !validNames[entry.Name] {
|
||||
if err := filer_pb.DoRemove(ctx, client, dir, entry.Name, false, false, false, false, nil); err != nil {
|
||||
glog.Warningf("Failed to delete obsolete group 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 {
|
||||
// Check if user exists (read specific file)
|
||||
existing, err := store.GetUser(ctx, identity.Name)
|
||||
if err == nil && existing != nil {
|
||||
return credential.ErrUserAlreadyExists
|
||||
}
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) GetUser(ctx context.Context, username string) (*iam_pb.Identity, error) {
|
||||
var identity *iam_pb.Identity
|
||||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := filer.ReadInsideFiler(ctx, client, filer.IamConfigDirectory+"/"+IamIdentitiesDirectory, username+".json")
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
identity = &iam_pb.Identity{}
|
||||
return json.Unmarshal(data, identity)
|
||||
})
|
||||
return identity, err
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) UpdateUser(ctx context.Context, username string, identity *iam_pb.Identity) error {
|
||||
// Ensure user exists
|
||||
if _, err := store.GetUser(ctx, username); err != nil {
|
||||
return err
|
||||
}
|
||||
// If username changes, we need to delete old and create new. But usually UpdateUser keeps username unless renames are allowed.
|
||||
// identity.Name vs username.
|
||||
if username != identity.Name {
|
||||
// Rename case
|
||||
if err := store.DeleteUser(ctx, username); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
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 {
|
||||
resp, err := client.DeleteEntry(ctx, &filer_pb.DeleteEntryRequest{
|
||||
Directory: filer.IamConfigDirectory + "/" + IamIdentitiesDirectory,
|
||||
Name: username + ".json",
|
||||
})
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), filer_pb.ErrNotFound.Error()) {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
if resp != nil && resp.Error != "" {
|
||||
if strings.Contains(resp.Error, filer_pb.ErrNotFound.Error()) {
|
||||
return credential.ErrUserNotFound
|
||||
}
|
||||
return fmt.Errorf("delete user %s: %s", username, resp.Error)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) ListUsers(ctx context.Context) ([]string, error) {
|
||||
var usernames []string
|
||||
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 {
|
||||
if !entry.IsDirectory && len(entry.Name) > 5 && entry.Name[len(entry.Name)-5:] == ".json" {
|
||||
usernames = append(usernames, entry.Name[:len(entry.Name)-5])
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return usernames, err
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) GetUserByAccessKey(ctx context.Context, accessKey string) (*iam_pb.Identity, error) {
|
||||
// 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(ctx, 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
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if foundIdentity != nil {
|
||||
return foundIdentity, nil
|
||||
}
|
||||
|
||||
return nil, credential.ErrAccessKeyNotFound
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) CreateAccessKey(ctx context.Context, username string, cred *iam_pb.Credential) error {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, existing := range identity.Credentials {
|
||||
if existing.AccessKey == cred.AccessKey {
|
||||
return fmt.Errorf("access key %s already exists", cred.AccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
identity.Credentials = append(identity.Credentials, cred)
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) 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.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
// Helpers
|
||||
|
||||
func (store *FilerEtcStore) saveIdentity(ctx context.Context, identity *iam_pb.Identity) error {
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
data, err := json.MarshalIndent(identity, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return filer.SaveInsideFiler(context.Background(), client, filer.IamConfigDirectory+"/"+IamIdentitiesDirectory, identity.Name+".json", data)
|
||||
})
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) readInsideFiler(ctx context.Context, dir string, name string) ([]byte, bool, error) {
|
||||
var content []byte
|
||||
found := false
|
||||
err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
c, err := filer.ReadInsideFiler(ctx, client, dir, name)
|
||||
if err != nil {
|
||||
if err == filer_pb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
content = c
|
||||
found = true
|
||||
return nil
|
||||
})
|
||||
return content, found, err
|
||||
}
|
||||
|
||||
func listEntries(ctx context.Context, client filer_pb.SeaweedFilerClient, dir string) ([]*filer_pb.Entry, error) {
|
||||
var entries []*filer_pb.Entry
|
||||
err := filer_pb.SeaweedList(ctx, client, dir, "", func(entry *filer_pb.Entry, isLast bool) error {
|
||||
entries = append(entries, entry)
|
||||
return nil
|
||||
}, "", false, 100000)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
// AttachUserPolicy attaches a managed policy to a user by policy name
|
||||
func (store *FilerEtcStore) AttachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
// Get user
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify policy exists
|
||||
policy, err := store.GetPolicy(ctx, policyName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if policy == nil {
|
||||
return credential.ErrPolicyNotFound
|
||||
}
|
||||
|
||||
// Check if already attached
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
return credential.ErrPolicyAlreadyAttached
|
||||
}
|
||||
}
|
||||
|
||||
identity.PolicyNames = append(identity.PolicyNames, policyName)
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
// DetachUserPolicy detaches a managed policy from a user
|
||||
func (store *FilerEtcStore) DetachUserPolicy(ctx context.Context, username string, policyName string) error {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
var newPolicies []string
|
||||
for _, p := range identity.PolicyNames {
|
||||
if p == policyName {
|
||||
found = true
|
||||
} else {
|
||||
newPolicies = append(newPolicies, p)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return credential.ErrPolicyNotAttached
|
||||
}
|
||||
|
||||
identity.PolicyNames = newPolicies
|
||||
return store.saveIdentity(ctx, identity)
|
||||
}
|
||||
|
||||
// ListAttachedUserPolicies returns the list of policy names attached to a user
|
||||
func (store *FilerEtcStore) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) {
|
||||
identity, err := store.GetUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return identity.PolicyNames, nil
|
||||
}
|
||||
144
weed/credential/filer_etc/filer_etc_identity_test.go
Normal file
144
weed/credential/filer_etc/filer_etc_identity_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package filer_etc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func putEntry(t *testing.T, server *policyTestFilerServer, dir, name string, content []byte) {
|
||||
t.Helper()
|
||||
_, err := server.CreateEntry(context.Background(), &filer_pb.CreateEntryRequest{
|
||||
Directory: dir,
|
||||
Entry: &filer_pb.Entry{Name: name, Content: content},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// A mid-rewrite identity file (empty content, as seen while a secrets tool
|
||||
// truncates and rewrites the file) must fail the snapshot instead of being
|
||||
// silently dropped. Silently dropping it would make a full reload install an
|
||||
// incomplete identity set and deny unrelated clients mid-reload.
|
||||
func TestLoadConfigurationFailsOnEmptyIdentityFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
identDir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
putEntry(t, server, identDir, "alice.json", []byte(`{"name":"alice","credentials":[{"accessKey":"AK","secretKey":"SK"}]}`))
|
||||
putEntry(t, server, identDir, "bob.json", []byte{}) // mid-rewrite: empty
|
||||
|
||||
_, err := store.LoadConfiguration(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bob.json")
|
||||
}
|
||||
|
||||
// A malformed identity file (partial JSON from a mid-rewrite) must also fail
|
||||
// the snapshot rather than being skipped.
|
||||
func TestLoadConfigurationFailsOnMalformedIdentityFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
identDir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
putEntry(t, server, identDir, "alice.json", []byte(`{"name":"alice"}`))
|
||||
putEntry(t, server, identDir, "bob.json", []byte(`{"name":"bob"`)) // truncated
|
||||
|
||||
_, err := store.LoadConfiguration(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bob.json")
|
||||
}
|
||||
|
||||
// A mid-rewrite policy file must fail the snapshot rather than be dropped.
|
||||
func TestLoadManagedPoliciesFailsOnEmptyPolicyFile(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
polDir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory
|
||||
putEntry(t, server, polDir, "good.json", []byte(`{"Version":"2012-10-17","Statement":[]}`))
|
||||
putEntry(t, server, polDir, "bad.json", []byte{}) // mid-rewrite: empty
|
||||
|
||||
_, err := store.LoadManagedPolicies(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "bad.json")
|
||||
}
|
||||
|
||||
// A non-JSON auxiliary file (README, .DS_Store, a migration backup) in an IAM
|
||||
// directory must not fail the snapshot: only *.json files are IAM objects.
|
||||
func TestLoadConfigurationIgnoresNonJsonAuxiliaryFiles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
identDir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
putEntry(t, server, identDir, "alice.json", []byte(`{"name":"alice","credentials":[{"accessKey":"AK","secretKey":"SK"}]}`))
|
||||
putEntry(t, server, identDir, "README.txt", []byte(`not an identity`))
|
||||
putEntry(t, server, identDir, "alice.json.old", []byte(`{"name":"alice"}`))
|
||||
|
||||
polDir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory
|
||||
putEntry(t, server, polDir, "p.json", []byte(`{"Version":"2012-10-17","Statement":[]}`))
|
||||
putEntry(t, server, polDir, "notes.md", []byte(`# policies`))
|
||||
|
||||
cfg, err := store.LoadConfiguration(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, cfg.Identities, 1)
|
||||
assert.Equal(t, "alice", cfg.Identities[0].Name)
|
||||
}
|
||||
|
||||
// A valid JSON file with an empty name (e.g. `{}`) would unmarshal cleanly but
|
||||
// install a garbage empty-key record that can displace a real one. Reject it.
|
||||
func TestLoadConfigurationFailsOnEmptyIdentityName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
identDir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
putEntry(t, server, identDir, "alice.json", []byte(`{"name":"alice"}`))
|
||||
putEntry(t, server, identDir, "empty.json", []byte(`{}`))
|
||||
|
||||
_, err := store.LoadConfiguration(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "empty.json")
|
||||
}
|
||||
|
||||
func TestLoadConfigurationFailsOnEmptyGroupName(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
groupDir := filer.IamConfigDirectory + "/" + IamGroupsDirectory
|
||||
putEntry(t, server, groupDir, "empty.json", []byte(`{}`))
|
||||
|
||||
_, err := store.LoadConfiguration(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "empty.json")
|
||||
}
|
||||
|
||||
func TestLoadConfigurationFailsOnEmptyServiceAccountId(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
saDir := filer.IamConfigDirectory + "/" + IamServiceAccountsDirectory
|
||||
putEntry(t, server, saDir, "empty.json", []byte(`{}`))
|
||||
|
||||
_, err := store.LoadConfiguration(ctx)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "empty.json")
|
||||
}
|
||||
|
||||
// A valid full snapshot loads without error (regression guard).
|
||||
func TestLoadConfigurationSucceedsOnValidFiles(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
identDir := filer.IamConfigDirectory + "/" + IamIdentitiesDirectory
|
||||
putEntry(t, server, identDir, "alice.json", []byte(`{"name":"alice","credentials":[{"accessKey":"AK","secretKey":"SK"}]}`))
|
||||
putEntry(t, server, identDir, "bob.json", []byte(`{"name":"bob","credentials":[{"accessKey":"BK","secretKey":"SK"}]}`))
|
||||
|
||||
cfg, err := store.LoadConfiguration(ctx)
|
||||
require.NoError(t, err)
|
||||
names := make([]string, 0, len(cfg.Identities))
|
||||
for _, id := range cfg.Identities {
|
||||
names = append(names, id.Name)
|
||||
}
|
||||
assert.ElementsMatch(t, []string{"alice", "bob"}, names)
|
||||
}
|
||||
517
weed/credential/filer_etc/filer_etc_policy.go
Normal file
517
weed/credential/filer_etc/filer_etc_policy.go
Normal file
@@ -0,0 +1,517 @@
|
||||
package filer_etc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"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"
|
||||
"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"`
|
||||
InlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"inlinePolicies"`
|
||||
GroupInlinePolicies map[string]map[string]policy_engine.PolicyDocument `json:"groupInlinePolicies,omitempty"`
|
||||
}
|
||||
|
||||
func validatePolicyName(name string) error {
|
||||
return credential.ValidatePolicyName(name)
|
||||
}
|
||||
|
||||
func newPoliciesCollection() *PoliciesCollection {
|
||||
return &PoliciesCollection{
|
||||
Policies: make(map[string]policy_engine.PolicyDocument),
|
||||
InlinePolicies: make(map[string]map[string]policy_engine.PolicyDocument),
|
||||
GroupInlinePolicies: make(map[string]map[string]policy_engine.PolicyDocument),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) loadLegacyPoliciesCollection(ctx context.Context) (*PoliciesCollection, bool, error) {
|
||||
policiesCollection := newPoliciesCollection()
|
||||
|
||||
content, foundLegacy, err := store.readInsideFiler(ctx, filer.IamConfigDirectory, filer.IamPoliciesFile)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if !foundLegacy || len(content) == 0 {
|
||||
return policiesCollection, foundLegacy, nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(content, policiesCollection); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
if policiesCollection.Policies == nil {
|
||||
policiesCollection.Policies = make(map[string]policy_engine.PolicyDocument)
|
||||
}
|
||||
if policiesCollection.InlinePolicies == nil {
|
||||
policiesCollection.InlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument)
|
||||
}
|
||||
if policiesCollection.GroupInlinePolicies == nil {
|
||||
policiesCollection.GroupInlinePolicies = make(map[string]map[string]policy_engine.PolicyDocument)
|
||||
}
|
||||
|
||||
return policiesCollection, true, nil
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) saveLegacyPoliciesCollection(ctx context.Context, policiesCollection *PoliciesCollection) error {
|
||||
return store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
content, err := json.MarshalIndent(policiesCollection, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return filer.SaveInsideFiler(context.Background(), client, filer.IamConfigDirectory, filer.IamPoliciesFile, content)
|
||||
})
|
||||
}
|
||||
|
||||
func policyDocumentToPbPolicy(name string, policy policy_engine.PolicyDocument) (*iam_pb.Policy, error) {
|
||||
content, err := json.Marshal(policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iam_pb.Policy{Name: name, Content: string(content)}, nil
|
||||
}
|
||||
|
||||
// LoadManagedPolicies loads managed policies for the S3 runtime without
|
||||
// triggering legacy-to-multifile migration. This lets the runtime hydrate
|
||||
// policies while preserving any legacy inline policy data stored alongside
|
||||
// managed policies.
|
||||
func (store *FilerEtcStore) LoadManagedPolicies(ctx context.Context) ([]*iam_pb.Policy, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
policies := make(map[string]policy_engine.PolicyDocument, len(policiesCollection.Policies))
|
||||
for name, policy := range policiesCollection.Policies {
|
||||
policies[name] = policy
|
||||
}
|
||||
|
||||
if err := store.loadPoliciesFromMultiFile(ctx, policies); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
managedPolicies := make([]*iam_pb.Policy, 0, len(policies))
|
||||
for name, policy := range policies {
|
||||
pbPolicy, err := policyDocumentToPbPolicy(name, policy)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
managedPolicies = append(managedPolicies, pbPolicy)
|
||||
}
|
||||
|
||||
return managedPolicies, nil
|
||||
}
|
||||
|
||||
// LoadInlinePolicies loads legacy inline policies keyed by user name. Inline
|
||||
// policies are still stored in the legacy shared policies file.
|
||||
func (store *FilerEtcStore) LoadInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
inlinePolicies := make(map[string]map[string]policy_engine.PolicyDocument, len(policiesCollection.InlinePolicies))
|
||||
for userName, userPolicies := range policiesCollection.InlinePolicies {
|
||||
inlinePolicies[userName] = make(map[string]policy_engine.PolicyDocument, len(userPolicies))
|
||||
for policyName, policy := range userPolicies {
|
||||
inlinePolicies[userName][policyName] = policy
|
||||
}
|
||||
}
|
||||
|
||||
return inlinePolicies, nil
|
||||
}
|
||||
|
||||
// GetPolicies retrieves all IAM policies from the filer
|
||||
func (store *FilerEtcStore) GetPolicies(ctx context.Context) (map[string]policy_engine.PolicyDocument, error) {
|
||||
policies := make(map[string]policy_engine.PolicyDocument)
|
||||
|
||||
// Check if filer client is configured (with mutex protection)
|
||||
store.mu.RLock()
|
||||
configured := store.filerAddressFunc != nil
|
||||
store.mu.RUnlock()
|
||||
|
||||
if !configured {
|
||||
glog.V(1).Infof("Filer client not configured for policy retrieval, returning empty policies")
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
glog.V(2).Infof("Loading IAM policies from %s/%s (using current active filer)",
|
||||
filer.IamConfigDirectory, filer.IamPoliciesFile)
|
||||
|
||||
// 1. Load from legacy single file (low priority)
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDirectory {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if len(entry.Content) > 0 {
|
||||
content = entry.Content
|
||||
} else {
|
||||
c, err := filer.ReadInsideFiler(ctx, client, dir, entry.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read policy file %s: %w", entry.Name, err)
|
||||
}
|
||||
content = c
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("policy file %s is empty", entry.Name)
|
||||
}
|
||||
var policy policy_engine.PolicyDocument
|
||||
if err := json.Unmarshal(content, &policy); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal policy %s: %w", entry.Name, err)
|
||||
}
|
||||
|
||||
policyName := strings.TrimSuffix(entry.Name, ".json")
|
||||
policies[policyName] = policy
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
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(context.Background(), 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.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.savePolicy(ctx, name, document)
|
||||
}
|
||||
|
||||
// PutPolicy creates or updates an IAM policy in the filer
|
||||
func (store *FilerEtcStore) PutPolicy(ctx context.Context, name string, document policy_engine.PolicyDocument) error {
|
||||
return store.UpdatePolicy(ctx, name, document)
|
||||
}
|
||||
|
||||
// DeletePolicy deletes an IAM policy from the filer
|
||||
func (store *FilerEtcStore) DeletePolicy(ctx context.Context, name string) error {
|
||||
if err := validatePolicyName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
store.policyMu.Lock()
|
||||
defer store.policyMu.Unlock()
|
||||
|
||||
policiesCollection, foundLegacy, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteLegacyPolicy := false
|
||||
if foundLegacy {
|
||||
if _, exists := policiesCollection.Policies[name]; exists {
|
||||
delete(policiesCollection.Policies, name)
|
||||
deleteLegacyPolicy = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := store.withFilerClient(func(client filer_pb.SeaweedFilerClient) error {
|
||||
return filer_pb.DoRemove(ctx, client, filer.IamConfigDirectory+"/"+IamPoliciesDirectory, name+".json", false, false, false, false, nil)
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if deleteLegacyPolicy {
|
||||
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
|
||||
}
|
||||
|
||||
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(ctx, 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 p, exists := policies[name]; exists {
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
return nil, nil // Policy not found
|
||||
}
|
||||
|
||||
// PutUserInlinePolicy stores a per-user inline policy document.
|
||||
func (store *FilerEtcStore) PutUserInlinePolicy(ctx context.Context, userName, policyName string, document policy_engine.PolicyDocument) error {
|
||||
store.policyMu.Lock()
|
||||
defer store.policyMu.Unlock()
|
||||
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if policiesCollection.InlinePolicies[userName] == nil {
|
||||
policiesCollection.InlinePolicies[userName] = make(map[string]policy_engine.PolicyDocument)
|
||||
}
|
||||
policiesCollection.InlinePolicies[userName][policyName] = document
|
||||
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
|
||||
}
|
||||
|
||||
// GetUserInlinePolicy retrieves a per-user inline policy document.
|
||||
func (store *FilerEtcStore) GetUserInlinePolicy(ctx context.Context, userName, policyName string) (*policy_engine.PolicyDocument, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userPolicies := policiesCollection.InlinePolicies[userName]; userPolicies != nil {
|
||||
if doc, exists := userPolicies[policyName]; exists {
|
||||
return &doc, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteUserInlinePolicy removes a per-user inline policy document.
|
||||
func (store *FilerEtcStore) DeleteUserInlinePolicy(ctx context.Context, userName, policyName string) error {
|
||||
store.policyMu.Lock()
|
||||
defer store.policyMu.Unlock()
|
||||
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if userPolicies := policiesCollection.InlinePolicies[userName]; userPolicies != nil {
|
||||
delete(userPolicies, policyName)
|
||||
if len(userPolicies) == 0 {
|
||||
delete(policiesCollection.InlinePolicies, userName)
|
||||
}
|
||||
}
|
||||
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
|
||||
}
|
||||
|
||||
// ListUserInlinePolicies returns the names of all inline policies for a user.
|
||||
func (store *FilerEtcStore) ListUserInlinePolicies(ctx context.Context, userName string) ([]string, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
userPolicies := policiesCollection.InlinePolicies[userName]
|
||||
names := make([]string, 0, len(userPolicies))
|
||||
for name := range userPolicies {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// PutGroupInlinePolicy stores a per-group inline policy document.
|
||||
func (store *FilerEtcStore) PutGroupInlinePolicy(ctx context.Context, groupName, policyName string, document policy_engine.PolicyDocument) error {
|
||||
store.policyMu.Lock()
|
||||
defer store.policyMu.Unlock()
|
||||
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if policiesCollection.GroupInlinePolicies[groupName] == nil {
|
||||
policiesCollection.GroupInlinePolicies[groupName] = make(map[string]policy_engine.PolicyDocument)
|
||||
}
|
||||
policiesCollection.GroupInlinePolicies[groupName][policyName] = document
|
||||
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
|
||||
}
|
||||
|
||||
// GetGroupInlinePolicy retrieves a per-group inline policy document.
|
||||
func (store *FilerEtcStore) GetGroupInlinePolicy(ctx context.Context, groupName, policyName string) (*policy_engine.PolicyDocument, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if groupPolicies := policiesCollection.GroupInlinePolicies[groupName]; groupPolicies != nil {
|
||||
if doc, exists := groupPolicies[policyName]; exists {
|
||||
return &doc, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// DeleteGroupInlinePolicy removes a per-group inline policy document.
|
||||
func (store *FilerEtcStore) DeleteGroupInlinePolicy(ctx context.Context, groupName, policyName string) error {
|
||||
store.policyMu.Lock()
|
||||
defer store.policyMu.Unlock()
|
||||
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if groupPolicies := policiesCollection.GroupInlinePolicies[groupName]; groupPolicies != nil {
|
||||
delete(groupPolicies, policyName)
|
||||
if len(groupPolicies) == 0 {
|
||||
delete(policiesCollection.GroupInlinePolicies, groupName)
|
||||
}
|
||||
}
|
||||
return store.saveLegacyPoliciesCollection(ctx, policiesCollection)
|
||||
}
|
||||
|
||||
// ListGroupInlinePolicies returns the names of all inline policies for a group.
|
||||
func (store *FilerEtcStore) ListGroupInlinePolicies(ctx context.Context, groupName string) ([]string, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
groupPolicies := policiesCollection.GroupInlinePolicies[groupName]
|
||||
names := make([]string, 0, len(groupPolicies))
|
||||
for name := range groupPolicies {
|
||||
names = append(names, name)
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
|
||||
// LoadGroupInlinePolicies returns all group inline policies stored in the
|
||||
// legacy file keyed by group name then policy name.
|
||||
func (store *FilerEtcStore) LoadGroupInlinePolicies(ctx context.Context) (map[string]map[string]policy_engine.PolicyDocument, error) {
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]map[string]policy_engine.PolicyDocument, len(policiesCollection.GroupInlinePolicies))
|
||||
for groupName, groupPolicies := range policiesCollection.GroupInlinePolicies {
|
||||
copied := make(map[string]policy_engine.PolicyDocument, len(groupPolicies))
|
||||
for policyName, doc := range groupPolicies {
|
||||
copied[policyName] = doc
|
||||
}
|
||||
result[groupName] = copied
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// ListPolicyNames returns all managed policy names stored in the filer.
|
||||
func (store *FilerEtcStore) ListPolicyNames(ctx context.Context) ([]string, error) {
|
||||
names := make([]string, 0)
|
||||
seenNames := make(map[string]struct{})
|
||||
|
||||
store.mu.RLock()
|
||||
configured := store.filerAddressFunc != nil
|
||||
store.mu.RUnlock()
|
||||
|
||||
if !configured {
|
||||
return names, nil
|
||||
}
|
||||
|
||||
policiesCollection, _, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for name := range policiesCollection.Policies {
|
||||
if _, found := seenNames[name]; found {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
seenNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
err = 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 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDirectory {
|
||||
continue
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
name := entry.Name[:len(entry.Name)-5]
|
||||
if _, found := seenNames[name]; found {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
seenNames[name] = struct{}{}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return names, nil
|
||||
}
|
||||
386
weed/credential/filer_etc/filer_etc_policy_test.go
Normal file
386
weed/credential/filer_etc/filer_etc_policy_test.go
Normal file
@@ -0,0 +1,386 @@
|
||||
package filer_etc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/filer"
|
||||
"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/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
type policyTestFilerServer struct {
|
||||
filer_pb.UnimplementedSeaweedFilerServer
|
||||
mu sync.RWMutex
|
||||
entries map[string]*filer_pb.Entry
|
||||
contentlessListEntry map[string]struct{}
|
||||
beforeLookup func(context.Context, string, string) error
|
||||
afterListEntry func(string, string)
|
||||
beforeDelete func(string, string) error
|
||||
beforeUpdate func(string, string) error
|
||||
}
|
||||
|
||||
func newPolicyTestFilerServer() *policyTestFilerServer {
|
||||
return &policyTestFilerServer{
|
||||
entries: make(map[string]*filer_pb.Entry),
|
||||
contentlessListEntry: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *policyTestFilerServer) LookupDirectoryEntry(ctx context.Context, req *filer_pb.LookupDirectoryEntryRequest) (*filer_pb.LookupDirectoryEntryResponse, error) {
|
||||
s.mu.RLock()
|
||||
beforeLookup := s.beforeLookup
|
||||
s.mu.RUnlock()
|
||||
if beforeLookup != nil {
|
||||
if err := beforeLookup(ctx, req.Directory, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
entry, found := s.entries[filerEntryKey(req.Directory, req.Name)]
|
||||
if !found {
|
||||
return nil, status.Error(codes.NotFound, filer_pb.ErrNotFound.Error())
|
||||
}
|
||||
|
||||
return &filer_pb.LookupDirectoryEntryResponse{Entry: cloneEntry(entry)}, nil
|
||||
}
|
||||
|
||||
func (s *policyTestFilerServer) ListEntries(req *filer_pb.ListEntriesRequest, stream grpc.ServerStreamingServer[filer_pb.ListEntriesResponse]) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
names := make([]string, 0)
|
||||
for key := range s.entries {
|
||||
dir, name := splitFilerEntryKey(key)
|
||||
if dir != req.Directory {
|
||||
continue
|
||||
}
|
||||
names = append(names, name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
|
||||
for _, name := range names {
|
||||
entry := cloneEntry(s.entries[filerEntryKey(req.Directory, name)])
|
||||
if _, found := s.contentlessListEntry[filerEntryKey(req.Directory, name)]; found {
|
||||
entry.Content = nil
|
||||
}
|
||||
if err := stream.Send(&filer_pb.ListEntriesResponse{Entry: entry}); err != nil {
|
||||
return err
|
||||
}
|
||||
if s.afterListEntry != nil {
|
||||
s.afterListEntry(req.Directory, name)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *policyTestFilerServer) CreateEntry(_ context.Context, req *filer_pb.CreateEntryRequest) (*filer_pb.CreateEntryResponse, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.entries[filerEntryKey(req.Directory, req.Entry.Name)] = cloneEntry(req.Entry)
|
||||
return &filer_pb.CreateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *policyTestFilerServer) UpdateEntry(_ context.Context, req *filer_pb.UpdateEntryRequest) (*filer_pb.UpdateEntryResponse, error) {
|
||||
s.mu.RLock()
|
||||
beforeUpdate := s.beforeUpdate
|
||||
s.mu.RUnlock()
|
||||
if beforeUpdate != nil {
|
||||
if err := beforeUpdate(req.Directory, req.Entry.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.entries[filerEntryKey(req.Directory, req.Entry.Name)] = cloneEntry(req.Entry)
|
||||
return &filer_pb.UpdateEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func (s *policyTestFilerServer) DeleteEntry(_ context.Context, req *filer_pb.DeleteEntryRequest) (*filer_pb.DeleteEntryResponse, error) {
|
||||
s.mu.RLock()
|
||||
beforeDelete := s.beforeDelete
|
||||
s.mu.RUnlock()
|
||||
if beforeDelete != nil {
|
||||
if err := beforeDelete(req.Directory, req.Name); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
key := filerEntryKey(req.Directory, req.Name)
|
||||
if _, found := s.entries[key]; !found {
|
||||
return nil, status.Error(codes.NotFound, filer_pb.ErrNotFound.Error())
|
||||
}
|
||||
|
||||
delete(s.entries, key)
|
||||
return &filer_pb.DeleteEntryResponse{}, nil
|
||||
}
|
||||
|
||||
func newPolicyTestStore(t *testing.T) *FilerEtcStore {
|
||||
store, _ := newPolicyTestStoreWithServer(t)
|
||||
return store
|
||||
}
|
||||
|
||||
func newPolicyTestStoreWithServer(t *testing.T) (*FilerEtcStore, *policyTestFilerServer) {
|
||||
t.Helper()
|
||||
|
||||
lis, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
require.NoError(t, err)
|
||||
|
||||
server := newPolicyTestFilerServer()
|
||||
grpcServer := pb.NewGrpcServer()
|
||||
filer_pb.RegisterSeaweedFilerServer(grpcServer, server)
|
||||
go func() {
|
||||
_ = grpcServer.Serve(lis)
|
||||
}()
|
||||
|
||||
t.Cleanup(func() {
|
||||
grpcServer.Stop()
|
||||
_ = lis.Close()
|
||||
})
|
||||
|
||||
store := &FilerEtcStore{}
|
||||
host, portString, err := net.SplitHostPort(lis.Addr().String())
|
||||
require.NoError(t, err)
|
||||
grpcPort, err := strconv.Atoi(portString)
|
||||
require.NoError(t, err)
|
||||
store.SetFilerAddressFunc(func() pb.ServerAddress {
|
||||
return pb.NewServerAddress(host, 1, grpcPort)
|
||||
}, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
||||
|
||||
return store, server
|
||||
}
|
||||
|
||||
func TestFilerEtcStoreListPolicyNamesIncludesLegacyPolicies(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newPolicyTestStore(t)
|
||||
|
||||
legacyPolicies := newPoliciesCollection()
|
||||
legacyPolicies.Policies["legacy-only"] = testPolicyDocument("s3:GetObject", "arn:aws:s3:::legacy-only/*")
|
||||
legacyPolicies.Policies["shared"] = testPolicyDocument("s3:GetObject", "arn:aws:s3:::shared/*")
|
||||
require.NoError(t, store.saveLegacyPoliciesCollection(ctx, legacyPolicies))
|
||||
|
||||
require.NoError(t, store.savePolicy(ctx, "multi-file-only", testPolicyDocument("s3:PutObject", "arn:aws:s3:::multi-file-only/*")))
|
||||
require.NoError(t, store.savePolicy(ctx, "shared", testPolicyDocument("s3:DeleteObject", "arn:aws:s3:::shared/*")))
|
||||
|
||||
names, err := store.ListPolicyNames(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.ElementsMatch(t, []string{"legacy-only", "multi-file-only", "shared"}, names)
|
||||
}
|
||||
|
||||
// A non-JSON auxiliary file in the policies directory must not be listed as a
|
||||
// policy name (it cannot be retrieved by GetPolicy either).
|
||||
func TestFilerEtcStoreListPolicyNamesSkipsNonJsonAuxiliary(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
require.NoError(t, store.savePolicy(ctx, "real", testPolicyDocument("s3:GetObject", "arn:aws:s3:::real/*")))
|
||||
|
||||
polDir := filer.IamConfigDirectory + "/" + IamPoliciesDirectory
|
||||
putEntry(t, server, polDir, "notes.md", []byte(`# not a policy`))
|
||||
|
||||
names, err := store.ListPolicyNames(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.ElementsMatch(t, []string{"real"}, names)
|
||||
}
|
||||
|
||||
func TestFilerEtcStoreDeletePolicyRemovesLegacyManagedCopy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store := newPolicyTestStore(t)
|
||||
|
||||
inlinePolicy := testPolicyDocument("s3:PutObject", "arn:aws:s3:::inline-user/*")
|
||||
legacyPolicies := newPoliciesCollection()
|
||||
legacyPolicies.Policies["legacy-only"] = testPolicyDocument("s3:GetObject", "arn:aws:s3:::legacy-only/*")
|
||||
legacyPolicies.InlinePolicies["inline-user"] = map[string]policy_engine.PolicyDocument{
|
||||
"PutOnly": inlinePolicy,
|
||||
}
|
||||
require.NoError(t, store.saveLegacyPoliciesCollection(ctx, legacyPolicies))
|
||||
|
||||
managedPolicies, err := store.LoadManagedPolicies(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, []string{"legacy-only"}, managedPolicyNames(managedPolicies))
|
||||
|
||||
require.NoError(t, store.DeletePolicy(ctx, "legacy-only"))
|
||||
|
||||
managedPolicies, err = store.LoadManagedPolicies(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, managedPolicies)
|
||||
|
||||
inlinePolicies, err := store.LoadInlinePolicies(ctx)
|
||||
require.NoError(t, err)
|
||||
assertInlinePolicyPreserved(t, inlinePolicies, "inline-user", "PutOnly")
|
||||
|
||||
loadedLegacyPolicies, foundLegacy, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, foundLegacy)
|
||||
assert.Empty(t, loadedLegacyPolicies.Policies)
|
||||
assertInlinePolicyPreserved(t, loadedLegacyPolicies.InlinePolicies, "inline-user", "PutOnly")
|
||||
}
|
||||
|
||||
func TestFilerEtcStoreDeletePolicySerializesLegacyUpdates(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
legacyPolicies := newPoliciesCollection()
|
||||
legacyPolicies.Policies["first"] = testPolicyDocument("s3:GetObject", "arn:aws:s3:::first/*")
|
||||
legacyPolicies.Policies["second"] = testPolicyDocument("s3:GetObject", "arn:aws:s3:::second/*")
|
||||
require.NoError(t, store.saveLegacyPoliciesCollection(ctx, legacyPolicies))
|
||||
require.NoError(t, store.savePolicy(ctx, "first", testPolicyDocument("s3:GetObject", "arn:aws:s3:::first/*")))
|
||||
require.NoError(t, store.savePolicy(ctx, "second", testPolicyDocument("s3:GetObject", "arn:aws:s3:::second/*")))
|
||||
|
||||
firstSaveStarted := make(chan struct{})
|
||||
releaseFirstSave := make(chan struct{})
|
||||
secondReachedDelete := make(chan struct{}, 1)
|
||||
var blockOnce sync.Once
|
||||
|
||||
server.mu.Lock()
|
||||
server.beforeUpdate = func(dir string, name string) error {
|
||||
if dir == filer.IamConfigDirectory && name == filer.IamPoliciesFile {
|
||||
blockOnce.Do(func() {
|
||||
close(firstSaveStarted)
|
||||
<-releaseFirstSave
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
server.beforeDelete = func(dir string, name string) error {
|
||||
if dir == filer.IamConfigDirectory+"/"+IamPoliciesDirectory && name == "second.json" {
|
||||
select {
|
||||
case secondReachedDelete <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
server.mu.Unlock()
|
||||
|
||||
firstDeleteErr := make(chan error, 1)
|
||||
go func() {
|
||||
firstDeleteErr <- store.DeletePolicy(ctx, "first")
|
||||
}()
|
||||
|
||||
<-firstSaveStarted
|
||||
|
||||
secondDeleteErr := make(chan error, 1)
|
||||
go func() {
|
||||
secondDeleteErr <- store.DeletePolicy(ctx, "second")
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-secondReachedDelete:
|
||||
t.Fatal("second delete reached filer mutation while first delete was still blocked")
|
||||
case <-time.After(300 * time.Millisecond):
|
||||
}
|
||||
|
||||
close(releaseFirstSave)
|
||||
|
||||
require.NoError(t, <-firstDeleteErr)
|
||||
require.NoError(t, <-secondDeleteErr)
|
||||
|
||||
loadedLegacyPolicies, foundLegacy, err := store.loadLegacyPoliciesCollection(ctx)
|
||||
require.NoError(t, err)
|
||||
require.True(t, foundLegacy)
|
||||
assert.Empty(t, loadedLegacyPolicies.Policies)
|
||||
}
|
||||
|
||||
func TestFilerEtcStoreLoadManagedPoliciesRespectsReadContext(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
store, server := newPolicyTestStoreWithServer(t)
|
||||
|
||||
require.NoError(t, store.savePolicy(context.Background(), "cancel-me", testPolicyDocument("s3:GetObject", "arn:aws:s3:::cancel-me/*")))
|
||||
|
||||
server.mu.Lock()
|
||||
server.contentlessListEntry[filerEntryKey(filer.IamConfigDirectory+"/"+IamPoliciesDirectory, "cancel-me.json")] = struct{}{}
|
||||
server.beforeLookup = func(ctx context.Context, dir string, name string) error {
|
||||
if dir == filer.IamConfigDirectory+"/"+IamPoliciesDirectory && name == "cancel-me.json" {
|
||||
cancel()
|
||||
return status.Error(codes.Canceled, context.Canceled.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
server.mu.Unlock()
|
||||
|
||||
// the canceled read must fail the snapshot, not silently omit the policy
|
||||
managedPolicies, err := store.LoadManagedPolicies(ctx)
|
||||
require.ErrorContains(t, err, "cancel-me.json")
|
||||
assert.Empty(t, managedPolicies)
|
||||
}
|
||||
|
||||
func testPolicyDocument(action string, resource string) policy_engine.PolicyDocument {
|
||||
return policy_engine.PolicyDocument{
|
||||
Version: policy_engine.PolicyVersion2012_10_17,
|
||||
Statement: []policy_engine.PolicyStatement{
|
||||
{
|
||||
Effect: policy_engine.PolicyEffectAllow,
|
||||
Action: policy_engine.NewStringOrStringSlice(action),
|
||||
Resource: policy_engine.NewStringOrStringSlicePtr(resource),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func managedPolicyNames(policies []*iam_pb.Policy) []string {
|
||||
names := make([]string, 0, len(policies))
|
||||
for _, policy := range policies {
|
||||
names = append(names, policy.Name)
|
||||
}
|
||||
sort.Strings(names)
|
||||
return names
|
||||
}
|
||||
|
||||
func assertInlinePolicyPreserved(t *testing.T, inlinePolicies map[string]map[string]policy_engine.PolicyDocument, userName string, policyName string) {
|
||||
t.Helper()
|
||||
|
||||
userPolicies, found := inlinePolicies[userName]
|
||||
require.True(t, found)
|
||||
|
||||
policy, found := userPolicies[policyName]
|
||||
require.True(t, found)
|
||||
assert.Equal(t, policy_engine.PolicyVersion2012_10_17, policy.Version)
|
||||
require.Len(t, policy.Statement, 1)
|
||||
assert.Equal(t, policy_engine.PolicyEffectAllow, policy.Statement[0].Effect)
|
||||
}
|
||||
|
||||
func cloneEntry(entry *filer_pb.Entry) *filer_pb.Entry {
|
||||
if entry == nil {
|
||||
return nil
|
||||
}
|
||||
return proto.Clone(entry).(*filer_pb.Entry)
|
||||
}
|
||||
|
||||
func filerEntryKey(dir string, name string) string {
|
||||
return dir + "\x00" + name
|
||||
}
|
||||
|
||||
func splitFilerEntryKey(key string) (dir string, name string) {
|
||||
for idx := 0; idx < len(key); idx++ {
|
||||
if key[idx] == '\x00' {
|
||||
return key[:idx], key[idx+1:]
|
||||
}
|
||||
}
|
||||
return key, ""
|
||||
}
|
||||
211
weed/credential/filer_etc/filer_etc_service_account.go
Normal file
211
weed/credential/filer_etc/filer_etc_service_account.go
Normal file
@@ -0,0 +1,211 @@
|
||||
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
|
||||
}
|
||||
if !strings.HasSuffix(entry.Name, ".json") {
|
||||
continue
|
||||
}
|
||||
|
||||
var content []byte
|
||||
if len(entry.Content) > 0 {
|
||||
content = entry.Content
|
||||
} else {
|
||||
c, err := filer.ReadInsideFiler(ctx, client, dir, entry.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read service account file %s: %w", entry.Name, err)
|
||||
}
|
||||
content = c
|
||||
}
|
||||
|
||||
if len(content) == 0 {
|
||||
return fmt.Errorf("service account file %s is empty", entry.Name)
|
||||
}
|
||||
sa := &iam_pb.ServiceAccount{}
|
||||
if err := json.Unmarshal(content, sa); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal service account %s: %w", entry.Name, err)
|
||||
}
|
||||
if err := validateServiceAccountId(sa.Id); err != nil {
|
||||
return fmt.Errorf("service account file %s: %w", entry.Name, err)
|
||||
}
|
||||
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.MarshalIndent(sa, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return filer.SaveInsideFiler(context.Background(), 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(ctx, 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(ctx, 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
|
||||
}
|
||||
79
weed/credential/filer_etc/filer_etc_store.go
Normal file
79
weed/credential/filer_etc/filer_etc_store.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package filer_etc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/credential"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
func init() {
|
||||
credential.Stores = append(credential.Stores, &FilerEtcStore{})
|
||||
}
|
||||
|
||||
// FilerEtcStore implements CredentialStore using SeaweedFS filer for storage
|
||||
type FilerEtcStore struct {
|
||||
filerAddressFunc func() pb.ServerAddress // Function to get current active filer
|
||||
grpcDialOption grpc.DialOption
|
||||
mu sync.RWMutex // Protects filerAddressFunc and grpcDialOption
|
||||
policyMu sync.Mutex // Serializes legacy managed-policy mutations
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) GetName() credential.CredentialStoreTypeName {
|
||||
return credential.StoreTypeFilerEtc
|
||||
}
|
||||
|
||||
func (store *FilerEtcStore) 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()
|
||||
}
|
||||
// TODO: Initialize grpcDialOption based on configuration
|
||||
}
|
||||
// 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 *FilerEtcStore) 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 *FilerEtcStore) withFilerClient(fn func(client filer_pb.SeaweedFilerClient) error) error {
|
||||
store.mu.RLock()
|
||||
if store.filerAddressFunc == nil {
|
||||
store.mu.RUnlock()
|
||||
return fmt.Errorf("filer_etc: 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_etc: 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 *FilerEtcStore) Shutdown() {
|
||||
// No cleanup needed for file store
|
||||
}
|
||||
Reference in New Issue
Block a user