auth_credentials_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. package s3api
  2. import (
  3. "os"
  4. "reflect"
  5. "testing"
  6. "github.com/seaweedfs/seaweedfs/weed/credential"
  7. . "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  8. "github.com/stretchr/testify/assert"
  9. "github.com/seaweedfs/seaweedfs/weed/pb/iam_pb"
  10. jsonpb "google.golang.org/protobuf/encoding/protojson"
  11. )
  12. func TestIdentityListFileFormat(t *testing.T) {
  13. s3ApiConfiguration := &iam_pb.S3ApiConfiguration{}
  14. identity1 := &iam_pb.Identity{
  15. Name: "some_name",
  16. Credentials: []*iam_pb.Credential{
  17. {
  18. AccessKey: "some_access_key1",
  19. SecretKey: "some_secret_key2",
  20. },
  21. },
  22. Actions: []string{
  23. ACTION_ADMIN,
  24. ACTION_READ,
  25. ACTION_WRITE,
  26. },
  27. }
  28. identity2 := &iam_pb.Identity{
  29. Name: "some_read_only_user",
  30. Credentials: []*iam_pb.Credential{
  31. {
  32. AccessKey: "some_access_key1",
  33. SecretKey: "some_secret_key1",
  34. },
  35. },
  36. Actions: []string{
  37. ACTION_READ,
  38. },
  39. }
  40. identity3 := &iam_pb.Identity{
  41. Name: "some_normal_user",
  42. Credentials: []*iam_pb.Credential{
  43. {
  44. AccessKey: "some_access_key2",
  45. SecretKey: "some_secret_key2",
  46. },
  47. },
  48. Actions: []string{
  49. ACTION_READ,
  50. ACTION_WRITE,
  51. },
  52. }
  53. s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity1)
  54. s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity2)
  55. s3ApiConfiguration.Identities = append(s3ApiConfiguration.Identities, identity3)
  56. m := jsonpb.MarshalOptions{
  57. EmitUnpopulated: true,
  58. Indent: " ",
  59. }
  60. text, _ := m.Marshal(s3ApiConfiguration)
  61. println(string(text))
  62. }
  63. func TestCanDo(t *testing.T) {
  64. ident1 := &Identity{
  65. Name: "anything",
  66. Actions: []Action{
  67. "Write:bucket1/a/b/c/*",
  68. "Write:bucket1/a/b/other",
  69. },
  70. }
  71. // object specific
  72. assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
  73. assert.Equal(t, true, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d/e.txt"))
  74. assert.Equal(t, false, ident1.canDo(ACTION_DELETE_BUCKET, "bucket1", ""))
  75. assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/other/some"), "action without *")
  76. assert.Equal(t, false, ident1.canDo(ACTION_WRITE, "bucket1", "/a/b/*"), "action on parent directory")
  77. // bucket specific
  78. ident2 := &Identity{
  79. Name: "anything",
  80. Actions: []Action{
  81. "Read:bucket1",
  82. "Write:bucket1/*",
  83. "WriteAcp:bucket1",
  84. },
  85. }
  86. assert.Equal(t, true, ident2.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
  87. assert.Equal(t, true, ident2.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
  88. assert.Equal(t, true, ident2.canDo(ACTION_WRITE_ACP, "bucket1", ""))
  89. assert.Equal(t, false, ident2.canDo(ACTION_READ_ACP, "bucket1", ""))
  90. assert.Equal(t, false, ident2.canDo(ACTION_LIST, "bucket1", "/a/b/c/d.txt"))
  91. // across buckets
  92. ident3 := &Identity{
  93. Name: "anything",
  94. Actions: []Action{
  95. "Read",
  96. "Write",
  97. },
  98. }
  99. assert.Equal(t, true, ident3.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
  100. assert.Equal(t, true, ident3.canDo(ACTION_WRITE, "bucket1", "/a/b/c/d.txt"))
  101. assert.Equal(t, false, ident3.canDo(ACTION_LIST, "bucket1", "/a/b/other/some"))
  102. assert.Equal(t, false, ident3.canDo(ACTION_WRITE_ACP, "bucket1", ""))
  103. // partial buckets
  104. ident4 := &Identity{
  105. Name: "anything",
  106. Actions: []Action{
  107. "Read:special_*",
  108. "ReadAcp:special_*",
  109. },
  110. }
  111. assert.Equal(t, true, ident4.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt"))
  112. assert.Equal(t, true, ident4.canDo(ACTION_READ_ACP, "special_bucket", ""))
  113. assert.Equal(t, false, ident4.canDo(ACTION_READ, "bucket1", "/a/b/c/d.txt"))
  114. // admin buckets
  115. ident5 := &Identity{
  116. Name: "anything",
  117. Actions: []Action{
  118. "Admin:special_*",
  119. },
  120. }
  121. assert.Equal(t, true, ident5.canDo(ACTION_READ, "special_bucket", "/a/b/c/d.txt"))
  122. assert.Equal(t, true, ident5.canDo(ACTION_READ_ACP, "special_bucket", ""))
  123. assert.Equal(t, true, ident5.canDo(ACTION_WRITE, "special_bucket", "/a/b/c/d.txt"))
  124. assert.Equal(t, true, ident5.canDo(ACTION_WRITE_ACP, "special_bucket", ""))
  125. // anonymous buckets
  126. ident6 := &Identity{
  127. Name: "anonymous",
  128. Actions: []Action{
  129. "Read",
  130. },
  131. }
  132. assert.Equal(t, true, ident6.canDo(ACTION_READ, "anything_bucket", "/a/b/c/d.txt"))
  133. //test deleteBucket operation
  134. ident7 := &Identity{
  135. Name: "anything",
  136. Actions: []Action{
  137. "DeleteBucket:bucket1",
  138. },
  139. }
  140. assert.Equal(t, true, ident7.canDo(ACTION_DELETE_BUCKET, "bucket1", ""))
  141. }
  142. type LoadS3ApiConfigurationTestCase struct {
  143. pbAccount *iam_pb.Account
  144. pbIdent *iam_pb.Identity
  145. expectIdent *Identity
  146. }
  147. func TestLoadS3ApiConfiguration(t *testing.T) {
  148. specifiedAccount := Account{
  149. Id: "specifiedAccountID",
  150. DisplayName: "specifiedAccountName",
  151. EmailAddress: "specifiedAccounEmail@example.com",
  152. }
  153. pbSpecifiedAccount := iam_pb.Account{
  154. Id: "specifiedAccountID",
  155. DisplayName: "specifiedAccountName",
  156. EmailAddress: "specifiedAccounEmail@example.com",
  157. }
  158. testCases := map[string]*LoadS3ApiConfigurationTestCase{
  159. "notSpecifyAccountId": {
  160. pbIdent: &iam_pb.Identity{
  161. Name: "notSpecifyAccountId",
  162. Actions: []string{
  163. "Read",
  164. "Write",
  165. },
  166. Credentials: []*iam_pb.Credential{
  167. {
  168. AccessKey: "some_access_key1",
  169. SecretKey: "some_secret_key2",
  170. },
  171. },
  172. },
  173. expectIdent: &Identity{
  174. Name: "notSpecifyAccountId",
  175. Account: &AccountAdmin,
  176. PrincipalArn: "arn:seaweed:iam::user/notSpecifyAccountId",
  177. Actions: []Action{
  178. "Read",
  179. "Write",
  180. },
  181. Credentials: []*Credential{
  182. {
  183. AccessKey: "some_access_key1",
  184. SecretKey: "some_secret_key2",
  185. },
  186. },
  187. },
  188. },
  189. "specifiedAccountID": {
  190. pbAccount: &pbSpecifiedAccount,
  191. pbIdent: &iam_pb.Identity{
  192. Name: "specifiedAccountID",
  193. Account: &pbSpecifiedAccount,
  194. Actions: []string{
  195. "Read",
  196. "Write",
  197. },
  198. },
  199. expectIdent: &Identity{
  200. Name: "specifiedAccountID",
  201. Account: &specifiedAccount,
  202. PrincipalArn: "arn:seaweed:iam::user/specifiedAccountID",
  203. Actions: []Action{
  204. "Read",
  205. "Write",
  206. },
  207. },
  208. },
  209. "anonymous": {
  210. pbIdent: &iam_pb.Identity{
  211. Name: "anonymous",
  212. Actions: []string{
  213. "Read",
  214. "Write",
  215. },
  216. },
  217. expectIdent: &Identity{
  218. Name: "anonymous",
  219. Account: &AccountAnonymous,
  220. PrincipalArn: "arn:seaweed:iam::user/anonymous",
  221. Actions: []Action{
  222. "Read",
  223. "Write",
  224. },
  225. },
  226. },
  227. }
  228. config := &iam_pb.S3ApiConfiguration{
  229. Identities: make([]*iam_pb.Identity, 0),
  230. }
  231. for _, v := range testCases {
  232. config.Identities = append(config.Identities, v.pbIdent)
  233. if v.pbAccount != nil {
  234. config.Accounts = append(config.Accounts, v.pbAccount)
  235. }
  236. }
  237. iam := IdentityAccessManagement{}
  238. err := iam.loadS3ApiConfiguration(config)
  239. if err != nil {
  240. return
  241. }
  242. for _, ident := range iam.identities {
  243. tc := testCases[ident.Name]
  244. if !reflect.DeepEqual(ident, tc.expectIdent) {
  245. t.Errorf("not expect for ident name %s", ident.Name)
  246. }
  247. }
  248. }
  249. func TestNewIdentityAccessManagementWithStoreEnvVars(t *testing.T) {
  250. // Save original environment
  251. originalAccessKeyId := os.Getenv("AWS_ACCESS_KEY_ID")
  252. originalSecretAccessKey := os.Getenv("AWS_SECRET_ACCESS_KEY")
  253. // Clean up after test
  254. defer func() {
  255. if originalAccessKeyId != "" {
  256. os.Setenv("AWS_ACCESS_KEY_ID", originalAccessKeyId)
  257. } else {
  258. os.Unsetenv("AWS_ACCESS_KEY_ID")
  259. }
  260. if originalSecretAccessKey != "" {
  261. os.Setenv("AWS_SECRET_ACCESS_KEY", originalSecretAccessKey)
  262. } else {
  263. os.Unsetenv("AWS_SECRET_ACCESS_KEY")
  264. }
  265. }()
  266. tests := []struct {
  267. name string
  268. accessKeyId string
  269. secretAccessKey string
  270. expectEnvIdentity bool
  271. expectedName string
  272. description string
  273. }{
  274. {
  275. name: "Environment variables used as fallback",
  276. accessKeyId: "AKIA1234567890ABCDEF",
  277. secretAccessKey: "secret123456789012345678901234567890abcdef12",
  278. expectEnvIdentity: true,
  279. expectedName: "admin-AKIA1234",
  280. description: "When no config file and no filer config, environment variables should be used",
  281. },
  282. {
  283. name: "Short access key fallback",
  284. accessKeyId: "SHORT",
  285. secretAccessKey: "secret123456789012345678901234567890abcdef12",
  286. expectEnvIdentity: true,
  287. expectedName: "admin-SHORT",
  288. description: "Short access keys should work correctly as fallback",
  289. },
  290. {
  291. name: "No env vars means no identities",
  292. accessKeyId: "",
  293. secretAccessKey: "",
  294. expectEnvIdentity: false,
  295. expectedName: "",
  296. description: "When no env vars and no config, should have no identities",
  297. },
  298. }
  299. for _, tt := range tests {
  300. t.Run(tt.name, func(t *testing.T) {
  301. // Set up environment variables
  302. if tt.accessKeyId != "" {
  303. os.Setenv("AWS_ACCESS_KEY_ID", tt.accessKeyId)
  304. } else {
  305. os.Unsetenv("AWS_ACCESS_KEY_ID")
  306. }
  307. if tt.secretAccessKey != "" {
  308. os.Setenv("AWS_SECRET_ACCESS_KEY", tt.secretAccessKey)
  309. } else {
  310. os.Unsetenv("AWS_SECRET_ACCESS_KEY")
  311. }
  312. // Create IAM instance with memory store for testing (no config file)
  313. option := &S3ApiServerOption{
  314. Config: "", // No config file - this should trigger environment variable fallback
  315. }
  316. iam := NewIdentityAccessManagementWithStore(option, string(credential.StoreTypeMemory))
  317. if tt.expectEnvIdentity {
  318. // Should have exactly one identity from environment variables
  319. assert.Len(t, iam.identities, 1, "Should have exactly one identity from environment variables")
  320. identity := iam.identities[0]
  321. assert.Equal(t, tt.expectedName, identity.Name, "Identity name should match expected")
  322. assert.Len(t, identity.Credentials, 1, "Should have one credential")
  323. assert.Equal(t, tt.accessKeyId, identity.Credentials[0].AccessKey, "Access key should match environment variable")
  324. assert.Equal(t, tt.secretAccessKey, identity.Credentials[0].SecretKey, "Secret key should match environment variable")
  325. assert.Contains(t, identity.Actions, Action(ACTION_ADMIN), "Should have admin action")
  326. } else {
  327. // When no env vars, should have no identities (since no config file)
  328. assert.Len(t, iam.identities, 0, "Should have no identities when no env vars and no config file")
  329. }
  330. })
  331. }
  332. }
  333. // TestBucketLevelListPermissions tests that bucket-level List permissions work correctly
  334. // This test validates the fix for issue #7066
  335. func TestBucketLevelListPermissions(t *testing.T) {
  336. // Test the functionality that was broken in issue #7066
  337. t.Run("Bucket Wildcard Permissions", func(t *testing.T) {
  338. // Create identity with bucket-level List permission using wildcards
  339. identity := &Identity{
  340. Name: "bucket-user",
  341. Actions: []Action{
  342. "List:mybucket*",
  343. "Read:mybucket*",
  344. "ReadAcp:mybucket*",
  345. "Write:mybucket*",
  346. "WriteAcp:mybucket*",
  347. "Tagging:mybucket*",
  348. },
  349. }
  350. // Test cases for bucket-level wildcard permissions
  351. testCases := []struct {
  352. name string
  353. action Action
  354. bucket string
  355. object string
  356. shouldAllow bool
  357. description string
  358. }{
  359. {
  360. name: "exact bucket match",
  361. action: "List",
  362. bucket: "mybucket",
  363. object: "",
  364. shouldAllow: true,
  365. description: "Should allow access to exact bucket name",
  366. },
  367. {
  368. name: "bucket with suffix",
  369. action: "List",
  370. bucket: "mybucket-prod",
  371. object: "",
  372. shouldAllow: true,
  373. description: "Should allow access to bucket with matching prefix",
  374. },
  375. {
  376. name: "bucket with numbers",
  377. action: "List",
  378. bucket: "mybucket123",
  379. object: "",
  380. shouldAllow: true,
  381. description: "Should allow access to bucket with numbers",
  382. },
  383. {
  384. name: "different bucket",
  385. action: "List",
  386. bucket: "otherbucket",
  387. object: "",
  388. shouldAllow: false,
  389. description: "Should deny access to bucket with different prefix",
  390. },
  391. {
  392. name: "partial match",
  393. action: "List",
  394. bucket: "notmybucket",
  395. object: "",
  396. shouldAllow: false,
  397. description: "Should deny access to bucket that contains but doesn't start with the prefix",
  398. },
  399. }
  400. for _, tc := range testCases {
  401. t.Run(tc.name, func(t *testing.T) {
  402. result := identity.canDo(tc.action, tc.bucket, tc.object)
  403. assert.Equal(t, tc.shouldAllow, result, tc.description)
  404. })
  405. }
  406. })
  407. t.Run("Global List Permission", func(t *testing.T) {
  408. // Create identity with global List permission
  409. identity := &Identity{
  410. Name: "global-user",
  411. Actions: []Action{
  412. "List",
  413. },
  414. }
  415. // Should allow access to any bucket
  416. testCases := []string{"anybucket", "mybucket", "test-bucket", "prod-data"}
  417. for _, bucket := range testCases {
  418. result := identity.canDo("List", bucket, "")
  419. assert.True(t, result, "Global List permission should allow access to bucket %s", bucket)
  420. }
  421. })
  422. t.Run("No Wildcard Exact Match", func(t *testing.T) {
  423. // Create identity with exact bucket permission (no wildcard)
  424. identity := &Identity{
  425. Name: "exact-user",
  426. Actions: []Action{
  427. "List:specificbucket",
  428. },
  429. }
  430. // Should only allow access to the exact bucket
  431. assert.True(t, identity.canDo("List", "specificbucket", ""), "Should allow access to exact bucket")
  432. assert.False(t, identity.canDo("List", "specificbucket-test", ""), "Should deny access to bucket with suffix")
  433. assert.False(t, identity.canDo("List", "otherbucket", ""), "Should deny access to different bucket")
  434. })
  435. t.Log("This test validates the fix for issue #7066")
  436. t.Log("Bucket-level List permissions like 'List:bucket*' work correctly")
  437. t.Log("ListBucketsHandler now uses consistent authentication flow")
  438. }
  439. // TestListBucketsAuthRequest tests that authRequest works correctly for ListBuckets operations
  440. // This test validates that the fix for the regression identified in PR #7067 works correctly
  441. func TestListBucketsAuthRequest(t *testing.T) {
  442. t.Run("ListBuckets special case handling", func(t *testing.T) {
  443. // Create identity with bucket-specific permissions (no global List permission)
  444. identity := &Identity{
  445. Name: "bucket-user",
  446. Account: &AccountAdmin,
  447. Actions: []Action{
  448. Action("List:mybucket*"),
  449. Action("Read:mybucket*"),
  450. },
  451. }
  452. // Test 1: ListBuckets operation should succeed (bucket = "")
  453. // This would have failed before the fix because canDo("List", "", "") would return false
  454. // After the fix, it bypasses the canDo check for ListBuckets operations
  455. // Simulate what happens in authRequest for ListBuckets:
  456. // action = ACTION_LIST, bucket = "", object = ""
  457. // Before fix: identity.canDo(ACTION_LIST, "", "") would fail
  458. // After fix: the canDo check should be bypassed
  459. // Test the individual canDo method to show it would fail without the special case
  460. result := identity.canDo(Action(ACTION_LIST), "", "")
  461. assert.False(t, result, "canDo should return false for empty bucket with bucket-specific permissions")
  462. // Test with a specific bucket that matches the permission
  463. result2 := identity.canDo(Action(ACTION_LIST), "mybucket", "")
  464. assert.True(t, result2, "canDo should return true for matching bucket")
  465. // Test with a specific bucket that doesn't match
  466. result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "")
  467. assert.False(t, result3, "canDo should return false for non-matching bucket")
  468. })
  469. t.Run("Object listing maintains permission enforcement", func(t *testing.T) {
  470. // Create identity with bucket-specific permissions
  471. identity := &Identity{
  472. Name: "bucket-user",
  473. Account: &AccountAdmin,
  474. Actions: []Action{
  475. Action("List:mybucket*"),
  476. },
  477. }
  478. // For object listing operations, the normal permission checks should still apply
  479. // These operations have a specific bucket in the URL
  480. // Should succeed for allowed bucket
  481. result1 := identity.canDo(Action(ACTION_LIST), "mybucket", "prefix/")
  482. assert.True(t, result1, "Should allow listing objects in permitted bucket")
  483. result2 := identity.canDo(Action(ACTION_LIST), "mybucket-prod", "")
  484. assert.True(t, result2, "Should allow listing objects in wildcard-matched bucket")
  485. // Should fail for disallowed bucket
  486. result3 := identity.canDo(Action(ACTION_LIST), "otherbucket", "")
  487. assert.False(t, result3, "Should deny listing objects in non-permitted bucket")
  488. })
  489. t.Log("This test validates the fix for the regression identified in PR #7067")
  490. t.Log("ListBuckets operation bypasses global permission check when bucket is empty")
  491. t.Log("Object listing still properly enforces bucket-level permissions")
  492. }