| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- 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)
- }
|