policy_engine.go 32 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142
  1. package policy
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "path/filepath"
  7. "regexp"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "time"
  12. )
  13. // Effect represents the policy evaluation result
  14. type Effect string
  15. const (
  16. EffectAllow Effect = "Allow"
  17. EffectDeny Effect = "Deny"
  18. )
  19. // Package-level regex cache for performance optimization
  20. var (
  21. regexCache = make(map[string]*regexp.Regexp)
  22. regexCacheMu sync.RWMutex
  23. )
  24. // PolicyEngine evaluates policies against requests
  25. type PolicyEngine struct {
  26. config *PolicyEngineConfig
  27. initialized bool
  28. store PolicyStore
  29. }
  30. // PolicyEngineConfig holds policy engine configuration
  31. type PolicyEngineConfig struct {
  32. // DefaultEffect when no policies match (Allow or Deny)
  33. DefaultEffect string `json:"defaultEffect"`
  34. // StoreType specifies the policy store backend (memory, filer, etc.)
  35. StoreType string `json:"storeType"`
  36. // StoreConfig contains store-specific configuration
  37. StoreConfig map[string]interface{} `json:"storeConfig,omitempty"`
  38. }
  39. // PolicyDocument represents an IAM policy document
  40. type PolicyDocument struct {
  41. // Version of the policy language (e.g., "2012-10-17")
  42. Version string `json:"Version"`
  43. // Id is an optional policy identifier
  44. Id string `json:"Id,omitempty"`
  45. // Statement contains the policy statements
  46. Statement []Statement `json:"Statement"`
  47. }
  48. // Statement represents a single policy statement
  49. type Statement struct {
  50. // Sid is an optional statement identifier
  51. Sid string `json:"Sid,omitempty"`
  52. // Effect specifies whether to Allow or Deny
  53. Effect string `json:"Effect"`
  54. // Principal specifies who the statement applies to (optional in role policies)
  55. Principal interface{} `json:"Principal,omitempty"`
  56. // NotPrincipal specifies who the statement does NOT apply to
  57. NotPrincipal interface{} `json:"NotPrincipal,omitempty"`
  58. // Action specifies the actions this statement applies to
  59. Action []string `json:"Action"`
  60. // NotAction specifies actions this statement does NOT apply to
  61. NotAction []string `json:"NotAction,omitempty"`
  62. // Resource specifies the resources this statement applies to
  63. Resource []string `json:"Resource"`
  64. // NotResource specifies resources this statement does NOT apply to
  65. NotResource []string `json:"NotResource,omitempty"`
  66. // Condition specifies conditions for when this statement applies
  67. Condition map[string]map[string]interface{} `json:"Condition,omitempty"`
  68. }
  69. // EvaluationContext provides context for policy evaluation
  70. type EvaluationContext struct {
  71. // Principal making the request (e.g., "user:alice", "role:admin")
  72. Principal string `json:"principal"`
  73. // Action being requested (e.g., "s3:GetObject")
  74. Action string `json:"action"`
  75. // Resource being accessed (e.g., "arn:seaweed:s3:::bucket/key")
  76. Resource string `json:"resource"`
  77. // RequestContext contains additional request information
  78. RequestContext map[string]interface{} `json:"requestContext,omitempty"`
  79. }
  80. // EvaluationResult contains the result of policy evaluation
  81. type EvaluationResult struct {
  82. // Effect is the final decision (Allow or Deny)
  83. Effect Effect `json:"effect"`
  84. // MatchingStatements contains statements that matched the request
  85. MatchingStatements []StatementMatch `json:"matchingStatements,omitempty"`
  86. // EvaluationDetails provides detailed evaluation information
  87. EvaluationDetails *EvaluationDetails `json:"evaluationDetails,omitempty"`
  88. }
  89. // StatementMatch represents a statement that matched during evaluation
  90. type StatementMatch struct {
  91. // PolicyName is the name of the policy containing this statement
  92. PolicyName string `json:"policyName"`
  93. // StatementSid is the statement identifier
  94. StatementSid string `json:"statementSid,omitempty"`
  95. // Effect is the effect of this statement
  96. Effect Effect `json:"effect"`
  97. // Reason explains why this statement matched
  98. Reason string `json:"reason,omitempty"`
  99. }
  100. // EvaluationDetails provides detailed information about policy evaluation
  101. type EvaluationDetails struct {
  102. // Principal that was evaluated
  103. Principal string `json:"principal"`
  104. // Action that was evaluated
  105. Action string `json:"action"`
  106. // Resource that was evaluated
  107. Resource string `json:"resource"`
  108. // PoliciesEvaluated lists all policies that were evaluated
  109. PoliciesEvaluated []string `json:"policiesEvaluated"`
  110. // ConditionsEvaluated lists all conditions that were evaluated
  111. ConditionsEvaluated []string `json:"conditionsEvaluated,omitempty"`
  112. }
  113. // PolicyStore defines the interface for storing and retrieving policies
  114. type PolicyStore interface {
  115. // StorePolicy stores a policy document (filerAddress ignored for memory stores)
  116. StorePolicy(ctx context.Context, filerAddress string, name string, policy *PolicyDocument) error
  117. // GetPolicy retrieves a policy document (filerAddress ignored for memory stores)
  118. GetPolicy(ctx context.Context, filerAddress string, name string) (*PolicyDocument, error)
  119. // DeletePolicy deletes a policy document (filerAddress ignored for memory stores)
  120. DeletePolicy(ctx context.Context, filerAddress string, name string) error
  121. // ListPolicies lists all policy names (filerAddress ignored for memory stores)
  122. ListPolicies(ctx context.Context, filerAddress string) ([]string, error)
  123. }
  124. // NewPolicyEngine creates a new policy engine
  125. func NewPolicyEngine() *PolicyEngine {
  126. return &PolicyEngine{}
  127. }
  128. // Initialize initializes the policy engine with configuration
  129. func (e *PolicyEngine) Initialize(config *PolicyEngineConfig) error {
  130. if config == nil {
  131. return fmt.Errorf("config cannot be nil")
  132. }
  133. if err := e.validateConfig(config); err != nil {
  134. return fmt.Errorf("invalid configuration: %w", err)
  135. }
  136. e.config = config
  137. // Initialize policy store
  138. store, err := e.createPolicyStore(config)
  139. if err != nil {
  140. return fmt.Errorf("failed to create policy store: %w", err)
  141. }
  142. e.store = store
  143. e.initialized = true
  144. return nil
  145. }
  146. // InitializeWithProvider initializes the policy engine with configuration and a filer address provider
  147. func (e *PolicyEngine) InitializeWithProvider(config *PolicyEngineConfig, filerAddressProvider func() string) error {
  148. if config == nil {
  149. return fmt.Errorf("config cannot be nil")
  150. }
  151. if err := e.validateConfig(config); err != nil {
  152. return fmt.Errorf("invalid configuration: %w", err)
  153. }
  154. e.config = config
  155. // Initialize policy store with provider
  156. store, err := e.createPolicyStoreWithProvider(config, filerAddressProvider)
  157. if err != nil {
  158. return fmt.Errorf("failed to create policy store: %w", err)
  159. }
  160. e.store = store
  161. e.initialized = true
  162. return nil
  163. }
  164. // validateConfig validates the policy engine configuration
  165. func (e *PolicyEngine) validateConfig(config *PolicyEngineConfig) error {
  166. if config.DefaultEffect != "Allow" && config.DefaultEffect != "Deny" {
  167. return fmt.Errorf("invalid default effect: %s", config.DefaultEffect)
  168. }
  169. if config.StoreType == "" {
  170. config.StoreType = "filer" // Default to filer store for persistence
  171. }
  172. return nil
  173. }
  174. // createPolicyStore creates a policy store based on configuration
  175. func (e *PolicyEngine) createPolicyStore(config *PolicyEngineConfig) (PolicyStore, error) {
  176. switch config.StoreType {
  177. case "memory":
  178. return NewMemoryPolicyStore(), nil
  179. case "", "filer":
  180. // Check if caching is explicitly disabled
  181. if config.StoreConfig != nil {
  182. if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
  183. return NewFilerPolicyStore(config.StoreConfig, nil)
  184. }
  185. }
  186. // Default to generic cached filer store for better performance
  187. return NewGenericCachedPolicyStore(config.StoreConfig, nil)
  188. case "cached-filer", "generic-cached":
  189. return NewGenericCachedPolicyStore(config.StoreConfig, nil)
  190. default:
  191. return nil, fmt.Errorf("unsupported store type: %s", config.StoreType)
  192. }
  193. }
  194. // createPolicyStoreWithProvider creates a policy store with a filer address provider function
  195. func (e *PolicyEngine) createPolicyStoreWithProvider(config *PolicyEngineConfig, filerAddressProvider func() string) (PolicyStore, error) {
  196. switch config.StoreType {
  197. case "memory":
  198. return NewMemoryPolicyStore(), nil
  199. case "", "filer":
  200. // Check if caching is explicitly disabled
  201. if config.StoreConfig != nil {
  202. if noCache, ok := config.StoreConfig["noCache"].(bool); ok && noCache {
  203. return NewFilerPolicyStore(config.StoreConfig, filerAddressProvider)
  204. }
  205. }
  206. // Default to generic cached filer store for better performance
  207. return NewGenericCachedPolicyStore(config.StoreConfig, filerAddressProvider)
  208. case "cached-filer", "generic-cached":
  209. return NewGenericCachedPolicyStore(config.StoreConfig, filerAddressProvider)
  210. default:
  211. return nil, fmt.Errorf("unsupported store type: %s", config.StoreType)
  212. }
  213. }
  214. // IsInitialized returns whether the engine is initialized
  215. func (e *PolicyEngine) IsInitialized() bool {
  216. return e.initialized
  217. }
  218. // AddPolicy adds a policy to the engine (filerAddress ignored for memory stores)
  219. func (e *PolicyEngine) AddPolicy(filerAddress string, name string, policy *PolicyDocument) error {
  220. if !e.initialized {
  221. return fmt.Errorf("policy engine not initialized")
  222. }
  223. if name == "" {
  224. return fmt.Errorf("policy name cannot be empty")
  225. }
  226. if policy == nil {
  227. return fmt.Errorf("policy cannot be nil")
  228. }
  229. if err := ValidatePolicyDocument(policy); err != nil {
  230. return fmt.Errorf("invalid policy document: %w", err)
  231. }
  232. return e.store.StorePolicy(context.Background(), filerAddress, name, policy)
  233. }
  234. // Evaluate evaluates policies against a request context (filerAddress ignored for memory stores)
  235. func (e *PolicyEngine) Evaluate(ctx context.Context, filerAddress string, evalCtx *EvaluationContext, policyNames []string) (*EvaluationResult, error) {
  236. if !e.initialized {
  237. return nil, fmt.Errorf("policy engine not initialized")
  238. }
  239. if evalCtx == nil {
  240. return nil, fmt.Errorf("evaluation context cannot be nil")
  241. }
  242. result := &EvaluationResult{
  243. Effect: Effect(e.config.DefaultEffect),
  244. EvaluationDetails: &EvaluationDetails{
  245. Principal: evalCtx.Principal,
  246. Action: evalCtx.Action,
  247. Resource: evalCtx.Resource,
  248. PoliciesEvaluated: policyNames,
  249. },
  250. }
  251. var matchingStatements []StatementMatch
  252. explicitDeny := false
  253. hasAllow := false
  254. // Evaluate each policy
  255. for _, policyName := range policyNames {
  256. policy, err := e.store.GetPolicy(ctx, filerAddress, policyName)
  257. if err != nil {
  258. continue // Skip policies that can't be loaded
  259. }
  260. // Evaluate each statement in the policy
  261. for _, statement := range policy.Statement {
  262. if e.statementMatches(&statement, evalCtx) {
  263. match := StatementMatch{
  264. PolicyName: policyName,
  265. StatementSid: statement.Sid,
  266. Effect: Effect(statement.Effect),
  267. Reason: "Action, Resource, and Condition matched",
  268. }
  269. matchingStatements = append(matchingStatements, match)
  270. if statement.Effect == "Deny" {
  271. explicitDeny = true
  272. } else if statement.Effect == "Allow" {
  273. hasAllow = true
  274. }
  275. }
  276. }
  277. }
  278. result.MatchingStatements = matchingStatements
  279. // AWS IAM evaluation logic:
  280. // 1. If there's an explicit Deny, the result is Deny
  281. // 2. If there's an Allow and no Deny, the result is Allow
  282. // 3. Otherwise, use the default effect
  283. if explicitDeny {
  284. result.Effect = EffectDeny
  285. } else if hasAllow {
  286. result.Effect = EffectAllow
  287. }
  288. return result, nil
  289. }
  290. // statementMatches checks if a statement matches the evaluation context
  291. func (e *PolicyEngine) statementMatches(statement *Statement, evalCtx *EvaluationContext) bool {
  292. // Check action match
  293. if !e.matchesActions(statement.Action, evalCtx.Action, evalCtx) {
  294. return false
  295. }
  296. // Check resource match
  297. if !e.matchesResources(statement.Resource, evalCtx.Resource, evalCtx) {
  298. return false
  299. }
  300. // Check conditions
  301. if !e.matchesConditions(statement.Condition, evalCtx) {
  302. return false
  303. }
  304. return true
  305. }
  306. // matchesActions checks if any action in the list matches the requested action
  307. func (e *PolicyEngine) matchesActions(actions []string, requestedAction string, evalCtx *EvaluationContext) bool {
  308. for _, action := range actions {
  309. if awsIAMMatch(action, requestedAction, evalCtx) {
  310. return true
  311. }
  312. }
  313. return false
  314. }
  315. // matchesResources checks if any resource in the list matches the requested resource
  316. func (e *PolicyEngine) matchesResources(resources []string, requestedResource string, evalCtx *EvaluationContext) bool {
  317. for _, resource := range resources {
  318. if awsIAMMatch(resource, requestedResource, evalCtx) {
  319. return true
  320. }
  321. }
  322. return false
  323. }
  324. // matchesConditions checks if all conditions are satisfied
  325. func (e *PolicyEngine) matchesConditions(conditions map[string]map[string]interface{}, evalCtx *EvaluationContext) bool {
  326. if len(conditions) == 0 {
  327. return true // No conditions means always match
  328. }
  329. for conditionType, conditionBlock := range conditions {
  330. if !e.evaluateConditionBlock(conditionType, conditionBlock, evalCtx) {
  331. return false
  332. }
  333. }
  334. return true
  335. }
  336. // evaluateConditionBlock evaluates a single condition block
  337. func (e *PolicyEngine) evaluateConditionBlock(conditionType string, block map[string]interface{}, evalCtx *EvaluationContext) bool {
  338. switch conditionType {
  339. // IP Address conditions
  340. case "IpAddress":
  341. return e.evaluateIPCondition(block, evalCtx, true)
  342. case "NotIpAddress":
  343. return e.evaluateIPCondition(block, evalCtx, false)
  344. // String conditions
  345. case "StringEquals":
  346. return e.EvaluateStringCondition(block, evalCtx, true, false)
  347. case "StringNotEquals":
  348. return e.EvaluateStringCondition(block, evalCtx, false, false)
  349. case "StringLike":
  350. return e.EvaluateStringCondition(block, evalCtx, true, true)
  351. case "StringEqualsIgnoreCase":
  352. return e.evaluateStringConditionIgnoreCase(block, evalCtx, true, false)
  353. case "StringNotEqualsIgnoreCase":
  354. return e.evaluateStringConditionIgnoreCase(block, evalCtx, false, false)
  355. case "StringLikeIgnoreCase":
  356. return e.evaluateStringConditionIgnoreCase(block, evalCtx, true, true)
  357. // Numeric conditions
  358. case "NumericEquals":
  359. return e.evaluateNumericCondition(block, evalCtx, "==")
  360. case "NumericNotEquals":
  361. return e.evaluateNumericCondition(block, evalCtx, "!=")
  362. case "NumericLessThan":
  363. return e.evaluateNumericCondition(block, evalCtx, "<")
  364. case "NumericLessThanEquals":
  365. return e.evaluateNumericCondition(block, evalCtx, "<=")
  366. case "NumericGreaterThan":
  367. return e.evaluateNumericCondition(block, evalCtx, ">")
  368. case "NumericGreaterThanEquals":
  369. return e.evaluateNumericCondition(block, evalCtx, ">=")
  370. // Date conditions
  371. case "DateEquals":
  372. return e.evaluateDateCondition(block, evalCtx, "==")
  373. case "DateNotEquals":
  374. return e.evaluateDateCondition(block, evalCtx, "!=")
  375. case "DateLessThan":
  376. return e.evaluateDateCondition(block, evalCtx, "<")
  377. case "DateLessThanEquals":
  378. return e.evaluateDateCondition(block, evalCtx, "<=")
  379. case "DateGreaterThan":
  380. return e.evaluateDateCondition(block, evalCtx, ">")
  381. case "DateGreaterThanEquals":
  382. return e.evaluateDateCondition(block, evalCtx, ">=")
  383. // Boolean conditions
  384. case "Bool":
  385. return e.evaluateBoolCondition(block, evalCtx)
  386. // Null conditions
  387. case "Null":
  388. return e.evaluateNullCondition(block, evalCtx)
  389. default:
  390. // Unknown condition types default to false (more secure)
  391. return false
  392. }
  393. }
  394. // evaluateIPCondition evaluates IP address conditions
  395. func (e *PolicyEngine) evaluateIPCondition(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool) bool {
  396. sourceIP, exists := evalCtx.RequestContext["sourceIP"]
  397. if !exists {
  398. return !shouldMatch // If no IP in context, condition fails for positive match
  399. }
  400. sourceIPStr, ok := sourceIP.(string)
  401. if !ok {
  402. return !shouldMatch
  403. }
  404. sourceIPAddr := net.ParseIP(sourceIPStr)
  405. if sourceIPAddr == nil {
  406. return !shouldMatch
  407. }
  408. for key, value := range block {
  409. if key == "seaweed:SourceIP" {
  410. ranges, ok := value.([]string)
  411. if !ok {
  412. continue
  413. }
  414. for _, ipRange := range ranges {
  415. if strings.Contains(ipRange, "/") {
  416. // CIDR range
  417. _, cidr, err := net.ParseCIDR(ipRange)
  418. if err != nil {
  419. continue
  420. }
  421. if cidr.Contains(sourceIPAddr) {
  422. return shouldMatch
  423. }
  424. } else {
  425. // Single IP
  426. if sourceIPStr == ipRange {
  427. return shouldMatch
  428. }
  429. }
  430. }
  431. }
  432. }
  433. return !shouldMatch
  434. }
  435. // EvaluateStringCondition evaluates string-based conditions
  436. func (e *PolicyEngine) EvaluateStringCondition(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool, useWildcard bool) bool {
  437. // Iterate through all condition keys in the block
  438. for conditionKey, conditionValue := range block {
  439. // Get the context values for this condition key
  440. contextValues, exists := evalCtx.RequestContext[conditionKey]
  441. if !exists {
  442. // If the context key doesn't exist, condition fails for positive match
  443. if shouldMatch {
  444. return false
  445. }
  446. continue
  447. }
  448. // Convert context value to string slice
  449. var contextStrings []string
  450. switch v := contextValues.(type) {
  451. case string:
  452. contextStrings = []string{v}
  453. case []string:
  454. contextStrings = v
  455. case []interface{}:
  456. for _, item := range v {
  457. if str, ok := item.(string); ok {
  458. contextStrings = append(contextStrings, str)
  459. }
  460. }
  461. default:
  462. // Convert to string as fallback
  463. contextStrings = []string{fmt.Sprintf("%v", v)}
  464. }
  465. // Convert condition value to string slice
  466. var expectedStrings []string
  467. switch v := conditionValue.(type) {
  468. case string:
  469. expectedStrings = []string{v}
  470. case []string:
  471. expectedStrings = v
  472. case []interface{}:
  473. for _, item := range v {
  474. if str, ok := item.(string); ok {
  475. expectedStrings = append(expectedStrings, str)
  476. } else {
  477. expectedStrings = append(expectedStrings, fmt.Sprintf("%v", item))
  478. }
  479. }
  480. default:
  481. expectedStrings = []string{fmt.Sprintf("%v", v)}
  482. }
  483. // Evaluate the condition using AWS IAM-compliant matching
  484. conditionMet := false
  485. for _, expected := range expectedStrings {
  486. for _, contextValue := range contextStrings {
  487. if useWildcard {
  488. // Use AWS IAM-compliant wildcard matching for StringLike conditions
  489. // This handles case-insensitivity and policy variables
  490. if awsIAMMatch(expected, contextValue, evalCtx) {
  491. conditionMet = true
  492. break
  493. }
  494. } else {
  495. // For StringEquals/StringNotEquals, also support policy variables but be case-sensitive
  496. expandedExpected := expandPolicyVariables(expected, evalCtx)
  497. if expandedExpected == contextValue {
  498. conditionMet = true
  499. break
  500. }
  501. }
  502. }
  503. if conditionMet {
  504. break
  505. }
  506. }
  507. // For shouldMatch=true (StringEquals, StringLike): condition must be met
  508. // For shouldMatch=false (StringNotEquals): condition must NOT be met
  509. if shouldMatch && !conditionMet {
  510. return false
  511. }
  512. if !shouldMatch && conditionMet {
  513. return false
  514. }
  515. }
  516. return true
  517. }
  518. // ValidatePolicyDocument validates a policy document structure
  519. func ValidatePolicyDocument(policy *PolicyDocument) error {
  520. return ValidatePolicyDocumentWithType(policy, "resource")
  521. }
  522. // ValidateTrustPolicyDocument validates a trust policy document structure
  523. func ValidateTrustPolicyDocument(policy *PolicyDocument) error {
  524. return ValidatePolicyDocumentWithType(policy, "trust")
  525. }
  526. // ValidatePolicyDocumentWithType validates a policy document for specific type
  527. func ValidatePolicyDocumentWithType(policy *PolicyDocument, policyType string) error {
  528. if policy == nil {
  529. return fmt.Errorf("policy document cannot be nil")
  530. }
  531. if policy.Version == "" {
  532. return fmt.Errorf("version is required")
  533. }
  534. if len(policy.Statement) == 0 {
  535. return fmt.Errorf("at least one statement is required")
  536. }
  537. for i, statement := range policy.Statement {
  538. if err := validateStatementWithType(&statement, policyType); err != nil {
  539. return fmt.Errorf("statement %d is invalid: %w", i, err)
  540. }
  541. }
  542. return nil
  543. }
  544. // validateStatement validates a single statement (for backward compatibility)
  545. func validateStatement(statement *Statement) error {
  546. return validateStatementWithType(statement, "resource")
  547. }
  548. // validateStatementWithType validates a single statement based on policy type
  549. func validateStatementWithType(statement *Statement, policyType string) error {
  550. if statement.Effect != "Allow" && statement.Effect != "Deny" {
  551. return fmt.Errorf("invalid effect: %s (must be Allow or Deny)", statement.Effect)
  552. }
  553. if len(statement.Action) == 0 {
  554. return fmt.Errorf("at least one action is required")
  555. }
  556. // Trust policies don't require Resource field, but resource policies do
  557. if policyType == "resource" {
  558. if len(statement.Resource) == 0 {
  559. return fmt.Errorf("at least one resource is required")
  560. }
  561. } else if policyType == "trust" {
  562. // Trust policies should have Principal field
  563. if statement.Principal == nil {
  564. return fmt.Errorf("trust policy statement must have Principal field")
  565. }
  566. // Trust policies typically have specific actions
  567. validTrustActions := map[string]bool{
  568. "sts:AssumeRole": true,
  569. "sts:AssumeRoleWithWebIdentity": true,
  570. "sts:AssumeRoleWithCredentials": true,
  571. }
  572. for _, action := range statement.Action {
  573. if !validTrustActions[action] {
  574. return fmt.Errorf("invalid action for trust policy: %s", action)
  575. }
  576. }
  577. }
  578. return nil
  579. }
  580. // matchResource checks if a resource pattern matches a requested resource
  581. // Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
  582. func matchResource(pattern, resource string) bool {
  583. if pattern == resource {
  584. return true
  585. }
  586. // Handle simple suffix wildcard (backward compatibility)
  587. if strings.HasSuffix(pattern, "*") {
  588. prefix := pattern[:len(pattern)-1]
  589. return strings.HasPrefix(resource, prefix)
  590. }
  591. // For complex patterns, use filepath.Match for advanced wildcard support (*, ?, [])
  592. matched, err := filepath.Match(pattern, resource)
  593. if err != nil {
  594. // Fallback to exact match if pattern is malformed
  595. return pattern == resource
  596. }
  597. return matched
  598. }
  599. // awsIAMMatch performs AWS IAM-compliant pattern matching with case-insensitivity and policy variable support
  600. func awsIAMMatch(pattern, value string, evalCtx *EvaluationContext) bool {
  601. // Step 1: Substitute policy variables (e.g., ${aws:username}, ${saml:username})
  602. expandedPattern := expandPolicyVariables(pattern, evalCtx)
  603. // Step 2: Handle special patterns
  604. if expandedPattern == "*" {
  605. return true // Universal wildcard
  606. }
  607. // Step 3: Case-insensitive exact match
  608. if strings.EqualFold(expandedPattern, value) {
  609. return true
  610. }
  611. // Step 4: Handle AWS-style wildcards (case-insensitive)
  612. if strings.Contains(expandedPattern, "*") || strings.Contains(expandedPattern, "?") {
  613. return AwsWildcardMatch(expandedPattern, value)
  614. }
  615. return false
  616. }
  617. // expandPolicyVariables substitutes AWS policy variables in the pattern
  618. func expandPolicyVariables(pattern string, evalCtx *EvaluationContext) string {
  619. if evalCtx == nil || evalCtx.RequestContext == nil {
  620. return pattern
  621. }
  622. expanded := pattern
  623. // Common AWS policy variables that might be used in SeaweedFS
  624. variableMap := map[string]string{
  625. "${aws:username}": getContextValue(evalCtx, "aws:username", ""),
  626. "${saml:username}": getContextValue(evalCtx, "saml:username", ""),
  627. "${oidc:sub}": getContextValue(evalCtx, "oidc:sub", ""),
  628. "${aws:userid}": getContextValue(evalCtx, "aws:userid", ""),
  629. "${aws:principaltype}": getContextValue(evalCtx, "aws:principaltype", ""),
  630. }
  631. for variable, value := range variableMap {
  632. if value != "" {
  633. expanded = strings.ReplaceAll(expanded, variable, value)
  634. }
  635. }
  636. return expanded
  637. }
  638. // getContextValue safely gets a value from the evaluation context
  639. func getContextValue(evalCtx *EvaluationContext, key, defaultValue string) string {
  640. if value, exists := evalCtx.RequestContext[key]; exists {
  641. if str, ok := value.(string); ok {
  642. return str
  643. }
  644. }
  645. return defaultValue
  646. }
  647. // AwsWildcardMatch performs case-insensitive wildcard matching like AWS IAM
  648. func AwsWildcardMatch(pattern, value string) bool {
  649. // Create regex pattern key for caching
  650. // First escape all regex metacharacters, then replace wildcards
  651. regexPattern := regexp.QuoteMeta(pattern)
  652. regexPattern = strings.ReplaceAll(regexPattern, "\\*", ".*")
  653. regexPattern = strings.ReplaceAll(regexPattern, "\\?", ".")
  654. regexPattern = "^" + regexPattern + "$"
  655. regexKey := "(?i)" + regexPattern
  656. // Try to get compiled regex from cache
  657. regexCacheMu.RLock()
  658. regex, found := regexCache[regexKey]
  659. regexCacheMu.RUnlock()
  660. if !found {
  661. // Compile and cache the regex
  662. compiledRegex, err := regexp.Compile(regexKey)
  663. if err != nil {
  664. // Fallback to simple case-insensitive comparison if regex fails
  665. return strings.EqualFold(pattern, value)
  666. }
  667. // Store in cache with write lock
  668. regexCacheMu.Lock()
  669. // Double-check in case another goroutine added it
  670. if existingRegex, exists := regexCache[regexKey]; exists {
  671. regex = existingRegex
  672. } else {
  673. regexCache[regexKey] = compiledRegex
  674. regex = compiledRegex
  675. }
  676. regexCacheMu.Unlock()
  677. }
  678. return regex.MatchString(value)
  679. }
  680. // matchAction checks if an action pattern matches a requested action
  681. // Uses hybrid approach: simple suffix wildcards for compatibility, filepath.Match for complex patterns
  682. func matchAction(pattern, action string) bool {
  683. if pattern == action {
  684. return true
  685. }
  686. // Handle simple suffix wildcard (backward compatibility)
  687. if strings.HasSuffix(pattern, "*") {
  688. prefix := pattern[:len(pattern)-1]
  689. return strings.HasPrefix(action, prefix)
  690. }
  691. // For complex patterns, use filepath.Match for advanced wildcard support (*, ?, [])
  692. matched, err := filepath.Match(pattern, action)
  693. if err != nil {
  694. // Fallback to exact match if pattern is malformed
  695. return pattern == action
  696. }
  697. return matched
  698. }
  699. // evaluateStringConditionIgnoreCase evaluates string conditions with case insensitivity
  700. func (e *PolicyEngine) evaluateStringConditionIgnoreCase(block map[string]interface{}, evalCtx *EvaluationContext, shouldMatch bool, useWildcard bool) bool {
  701. for key, expectedValues := range block {
  702. contextValue, exists := evalCtx.RequestContext[key]
  703. if !exists {
  704. if !shouldMatch {
  705. continue // For NotEquals, missing key is OK
  706. }
  707. return false
  708. }
  709. contextStr, ok := contextValue.(string)
  710. if !ok {
  711. return false
  712. }
  713. contextStr = strings.ToLower(contextStr)
  714. matched := false
  715. // Handle different value types
  716. switch v := expectedValues.(type) {
  717. case string:
  718. expectedStr := strings.ToLower(v)
  719. if useWildcard {
  720. matched, _ = filepath.Match(expectedStr, contextStr)
  721. } else {
  722. matched = expectedStr == contextStr
  723. }
  724. case []interface{}:
  725. for _, val := range v {
  726. if valStr, ok := val.(string); ok {
  727. expectedStr := strings.ToLower(valStr)
  728. if useWildcard {
  729. if m, _ := filepath.Match(expectedStr, contextStr); m {
  730. matched = true
  731. break
  732. }
  733. } else {
  734. if expectedStr == contextStr {
  735. matched = true
  736. break
  737. }
  738. }
  739. }
  740. }
  741. }
  742. if shouldMatch && !matched {
  743. return false
  744. }
  745. if !shouldMatch && matched {
  746. return false
  747. }
  748. }
  749. return true
  750. }
  751. // evaluateNumericCondition evaluates numeric conditions
  752. func (e *PolicyEngine) evaluateNumericCondition(block map[string]interface{}, evalCtx *EvaluationContext, operator string) bool {
  753. for key, expectedValues := range block {
  754. contextValue, exists := evalCtx.RequestContext[key]
  755. if !exists {
  756. return false
  757. }
  758. contextNum, err := parseNumeric(contextValue)
  759. if err != nil {
  760. return false
  761. }
  762. matched := false
  763. // Handle different value types
  764. switch v := expectedValues.(type) {
  765. case string:
  766. expectedNum, err := parseNumeric(v)
  767. if err != nil {
  768. return false
  769. }
  770. matched = compareNumbers(contextNum, expectedNum, operator)
  771. case []interface{}:
  772. for _, val := range v {
  773. expectedNum, err := parseNumeric(val)
  774. if err != nil {
  775. continue
  776. }
  777. if compareNumbers(contextNum, expectedNum, operator) {
  778. matched = true
  779. break
  780. }
  781. }
  782. }
  783. if !matched {
  784. return false
  785. }
  786. }
  787. return true
  788. }
  789. // evaluateDateCondition evaluates date conditions
  790. func (e *PolicyEngine) evaluateDateCondition(block map[string]interface{}, evalCtx *EvaluationContext, operator string) bool {
  791. for key, expectedValues := range block {
  792. contextValue, exists := evalCtx.RequestContext[key]
  793. if !exists {
  794. return false
  795. }
  796. contextTime, err := parseDateTime(contextValue)
  797. if err != nil {
  798. return false
  799. }
  800. matched := false
  801. // Handle different value types
  802. switch v := expectedValues.(type) {
  803. case string:
  804. expectedTime, err := parseDateTime(v)
  805. if err != nil {
  806. return false
  807. }
  808. matched = compareDates(contextTime, expectedTime, operator)
  809. case []interface{}:
  810. for _, val := range v {
  811. expectedTime, err := parseDateTime(val)
  812. if err != nil {
  813. continue
  814. }
  815. if compareDates(contextTime, expectedTime, operator) {
  816. matched = true
  817. break
  818. }
  819. }
  820. }
  821. if !matched {
  822. return false
  823. }
  824. }
  825. return true
  826. }
  827. // evaluateBoolCondition evaluates boolean conditions
  828. func (e *PolicyEngine) evaluateBoolCondition(block map[string]interface{}, evalCtx *EvaluationContext) bool {
  829. for key, expectedValues := range block {
  830. contextValue, exists := evalCtx.RequestContext[key]
  831. if !exists {
  832. return false
  833. }
  834. contextBool, err := parseBool(contextValue)
  835. if err != nil {
  836. return false
  837. }
  838. matched := false
  839. // Handle different value types
  840. switch v := expectedValues.(type) {
  841. case string:
  842. expectedBool, err := parseBool(v)
  843. if err != nil {
  844. return false
  845. }
  846. matched = contextBool == expectedBool
  847. case bool:
  848. matched = contextBool == v
  849. case []interface{}:
  850. for _, val := range v {
  851. expectedBool, err := parseBool(val)
  852. if err != nil {
  853. continue
  854. }
  855. if contextBool == expectedBool {
  856. matched = true
  857. break
  858. }
  859. }
  860. }
  861. if !matched {
  862. return false
  863. }
  864. }
  865. return true
  866. }
  867. // evaluateNullCondition evaluates null conditions
  868. func (e *PolicyEngine) evaluateNullCondition(block map[string]interface{}, evalCtx *EvaluationContext) bool {
  869. for key, expectedValues := range block {
  870. _, exists := evalCtx.RequestContext[key]
  871. expectedNull := false
  872. switch v := expectedValues.(type) {
  873. case string:
  874. expectedNull = v == "true"
  875. case bool:
  876. expectedNull = v
  877. }
  878. // If we expect null (true) and key exists, or expect non-null (false) and key doesn't exist
  879. if expectedNull == exists {
  880. return false
  881. }
  882. }
  883. return true
  884. }
  885. // Helper functions for parsing and comparing values
  886. // parseNumeric parses a value as a float64
  887. func parseNumeric(value interface{}) (float64, error) {
  888. switch v := value.(type) {
  889. case float64:
  890. return v, nil
  891. case float32:
  892. return float64(v), nil
  893. case int:
  894. return float64(v), nil
  895. case int64:
  896. return float64(v), nil
  897. case string:
  898. return strconv.ParseFloat(v, 64)
  899. default:
  900. return 0, fmt.Errorf("cannot parse %T as numeric", value)
  901. }
  902. }
  903. // compareNumbers compares two numbers using the given operator
  904. func compareNumbers(a, b float64, operator string) bool {
  905. switch operator {
  906. case "==":
  907. return a == b
  908. case "!=":
  909. return a != b
  910. case "<":
  911. return a < b
  912. case "<=":
  913. return a <= b
  914. case ">":
  915. return a > b
  916. case ">=":
  917. return a >= b
  918. default:
  919. return false
  920. }
  921. }
  922. // parseDateTime parses a value as a time.Time
  923. func parseDateTime(value interface{}) (time.Time, error) {
  924. switch v := value.(type) {
  925. case string:
  926. // Try common date formats
  927. formats := []string{
  928. time.RFC3339,
  929. "2006-01-02T15:04:05Z",
  930. "2006-01-02T15:04:05",
  931. "2006-01-02 15:04:05",
  932. "2006-01-02",
  933. }
  934. for _, format := range formats {
  935. if t, err := time.Parse(format, v); err == nil {
  936. return t, nil
  937. }
  938. }
  939. return time.Time{}, fmt.Errorf("cannot parse date: %s", v)
  940. case time.Time:
  941. return v, nil
  942. default:
  943. return time.Time{}, fmt.Errorf("cannot parse %T as date", value)
  944. }
  945. }
  946. // compareDates compares two dates using the given operator
  947. func compareDates(a, b time.Time, operator string) bool {
  948. switch operator {
  949. case "==":
  950. return a.Equal(b)
  951. case "!=":
  952. return !a.Equal(b)
  953. case "<":
  954. return a.Before(b)
  955. case "<=":
  956. return a.Before(b) || a.Equal(b)
  957. case ">":
  958. return a.After(b)
  959. case ">=":
  960. return a.After(b) || a.Equal(b)
  961. default:
  962. return false
  963. }
  964. }
  965. // parseBool parses a value as a boolean
  966. func parseBool(value interface{}) (bool, error) {
  967. switch v := value.(type) {
  968. case bool:
  969. return v, nil
  970. case string:
  971. return strconv.ParseBool(v)
  972. default:
  973. return false, fmt.Errorf("cannot parse %T as boolean", value)
  974. }
  975. }