Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions DESIGN.ru.md
Original file line number Diff line number Diff line change
Expand Up @@ -678,9 +678,29 @@ type Driver interface {
каждый доступ — отдельный statement с **бесколлизионным** per-access `Sid`
(хеш uid, а не срезка небуквенных символов), а запись политики **fail-closed** —
при непарсимой политике контроллер не перезаписывает её, чтобы не стереть
statements других арендаторов/выставленные извне. Запись SeaweedFS
`identity.json` сериализуется per-cluster (read-modify-write под блокировкой),
чтобы параллельные reconcile не теряли чужие обновления. Новая пара минтится
statements других арендаторов/выставленные извне. IAM SeaweedFS
(4.39) хранится **по файлу на identity** в `/etc/iam/identities/<имя>.json`;
единый `identity.json` — legacy, который gateway при первой загрузке сам
мигрирует в эту раскладку (и переименовывает в `.old`). Контроллер управляет
per-identity файлами напрямую, тем же способом, что и собственный
credential manager gateway'я: по gRPC filer'а, с полезной нагрузкой в
inline-поле `content` entry (единственное представление, которое читает
gateway — его загрузчик берёт только `Entry.Content` и не читает чанки;
оба HTTP-пути записи на 4.39 дают файлы, которые gateway либо драйвер не
может прочитать обратно, а работа через legacy-файл гонялась бы с миграцией
и ломала отзыв — удалённая из него учётка уже мигрирована и продолжает
работать). Раскладка «файл на identity» структурно снимает риск затирания
чужих ключей: каждый reconcile трогает только свой файл, общего
read-modify-write больше нет. Декодирование строгое
(`DisallowUnknownFields`), запись верифицируется перечиткой (принятая
filer'ом запись — не доказательство, что она читается), отзыв идемпотентен,
но не может отчитаться об успехе, не подтвердив отсутствие файла.
JSON-ключи файлов — snake_case (`access_key`/`secret_key`), как в
сгенерированных тегах `iam_pb.Identity`, которыми gateway их декодирует.
Проверено против стокового 4.39 (`weed server -filer -s3`) живым тестом
`live_filer_test.go` (гейтится `LIVE_FILER_GRPC`), включая enforcement на
gateway: анонимный запрос 403, выданный ключ 200, отозванный —
`InvalidAccessKeyId`. Новая пара минтится
только по реальному триггеру (смена аннотации ротации, ключа ещё нет, или
`Secret` действительно отсутствует — проверяется по авторитетному
`APIReader`), поэтому кэш-гонка не ротирует спонтанно живой ключ. Выдача
Expand Down
67 changes: 29 additions & 38 deletions images/controller/internal/backend/seaweedfs/access.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ import (
)

// EnsureAccess provisions an IAM identity scoped to the bucket for the given
// access. SeaweedFS stores credentials in the filer IAM config, so the secret
// key is recoverable and always returned; mintFresh replaces it with a new
// random pair (rotation), which revokes the previous key.
// access — one per-identity file on the filer, touching no other identity's
// credentials. SeaweedFS stores the secret key retrievably, so it is always
// returned; mintFresh replaces the pair with a new random one (rotation), which
// revokes the previous key.
func (d *Driver) EnsureAccess(ctx context.Context, cluster *v1alpha1.ObjectStore, bucket *v1alpha1.Bucket, access *v1alpha1.BucketAccess, mintFresh bool) (backend.AccessState, error) {
adminAK, _, err := d.adminCreds(ctx, cluster)
if err != nil {
Expand All @@ -40,31 +41,31 @@ func (d *Driver) EnsureAccess(ctx context.Context, cluster *v1alpha1.ObjectStore

name := backend.BucketDisplayName(bucket)
identityName := backend.AccessResourceName(access)
files := d.identityFilesFor(cluster)

// Serialize the whole read-decide-write cycle so a concurrent reconcile
// cannot clobber the identity we issue here (or vice versa).
var accessKey, secretKey string
if err := d.mutateIdentities(ctx, cluster, func(cfg *identityConfig) (bool, error) {
accessKey, secretKey = "", ""
if !mintFresh {
if cur, ok := findCredentials(cfg, identityName); ok {
accessKey, secretKey = cur.AccessKey, cur.SecretKey
}
if !mintFresh {
existing, found, err := files.readIdentity(ctx, identityName)
if err != nil {
return backend.AccessState{}, err
}
if accessKey == "" || secretKey == "" {
var e error
if accessKey, e = randomHex(16); e != nil {
return false, e
}
if secretKey, e = randomHex(32); e != nil {
return false, e
}
if found && len(existing.Credentials) > 0 {
accessKey, secretKey = existing.Credentials[0].AccessKey, existing.Credentials[0].SecretKey
}
return cfg.upsert(s3Identity{
Name: identityName,
Credentials: []s3Credential{{AccessKey: accessKey, SecretKey: secretKey}},
Actions: bucketActions(name, access.Spec.Permission),
}), nil
}
if accessKey == "" || secretKey == "" {
if accessKey, err = randomHex(16); err != nil {
return backend.AccessState{}, err
}
if secretKey, err = randomHex(32); err != nil {
return backend.AccessState{}, err
}
}

if err := files.writeIdentity(ctx, &s3Identity{
Name: identityName,
Credentials: []s3Credential{{AccessKey: accessKey, SecretKey: secretKey}},
Actions: bucketActions(name, access.Spec.Permission),
}); err != nil {
return backend.AccessState{}, err
}
Expand All @@ -78,7 +79,9 @@ func (d *Driver) EnsureAccess(ctx context.Context, cluster *v1alpha1.ObjectStore
}

// DeleteAccess removes the IAM identity issued for the access. Idempotent and
// tolerant of an already-deleted cluster.
// tolerant of an already-deleted cluster; an identity file that cannot be
// confirmed gone is an error, so the access keeps its finalizer and retries
// rather than releasing while a live credential may remain.
func (d *Driver) DeleteAccess(ctx context.Context, cluster *v1alpha1.ObjectStore, _ *v1alpha1.Bucket, access *v1alpha1.BucketAccess) error {
adminAK, _, err := d.adminCreds(ctx, cluster)
if err != nil {
Expand All @@ -91,17 +94,5 @@ func (d *Driver) DeleteAccess(ctx context.Context, cluster *v1alpha1.ObjectStore
return nil
}

return d.mutateIdentities(ctx, cluster, func(cfg *identityConfig) (bool, error) {
return cfg.remove(backend.AccessResourceName(access)), nil
})
}

// findCredentials returns the first credential pair of the named identity.
func findCredentials(cfg *identityConfig, name string) (s3Credential, bool) {
for i := range cfg.Identities {
if cfg.Identities[i].Name == name && len(cfg.Identities[i].Credentials) > 0 {
return cfg.Identities[i].Credentials[0], true
}
}
return s3Credential{}, false
return d.identityFilesFor(cluster).deleteIdentity(ctx, backend.AccessResourceName(access))
}
Loading
Loading