integration.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. package policy_engine
  2. import (
  3. "fmt"
  4. "net/http"
  5. "strings"
  6. "github.com/seaweedfs/seaweedfs/weed/glog"
  7. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  8. )
  9. // Action represents an S3 action - this should match the type in auth_credentials.go
  10. type Action string
  11. // Identity represents a user identity - this should match the type in auth_credentials.go
  12. type Identity interface {
  13. canDo(action Action, bucket string, objectKey string) bool
  14. }
  15. // PolicyBackedIAM provides policy-based access control with fallback to legacy IAM
  16. type PolicyBackedIAM struct {
  17. policyEngine *PolicyEngine
  18. legacyIAM LegacyIAM // Interface to delegate to existing IAM system
  19. }
  20. // LegacyIAM interface for delegating to existing IAM implementation
  21. type LegacyIAM interface {
  22. authRequest(r *http.Request, action Action) (Identity, s3err.ErrorCode)
  23. }
  24. // NewPolicyBackedIAM creates a new policy-backed IAM system
  25. func NewPolicyBackedIAM() *PolicyBackedIAM {
  26. return &PolicyBackedIAM{
  27. policyEngine: NewPolicyEngine(),
  28. legacyIAM: nil, // Will be set when integrated with existing IAM
  29. }
  30. }
  31. // NewPolicyBackedIAMWithLegacy creates a new policy-backed IAM system with legacy IAM set
  32. func NewPolicyBackedIAMWithLegacy(legacyIAM LegacyIAM) *PolicyBackedIAM {
  33. return &PolicyBackedIAM{
  34. policyEngine: NewPolicyEngine(),
  35. legacyIAM: legacyIAM,
  36. }
  37. }
  38. // SetLegacyIAM sets the legacy IAM system for fallback
  39. func (p *PolicyBackedIAM) SetLegacyIAM(legacyIAM LegacyIAM) {
  40. p.legacyIAM = legacyIAM
  41. }
  42. // SetBucketPolicy sets the policy for a bucket
  43. func (p *PolicyBackedIAM) SetBucketPolicy(bucketName string, policyJSON string) error {
  44. return p.policyEngine.SetBucketPolicy(bucketName, policyJSON)
  45. }
  46. // GetBucketPolicy gets the policy for a bucket
  47. func (p *PolicyBackedIAM) GetBucketPolicy(bucketName string) (*PolicyDocument, error) {
  48. return p.policyEngine.GetBucketPolicy(bucketName)
  49. }
  50. // DeleteBucketPolicy deletes the policy for a bucket
  51. func (p *PolicyBackedIAM) DeleteBucketPolicy(bucketName string) error {
  52. return p.policyEngine.DeleteBucketPolicy(bucketName)
  53. }
  54. // CanDo checks if a principal can perform an action on a resource
  55. func (p *PolicyBackedIAM) CanDo(action, bucketName, objectName, principal string, r *http.Request) bool {
  56. // If there's a bucket policy, evaluate it
  57. if p.policyEngine.HasPolicyForBucket(bucketName) {
  58. result := p.policyEngine.EvaluatePolicyForRequest(bucketName, objectName, action, principal, r)
  59. switch result {
  60. case PolicyResultAllow:
  61. return true
  62. case PolicyResultDeny:
  63. return false
  64. case PolicyResultIndeterminate:
  65. // Fall through to legacy system
  66. }
  67. }
  68. // No bucket policy or indeterminate result, use legacy conversion
  69. return p.evaluateLegacyAction(action, bucketName, objectName, principal)
  70. }
  71. // evaluateLegacyAction evaluates actions using legacy identity-based rules
  72. func (p *PolicyBackedIAM) evaluateLegacyAction(action, bucketName, objectName, principal string) bool {
  73. // If we have a legacy IAM system to delegate to, use it
  74. if p.legacyIAM != nil {
  75. // Create a dummy request for legacy evaluation
  76. // In real implementation, this would use the actual request
  77. r := &http.Request{
  78. Header: make(http.Header),
  79. }
  80. // Convert the action string to Action type
  81. legacyAction := Action(action)
  82. // Use legacy IAM to check permission
  83. identity, errCode := p.legacyIAM.authRequest(r, legacyAction)
  84. if errCode != s3err.ErrNone {
  85. return false
  86. }
  87. // If we have an identity, check if it can perform the action
  88. if identity != nil {
  89. return identity.canDo(legacyAction, bucketName, objectName)
  90. }
  91. }
  92. // No legacy IAM available, convert to policy and evaluate
  93. return p.evaluateUsingPolicyConversion(action, bucketName, objectName, principal)
  94. }
  95. // evaluateUsingPolicyConversion converts legacy action to policy and evaluates
  96. func (p *PolicyBackedIAM) evaluateUsingPolicyConversion(action, bucketName, objectName, principal string) bool {
  97. // For now, use a conservative approach for legacy actions
  98. // In a real implementation, this would integrate with the existing identity system
  99. glog.V(2).Infof("Legacy action evaluation for %s on %s/%s by %s", action, bucketName, objectName, principal)
  100. // Return false to maintain security until proper legacy integration is implemented
  101. // This ensures no unintended access is granted
  102. return false
  103. }
  104. // ConvertIdentityToPolicy converts a legacy identity action to an AWS policy
  105. func ConvertIdentityToPolicy(identityActions []string, bucketName string) (*PolicyDocument, error) {
  106. statements := make([]PolicyStatement, 0)
  107. for _, action := range identityActions {
  108. stmt, err := convertSingleAction(action, bucketName)
  109. if err != nil {
  110. glog.Warningf("Failed to convert action %s: %v", action, err)
  111. continue
  112. }
  113. if stmt != nil {
  114. statements = append(statements, *stmt)
  115. }
  116. }
  117. if len(statements) == 0 {
  118. return nil, fmt.Errorf("no valid statements generated")
  119. }
  120. return &PolicyDocument{
  121. Version: PolicyVersion2012_10_17,
  122. Statement: statements,
  123. }, nil
  124. }
  125. // convertSingleAction converts a single legacy action to a policy statement
  126. func convertSingleAction(action, bucketName string) (*PolicyStatement, error) {
  127. parts := strings.Split(action, ":")
  128. if len(parts) != 2 {
  129. return nil, fmt.Errorf("invalid action format: %s", action)
  130. }
  131. actionType := parts[0]
  132. resourcePattern := parts[1]
  133. var s3Actions []string
  134. var resources []string
  135. switch actionType {
  136. case "Read":
  137. s3Actions = []string{"s3:GetObject", "s3:GetObjectVersion", "s3:ListBucket"}
  138. if strings.HasSuffix(resourcePattern, "/*") {
  139. // Object-level read access
  140. bucket := strings.TrimSuffix(resourcePattern, "/*")
  141. resources = []string{
  142. fmt.Sprintf("arn:aws:s3:::%s", bucket),
  143. fmt.Sprintf("arn:aws:s3:::%s/*", bucket),
  144. }
  145. } else {
  146. // Bucket-level read access
  147. resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)}
  148. }
  149. case "Write":
  150. s3Actions = []string{"s3:PutObject", "s3:DeleteObject", "s3:PutObjectAcl"}
  151. if strings.HasSuffix(resourcePattern, "/*") {
  152. // Object-level write access
  153. bucket := strings.TrimSuffix(resourcePattern, "/*")
  154. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  155. } else {
  156. // Bucket-level write access
  157. resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)}
  158. }
  159. case "Admin":
  160. s3Actions = []string{"s3:*"}
  161. resources = []string{
  162. fmt.Sprintf("arn:aws:s3:::%s", resourcePattern),
  163. fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern),
  164. }
  165. case "List":
  166. s3Actions = []string{"s3:ListBucket", "s3:ListBucketVersions"}
  167. if strings.HasSuffix(resourcePattern, "/*") {
  168. // Object-level list access - extract bucket from "bucket/prefix/*" pattern
  169. patternWithoutWildcard := strings.TrimSuffix(resourcePattern, "/*")
  170. parts := strings.SplitN(patternWithoutWildcard, "/", 2)
  171. bucket := parts[0]
  172. resources = []string{
  173. fmt.Sprintf("arn:aws:s3:::%s", bucket),
  174. fmt.Sprintf("arn:aws:s3:::%s/*", bucket),
  175. }
  176. } else {
  177. // Bucket-level list access
  178. resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)}
  179. }
  180. case "Tagging":
  181. s3Actions = []string{"s3:GetObjectTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging"}
  182. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  183. case "BypassGovernanceRetention":
  184. s3Actions = []string{"s3:BypassGovernanceRetention"}
  185. if strings.HasSuffix(resourcePattern, "/*") {
  186. // Object-level bypass governance access
  187. bucket := strings.TrimSuffix(resourcePattern, "/*")
  188. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  189. } else {
  190. // Bucket-level bypass governance access
  191. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  192. }
  193. case "GetObjectRetention":
  194. s3Actions = []string{"s3:GetObjectRetention"}
  195. if strings.HasSuffix(resourcePattern, "/*") {
  196. bucket := strings.TrimSuffix(resourcePattern, "/*")
  197. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  198. } else {
  199. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  200. }
  201. case "PutObjectRetention":
  202. s3Actions = []string{"s3:PutObjectRetention"}
  203. if strings.HasSuffix(resourcePattern, "/*") {
  204. bucket := strings.TrimSuffix(resourcePattern, "/*")
  205. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  206. } else {
  207. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  208. }
  209. case "GetObjectLegalHold":
  210. s3Actions = []string{"s3:GetObjectLegalHold"}
  211. if strings.HasSuffix(resourcePattern, "/*") {
  212. bucket := strings.TrimSuffix(resourcePattern, "/*")
  213. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  214. } else {
  215. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  216. }
  217. case "PutObjectLegalHold":
  218. s3Actions = []string{"s3:PutObjectLegalHold"}
  219. if strings.HasSuffix(resourcePattern, "/*") {
  220. bucket := strings.TrimSuffix(resourcePattern, "/*")
  221. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", bucket)}
  222. } else {
  223. resources = []string{fmt.Sprintf("arn:aws:s3:::%s/*", resourcePattern)}
  224. }
  225. case "GetBucketObjectLockConfiguration":
  226. s3Actions = []string{"s3:GetBucketObjectLockConfiguration"}
  227. resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)}
  228. case "PutBucketObjectLockConfiguration":
  229. s3Actions = []string{"s3:PutBucketObjectLockConfiguration"}
  230. resources = []string{fmt.Sprintf("arn:aws:s3:::%s", resourcePattern)}
  231. default:
  232. return nil, fmt.Errorf("unknown action type: %s", actionType)
  233. }
  234. return &PolicyStatement{
  235. Effect: PolicyEffectAllow,
  236. Action: NewStringOrStringSlice(s3Actions...),
  237. Resource: NewStringOrStringSlice(resources...),
  238. }, nil
  239. }
  240. // GetActionMappings returns the mapping of legacy actions to S3 actions
  241. func GetActionMappings() map[string][]string {
  242. return map[string][]string{
  243. "Read": {
  244. "s3:GetObject",
  245. "s3:GetObjectVersion",
  246. "s3:GetObjectAcl",
  247. "s3:GetObjectVersionAcl",
  248. "s3:GetObjectTagging",
  249. "s3:GetObjectVersionTagging",
  250. "s3:ListBucket",
  251. "s3:ListBucketVersions",
  252. "s3:GetBucketLocation",
  253. "s3:GetBucketVersioning",
  254. "s3:GetBucketAcl",
  255. "s3:GetBucketCors",
  256. "s3:GetBucketTagging",
  257. "s3:GetBucketNotification",
  258. },
  259. "Write": {
  260. "s3:PutObject",
  261. "s3:PutObjectAcl",
  262. "s3:PutObjectTagging",
  263. "s3:DeleteObject",
  264. "s3:DeleteObjectVersion",
  265. "s3:DeleteObjectTagging",
  266. "s3:AbortMultipartUpload",
  267. "s3:ListMultipartUploads",
  268. "s3:ListParts",
  269. "s3:PutBucketAcl",
  270. "s3:PutBucketCors",
  271. "s3:PutBucketTagging",
  272. "s3:PutBucketNotification",
  273. "s3:PutBucketVersioning",
  274. "s3:DeleteBucketTagging",
  275. "s3:DeleteBucketCors",
  276. },
  277. "Admin": {
  278. "s3:*",
  279. },
  280. "List": {
  281. "s3:ListBucket",
  282. "s3:ListBucketVersions",
  283. "s3:ListAllMyBuckets",
  284. },
  285. "Tagging": {
  286. "s3:GetObjectTagging",
  287. "s3:PutObjectTagging",
  288. "s3:DeleteObjectTagging",
  289. "s3:GetBucketTagging",
  290. "s3:PutBucketTagging",
  291. "s3:DeleteBucketTagging",
  292. },
  293. "BypassGovernanceRetention": {
  294. "s3:BypassGovernanceRetention",
  295. },
  296. "GetObjectRetention": {
  297. "s3:GetObjectRetention",
  298. },
  299. "PutObjectRetention": {
  300. "s3:PutObjectRetention",
  301. },
  302. "GetObjectLegalHold": {
  303. "s3:GetObjectLegalHold",
  304. },
  305. "PutObjectLegalHold": {
  306. "s3:PutObjectLegalHold",
  307. },
  308. "GetBucketObjectLockConfiguration": {
  309. "s3:GetBucketObjectLockConfiguration",
  310. },
  311. "PutBucketObjectLockConfiguration": {
  312. "s3:PutBucketObjectLockConfiguration",
  313. },
  314. }
  315. }
  316. // ValidateActionMapping validates that a legacy action can be mapped to S3 actions
  317. func ValidateActionMapping(action string) error {
  318. mappings := GetActionMappings()
  319. parts := strings.Split(action, ":")
  320. if len(parts) != 2 {
  321. return fmt.Errorf("invalid action format: %s, expected format: 'ActionType:Resource'", action)
  322. }
  323. actionType := parts[0]
  324. resource := parts[1]
  325. if _, exists := mappings[actionType]; !exists {
  326. return fmt.Errorf("unknown action type: %s", actionType)
  327. }
  328. if resource == "" {
  329. return fmt.Errorf("resource cannot be empty")
  330. }
  331. return nil
  332. }
  333. // ConvertLegacyActions converts an array of legacy actions to S3 actions
  334. func ConvertLegacyActions(legacyActions []string) ([]string, error) {
  335. mappings := GetActionMappings()
  336. s3Actions := make([]string, 0)
  337. for _, legacyAction := range legacyActions {
  338. if err := ValidateActionMapping(legacyAction); err != nil {
  339. return nil, err
  340. }
  341. parts := strings.Split(legacyAction, ":")
  342. actionType := parts[0]
  343. if actionType == "Admin" {
  344. // Admin gives all permissions, so we can just return s3:*
  345. return []string{"s3:*"}, nil
  346. }
  347. if mapped, exists := mappings[actionType]; exists {
  348. s3Actions = append(s3Actions, mapped...)
  349. }
  350. }
  351. // Remove duplicates
  352. uniqueActions := make([]string, 0)
  353. seen := make(map[string]bool)
  354. for _, action := range s3Actions {
  355. if !seen[action] {
  356. uniqueActions = append(uniqueActions, action)
  357. seen[action] = true
  358. }
  359. }
  360. return uniqueActions, nil
  361. }
  362. // GetResourcesFromLegacyAction extracts resources from a legacy action
  363. func GetResourcesFromLegacyAction(legacyAction string) ([]string, error) {
  364. parts := strings.Split(legacyAction, ":")
  365. if len(parts) != 2 {
  366. return nil, fmt.Errorf("invalid action format: %s", legacyAction)
  367. }
  368. resourcePattern := parts[1]
  369. resources := make([]string, 0)
  370. if strings.HasSuffix(resourcePattern, "/*") {
  371. // Object-level access
  372. bucket := strings.TrimSuffix(resourcePattern, "/*")
  373. resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s", bucket))
  374. resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s/*", bucket))
  375. } else {
  376. // Bucket-level access
  377. resources = append(resources, fmt.Sprintf("arn:aws:s3:::%s", resourcePattern))
  378. }
  379. return resources, nil
  380. }
  381. // CreatePolicyFromLegacyIdentity creates a policy document from legacy identity actions
  382. func CreatePolicyFromLegacyIdentity(identityName string, actions []string) (*PolicyDocument, error) {
  383. statements := make([]PolicyStatement, 0)
  384. // Group actions by resource pattern
  385. resourceActions := make(map[string][]string)
  386. for _, action := range actions {
  387. parts := strings.Split(action, ":")
  388. if len(parts) != 2 {
  389. continue
  390. }
  391. resourcePattern := parts[1]
  392. actionType := parts[0]
  393. if _, exists := resourceActions[resourcePattern]; !exists {
  394. resourceActions[resourcePattern] = make([]string, 0)
  395. }
  396. resourceActions[resourcePattern] = append(resourceActions[resourcePattern], actionType)
  397. }
  398. // Create statements for each resource pattern
  399. for resourcePattern, actionTypes := range resourceActions {
  400. s3Actions := make([]string, 0)
  401. for _, actionType := range actionTypes {
  402. if actionType == "Admin" {
  403. s3Actions = []string{"s3:*"}
  404. break
  405. }
  406. if mapped, exists := GetActionMappings()[actionType]; exists {
  407. s3Actions = append(s3Actions, mapped...)
  408. }
  409. }
  410. resources, err := GetResourcesFromLegacyAction(fmt.Sprintf("dummy:%s", resourcePattern))
  411. if err != nil {
  412. continue
  413. }
  414. statement := PolicyStatement{
  415. Sid: fmt.Sprintf("%s-%s", identityName, strings.ReplaceAll(resourcePattern, "/", "-")),
  416. Effect: PolicyEffectAllow,
  417. Action: NewStringOrStringSlice(s3Actions...),
  418. Resource: NewStringOrStringSlice(resources...),
  419. }
  420. statements = append(statements, statement)
  421. }
  422. if len(statements) == 0 {
  423. return nil, fmt.Errorf("no valid statements generated for identity %s", identityName)
  424. }
  425. return &PolicyDocument{
  426. Version: PolicyVersion2012_10_17,
  427. Statement: statements,
  428. }, nil
  429. }
  430. // HasPolicyForBucket checks if a bucket has a policy
  431. func (p *PolicyBackedIAM) HasPolicyForBucket(bucketName string) bool {
  432. return p.policyEngine.HasPolicyForBucket(bucketName)
  433. }
  434. // GetPolicyEngine returns the underlying policy engine
  435. func (p *PolicyBackedIAM) GetPolicyEngine() *PolicyEngine {
  436. return p.policyEngine
  437. }