aboutsummaryrefslogtreecommitdiff
path: root/weed/s3api/policy_engine/integration.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/s3api/policy_engine/integration.go')
-rw-r--r--weed/s3api/policy_engine/integration.go438
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
+}