s3_iam_middleware.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. package s3api
  2. import (
  3. "context"
  4. "fmt"
  5. "net"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "time"
  10. "github.com/golang-jwt/jwt/v5"
  11. "github.com/seaweedfs/seaweedfs/weed/glog"
  12. "github.com/seaweedfs/seaweedfs/weed/iam/integration"
  13. "github.com/seaweedfs/seaweedfs/weed/iam/providers"
  14. "github.com/seaweedfs/seaweedfs/weed/iam/sts"
  15. "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
  16. "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
  17. )
  18. // S3IAMIntegration provides IAM integration for S3 API
  19. type S3IAMIntegration struct {
  20. iamManager *integration.IAMManager
  21. stsService *sts.STSService
  22. filerAddress string
  23. enabled bool
  24. }
  25. // NewS3IAMIntegration creates a new S3 IAM integration
  26. func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string) *S3IAMIntegration {
  27. var stsService *sts.STSService
  28. if iamManager != nil {
  29. stsService = iamManager.GetSTSService()
  30. }
  31. return &S3IAMIntegration{
  32. iamManager: iamManager,
  33. stsService: stsService,
  34. filerAddress: filerAddress,
  35. enabled: iamManager != nil,
  36. }
  37. }
  38. // AuthenticateJWT authenticates JWT tokens using our STS service
  39. func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
  40. if !s3iam.enabled {
  41. return nil, s3err.ErrNotImplemented
  42. }
  43. // Extract bearer token from Authorization header
  44. authHeader := r.Header.Get("Authorization")
  45. if !strings.HasPrefix(authHeader, "Bearer ") {
  46. return nil, s3err.ErrAccessDenied
  47. }
  48. sessionToken := strings.TrimPrefix(authHeader, "Bearer ")
  49. if sessionToken == "" {
  50. return nil, s3err.ErrAccessDenied
  51. }
  52. // Basic token format validation - reject obviously invalid tokens
  53. if sessionToken == "invalid-token" || len(sessionToken) < 10 {
  54. glog.V(3).Info("Session token format is invalid")
  55. return nil, s3err.ErrAccessDenied
  56. }
  57. // Try to parse as STS session token first
  58. tokenClaims, err := parseJWTToken(sessionToken)
  59. if err != nil {
  60. glog.V(3).Infof("Failed to parse JWT token: %v", err)
  61. return nil, s3err.ErrAccessDenied
  62. }
  63. // Determine token type by issuer claim (more robust than checking role claim)
  64. issuer, issuerOk := tokenClaims["iss"].(string)
  65. if !issuerOk {
  66. glog.V(3).Infof("Token missing issuer claim - invalid JWT")
  67. return nil, s3err.ErrAccessDenied
  68. }
  69. // Check if this is an STS-issued token by examining the issuer
  70. if !s3iam.isSTSIssuer(issuer) {
  71. // Not an STS session token, try to validate as OIDC token with timeout
  72. // Create a context with a reasonable timeout to prevent hanging
  73. ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
  74. defer cancel()
  75. identity, err := s3iam.validateExternalOIDCToken(ctx, sessionToken)
  76. if err != nil {
  77. return nil, s3err.ErrAccessDenied
  78. }
  79. // Extract role from OIDC identity
  80. if identity.RoleArn == "" {
  81. return nil, s3err.ErrAccessDenied
  82. }
  83. // Return IAM identity for OIDC token
  84. return &IAMIdentity{
  85. Name: identity.UserID,
  86. Principal: identity.RoleArn,
  87. SessionToken: sessionToken,
  88. Account: &Account{
  89. DisplayName: identity.UserID,
  90. EmailAddress: identity.UserID + "@oidc.local",
  91. Id: identity.UserID,
  92. },
  93. }, s3err.ErrNone
  94. }
  95. // This is an STS-issued token - extract STS session information
  96. // Extract role claim from STS token
  97. roleName, roleOk := tokenClaims["role"].(string)
  98. if !roleOk || roleName == "" {
  99. glog.V(3).Infof("STS token missing role claim")
  100. return nil, s3err.ErrAccessDenied
  101. }
  102. sessionName, ok := tokenClaims["snam"].(string)
  103. if !ok || sessionName == "" {
  104. sessionName = "jwt-session" // Default fallback
  105. }
  106. subject, ok := tokenClaims["sub"].(string)
  107. if !ok || subject == "" {
  108. subject = "jwt-user" // Default fallback
  109. }
  110. // Use the principal ARN directly from token claims, or build it if not available
  111. principalArn, ok := tokenClaims["principal"].(string)
  112. if !ok || principalArn == "" {
  113. // Fallback: extract role name from role ARN and build principal ARN
  114. roleNameOnly := roleName
  115. if strings.Contains(roleName, "/") {
  116. parts := strings.Split(roleName, "/")
  117. roleNameOnly = parts[len(parts)-1]
  118. }
  119. principalArn = fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleNameOnly, sessionName)
  120. }
  121. // Validate the JWT token directly using STS service (avoid circular dependency)
  122. // Note: We don't call IsActionAllowed here because that would create a circular dependency
  123. // Authentication should only validate the token, authorization happens later
  124. _, err = s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
  125. if err != nil {
  126. glog.V(3).Infof("STS session validation failed: %v", err)
  127. return nil, s3err.ErrAccessDenied
  128. }
  129. // Create IAM identity from validated token
  130. identity := &IAMIdentity{
  131. Name: subject,
  132. Principal: principalArn,
  133. SessionToken: sessionToken,
  134. Account: &Account{
  135. DisplayName: roleName,
  136. EmailAddress: subject + "@seaweedfs.local",
  137. Id: subject,
  138. },
  139. }
  140. glog.V(3).Infof("JWT authentication successful for principal: %s", identity.Principal)
  141. return identity, s3err.ErrNone
  142. }
  143. // AuthorizeAction authorizes actions using our policy engine
  144. func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IAMIdentity, action Action, bucket string, objectKey string, r *http.Request) s3err.ErrorCode {
  145. if !s3iam.enabled {
  146. return s3err.ErrNone // Fallback to existing authorization
  147. }
  148. if identity.SessionToken == "" {
  149. return s3err.ErrAccessDenied
  150. }
  151. // Build resource ARN for the S3 operation
  152. resourceArn := buildS3ResourceArn(bucket, objectKey)
  153. // Extract request context for policy conditions
  154. requestContext := extractRequestContext(r)
  155. // Determine the specific S3 action based on the HTTP request details
  156. specificAction := determineGranularS3Action(r, action, bucket, objectKey)
  157. // Create action request
  158. actionRequest := &integration.ActionRequest{
  159. Principal: identity.Principal,
  160. Action: specificAction,
  161. Resource: resourceArn,
  162. SessionToken: identity.SessionToken,
  163. RequestContext: requestContext,
  164. }
  165. // Check if action is allowed using our policy engine
  166. allowed, err := s3iam.iamManager.IsActionAllowed(ctx, actionRequest)
  167. if err != nil {
  168. return s3err.ErrAccessDenied
  169. }
  170. if !allowed {
  171. return s3err.ErrAccessDenied
  172. }
  173. return s3err.ErrNone
  174. }
  175. // IAMIdentity represents an authenticated identity with session information
  176. type IAMIdentity struct {
  177. Name string
  178. Principal string
  179. SessionToken string
  180. Account *Account
  181. }
  182. // IsAdmin checks if the identity has admin privileges
  183. func (identity *IAMIdentity) IsAdmin() bool {
  184. // In our IAM system, admin status is determined by policies, not identity
  185. // This is handled by the policy engine during authorization
  186. return false
  187. }
  188. // Mock session structures for validation
  189. type MockSessionInfo struct {
  190. AssumedRoleUser MockAssumedRoleUser
  191. }
  192. type MockAssumedRoleUser struct {
  193. AssumedRoleId string
  194. Arn string
  195. }
  196. // Helper functions
  197. // buildS3ResourceArn builds an S3 resource ARN from bucket and object
  198. func buildS3ResourceArn(bucket string, objectKey string) string {
  199. if bucket == "" {
  200. return "arn:seaweed:s3:::*"
  201. }
  202. if objectKey == "" || objectKey == "/" {
  203. return "arn:seaweed:s3:::" + bucket
  204. }
  205. // Remove leading slash from object key if present
  206. if strings.HasPrefix(objectKey, "/") {
  207. objectKey = objectKey[1:]
  208. }
  209. return "arn:seaweed:s3:::" + bucket + "/" + objectKey
  210. }
  211. // determineGranularS3Action determines the specific S3 IAM action based on HTTP request details
  212. // This provides granular, operation-specific actions for accurate IAM policy enforcement
  213. func determineGranularS3Action(r *http.Request, fallbackAction Action, bucket string, objectKey string) string {
  214. method := r.Method
  215. query := r.URL.Query()
  216. // Check if there are specific query parameters indicating granular operations
  217. // If there are, always use granular mapping regardless of method-action alignment
  218. hasGranularIndicators := hasSpecificQueryParameters(query)
  219. // Only check for method-action mismatch when there are NO granular indicators
  220. // This provides fallback behavior for cases where HTTP method doesn't align with intended action
  221. if !hasGranularIndicators && isMethodActionMismatch(method, fallbackAction) {
  222. return mapLegacyActionToIAM(fallbackAction)
  223. }
  224. // Handle object-level operations when method and action are aligned
  225. if objectKey != "" && objectKey != "/" {
  226. switch method {
  227. case "GET", "HEAD":
  228. // Object read operations - check for specific query parameters
  229. if _, hasAcl := query["acl"]; hasAcl {
  230. return "s3:GetObjectAcl"
  231. }
  232. if _, hasTagging := query["tagging"]; hasTagging {
  233. return "s3:GetObjectTagging"
  234. }
  235. if _, hasRetention := query["retention"]; hasRetention {
  236. return "s3:GetObjectRetention"
  237. }
  238. if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
  239. return "s3:GetObjectLegalHold"
  240. }
  241. if _, hasVersions := query["versions"]; hasVersions {
  242. return "s3:GetObjectVersion"
  243. }
  244. if _, hasUploadId := query["uploadId"]; hasUploadId {
  245. return "s3:ListParts"
  246. }
  247. // Default object read
  248. return "s3:GetObject"
  249. case "PUT", "POST":
  250. // Object write operations - check for specific query parameters
  251. if _, hasAcl := query["acl"]; hasAcl {
  252. return "s3:PutObjectAcl"
  253. }
  254. if _, hasTagging := query["tagging"]; hasTagging {
  255. return "s3:PutObjectTagging"
  256. }
  257. if _, hasRetention := query["retention"]; hasRetention {
  258. return "s3:PutObjectRetention"
  259. }
  260. if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
  261. return "s3:PutObjectLegalHold"
  262. }
  263. // Check for multipart upload operations
  264. if _, hasUploads := query["uploads"]; hasUploads {
  265. return "s3:CreateMultipartUpload"
  266. }
  267. if _, hasUploadId := query["uploadId"]; hasUploadId {
  268. if _, hasPartNumber := query["partNumber"]; hasPartNumber {
  269. return "s3:UploadPart"
  270. }
  271. return "s3:CompleteMultipartUpload" // Complete multipart upload
  272. }
  273. // Default object write
  274. return "s3:PutObject"
  275. case "DELETE":
  276. // Object delete operations
  277. if _, hasTagging := query["tagging"]; hasTagging {
  278. return "s3:DeleteObjectTagging"
  279. }
  280. if _, hasUploadId := query["uploadId"]; hasUploadId {
  281. return "s3:AbortMultipartUpload"
  282. }
  283. // Default object delete
  284. return "s3:DeleteObject"
  285. }
  286. }
  287. // Handle bucket-level operations
  288. if bucket != "" {
  289. switch method {
  290. case "GET", "HEAD":
  291. // Bucket read operations - check for specific query parameters
  292. if _, hasAcl := query["acl"]; hasAcl {
  293. return "s3:GetBucketAcl"
  294. }
  295. if _, hasPolicy := query["policy"]; hasPolicy {
  296. return "s3:GetBucketPolicy"
  297. }
  298. if _, hasTagging := query["tagging"]; hasTagging {
  299. return "s3:GetBucketTagging"
  300. }
  301. if _, hasCors := query["cors"]; hasCors {
  302. return "s3:GetBucketCors"
  303. }
  304. if _, hasVersioning := query["versioning"]; hasVersioning {
  305. return "s3:GetBucketVersioning"
  306. }
  307. if _, hasNotification := query["notification"]; hasNotification {
  308. return "s3:GetBucketNotification"
  309. }
  310. if _, hasObjectLock := query["object-lock"]; hasObjectLock {
  311. return "s3:GetBucketObjectLockConfiguration"
  312. }
  313. if _, hasUploads := query["uploads"]; hasUploads {
  314. return "s3:ListMultipartUploads"
  315. }
  316. if _, hasVersions := query["versions"]; hasVersions {
  317. return "s3:ListBucketVersions"
  318. }
  319. // Default bucket read/list
  320. return "s3:ListBucket"
  321. case "PUT":
  322. // Bucket write operations - check for specific query parameters
  323. if _, hasAcl := query["acl"]; hasAcl {
  324. return "s3:PutBucketAcl"
  325. }
  326. if _, hasPolicy := query["policy"]; hasPolicy {
  327. return "s3:PutBucketPolicy"
  328. }
  329. if _, hasTagging := query["tagging"]; hasTagging {
  330. return "s3:PutBucketTagging"
  331. }
  332. if _, hasCors := query["cors"]; hasCors {
  333. return "s3:PutBucketCors"
  334. }
  335. if _, hasVersioning := query["versioning"]; hasVersioning {
  336. return "s3:PutBucketVersioning"
  337. }
  338. if _, hasNotification := query["notification"]; hasNotification {
  339. return "s3:PutBucketNotification"
  340. }
  341. if _, hasObjectLock := query["object-lock"]; hasObjectLock {
  342. return "s3:PutBucketObjectLockConfiguration"
  343. }
  344. // Default bucket creation
  345. return "s3:CreateBucket"
  346. case "DELETE":
  347. // Bucket delete operations - check for specific query parameters
  348. if _, hasPolicy := query["policy"]; hasPolicy {
  349. return "s3:DeleteBucketPolicy"
  350. }
  351. if _, hasTagging := query["tagging"]; hasTagging {
  352. return "s3:DeleteBucketTagging"
  353. }
  354. if _, hasCors := query["cors"]; hasCors {
  355. return "s3:DeleteBucketCors"
  356. }
  357. // Default bucket delete
  358. return "s3:DeleteBucket"
  359. }
  360. }
  361. // Fallback to legacy mapping for specific known actions
  362. return mapLegacyActionToIAM(fallbackAction)
  363. }
  364. // hasSpecificQueryParameters checks if the request has query parameters that indicate specific granular operations
  365. func hasSpecificQueryParameters(query url.Values) bool {
  366. // Check for object-level operation indicators
  367. objectParams := []string{
  368. "acl", // ACL operations
  369. "tagging", // Tagging operations
  370. "retention", // Object retention
  371. "legal-hold", // Legal hold
  372. "versions", // Versioning operations
  373. }
  374. // Check for multipart operation indicators
  375. multipartParams := []string{
  376. "uploads", // List/initiate multipart uploads
  377. "uploadId", // Part operations, complete, abort
  378. "partNumber", // Upload part
  379. }
  380. // Check for bucket-level operation indicators
  381. bucketParams := []string{
  382. "policy", // Bucket policy operations
  383. "website", // Website configuration
  384. "cors", // CORS configuration
  385. "lifecycle", // Lifecycle configuration
  386. "notification", // Event notification
  387. "replication", // Cross-region replication
  388. "encryption", // Server-side encryption
  389. "accelerate", // Transfer acceleration
  390. "requestPayment", // Request payment
  391. "logging", // Access logging
  392. "versioning", // Versioning configuration
  393. "inventory", // Inventory configuration
  394. "analytics", // Analytics configuration
  395. "metrics", // CloudWatch metrics
  396. "location", // Bucket location
  397. }
  398. // Check if any of these parameters are present
  399. allParams := append(append(objectParams, multipartParams...), bucketParams...)
  400. for _, param := range allParams {
  401. if _, exists := query[param]; exists {
  402. return true
  403. }
  404. }
  405. return false
  406. }
  407. // isMethodActionMismatch detects when HTTP method doesn't align with the intended S3 action
  408. // This provides a mechanism to use fallback action mapping when there's a semantic mismatch
  409. func isMethodActionMismatch(method string, fallbackAction Action) bool {
  410. switch fallbackAction {
  411. case s3_constants.ACTION_WRITE:
  412. // WRITE actions should typically use PUT, POST, or DELETE methods
  413. // GET/HEAD methods indicate read-oriented operations
  414. return method == "GET" || method == "HEAD"
  415. case s3_constants.ACTION_READ:
  416. // READ actions should typically use GET or HEAD methods
  417. // PUT, POST, DELETE methods indicate write-oriented operations
  418. return method == "PUT" || method == "POST" || method == "DELETE"
  419. case s3_constants.ACTION_LIST:
  420. // LIST actions should typically use GET method
  421. // PUT, POST, DELETE methods indicate write-oriented operations
  422. return method == "PUT" || method == "POST" || method == "DELETE"
  423. case s3_constants.ACTION_DELETE_BUCKET:
  424. // DELETE_BUCKET should use DELETE method
  425. // Other methods indicate different operation types
  426. return method != "DELETE"
  427. default:
  428. // For unknown actions or actions that already have s3: prefix, don't assume mismatch
  429. return false
  430. }
  431. }
  432. // mapLegacyActionToIAM provides fallback mapping for legacy actions
  433. // This ensures backward compatibility while the system transitions to granular actions
  434. func mapLegacyActionToIAM(legacyAction Action) string {
  435. switch legacyAction {
  436. case s3_constants.ACTION_READ:
  437. return "s3:GetObject" // Fallback for unmapped read operations
  438. case s3_constants.ACTION_WRITE:
  439. return "s3:PutObject" // Fallback for unmapped write operations
  440. case s3_constants.ACTION_LIST:
  441. return "s3:ListBucket" // Fallback for unmapped list operations
  442. case s3_constants.ACTION_TAGGING:
  443. return "s3:GetObjectTagging" // Fallback for unmapped tagging operations
  444. case s3_constants.ACTION_READ_ACP:
  445. return "s3:GetObjectAcl" // Fallback for unmapped ACL read operations
  446. case s3_constants.ACTION_WRITE_ACP:
  447. return "s3:PutObjectAcl" // Fallback for unmapped ACL write operations
  448. case s3_constants.ACTION_DELETE_BUCKET:
  449. return "s3:DeleteBucket" // Fallback for unmapped bucket delete operations
  450. case s3_constants.ACTION_ADMIN:
  451. return "s3:*" // Fallback for unmapped admin operations
  452. // Handle granular multipart actions (already correctly mapped)
  453. case s3_constants.ACTION_CREATE_MULTIPART_UPLOAD:
  454. return "s3:CreateMultipartUpload"
  455. case s3_constants.ACTION_UPLOAD_PART:
  456. return "s3:UploadPart"
  457. case s3_constants.ACTION_COMPLETE_MULTIPART:
  458. return "s3:CompleteMultipartUpload"
  459. case s3_constants.ACTION_ABORT_MULTIPART:
  460. return "s3:AbortMultipartUpload"
  461. case s3_constants.ACTION_LIST_MULTIPART_UPLOADS:
  462. return "s3:ListMultipartUploads"
  463. case s3_constants.ACTION_LIST_PARTS:
  464. return "s3:ListParts"
  465. default:
  466. // If it's already a properly formatted S3 action, return as-is
  467. actionStr := string(legacyAction)
  468. if strings.HasPrefix(actionStr, "s3:") {
  469. return actionStr
  470. }
  471. // Fallback: convert to S3 action format
  472. return "s3:" + actionStr
  473. }
  474. }
  475. // extractRequestContext extracts request context for policy conditions
  476. func extractRequestContext(r *http.Request) map[string]interface{} {
  477. context := make(map[string]interface{})
  478. // Extract source IP for IP-based conditions
  479. sourceIP := extractSourceIP(r)
  480. if sourceIP != "" {
  481. context["sourceIP"] = sourceIP
  482. }
  483. // Extract user agent
  484. if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
  485. context["userAgent"] = userAgent
  486. }
  487. // Extract request time
  488. context["requestTime"] = r.Context().Value("requestTime")
  489. // Extract additional headers that might be useful for conditions
  490. if referer := r.Header.Get("Referer"); referer != "" {
  491. context["referer"] = referer
  492. }
  493. return context
  494. }
  495. // extractSourceIP extracts the real source IP from the request
  496. func extractSourceIP(r *http.Request) string {
  497. // Check X-Forwarded-For header (most common for proxied requests)
  498. if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
  499. // X-Forwarded-For can contain multiple IPs, take the first one
  500. if ips := strings.Split(forwardedFor, ","); len(ips) > 0 {
  501. return strings.TrimSpace(ips[0])
  502. }
  503. }
  504. // Check X-Real-IP header
  505. if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
  506. return strings.TrimSpace(realIP)
  507. }
  508. // Fall back to RemoteAddr
  509. if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
  510. return ip
  511. }
  512. return r.RemoteAddr
  513. }
  514. // parseJWTToken parses a JWT token and returns its claims without verification
  515. // Note: This is for extracting claims only. Verification is done by the IAM system.
  516. func parseJWTToken(tokenString string) (jwt.MapClaims, error) {
  517. token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
  518. if err != nil {
  519. return nil, fmt.Errorf("failed to parse JWT token: %v", err)
  520. }
  521. claims, ok := token.Claims.(jwt.MapClaims)
  522. if !ok {
  523. return nil, fmt.Errorf("invalid token claims")
  524. }
  525. return claims, nil
  526. }
  527. // minInt returns the minimum of two integers
  528. func minInt(a, b int) int {
  529. if a < b {
  530. return a
  531. }
  532. return b
  533. }
  534. // SetIAMIntegration adds advanced IAM integration to the S3ApiServer
  535. func (s3a *S3ApiServer) SetIAMIntegration(iamManager *integration.IAMManager) {
  536. if s3a.iam != nil {
  537. s3a.iam.iamIntegration = NewS3IAMIntegration(iamManager, "localhost:8888")
  538. glog.V(0).Infof("IAM integration successfully set on S3ApiServer")
  539. } else {
  540. glog.Errorf("Cannot set IAM integration: s3a.iam is nil")
  541. }
  542. }
  543. // EnhancedS3ApiServer extends S3ApiServer with IAM integration
  544. type EnhancedS3ApiServer struct {
  545. *S3ApiServer
  546. iamIntegration *S3IAMIntegration
  547. }
  548. // NewEnhancedS3ApiServer creates an S3 API server with IAM integration
  549. func NewEnhancedS3ApiServer(baseServer *S3ApiServer, iamManager *integration.IAMManager) *EnhancedS3ApiServer {
  550. // Set the IAM integration on the base server
  551. baseServer.SetIAMIntegration(iamManager)
  552. return &EnhancedS3ApiServer{
  553. S3ApiServer: baseServer,
  554. iamIntegration: NewS3IAMIntegration(iamManager, "localhost:8888"),
  555. }
  556. }
  557. // AuthenticateJWTRequest handles JWT authentication for S3 requests
  558. func (enhanced *EnhancedS3ApiServer) AuthenticateJWTRequest(r *http.Request) (*Identity, s3err.ErrorCode) {
  559. ctx := r.Context()
  560. // Use our IAM integration for JWT authentication
  561. iamIdentity, errCode := enhanced.iamIntegration.AuthenticateJWT(ctx, r)
  562. if errCode != s3err.ErrNone {
  563. return nil, errCode
  564. }
  565. // Convert IAMIdentity to the existing Identity structure
  566. identity := &Identity{
  567. Name: iamIdentity.Name,
  568. Account: iamIdentity.Account,
  569. // Note: Actions will be determined by policy evaluation
  570. Actions: []Action{}, // Empty - authorization handled by policy engine
  571. }
  572. // Store session token for later authorization
  573. r.Header.Set("X-SeaweedFS-Session-Token", iamIdentity.SessionToken)
  574. r.Header.Set("X-SeaweedFS-Principal", iamIdentity.Principal)
  575. return identity, s3err.ErrNone
  576. }
  577. // AuthorizeRequest handles authorization for S3 requests using policy engine
  578. func (enhanced *EnhancedS3ApiServer) AuthorizeRequest(r *http.Request, identity *Identity, action Action) s3err.ErrorCode {
  579. ctx := r.Context()
  580. // Get session info from request headers (set during authentication)
  581. sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
  582. principal := r.Header.Get("X-SeaweedFS-Principal")
  583. if sessionToken == "" || principal == "" {
  584. glog.V(3).Info("No session information available for authorization")
  585. return s3err.ErrAccessDenied
  586. }
  587. // Extract bucket and object from request
  588. bucket, object := s3_constants.GetBucketAndObject(r)
  589. prefix := s3_constants.GetPrefix(r)
  590. // For List operations, use prefix for permission checking if available
  591. if action == s3_constants.ACTION_LIST && object == "" && prefix != "" {
  592. object = prefix
  593. } else if (object == "/" || object == "") && prefix != "" {
  594. object = prefix
  595. }
  596. // Create IAM identity for authorization
  597. iamIdentity := &IAMIdentity{
  598. Name: identity.Name,
  599. Principal: principal,
  600. SessionToken: sessionToken,
  601. Account: identity.Account,
  602. }
  603. // Use our IAM integration for authorization
  604. return enhanced.iamIntegration.AuthorizeAction(ctx, iamIdentity, action, bucket, object, r)
  605. }
  606. // OIDCIdentity represents an identity validated through OIDC
  607. type OIDCIdentity struct {
  608. UserID string
  609. RoleArn string
  610. Provider string
  611. }
  612. // validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
  613. // This method delegates to the STS service's validateWebIdentityToken for better security and efficiency
  614. func (s3iam *S3IAMIntegration) validateExternalOIDCToken(ctx context.Context, token string) (*OIDCIdentity, error) {
  615. if s3iam.iamManager == nil {
  616. return nil, fmt.Errorf("IAM manager not available")
  617. }
  618. // Get STS service for secure token validation
  619. stsService := s3iam.iamManager.GetSTSService()
  620. if stsService == nil {
  621. return nil, fmt.Errorf("STS service not available")
  622. }
  623. // Use the STS service's secure validateWebIdentityToken method
  624. // This method uses issuer-based lookup to select the correct provider, which is more secure and efficient
  625. externalIdentity, provider, err := stsService.ValidateWebIdentityToken(ctx, token)
  626. if err != nil {
  627. return nil, fmt.Errorf("token validation failed: %w", err)
  628. }
  629. if externalIdentity == nil {
  630. return nil, fmt.Errorf("authentication succeeded but no identity returned")
  631. }
  632. // Extract role from external identity attributes
  633. rolesAttr, exists := externalIdentity.Attributes["roles"]
  634. if !exists || rolesAttr == "" {
  635. glog.V(3).Infof("No roles found in external identity")
  636. return nil, fmt.Errorf("no roles found in external identity")
  637. }
  638. // Parse roles (stored as comma-separated string)
  639. rolesStr := strings.TrimSpace(rolesAttr)
  640. roles := strings.Split(rolesStr, ",")
  641. // Clean up role names
  642. var cleanRoles []string
  643. for _, role := range roles {
  644. cleanRole := strings.TrimSpace(role)
  645. if cleanRole != "" {
  646. cleanRoles = append(cleanRoles, cleanRole)
  647. }
  648. }
  649. if len(cleanRoles) == 0 {
  650. glog.V(3).Infof("Empty roles list after parsing")
  651. return nil, fmt.Errorf("no valid roles found in token")
  652. }
  653. // Determine the primary role using intelligent selection
  654. roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)
  655. return &OIDCIdentity{
  656. UserID: externalIdentity.UserID,
  657. RoleArn: roleArn,
  658. Provider: fmt.Sprintf("%T", provider), // Use provider type as identifier
  659. }, nil
  660. }
  661. // selectPrimaryRole simply picks the first role from the list
  662. // The OIDC provider should return roles in priority order (most important first)
  663. func (s3iam *S3IAMIntegration) selectPrimaryRole(roles []string, externalIdentity *providers.ExternalIdentity) string {
  664. if len(roles) == 0 {
  665. return ""
  666. }
  667. // Just pick the first one - keep it simple
  668. selectedRole := roles[0]
  669. return selectedRole
  670. }
  671. // isSTSIssuer determines if an issuer belongs to the STS service
  672. // Uses exact match against configured STS issuer for security and correctness
  673. func (s3iam *S3IAMIntegration) isSTSIssuer(issuer string) bool {
  674. if s3iam.stsService == nil || s3iam.stsService.Config == nil {
  675. return false
  676. }
  677. // Directly compare with the configured STS issuer for exact match
  678. // This prevents false positives from external OIDC providers that might
  679. // contain STS-related keywords in their issuer URLs
  680. return issuer == s3iam.stsService.Config.Issuer
  681. }