diff options
| author | Chris Lu <chrislusf@users.noreply.github.com> | 2025-07-13 16:21:36 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-07-13 16:21:36 -0700 |
| commit | 7cb1ca13082568bfdcdab974d8cefddf650443c5 (patch) | |
| tree | 573b5e15d080d37b9312cade4151da9e3fb7ddee /weed/s3api/policy_engine/integration.go | |
| parent | 1549ee2e154ab040e211ac7b3bc361272069abef (diff) | |
| download | seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.tar.xz seaweedfs-7cb1ca13082568bfdcdab974d8cefddf650443c5.zip | |
Add policy engine (#6970)
Diffstat (limited to 'weed/s3api/policy_engine/integration.go')
| -rw-r--r-- | weed/s3api/policy_engine/integration.go | 438 |
1 files changed, 438 insertions, 0 deletions
diff --git a/weed/s3api/policy_engine/integration.go b/weed/s3api/policy_engine/integration.go new file mode 100644 index 000000000..2a6a5c8fa --- /dev/null +++ b/weed/s3api/policy_engine/integration.go @@ -0,0 +1,438 @@ +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)} + } + + 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", + }, + } +} + +// 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 +} |
