mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 23:10:44 +01:00
s3tables: tighten legacy OIDC subject re-anchoring (#11808)
* sts: recognize issuer-scoped parent user ids
ComputeParentUser output has a fixed shape (base64url SHA-256); let
callers distinguish it from bare subject strings.
* s3tables: keep legacy owner re-anchoring off local and scoped account ids
Re-anchoring a stored bare subject under the single trusted issuer must
only apply to strings that could actually be a legacy OIDC subject. A
stored owner naming the reserved or a configured local account, or an
already issuer-scoped id, can otherwise be claimed by any caller whose
token asserts that string as sub.
* s3tables: re-anchor legacy policy principals in map and array forms
A stored bare subject can appear as {"AWS": sub} or inside a principal
list, not only as a top-level string; re-anchoring only the string form
left legacy Deny statements unable to match the caller.
* s3tables: keep built-in legacy exclusions independent of the account checker
A missing local-account predicate must not disable re-anchoring entirely:
the reserved accounts, ARN-shaped values, and scoped-id shapes are never
OIDC subjects regardless, while legacy Deny statements for plain subjects
still need to resolve so a denied caller cannot fall through to an
identity grant.
* s3tables: exercise legacy Deny against an owner grant
The previous case denied the caller regardless of whether the statement
principal matched; make the caller the owner so a missed match shows.
Cover the array Deny form alongside the AWS map.
This commit is contained in:
1 parent
88d9926b35
commit
8c55425d55
15 files changed
+410
-192
No files matched your search
@@ -60,6 +60,17 @@ func TestComputeParentUserEncoding(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsParentUserID(t *testing.T) {
|
||||
if !IsParentUserID(ComputeParentUser("alice", "https://idp.example/")) {
|
||||
t.Fatal("a ComputeParentUser result must be recognized")
|
||||
}
|
||||
for _, s := range []string{"", "alice", "admin", "arn:aws:iam::123:user/admin", "dGVzdA=="} {
|
||||
if IsParentUserID(s) {
|
||||
t.Fatalf("%q is not a scoped parent user id", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionClaimsRoundTripParentUser(t *testing.T) {
|
||||
parent := ComputeParentUser("alice", "https://idp.example/")
|
||||
claims := NewSTSSessionClaims("sid-1", "issuer", time.Now().Add(time.Hour)).
|
||||
|
||||
@@ -24,6 +24,14 @@ func ComputeParentUser(sub, iss string) string {
|
||||
return base64.RawURLEncoding.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// IsParentUserID reports whether s has the shape ComputeParentUser produces —
|
||||
// a base64url-encoded SHA-256 — i.e. an already issuer-scoped id rather than a
|
||||
// bare subject string.
|
||||
func IsParentUserID(s string) bool {
|
||||
b, err := base64.RawURLEncoding.DecodeString(s)
|
||||
return err == nil && len(b) == sha256.Size
|
||||
}
|
||||
|
||||
// identityClaimPriority is the order in which ResolveIdentityClaim looks for a
|
||||
// human-readable, authoritative identity attribute in an STS request context.
|
||||
// The context is populated at federation time from the validated OIDC token
|
||||
|
||||
@@ -290,3 +290,32 @@ func TestDeclaredAccountOutranksInlineIdentityAccount(t *testing.T) {
|
||||
assert.Equal(t, "100000000001", iam.GetAccountIdByEmail("alice@example.com"), "the declared email must stay indexed")
|
||||
assert.Empty(t, iam.GetAccountIdByEmail("wrong@example.com"), "an inline block must not claim an email for a declared account")
|
||||
}
|
||||
|
||||
// Stored owner strings that name a configured identity or account were never
|
||||
// OIDC subjects; recognizing them keeps issuer re-anchoring away from local
|
||||
// principals.
|
||||
func TestIsConfiguredAccountID(t *testing.T) {
|
||||
resetMemoryStore()
|
||||
|
||||
config := `{
|
||||
"identities": [
|
||||
{"name": "alice", "credentials": [{"accessKey": "alice_ak", "secretKey": "alice_sk"}], "actions": ["Read"]},
|
||||
{"name": "bob", "account": {"id": "100000000002"}, "credentials": [{"accessKey": "bob_ak", "secretKey": "bob_sk"}], "actions": ["Read"]}
|
||||
]
|
||||
}`
|
||||
tmp, err := os.CreateTemp("", "s3-config-*.json")
|
||||
require.NoError(t, err)
|
||||
defer os.Remove(tmp.Name())
|
||||
_, err = tmp.WriteString(config)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, tmp.Close())
|
||||
|
||||
iam := NewIdentityAccessManagementWithStore(&S3ApiServerOption{Config: tmp.Name()}, nil, "memory")
|
||||
|
||||
for _, id := range []string{"admin", "anonymous", "alice", "bob", "100000000002"} {
|
||||
assert.True(t, iam.isConfiguredAccountID(id), "%q is a configured local account", id)
|
||||
}
|
||||
for _, id := range []string{"mallory", "alice-sub", "000000000042"} {
|
||||
assert.False(t, iam.isConfiguredAccountID(id), "%q is not a configured local account", id)
|
||||
}
|
||||
}
|
||||
@@ -1645,6 +1645,29 @@ func (iam *IdentityAccessManagement) GetAccountIdByEmail(email string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// isConfiguredAccountID reports whether id names a configured local identity
|
||||
// or account — the reserved accounts included — i.e. an owner string that was
|
||||
// never an OIDC subject.
|
||||
func (iam *IdentityAccessManagement) isConfiguredAccountID(id string) bool {
|
||||
if id == "" || id == s3_constants.AccountAdminId || id == s3_constants.AccountAnonymousId {
|
||||
return true
|
||||
}
|
||||
iam.m.RLock()
|
||||
defer iam.m.RUnlock()
|
||||
if _, ok := iam.nameToIdentity[id]; ok {
|
||||
return true
|
||||
}
|
||||
if _, ok := iam.accounts[id]; ok {
|
||||
return true
|
||||
}
|
||||
for _, identity := range iam.identities {
|
||||
if identity.Account != nil && identity.Account.Id == id {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (iam *IdentityAccessManagement) Auth(f http.HandlerFunc, action Action) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
if !iam.isEnabled() {
|
||||
|
||||
@@ -52,6 +52,12 @@ func (st *S3TablesApiServer) SetIAMAuthorizer(authorizer s3tables.IAMAuthorizer)
|
||||
st.handler.SetIAMAuthorizer(authorizer)
|
||||
}
|
||||
|
||||
// SetLocalAccountChecker injects the predicate recognizing configured local
|
||||
// account IDs for S3 Tables legacy-owner checks.
|
||||
func (st *S3TablesApiServer) SetLocalAccountChecker(fn func(string) bool) {
|
||||
st.handler.SetLocalAccountChecker(fn)
|
||||
}
|
||||
|
||||
// S3TablesHandler handles S3 Tables API requests
|
||||
func (st *S3TablesApiServer) S3TablesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
st.handler.HandleRequest(w, r, st)
|
||||
@@ -66,6 +72,9 @@ func (st *S3TablesApiServer) WithFilerClient(streamingMode bool, fn func(filer_p
|
||||
func (s3a *S3ApiServer) registerS3TablesRoutes(router *mux.Router) {
|
||||
// Create S3 Tables handler
|
||||
s3TablesApi := NewS3TablesApiServer(s3a)
|
||||
if s3a.iam != nil {
|
||||
s3TablesApi.SetLocalAccountChecker(s3a.iam.isConfiguredAccountID)
|
||||
}
|
||||
if s3a.iam != nil && s3a.iam.iamIntegration != nil {
|
||||
s3TablesApi.SetDefaultAllow(s3a.iam.iamIntegration.DefaultAllow())
|
||||
if s3Integration, ok := s3a.iam.iamIntegration.(*S3IAMIntegration); ok && s3Integration.iamManager != nil {
|
||||
|
||||
@@ -57,11 +57,12 @@ const (
|
||||
|
||||
// S3TablesHandler handles S3 Tables API requests
|
||||
type S3TablesHandler struct {
|
||||
region string
|
||||
accountID string
|
||||
defaultAllow bool // Whether to allow access by default (for zero-config IAM)
|
||||
trusted bool // Trusted local tooling (shell/admin) bypasses authorization
|
||||
iamAuthorizer IAMAuthorizer
|
||||
region string
|
||||
accountID string
|
||||
defaultAllow bool // Whether to allow access by default (for zero-config IAM)
|
||||
trusted bool // Trusted local tooling (shell/admin) bypasses authorization
|
||||
iamAuthorizer IAMAuthorizer
|
||||
localAccountIDs func(string) bool
|
||||
}
|
||||
|
||||
// NewS3TablesHandler creates a new S3 Tables handler
|
||||
@@ -98,6 +99,13 @@ func (h *S3TablesHandler) SetTrusted(trusted bool) {
|
||||
h.trusted = trusted
|
||||
}
|
||||
|
||||
// SetLocalAccountChecker injects the predicate that recognizes account IDs of
|
||||
// configured local identities, so a stored owner that names one is never
|
||||
// re-anchored to an OIDC subject.
|
||||
func (h *S3TablesHandler) SetLocalAccountChecker(fn func(string) bool) {
|
||||
h.localAccountIDs = fn
|
||||
}
|
||||
|
||||
// FilerClient interface for filer operations
|
||||
type FilerClient interface {
|
||||
WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error
|
||||
|
||||
@@ -65,9 +65,10 @@ func (h *S3TablesHandler) handleCreateTableBucket(w http.ResponseWriter, r *http
|
||||
owner = DefaultAccountID
|
||||
}
|
||||
if !CheckPermissionWithContext("CreateTableBucket", principal, owner, "", "", &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create table buckets")
|
||||
return NewAuthError("CreateTableBucket", principal, "not authorized to create table buckets")
|
||||
|
||||
@@ -70,10 +70,11 @@ func (h *S3TablesHandler) handleGetTableBucket(w http.ResponseWriter, r *http.Re
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucket", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
||||
return ErrAccessDenied
|
||||
@@ -193,10 +194,11 @@ func (h *S3TablesHandler) handleListTableBuckets(w http.ResponseWriter, r *http.
|
||||
bucketARN := h.generateTableBucketARN(metadata.OwnerAccountID, entry.Entry.Name)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucket", accountID, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: entry.Entry.Name,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: entry.Entry.Name,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
continue
|
||||
}
|
||||
@@ -298,10 +300,11 @@ func (h *S3TablesHandler) handleDeleteTableBucket(w http.ResponseWriter, r *http
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTableBucket", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return NewAuthError("DeleteTableBucket", principal, fmt.Sprintf("not authorized to delete bucket %s", bucketName))
|
||||
}
|
||||
|
||||
@@ -349,10 +349,11 @@ func (h *S3TablesHandler) authorizeMaintenanceBucket(r *http.Request, filerClien
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CheckPermissionWithContext(operation, principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: getIdentityActions(r),
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: getIdentityActions(r),
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return "", NewAuthError(operation, principal, "not authorized to "+operation)
|
||||
}
|
||||
@@ -386,12 +387,13 @@ func (h *S3TablesHandler) authorizeMaintenanceTable(r *http.Request, filerClient
|
||||
tableARN := h.generateTableARN(metadata.OwnerAccountID, bucketName, namespaceName+"/"+tableName)
|
||||
principal := h.getAccountID(r)
|
||||
if !CheckPermissionWithContext(operation, principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: getIdentityActions(r),
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: getIdentityActions(r),
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return "", NewAuthError(operation, principal, "not authorized to "+operation)
|
||||
}
|
||||
|
||||
@@ -114,12 +114,13 @@ func (h *S3TablesHandler) handleCreateNamespace(w http.ResponseWriter, r *http.R
|
||||
identityActions := getIdentityActions(r)
|
||||
glog.Infof("S3Tables: CreateNamespace permission check - principal=%s, owner=%s, actions=%v", principal, bucketMetadata.OwnerAccountID, identityActions)
|
||||
if !CheckPermissionWithContext("CreateNamespace", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
glog.Infof("S3Tables: Permission denied for CreateNamespace - principal=%s, owner=%s", principal, bucketMetadata.OwnerAccountID)
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to create namespace in this bucket")
|
||||
@@ -256,12 +257,13 @@ func (h *S3TablesHandler) handleGetNamespace(w http.ResponseWriter, r *http.Requ
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetNamespace", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", flattenNamespace(req.Namespace)))
|
||||
return ErrAccessDenied
|
||||
@@ -347,12 +349,13 @@ func (h *S3TablesHandler) handleUpdateNamespace(w http.ResponseWriter, r *http.R
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("UpdateNamespace", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", flattenNamespace(req.Namespace)))
|
||||
return ErrAccessDenied
|
||||
@@ -450,11 +453,12 @@ func (h *S3TablesHandler) handleListNamespaces(w http.ResponseWriter, r *http.Re
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListNamespaces", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchBucket, fmt.Sprintf("table bucket %s not found", bucketName))
|
||||
return ErrAccessDenied
|
||||
@@ -635,12 +639,13 @@ func (h *S3TablesHandler) handleDeleteNamespace(w http.ResponseWriter, r *http.R
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteNamespace", principal, metadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", flattenNamespace(req.Namespace)))
|
||||
return ErrAccessDenied
|
||||
|
||||
@@ -92,10 +92,11 @@ func (h *S3TablesHandler) handlePutTableBucketPolicy(w http.ResponseWriter, r *h
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("PutTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, "", bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to put table bucket policy")
|
||||
return NewAuthError("PutTableBucketPolicy", principal, "not authorized to put table bucket policy")
|
||||
@@ -171,10 +172,11 @@ func (h *S3TablesHandler) handleGetTableBucketPolicy(w http.ResponseWriter, r *h
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, string(policy), bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to get table bucket policy")
|
||||
return NewAuthError("GetTableBucketPolicy", principal, "not authorized to get table bucket policy")
|
||||
@@ -248,10 +250,11 @@ func (h *S3TablesHandler) handleDeleteTableBucketPolicy(w http.ResponseWriter, r
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTableBucketPolicy", principal, bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table bucket policy")
|
||||
return NewAuthError("DeleteTableBucketPolicy", principal, "not authorized to delete table bucket policy")
|
||||
@@ -348,12 +351,13 @@ func (h *S3TablesHandler) handlePutTablePolicy(w http.ResponseWriter, r *http.Re
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("PutTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to put table policy")
|
||||
return NewAuthError("PutTablePolicy", principal, "not authorized to put table policy")
|
||||
@@ -457,12 +461,13 @@ func (h *S3TablesHandler) handleGetTablePolicy(w http.ResponseWriter, r *http.Re
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("GetTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to get table policy")
|
||||
return NewAuthError("GetTablePolicy", principal, "not authorized to get table policy")
|
||||
@@ -548,12 +553,13 @@ func (h *S3TablesHandler) handleDeleteTablePolicy(w http.ResponseWriter, r *http
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("DeleteTablePolicy", principal, metadata.OwnerAccountID, bucketPolicy, tableARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusForbidden, ErrCodeAccessDenied, "not authorized to delete table policy")
|
||||
return NewAuthError("DeleteTablePolicy", principal, "not authorized to delete table policy")
|
||||
@@ -646,14 +652,15 @@ func (h *S3TablesHandler) handleTagResource(w http.ResponseWriter, r *http.Reque
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("TagResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
RequestTags: req.Tags,
|
||||
TagKeys: requestTagKeys,
|
||||
ResourceTags: existingTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
RequestTags: req.Tags,
|
||||
TagKeys: requestTagKeys,
|
||||
ResourceTags: existingTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return NewAuthError("TagResource", principal, "not authorized to tag resource")
|
||||
}
|
||||
@@ -767,12 +774,13 @@ func (h *S3TablesHandler) handleListTagsForResource(w http.ResponseWriter, r *ht
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListTagsForResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return NewAuthError("ListTagsForResource", principal, "not authorized to list tags for resource")
|
||||
}
|
||||
@@ -875,13 +883,14 @@ func (h *S3TablesHandler) handleUntagResource(w http.ResponseWriter, r *http.Req
|
||||
principal := h.getAccountID(r)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("UntagResource", principal, ownerAccountID, bucketPolicy, resourceARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
TagKeys: req.TagKeys,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
TagKeys: req.TagKeys,
|
||||
ResourceTags: tags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return NewAuthError("UntagResource", principal, "not authorized to untag resource")
|
||||
}
|
||||
|
||||
@@ -422,14 +422,15 @@ func (h *S3TablesHandler) handleGetTable(w http.ResponseWriter, r *http.Request,
|
||||
allowed := CheckTableOrBucketPermission("GetTable", accountID,
|
||||
metadata.OwnerAccountID, tablePolicy, tableARN,
|
||||
bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
})
|
||||
|
||||
if !allowed {
|
||||
@@ -557,12 +558,13 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
if !CheckTableOrBucketPermission("ListTables", accountID,
|
||||
nsMeta.OwnerAccountID, namespacePolicy, bucketARN,
|
||||
bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -603,11 +605,12 @@ func (h *S3TablesHandler) handleListTables(w http.ResponseWriter, r *http.Reques
|
||||
bucketARN := h.generateTableBucketARN(bucketMeta.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
if !CheckPermissionWithContext("ListTables", accountID, bucketMeta.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
return ErrAccessDenied
|
||||
}
|
||||
@@ -943,14 +946,15 @@ func (h *S3TablesHandler) handleDeleteTable(w http.ResponseWriter, r *http.Reque
|
||||
allowed := CheckTableOrBucketPermission("DeleteTable", principal,
|
||||
metadata.OwnerAccountID, tablePolicy, tableARN,
|
||||
bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
})
|
||||
if !allowed {
|
||||
if h.canReadEntry("GetTable", principal, metadata.OwnerAccountID, bucketMetadata.OwnerAccountID, tablePolicy, bucketPolicy, tableARN, bucketARN, bucketName, namespaceName, tableName, bucketTags, tableTags, identityActions, r) {
|
||||
@@ -1115,14 +1119,15 @@ func (h *S3TablesHandler) readTableMetadata(ctx context.Context, client filer_pb
|
||||
// cannot see the entry keeps the not-found veil.
|
||||
func (h *S3TablesHandler) canReadEntry(readOp, principal, tableOwner, bucketOwner, tablePolicy, bucketPolicy, tableARN, bucketARN, bucketName, namespace, name string, bucketTags, tableTags map[string]string, identityActions []string, r *http.Request) bool {
|
||||
ctx := &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: name,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: name,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}
|
||||
return CheckTableOrBucketPermission(readOp, principal,
|
||||
tableOwner, tablePolicy, tableARN,
|
||||
@@ -1300,14 +1305,15 @@ func (h *S3TablesHandler) renameCatalogEntry(w http.ResponseWriter, r *http.Requ
|
||||
allowed := CheckTableOrBucketPermission(kind.renameOp, principal,
|
||||
metadata.OwnerAccountID, tablePolicy, tableARN,
|
||||
bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: srcNamespace,
|
||||
TableName: srcName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: srcNamespace,
|
||||
TableName: srcName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
})
|
||||
if !allowed {
|
||||
readTags := tableTags
|
||||
@@ -1367,13 +1373,14 @@ func (h *S3TablesHandler) renameCatalogEntry(w http.ResponseWriter, r *http.Requ
|
||||
if !CheckTableOrBucketPermission(kind.createOp, principal,
|
||||
destNamespaceMetadata.OwnerAccountID, destNamespacePolicy, bucketARN,
|
||||
bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: destNamespace,
|
||||
TableName: destName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: destNamespace,
|
||||
TableName: destName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}) {
|
||||
h.writeError(w, http.StatusNotFound, ErrCodeNoSuchNamespace, fmt.Sprintf("namespace %s not found", destNamespace))
|
||||
return NewAuthError(kind.renameOp, principal, "not authorized to create "+kind.noun+" in the destination namespace")
|
||||
@@ -1572,14 +1579,15 @@ func (h *S3TablesHandler) handleUpdateTable(w http.ResponseWriter, r *http.Reque
|
||||
allowed := CheckTableOrBucketPermission("UpdateTable", principal,
|
||||
metadata.OwnerAccountID, tablePolicy, tableARN,
|
||||
bucketMetadata.OwnerAccountID, bucketPolicy, bucketARN, &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
TableBucketTags: bucketTags,
|
||||
ResourceTags: tableTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
})
|
||||
|
||||
if !allowed {
|
||||
@@ -1777,15 +1785,16 @@ func (h *S3TablesHandler) authorizeCreateTable(w http.ResponseWriter, r *http.Re
|
||||
bucketARN := h.generateTableBucketARN(bucketMetadata.OwnerAccountID, bucketName)
|
||||
identityActions := getIdentityActions(r)
|
||||
policyContext := &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
RequestTags: requestTags,
|
||||
TagKeys: mapKeys(requestTags),
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespaceName,
|
||||
TableName: tableName,
|
||||
RequestTags: requestTags,
|
||||
TagKeys: mapKeys(requestTags),
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}
|
||||
if !CheckTableOrBucketPermission("CreateTable", accountID,
|
||||
namespaceMetadata.OwnerAccountID, namespacePolicy, bucketARN,
|
||||
|
||||
@@ -635,13 +635,14 @@ func (h *S3TablesHandler) loadAuthContext(r *http.Request, filerClient FilerClie
|
||||
func (h *S3TablesHandler) authorizeViewOp(r *http.Request, operation, principal, resourceOwner, bucketOwner, resourcePolicy, bucketPolicy, resourceARN, bucketName, namespace, viewName string, bucketTags map[string]string) bool {
|
||||
identityActions := getIdentityActions(r)
|
||||
ctx := &PolicyContext{
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: viewName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
TrustedIssuers: h.trustedIssuers(r),
|
||||
IsLocalAccountID: h.localAccountIDs,
|
||||
TableBucketName: bucketName,
|
||||
Namespace: namespace,
|
||||
TableName: viewName,
|
||||
TableBucketTags: bucketTags,
|
||||
IdentityActions: identityActions,
|
||||
DefaultAllow: h.defaultAllowFor(r),
|
||||
}
|
||||
bucketARN := h.generateTableBucketARN(bucketOwner, bucketName)
|
||||
return CheckTableOrBucketPermission(operation, principal,
|
||||
|
||||
@@ -94,6 +94,11 @@ type PolicyContext struct {
|
||||
// account IDs. A bare subject is only resolvable when exactly one issuer
|
||||
// is configured; more than one leaves it ambiguous, so it must not match.
|
||||
TrustedIssuers []string
|
||||
// IsLocalAccountID reports whether an account ID belongs to a configured
|
||||
// local identity. Such owner strings were never OIDC subjects and must not
|
||||
// be re-anchored; a nil predicate exempts nothing beyond the built-in
|
||||
// reserved, ARN, and scoped-id checks.
|
||||
IsLocalAccountID func(string) bool
|
||||
}
|
||||
|
||||
// CheckPermissionWithContext checks permission with optional resource and condition context.
|
||||
@@ -295,8 +300,21 @@ func matchesCaller(principalSpec interface{}, principal string, ctx *PolicyConte
|
||||
if matchesPrincipal(principalSpec, principal) {
|
||||
return true
|
||||
}
|
||||
legacy, ok := principalSpec.(string)
|
||||
return ok && matchesLegacySubject(legacy, principal, ctx)
|
||||
switch p := principalSpec.(type) {
|
||||
case string:
|
||||
return matchesLegacySubject(p, principal, ctx)
|
||||
case []interface{}:
|
||||
for _, item := range p {
|
||||
if str, ok := item.(string); ok && matchesLegacySubject(str, principal, ctx) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case map[string]interface{}:
|
||||
if aws, ok := p["AWS"]; ok {
|
||||
return matchesCaller(aws, principal, ctx)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// matchesLegacySubject reports whether a bare subject stored before issuer
|
||||
@@ -309,6 +327,17 @@ func matchesLegacySubject(legacy, principal string, ctx *PolicyContext) bool {
|
||||
if ctx == nil || len(ctx.TrustedIssuers) != 1 {
|
||||
return false
|
||||
}
|
||||
// The reserved accounts, ARN values, and already issuer-scoped ids are never
|
||||
// OIDC subjects — or, for strings that could be either a legacy subject or a
|
||||
// scoped id, resolving to scoped fails closed. A configured local account
|
||||
// id is excluded the same way once the caller supplies the predicate. Any
|
||||
// of them could otherwise be claimed by a token asserting that string as
|
||||
// sub — including the admin account or a victim's scoped id.
|
||||
if legacy == s3_constants.AccountAdminId || legacy == s3_constants.AccountAnonymousId ||
|
||||
strings.HasPrefix(legacy, "arn:") || sts.IsParentUserID(legacy) ||
|
||||
(ctx.IsLocalAccountID != nil && ctx.IsLocalAccountID(legacy)) {
|
||||
return false
|
||||
}
|
||||
return sts.ComputeParentUser(legacy, ctx.TrustedIssuers[0]) == principal
|
||||
}
|
||||
|
||||
|
||||
@@ -334,26 +334,97 @@ func TestCheckTableOrBucketPermissionOwnerlessFailsClosed(t *testing.T) {
|
||||
func TestCheckPermissionLegacyOwnerScopedToIssuer(t *testing.T) {
|
||||
owner := "alice-sub"
|
||||
principal := sts.ComputeParentUser(owner, "corp-idp")
|
||||
noLocal := &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp"},
|
||||
IsLocalAccountID: func(string) bool { return false },
|
||||
}
|
||||
|
||||
if !CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp"},
|
||||
}) {
|
||||
if !CheckPermissionWithContext("GetTable", principal, owner, "", "arn", noLocal) {
|
||||
t.Error("single-issuer deployments must keep recognizing the legacy owner")
|
||||
}
|
||||
|
||||
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
||||
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
||||
IsLocalAccountID: func(string) bool { return false },
|
||||
}) {
|
||||
t.Error("a bare subject is ambiguous across multiple issuers and must not match")
|
||||
}
|
||||
|
||||
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(owner, "evil-idp"), owner, "", "arn", &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
||||
TrustedIssuers: []string{"corp-idp", "evil-idp"},
|
||||
IsLocalAccountID: func(string) bool { return false },
|
||||
}) {
|
||||
t.Error("a same-sub caller from another issuer must not claim the legacy owner")
|
||||
}
|
||||
|
||||
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{}) {
|
||||
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
||||
IsLocalAccountID: func(string) bool { return false },
|
||||
}) {
|
||||
t.Error("legacy owner matching must not engage without a trusted issuer")
|
||||
}
|
||||
|
||||
// With no local-account predicate only the unconditional exclusions apply;
|
||||
// a plain legacy subject still re-anchors.
|
||||
if !CheckPermissionWithContext("GetTable", principal, owner, "", "arn", &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp"},
|
||||
}) {
|
||||
t.Error("a missing local-account check keeps plain legacy subjects working")
|
||||
}
|
||||
}
|
||||
|
||||
// A stored owner that names a reserved or configured local account was never
|
||||
// an OIDC subject, and an already issuer-scoped id is not a legacy subject
|
||||
// either; re-anchoring them would let a caller pick sub=<stored owner> and
|
||||
// claim the resource.
|
||||
func TestCheckPermissionLegacyOwnerExcludesLocalAndScoped(t *testing.T) {
|
||||
ctx := &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp"},
|
||||
IsLocalAccountID: func(id string) bool { return id == "admin" || id == "owner2" },
|
||||
}
|
||||
|
||||
for _, owner := range []string{"admin", "owner2"} {
|
||||
principal := sts.ComputeParentUser(owner, "corp-idp")
|
||||
if CheckPermissionWithContext("GetTable", principal, owner, "", "arn", ctx) {
|
||||
t.Errorf("sub=%q must not re-anchor onto the local account owner", owner)
|
||||
}
|
||||
}
|
||||
|
||||
scoped := sts.ComputeParentUser("victim-sub", "corp-idp")
|
||||
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(scoped, "corp-idp"), scoped, "", "arn", ctx) {
|
||||
t.Error("sub equal to a stored scoped id must not re-anchor")
|
||||
}
|
||||
|
||||
// An ARN-shaped subject claims nothing; ARNs name IAM principals, not OIDC
|
||||
// subjects, and this shape is what let a forged sub reach the admin account.
|
||||
arn := "arn:aws:iam::123456789012:user/admin"
|
||||
if CheckPermissionWithContext("GetTable", sts.ComputeParentUser(arn, "corp-idp"), arn, "", "arn", ctx) {
|
||||
t.Error("an ARN-shaped stored owner must not re-anchor")
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy policies can carry the bare subject in the AWS map or array forms,
|
||||
// not only as a top-level string; re-anchoring must reach all of them or a
|
||||
// pre-scoping Deny silently stops matching the caller.
|
||||
func TestCheckPermissionLegacyPolicyPrincipalShapes(t *testing.T) {
|
||||
ctx := &PolicyContext{
|
||||
TrustedIssuers: []string{"corp-idp"},
|
||||
IsLocalAccountID: func(string) bool { return false },
|
||||
}
|
||||
principal := sts.ComputeParentUser("alice-sub", "corp-idp")
|
||||
other := "other-owner"
|
||||
|
||||
// The caller owns the resource, so only a matched Deny can veto it.
|
||||
for _, policy := range []string{
|
||||
`{"Statement":[{"Effect":"Deny","Principal":{"AWS":"alice-sub"},"Action":"s3tables:GetTable"}]}`,
|
||||
`{"Statement":[{"Effect":"Deny","Principal":["alice-sub"],"Action":"s3tables:GetTable"}]}`,
|
||||
} {
|
||||
if CheckPermissionWithContext("GetTable", principal, principal, policy, "arn", ctx) {
|
||||
t.Errorf("legacy Deny must veto the scoped caller's owner grant: %s", policy)
|
||||
}
|
||||
}
|
||||
|
||||
allow := `{"Statement":[{"Effect":"Allow","Principal":["alice-sub"],"Action":"s3tables:GetTable"}]}`
|
||||
if !CheckPermissionWithContext("GetTable", principal, other, allow, "arn", ctx) {
|
||||
t.Error("legacy Allow with array principal must still match the scoped caller")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user