Files
versitygw/auth/access-control.go
niksis02 41783bd814 fix: combine bucket policy with ACL so a policy no longer locks the owner out
Once a bucket had any policy, `verifyResourceAccess` evaluated the policy alone and ignored the bucket ACL, so a non-root bucket owner kept only what the policy explicitly allowed. A Deny-only policy (e.g. Deny `*` `s3:GetObject` on one prefix) therefore locked the owner out of every operation, including `DeleteBucketPolicy`, and only root/admin could recover. ACL grants also authorized every bucket operation through the controller's `AclPermission`, so a non-owner with a `WRITE` grant could install a bucket policy that locked out the owner and every other grantee.

The policy and the ACL are now combined per action the way AWS does it: an explicit policy Deny wins, and otherwise the action is allowed by either a policy Allow or an ACL grant. The bucket owner keeps everything a policy doesn't explicitly deny and, like an AWS account root user, can always call `GetBucketPolicy`, `PutBucketPolicy` and `DeleteBucketPolicy` even past a Deny. The protection covers only those three APIs; `GetBucketPolicyStatus` and every other action stay deniable, matching AWS.

ACL grants to non-owners now follow AWS's ACL-to-action mapping through the new `aclActionPermissions` table: `READ` grants `s3:ListBucket`, `s3:ListBucketVersions`, `s3:ListBucketMultipartUploads`, `s3:GetObject`, `s3:GetObjectVersion` and `s3:GetObjectAttributes`; `WRITE` grants `s3:PutObject` and `s3:DeleteObject`; `READ_ACP` and `WRITE_ACP` grant reading and writing the bucket and object ACLs. Bucket configuration, the bucket policy, tagging, object lock settings and `DeleteBucket` are the owner's alone unless a policy grants them. `verifyACL` is replaced by `aclAllows`, which derives the required permission from the action, so `AccessOptions.AclPermission` now only feeds the readonly gate. The anonymous path uses the same table in place of `publicACLAllowedActions`, which leaves its behavior unchanged.

`GetBucketPolicyStatus` also reported a Deny-only `Principal: "*"` policy as public; only Allow statements now count toward `IsPublic`.
2026-10-09 00:25:18 +01:00

671 lines
25 KiB
Go

// Copyright 2023 Versity Software
// This file is licensed under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package auth
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"slices"
"strings"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/gofiber/fiber/v3"
"github.com/versity/versitygw/backend"
"github.com/versity/versitygw/s3err"
)
func VerifyObjectCopyAccess(ctx fiber.Ctx, be backend.Backend, copySource string, opts AccessOptions) error {
// Verify destination bucket access first. VerifyAccess enforces the
// readonly gate before its own root/admin bypass, and that ordering
// must hold here too — readonly mode blocks writes for everyone,
// including root/admin, not just ordinary users.
if err := VerifyAccess(ctx, be, opts); err != nil {
return err
}
// Root/admin already cleared the destination check above; skip the
// source-bucket ACL lookup entirely for them, same as before.
if opts.IsRoot {
return nil
}
if opts.Acc.Role == RoleAdmin {
return nil
}
// Verify source bucket access.
// URL-decode the copy source before splitting so that clients which send
// the bucket/key separator as "%2F" are handled correctly.
// Callers are expected to have already stripped any leading '/'.
decodedSrc, err := url.QueryUnescape(copySource)
if err != nil {
return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceEncoding, copySource)
}
srcBucket, srcObject, found := strings.Cut(decodedSrc, "/")
if !found {
return s3err.GetInvalidArgumentErr(s3err.InvalidArgCopySourceBucket, copySource)
}
// Get source bucket ACL
srcBucketACLBytes, err := be.GetBucketAcl(ctx.RequestCtx(), &s3.GetBucketAclInput{Bucket: &srcBucket})
if err != nil {
return err
}
var srcBucketAcl ACL
if err := json.Unmarshal(srcBucketACLBytes, &srcBucketAcl); err != nil {
return err
}
if err := VerifyAccess(ctx, be, AccessOptions{
Acl: srcBucketAcl,
AclPermission: PermissionRead,
IsRoot: opts.IsRoot,
Acc: opts.Acc,
Bucket: srcBucket,
Object: srcObject,
Actions: []Action{GetObjectAction},
Iam: opts.Iam,
DisableACL: opts.DisableACL,
}); err != nil {
return err
}
return nil
}
type AccessOptions struct {
Acl ACL
AclPermission Permission
IsRoot bool
Acc Account
Bucket string
Object string
Actions []Action
Readonly bool
IsPublicRequest bool
DisableACL bool
Iam IAMService
}
// VerifyAccess decides whether opts.Acc may perform opts.Actions against
// opts.Bucket/opts.Object, combining the bucket's own resource-based
// decision (its policy and ACL) with an identity-based decision from
// opts.Iam when it implements PolicyEvaluator. An explicit Deny from either
// source denies the request outright, even when the other source would
// otherwise allow it; absent any explicit Deny, each action is allowed by
// either source's Allow on its own, and the request needs every action
// allowed. All three denial shapes are Code: AccessDenied, HTTP 403 —
// differing only in the dynamic Message text.
func VerifyAccess(ctx fiber.Ctx, be backend.Backend, opts AccessOptions) error {
if err := verifyAccessGates(opts); err != nil || !authorizationApplies(opts) {
return err
}
errs, err := objectsAccessErrors(ctx.RequestCtx(), be, opts, []string{opts.Object}, requestConditionContext(ctx, opts.Actions))
if err != nil {
return err
}
return errs[0]
}
// VerifyObjectsAccess authorizes a multi-object delete — the parsed contents
// of a DeleteObjects request body, passed straight through. It answers, for
// every object independently, whether policy allows deleting it and whether
// an object lock protects it, in a single pass. DeleteObjects supports
// partial success — unlike every other write path — so a denial on one
// object must not affect any other: the caller sends only the objects that
// pass through to the backend, and reports the rest as per-object errors
// straight from the returned slice.
//
// Both halves are deliberately here rather than split across the caller: the
// per-object work needs one loop, not one loop per concern at a different
// layer, and the expensive parts of each half — the bucket policy, the
// batched identity-policy round trip, the bucket's lock configuration — are
// resolved once up front for the whole request.
//
// Every key is authorized against its own object ARN, the way real AWS does
// it: a policy granting s3:DeleteObject on "arn:aws:s3:::bucket/*" and
// nothing else deletes successfully. An object named with a VersionId is
// authorized against s3:DeleteObjectVersion instead of s3:DeleteObject, the
// same split the single-object DELETE path already makes: a policy granting
// only s3:DeleteObject denies the versioned deletes in the same batch that
// its keyed deletes succeed under, and the batch's response reports that
// denial on just that object, the rest unaffected.
//
// The returned slice has one entry per object: nil where that object may
// proceed, an AWS-shaped denial otherwise. opts.Object and opts.Actions are
// both ignored in favor of objects. The second return is non-nil only for a
// failure that isn't about any one object — readonly mode, or an error
// resolving the bucket's policy or lock configuration — and fails the whole
// request, matching what a hard failure did before this returned per-object
// results at all.
func VerifyObjectsAccess(ctx fiber.Ctx, be backend.Backend, opts AccessOptions, objects []types.ObjectIdentifier, bypass BypassMode) ([]error, error) {
if err := verifyAccessGates(opts); err != nil {
return nil, err
}
if len(objects) == 0 {
return nil, nil
}
rctx := ctx.RequestCtx()
// A DeleteObjects batch reads no If-Match/If-None-Match, so no
// conditional-write key applies to any object in it.
condCtx := requestConditionContext(ctx, nil)
keys := make([]string, len(objects))
for i, obj := range objects {
if obj.Key != nil {
keys[i] = *obj.Key
}
}
errs := make([]error, len(objects))
// Authorization doesn't apply to root, admin, or a public-bucket
// request — errs stays all-nil from policy's perspective, and object
// locks still apply to them, so the loop below runs either way.
if authorizationApplies(opts) {
var plainIdx, versionedIdx []int
for i, obj := range objects {
if obj.VersionId != nil && *obj.VersionId != "" {
versionedIdx = append(versionedIdx, i)
} else {
plainIdx = append(plainIdx, i)
}
}
if err := authorizeObjectSubset(rctx, be, opts, keys, plainIdx, DeleteObjectAction, errs, condCtx); err != nil {
return nil, err
}
if err := authorizeObjectSubset(rctx, be, opts, keys, versionedIdx, DeleteObjectVersionAction, errs, condCtx); err != nil {
return nil, err
}
}
lockState, err := loadObjectLockState(rctx, be, opts.Bucket, false)
if err != nil {
return nil, err
}
if lockState.applies {
for i, obj := range objects {
if errs[i] != nil {
// Already denied by policy — no need to also resolve this
// object's lock state, and a lock error here would only
// overwrite the more specific policy denial.
continue
}
if err := lockState.checkObject(rctx, be, opts.Iam, opts.Acc, opts.Bucket, obj, bypass, opts.IsPublicRequest, condCtx); err != nil {
errs[i] = err
}
}
}
return errs, nil
}
// authorizeObjectSubset runs objectsAccessErrors for the objects at idx (a
// subset of keys, given by original index) against a single action, and
// scatters the results back into errs at their original positions. Splitting
// DeleteObjects' batch into one group per action this way keeps the
// round-trip count at one per distinct action in the batch — normally one or
// two — rather than one per object.
func authorizeObjectSubset(ctx context.Context, be backend.Backend, opts AccessOptions, keys []string, idx []int, action Action, errs []error, condCtx map[string][]string) error {
if len(idx) == 0 {
return nil
}
subKeys := make([]string, len(idx))
for i, origIdx := range idx {
subKeys[i] = keys[origIdx]
}
subOpts := opts
subOpts.Actions = []Action{action}
subErrs, err := objectsAccessErrors(ctx, be, subOpts, subKeys, condCtx)
if err != nil {
return err
}
for i, origIdx := range idx {
errs[origIdx] = subErrs[i]
}
return nil
}
// verifyAccessGates applies the checks that depend on gateway configuration
// rather than on the caller's policies. Readonly mode blocks writes for
// everyone, root and admin included, which is why it runs before any bypass.
func verifyAccessGates(opts AccessOptions) error {
if opts.Readonly {
if opts.AclPermission == PermissionWrite || opts.AclPermission == PermissionWriteAcp {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
}
return nil
}
// authorizationApplies reports whether policy/ACL evaluation is meaningful
// for this caller at all. It is not for an anonymous request to a public
// bucket (already authorized by the public-access check) nor for root/admin
// (who bypass policy entirely — though not object locks).
func authorizationApplies(opts AccessOptions) bool {
return !opts.IsPublicRequest && !opts.IsRoot && opts.Acc.Role != RoleAdmin
}
// objectsAccessErrors evaluates every key against the bucket's resource
// policy and ACL and the caller's identity policy, returning one result per
// key: nil where the key is authorized, and the AWS-shaped denial otherwise.
// The returned slice always has one entry per key.
//
// The keys are evaluated as one batch, not one VerifyAccess call each: the
// bucket policy is fetched once, and the identity policy is evaluated for
// every key in a single round trip to the IAM service. A per-key loop would
// cost a backend call and a network round trip per object, and DeleteObjects
// accepts up to 1000 of them.
//
// Within a key, each action is authorized on its own: it is allowed when
// either the bucket policy or the identity policy allows it, and the key is
// allowed when every action is. A tagged PutObject whose s3:PutObject comes
// from the bucket policy and whose s3:PutObjectTagging comes from the
// identity policy is therefore allowed, though neither source allows both.
func objectsAccessErrors(ctx context.Context, be backend.Backend, opts AccessOptions, keys []string, condCtx map[string][]string) ([]error, error) {
if len(opts.Actions) == 0 {
// With every action decided on its own, no actions would leave
// nothing to deny. Fail closed rather than allow vacuously.
return nil, errors.New("no actions to authorize")
}
resourceDecisions, err := verifyResourceAccess(ctx, be, opts, keys, condCtx)
if err != nil {
return nil, err
}
errs := make([]error, len(keys))
// An explicit deny from the bucket policy wins outright, whatever the
// IAM backend is and whatever an identity policy would have said, so
// every denied key is settled here and never revisited below. Each key
// is settled on its own: this is a partial-success API, so a deny on one
// key says nothing about the next one, which still has to be evaluated
// on its own merits.
allDenied := true
for i, perAction := range resourceDecisions {
if j := slices.Index(perAction, policyDecisionDeny); j >= 0 {
errs[i] = s3err.GetExplicitDenyAccessErr(principalName(opts.Acc), string(opts.Actions[j]), objectPolicyArn(opts.Bucket, keys[i], be.NormalizeObjectKey), "a resource-based policy")
continue
}
allDenied = false
}
// Nothing is left to decide, so skip the identity policy entirely —
// which also saves the standalone IAM service round trip. That shortcut
// only holds when the bucket policy denied every key; a single
// undecided key still needs the identity policy consulted for it.
if allDenied {
return errs, nil
}
pe, hasPolicyEvaluator := opts.Iam.(PolicyEvaluator)
if !hasPolicyEvaluator {
// No identity-policy layer exists for this backend at all: preserve
// today's exact behavior and generic message, unconditionally, for
// every internal/LDAP/Vault/IPA/S3-IAM deployment.
for i, perAction := range resourceDecisions {
if errs[i] != nil {
// Explicitly denied above — keep that specific message
// rather than flattening it to the generic one.
continue
}
if slices.ContainsFunc(perAction, func(d policyDecision) bool { return d != policyDecisionAllow }) {
errs[i] = s3err.GetAPIError(s3err.ErrAccessDenied)
}
}
return errs, nil
}
identity, err := identityPolicyDecisions(pe, opts, keys, be.NormalizeObjectKey, condCtx)
if err != nil {
return nil, err
}
principal := identity.PrincipalArn
if principal == "" {
principal = principalName(opts.Acc)
}
for i := range keys {
if errs[i] != nil {
// Explicitly denied by the bucket policy. An explicit deny is
// final, so no identity-policy result can clear it, and the
// resource-based message is the one AWS reports for it.
continue
}
resourceArn := objectPolicyArn(opts.Bucket, keys[i], be.NormalizeObjectKey)
if j := slices.Index(identity.Decisions[i], policyDecisionDeny); j >= 0 {
errs[i] = s3err.GetExplicitDenyAccessErr(principal, string(opts.Actions[j]), resourceArn, "an identity-based policy")
continue
}
if identity.HasSessionPolicy {
if j := slices.Index(identity.SessionDecisions[i], policyDecisionDeny); j >= 0 {
errs[i] = s3err.GetExplicitDenyAccessErr(principal, string(opts.Actions[j]), resourceArn, "an identity-based policy")
continue
}
}
// The first action neither source allows is the one the denial
// names, which matches AWS: a tagged PutObject missing both actions
// names s3:PutObject, and one missing only the tagging action names
// s3:PutObjectTagging.
for j, action := range opts.Actions {
granted := resourceDecisions[i][j] == policyDecisionAllow ||
identity.Decisions[i][j] == policyDecisionAllow
// A session policy filters everything the session can do —
// including what the bucket policy granted it, not just what the
// role's own policies did: a role with no identity policy at
// all, a bucket policy granting it both s3:GetObject and
// s3:PutObject, and a session policy allowing only s3:GetObject
// yields a successful Get and a denied Put.
if identity.HasSessionPolicy && identity.SessionDecisions[i][j] != policyDecisionAllow {
granted = false
}
if !granted {
errs[i] = s3err.GetImplicitDenyAccessErr(principal, string(action), resourceArn)
break
}
}
}
return errs, nil
}
// principalName is how a denial message names acc: by its principal ARN
// where the IAM backend gives it one, and by its access key id otherwise —
// the only name the other backends have for it.
func principalName(acc Account) string {
if acc.Arn != "" {
return acc.Arn
}
return acc.Access
}
// verifyResourceAccess combines the bucket's policy and ACL for every action
// on each object key, returning decisions[i][j] for objects[i] and
// opts.Actions[j]. The bucket policy is fetched and parsed once regardless
// of how many keys there are. An explicit Deny in the policy wins;
// otherwise the action is allowed when either the policy or the ACL allows
// it. The ACL can never deny, and it grants on the whole bucket rather than
// on an object, so its verdict for an action is shared by every key.
//
// The bucket owner may always read, replace and delete the bucket's
// policy, whatever the policy says, as an AWS account's root user may on
// its own bucket. No policy can lock the owner out of repairing it. Root
// and admin don't need this, because they bypass policy altogether.
func verifyResourceAccess(ctx context.Context, be backend.Backend, opts AccessOptions, objects []string, condCtx map[string][]string) ([][]policyDecision, error) {
policy, err := be.GetBucketPolicy(ctx, opts.Bucket)
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
return nil, err
}
hasPolicy := err == nil
var bp BucketPolicy
if hasPolicy {
if err := json.Unmarshal(policy, &bp); err != nil {
return nil, fmt.Errorf("failed to parse the bucket policy: %w", err)
}
}
isOwner := opts.Acc.Access == opts.Acl.Owner
aclDecisions := make([]policyDecision, len(opts.Actions))
for j, action := range opts.Actions {
if aclAllows(opts.Acl, opts.Acc.Access, action, opts.DisableACL) {
aclDecisions[j] = policyDecisionAllow
}
}
decisions := make([][]policyDecision, len(objects))
for i, object := range objects {
if !hasPolicy {
decisions[i] = aclDecisions
continue
}
resource := makePolicyResource(opts.Bucket, object, be.NormalizeObjectKey)
decisions[i] = make([]policyDecision, len(opts.Actions))
for j, action := range opts.Actions {
decision := bp.decisionFor(opts.Acc, action, resource, condCtx, be.NormalizeObjectKey)
switch {
case isOwner && isBucketPolicyAction(action):
decision = policyDecisionAllow
case decision == policyDecisionNoMatch:
decision = aclDecisions[j]
}
decisions[i][j] = decision
}
}
return decisions, nil
}
// isBucketPolicyAction reports whether action reads, replaces or deletes
// the bucket policy itself.
func isBucketPolicyAction(action Action) bool {
switch action {
case GetBucketPolicyAction, PutBucketPolicyAction, DeleteBucketPolicyAction:
return true
}
return false
}
// identityPolicyDecisions evaluates every action in opts.Actions against
// every object key, all in a single request. The result keeps one decision
// per action — Decisions[i][j] for objects[i] and opts.Actions[j], and the
// same for SessionDecisions when HasSessionPolicy is set — so the caller can
// combine each action with the bucket policy's decision for that same
// action. PrincipalArn is shared across the whole batch since one call
// always evaluates a single identity.
func identityPolicyDecisions(pe PolicyEvaluator, opts AccessOptions, objects []string, normalizeObjectKey objectKeyNormalizer, condition map[string][]string) (PolicyEvaluation, error) {
resources := make([]string, len(objects))
for i, object := range objects {
resources[i] = objectPolicyArn(opts.Bucket, object, normalizeObjectKey)
}
eval, err := pe.EvaluatePolicy(opts.Acc.Access, opts.Acc.SessionToken, opts.Actions, resources, condition)
if err != nil {
return PolicyEvaluation{}, err
}
if err := checkDecisionMatrix(eval.Decisions, len(resources), len(opts.Actions)); err != nil {
return PolicyEvaluation{}, err
}
if eval.HasSessionPolicy {
if err := checkDecisionMatrix(eval.SessionDecisions, len(resources), len(opts.Actions)); err != nil {
return PolicyEvaluation{}, err
}
}
return eval, nil
}
// checkDecisionMatrix confirms matrix holds a decision for every resource
// and action that was asked about. A mismatch is a protocol mismatch
// between the gateway and IAM service builds — fail closed rather than
// authorizing a key or action nobody evaluated.
func checkDecisionMatrix(matrix [][]policyDecision, resources, actions int) error {
if len(matrix) != resources {
return fmt.Errorf("evaluate policy returned %d resource decisions for %d resources", len(matrix), resources)
}
for _, perAction := range matrix {
if len(perAction) != actions {
return fmt.Errorf("evaluate policy returned %d action decisions for %d actions", len(perAction), actions)
}
}
return nil
}
// objectPolicyArn builds the ARN a policy statement is matched against for
// one bucket/object pair — the bucket's own ARN when object is empty.
func objectPolicyArn(bucket, object string, normalizeObjectKey objectKeyNormalizer) string {
return ResourceArnPrefix + makePolicyResource(bucket, object, normalizeObjectKey)
}
// VerifyPublicAccess checks that the bucket grants every one of actions to
// anonymous requesters. Each action is granted on its own, by the bucket
// policy or, failing that, the bucket ACL, and the request needs them all:
// a tagged upload to a public-read-write bucket whose policy publicly grants
// s3:PutObjectTagging is allowed, the ACL granting its s3:PutObject. An
// explicit public Deny of any one action denies the request, whatever the
// ACL grants.
func VerifyPublicAccess(ctx fiber.Ctx, be backend.Backend, actions []Action, permission Permission, bucket, object string) error {
policy, err := be.GetBucketPolicy(ctx.RequestCtx(), bucket)
if err != nil && !errors.Is(err, s3err.GetAPIError(s3err.ErrNoSuchBucketPolicy)) {
return err
}
hasPolicy := err == nil
condCtx := requestConditionContext(ctx, actions)
var aclFallback bool
for _, action := range actions {
if hasPolicy {
err := VerifyPublicBucketPolicy(policy, bucket, object, condCtx, be.NormalizeObjectKey, action)
if errors.Is(err, errExplicitDeny) {
// Explicit public-policy Deny has higher precedence than any
// public ACL grant, so do not continue to ACL fallback.
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
if err == nil {
// if ACLs are disabled, and the bucket grants public access,
// policy actions should return 'MethodNotAllowed'
switch action {
case GetBucketPolicyAction:
return s3err.GetMethodNotAllowedErr(http.MethodGet, s3err.ResourceTypeBucketPolicy, nil)
case PutBucketPolicyAction:
return s3err.GetMethodNotAllowedErr(http.MethodPut, s3err.ResourceTypeBucketPolicy, nil)
case DeleteBucketPolicyAction:
return s3err.GetMethodNotAllowedErr(http.MethodDelete, s3err.ResourceTypeBucketPolicy, nil)
}
continue
}
}
// an action no ACL grant can authorize is denied
if _, ok := aclActionPermissions[action]; !ok {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
aclFallback = true
}
if !aclFallback {
return nil
}
// An ACL grants a permission on the whole bucket rather than an action
// on an object, so one lookup decides every action left to it.
if err := VerifyPublicBucketACL(ctx.RequestCtx(), be, bucket, permission); err != nil {
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
return nil
}
// VerifyCreateBucketAccess decides whether acc may create a bucket named
// bucket. Unlike VerifyAccess, the bucket doesn't exist yet at this point,
// so there is no bucket policy or ACL to consult — root/admin always
// bypass, and otherwise authorization comes from whichever mechanism the
// configured iam backend actually supports: for backends that implement
// PolicyEvaluator (currently only the standalone IAM service client), an
// identity-policy Allow for s3:CreateBucket grants access, exactly like any
// other IAM-policy-gated action; the legacy userplus-role bypass applies
// only to backends with no such policy layer (internal/LDAP/Vault/IPA/S3-IAM),
// since those have no other way to grant a plain "user" account this
// permission.
func VerifyCreateBucketAccess(ctx fiber.Ctx, iam IAMService, isRoot bool, acc Account, bucket string) error {
if isRoot || acc.Role == RoleAdmin {
return nil
}
pe, hasPolicyEvaluator := iam.(PolicyEvaluator)
if !hasPolicyEvaluator {
if acc.Role == RoleUserPlus {
return nil
}
return s3err.GetAPIError(s3err.ErrAccessDenied)
}
return verifyIdentityOnlyAccess(ctx, pe, acc, CreateBucketAction, bucket)
}
// VerifyListAllMyBucketsAccess decides whether acc may list buckets. The
// request names no bucket, so only identity policies apply: an Allow for
// s3:ListAllMyBuckets on "arn:aws:s3:::*", the ARN AWS's own bucket-listing
// policy names. Backends with no identity-policy layer already narrow the
// listing to the caller's own buckets, so they need no permission of their own.
func VerifyListAllMyBucketsAccess(ctx fiber.Ctx, iam IAMService, isRoot bool, acc Account) error {
if isRoot || acc.Role == RoleAdmin {
return nil
}
pe, hasPolicyEvaluator := iam.(PolicyEvaluator)
if !hasPolicyEvaluator {
return nil
}
return verifyIdentityOnlyAccess(ctx, pe, acc, ListAllMyBucketsAction, "*")
}
// verifyIdentityOnlyAccess decides one action from the caller's identity
// policies alone, for requests naming no existing bucket and therefore no
// resource-based policy. resource is the ARN part after "arn:aws:s3:::": a
// bucket name for CreateBucket, "*" for an account-level action.
func verifyIdentityOnlyAccess(ctx fiber.Ctx, pe PolicyEvaluator, acc Account, action Action, resource string) error {
resourceArn := ResourceArnPrefix + resource
identity, err := identityPolicyDecisions(pe, AccessOptions{
Acc: acc,
Bucket: resource,
Actions: []Action{action},
}, []string{""}, nil, requestConditionContext(ctx, []Action{action}))
if err != nil {
return err
}
principal := identity.PrincipalArn
if principal == "" {
principal = principalName(acc)
}
// A session policy narrows what the session may do; there is no resource
// policy to combine with here, so the two decisions simply intersect.
decision := identity.Decisions[0][0]
if identity.HasSessionPolicy {
switch sd := identity.SessionDecisions[0][0]; {
case sd == policyDecisionDeny:
decision = policyDecisionDeny
case sd != policyDecisionAllow && decision == policyDecisionAllow:
decision = policyDecisionNoMatch
}
}
switch decision {
case policyDecisionDeny:
return s3err.GetExplicitDenyAccessErr(principal, string(action), resourceArn, "an identity-based policy")
case policyDecisionAllow:
return nil
}
return s3err.GetImplicitDenyAccessErr(principal, string(action), resourceArn)
}