Files
zeekay e395fa9439 Remove MinIO, SUBNET and server admin from the client
Vendor the CLI helpers into pkg/, use hanzos3/go, rename the module
to hanzoai/s3-cli, and gate the names in cmd/names_test.go.

Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
2026-09-29 19:37:40 -07:00

281 lines
6.8 KiB
Go

//
// This file is part of Hanzo S3 stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package policy
import (
"bytes"
"encoding/json"
"path"
"strings"
"github.com/hanzoai/s3-cli/pkg/policy/condition"
"github.com/hanzoai/s3-cli/pkg/wildcard"
)
const (
// ResourceARNPrefix - resource S3 ARN prefix as per S3 specification.
ResourceARNPrefix = "arn:aws:s3:::"
// ResourceARNKMSPrefix is for KMS key resources. Hanzo S3 specific API.
ResourceARNKMSPrefix = "arn:s3:kms:::"
)
// ResourceARNType - ARN prefix type
type ResourceARNType uint32
const (
// Zero value for detecting errors
unknownARN ResourceARNType = iota
// ResourceARNS3 is the ARN prefix type for S3 resources.
ResourceARNS3
// ResourceARNKMS is the ARN prefix type for Hanzo S3 KMS resources.
ResourceARNKMS
// ResourceARNAll is the ARN '*'
ResourceARNAll
)
// ARNTypeToPrefix maps the type to prefix string
var ARNTypeToPrefix = map[ResourceARNType]string{
ResourceARNS3: ResourceARNPrefix,
ResourceARNKMS: ResourceARNKMSPrefix,
ResourceARNAll: "*",
}
// ARNPrefixToType maps prefix to types.
var ARNPrefixToType map[string]ResourceARNType
func init() {
ARNPrefixToType = make(map[string]ResourceARNType)
for k, v := range ARNTypeToPrefix {
ARNPrefixToType[v] = k
}
}
func (a ResourceARNType) String() string {
return ARNTypeToPrefix[a]
}
// Resource - resource in policy statement.
type Resource struct {
Pattern string
Type ResourceARNType
}
func (r Resource) isKMS() bool {
return r.Type == ResourceARNKMS || r.Type == ResourceARNAll
}
func (r Resource) isS3() bool {
return r.Type == ResourceARNS3 || r.Type == ResourceARNAll
}
func (r Resource) isBucketPattern() bool {
return !strings.Contains(r.Pattern, "/") || r.Pattern == "*"
}
func (r Resource) isObjectPattern() bool {
return strings.Contains(r.Pattern, "/") || strings.Contains(r.Pattern, "*")
}
// IsValid - checks whether Resource is valid or not.
func (r Resource) IsValid() bool {
if r.Type == unknownARN {
return false
}
if r.isS3() {
if strings.HasPrefix(r.Pattern, "/") {
return false
}
}
if r.isKMS() {
if strings.IndexFunc(r.Pattern, func(c rune) bool {
return c == '/' || c == '\\' || c == '.'
}) >= 0 {
return false
}
}
return r.Pattern != ""
}
// MatchResource matches object name with resource pattern only.
func (r Resource) MatchResource(resource string) bool {
return r.Match(resource, nil)
}
// Match - matches object name with resource pattern, including specific conditionals.
func (r Resource) Match(resource string, conditionValues map[string][]string) bool {
// Happy path, with no replacements
idx := strings.IndexByte(r.Pattern, '$')
if idx < 0 {
if cp := path.Clean(resource); cp != "." && cp == r.Pattern {
return true
}
return wildcard.Match(r.Pattern, resource)
}
// Use a small buffer
pat := smallBufPool.Get().(*bytes.Buffer)
defer smallBufPool.Put(pat)
pat.Reset()
// Do replacement of known keys.
pat.WriteString(r.Pattern[:idx])
remain := r.Pattern[idx:]
for len(remain) > 0 {
val := remain[0]
if val != '$' || len(remain) < 3 {
pat.WriteByte(val)
remain = remain[1:]
continue
}
keyEnds := strings.IndexByte(remain, '}')
// If no curly brackets, emit as-is.
if remain[1] != '{' || keyEnds < 0 {
pat.WriteByte('$')
remain = remain[1:]
continue
}
ckey := condition.KeyName(remain[2:keyEnds])
// Only replace keys we know
if rvalues, ok := conditionValues[ckey.Name()]; condition.CommonKeysMap[ckey] && ok && rvalues[0] != "" {
pat.WriteString(rvalues[0])
} else {
// Write without replacing...
pat.WriteString("${")
pat.WriteString(string(ckey))
pat.WriteString("}")
}
remain = remain[keyEnds+1:]
}
pattern := pat.String()
if cp := path.Clean(resource); cp != "." && cp == pattern {
return true
}
return wildcard.Match(pattern, resource)
}
// MarshalJSON - encodes Resource to JSON data.
func (r Resource) MarshalJSON() ([]byte, error) {
if !r.IsValid() {
return nil, Errorf("invalid resource %v", r)
}
return json.Marshal(r.String())
}
func (r Resource) String() string {
return r.Type.String() + r.Pattern
}
// UnmarshalJSON - decodes JSON data to Resource.
func (r *Resource) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
parsedResource, err := parseResource(s)
if err != nil {
return err
}
*r = parsedResource
return nil
}
// Validate - validates Resource.
func (r Resource) Validate() error {
if !r.IsValid() {
return Errorf("invalid resource")
}
return nil
}
// ValidateBucket - validates that given bucketName is matched by Resource.
func (r Resource) ValidateBucket(bucketName string) error {
if !r.IsValid() {
return Errorf("invalid resource")
}
// For the resource to match the bucket, there are two cases:
//
// 1. the whole resource pattern must match the bucket name (e.g.
// `example*a` matches bucket 'example-east-a'), or
//
// 2. bucket name followed by '/' must match as a prefix of the resource
// pattern (e.g. `example*a` includes resources in a bucket 'example22'
// for example the object `example22/2023/a` is matched by this resource).
if !wildcard.Match(r.Pattern, bucketName) &&
!wildcard.MatchAsPatternPrefix(r.Pattern, bucketName+"/") {
return Errorf("bucket name does not match")
}
return nil
}
// parseResource - parses string to Resource.
func parseResource(s string) (Resource, error) {
r := Resource{}
for k, v := range ARNPrefixToType {
if s == k {
// all pattern
r.Type = ResourceARNAll
r.Pattern = k
continue
}
if rem, ok := strings.CutPrefix(s, k); ok {
r.Type = v
r.Pattern = rem
break
}
}
if r.Type == unknownARN {
return r, Errorf("invalid resource '%v'", s)
}
if strings.HasPrefix(r.Pattern, "/") {
return r, Errorf("invalid resource '%v' - starts with '/' will not match a bucket", s)
}
return r, nil
}
// NewResource - creates new resource with the default ARN type of S3.
func NewResource(pattern string) Resource {
return Resource{
Pattern: pattern,
Type: ResourceARNS3,
}
}
// NewKMSResource - creates new resource with type KMS
func NewKMSResource(pattern string) Resource {
return Resource{
Pattern: pattern,
Type: ResourceARNKMS,
}
}