mirror of
https://github.com/versity/versitygw.git
synced 2026-09-28 18:55:54 +00:00
fix: role last-used tracking, and record S3 requests in last-used metadata
Role last-used tracking was missing entirely - `GetRole` returned a `RoleLastUsed` element that nothing ever wrote, rendering the zero time instead of the empty element AWS returns for an unused role - and access key last-used only ever saw the `IAM`/`STS` control plane, so a credential used exclusively against the S3 gateway reported as never used. Roles now record a use whenever a request authenticates with one of their session credentials, through a new `Storer.RecordRoleUsage` mirroring `RecordAccessKeyUsage`, gated on the session's role still being the one it was minted against so a session outliving its role can't attribute its use to a same-named replacement. `LastUsedDate` became a `*time.Time` so an unused role renders as an empty element. Both records now cover the S3 data plane as well: the gateway sends its configured region and `s3` on evaluate-policy and the IAM service records the caller there, so `GetAccessKeyLastUsed's` `ServiceName` is now iam, sts or s3. That call was chosen over derive-signing-key, which runs before signature verification and takes its region and service from the caller's own `Authorization` header - recording there would let anyone who knows an access key id refresh and poison another identity's audit record. Requests denied by a bucket policy or made against a public bucket are not recorded, since neither reaches identity-policy evaluation. To keep per-request recording affordable, an update is skipped while the stored record has the same service and region and is under a minute old; a change of either is written through immediately. Assuming a role is not a use, a request denied by an identity policy is, and both successful and denied S3 requests update the record. Also moves the `OIDC-dependent` tests into the `s3-iam-session` group so runoidctests.sh runs a single group.
This commit is contained in:
+100
-33
@@ -523,6 +523,28 @@ func (s *InternalStore) DeleteAccessKey(_ context.Context, username, accessKeyID
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
// lookupAccessKey resolves accessKeyID to its stored entry and owning user
|
||||
// name through the access key index, reporting NoSuchEntity for a key that
|
||||
// resolves to nothing at any step.
|
||||
func lookupAccessKey(conf iamConfig, accessKeyID string) (types.AccessKeyEntry, string, error) {
|
||||
username, ok := conf.AccessKeyIndex[accessKeyID]
|
||||
if !ok {
|
||||
return types.AccessKeyEntry{}, "", iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
user, ok := conf.Users[username]
|
||||
if !ok {
|
||||
return types.AccessKeyEntry{}, "", iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
|
||||
for _, key := range user.AccessKeys {
|
||||
if key.AccessKeyId == accessKeyID {
|
||||
return key, username, nil
|
||||
}
|
||||
}
|
||||
|
||||
return types.AccessKeyEntry{}, "", iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
|
||||
func (s *InternalStore) GetAccessKeyLastUsed(_ context.Context, accessKeyID string) (*GetAccessKeyLastUsedOutput, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
@@ -532,63 +554,61 @@ func (s *InternalStore) GetAccessKeyLastUsed(_ context.Context, accessKeyID stri
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username, ok := conf.AccessKeyIndex[accessKeyID]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
user, ok := conf.Users[username]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
key, username, err := lookupAccessKey(conf, accessKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, key := range user.AccessKeys {
|
||||
if key.AccessKeyId == accessKeyID {
|
||||
return &GetAccessKeyLastUsedOutput{
|
||||
UserName: username,
|
||||
LastUsedDate: key.LastUsedDate,
|
||||
ServiceName: key.LastUsedService,
|
||||
Region: key.LastUsedRegion,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
return &GetAccessKeyLastUsedOutput{
|
||||
UserName: username,
|
||||
LastUsedDate: key.LastUsedDate,
|
||||
ServiceName: key.LastUsedService,
|
||||
Region: key.LastUsedRegion,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RecordAccessKeyUsage rewrites the whole IAM file, so it first reads the
|
||||
// stored record and returns without writing anything when the update would
|
||||
// be redundant — a read per request instead of a file rewrite per request,
|
||||
// which is what makes recording every S3 data-plane request affordable here.
|
||||
func (s *InternalStore) RecordAccessKeyUsage(_ context.Context, accessKeyID, service, region string, when time.Time) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
err := s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, _, err := lookupAccessKey(conf, accessKeyID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !shouldRecordUsage(key.LastUsedDate, key.LastUsedService, key.LastUsedRegion, service, region, when) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
username, ok := conf.AccessKeyIndex[accessKeyID]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
user, ok := conf.Users[username]
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
_, username, err := lookupAccessKey(conf, accessKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
user := conf.Users[username]
|
||||
for i, key := range user.AccessKeys {
|
||||
if key.AccessKeyId == accessKeyID {
|
||||
user.AccessKeys[i].LastUsedDate = when
|
||||
user.AccessKeys[i].LastUsedService = service
|
||||
user.AccessKeys[i].LastUsedRegion = region
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return nil, iamerr.NoSuchEntityAccessKey(accessKeyID)
|
||||
}
|
||||
|
||||
conf.Users[username] = user
|
||||
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
@@ -957,6 +977,48 @@ func (s *InternalStore) UpdateAssumeRolePolicy(_ context.Context, input UpdateAs
|
||||
return cloneRole(updated), nil
|
||||
}
|
||||
|
||||
// RecordRoleUsage is RecordAccessKeyUsage's role counterpart, including its
|
||||
// read-first check: a redundant update writes nothing at all, and an update
|
||||
// worth keeping rewrites the whole IAM file.
|
||||
func (s *InternalStore) RecordRoleUsage(_ context.Context, roleName, region string, when time.Time) error {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
conf, err := s.engine.GetIAM()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
prev, prevRegion := roleLastUsedRecord(role)
|
||||
if !shouldRecordUsage(prev, "", prevRegion, "", region, when) {
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.engine.StoreIAM(func(data []byte) ([]byte, error) {
|
||||
conf, err := s.engine.ParseIAM(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
canonical, role, ok := lookupRole(conf, roleName)
|
||||
if !ok {
|
||||
return nil, iamerr.NoSuchEntityRole(roleName)
|
||||
}
|
||||
|
||||
role.RoleLastUsed = &types.RoleLastUsed{
|
||||
LastUsedDate: &when,
|
||||
Region: region,
|
||||
}
|
||||
conf.Roles[canonical] = role
|
||||
|
||||
return json.Marshal(conf)
|
||||
})
|
||||
return unwrapAPIError(err)
|
||||
}
|
||||
|
||||
func (s *InternalStore) TagRole(_ context.Context, roleName string, tags []types.Tag) error {
|
||||
return s.updateRoleTags(roleName, func(role *types.Role) error {
|
||||
merged, err := mergeTags(role.Tags, tags, iamutil.TagKeysFolded)
|
||||
@@ -1183,6 +1245,11 @@ func cloneRole(role types.Role) *types.Role {
|
||||
cloned := role
|
||||
cloned.Tags = slices.Clone(role.Tags)
|
||||
cloned.Policies.Inline = slices.Clone(role.Policies.Inline)
|
||||
if role.RoleLastUsed != nil {
|
||||
lastUsed := *role.RoleLastUsed
|
||||
cloned.RoleLastUsed = &lastUsed
|
||||
}
|
||||
cloned.EnsureRoleLastUsed()
|
||||
return &cloned
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user