mirror of
https://github.com/chrislusf/seaweedfs
synced 2025-07-25 21:12:47 +02:00
500 lines
15 KiB
Go
500 lines
15 KiB
Go
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
|
|
}
|