mirror of
https://github.com/pgsty/minio.git
synced 2026-11-09 17:21:51 +03:00
303e340292
A batch job ran with the server's object access, whoever submitted it, and acted on versions that Object Lock protects. Submitter: StartBatchJob binds the authenticated credential to the job (type, access key, parent, identity revision and a keyed fingerprint). Submission, every start and every object resolve the binding against IAM and evaluate the action with the submitter's policies and session policy. A credential that is gone, disabled, expired or changed fails the job. A policy statement that would need request context the job does not have refuses the action. With an authorization plugin, the plugin and the session policy must both allow. A refusal is persisted as the job's failure reason and ends the job. A job stored without a binding fails and has to be submitted again; a definition holds one job type. Expire deletes each older version on its own, checking retention and legal hold and authorizing it under the delete's lock; a locked version is left and counted. Until the exact-key purge is integrated, a retainVersions: 0 key is refused when a version the walk saw is locked. Pull writes refuse to replace a locked destination version, use the destination's versioning, validate copied retention and apply default retention. Rotation leaves locked versions as they are. The status reports versionsSkippedLocked, keysRefused and the failure reason. Expire and rotation persist their cursor together with these counts, so a resumed job counts each version once. Describe returns a redacted copy of the job. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Feng Ruohang <rh@vonng.com>
743 lines
24 KiB
Go
743 lines
24 KiB
Go
// Copyright (c) 2026 PGSTY
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/minio/minio-go/v7/pkg/tags"
|
|
"github.com/minio/minio/internal/arn"
|
|
"github.com/minio/minio/internal/auth"
|
|
"github.com/pgsty/silo-pkg/v3/policy"
|
|
"github.com/pgsty/silo-pkg/v3/policy/condition"
|
|
)
|
|
|
|
// A batch job acts on objects long after the request that submitted it has
|
|
// gone. It therefore records who submitted it and acts only as that
|
|
// principal:
|
|
//
|
|
// - StartBatchJob binds the authenticated credential to the job
|
|
// (batchJobSubmitter). Nothing in the submitted definition names it.
|
|
// - Every start of the job, and every object it touches, resolves the
|
|
// binding against IAM again. A credential that is gone, disabled,
|
|
// expired, revoked or edited since submission fails the job; a reused
|
|
// access key does not match the binding.
|
|
// - Each action is evaluated with the submitter's policies and session
|
|
// policy but without a request. A policy statement that would need
|
|
// request context the job does not have (source address, transport,
|
|
// headers and so on) refuses the action instead of being skipped.
|
|
//
|
|
// A job without a binding was queued by an earlier release. It is never
|
|
// authorized, whoever submitted it, and has to be submitted again.
|
|
|
|
//go:generate msgp -file $GOFILE -unexported
|
|
|
|
// batchJobSubmitter is the server-assigned identity of the credential that
|
|
// submitted a batch job.
|
|
type batchJobSubmitter struct {
|
|
Version int `msg:"v"`
|
|
Type string `msg:"t"`
|
|
AccessKey string `msg:"ak"`
|
|
// ParentUser is the parent of a derived credential.
|
|
ParentUser string `msg:"pu"`
|
|
// UpdatedAt and RevokedBefore are the revision of the identity record;
|
|
// any edit of the identity changes them. They are unset for root.
|
|
UpdatedAt time.Time `msg:"ua"`
|
|
RevokedBefore time.Time `msg:"rb"`
|
|
// Binding is a keyed fingerprint of the credential itself, see
|
|
// batchSubmitterBinding.
|
|
Binding []byte `msg:"bd"`
|
|
}
|
|
|
|
const batchJobSubmitterVersion = 1
|
|
|
|
// Submitter types. A credential is classified in the order IsAllowed
|
|
// evaluates it.
|
|
const (
|
|
batchSubmitterRoot = "root"
|
|
batchSubmitterUser = "user"
|
|
batchSubmitterTemporary = "temporary"
|
|
batchSubmitterService = "service-account"
|
|
)
|
|
|
|
//msgp:ignore batchJobAuthzError
|
|
|
|
// batchJobAuthzError refuses a batch job, or one of its actions, for its
|
|
// submitter. It ends the job: it is persisted as the job's failure and is
|
|
// never retried.
|
|
type batchJobAuthzError struct {
|
|
reason string
|
|
}
|
|
|
|
func (e batchJobAuthzError) Error() string {
|
|
return e.reason
|
|
}
|
|
|
|
func isBatchJobAuthzError(err error) bool {
|
|
var e batchJobAuthzError
|
|
return errors.As(err, &e)
|
|
}
|
|
|
|
var (
|
|
errBatchJobResubmit = batchJobAuthzError{reason: "the batch job does not record who submitted it; submit it again"}
|
|
errBatchJobChanged = batchJobAuthzError{reason: "the credential that submitted the batch job has changed since submission; submit it again"}
|
|
errBatchJobInvalid = batchJobAuthzError{reason: "the credential that submitted the batch job is no longer valid"}
|
|
)
|
|
|
|
func batchSubmitterType(cred auth.Credentials) string {
|
|
switch {
|
|
case cred.IsTemp():
|
|
return batchSubmitterTemporary
|
|
case cred.IsServiceAccount():
|
|
return batchSubmitterService
|
|
default:
|
|
return batchSubmitterUser
|
|
}
|
|
}
|
|
|
|
// batchSubmitterBinding is a fingerprint of one credential keyed with its own
|
|
// secret. It changes when the secret, the session token or the parent
|
|
// changes, and a new credential that reuses the access key does not have it.
|
|
func batchSubmitterBinding(typ string, cred auth.Credentials) []byte {
|
|
mac := hmac.New(sha256.New, []byte(cred.SecretKey))
|
|
for _, s := range []string{"silo batch-job submitter v1", typ, cred.AccessKey, cred.ParentUser, cred.SessionToken} {
|
|
var n [8]byte
|
|
binary.BigEndian.PutUint64(n[:], uint64(len(s)))
|
|
mac.Write(n[:])
|
|
mac.Write([]byte(s))
|
|
}
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
// identityRecord returns the stored identity of a non-root access key.
|
|
func (sys *IAMSys) identityRecord(accessKey string) (UserIdentity, bool, error) {
|
|
if !sys.Initialized() {
|
|
return UserIdentity{}, false, errServerNotInitialized
|
|
}
|
|
u, ok := sys.store.GetUser(accessKey)
|
|
return u, ok, nil
|
|
}
|
|
|
|
// newBatchJobSubmitter binds the credential that authenticated a
|
|
// StartBatchJob request.
|
|
func newBatchJobSubmitter(cred auth.Credentials) (*batchJobSubmitter, error) {
|
|
root := globalActiveCred
|
|
if cred.AccessKey == "" {
|
|
return nil, errBatchJobInvalid
|
|
}
|
|
if cred.AccessKey == root.AccessKey {
|
|
if subtle.ConstantTimeCompare([]byte(cred.SecretKey), []byte(root.SecretKey)) != 1 {
|
|
return nil, errBatchJobChanged
|
|
}
|
|
return &batchJobSubmitter{
|
|
Version: batchJobSubmitterVersion,
|
|
Type: batchSubmitterRoot,
|
|
AccessKey: root.AccessKey,
|
|
Binding: batchSubmitterBinding(batchSubmitterRoot, root),
|
|
}, nil
|
|
}
|
|
u, ok, err := globalIAMSys.identityRecord(cred.AccessKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The record must be the one the request was authenticated with.
|
|
if !ok || u.Credentials.AccessKey != cred.AccessKey || u.Credentials.ParentUser != cred.ParentUser ||
|
|
subtle.ConstantTimeCompare([]byte(u.Credentials.SecretKey), []byte(cred.SecretKey)) != 1 ||
|
|
subtle.ConstantTimeCompare([]byte(u.Credentials.SessionToken), []byte(cred.SessionToken)) != 1 {
|
|
return nil, errBatchJobChanged
|
|
}
|
|
typ := batchSubmitterType(u.Credentials)
|
|
return &batchJobSubmitter{
|
|
Version: batchJobSubmitterVersion,
|
|
Type: typ,
|
|
AccessKey: u.Credentials.AccessKey,
|
|
ParentUser: u.Credentials.ParentUser,
|
|
UpdatedAt: u.UpdatedAt,
|
|
RevokedBefore: u.RevokedBefore,
|
|
Binding: batchSubmitterBinding(typ, u.Credentials),
|
|
}, nil
|
|
}
|
|
|
|
//msgp:ignore batchJobPrincipal batchAuthzRequest
|
|
|
|
// batchJobPrincipal is a submitter resolved against current IAM state.
|
|
type batchJobPrincipal struct {
|
|
typ string
|
|
accessKey string
|
|
parentUser string
|
|
groups []string
|
|
claims map[string]any
|
|
owner bool
|
|
temp bool // evaluated as a temporary credential
|
|
service bool // evaluated as a service account
|
|
}
|
|
|
|
// principal resolves the binding against current IAM state. It fails if the
|
|
// credential no longer exists, is no longer valid, or is no longer the one
|
|
// that was bound.
|
|
func (s *batchJobSubmitter) principal(ctx context.Context) (batchJobPrincipal, error) {
|
|
if s == nil || s.AccessKey == "" {
|
|
return batchJobPrincipal{}, errBatchJobResubmit
|
|
}
|
|
if s.Version != batchJobSubmitterVersion {
|
|
return batchJobPrincipal{}, batchJobAuthzError{reason: fmt.Sprintf("unsupported batch job submitter version %d; submit the job again", s.Version)}
|
|
}
|
|
root := globalActiveCred
|
|
if s.Type == batchSubmitterRoot {
|
|
if s.AccessKey != root.AccessKey || !hmac.Equal(s.Binding, batchSubmitterBinding(batchSubmitterRoot, root)) {
|
|
return batchJobPrincipal{}, errBatchJobChanged
|
|
}
|
|
if !globalAPIConfig.permitRootAccess() {
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
return batchJobPrincipal{typ: batchSubmitterRoot, accessKey: root.AccessKey, owner: true}, nil
|
|
}
|
|
switch s.Type {
|
|
case batchSubmitterUser, batchSubmitterTemporary, batchSubmitterService:
|
|
default:
|
|
return batchJobPrincipal{}, errBatchJobResubmit
|
|
}
|
|
if s.AccessKey == root.AccessKey {
|
|
return batchJobPrincipal{}, errBatchJobChanged
|
|
}
|
|
u, ok, err := globalIAMSys.CheckKey(ctx, s.AccessKey)
|
|
if err != nil {
|
|
return batchJobPrincipal{}, err
|
|
}
|
|
if !ok {
|
|
// Deleted, disabled, expired or revoked.
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
cred := u.Credentials
|
|
typ := batchSubmitterType(cred)
|
|
if typ != s.Type || cred.AccessKey != s.AccessKey || cred.ParentUser != s.ParentUser ||
|
|
!u.UpdatedAt.Equal(s.UpdatedAt) || !u.RevokedBefore.Equal(s.RevokedBefore) ||
|
|
!hmac.Equal(s.Binding, batchSubmitterBinding(typ, cred)) {
|
|
return batchJobPrincipal{}, errBatchJobChanged
|
|
}
|
|
if cred.IsExpired() || cred.Status == auth.AccountOff {
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
p := batchJobPrincipal{
|
|
typ: typ,
|
|
accessKey: cred.AccessKey,
|
|
parentUser: cred.ParentUser,
|
|
groups: cred.Groups,
|
|
claims: map[string]any{},
|
|
temp: cred.IsTemp(),
|
|
service: !cred.IsTemp() && cred.IsServiceAccount(),
|
|
}
|
|
if typ != batchSubmitterUser {
|
|
// Verify the token as a request would, with the current signing keys.
|
|
jwtClaims, err := extractJWTClaims(u)
|
|
if err != nil {
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
p.claims = jwtClaims.Map()
|
|
if err := decodeBatchSessionPolicy(p.claims); err != nil {
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
}
|
|
p.owner = cred.ParentUser == root.AccessKey && cred.AccessKey != siteReplicatorSvcAcc
|
|
if p.owner && !globalAPIConfig.permitRootAccess() {
|
|
return batchJobPrincipal{}, errBatchJobInvalid
|
|
}
|
|
if _, ok := p.claims[policy.SessionPolicyName]; ok {
|
|
p.owner = false
|
|
}
|
|
return p, nil
|
|
}
|
|
|
|
// decodeBatchSessionPolicy decodes a session policy claim for evaluation.
|
|
// A request decodes it while verifying its token, except when an
|
|
// authorization plugin is configured; a batch job decodes it always, so the
|
|
// session policy also limits what the plugin allows.
|
|
func decodeBatchSessionPolicy(claims map[string]any) error {
|
|
sp, ok := claims[policy.SessionPolicyName]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
s, ok := sp.(string)
|
|
if !ok {
|
|
return errAuthentication
|
|
}
|
|
b, err := base64.StdEncoding.DecodeString(s)
|
|
if err != nil {
|
|
return errAuthentication
|
|
}
|
|
claims[sessionPolicyNameExtracted] = string(b)
|
|
return nil
|
|
}
|
|
|
|
// batchAuthzRequest is one action a batch job is about to take.
|
|
type batchAuthzRequest struct {
|
|
action policy.Action
|
|
bucket string
|
|
object string // empty for a bucket action
|
|
// versionID is the version an object version action addresses.
|
|
versionID string
|
|
// tags are the current tags of the object version acted on.
|
|
tags string
|
|
// tagsUnknown is set when the job does not read the version it acts
|
|
// on, so s3:ExistingObjectTag is context it does not have.
|
|
tagsUnknown bool
|
|
// values are request values the job supplies itself, such as the
|
|
// listing prefix or the encryption and retention it writes, keyed by
|
|
// condition key name.
|
|
values map[string][]string
|
|
}
|
|
|
|
// batchAuthzMarker marks the condition values of a batch job for an
|
|
// authorization plugin. Requests cannot set s3:authType.
|
|
const batchAuthzMarker = "batch-job"
|
|
|
|
// Condition keys a batch job always provides, as a request would: identity,
|
|
// time, the addressed version and the stored tags. Keys a request takes from
|
|
// itself are provided only when the job sets them (see batchAuthzRequest
|
|
// values); every other key is request context the job does not have.
|
|
var batchServerConditionKeys = map[string]bool{
|
|
condition.AWSCurrentTime.ToKey().String(): true,
|
|
condition.AWSEpochTime.ToKey().String(): true,
|
|
condition.AWSPrincipalType.ToKey().String(): true,
|
|
condition.AWSUserID.ToKey().String(): true,
|
|
condition.AWSUsername.ToKey().String(): true,
|
|
condition.AWSGroups.ToKey().String(): true,
|
|
condition.S3VersionID.ToKey().String(): true,
|
|
condition.ExistingObjectTag.ToKey().String(): true,
|
|
}
|
|
|
|
// Request keys a job may supply, by the name they have in condition values.
|
|
var batchRequestConditionKeys = map[string]condition.KeyName{
|
|
condition.S3Prefix.Name(): condition.S3Prefix,
|
|
condition.S3XAmzServerSideEncryption.Name(): condition.S3XAmzServerSideEncryption,
|
|
condition.S3XAmzServerSideEncryptionAwsKmsKeyID.Name(): condition.S3XAmzServerSideEncryptionAwsKmsKeyID,
|
|
condition.S3ObjectLockMode.Name(): condition.S3ObjectLockMode,
|
|
condition.S3ObjectLockRetainUntilDate.Name(): condition.S3ObjectLockRetainUntilDate,
|
|
condition.S3ObjectLockLegalHold.Name(): condition.S3ObjectLockLegalHold,
|
|
condition.RequestObjectTagKeys.Name(): condition.RequestObjectTagKeys,
|
|
}
|
|
|
|
// suppliedConditionKeys returns the condition keys whose values req provides.
|
|
func (req batchAuthzRequest) suppliedConditionKeys() map[string]bool {
|
|
supplied := make(map[string]bool, len(batchServerConditionKeys)+len(req.values))
|
|
for k := range batchServerConditionKeys {
|
|
supplied[k] = true
|
|
}
|
|
if req.tagsUnknown {
|
|
delete(supplied, condition.ExistingObjectTag.ToKey().String())
|
|
}
|
|
for name := range req.values {
|
|
if key, ok := batchRequestConditionKeys[name]; ok {
|
|
supplied[key.ToKey().String()] = true
|
|
}
|
|
if strings.HasPrefix(name, condition.RequestObjectTag.Name()+"/") {
|
|
supplied[condition.RequestObjectTag.ToKey().String()] = true
|
|
}
|
|
}
|
|
return supplied
|
|
}
|
|
|
|
func batchKeySupplied(supplied map[string]bool, name string) bool {
|
|
if i := strings.IndexByte(name, '/'); i >= 0 {
|
|
name = name[:i]
|
|
}
|
|
if strings.HasPrefix(name, "jwt:") || strings.HasPrefix(name, "ldap:") {
|
|
// Claims of the verified token, as a request has them.
|
|
return true
|
|
}
|
|
return supplied[name]
|
|
}
|
|
|
|
// conditionValues are the values a request with the same identity would
|
|
// carry, without any value taken from the request itself.
|
|
func (p batchJobPrincipal) conditionValues(req batchAuthzRequest) map[string][]string {
|
|
now := UTCNow()
|
|
username := p.accessKey
|
|
if p.temp || p.service {
|
|
username = p.parentUser
|
|
}
|
|
principalType := "User"
|
|
if len(p.claims) > 0 {
|
|
principalType = "AssumedRole"
|
|
}
|
|
if username == globalActiveCred.AccessKey {
|
|
principalType = "Account"
|
|
}
|
|
// Identity and time, then the claims over them, as a request has them
|
|
// (an LDAP session's short username replaces its parent DN).
|
|
values := map[string][]string{
|
|
"CurrentTime": {now.Format(time.RFC3339)},
|
|
"EpochTime": {strconv.FormatInt(now.Unix(), 10)},
|
|
"principaltype": {principalType},
|
|
"userid": {username},
|
|
"username": {username},
|
|
}
|
|
for k, v := range p.claims {
|
|
if s, ok := v.(string); ok {
|
|
values[strings.ToLower(strings.TrimPrefix(k, "ldap"))] = []string{s}
|
|
}
|
|
}
|
|
// No claim stands for a value of the object or of the job, which
|
|
// follow and always win.
|
|
for k := range values {
|
|
if k == "versionid" || k == "prefix" || k == "authtype" ||
|
|
strings.HasPrefix(k, "existingobjecttag/") || strings.HasPrefix(k, "requestobjecttag") ||
|
|
strings.HasPrefix(k, "x-amz-") || strings.HasPrefix(k, "object-lock-") {
|
|
delete(values, k)
|
|
}
|
|
}
|
|
// For an authorization plugin: this is a batch job, not a request.
|
|
values["authType"] = []string{batchAuthzMarker}
|
|
if req.versionID != "" {
|
|
values["versionid"] = []string{req.versionID}
|
|
}
|
|
if req.tags != "" {
|
|
if t, err := tags.ParseObjectTags(req.tags); err == nil {
|
|
for k, v := range t.ToMap() {
|
|
values[pathJoin("ExistingObjectTag", k)] = []string{v}
|
|
}
|
|
}
|
|
}
|
|
for k, v := range req.values {
|
|
values[k] = v
|
|
}
|
|
if grpsVal, ok := p.claims["groups"]; ok {
|
|
if grpsIs, ok := grpsVal.([]any); ok {
|
|
grps := []string{}
|
|
for _, gI := range grpsIs {
|
|
if g, ok := gI.(string); ok {
|
|
grps = append(grps, g)
|
|
}
|
|
}
|
|
if len(grps) > 0 {
|
|
values["groups"] = grps
|
|
}
|
|
}
|
|
}
|
|
if _, ok := values["groups"]; !ok && len(p.groups) > 0 {
|
|
values["groups"] = p.groups
|
|
}
|
|
return values
|
|
}
|
|
|
|
// authorize evaluates one action of the job as its submitter.
|
|
func (p batchJobPrincipal) authorize(req batchAuthzRequest) error {
|
|
args := policy.Args{
|
|
AccountName: p.accessKey,
|
|
Groups: p.groups,
|
|
Action: req.action,
|
|
BucketName: req.bucket,
|
|
ObjectName: req.object,
|
|
ConditionValues: p.conditionValues(req),
|
|
IsOwner: p.owner,
|
|
Claims: p.claims,
|
|
}
|
|
denied := batchJobAuthzError{reason: batchDeniedReason(req)}
|
|
plugin := newGlobalAuthZPluginFn()
|
|
policies, err := globalIAMSys.batchLocalPolicies(p, args, plugin == nil)
|
|
if err != nil {
|
|
return denied
|
|
}
|
|
supplied := req.suppliedConditionKeys()
|
|
for _, pol := range policies {
|
|
for _, st := range pol.Statements {
|
|
if batchStatementNeedsRequest(st, args, supplied) {
|
|
return batchJobAuthzError{reason: denied.reason + ": a policy statement depends on request context that a batch job does not have"}
|
|
}
|
|
}
|
|
}
|
|
if plugin != nil {
|
|
ok, err := plugin.IsAllowed(args)
|
|
if err != nil {
|
|
authZLogIf(GlobalContext, err)
|
|
}
|
|
if !ok {
|
|
return denied
|
|
}
|
|
// The plugin does not see a session policy; it only limits further.
|
|
if has, allowed := p.sessionPolicyDecision(args); has && !allowed {
|
|
return denied
|
|
}
|
|
return nil
|
|
}
|
|
if !globalIAMSys.isAllowedLocally(args) {
|
|
return denied
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func batchDeniedReason(req batchAuthzRequest) string {
|
|
resource := req.bucket
|
|
if req.object != "" {
|
|
resource = pathJoin(req.bucket, req.object)
|
|
}
|
|
if req.versionID != "" {
|
|
resource += " (versionId " + req.versionID + ")"
|
|
}
|
|
return fmt.Sprintf("the batch job submitter is not allowed %s on %s", req.action, resource)
|
|
}
|
|
|
|
// sessionPolicyDecision evaluates the session policy the way IsAllowed would
|
|
// for this credential.
|
|
func (p batchJobPrincipal) sessionPolicyDecision(args policy.Args) (bool, bool) {
|
|
switch {
|
|
case p.temp:
|
|
return isAllowedBySessionPolicy(args)
|
|
case p.service:
|
|
if p.claims[iamPolicyClaimNameSA()] == inheritedPolicyType {
|
|
return false, false
|
|
}
|
|
return isAllowedBySessionPolicyForServiceAccount(args)
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// batchLocalPolicies returns every local policy that can take part in the
|
|
// decision on args: the mapped policies that IsAllowed would merge when
|
|
// withMapped is set (no authorization plugin), and the session policy.
|
|
func (sys *IAMSys) batchLocalPolicies(p batchJobPrincipal, args policy.Args, withMapped bool) ([]policy.Policy, error) {
|
|
var out []policy.Policy
|
|
if withMapped && args.IsOwner {
|
|
// IsAllowed does not evaluate any policy for the owner.
|
|
return nil, nil
|
|
}
|
|
if withMapped {
|
|
var names []string
|
|
ownerDerived := (p.temp || p.service) && p.parentUser == globalActiveCred.AccessKey
|
|
switch {
|
|
case p.typ == batchSubmitterRoot || ownerDerived:
|
|
case p.temp || p.service:
|
|
if roleArn := args.GetRoleArn(); roleArn != "" {
|
|
a, err := arn.Parse(roleArn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names = append(names, newMappedPolicy(sys.rolesMap[a]).toSlice()...)
|
|
} else {
|
|
mapped, err := sys.PolicyDBGet(p.parentUser, args.Groups...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names = append(names, mapped...)
|
|
// The claimed policies apply only when nothing is mapped.
|
|
if len(mapped) == 0 {
|
|
if claimed, ok := policy.GetPoliciesFromClaims(args.Claims, iamPolicyClaimNameOpenID()); ok {
|
|
names = append(names, claimed.ToSlice()...)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
mapped, err := sys.PolicyDBGet(p.accessKey, args.Groups...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
names = append(names, mapped...)
|
|
}
|
|
if len(names) > 0 {
|
|
out = append(out, sys.GetCombinedPolicy(names...))
|
|
}
|
|
}
|
|
if p.temp || p.service {
|
|
if p.service && p.claims[iamPolicyClaimNameSA()] == inheritedPolicyType {
|
|
return out, nil
|
|
}
|
|
if sp, ok := p.claims[sessionPolicyNameExtracted]; ok {
|
|
s, ok := sp.(string)
|
|
if !ok {
|
|
return nil, errAuthentication
|
|
}
|
|
parsed, err := policy.ParseConfig(strings.NewReader(s))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, *parsed)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// batchStatementNeedsRequest reports whether a statement that applies to
|
|
// args depends on a condition key that the job does not supply. Applying
|
|
// such a statement without its value could grant (Allow) or drop (Deny)
|
|
// what a request with that context would not, so the job refuses the action.
|
|
// Resources are matched without conditions; a statement is taken to apply
|
|
// whenever it might.
|
|
func batchStatementNeedsRequest(st policy.Statement, args policy.Args, supplied map[string]bool) bool {
|
|
if (!st.Actions.Match(args.Action) && !st.Actions.IsEmpty()) || st.NotActions.Match(args.Action) {
|
|
return false
|
|
}
|
|
// Policy variables in resources or condition values.
|
|
if data, err := json.Marshal(st); err == nil {
|
|
s := string(data)
|
|
for {
|
|
i := strings.Index(s, "${")
|
|
if i < 0 {
|
|
break
|
|
}
|
|
s = s[i+2:]
|
|
j := strings.IndexByte(s, '}')
|
|
if j < 0 {
|
|
break
|
|
}
|
|
if !batchKeySupplied(supplied, s[:j]) {
|
|
return true
|
|
}
|
|
s = s[j+1:]
|
|
}
|
|
}
|
|
if !batchStatementMayApply(st, args) {
|
|
return false
|
|
}
|
|
for key := range st.Conditions.Keys() {
|
|
if !batchKeySupplied(supplied, key.String()) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// batchStatementMayApply matches the resources of a statement, in both forms
|
|
// a bucket request can take. A statement whose resources the evaluator
|
|
// translates (S3 Tables and vectors actions cover S3 data actions) is taken
|
|
// to apply.
|
|
func batchStatementMayApply(st policy.Statement, args policy.Args) bool {
|
|
for _, actions := range []policy.ActionSet{st.Actions, st.NotActions} {
|
|
for action := range actions {
|
|
if policy.TableAction(action).IsValid() || policy.VectorsAction(action).IsValid() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
for _, resources := range []policy.ResourceSet{st.Resources, st.NotResources} {
|
|
for r := range resources {
|
|
if r.Type != policy.ResourceARNS3 && r.Type != policy.ResourceARNAll {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
resources := []string{args.BucketName + "/" + strings.TrimPrefix(args.ObjectName, "/")}
|
|
if args.ObjectName == "" {
|
|
resources = []string{args.BucketName, args.BucketName + "/"}
|
|
}
|
|
for _, resource := range resources {
|
|
if len(st.Resources) > 0 && !st.Resources.Match(resource, args.ConditionValues) {
|
|
continue
|
|
}
|
|
if len(st.NotResources) > 0 && st.NotResources.Match(resource, args.ConditionValues) {
|
|
continue
|
|
}
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// authorize resolves the submitter and evaluates each request.
|
|
func (s *batchJobSubmitter) authorize(ctx context.Context, reqs ...batchAuthzRequest) error {
|
|
p, err := s.principal(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, req := range reqs {
|
|
if err := p.authorize(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// startAuthzRequests are the bucket actions a job needs before it acts on any
|
|
// object: the listings it walks. Object actions are authorized per object.
|
|
func (j BatchJobRequest) startAuthzRequests() []batchAuthzRequest {
|
|
list := func(bucket string, prefixes []string) []batchAuthzRequest {
|
|
if len(prefixes) == 0 {
|
|
prefixes = []string{""}
|
|
}
|
|
var reqs []batchAuthzRequest
|
|
for _, prefix := range prefixes {
|
|
values := map[string][]string{condition.S3Prefix.Name(): {prefix}}
|
|
reqs = append(reqs,
|
|
batchAuthzRequest{action: policy.ListBucketAction, bucket: bucket, values: values},
|
|
batchAuthzRequest{action: policy.ListBucketVersionsAction, bucket: bucket, values: values},
|
|
)
|
|
}
|
|
return reqs
|
|
}
|
|
// The same order as Type() and the job pool.
|
|
switch {
|
|
case j.Replicate != nil:
|
|
if !j.Replicate.RemoteToLocal() {
|
|
return list(j.Replicate.Source.Bucket, j.Replicate.Source.Prefix.F())
|
|
}
|
|
case j.KeyRotate != nil:
|
|
return list(j.KeyRotate.Bucket, []string{j.KeyRotate.Prefix})
|
|
case j.Expire != nil:
|
|
return list(j.Expire.Bucket, batchCanonicalPrefixes(j.Expire.Prefix.F()))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// jobSections counts the job types a definition holds. A job is one of them.
|
|
func (j BatchJobRequest) jobSections() int {
|
|
n := 0
|
|
for _, set := range []bool{j.Replicate != nil, j.KeyRotate != nil, j.Expire != nil} {
|
|
if set {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
var errBatchJobSections = batchJobAuthzError{reason: "a batch job must define exactly one job type"}
|
|
|
|
// refuse ends the job for its submitter.
|
|
func (ri *batchJobInfo) refuse(err error) {
|
|
ri.mu.Lock()
|
|
defer ri.mu.Unlock()
|
|
if ri.FailureReason == "" {
|
|
ri.FailureReason = err.Error()
|
|
}
|
|
ri.Complete = false
|
|
ri.Failed = true
|
|
}
|
|
|
|
func (ri *batchJobInfo) refused() bool {
|
|
ri.mu.RLock()
|
|
defer ri.mu.RUnlock()
|
|
return ri.FailureReason != ""
|
|
}
|
|
|
|
// startAuthorized checks the submitter of a job that is about to run. It
|
|
// returns done for a job that must not run now: one refused before, which
|
|
// stays refused, or one refused now, which is persisted as failed first.
|
|
// An error that is not a decision leaves the job to a later start.
|
|
func (ri *batchJobInfo) startAuthorized(ctx context.Context, api ObjectLayer, job BatchJobRequest) (done bool, err error) {
|
|
if !ri.refused() {
|
|
err := job.Submitter.authorize(ctx, job.startAuthzRequests()...)
|
|
if err == nil && job.jobSections() != 1 {
|
|
err = errBatchJobSections
|
|
}
|
|
if err == nil {
|
|
return false, nil
|
|
}
|
|
if !isBatchJobAuthzError(err) {
|
|
return true, err
|
|
}
|
|
ri.refuse(err)
|
|
batchLogIf(ctx, fmt.Errorf("batch job %s refused: %w", job.ID, err))
|
|
}
|
|
globalBatchJobsMetrics.save(job.ID, ri)
|
|
return true, ri.updateAfter(ctx, api, 0, job)
|
|
}
|