| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794 |
- package s3api
- import (
- "context"
- "fmt"
- "net"
- "net/http"
- "net/url"
- "strings"
- "time"
- "github.com/golang-jwt/jwt/v5"
- "github.com/seaweedfs/seaweedfs/weed/glog"
- "github.com/seaweedfs/seaweedfs/weed/iam/integration"
- "github.com/seaweedfs/seaweedfs/weed/iam/providers"
- "github.com/seaweedfs/seaweedfs/weed/iam/sts"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
- "github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
- )
- // S3IAMIntegration provides IAM integration for S3 API
- type S3IAMIntegration struct {
- iamManager *integration.IAMManager
- stsService *sts.STSService
- filerAddress string
- enabled bool
- }
- // NewS3IAMIntegration creates a new S3 IAM integration
- func NewS3IAMIntegration(iamManager *integration.IAMManager, filerAddress string) *S3IAMIntegration {
- var stsService *sts.STSService
- if iamManager != nil {
- stsService = iamManager.GetSTSService()
- }
- return &S3IAMIntegration{
- iamManager: iamManager,
- stsService: stsService,
- filerAddress: filerAddress,
- enabled: iamManager != nil,
- }
- }
- // AuthenticateJWT authenticates JWT tokens using our STS service
- func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Request) (*IAMIdentity, s3err.ErrorCode) {
- if !s3iam.enabled {
- return nil, s3err.ErrNotImplemented
- }
- // Extract bearer token from Authorization header
- authHeader := r.Header.Get("Authorization")
- if !strings.HasPrefix(authHeader, "Bearer ") {
- return nil, s3err.ErrAccessDenied
- }
- sessionToken := strings.TrimPrefix(authHeader, "Bearer ")
- if sessionToken == "" {
- return nil, s3err.ErrAccessDenied
- }
- // Basic token format validation - reject obviously invalid tokens
- if sessionToken == "invalid-token" || len(sessionToken) < 10 {
- glog.V(3).Info("Session token format is invalid")
- return nil, s3err.ErrAccessDenied
- }
- // Try to parse as STS session token first
- tokenClaims, err := parseJWTToken(sessionToken)
- if err != nil {
- glog.V(3).Infof("Failed to parse JWT token: %v", err)
- return nil, s3err.ErrAccessDenied
- }
- // Determine token type by issuer claim (more robust than checking role claim)
- issuer, issuerOk := tokenClaims["iss"].(string)
- if !issuerOk {
- glog.V(3).Infof("Token missing issuer claim - invalid JWT")
- return nil, s3err.ErrAccessDenied
- }
- // Check if this is an STS-issued token by examining the issuer
- if !s3iam.isSTSIssuer(issuer) {
- // Not an STS session token, try to validate as OIDC token with timeout
- // Create a context with a reasonable timeout to prevent hanging
- ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
- defer cancel()
- identity, err := s3iam.validateExternalOIDCToken(ctx, sessionToken)
- if err != nil {
- return nil, s3err.ErrAccessDenied
- }
- // Extract role from OIDC identity
- if identity.RoleArn == "" {
- return nil, s3err.ErrAccessDenied
- }
- // Return IAM identity for OIDC token
- return &IAMIdentity{
- Name: identity.UserID,
- Principal: identity.RoleArn,
- SessionToken: sessionToken,
- Account: &Account{
- DisplayName: identity.UserID,
- EmailAddress: identity.UserID + "@oidc.local",
- Id: identity.UserID,
- },
- }, s3err.ErrNone
- }
- // This is an STS-issued token - extract STS session information
- // Extract role claim from STS token
- roleName, roleOk := tokenClaims["role"].(string)
- if !roleOk || roleName == "" {
- glog.V(3).Infof("STS token missing role claim")
- return nil, s3err.ErrAccessDenied
- }
- sessionName, ok := tokenClaims["snam"].(string)
- if !ok || sessionName == "" {
- sessionName = "jwt-session" // Default fallback
- }
- subject, ok := tokenClaims["sub"].(string)
- if !ok || subject == "" {
- subject = "jwt-user" // Default fallback
- }
- // Use the principal ARN directly from token claims, or build it if not available
- principalArn, ok := tokenClaims["principal"].(string)
- if !ok || principalArn == "" {
- // Fallback: extract role name from role ARN and build principal ARN
- roleNameOnly := roleName
- if strings.Contains(roleName, "/") {
- parts := strings.Split(roleName, "/")
- roleNameOnly = parts[len(parts)-1]
- }
- principalArn = fmt.Sprintf("arn:seaweed:sts::assumed-role/%s/%s", roleNameOnly, sessionName)
- }
- // Validate the JWT token directly using STS service (avoid circular dependency)
- // Note: We don't call IsActionAllowed here because that would create a circular dependency
- // Authentication should only validate the token, authorization happens later
- _, err = s3iam.stsService.ValidateSessionToken(ctx, sessionToken)
- if err != nil {
- glog.V(3).Infof("STS session validation failed: %v", err)
- return nil, s3err.ErrAccessDenied
- }
- // Create IAM identity from validated token
- identity := &IAMIdentity{
- Name: subject,
- Principal: principalArn,
- SessionToken: sessionToken,
- Account: &Account{
- DisplayName: roleName,
- EmailAddress: subject + "@seaweedfs.local",
- Id: subject,
- },
- }
- glog.V(3).Infof("JWT authentication successful for principal: %s", identity.Principal)
- return identity, s3err.ErrNone
- }
- // AuthorizeAction authorizes actions using our policy engine
- func (s3iam *S3IAMIntegration) AuthorizeAction(ctx context.Context, identity *IAMIdentity, action Action, bucket string, objectKey string, r *http.Request) s3err.ErrorCode {
- if !s3iam.enabled {
- return s3err.ErrNone // Fallback to existing authorization
- }
- if identity.SessionToken == "" {
- return s3err.ErrAccessDenied
- }
- // Build resource ARN for the S3 operation
- resourceArn := buildS3ResourceArn(bucket, objectKey)
- // Extract request context for policy conditions
- requestContext := extractRequestContext(r)
- // Determine the specific S3 action based on the HTTP request details
- specificAction := determineGranularS3Action(r, action, bucket, objectKey)
- // Create action request
- actionRequest := &integration.ActionRequest{
- Principal: identity.Principal,
- Action: specificAction,
- Resource: resourceArn,
- SessionToken: identity.SessionToken,
- RequestContext: requestContext,
- }
- // Check if action is allowed using our policy engine
- allowed, err := s3iam.iamManager.IsActionAllowed(ctx, actionRequest)
- if err != nil {
- return s3err.ErrAccessDenied
- }
- if !allowed {
- return s3err.ErrAccessDenied
- }
- return s3err.ErrNone
- }
- // IAMIdentity represents an authenticated identity with session information
- type IAMIdentity struct {
- Name string
- Principal string
- SessionToken string
- Account *Account
- }
- // IsAdmin checks if the identity has admin privileges
- func (identity *IAMIdentity) IsAdmin() bool {
- // In our IAM system, admin status is determined by policies, not identity
- // This is handled by the policy engine during authorization
- return false
- }
- // Mock session structures for validation
- type MockSessionInfo struct {
- AssumedRoleUser MockAssumedRoleUser
- }
- type MockAssumedRoleUser struct {
- AssumedRoleId string
- Arn string
- }
- // Helper functions
- // buildS3ResourceArn builds an S3 resource ARN from bucket and object
- func buildS3ResourceArn(bucket string, objectKey string) string {
- if bucket == "" {
- return "arn:seaweed:s3:::*"
- }
- if objectKey == "" || objectKey == "/" {
- return "arn:seaweed:s3:::" + bucket
- }
- // Remove leading slash from object key if present
- if strings.HasPrefix(objectKey, "/") {
- objectKey = objectKey[1:]
- }
- return "arn:seaweed:s3:::" + bucket + "/" + objectKey
- }
- // determineGranularS3Action determines the specific S3 IAM action based on HTTP request details
- // This provides granular, operation-specific actions for accurate IAM policy enforcement
- func determineGranularS3Action(r *http.Request, fallbackAction Action, bucket string, objectKey string) string {
- method := r.Method
- query := r.URL.Query()
- // Check if there are specific query parameters indicating granular operations
- // If there are, always use granular mapping regardless of method-action alignment
- hasGranularIndicators := hasSpecificQueryParameters(query)
- // Only check for method-action mismatch when there are NO granular indicators
- // This provides fallback behavior for cases where HTTP method doesn't align with intended action
- if !hasGranularIndicators && isMethodActionMismatch(method, fallbackAction) {
- return mapLegacyActionToIAM(fallbackAction)
- }
- // Handle object-level operations when method and action are aligned
- if objectKey != "" && objectKey != "/" {
- switch method {
- case "GET", "HEAD":
- // Object read operations - check for specific query parameters
- if _, hasAcl := query["acl"]; hasAcl {
- return "s3:GetObjectAcl"
- }
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:GetObjectTagging"
- }
- if _, hasRetention := query["retention"]; hasRetention {
- return "s3:GetObjectRetention"
- }
- if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
- return "s3:GetObjectLegalHold"
- }
- if _, hasVersions := query["versions"]; hasVersions {
- return "s3:GetObjectVersion"
- }
- if _, hasUploadId := query["uploadId"]; hasUploadId {
- return "s3:ListParts"
- }
- // Default object read
- return "s3:GetObject"
- case "PUT", "POST":
- // Object write operations - check for specific query parameters
- if _, hasAcl := query["acl"]; hasAcl {
- return "s3:PutObjectAcl"
- }
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:PutObjectTagging"
- }
- if _, hasRetention := query["retention"]; hasRetention {
- return "s3:PutObjectRetention"
- }
- if _, hasLegalHold := query["legal-hold"]; hasLegalHold {
- return "s3:PutObjectLegalHold"
- }
- // Check for multipart upload operations
- if _, hasUploads := query["uploads"]; hasUploads {
- return "s3:CreateMultipartUpload"
- }
- if _, hasUploadId := query["uploadId"]; hasUploadId {
- if _, hasPartNumber := query["partNumber"]; hasPartNumber {
- return "s3:UploadPart"
- }
- return "s3:CompleteMultipartUpload" // Complete multipart upload
- }
- // Default object write
- return "s3:PutObject"
- case "DELETE":
- // Object delete operations
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:DeleteObjectTagging"
- }
- if _, hasUploadId := query["uploadId"]; hasUploadId {
- return "s3:AbortMultipartUpload"
- }
- // Default object delete
- return "s3:DeleteObject"
- }
- }
- // Handle bucket-level operations
- if bucket != "" {
- switch method {
- case "GET", "HEAD":
- // Bucket read operations - check for specific query parameters
- if _, hasAcl := query["acl"]; hasAcl {
- return "s3:GetBucketAcl"
- }
- if _, hasPolicy := query["policy"]; hasPolicy {
- return "s3:GetBucketPolicy"
- }
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:GetBucketTagging"
- }
- if _, hasCors := query["cors"]; hasCors {
- return "s3:GetBucketCors"
- }
- if _, hasVersioning := query["versioning"]; hasVersioning {
- return "s3:GetBucketVersioning"
- }
- if _, hasNotification := query["notification"]; hasNotification {
- return "s3:GetBucketNotification"
- }
- if _, hasObjectLock := query["object-lock"]; hasObjectLock {
- return "s3:GetBucketObjectLockConfiguration"
- }
- if _, hasUploads := query["uploads"]; hasUploads {
- return "s3:ListMultipartUploads"
- }
- if _, hasVersions := query["versions"]; hasVersions {
- return "s3:ListBucketVersions"
- }
- // Default bucket read/list
- return "s3:ListBucket"
- case "PUT":
- // Bucket write operations - check for specific query parameters
- if _, hasAcl := query["acl"]; hasAcl {
- return "s3:PutBucketAcl"
- }
- if _, hasPolicy := query["policy"]; hasPolicy {
- return "s3:PutBucketPolicy"
- }
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:PutBucketTagging"
- }
- if _, hasCors := query["cors"]; hasCors {
- return "s3:PutBucketCors"
- }
- if _, hasVersioning := query["versioning"]; hasVersioning {
- return "s3:PutBucketVersioning"
- }
- if _, hasNotification := query["notification"]; hasNotification {
- return "s3:PutBucketNotification"
- }
- if _, hasObjectLock := query["object-lock"]; hasObjectLock {
- return "s3:PutBucketObjectLockConfiguration"
- }
- // Default bucket creation
- return "s3:CreateBucket"
- case "DELETE":
- // Bucket delete operations - check for specific query parameters
- if _, hasPolicy := query["policy"]; hasPolicy {
- return "s3:DeleteBucketPolicy"
- }
- if _, hasTagging := query["tagging"]; hasTagging {
- return "s3:DeleteBucketTagging"
- }
- if _, hasCors := query["cors"]; hasCors {
- return "s3:DeleteBucketCors"
- }
- // Default bucket delete
- return "s3:DeleteBucket"
- }
- }
- // Fallback to legacy mapping for specific known actions
- return mapLegacyActionToIAM(fallbackAction)
- }
- // hasSpecificQueryParameters checks if the request has query parameters that indicate specific granular operations
- func hasSpecificQueryParameters(query url.Values) bool {
- // Check for object-level operation indicators
- objectParams := []string{
- "acl", // ACL operations
- "tagging", // Tagging operations
- "retention", // Object retention
- "legal-hold", // Legal hold
- "versions", // Versioning operations
- }
- // Check for multipart operation indicators
- multipartParams := []string{
- "uploads", // List/initiate multipart uploads
- "uploadId", // Part operations, complete, abort
- "partNumber", // Upload part
- }
- // Check for bucket-level operation indicators
- bucketParams := []string{
- "policy", // Bucket policy operations
- "website", // Website configuration
- "cors", // CORS configuration
- "lifecycle", // Lifecycle configuration
- "notification", // Event notification
- "replication", // Cross-region replication
- "encryption", // Server-side encryption
- "accelerate", // Transfer acceleration
- "requestPayment", // Request payment
- "logging", // Access logging
- "versioning", // Versioning configuration
- "inventory", // Inventory configuration
- "analytics", // Analytics configuration
- "metrics", // CloudWatch metrics
- "location", // Bucket location
- }
- // Check if any of these parameters are present
- allParams := append(append(objectParams, multipartParams...), bucketParams...)
- for _, param := range allParams {
- if _, exists := query[param]; exists {
- return true
- }
- }
- return false
- }
- // isMethodActionMismatch detects when HTTP method doesn't align with the intended S3 action
- // This provides a mechanism to use fallback action mapping when there's a semantic mismatch
- func isMethodActionMismatch(method string, fallbackAction Action) bool {
- switch fallbackAction {
- case s3_constants.ACTION_WRITE:
- // WRITE actions should typically use PUT, POST, or DELETE methods
- // GET/HEAD methods indicate read-oriented operations
- return method == "GET" || method == "HEAD"
- case s3_constants.ACTION_READ:
- // READ actions should typically use GET or HEAD methods
- // PUT, POST, DELETE methods indicate write-oriented operations
- return method == "PUT" || method == "POST" || method == "DELETE"
- case s3_constants.ACTION_LIST:
- // LIST actions should typically use GET method
- // PUT, POST, DELETE methods indicate write-oriented operations
- return method == "PUT" || method == "POST" || method == "DELETE"
- case s3_constants.ACTION_DELETE_BUCKET:
- // DELETE_BUCKET should use DELETE method
- // Other methods indicate different operation types
- return method != "DELETE"
- default:
- // For unknown actions or actions that already have s3: prefix, don't assume mismatch
- return false
- }
- }
- // mapLegacyActionToIAM provides fallback mapping for legacy actions
- // This ensures backward compatibility while the system transitions to granular actions
- func mapLegacyActionToIAM(legacyAction Action) string {
- switch legacyAction {
- case s3_constants.ACTION_READ:
- return "s3:GetObject" // Fallback for unmapped read operations
- case s3_constants.ACTION_WRITE:
- return "s3:PutObject" // Fallback for unmapped write operations
- case s3_constants.ACTION_LIST:
- return "s3:ListBucket" // Fallback for unmapped list operations
- case s3_constants.ACTION_TAGGING:
- return "s3:GetObjectTagging" // Fallback for unmapped tagging operations
- case s3_constants.ACTION_READ_ACP:
- return "s3:GetObjectAcl" // Fallback for unmapped ACL read operations
- case s3_constants.ACTION_WRITE_ACP:
- return "s3:PutObjectAcl" // Fallback for unmapped ACL write operations
- case s3_constants.ACTION_DELETE_BUCKET:
- return "s3:DeleteBucket" // Fallback for unmapped bucket delete operations
- case s3_constants.ACTION_ADMIN:
- return "s3:*" // Fallback for unmapped admin operations
- // Handle granular multipart actions (already correctly mapped)
- case s3_constants.ACTION_CREATE_MULTIPART_UPLOAD:
- return "s3:CreateMultipartUpload"
- case s3_constants.ACTION_UPLOAD_PART:
- return "s3:UploadPart"
- case s3_constants.ACTION_COMPLETE_MULTIPART:
- return "s3:CompleteMultipartUpload"
- case s3_constants.ACTION_ABORT_MULTIPART:
- return "s3:AbortMultipartUpload"
- case s3_constants.ACTION_LIST_MULTIPART_UPLOADS:
- return "s3:ListMultipartUploads"
- case s3_constants.ACTION_LIST_PARTS:
- return "s3:ListParts"
- default:
- // If it's already a properly formatted S3 action, return as-is
- actionStr := string(legacyAction)
- if strings.HasPrefix(actionStr, "s3:") {
- return actionStr
- }
- // Fallback: convert to S3 action format
- return "s3:" + actionStr
- }
- }
- // extractRequestContext extracts request context for policy conditions
- func extractRequestContext(r *http.Request) map[string]interface{} {
- context := make(map[string]interface{})
- // Extract source IP for IP-based conditions
- sourceIP := extractSourceIP(r)
- if sourceIP != "" {
- context["sourceIP"] = sourceIP
- }
- // Extract user agent
- if userAgent := r.Header.Get("User-Agent"); userAgent != "" {
- context["userAgent"] = userAgent
- }
- // Extract request time
- context["requestTime"] = r.Context().Value("requestTime")
- // Extract additional headers that might be useful for conditions
- if referer := r.Header.Get("Referer"); referer != "" {
- context["referer"] = referer
- }
- return context
- }
- // extractSourceIP extracts the real source IP from the request
- func extractSourceIP(r *http.Request) string {
- // Check X-Forwarded-For header (most common for proxied requests)
- if forwardedFor := r.Header.Get("X-Forwarded-For"); forwardedFor != "" {
- // X-Forwarded-For can contain multiple IPs, take the first one
- if ips := strings.Split(forwardedFor, ","); len(ips) > 0 {
- return strings.TrimSpace(ips[0])
- }
- }
- // Check X-Real-IP header
- if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
- return strings.TrimSpace(realIP)
- }
- // Fall back to RemoteAddr
- if ip, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
- return ip
- }
- return r.RemoteAddr
- }
- // parseJWTToken parses a JWT token and returns its claims without verification
- // Note: This is for extracting claims only. Verification is done by the IAM system.
- func parseJWTToken(tokenString string) (jwt.MapClaims, error) {
- token, _, err := new(jwt.Parser).ParseUnverified(tokenString, jwt.MapClaims{})
- if err != nil {
- return nil, fmt.Errorf("failed to parse JWT token: %v", err)
- }
- claims, ok := token.Claims.(jwt.MapClaims)
- if !ok {
- return nil, fmt.Errorf("invalid token claims")
- }
- return claims, nil
- }
- // minInt returns the minimum of two integers
- func minInt(a, b int) int {
- if a < b {
- return a
- }
- return b
- }
- // SetIAMIntegration adds advanced IAM integration to the S3ApiServer
- func (s3a *S3ApiServer) SetIAMIntegration(iamManager *integration.IAMManager) {
- if s3a.iam != nil {
- s3a.iam.iamIntegration = NewS3IAMIntegration(iamManager, "localhost:8888")
- glog.V(0).Infof("IAM integration successfully set on S3ApiServer")
- } else {
- glog.Errorf("Cannot set IAM integration: s3a.iam is nil")
- }
- }
- // EnhancedS3ApiServer extends S3ApiServer with IAM integration
- type EnhancedS3ApiServer struct {
- *S3ApiServer
- iamIntegration *S3IAMIntegration
- }
- // NewEnhancedS3ApiServer creates an S3 API server with IAM integration
- func NewEnhancedS3ApiServer(baseServer *S3ApiServer, iamManager *integration.IAMManager) *EnhancedS3ApiServer {
- // Set the IAM integration on the base server
- baseServer.SetIAMIntegration(iamManager)
- return &EnhancedS3ApiServer{
- S3ApiServer: baseServer,
- iamIntegration: NewS3IAMIntegration(iamManager, "localhost:8888"),
- }
- }
- // AuthenticateJWTRequest handles JWT authentication for S3 requests
- func (enhanced *EnhancedS3ApiServer) AuthenticateJWTRequest(r *http.Request) (*Identity, s3err.ErrorCode) {
- ctx := r.Context()
- // Use our IAM integration for JWT authentication
- iamIdentity, errCode := enhanced.iamIntegration.AuthenticateJWT(ctx, r)
- if errCode != s3err.ErrNone {
- return nil, errCode
- }
- // Convert IAMIdentity to the existing Identity structure
- identity := &Identity{
- Name: iamIdentity.Name,
- Account: iamIdentity.Account,
- // Note: Actions will be determined by policy evaluation
- Actions: []Action{}, // Empty - authorization handled by policy engine
- }
- // Store session token for later authorization
- r.Header.Set("X-SeaweedFS-Session-Token", iamIdentity.SessionToken)
- r.Header.Set("X-SeaweedFS-Principal", iamIdentity.Principal)
- return identity, s3err.ErrNone
- }
- // AuthorizeRequest handles authorization for S3 requests using policy engine
- func (enhanced *EnhancedS3ApiServer) AuthorizeRequest(r *http.Request, identity *Identity, action Action) s3err.ErrorCode {
- ctx := r.Context()
- // Get session info from request headers (set during authentication)
- sessionToken := r.Header.Get("X-SeaweedFS-Session-Token")
- principal := r.Header.Get("X-SeaweedFS-Principal")
- if sessionToken == "" || principal == "" {
- glog.V(3).Info("No session information available for authorization")
- return s3err.ErrAccessDenied
- }
- // Extract bucket and object from request
- bucket, object := s3_constants.GetBucketAndObject(r)
- prefix := s3_constants.GetPrefix(r)
- // For List operations, use prefix for permission checking if available
- if action == s3_constants.ACTION_LIST && object == "" && prefix != "" {
- object = prefix
- } else if (object == "/" || object == "") && prefix != "" {
- object = prefix
- }
- // Create IAM identity for authorization
- iamIdentity := &IAMIdentity{
- Name: identity.Name,
- Principal: principal,
- SessionToken: sessionToken,
- Account: identity.Account,
- }
- // Use our IAM integration for authorization
- return enhanced.iamIntegration.AuthorizeAction(ctx, iamIdentity, action, bucket, object, r)
- }
- // OIDCIdentity represents an identity validated through OIDC
- type OIDCIdentity struct {
- UserID string
- RoleArn string
- Provider string
- }
- // validateExternalOIDCToken validates an external OIDC token using the STS service's secure issuer-based lookup
- // This method delegates to the STS service's validateWebIdentityToken for better security and efficiency
- func (s3iam *S3IAMIntegration) validateExternalOIDCToken(ctx context.Context, token string) (*OIDCIdentity, error) {
- if s3iam.iamManager == nil {
- return nil, fmt.Errorf("IAM manager not available")
- }
- // Get STS service for secure token validation
- stsService := s3iam.iamManager.GetSTSService()
- if stsService == nil {
- return nil, fmt.Errorf("STS service not available")
- }
- // Use the STS service's secure validateWebIdentityToken method
- // This method uses issuer-based lookup to select the correct provider, which is more secure and efficient
- externalIdentity, provider, err := stsService.ValidateWebIdentityToken(ctx, token)
- if err != nil {
- return nil, fmt.Errorf("token validation failed: %w", err)
- }
- if externalIdentity == nil {
- return nil, fmt.Errorf("authentication succeeded but no identity returned")
- }
- // Extract role from external identity attributes
- rolesAttr, exists := externalIdentity.Attributes["roles"]
- if !exists || rolesAttr == "" {
- glog.V(3).Infof("No roles found in external identity")
- return nil, fmt.Errorf("no roles found in external identity")
- }
- // Parse roles (stored as comma-separated string)
- rolesStr := strings.TrimSpace(rolesAttr)
- roles := strings.Split(rolesStr, ",")
- // Clean up role names
- var cleanRoles []string
- for _, role := range roles {
- cleanRole := strings.TrimSpace(role)
- if cleanRole != "" {
- cleanRoles = append(cleanRoles, cleanRole)
- }
- }
- if len(cleanRoles) == 0 {
- glog.V(3).Infof("Empty roles list after parsing")
- return nil, fmt.Errorf("no valid roles found in token")
- }
- // Determine the primary role using intelligent selection
- roleArn := s3iam.selectPrimaryRole(cleanRoles, externalIdentity)
- return &OIDCIdentity{
- UserID: externalIdentity.UserID,
- RoleArn: roleArn,
- Provider: fmt.Sprintf("%T", provider), // Use provider type as identifier
- }, nil
- }
- // selectPrimaryRole simply picks the first role from the list
- // The OIDC provider should return roles in priority order (most important first)
- func (s3iam *S3IAMIntegration) selectPrimaryRole(roles []string, externalIdentity *providers.ExternalIdentity) string {
- if len(roles) == 0 {
- return ""
- }
- // Just pick the first one - keep it simple
- selectedRole := roles[0]
- return selectedRole
- }
- // isSTSIssuer determines if an issuer belongs to the STS service
- // Uses exact match against configured STS issuer for security and correctness
- func (s3iam *S3IAMIntegration) isSTSIssuer(issuer string) bool {
- if s3iam.stsService == nil || s3iam.stsService.Config == nil {
- return false
- }
- // Directly compare with the configured STS issuer for exact match
- // This prevents false positives from external OIDC providers that might
- // contain STS-related keywords in their issuer URLs
- return issuer == s3iam.stsService.Config.Issuer
- }
|