iam_integration_test.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. package integration
  2. import (
  3. "context"
  4. "testing"
  5. "time"
  6. "github.com/golang-jwt/jwt/v5"
  7. "github.com/seaweedfs/seaweedfs/weed/iam/ldap"
  8. "github.com/seaweedfs/seaweedfs/weed/iam/oidc"
  9. "github.com/seaweedfs/seaweedfs/weed/iam/policy"
  10. "github.com/seaweedfs/seaweedfs/weed/iam/sts"
  11. "github.com/stretchr/testify/assert"
  12. "github.com/stretchr/testify/require"
  13. )
  14. // TestFullOIDCWorkflow tests the complete OIDC → STS → Policy workflow
  15. func TestFullOIDCWorkflow(t *testing.T) {
  16. // Set up integrated IAM system
  17. iamManager := setupIntegratedIAMSystem(t)
  18. // Create JWT tokens for testing with the correct issuer
  19. validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  20. invalidJWTToken := createTestJWT(t, "https://invalid-issuer.com", "test-user", "wrong-key")
  21. tests := []struct {
  22. name string
  23. roleArn string
  24. sessionName string
  25. webToken string
  26. expectedAllow bool
  27. testAction string
  28. testResource string
  29. }{
  30. {
  31. name: "successful role assumption with policy validation",
  32. roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  33. sessionName: "oidc-session",
  34. webToken: validJWTToken,
  35. expectedAllow: true,
  36. testAction: "s3:GetObject",
  37. testResource: "arn:seaweed:s3:::test-bucket/file.txt",
  38. },
  39. {
  40. name: "role assumption denied by trust policy",
  41. roleArn: "arn:seaweed:iam::role/RestrictedRole",
  42. sessionName: "oidc-session",
  43. webToken: validJWTToken,
  44. expectedAllow: false,
  45. },
  46. {
  47. name: "invalid token rejected",
  48. roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  49. sessionName: "oidc-session",
  50. webToken: invalidJWTToken,
  51. expectedAllow: false,
  52. },
  53. }
  54. for _, tt := range tests {
  55. t.Run(tt.name, func(t *testing.T) {
  56. ctx := context.Background()
  57. // Step 1: Attempt role assumption
  58. assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
  59. RoleArn: tt.roleArn,
  60. WebIdentityToken: tt.webToken,
  61. RoleSessionName: tt.sessionName,
  62. }
  63. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
  64. if !tt.expectedAllow {
  65. assert.Error(t, err)
  66. assert.Nil(t, response)
  67. return
  68. }
  69. // Should succeed if expectedAllow is true
  70. require.NoError(t, err)
  71. require.NotNil(t, response)
  72. require.NotNil(t, response.Credentials)
  73. // Step 2: Test policy enforcement with assumed credentials
  74. if tt.testAction != "" && tt.testResource != "" {
  75. allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
  76. Principal: response.AssumedRoleUser.Arn,
  77. Action: tt.testAction,
  78. Resource: tt.testResource,
  79. SessionToken: response.Credentials.SessionToken,
  80. })
  81. require.NoError(t, err)
  82. assert.True(t, allowed, "Action should be allowed by role policy")
  83. }
  84. })
  85. }
  86. }
  87. // TestFullLDAPWorkflow tests the complete LDAP → STS → Policy workflow
  88. func TestFullLDAPWorkflow(t *testing.T) {
  89. iamManager := setupIntegratedIAMSystem(t)
  90. tests := []struct {
  91. name string
  92. roleArn string
  93. sessionName string
  94. username string
  95. password string
  96. expectedAllow bool
  97. testAction string
  98. testResource string
  99. }{
  100. {
  101. name: "successful LDAP role assumption",
  102. roleArn: "arn:seaweed:iam::role/LDAPUserRole",
  103. sessionName: "ldap-session",
  104. username: "testuser",
  105. password: "testpass",
  106. expectedAllow: true,
  107. testAction: "filer:CreateEntry",
  108. testResource: "arn:seaweed:filer::path/user-docs/*",
  109. },
  110. {
  111. name: "invalid LDAP credentials",
  112. roleArn: "arn:seaweed:iam::role/LDAPUserRole",
  113. sessionName: "ldap-session",
  114. username: "testuser",
  115. password: "wrongpass",
  116. expectedAllow: false,
  117. },
  118. }
  119. for _, tt := range tests {
  120. t.Run(tt.name, func(t *testing.T) {
  121. ctx := context.Background()
  122. // Step 1: Attempt role assumption with LDAP credentials
  123. assumeRequest := &sts.AssumeRoleWithCredentialsRequest{
  124. RoleArn: tt.roleArn,
  125. Username: tt.username,
  126. Password: tt.password,
  127. RoleSessionName: tt.sessionName,
  128. ProviderName: "test-ldap",
  129. }
  130. response, err := iamManager.AssumeRoleWithCredentials(ctx, assumeRequest)
  131. if !tt.expectedAllow {
  132. assert.Error(t, err)
  133. assert.Nil(t, response)
  134. return
  135. }
  136. require.NoError(t, err)
  137. require.NotNil(t, response)
  138. // Step 2: Test policy enforcement
  139. if tt.testAction != "" && tt.testResource != "" {
  140. allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
  141. Principal: response.AssumedRoleUser.Arn,
  142. Action: tt.testAction,
  143. Resource: tt.testResource,
  144. SessionToken: response.Credentials.SessionToken,
  145. })
  146. require.NoError(t, err)
  147. assert.True(t, allowed)
  148. }
  149. })
  150. }
  151. }
  152. // TestPolicyEnforcement tests policy evaluation for various scenarios
  153. func TestPolicyEnforcement(t *testing.T) {
  154. iamManager := setupIntegratedIAMSystem(t)
  155. // Create a valid JWT token for testing
  156. validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  157. // Create a session for testing
  158. ctx := context.Background()
  159. assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
  160. RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  161. WebIdentityToken: validJWTToken,
  162. RoleSessionName: "policy-test-session",
  163. }
  164. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
  165. require.NoError(t, err)
  166. sessionToken := response.Credentials.SessionToken
  167. principal := response.AssumedRoleUser.Arn
  168. tests := []struct {
  169. name string
  170. action string
  171. resource string
  172. shouldAllow bool
  173. reason string
  174. }{
  175. {
  176. name: "allow read access",
  177. action: "s3:GetObject",
  178. resource: "arn:seaweed:s3:::test-bucket/file.txt",
  179. shouldAllow: true,
  180. reason: "S3ReadOnlyRole should allow GetObject",
  181. },
  182. {
  183. name: "allow list bucket",
  184. action: "s3:ListBucket",
  185. resource: "arn:seaweed:s3:::test-bucket",
  186. shouldAllow: true,
  187. reason: "S3ReadOnlyRole should allow ListBucket",
  188. },
  189. {
  190. name: "deny write access",
  191. action: "s3:PutObject",
  192. resource: "arn:seaweed:s3:::test-bucket/newfile.txt",
  193. shouldAllow: false,
  194. reason: "S3ReadOnlyRole should deny write operations",
  195. },
  196. {
  197. name: "deny delete access",
  198. action: "s3:DeleteObject",
  199. resource: "arn:seaweed:s3:::test-bucket/file.txt",
  200. shouldAllow: false,
  201. reason: "S3ReadOnlyRole should deny delete operations",
  202. },
  203. {
  204. name: "deny filer access",
  205. action: "filer:CreateEntry",
  206. resource: "arn:seaweed:filer::path/test",
  207. shouldAllow: false,
  208. reason: "S3ReadOnlyRole should not allow filer operations",
  209. },
  210. }
  211. for _, tt := range tests {
  212. t.Run(tt.name, func(t *testing.T) {
  213. allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
  214. Principal: principal,
  215. Action: tt.action,
  216. Resource: tt.resource,
  217. SessionToken: sessionToken,
  218. })
  219. require.NoError(t, err)
  220. assert.Equal(t, tt.shouldAllow, allowed, tt.reason)
  221. })
  222. }
  223. }
  224. // TestSessionExpiration tests session expiration and cleanup
  225. func TestSessionExpiration(t *testing.T) {
  226. iamManager := setupIntegratedIAMSystem(t)
  227. ctx := context.Background()
  228. // Create a valid JWT token for testing
  229. validJWTToken := createTestJWT(t, "https://test-issuer.com", "test-user-123", "test-signing-key")
  230. // Create a short-lived session
  231. assumeRequest := &sts.AssumeRoleWithWebIdentityRequest{
  232. RoleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  233. WebIdentityToken: validJWTToken,
  234. RoleSessionName: "expiration-test",
  235. DurationSeconds: int64Ptr(900), // 15 minutes
  236. }
  237. response, err := iamManager.AssumeRoleWithWebIdentity(ctx, assumeRequest)
  238. require.NoError(t, err)
  239. sessionToken := response.Credentials.SessionToken
  240. // Verify session is initially valid
  241. allowed, err := iamManager.IsActionAllowed(ctx, &ActionRequest{
  242. Principal: response.AssumedRoleUser.Arn,
  243. Action: "s3:GetObject",
  244. Resource: "arn:seaweed:s3:::test-bucket/file.txt",
  245. SessionToken: sessionToken,
  246. })
  247. require.NoError(t, err)
  248. assert.True(t, allowed)
  249. // Verify the expiration time is set correctly
  250. assert.True(t, response.Credentials.Expiration.After(time.Now()))
  251. assert.True(t, response.Credentials.Expiration.Before(time.Now().Add(16*time.Minute)))
  252. // Test session expiration behavior in stateless JWT system
  253. // In a stateless system, manual expiration is not supported
  254. err = iamManager.ExpireSessionForTesting(ctx, sessionToken)
  255. require.Error(t, err, "Manual session expiration should not be supported in stateless system")
  256. assert.Contains(t, err.Error(), "manual session expiration not supported")
  257. // Verify session is still valid (since it hasn't naturally expired)
  258. allowed, err = iamManager.IsActionAllowed(ctx, &ActionRequest{
  259. Principal: response.AssumedRoleUser.Arn,
  260. Action: "s3:GetObject",
  261. Resource: "arn:seaweed:s3:::test-bucket/file.txt",
  262. SessionToken: sessionToken,
  263. })
  264. require.NoError(t, err, "Session should still be valid in stateless system")
  265. assert.True(t, allowed, "Access should still be allowed since token hasn't naturally expired")
  266. }
  267. // TestTrustPolicyValidation tests role trust policy validation
  268. func TestTrustPolicyValidation(t *testing.T) {
  269. iamManager := setupIntegratedIAMSystem(t)
  270. ctx := context.Background()
  271. tests := []struct {
  272. name string
  273. roleArn string
  274. provider string
  275. userID string
  276. shouldAllow bool
  277. reason string
  278. }{
  279. {
  280. name: "OIDC user allowed by trust policy",
  281. roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  282. provider: "oidc",
  283. userID: "test-user-id",
  284. shouldAllow: true,
  285. reason: "Trust policy should allow OIDC users",
  286. },
  287. {
  288. name: "LDAP user allowed by different role",
  289. roleArn: "arn:seaweed:iam::role/LDAPUserRole",
  290. provider: "ldap",
  291. userID: "testuser",
  292. shouldAllow: true,
  293. reason: "Trust policy should allow LDAP users for LDAP role",
  294. },
  295. {
  296. name: "Wrong provider for role",
  297. roleArn: "arn:seaweed:iam::role/S3ReadOnlyRole",
  298. provider: "ldap",
  299. userID: "testuser",
  300. shouldAllow: false,
  301. reason: "S3ReadOnlyRole trust policy should reject LDAP users",
  302. },
  303. }
  304. for _, tt := range tests {
  305. t.Run(tt.name, func(t *testing.T) {
  306. // This would test trust policy evaluation
  307. // For now, we'll implement this as part of the IAM manager
  308. result := iamManager.ValidateTrustPolicy(ctx, tt.roleArn, tt.provider, tt.userID)
  309. assert.Equal(t, tt.shouldAllow, result, tt.reason)
  310. })
  311. }
  312. }
  313. // Helper functions and test setup
  314. // createTestJWT creates a test JWT token with the specified issuer, subject and signing key
  315. func createTestJWT(t *testing.T, issuer, subject, signingKey string) string {
  316. token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
  317. "iss": issuer,
  318. "sub": subject,
  319. "aud": "test-client-id",
  320. "exp": time.Now().Add(time.Hour).Unix(),
  321. "iat": time.Now().Unix(),
  322. // Add claims that trust policy validation expects
  323. "idp": "test-oidc", // Identity provider claim for trust policy matching
  324. })
  325. tokenString, err := token.SignedString([]byte(signingKey))
  326. require.NoError(t, err)
  327. return tokenString
  328. }
  329. func setupIntegratedIAMSystem(t *testing.T) *IAMManager {
  330. // Create IAM manager with all components
  331. manager := NewIAMManager()
  332. // Configure and initialize
  333. config := &IAMConfig{
  334. STS: &sts.STSConfig{
  335. TokenDuration: sts.FlexibleDuration{time.Hour},
  336. MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
  337. Issuer: "test-sts",
  338. SigningKey: []byte("test-signing-key-32-characters-long"),
  339. },
  340. Policy: &policy.PolicyEngineConfig{
  341. DefaultEffect: "Deny",
  342. StoreType: "memory", // Use memory for unit tests
  343. },
  344. Roles: &RoleStoreConfig{
  345. StoreType: "memory", // Use memory for unit tests
  346. },
  347. }
  348. err := manager.Initialize(config, func() string {
  349. return "localhost:8888" // Mock filer address for testing
  350. })
  351. require.NoError(t, err)
  352. // Set up test providers
  353. setupTestProviders(t, manager)
  354. // Set up test policies and roles
  355. setupTestPoliciesAndRoles(t, manager)
  356. return manager
  357. }
  358. func setupTestProviders(t *testing.T, manager *IAMManager) {
  359. // Set up OIDC provider
  360. oidcProvider := oidc.NewMockOIDCProvider("test-oidc")
  361. oidcConfig := &oidc.OIDCConfig{
  362. Issuer: "https://test-issuer.com",
  363. ClientID: "test-client-id",
  364. }
  365. err := oidcProvider.Initialize(oidcConfig)
  366. require.NoError(t, err)
  367. oidcProvider.SetupDefaultTestData()
  368. // Set up LDAP mock provider (no config needed for mock)
  369. ldapProvider := ldap.NewMockLDAPProvider("test-ldap")
  370. err = ldapProvider.Initialize(nil) // Mock doesn't need real config
  371. require.NoError(t, err)
  372. ldapProvider.SetupDefaultTestData()
  373. // Register providers
  374. err = manager.RegisterIdentityProvider(oidcProvider)
  375. require.NoError(t, err)
  376. err = manager.RegisterIdentityProvider(ldapProvider)
  377. require.NoError(t, err)
  378. }
  379. func setupTestPoliciesAndRoles(t *testing.T, manager *IAMManager) {
  380. ctx := context.Background()
  381. // Create S3 read-only policy
  382. s3ReadPolicy := &policy.PolicyDocument{
  383. Version: "2012-10-17",
  384. Statement: []policy.Statement{
  385. {
  386. Sid: "S3ReadAccess",
  387. Effect: "Allow",
  388. Action: []string{"s3:GetObject", "s3:ListBucket"},
  389. Resource: []string{
  390. "arn:seaweed:s3:::*",
  391. "arn:seaweed:s3:::*/*",
  392. },
  393. },
  394. },
  395. }
  396. err := manager.CreatePolicy(ctx, "", "S3ReadOnlyPolicy", s3ReadPolicy)
  397. require.NoError(t, err)
  398. // Create LDAP user policy
  399. ldapUserPolicy := &policy.PolicyDocument{
  400. Version: "2012-10-17",
  401. Statement: []policy.Statement{
  402. {
  403. Sid: "FilerAccess",
  404. Effect: "Allow",
  405. Action: []string{"filer:*"},
  406. Resource: []string{
  407. "arn:seaweed:filer::path/user-docs/*",
  408. },
  409. },
  410. },
  411. }
  412. err = manager.CreatePolicy(ctx, "", "LDAPUserPolicy", ldapUserPolicy)
  413. require.NoError(t, err)
  414. // Create roles with trust policies
  415. err = manager.CreateRole(ctx, "", "S3ReadOnlyRole", &RoleDefinition{
  416. RoleName: "S3ReadOnlyRole",
  417. TrustPolicy: &policy.PolicyDocument{
  418. Version: "2012-10-17",
  419. Statement: []policy.Statement{
  420. {
  421. Effect: "Allow",
  422. Principal: map[string]interface{}{
  423. "Federated": "test-oidc",
  424. },
  425. Action: []string{"sts:AssumeRoleWithWebIdentity"},
  426. },
  427. },
  428. },
  429. AttachedPolicies: []string{"S3ReadOnlyPolicy"},
  430. })
  431. require.NoError(t, err)
  432. err = manager.CreateRole(ctx, "", "LDAPUserRole", &RoleDefinition{
  433. RoleName: "LDAPUserRole",
  434. TrustPolicy: &policy.PolicyDocument{
  435. Version: "2012-10-17",
  436. Statement: []policy.Statement{
  437. {
  438. Effect: "Allow",
  439. Principal: map[string]interface{}{
  440. "Federated": "test-ldap",
  441. },
  442. Action: []string{"sts:AssumeRoleWithCredentials"},
  443. },
  444. },
  445. },
  446. AttachedPolicies: []string{"LDAPUserPolicy"},
  447. })
  448. require.NoError(t, err)
  449. }
  450. func int64Ptr(v int64) *int64 {
  451. return &v
  452. }