215 lines
5.3 KiB
Go
215 lines
5.3 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type BucketPolicy struct {
|
|
Version string `json:"Version"`
|
|
Statement []PolicyStatement `json:"Statement"`
|
|
}
|
|
|
|
type PolicyStatement struct {
|
|
Sid string `json:"Sid,omitempty"`
|
|
Effect string `json:"Effect"`
|
|
Principal map[string]any `json:"Principal,omitempty"`
|
|
Action []string `json:"Action"`
|
|
Resource []string `json:"Resource"`
|
|
Condition map[string]any `json:"Condition,omitempty"`
|
|
}
|
|
|
|
type BucketPolicyRecord struct {
|
|
ID int64 `json:"id"`
|
|
BucketID int64 `json:"bucketId"`
|
|
Policy string `json:"policy"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
func ValidateBucketPolicy(policyJSON string) (*BucketPolicy, error) {
|
|
var policy BucketPolicy
|
|
if err := json.Unmarshal([]byte(policyJSON), &policy); err != nil {
|
|
return nil, fmt.Errorf("invalid JSON format: %w", err)
|
|
}
|
|
|
|
if policy.Version == "" {
|
|
policy.Version = "2012-10-17"
|
|
}
|
|
|
|
if len(policy.Statement) == 0 {
|
|
return nil, fmt.Errorf("policy must contain at least one statement")
|
|
}
|
|
|
|
for i, stmt := range policy.Statement {
|
|
if err := validateStatement(stmt); err != nil {
|
|
return nil, fmt.Errorf("statement %d: %w", i, err)
|
|
}
|
|
}
|
|
|
|
return &policy, nil
|
|
}
|
|
|
|
func validateStatement(stmt PolicyStatement) error {
|
|
if stmt.Effect != "Allow" && stmt.Effect != "Deny" {
|
|
return fmt.Errorf("effect must be 'Allow' or 'Deny'")
|
|
}
|
|
|
|
if len(stmt.Action) == 0 {
|
|
return fmt.Errorf("statement must specify at least one action")
|
|
}
|
|
|
|
validActions := map[string]bool{
|
|
"termcloud:GetObject": true,
|
|
"termcloud:PutObject": true,
|
|
"termcloud:DeleteObject": true,
|
|
"termcloud:ListObjects": true,
|
|
"termcloud:GetBucket": true,
|
|
"termcloud:DeleteBucket": true,
|
|
"*": true,
|
|
}
|
|
|
|
for _, action := range stmt.Action {
|
|
if !validActions[action] && !strings.HasSuffix(action, "*") {
|
|
return fmt.Errorf("invalid action: %s", action)
|
|
}
|
|
}
|
|
|
|
if len(stmt.Resource) == 0 {
|
|
return fmt.Errorf("statement must specify at least one resource")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BucketService) SetBucketPolicy(ctx context.Context, bucketID int64, policyJSON string) error {
|
|
if _, err := ValidateBucketPolicy(policyJSON); err != nil {
|
|
return fmt.Errorf("invalid policy: %w", err)
|
|
}
|
|
|
|
_, err := s.pool.Exec(ctx, `
|
|
INSERT INTO bucket_policies (bucket_id, policy)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (bucket_id)
|
|
DO UPDATE SET policy = $2, updated_at = NOW()`,
|
|
bucketID, policyJSON)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to set bucket policy: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BucketService) GetBucketPolicy(ctx context.Context, bucketID int64) (*BucketPolicyRecord, error) {
|
|
var record BucketPolicyRecord
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT id, bucket_id, policy, updated_at
|
|
FROM bucket_policies WHERE bucket_id = $1`,
|
|
bucketID).Scan(&record.ID, &record.BucketID, &record.Policy, &record.UpdatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get bucket policy: %w", err)
|
|
}
|
|
|
|
return &record, nil
|
|
}
|
|
|
|
func (s *BucketService) DeleteBucketPolicy(ctx context.Context, bucketID int64) error {
|
|
_, err := s.pool.Exec(ctx, "DELETE FROM bucket_policies WHERE bucket_id = $1", bucketID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete bucket policy: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *BucketService) EvaluatePolicy(ctx context.Context, bucketID int64, action, resource, principal string) (bool, error) {
|
|
policyRecord, err := s.GetBucketPolicy(ctx, bucketID)
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no rows") {
|
|
return true, nil
|
|
}
|
|
return false, err
|
|
}
|
|
|
|
policy, err := ValidateBucketPolicy(policyRecord.Policy)
|
|
if err != nil {
|
|
return false, fmt.Errorf("invalid stored policy: %w", err)
|
|
}
|
|
|
|
for _, stmt := range policy.Statement {
|
|
if matchesStatement(stmt, action, resource, principal) {
|
|
return stmt.Effect == "Allow", nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func matchesStatement(stmt PolicyStatement, action, resource, principal string) bool {
|
|
if !matchesAction(stmt.Action, action) {
|
|
return false
|
|
}
|
|
|
|
if !matchesResource(stmt.Resource, resource) {
|
|
return false
|
|
}
|
|
|
|
if stmt.Principal != nil && !matchesPrincipal(stmt.Principal, principal) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func matchesAction(allowedActions []string, action string) bool {
|
|
for _, allowed := range allowedActions {
|
|
if allowed == "*" || allowed == action {
|
|
return true
|
|
}
|
|
if strings.HasSuffix(allowed, "*") {
|
|
prefix := strings.TrimSuffix(allowed, "*")
|
|
if strings.HasPrefix(action, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchesResource(allowedResources []string, resource string) bool {
|
|
for _, allowed := range allowedResources {
|
|
if allowed == "*" || allowed == resource {
|
|
return true
|
|
}
|
|
if strings.HasSuffix(allowed, "*") {
|
|
prefix := strings.TrimSuffix(allowed, "*")
|
|
if strings.HasPrefix(resource, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchesPrincipal(allowedPrincipal map[string]any, principal string) bool {
|
|
if allowedPrincipal["*"] != nil {
|
|
return true
|
|
}
|
|
|
|
if users, ok := allowedPrincipal["User"].([]any); ok {
|
|
for _, user := range users {
|
|
if userStr, ok := user.(string); ok && userStr == principal {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
if userStr, ok := allowedPrincipal["User"].(string); ok && userStr == principal {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|