package objstore import ( "context" "fmt" "sync" ) // MemUser is the in-memory fake's view of an IAM principal: its ARN and its // live access keys (id -> secret). Exported so tests in this package can // assert on p.Users["u1"].Keys directly. type MemUser struct { ARN string Keys map[string]string // Policy is the inline policy document EnsureUser last wrote. The real // provisioner's PutUserPolicy overwrites wholesale on every call, so the // fake records it on every call too — including for a user that already // exists, which is the only case enforcement is ever turned on for. Policy string } // MemProvisioner is an in-memory Provisioner for walk tests: no AWS, no // network, deterministic key ids. It stays in this package (internal) — // nothing outside objstore needs to reach into its state. type MemProvisioner struct { mu sync.Mutex region string nextKey int // Buckets maps bucket name -> object key -> body. Buckets map[string]map[string][]byte // Users maps user name -> MemUser. Users map[string]MemUser // FailNext, keyed by method name, is consumed one-shot: the next call // to that method fails with the given error, then the map entry is // gone and subsequent calls succeed normally. FailNext map[string]error // MintSecret, when non-empty, is the secret every MintKey hands back // instead of a generated one — the seam for testing what a walk does // with a secret it cannot deliver (a real backend picks the bytes, so // a walk must not assume they are renderable). MintSecret string } // NewMemProvisioner returns an empty fake that answers region for every // EnsureBucket call. func NewMemProvisioner(region string) *MemProvisioner { return &MemProvisioner{ region: region, Buckets: map[string]map[string][]byte{}, Users: map[string]MemUser{}, FailNext: map[string]error{}, } } // takeFailure consumes and returns FailNext[method], if any, under the // caller's lock. Consumption happens before any state change so a failed // call leaves state untouched. func (p *MemProvisioner) takeFailure(method string) error { if err, ok := p.FailNext[method]; ok { delete(p.FailNext, method) return err } return nil } func (p *MemProvisioner) EnsureBucket(ctx context.Context, name string) (string, error) { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("EnsureBucket"); err != nil { return "", err } if p.Buckets[name] == nil { p.Buckets[name] = map[string][]byte{} } return p.region, nil } func (p *MemProvisioner) EnsureUser(ctx context.Context, name, path, bucketARN string, deny *DenyTag) (string, error) { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("EnsureUser"); err != nil { return "", err } policy := storePolicyJSON(bucketARN, deny) if u, ok := p.Users[name]; ok { // The policy is rewritten for an existing principal too, mirroring // PutUserPolicy running unconditionally on both of awsProvisioner's // branches. A fake that recorded it only on the create branch would // make enforcement look broken exactly where it matters: every store // this is ever turned on for already has its user. u.Policy = policy p.Users[name] = u // MemUser is a value in the map — mutate, store back return u.ARN, nil } arn := "arn:mem:iam::user" + path + name p.Users[name] = MemUser{ARN: arn, Keys: map[string]string{}, Policy: policy} return arn, nil } // PolicyFor is the inline policy the fake last wrote for a user, so a walk // test can assert that enforcement reached the backend rather than only // reaching the record. func (p *MemProvisioner) PolicyFor(user string) string { p.mu.Lock() defer p.mu.Unlock() return p.Users[user].Policy } func (p *MemProvisioner) MintKey(ctx context.Context, user string) (string, string, error) { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("MintKey"); err != nil { return "", "", err } u, ok := p.Users[user] if !ok { return "", "", fmt.Errorf("objstore: mem: no such user %q", user) } p.nextKey++ id := fmt.Sprintf("MEMKEY%d", p.nextKey) secret := fmt.Sprintf("MEMSEC%d", p.nextKey) if p.MintSecret != "" { secret = p.MintSecret } u.Keys[id] = secret return id, secret, nil } func (p *MemProvisioner) DeleteKey(ctx context.Context, user, id string) error { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("DeleteKey"); err != nil { return err } if u, ok := p.Users[user]; ok { delete(u.Keys, id) } return nil } func (p *MemProvisioner) ReapKeys(ctx context.Context, user string, keep []string) error { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("ReapKeys"); err != nil { return err } u, ok := p.Users[user] if !ok { return nil } keepSet := make(map[string]bool, len(keep)) for _, id := range keep { keepSet[id] = true } for id := range u.Keys { if !keepSet[id] { delete(u.Keys, id) } } return nil } func (p *MemProvisioner) EmptyBucket(ctx context.Context, name string) error { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("EmptyBucket"); err != nil { return err } if b, ok := p.Buckets[name]; ok { for k := range b { delete(b, k) } } return nil } func (p *MemProvisioner) DeleteBucket(ctx context.Context, name string) error { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("DeleteBucket"); err != nil { return err } delete(p.Buckets, name) return nil } func (p *MemProvisioner) DeleteUser(ctx context.Context, user string) error { p.mu.Lock() defer p.mu.Unlock() if err := p.takeFailure("DeleteUser"); err != nil { return err } delete(p.Users, user) return nil }