Files
seaweedfs/weed/s3api/auth_credentials_native_floor_test.go
Chris Lu 557fffa350 iam: preserve native Admin when IAM policies are attached (#11226) (#11232)
* iam: expose tri-state result from attached policy evaluation

evaluateIAMPolicies returned a bool that collapsed explicit Deny and
no-match into a single false, so the authorization path could not tell
"policies forbid this" from "policies say nothing". Introduce
evaluateAttachedIAMPolicies returning Allow/Deny/NoMatch and keep
evaluateIAMPolicies as a bool projection for existing callers. This is
preparation for unioning native permissions with attached policies while
preserving deny-always-wins.

* iam: preserve native Admin when IAM policies are attached

Attaching an IAM policy routed authorization exclusively to the attached
policies, dropping the identity native permissions. A user with native
Admin lost all access after attaching a non-granting policy, and stayed
locked out if that policy was deleted without being detached first
(#11226).

Treat a native bare Admin grant as a permission floor that survives
attached policies: when the attached policies do not explicitly allow,
fall back to isAdmin() on the attached-policy path, and on the IAM
integration path allow unless an attached policy explicitly denies.
Explicit Deny still wins on both paths.

Only bare Admin is consulted because inline policies flatten lossily into
Actions (dropping conditions), so scoped actions are not unambiguously
native and must keep flowing through the policy engine.

* iam: regression tests for native Admin surviving attached policies

Reproduces issue #11226:

- TestNativeAdminSurvivesAttachedPolicy: a user with native Admin keeps
  Write access after attaching a policy that does not grant it.
- TestNativeAdminSurvivesDeletedPolicy: the same user keeps Write access
  after the attached policy is deleted without being detached.
- TestAttachedPolicyExplicitDenyOverridesNativeAdmin: an explicit Deny in
  an attached policy still constrains a native admin (deny-always-wins).

* iam: apply native Admin floor before IAM principal validation

The native Admin floor in authorizeWithIAM ran after the auth-path
switch, which denies when no session principal or PrincipalArn is
present. An Admin identity without a PrincipalArn (no session token)
was therefore denied before the floor executed. Move the floor ahead of
the switch and derive the principal for its explicit-deny check with
buildPrincipalARN, which already handles identities without a
PrincipalArn. Adds a regression case for an Admin identity with an
empty PrincipalArn.

Addresses CodeRabbit review feedback on PR #11232.
2026-09-08 14:39:43 -07:00

127 lines
4.6 KiB
Go

package s3api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newNativeFloorTestIAM(t *testing.T) (*IdentityAccessManagement, *Identity) {
t.Helper()
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*"},
},
})
require.NoError(t, iam.PutPolicy("ListBucketsOnly", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"ListBucketsOnly"},
PrincipalArn: "arn:aws:iam::111122223333:user/admin",
}
return iam, identity
}
func putObjectRequest() *http.Request {
return httptest.NewRequest(http.MethodPut, "/mybucket/file.txt", nil)
}
// TestNativeAdminSurvivesAttachedPolicy reproduces issue #11226: attaching an
// IAM policy to a user with native Admin must not override the native grant.
// The effective permissions are the union of native permissions and attached
// policy grants, so a Write the attached policy never mentions still succeeds.
func TestNativeAdminSurvivesAttachedPolicy(t *testing.T) {
iam, identity := newNativeFloorTestIAM(t)
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective after attaching a policy")
}
// TestNativeAdminSurvivesDeletedPolicy reproduces the second half of #11226:
// deleting the attached policy without detaching it first must not leave the
// user locked out of their native permissions.
func TestNativeAdminSurvivesDeletedPolicy(t *testing.T) {
iam, identity := newNativeFloorTestIAM(t)
require.NoError(t, iam.DeletePolicy("ListBucketsOnly"))
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective after the attached policy is deleted")
}
// TestNativeAdminSurvivesAttachedPolicyWithoutPrincipalArn covers the IAM
// integration path when the Admin identity has no PrincipalArn (and no session
// token), so the auth-path switch would otherwise deny before the native floor.
func TestNativeAdminSurvivesAttachedPolicyWithoutPrincipalArn(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Allow", "Action": "s3:ListAllMyBuckets", "Resource": "*"},
},
})
require.NoError(t, iam.PutPolicy("ListBucketsOnly", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"ListBucketsOnly"},
}
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrNone, errCode,
"native Admin must remain effective without a PrincipalArn")
}
// TestAttachedPolicyExplicitDenyOverridesNativeAdmin ensures deny-always-wins:
// an explicit Deny in an attached policy still constrains a native admin.
func TestAttachedPolicyExplicitDenyOverridesNativeAdmin(t *testing.T) {
mgr := newTestIAMManager(t)
iam := &IdentityAccessManagement{}
iam.SetIAMIntegration(NewS3IAMIntegration(mgr, ""))
doc, _ := json.Marshal(map[string]interface{}{
"Version": "2012-10-17",
"Statement": []map[string]interface{}{
{"Effect": "Deny", "Action": "s3:PutObject", "Resource": "arn:aws:s3:::mybucket/*"},
},
})
require.NoError(t, iam.PutPolicy("DenyPutMyBucket", string(doc)))
identity := &Identity{
Name: "admin",
Account: &Account{DisplayName: "admin", Id: "admin"},
Actions: []Action{s3_constants.ACTION_ADMIN},
PolicyNames: []string{"DenyPutMyBucket"},
PrincipalArn: "arn:aws:iam::111122223333:user/admin",
}
errCode := iam.VerifyActionPermission(putObjectRequest(), identity,
s3_constants.ACTION_WRITE, "mybucket", "file.txt")
assert.Equal(t, s3err.ErrAccessDenied, errCode,
"explicit Deny in an attached policy must override native Admin")
}