| 1 | package objstore |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "fmt" |
| 6 | "sync" |
| 7 | ) |
| 8 | |
| 9 | // MemUser is the in-memory fake's view of an IAM principal: its ARN and its |
| 10 | // live access keys (id -> secret). Exported so tests in this package can |
| 11 | // assert on p.Users["u1"].Keys directly. |
| 12 | type MemUser struct { |
| 13 | ARN string |
| 14 | Keys map[string]string |
| 15 | // Policy is the inline policy document EnsureUser last wrote. The real |
| 16 | // provisioner's PutUserPolicy overwrites wholesale on every call, so the |
| 17 | // fake records it on every call too — including for a user that already |
| 18 | // exists, which is the only case enforcement is ever turned on for. |
| 19 | Policy string |
| 20 | } |
| 21 | |
| 22 | // MemProvisioner is an in-memory Provisioner for walk tests: no AWS, no |
| 23 | // network, deterministic key ids. It stays in this package (internal) — |
| 24 | // nothing outside objstore needs to reach into its state. |
| 25 | type MemProvisioner struct { |
| 26 | mu sync.Mutex |
| 27 | region string |
| 28 | nextKey int |
| 29 | |
| 30 | // Buckets maps bucket name -> object key -> body. |
| 31 | Buckets map[string]map[string][]byte |
| 32 | // Users maps user name -> MemUser. |
| 33 | Users map[string]MemUser |
| 34 | // FailNext, keyed by method name, is consumed one-shot: the next call |
| 35 | // to that method fails with the given error, then the map entry is |
| 36 | // gone and subsequent calls succeed normally. |
| 37 | FailNext map[string]error |
| 38 | // MintSecret, when non-empty, is the secret every MintKey hands back |
| 39 | // instead of a generated one — the seam for testing what a walk does |
| 40 | // with a secret it cannot deliver (a real backend picks the bytes, so |
| 41 | // a walk must not assume they are renderable). |
| 42 | MintSecret string |
| 43 | } |
| 44 | |
| 45 | // NewMemProvisioner returns an empty fake that answers region for every |
| 46 | // EnsureBucket call. |
| 47 | func NewMemProvisioner(region string) *MemProvisioner { |
| 48 | return &MemProvisioner{ |
| 49 | region: region, |
| 50 | Buckets: map[string]map[string][]byte{}, |
| 51 | Users: map[string]MemUser{}, |
| 52 | FailNext: map[string]error{}, |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | // takeFailure consumes and returns FailNext[method], if any, under the |
| 57 | // caller's lock. Consumption happens before any state change so a failed |
| 58 | // call leaves state untouched. |
| 59 | func (p *MemProvisioner) takeFailure(method string) error { |
| 60 | if err, ok := p.FailNext[method]; ok { |
| 61 | delete(p.FailNext, method) |
| 62 | return err |
| 63 | } |
| 64 | return nil |
| 65 | } |
| 66 | |
| 67 | func (p *MemProvisioner) EnsureBucket(ctx context.Context, name string) (string, error) { |
| 68 | p.mu.Lock() |
| 69 | defer p.mu.Unlock() |
| 70 | if err := p.takeFailure("EnsureBucket"); err != nil { |
| 71 | return "", err |
| 72 | } |
| 73 | if p.Buckets[name] == nil { |
| 74 | p.Buckets[name] = map[string][]byte{} |
| 75 | } |
| 76 | return p.region, nil |
| 77 | } |
| 78 | |
| 79 | func (p *MemProvisioner) EnsureUser(ctx context.Context, name, path, bucketARN string, deny *DenyTag) (string, error) { |
| 80 | p.mu.Lock() |
| 81 | defer p.mu.Unlock() |
| 82 | if err := p.takeFailure("EnsureUser"); err != nil { |
| 83 | return "", err |
| 84 | } |
| 85 | policy := storePolicyJSON(bucketARN, deny) |
| 86 | if u, ok := p.Users[name]; ok { |
| 87 | // The policy is rewritten for an existing principal too, mirroring |
| 88 | // PutUserPolicy running unconditionally on both of awsProvisioner's |
| 89 | // branches. A fake that recorded it only on the create branch would |
| 90 | // make enforcement look broken exactly where it matters: every store |
| 91 | // this is ever turned on for already has its user. |
| 92 | u.Policy = policy |
| 93 | p.Users[name] = u // MemUser is a value in the map — mutate, store back |
| 94 | return u.ARN, nil |
| 95 | } |
| 96 | arn := "arn:mem:iam::user" + path + name |
| 97 | p.Users[name] = MemUser{ARN: arn, Keys: map[string]string{}, Policy: policy} |
| 98 | return arn, nil |
| 99 | } |
| 100 | |
| 101 | // PolicyFor is the inline policy the fake last wrote for a user, so a walk |
| 102 | // test can assert that enforcement reached the backend rather than only |
| 103 | // reaching the record. |
| 104 | func (p *MemProvisioner) PolicyFor(user string) string { |
| 105 | p.mu.Lock() |
| 106 | defer p.mu.Unlock() |
| 107 | return p.Users[user].Policy |
| 108 | } |
| 109 | |
| 110 | func (p *MemProvisioner) MintKey(ctx context.Context, user string) (string, string, error) { |
| 111 | p.mu.Lock() |
| 112 | defer p.mu.Unlock() |
| 113 | if err := p.takeFailure("MintKey"); err != nil { |
| 114 | return "", "", err |
| 115 | } |
| 116 | u, ok := p.Users[user] |
| 117 | if !ok { |
| 118 | return "", "", fmt.Errorf("objstore: mem: no such user %q", user) |
| 119 | } |
| 120 | p.nextKey++ |
| 121 | id := fmt.Sprintf("MEMKEY%d", p.nextKey) |
| 122 | secret := fmt.Sprintf("MEMSEC%d", p.nextKey) |
| 123 | if p.MintSecret != "" { |
| 124 | secret = p.MintSecret |
| 125 | } |
| 126 | u.Keys[id] = secret |
| 127 | return id, secret, nil |
| 128 | } |
| 129 | |
| 130 | func (p *MemProvisioner) DeleteKey(ctx context.Context, user, id string) error { |
| 131 | p.mu.Lock() |
| 132 | defer p.mu.Unlock() |
| 133 | if err := p.takeFailure("DeleteKey"); err != nil { |
| 134 | return err |
| 135 | } |
| 136 | if u, ok := p.Users[user]; ok { |
| 137 | delete(u.Keys, id) |
| 138 | } |
| 139 | return nil |
| 140 | } |
| 141 | |
| 142 | func (p *MemProvisioner) ReapKeys(ctx context.Context, user string, keep []string) error { |
| 143 | p.mu.Lock() |
| 144 | defer p.mu.Unlock() |
| 145 | if err := p.takeFailure("ReapKeys"); err != nil { |
| 146 | return err |
| 147 | } |
| 148 | u, ok := p.Users[user] |
| 149 | if !ok { |
| 150 | return nil |
| 151 | } |
| 152 | keepSet := make(map[string]bool, len(keep)) |
| 153 | for _, id := range keep { |
| 154 | keepSet[id] = true |
| 155 | } |
| 156 | for id := range u.Keys { |
| 157 | if !keepSet[id] { |
| 158 | delete(u.Keys, id) |
| 159 | } |
| 160 | } |
| 161 | return nil |
| 162 | } |
| 163 | |
| 164 | func (p *MemProvisioner) EmptyBucket(ctx context.Context, name string) error { |
| 165 | p.mu.Lock() |
| 166 | defer p.mu.Unlock() |
| 167 | if err := p.takeFailure("EmptyBucket"); err != nil { |
| 168 | return err |
| 169 | } |
| 170 | if b, ok := p.Buckets[name]; ok { |
| 171 | for k := range b { |
| 172 | delete(b, k) |
| 173 | } |
| 174 | } |
| 175 | return nil |
| 176 | } |
| 177 | |
| 178 | func (p *MemProvisioner) DeleteBucket(ctx context.Context, name string) error { |
| 179 | p.mu.Lock() |
| 180 | defer p.mu.Unlock() |
| 181 | if err := p.takeFailure("DeleteBucket"); err != nil { |
| 182 | return err |
| 183 | } |
| 184 | delete(p.Buckets, name) |
| 185 | return nil |
| 186 | } |
| 187 | |
| 188 | func (p *MemProvisioner) DeleteUser(ctx context.Context, user string) error { |
| 189 | p.mu.Lock() |
| 190 | defer p.mu.Unlock() |
| 191 | if err := p.takeFailure("DeleteUser"); err != nil { |
| 192 | return err |
| 193 | } |
| 194 | delete(p.Users, user) |
| 195 | return nil |
| 196 | } |
| 197 | |