package policy_engine import ( "fmt" "net/http" "strings" "github.com/seaweedfs/seaweedfs/weed/glog" "github.com/seaweedfs/seaweedfs/weed/s3api/s3err" ) // Action represents an S3 action - this should match the type in auth_credentials.go type Action string // Identity represents a user identity - this should match the type in auth_credentials.go type Identity interface { canDo(action Action, bucket string, objectKey string) bool } // PolicyBackedIAM provides policy-based access control with fallback to legacy IAM type PolicyBackedIAM struct { policyEngine *PolicyEngine legacyIAM LegacyIAM // Interface to delegate to existing IAM system } // LegacyIAM interface for delegating to existing IAM implementation type LegacyIAM interface { authRequest(r *http.Request, action Action) (Identity, s3err.ErrorCode) } // NewPolicyBackedIAM creates a new policy-backed IAM system func NewPolicyBackedIAM() *PolicyBackedIAM { return &PolicyBackedIAM{ policyEngine: NewPolicyEngine(), legacyIAM: nil, // Will be set when integrated with existing IAM } } // NewPolicyBackedIAMWithLegacy creates a new policy-backed IAM system with legacy IAM set func NewPolicyBackedIAMWithLegacy(legacyIAM LegacyIAM) *PolicyBackedIAM { return &PolicyBackedIAM{ policyEngine: NewPolicyEngine(), legacyIAM: legacyIAM, } } // SetLegacyIAM sets the legacy IAM system for fallback func (p *PolicyBackedIAM) SetLegacyIAM(legacyIAM LegacyIAM) { p.legacyIAM = legacyIAM } // SetBucketPolicy sets the policy for a bucket func (p *PolicyBackedIAM) SetBucketPolicy(bucketName string, policyJSON string) error { return p.policyEngine.SetBucketPolicy(bucketName, policyJSON) } // GetBucketPolicy gets the policy for a bucket func (p *PolicyBackedIAM) GetBucketPolicy(bucketName string) (*PolicyDocument, error) { return p.policyEngine.GetBucketPolicy(bucketName) } // DeleteBucketPolicy deletes the policy for a bucket func (p *PolicyBackedIAM) DeleteBucketPolicy(bucketName string) error { return p.policyEngine.DeleteBucketPolicy(bucketName) } // CanDo checks if a principal can perform an action on a resource func (p *PolicyBackedIAM) CanDo(action, bucketName, objectName, principal string, r *http.Request) bool { // If there's a bucket policy, evaluate it if p.policyEngine.HasPolicyForBucket(bucketName) { result := p.policyEngine.EvaluatePolicyForRequest(bucketName, objectName, action, principal, r) switch result { case PolicyResultAllow: return true case PolicyResultDeny: return false case PolicyResultIndeterminate: // Fall through to legacy system } } // No bucket policy or indeterminate result, use legacy conversion return p.evaluateLegacyAction(action, bucketName, objectName, principal) } // evaluateLegacyAction evaluates actions using legacy identity-based rules func (p *PolicyBackedIAM) evaluateLegacyAction(action, bucketName, objectName, principal string) bool { // If we have a legacy IAM system to delegate to, use it if p.legacyIAM != nil { // Create a dummy request for legacy evaluation // In real implementation, this would use the actual request r := &http.Request{ Header: make(http.Header), } // Convert the action string to Action type legacyAction := Action(action) // Use legacy IAM to check permission identity, errCode := p.legacyIAM.authRequest(r, legacyAction) if errCode != s3err.ErrNone { return false } // If we have an identity, check if it can perform the action if identity != nil { return identity.canDo(legacyAction, bucketName, objectName) } } // No legacy IAM available, convert to policy and evaluate return p.evaluateUsingPolicyConversion(action, bucketName, objectName, principal) } // evaluateUsingPolicyConversion converts legacy action to policy and evaluates func (p *PolicyBackedIAM) evaluateUsingPolicyConversion(action, bucketName, objectName, principal string) bool { // For now, use a conservative approach for legacy actions // In a real implementation, this would integrate with the existing identity system glog.V(2).Infof("Legacy action evaluation for %s on %s/%s by %s", action, bucketName, objectName, principal) // Return false to maintain security until proper legacy integration is implemented // This ensures no unintended access is granted return false } // ConvertIdentityToPolicy converts a legacy identity action to an AWS policy func ConvertIdentityToPolicy(identityActions []string, bucketName string) (*PolicyDocument, error) { statements := make([]PolicyStatement, 0) for _, action := range identityActions { stmt, err := convertSingleAction(action, bucketName) if err != nil { glog.Warningf("Failed to convert action %s: %v", action, err) continue } if stmt != nil { statements = append(statements, *stmt) } } if len(statements) == 0 { return nil, fmt.Errorf("no valid statements generated") } return &PolicyDocument{ Version: PolicyVersion2012_10_17, Statement: statements, }, nil } // convertSingleAction converts a single legacy action to a policy statement func convertSingleAction(action, bucketName string) (*PolicyStatement, error) { parts := strings.Split(action, ":") if len(parts) != 2 { return nil, fmt.Errorf("invalid action format: %s", action) } actionType := parts[0] resourcePattern := parts[1] var s3Actions []string var resources []string switch actionType { case "Read": s3Actions = []string{"s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket"} if strings.HasSuffix(resourcePattern, "/*") { // Object-level read access bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{ fmt.Sprintf("arn:aws:s3:::%s", bucket), fmt.Sprintf("arn:aws:s3:::%s/*", bucket), } } else { // Bucket-level read access resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)} } case "Write": s3Actions = []string{"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl"} if strings.HasSuffix(resourcePattern, "/*") { // Object-level write access bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { // Bucket-level write access resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)} } case "Admin": s3Actions = []string{"s3:*"} resources = []string{ fmt.Sprintf("arn:aws:s3:::%s", resourcePattern), fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern), } case "List": s3Actions = []string{"s3:ListBucket", "s3:ListBucketVersions"} resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)} case "Tagging": s3Actions = []string{"s3:GetObjectTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging"} resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} case "BypassGovernanceRetention": s3Actions = []string{"s3:BypassGovernanceRetention"} if strings.HasSuffix(resourcePattern, "/*") { // Object-level bypass governance access bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { // Bucket-level bypass governance access resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} } case "GetObjectRetention": s3Actions = []string{"s3:GetObjectRetention"} if strings.HasSuffix(resourcePattern, "/*") { bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} } case "PutObjectRetention": s3Actions = []string{"s3:PutObjectRetention"} if strings.HasSuffix(resourcePattern, "/*") { bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} } case "GetObjectLegalHold": s3Actions = []string{"s3:GetObjectLegalHold"} if strings.HasSuffix(resourcePattern, "/*") { bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} } case "PutObjectLegalHold": s3Actions = []string{"s3:PutObjectLegalHold"} if strings.HasSuffix(resourcePattern, "/*") { bucket := strings.TrimSuffix(resourcePattern, "/*") resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)} } else { resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)} } case "GetBucketObjectLockConfiguration": s3Actions = []string{"s3:GetBucketObjectLockConfiguration"} resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)} case "PutBucketObjectLockConfiguration": s3Actions = []string{"s3:PutBucketObjectLockConfiguration"} resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)} default: return nil, fmt.Errorf("unknown action type: %s", actionType) } return &PolicyStatement{ Effect: PolicyEffectAllow, Action: NewStringOrStringSlice(s3Actions...), Resource: NewStringOrStringSlice(resources...), }, nil } // GetActionMappings returns the mapping of legacy actions to S3 actions func GetActionMappings() map[string][]string { return map[string][]string{ "Read": { "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:GetBucketTagging", "s3:GetBucketNotification", }, "Write": { "s3:PutObject", "s3:PutObjectAcl", "s3:PutObjectTagging", "s3:DeleteObject", "s3:DeleteObjectVersion", "s3:DeleteObjectTagging", "s3:AbortMultipartUpload", "s3:ListMultipartUploads", "s3:ListParts", "s3:PutBucketAcl", "s3:PutBucketCors", "s3:PutBucketTagging", "s3:PutBucketNotification", "s3:PutBucketVersioning", "s3:DeleteBucketTagging", "s3:DeleteBucketCors", }, "Admin": { "s3:*", }, "List": { "s3:ListBucket", "s3:ListBucketVersions", "s3:ListAllMyBuckets", }, "Tagging": { "s3:GetObjectTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging", "s3:GetBucketTagging", "s3:PutBucketTagging", "s3:DeleteBucketTagging", }, "BypassGovernanceRetention": { "s3:BypassGovernanceRetention", }, "GetObjectRetention": { "s3:GetObjectRetention", }, "PutObjectRetention": { "s3:PutObjectRetention", }, "GetObjectLegalHold": { "s3:GetObjectLegalHold", }, "PutObjectLegalHold": { "s3:PutObjectLegalHold", }, "GetBucketObjectLockConfiguration": { "s3:GetBucketObjectLockConfiguration", }, "PutBucketObjectLockConfiguration": { "s3:PutBucketObjectLockConfiguration", }, } } // ValidateActionMapping validates that a legacy action can be mapped to S3 actions func ValidateActionMapping(action string) error { mappings := GetActionMappings() parts := strings.Split(action, ":") if len(parts) != 2 { return fmt.Errorf("invalid action format: %s, expected format: 'ActionType:Resource'", action) } actionType := parts[0] resource := parts[1] if _, exists := mappings[actionType]; !exists { return fmt.Errorf("unknown action type: %s", actionType) } if resource == "" { return fmt.Errorf("resource cannot be empty") } return nil } // ConvertLegacyActions converts an array of legacy actions to S3 actions func ConvertLegacyActions(legacyActions []string) ([]string, error) { mappings := GetActionMappings() s3Actions := make([]string, 0) for _, legacyAction := range legacyActions { if err := ValidateActionMapping(legacyAction); err != nil { return nil, err } parts := strings.Split(legacyAction, ":") actionType := parts[0] if actionType == "Admin" { // Admin gives all permissions, so we can just return s3:* return []string{"s3:*"}, nil } if mapped, exists := mappings[actionType]; exists { s3Actions = append(s3Actions, mapped...) } } // Remove duplicates uniqueActions := make([]string, 0) seen := make(map[string]bool) for _, action := range s3Actions { if !seen[action] { uniqueActions = append(uniqueActions, action) seen[action] = true } } return uniqueActions, nil } // GetResourcesFromLegacyAction extracts resources from a legacy action func GetResourcesFromLegacyAction(legacyAction string) ([]string, error) { parts := strings.Split(legacyAction, ":") if len(parts) != 2 { return nil, fmt.Errorf("invalid action format: %s", legacyAction) } resourcePattern := parts[1] resources := make([]string, 0) if strings.HasSuffix(resourcePattern, "/*") { // Object-level access bucket := strings.TrimSuffix(resourcePattern, "/*") resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s", bucket)) resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s/*", bucket)) } else { // Bucket-level access resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)) } return resources, nil } // CreatePolicyFromLegacyIdentity creates a policy document from legacy identity actions func CreatePolicyFromLegacyIdentity(identityName string, actions []string) (*PolicyDocument, error) { statements := make([]PolicyStatement, 0) // Group actions by resource pattern resourceActions := make(map[string][]string) for _, action := range actions { parts := strings.Split(action, ":") if len(parts) != 2 { continue } resourcePattern := parts[1] actionType := parts[0] if _, exists := resourceActions[resourcePattern]; !exists { resourceActions[resourcePattern] = make([]string, 0) } resourceActions[resourcePattern] = append(resourceActions[resourcePattern], actionType) } // Create statements for each resource pattern for resourcePattern, actionTypes := range resourceActions { s3Actions := make([]string, 0) for _, actionType := range actionTypes { if actionType == "Admin" { s3Actions = []string{"s3:*"} break } if mapped, exists := GetActionMappings()[actionType]; exists { s3Actions = append(s3Actions, mapped...) } } resources, err := GetResourcesFromLegacyAction(fmt.Sprintf("dummy:%s", resourcePattern)) if err != nil { continue } statement := PolicyStatement{ Sid: fmt.Sprintf("%s-%s", identityName, strings.ReplaceAll(resourcePattern, "/", "-")), Effect: PolicyEffectAllow, Action: NewStringOrStringSlice(s3Actions...), Resource: NewStringOrStringSlice(resources...), } statements = append(statements, statement) } if len(statements) == 0 { return nil, fmt.Errorf("no valid statements generated for identity %s", identityName) } return &PolicyDocument{ Version: PolicyVersion2012_10_17, Statement: statements, }, nil } // HasPolicyForBucket checks if a bucket has a policy func (p *PolicyBackedIAM) HasPolicyForBucket(bucketName string) bool { return p.policyEngine.HasPolicyForBucket(bucketName) } // GetPolicyEngine returns the underlying policy engine func (p *PolicyBackedIAM) GetPolicyEngine() *PolicyEngine { return p.policyEngine }