1
0
Fork 0
mirror of https://github.com/chrislusf/seaweedfs synced 2025-07-25 21:12:47 +02:00
seaweedfs/weed/s3api/policy_engine/engine.go

432 lines
12 KiB
Go

package policy_engine
import (
"fmt"
"net"
"net/http"
"regexp"
"strings"
"sync"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
)
// PolicyEvaluationResult represents the result of policy evaluation
type PolicyEvaluationResult int
const (
PolicyResultDeny PolicyEvaluationResult = iota
PolicyResultAllow
PolicyResultIndeterminate
)
// PolicyEvaluationContext manages policy evaluation for a bucket
type PolicyEvaluationContext struct {
bucketName string
policy *CompiledPolicy
cache *PolicyCache
mutex sync.RWMutex
}
// PolicyEngine is the main policy evaluation engine
type PolicyEngine struct {
contexts map[string]*PolicyEvaluationContext
mutex sync.RWMutex
}
// NewPolicyEngine creates a new policy evaluation engine
func NewPolicyEngine() *PolicyEngine {
return &PolicyEngine{
contexts: make(map[string]*PolicyEvaluationContext),
}
}
// SetBucketPolicy sets the policy for a bucket
func (engine *PolicyEngine) SetBucketPolicy(bucketName string, policyJSON string) error {
policy, err := ParsePolicy(policyJSON)
if err != nil {
return fmt.Errorf("invalid policy: %w", err)
}
compiled, err := CompilePolicy(policy)
if err != nil {
return fmt.Errorf("failed to compile policy: %w", err)
}
engine.mutex.Lock()
defer engine.mutex.Unlock()
context := &PolicyEvaluationContext{
bucketName: bucketName,
policy: compiled,
cache: NewPolicyCache(),
}
engine.contexts[bucketName] = context
glog.V(2).Infof("Set bucket policy for %s", bucketName)
return nil
}
// GetBucketPolicy gets the policy for a bucket
func (engine *PolicyEngine) GetBucketPolicy(bucketName string) (*PolicyDocument, error) {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
context, exists := engine.contexts[bucketName]
if !exists {
return nil, fmt.Errorf("no policy found for bucket %s", bucketName)
}
return context.policy.Document, nil
}
// DeleteBucketPolicy deletes the policy for a bucket
func (engine *PolicyEngine) DeleteBucketPolicy(bucketName string) error {
engine.mutex.Lock()
defer engine.mutex.Unlock()
delete(engine.contexts, bucketName)
glog.V(2).Infof("Deleted bucket policy for %s", bucketName)
return nil
}
// EvaluatePolicy evaluates a policy for the given arguments
func (engine *PolicyEngine) EvaluatePolicy(bucketName string, args *PolicyEvaluationArgs) PolicyEvaluationResult {
engine.mutex.RLock()
context, exists := engine.contexts[bucketName]
engine.mutex.RUnlock()
if !exists {
return PolicyResultIndeterminate
}
return engine.evaluateCompiledPolicy(context.policy, args)
}
// evaluateCompiledPolicy evaluates a compiled policy
func (engine *PolicyEngine) evaluateCompiledPolicy(policy *CompiledPolicy, args *PolicyEvaluationArgs) PolicyEvaluationResult {
// AWS Policy evaluation logic:
// 1. Check for explicit Deny - if found, return Deny
// 2. Check for explicit Allow - if found, return Allow
// 3. If no explicit Allow is found, return Deny (default deny)
hasExplicitAllow := false
for _, stmt := range policy.Statements {
if engine.evaluateStatement(&stmt, args) {
if stmt.Statement.Effect == PolicyEffectDeny {
return PolicyResultDeny // Explicit deny trumps everything
}
if stmt.Statement.Effect == PolicyEffectAllow {
hasExplicitAllow = true
}
}
}
if hasExplicitAllow {
return PolicyResultAllow
}
return PolicyResultDeny // Default deny
}
// evaluateStatement evaluates a single policy statement
func (engine *PolicyEngine) evaluateStatement(stmt *CompiledStatement, args *PolicyEvaluationArgs) bool {
// Check if action matches
if !engine.matchesPatterns(stmt.ActionPatterns, args.Action) {
return false
}
// Check if resource matches
if !engine.matchesPatterns(stmt.ResourcePatterns, args.Resource) {
return false
}
// Check if principal matches (if specified)
if len(stmt.PrincipalPatterns) > 0 {
if !engine.matchesPatterns(stmt.PrincipalPatterns, args.Principal) {
return false
}
}
// Check conditions
if len(stmt.Statement.Condition) > 0 {
if !EvaluateConditions(stmt.Statement.Condition, args.Conditions) {
return false
}
}
return true
}
// matchesPatterns checks if a value matches any of the compiled patterns
func (engine *PolicyEngine) matchesPatterns(patterns []*regexp.Regexp, value string) bool {
for _, pattern := range patterns {
if pattern.MatchString(value) {
return true
}
}
return false
}
// ExtractConditionValuesFromRequest extracts condition values from HTTP request
func ExtractConditionValuesFromRequest(r *http.Request) map[string][]string {
values := make(map[string][]string)
// AWS condition keys
// Extract IP address without port for proper IP matching
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
// Log a warning if splitting fails
glog.Warningf("Failed to parse IP address from RemoteAddr %q: %v", r.RemoteAddr, err)
// If splitting fails, use the original RemoteAddr (might be just IP without port)
host = r.RemoteAddr
}
values["aws:SourceIp"] = []string{host}
values["aws:SecureTransport"] = []string{fmt.Sprintf("%t", r.TLS != nil)}
// Use AWS standard condition key for current time
values["aws:CurrentTime"] = []string{time.Now().Format(time.RFC3339)}
// Keep RequestTime for backward compatibility
values["aws:RequestTime"] = []string{time.Now().Format(time.RFC3339)}
// S3 specific condition keys
if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
values["aws:UserAgent"] = []string{userAgent}
}
if referer := r.Header.Get("Referer"); referer != "" {
values["aws:Referer"] = []string{referer}
}
// S3 object-level conditions
if r.Method == "GET" || r.Method == "HEAD" {
values["s3:ExistingObjectTag"] = extractObjectTags(r)
}
// S3 bucket-level conditions
if delimiter := r.URL.Query().Get("delimiter"); delimiter != "" {
values["s3:delimiter"] = []string{delimiter}
}
if prefix := r.URL.Query().Get("prefix"); prefix != "" {
values["s3:prefix"] = []string{prefix}
}
if maxKeys := r.URL.Query().Get("max-keys"); maxKeys != "" {
values["s3:max-keys"] = []string{maxKeys}
}
// Authentication method
if authHeader := r.Header.Get("Authorization"); authHeader != "" {
if strings.HasPrefix(authHeader, "AWS4-HMAC-SHA256") {
values["s3:authType"] = []string{"REST-HEADER"}
} else if strings.HasPrefix(authHeader, "AWS ") {
values["s3:authType"] = []string{"REST-HEADER"}
}
} else if r.URL.Query().Get("AWSAccessKeyId") != "" {
values["s3:authType"] = []string{"REST-QUERY-STRING"}
}
// HTTP method
values["s3:RequestMethod"] = []string{r.Method}
// Extract custom headers
for key, headerValues := range r.Header {
if strings.HasPrefix(strings.ToLower(key), "x-amz-") {
values[strings.ToLower(key)] = headerValues
}
}
return values
}
// extractObjectTags extracts object tags from request (placeholder implementation)
func extractObjectTags(r *http.Request) []string {
// This would need to be implemented based on how object tags are stored
// For now, return empty slice
return []string{}
}
// BuildResourceArn builds an ARN for the given bucket and object
func BuildResourceArn(bucketName, objectName string) string {
if objectName == "" {
return fmt.Sprintf("arn:aws:s3:::%s", bucketName)
}
return fmt.Sprintf("arn:aws:s3:::%s/%s", bucketName, objectName)
}
// BuildActionName builds a standardized action name
func BuildActionName(action string) string {
if strings.HasPrefix(action, "s3:") {
return action
}
return fmt.Sprintf("s3:%s", action)
}
// IsReadAction checks if an action is a read action
func IsReadAction(action string) bool {
readActions := []string{
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectAcl",
"s3:GetObjectVersionAcl",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:GetBucketLocation",
"s3:GetBucketVersioning",
"s3:GetBucketAcl",
"s3:GetBucketCors",
"s3:GetBucketPolicy",
"s3:GetBucketTagging",
"s3:GetBucketNotification",
"s3:GetBucketObjectLockConfiguration",
"s3:GetObjectRetention",
"s3:GetObjectLegalHold",
}
for _, readAction := range readActions {
if action == readAction {
return true
}
}
return false
}
// IsWriteAction checks if an action is a write action
func IsWriteAction(action string) bool {
writeActions := []string{
"s3:PutObject",
"s3:PutObjectAcl",
"s3:PutObjectTagging",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:DeleteObjectTagging",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploads",
"s3:ListParts",
"s3:PutBucketAcl",
"s3:PutBucketCors",
"s3:PutBucketPolicy",
"s3:PutBucketTagging",
"s3:PutBucketNotification",
"s3:PutBucketVersioning",
"s3:DeleteBucketPolicy",
"s3:DeleteBucketTagging",
"s3:DeleteBucketCors",
"s3:PutBucketObjectLockConfiguration",
"s3:PutObjectRetention",
"s3:PutObjectLegalHold",
"s3:BypassGovernanceRetention",
}
for _, writeAction := range writeActions {
if action == writeAction {
return true
}
}
return false
}
// GetBucketNameFromArn extracts bucket name from ARN
func GetBucketNameFromArn(arn string) string {
if strings.HasPrefix(arn, "arn:aws:s3:::") {
parts := strings.SplitN(arn[13:], "/", 2)
return parts[0]
}
return ""
}
// GetObjectNameFromArn extracts object name from ARN
func GetObjectNameFromArn(arn string) string {
if strings.HasPrefix(arn, "arn:aws:s3:::") {
parts := strings.SplitN(arn[13:], "/", 2)
if len(parts) > 1 {
return parts[1]
}
}
return ""
}
// HasPolicyForBucket checks if a bucket has a policy
func (engine *PolicyEngine) HasPolicyForBucket(bucketName string) bool {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
_, exists := engine.contexts[bucketName]
return exists
}
// GetPolicyStatements returns all policy statements for a bucket
func (engine *PolicyEngine) GetPolicyStatements(bucketName string) []PolicyStatement {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
context, exists := engine.contexts[bucketName]
if !exists {
return nil
}
return context.policy.Document.Statement
}
// ValidatePolicyForBucket validates if a policy is valid for a bucket
func (engine *PolicyEngine) ValidatePolicyForBucket(bucketName string, policyJSON string) error {
policy, err := ParsePolicy(policyJSON)
if err != nil {
return err
}
// Additional validation specific to the bucket
for _, stmt := range policy.Statement {
resources := normalizeToStringSlice(stmt.Resource)
for _, resource := range resources {
if resourceBucket := GetBucketFromResource(resource); resourceBucket != "" {
if resourceBucket != bucketName {
return fmt.Errorf("policy resource %s does not match bucket %s", resource, bucketName)
}
}
}
}
return nil
}
// ClearAllPolicies clears all bucket policies
func (engine *PolicyEngine) ClearAllPolicies() {
engine.mutex.Lock()
defer engine.mutex.Unlock()
engine.contexts = make(map[string]*PolicyEvaluationContext)
glog.V(2).Info("Cleared all bucket policies")
}
// GetAllBucketsWithPolicies returns all buckets that have policies
func (engine *PolicyEngine) GetAllBucketsWithPolicies() []string {
engine.mutex.RLock()
defer engine.mutex.RUnlock()
buckets := make([]string, 0, len(engine.contexts))
for bucketName := range engine.contexts {
buckets = append(buckets, bucketName)
}
return buckets
}
// EvaluatePolicyForRequest evaluates policy for an HTTP request
func (engine *PolicyEngine) EvaluatePolicyForRequest(bucketName, objectName, action, principal string, r *http.Request) PolicyEvaluationResult {
resource := BuildResourceArn(bucketName, objectName)
actionName := BuildActionName(action)
conditions := ExtractConditionValuesFromRequest(r)
args := &PolicyEvaluationArgs{
Action: actionName,
Resource: resource,
Principal: principal,
Conditions: conditions,
}
return engine.EvaluatePolicy(bucketName, args)
}