| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512 |
- 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"}
- if strings.HasSuffix(resourcePattern, "/*") {
- // Object-level list access - extract bucket from "bucket/prefix/*" pattern
- patternWithoutWildcard := strings.TrimSuffix(resourcePattern, "/*")
- parts := strings.SplitN(patternWithoutWildcard, "/", 2)
- bucket := parts[0]
- resources = []string{
- fmt.Sprintf("arn:aws:s3:::%s", bucket),
- fmt.Sprintf("arn:aws:s3:::%s/*", bucket),
- }
- } else {
- // Bucket-level list access
- 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
- }
|