diff --git a/weed/s3api/s3_policy_templates.go b/weed/s3api/s3_policy_templates.go new file mode 100644 index 000000000..ab04a5196 --- /dev/null +++ b/weed/s3api/s3_policy_templates.go @@ -0,0 +1,618 @@ +package s3api + +import ( + "time" + + "github.com/seaweedfs/seaweedfs/weed/iam/policy" +) + +// S3PolicyTemplates provides pre-built IAM policy templates for common S3 use cases +type S3PolicyTemplates struct{} + +// NewS3PolicyTemplates creates a new policy templates provider +func NewS3PolicyTemplates() *S3PolicyTemplates { + return &S3PolicyTemplates{} +} + +// GetS3ReadOnlyPolicy returns a policy that allows read-only access to all S3 resources +func (t *S3PolicyTemplates) GetS3ReadOnlyPolicy() *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "S3ReadOnlyAccess", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:GetBucketLocation", + "s3:GetBucketVersioning", + "s3:ListAllMyBuckets", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + }, + } +} + +// GetS3WriteOnlyPolicy returns a policy that allows write-only access to all S3 resources +func (t *S3PolicyTemplates) GetS3WriteOnlyPolicy() *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "S3WriteOnlyAccess", + Effect: "Allow", + Action: []string{ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploads", + "s3:ListParts", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + }, + } +} + +// GetS3AdminPolicy returns a policy that allows full admin access to all S3 resources +func (t *S3PolicyTemplates) GetS3AdminPolicy() *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "S3FullAccess", + Effect: "Allow", + Action: []string{ + "s3:*", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + }, + } +} + +// GetBucketSpecificReadPolicy returns a policy for read-only access to a specific bucket +func (t *S3PolicyTemplates) GetBucketSpecificReadPolicy(bucketName string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "BucketSpecificReadAccess", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:GetBucketLocation", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + "arn:seaweed:s3:::" + bucketName + "/*", + }, + }, + }, + } +} + +// GetBucketSpecificWritePolicy returns a policy for write-only access to a specific bucket +func (t *S3PolicyTemplates) GetBucketSpecificWritePolicy(bucketName string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "BucketSpecificWriteAccess", + Effect: "Allow", + Action: []string{ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploads", + "s3:ListParts", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + "arn:seaweed:s3:::" + bucketName + "/*", + }, + }, + }, + } +} + +// GetPathBasedAccessPolicy returns a policy that restricts access to a specific path within a bucket +func (t *S3PolicyTemplates) GetPathBasedAccessPolicy(bucketName, pathPrefix string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "ListBucketPermission", + Effect: "Allow", + Action: []string{ + "s3:ListBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + }, + Condition: map[string]map[string]interface{}{ + "StringLike": map[string]interface{}{ + "s3:prefix": []string{pathPrefix + "/*"}, + }, + }, + }, + { + Sid: "PathBasedObjectAccess", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:PutObject", + "s3:DeleteObject", + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + "s3:AbortMultipartUpload", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName + "/" + pathPrefix + "/*", + }, + }, + }, + } +} + +// GetIPRestrictedPolicy returns a policy that restricts access based on source IP +func (t *S3PolicyTemplates) GetIPRestrictedPolicy(allowedCIDRs []string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "IPRestrictedS3Access", + Effect: "Allow", + Action: []string{ + "s3:*", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + Condition: map[string]map[string]interface{}{ + "IpAddress": map[string]interface{}{ + "aws:SourceIp": allowedCIDRs, + }, + }, + }, + }, + } +} + +// GetTimeBasedAccessPolicy returns a policy that allows access only during specific hours +func (t *S3PolicyTemplates) GetTimeBasedAccessPolicy(startHour, endHour int) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "TimeBasedS3Access", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + Condition: map[string]map[string]interface{}{ + "DateGreaterThan": map[string]interface{}{ + "aws:CurrentTime": time.Now().Format("2006-01-02") + "T" + + formatHour(startHour) + ":00:00Z", + }, + "DateLessThan": map[string]interface{}{ + "aws:CurrentTime": time.Now().Format("2006-01-02") + "T" + + formatHour(endHour) + ":00:00Z", + }, + }, + }, + }, + } +} + +// GetMultipartUploadPolicy returns a policy specifically for multipart upload operations +func (t *S3PolicyTemplates) GetMultipartUploadPolicy(bucketName string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "MultipartUploadOperations", + Effect: "Allow", + Action: []string{ + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploads", + "s3:ListParts", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName + "/*", + }, + }, + { + Sid: "ListBucketForMultipart", + Effect: "Allow", + Action: []string{ + "s3:ListBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + }, + }, + }, + } +} + +// GetPresignedURLPolicy returns a policy for generating and using presigned URLs +func (t *S3PolicyTemplates) GetPresignedURLPolicy(bucketName string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "PresignedURLAccess", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:PutObject", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName + "/*", + }, + Condition: map[string]map[string]interface{}{ + "StringEquals": map[string]interface{}{ + "s3:x-amz-signature-version": "AWS4-HMAC-SHA256", + }, + }, + }, + }, + } +} + +// GetTemporaryAccessPolicy returns a policy for temporary access with expiration +func (t *S3PolicyTemplates) GetTemporaryAccessPolicy(bucketName string, expirationHours int) *policy.PolicyDocument { + expirationTime := time.Now().Add(time.Duration(expirationHours) * time.Hour) + + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "TemporaryS3Access", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:PutObject", + "s3:ListBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + "arn:seaweed:s3:::" + bucketName + "/*", + }, + Condition: map[string]map[string]interface{}{ + "DateLessThan": map[string]interface{}{ + "aws:CurrentTime": expirationTime.UTC().Format("2006-01-02T15:04:05Z"), + }, + }, + }, + }, + } +} + +// GetContentTypeRestrictedPolicy returns a policy that restricts uploads to specific content types +func (t *S3PolicyTemplates) GetContentTypeRestrictedPolicy(bucketName string, allowedContentTypes []string) *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "ContentTypeRestrictedUpload", + Effect: "Allow", + Action: []string{ + "s3:PutObject", + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName + "/*", + }, + Condition: map[string]map[string]interface{}{ + "StringEquals": map[string]interface{}{ + "s3:content-type": allowedContentTypes, + }, + }, + }, + { + Sid: "ReadAccess", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:ListBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::" + bucketName, + "arn:seaweed:s3:::" + bucketName + "/*", + }, + }, + }, + } +} + +// GetDenyDeletePolicy returns a policy that allows all operations except delete +func (t *S3PolicyTemplates) GetDenyDeletePolicy() *policy.PolicyDocument { + return &policy.PolicyDocument{ + Version: "2012-10-17", + Statement: []policy.Statement{ + { + Sid: "AllowAllExceptDelete", + Effect: "Allow", + Action: []string{ + "s3:GetObject", + "s3:GetObjectVersion", + "s3:PutObject", + "s3:PutObjectAcl", + "s3:ListBucket", + "s3:ListBucketVersions", + "s3:CreateMultipartUpload", + "s3:UploadPart", + "s3:CompleteMultipartUpload", + "s3:AbortMultipartUpload", + "s3:ListMultipartUploads", + "s3:ListParts", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + { + Sid: "DenyDeleteOperations", + Effect: "Deny", + Action: []string{ + "s3:DeleteObject", + "s3:DeleteObjectVersion", + "s3:DeleteBucket", + }, + Resource: []string{ + "arn:seaweed:s3:::*", + "arn:seaweed:s3:::*/*", + }, + }, + }, + } +} + +// Helper function to format hour with leading zero +func formatHour(hour int) string { + if hour < 10 { + return "0" + string(rune('0'+hour)) + } + return string(rune('0'+hour/10)) + string(rune('0'+hour%10)) +} + +// PolicyTemplateDefinition represents metadata about a policy template +type PolicyTemplateDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Category string `json:"category"` + UseCase string `json:"use_case"` + Parameters []PolicyTemplateParam `json:"parameters,omitempty"` + Policy *policy.PolicyDocument `json:"policy"` +} + +// PolicyTemplateParam represents a parameter for customizing policy templates +type PolicyTemplateParam struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` + Required bool `json:"required"` + DefaultValue string `json:"default_value,omitempty"` + Example string `json:"example,omitempty"` +} + +// GetAllPolicyTemplates returns all available policy templates with metadata +func (t *S3PolicyTemplates) GetAllPolicyTemplates() []PolicyTemplateDefinition { + return []PolicyTemplateDefinition{ + { + Name: "S3ReadOnlyAccess", + Description: "Provides read-only access to all S3 buckets and objects", + Category: "Basic Access", + UseCase: "Data consumers, backup services, monitoring applications", + Policy: t.GetS3ReadOnlyPolicy(), + }, + { + Name: "S3WriteOnlyAccess", + Description: "Provides write-only access to all S3 buckets and objects", + Category: "Basic Access", + UseCase: "Data ingestion services, backup applications", + Policy: t.GetS3WriteOnlyPolicy(), + }, + { + Name: "S3AdminAccess", + Description: "Provides full administrative access to all S3 resources", + Category: "Administrative", + UseCase: "S3 administrators, service accounts with full control", + Policy: t.GetS3AdminPolicy(), + }, + { + Name: "BucketSpecificRead", + Description: "Provides read-only access to a specific bucket", + Category: "Bucket-Specific", + UseCase: "Applications that need access to specific data sets", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket to grant access to", + Required: true, + Example: "my-data-bucket", + }, + }, + Policy: t.GetBucketSpecificReadPolicy("${bucketName}"), + }, + { + Name: "BucketSpecificWrite", + Description: "Provides write-only access to a specific bucket", + Category: "Bucket-Specific", + UseCase: "Upload services, data ingestion for specific datasets", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket to grant access to", + Required: true, + Example: "my-upload-bucket", + }, + }, + Policy: t.GetBucketSpecificWritePolicy("${bucketName}"), + }, + { + Name: "PathBasedAccess", + Description: "Restricts access to a specific path/prefix within a bucket", + Category: "Path-Restricted", + UseCase: "Multi-tenant applications, user-specific directories", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket", + Required: true, + Example: "shared-bucket", + }, + { + Name: "pathPrefix", + Type: "string", + Description: "Path prefix to restrict access to", + Required: true, + Example: "user123/documents", + }, + }, + Policy: t.GetPathBasedAccessPolicy("${bucketName}", "${pathPrefix}"), + }, + { + Name: "IPRestrictedAccess", + Description: "Allows access only from specific IP addresses or ranges", + Category: "Security", + UseCase: "Corporate networks, office-based access, VPN restrictions", + Parameters: []PolicyTemplateParam{ + { + Name: "allowedCIDRs", + Type: "array", + Description: "List of allowed IP addresses or CIDR ranges", + Required: true, + Example: "[\"192.168.1.0/24\", \"10.0.0.0/8\"]", + }, + }, + Policy: t.GetIPRestrictedPolicy([]string{"${allowedCIDRs}"}), + }, + { + Name: "MultipartUploadOnly", + Description: "Allows only multipart upload operations on a specific bucket", + Category: "Upload-Specific", + UseCase: "Large file upload services, streaming applications", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket for multipart uploads", + Required: true, + Example: "large-files-bucket", + }, + }, + Policy: t.GetMultipartUploadPolicy("${bucketName}"), + }, + { + Name: "PresignedURLAccess", + Description: "Policy for generating and using presigned URLs", + Category: "Presigned URLs", + UseCase: "Frontend applications, temporary file sharing", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket for presigned URL access", + Required: true, + Example: "shared-files-bucket", + }, + }, + Policy: t.GetPresignedURLPolicy("${bucketName}"), + }, + { + Name: "ContentTypeRestricted", + Description: "Restricts uploads to specific content types", + Category: "Content Control", + UseCase: "Image galleries, document repositories, media libraries", + Parameters: []PolicyTemplateParam{ + { + Name: "bucketName", + Type: "string", + Description: "Name of the S3 bucket", + Required: true, + Example: "media-bucket", + }, + { + Name: "allowedContentTypes", + Type: "array", + Description: "List of allowed MIME content types", + Required: true, + Example: "[\"image/jpeg\", \"image/png\", \"video/mp4\"]", + }, + }, + Policy: t.GetContentTypeRestrictedPolicy("${bucketName}", []string{"${allowedContentTypes}"}), + }, + { + Name: "DenyDeleteAccess", + Description: "Allows all operations except delete (immutable storage)", + Category: "Data Protection", + UseCase: "Compliance storage, audit logs, backup retention", + Policy: t.GetDenyDeletePolicy(), + }, + } +} + +// GetPolicyTemplateByName returns a specific policy template by name +func (t *S3PolicyTemplates) GetPolicyTemplateByName(name string) *PolicyTemplateDefinition { + templates := t.GetAllPolicyTemplates() + for _, template := range templates { + if template.Name == name { + return &template + } + } + return nil +} + +// GetPolicyTemplatesByCategory returns all policy templates in a specific category +func (t *S3PolicyTemplates) GetPolicyTemplatesByCategory(category string) []PolicyTemplateDefinition { + var result []PolicyTemplateDefinition + templates := t.GetAllPolicyTemplates() + for _, template := range templates { + if template.Category == category { + result = append(result, template) + } + } + return result +} diff --git a/weed/s3api/s3_policy_templates_test.go b/weed/s3api/s3_policy_templates_test.go new file mode 100644 index 000000000..e744fcd3b --- /dev/null +++ b/weed/s3api/s3_policy_templates_test.go @@ -0,0 +1,504 @@ +package s3api + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestS3PolicyTemplates(t *testing.T) { + templates := NewS3PolicyTemplates() + + t.Run("S3ReadOnlyPolicy", func(t *testing.T) { + policy := templates.GetS3ReadOnlyPolicy() + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "S3ReadOnlyAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:GetObject") + assert.Contains(t, stmt.Action, "s3:ListBucket") + assert.NotContains(t, stmt.Action, "s3:PutObject") + assert.NotContains(t, stmt.Action, "s3:DeleteObject") + + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*") + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*") + }) + + t.Run("S3WriteOnlyPolicy", func(t *testing.T) { + policy := templates.GetS3WriteOnlyPolicy() + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "S3WriteOnlyAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:PutObject") + assert.Contains(t, stmt.Action, "s3:CreateMultipartUpload") + assert.NotContains(t, stmt.Action, "s3:GetObject") + assert.NotContains(t, stmt.Action, "s3:DeleteObject") + + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*") + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*") + }) + + t.Run("S3AdminPolicy", func(t *testing.T) { + policy := templates.GetS3AdminPolicy() + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "S3FullAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:*") + + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*") + assert.Contains(t, stmt.Resource, "arn:seaweed:s3:::*/*") + }) +} + +func TestBucketSpecificPolicies(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "test-bucket" + + t.Run("BucketSpecificReadPolicy", func(t *testing.T) { + policy := templates.GetBucketSpecificReadPolicy(bucketName) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "BucketSpecificReadAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:GetObject") + assert.Contains(t, stmt.Action, "s3:ListBucket") + assert.NotContains(t, stmt.Action, "s3:PutObject") + + expectedBucketArn := "arn:seaweed:s3:::" + bucketName + expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*" + assert.Contains(t, stmt.Resource, expectedBucketArn) + assert.Contains(t, stmt.Resource, expectedObjectArn) + }) + + t.Run("BucketSpecificWritePolicy", func(t *testing.T) { + policy := templates.GetBucketSpecificWritePolicy(bucketName) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "BucketSpecificWriteAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:PutObject") + assert.Contains(t, stmt.Action, "s3:CreateMultipartUpload") + assert.NotContains(t, stmt.Action, "s3:GetObject") + + expectedBucketArn := "arn:seaweed:s3:::" + bucketName + expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*" + assert.Contains(t, stmt.Resource, expectedBucketArn) + assert.Contains(t, stmt.Resource, expectedObjectArn) + }) +} + +func TestPathBasedAccessPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "shared-bucket" + pathPrefix := "user123/documents" + + policy := templates.GetPathBasedAccessPolicy(bucketName, pathPrefix) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 2) + + // First statement: List bucket with prefix condition + listStmt := policy.Statement[0] + assert.Equal(t, "Allow", listStmt.Effect) + assert.Equal(t, "ListBucketPermission", listStmt.Sid) + assert.Contains(t, listStmt.Action, "s3:ListBucket") + assert.Contains(t, listStmt.Resource, "arn:seaweed:s3:::"+bucketName) + assert.NotNil(t, listStmt.Condition) + + // Second statement: Object operations on path + objectStmt := policy.Statement[1] + assert.Equal(t, "Allow", objectStmt.Effect) + assert.Equal(t, "PathBasedObjectAccess", objectStmt.Sid) + assert.Contains(t, objectStmt.Action, "s3:GetObject") + assert.Contains(t, objectStmt.Action, "s3:PutObject") + assert.Contains(t, objectStmt.Action, "s3:DeleteObject") + + expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/" + pathPrefix + "/*" + assert.Contains(t, objectStmt.Resource, expectedObjectArn) +} + +func TestIPRestrictedPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + allowedCIDRs := []string{"192.168.1.0/24", "10.0.0.0/8"} + + policy := templates.GetIPRestrictedPolicy(allowedCIDRs) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "IPRestrictedS3Access", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:*") + assert.NotNil(t, stmt.Condition) + + // Check IP condition structure + condition := stmt.Condition + ipAddress, exists := condition["IpAddress"] + assert.True(t, exists) + + sourceIp, exists := ipAddress["aws:SourceIp"] + assert.True(t, exists) + assert.Equal(t, allowedCIDRs, sourceIp) +} + +func TestTimeBasedAccessPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + startHour := 9 // 9 AM + endHour := 17 // 5 PM + + policy := templates.GetTimeBasedAccessPolicy(startHour, endHour) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "TimeBasedS3Access", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:GetObject") + assert.Contains(t, stmt.Action, "s3:PutObject") + assert.Contains(t, stmt.Action, "s3:ListBucket") + assert.NotNil(t, stmt.Condition) + + // Check time condition structure + condition := stmt.Condition + _, hasGreater := condition["DateGreaterThan"] + _, hasLess := condition["DateLessThan"] + assert.True(t, hasGreater) + assert.True(t, hasLess) +} + +func TestMultipartUploadPolicyTemplate(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "large-files" + + policy := templates.GetMultipartUploadPolicy(bucketName) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 2) + + // First statement: Multipart operations + multipartStmt := policy.Statement[0] + assert.Equal(t, "Allow", multipartStmt.Effect) + assert.Equal(t, "MultipartUploadOperations", multipartStmt.Sid) + assert.Contains(t, multipartStmt.Action, "s3:CreateMultipartUpload") + assert.Contains(t, multipartStmt.Action, "s3:UploadPart") + assert.Contains(t, multipartStmt.Action, "s3:CompleteMultipartUpload") + assert.Contains(t, multipartStmt.Action, "s3:AbortMultipartUpload") + assert.Contains(t, multipartStmt.Action, "s3:ListMultipartUploads") + assert.Contains(t, multipartStmt.Action, "s3:ListParts") + + expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*" + assert.Contains(t, multipartStmt.Resource, expectedObjectArn) + + // Second statement: List bucket + listStmt := policy.Statement[1] + assert.Equal(t, "Allow", listStmt.Effect) + assert.Equal(t, "ListBucketForMultipart", listStmt.Sid) + assert.Contains(t, listStmt.Action, "s3:ListBucket") + + expectedBucketArn := "arn:seaweed:s3:::" + bucketName + assert.Contains(t, listStmt.Resource, expectedBucketArn) +} + +func TestPresignedURLPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "shared-files" + + policy := templates.GetPresignedURLPolicy(bucketName) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "PresignedURLAccess", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:GetObject") + assert.Contains(t, stmt.Action, "s3:PutObject") + assert.NotNil(t, stmt.Condition) + + expectedObjectArn := "arn:seaweed:s3:::" + bucketName + "/*" + assert.Contains(t, stmt.Resource, expectedObjectArn) + + // Check signature version condition + condition := stmt.Condition + stringEquals, exists := condition["StringEquals"] + assert.True(t, exists) + + signatureVersion, exists := stringEquals["s3:x-amz-signature-version"] + assert.True(t, exists) + assert.Equal(t, "AWS4-HMAC-SHA256", signatureVersion) +} + +func TestTemporaryAccessPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "temp-bucket" + expirationHours := 24 + + policy := templates.GetTemporaryAccessPolicy(bucketName, expirationHours) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 1) + + stmt := policy.Statement[0] + assert.Equal(t, "Allow", stmt.Effect) + assert.Equal(t, "TemporaryS3Access", stmt.Sid) + assert.Contains(t, stmt.Action, "s3:GetObject") + assert.Contains(t, stmt.Action, "s3:PutObject") + assert.Contains(t, stmt.Action, "s3:ListBucket") + assert.NotNil(t, stmt.Condition) + + // Check expiration condition + condition := stmt.Condition + dateLessThan, exists := condition["DateLessThan"] + assert.True(t, exists) + + currentTime, exists := dateLessThan["aws:CurrentTime"] + assert.True(t, exists) + assert.IsType(t, "", currentTime) // Should be a string timestamp +} + +func TestContentTypeRestrictedPolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + bucketName := "media-bucket" + allowedTypes := []string{"image/jpeg", "image/png", "video/mp4"} + + policy := templates.GetContentTypeRestrictedPolicy(bucketName, allowedTypes) + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 2) + + // First statement: Upload with content type restriction + uploadStmt := policy.Statement[0] + assert.Equal(t, "Allow", uploadStmt.Effect) + assert.Equal(t, "ContentTypeRestrictedUpload", uploadStmt.Sid) + assert.Contains(t, uploadStmt.Action, "s3:PutObject") + assert.Contains(t, uploadStmt.Action, "s3:CreateMultipartUpload") + assert.NotNil(t, uploadStmt.Condition) + + // Check content type condition + condition := uploadStmt.Condition + stringEquals, exists := condition["StringEquals"] + assert.True(t, exists) + + contentType, exists := stringEquals["s3:content-type"] + assert.True(t, exists) + assert.Equal(t, allowedTypes, contentType) + + // Second statement: Read access without restrictions + readStmt := policy.Statement[1] + assert.Equal(t, "Allow", readStmt.Effect) + assert.Equal(t, "ReadAccess", readStmt.Sid) + assert.Contains(t, readStmt.Action, "s3:GetObject") + assert.Contains(t, readStmt.Action, "s3:ListBucket") + assert.Nil(t, readStmt.Condition) // No conditions for read access +} + +func TestDenyDeletePolicy(t *testing.T) { + templates := NewS3PolicyTemplates() + + policy := templates.GetDenyDeletePolicy() + + require.NotNil(t, policy) + assert.Equal(t, "2012-10-17", policy.Version) + assert.Len(t, policy.Statement, 2) + + // First statement: Allow everything except delete + allowStmt := policy.Statement[0] + assert.Equal(t, "Allow", allowStmt.Effect) + assert.Equal(t, "AllowAllExceptDelete", allowStmt.Sid) + assert.Contains(t, allowStmt.Action, "s3:GetObject") + assert.Contains(t, allowStmt.Action, "s3:PutObject") + assert.Contains(t, allowStmt.Action, "s3:ListBucket") + assert.NotContains(t, allowStmt.Action, "s3:DeleteObject") + assert.NotContains(t, allowStmt.Action, "s3:DeleteBucket") + + // Second statement: Explicitly deny delete operations + denyStmt := policy.Statement[1] + assert.Equal(t, "Deny", denyStmt.Effect) + assert.Equal(t, "DenyDeleteOperations", denyStmt.Sid) + assert.Contains(t, denyStmt.Action, "s3:DeleteObject") + assert.Contains(t, denyStmt.Action, "s3:DeleteObjectVersion") + assert.Contains(t, denyStmt.Action, "s3:DeleteBucket") +} + +func TestPolicyTemplateMetadata(t *testing.T) { + templates := NewS3PolicyTemplates() + + t.Run("GetAllPolicyTemplates", func(t *testing.T) { + allTemplates := templates.GetAllPolicyTemplates() + + assert.Greater(t, len(allTemplates), 10) // Should have many templates + + // Check that each template has required fields + for _, template := range allTemplates { + assert.NotEmpty(t, template.Name) + assert.NotEmpty(t, template.Description) + assert.NotEmpty(t, template.Category) + assert.NotEmpty(t, template.UseCase) + assert.NotNil(t, template.Policy) + assert.Equal(t, "2012-10-17", template.Policy.Version) + } + }) + + t.Run("GetPolicyTemplateByName", func(t *testing.T) { + // Test existing template + template := templates.GetPolicyTemplateByName("S3ReadOnlyAccess") + require.NotNil(t, template) + assert.Equal(t, "S3ReadOnlyAccess", template.Name) + assert.Equal(t, "Basic Access", template.Category) + + // Test non-existing template + nonExistent := templates.GetPolicyTemplateByName("NonExistentTemplate") + assert.Nil(t, nonExistent) + }) + + t.Run("GetPolicyTemplatesByCategory", func(t *testing.T) { + basicAccessTemplates := templates.GetPolicyTemplatesByCategory("Basic Access") + assert.GreaterOrEqual(t, len(basicAccessTemplates), 2) + + for _, template := range basicAccessTemplates { + assert.Equal(t, "Basic Access", template.Category) + } + + // Test non-existing category + emptyCategory := templates.GetPolicyTemplatesByCategory("NonExistentCategory") + assert.Empty(t, emptyCategory) + }) + + t.Run("PolicyTemplateParameters", func(t *testing.T) { + allTemplates := templates.GetAllPolicyTemplates() + + // Find a template with parameters (like BucketSpecificRead) + var templateWithParams *PolicyTemplateDefinition + for _, template := range allTemplates { + if template.Name == "BucketSpecificRead" { + templateWithParams = &template + break + } + } + + require.NotNil(t, templateWithParams) + assert.Greater(t, len(templateWithParams.Parameters), 0) + + param := templateWithParams.Parameters[0] + assert.Equal(t, "bucketName", param.Name) + assert.Equal(t, "string", param.Type) + assert.True(t, param.Required) + assert.NotEmpty(t, param.Description) + assert.NotEmpty(t, param.Example) + }) +} + +func TestFormatHourHelper(t *testing.T) { + tests := []struct { + hour int + expected string + }{ + {0, "00"}, + {5, "05"}, + {9, "09"}, + {10, "10"}, + {15, "15"}, + {23, "23"}, + } + + for _, tt := range tests { + t.Run(fmt.Sprintf("Hour_%d", tt.hour), func(t *testing.T) { + result := formatHour(tt.hour) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestPolicyTemplateCategories(t *testing.T) { + templates := NewS3PolicyTemplates() + allTemplates := templates.GetAllPolicyTemplates() + + // Extract all categories + categoryMap := make(map[string]int) + for _, template := range allTemplates { + categoryMap[template.Category]++ + } + + // Expected categories + expectedCategories := []string{ + "Basic Access", + "Administrative", + "Bucket-Specific", + "Path-Restricted", + "Security", + "Upload-Specific", + "Presigned URLs", + "Content Control", + "Data Protection", + } + + for _, expectedCategory := range expectedCategories { + count, exists := categoryMap[expectedCategory] + assert.True(t, exists, "Category %s should exist", expectedCategory) + assert.Greater(t, count, 0, "Category %s should have at least one template", expectedCategory) + } +} + +func TestPolicyValidation(t *testing.T) { + templates := NewS3PolicyTemplates() + allTemplates := templates.GetAllPolicyTemplates() + + // Test that all policies have valid structure + for _, template := range allTemplates { + t.Run("Policy_"+template.Name, func(t *testing.T) { + policy := template.Policy + + // Basic validation + assert.Equal(t, "2012-10-17", policy.Version) + assert.Greater(t, len(policy.Statement), 0) + + // Validate each statement + for i, stmt := range policy.Statement { + assert.NotEmpty(t, stmt.Effect, "Statement %d should have effect", i) + assert.Contains(t, []string{"Allow", "Deny"}, stmt.Effect, "Statement %d effect should be Allow or Deny", i) + assert.Greater(t, len(stmt.Action), 0, "Statement %d should have actions", i) + assert.Greater(t, len(stmt.Resource), 0, "Statement %d should have resources", i) + + // Check resource format + for _, resource := range stmt.Resource { + if resource != "*" { + assert.Contains(t, resource, "arn:seaweed:s3:::", "Resource should be valid SeaweedFS S3 ARN: %s", resource) + } + } + } + }) + } +}