You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							490 lines
						
					
					
						
							14 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							490 lines
						
					
					
						
							14 KiB
						
					
					
				
								package s3api
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"context"
							 | 
						|
									"net/http"
							 | 
						|
									"net/http/httptest"
							 | 
						|
									"net/url"
							 | 
						|
									"testing"
							 | 
						|
									"time"
							 | 
						|
								
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/integration"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/policy"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/sts"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/iam/utils"
							 | 
						|
									"github.com/seaweedfs/seaweedfs/weed/s3api/s3_constants"
							 | 
						|
									"github.com/stretchr/testify/assert"
							 | 
						|
									"github.com/stretchr/testify/require"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// TestS3IAMMiddleware tests the basic S3 IAM middleware functionality
							 | 
						|
								func TestS3IAMMiddleware(t *testing.T) {
							 | 
						|
									// Create IAM manager
							 | 
						|
									iamManager := integration.NewIAMManager()
							 | 
						|
								
							 | 
						|
									// Initialize with test configuration
							 | 
						|
									config := &integration.IAMConfig{
							 | 
						|
										STS: &sts.STSConfig{
							 | 
						|
											TokenDuration:    sts.FlexibleDuration{time.Hour},
							 | 
						|
											MaxSessionLength: sts.FlexibleDuration{time.Hour * 12},
							 | 
						|
											Issuer:           "test-sts",
							 | 
						|
											SigningKey:       []byte("test-signing-key-32-characters-long"),
							 | 
						|
										},
							 | 
						|
										Policy: &policy.PolicyEngineConfig{
							 | 
						|
											DefaultEffect: "Deny",
							 | 
						|
											StoreType:     "memory",
							 | 
						|
										},
							 | 
						|
										Roles: &integration.RoleStoreConfig{
							 | 
						|
											StoreType: "memory",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									err := iamManager.Initialize(config, func() string {
							 | 
						|
										return "localhost:8888" // Mock filer address for testing
							 | 
						|
									})
							 | 
						|
									require.NoError(t, err)
							 | 
						|
								
							 | 
						|
									// Create S3 IAM integration
							 | 
						|
									s3IAMIntegration := NewS3IAMIntegration(iamManager, "localhost:8888")
							 | 
						|
								
							 | 
						|
									// Test that integration is created successfully
							 | 
						|
									assert.NotNil(t, s3IAMIntegration)
							 | 
						|
									assert.True(t, s3IAMIntegration.enabled)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestS3IAMMiddlewareJWTAuth tests JWT authentication
							 | 
						|
								func TestS3IAMMiddlewareJWTAuth(t *testing.T) {
							 | 
						|
									// Skip for now since it requires full setup
							 | 
						|
									t.Skip("JWT authentication test requires full IAM setup")
							 | 
						|
								
							 | 
						|
									// Create IAM integration
							 | 
						|
									s3iam := NewS3IAMIntegration(nil, "localhost:8888") // Disabled integration
							 | 
						|
								
							 | 
						|
									// Create test request with JWT token
							 | 
						|
									req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
							 | 
						|
									req.Header.Set("Authorization", "Bearer test-token")
							 | 
						|
								
							 | 
						|
									// Test authentication (should return not implemented when disabled)
							 | 
						|
									ctx := context.Background()
							 | 
						|
									identity, errCode := s3iam.AuthenticateJWT(ctx, req)
							 | 
						|
								
							 | 
						|
									assert.Nil(t, identity)
							 | 
						|
									assert.NotEqual(t, errCode, 0) // Should return an error
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestBuildS3ResourceArn tests resource ARN building
							 | 
						|
								func TestBuildS3ResourceArn(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name     string
							 | 
						|
										bucket   string
							 | 
						|
										object   string
							 | 
						|
										expected string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:     "empty bucket and object",
							 | 
						|
											bucket:   "",
							 | 
						|
											object:   "",
							 | 
						|
											expected: "arn:seaweed:s3:::*",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:     "bucket only",
							 | 
						|
											bucket:   "test-bucket",
							 | 
						|
											object:   "",
							 | 
						|
											expected: "arn:seaweed:s3:::test-bucket",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:     "bucket and object",
							 | 
						|
											bucket:   "test-bucket",
							 | 
						|
											object:   "test-object.txt",
							 | 
						|
											expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:     "bucket and object with leading slash",
							 | 
						|
											bucket:   "test-bucket",
							 | 
						|
											object:   "/test-object.txt",
							 | 
						|
											expected: "arn:seaweed:s3:::test-bucket/test-object.txt",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:     "bucket and nested object",
							 | 
						|
											bucket:   "test-bucket",
							 | 
						|
											object:   "folder/subfolder/test-object.txt",
							 | 
						|
											expected: "arn:seaweed:s3:::test-bucket/folder/subfolder/test-object.txt",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											result := buildS3ResourceArn(tt.bucket, tt.object)
							 | 
						|
											assert.Equal(t, tt.expected, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestDetermineGranularS3Action tests granular S3 action determination from HTTP requests
							 | 
						|
								func TestDetermineGranularS3Action(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name           string
							 | 
						|
										method         string
							 | 
						|
										bucket         string
							 | 
						|
										objectKey      string
							 | 
						|
										queryParams    map[string]string
							 | 
						|
										fallbackAction Action
							 | 
						|
										expected       string
							 | 
						|
										description    string
							 | 
						|
									}{
							 | 
						|
										// Object-level operations
							 | 
						|
										{
							 | 
						|
											name:           "get_object",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_READ,
							 | 
						|
											expected:       "s3:GetObject",
							 | 
						|
											description:    "Basic object retrieval",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "get_object_acl",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{"acl": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_READ_ACP,
							 | 
						|
											expected:       "s3:GetObjectAcl",
							 | 
						|
											description:    "Object ACL retrieval",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "get_object_tagging",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{"tagging": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_TAGGING,
							 | 
						|
											expected:       "s3:GetObjectTagging",
							 | 
						|
											description:    "Object tagging retrieval",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "put_object",
							 | 
						|
											method:         "PUT",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:PutObject",
							 | 
						|
											description:    "Basic object upload",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "put_object_acl",
							 | 
						|
											method:         "PUT",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{"acl": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE_ACP,
							 | 
						|
											expected:       "s3:PutObjectAcl",
							 | 
						|
											description:    "Object ACL modification",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "delete_object",
							 | 
						|
											method:         "DELETE",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE, // DELETE object uses WRITE fallback
							 | 
						|
											expected:       "s3:DeleteObject",
							 | 
						|
											description:    "Object deletion - correctly mapped to DeleteObject (not PutObject)",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "delete_object_tagging",
							 | 
						|
											method:         "DELETE",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "test-object.txt",
							 | 
						|
											queryParams:    map[string]string{"tagging": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_TAGGING,
							 | 
						|
											expected:       "s3:DeleteObjectTagging",
							 | 
						|
											description:    "Object tag deletion",
							 | 
						|
										},
							 | 
						|
								
							 | 
						|
										// Multipart upload operations
							 | 
						|
										{
							 | 
						|
											name:           "create_multipart_upload",
							 | 
						|
											method:         "POST",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "large-file.txt",
							 | 
						|
											queryParams:    map[string]string{"uploads": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:CreateMultipartUpload",
							 | 
						|
											description:    "Multipart upload initiation",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "upload_part",
							 | 
						|
											method:         "PUT",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "large-file.txt",
							 | 
						|
											queryParams:    map[string]string{"uploadId": "12345", "partNumber": "1"},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:UploadPart",
							 | 
						|
											description:    "Multipart part upload",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "complete_multipart_upload",
							 | 
						|
											method:         "POST",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "large-file.txt",
							 | 
						|
											queryParams:    map[string]string{"uploadId": "12345"},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:CompleteMultipartUpload",
							 | 
						|
											description:    "Multipart upload completion",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "abort_multipart_upload",
							 | 
						|
											method:         "DELETE",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "large-file.txt",
							 | 
						|
											queryParams:    map[string]string{"uploadId": "12345"},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:AbortMultipartUpload",
							 | 
						|
											description:    "Multipart upload abort",
							 | 
						|
										},
							 | 
						|
								
							 | 
						|
										// Bucket-level operations
							 | 
						|
										{
							 | 
						|
											name:           "list_bucket",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_LIST,
							 | 
						|
											expected:       "s3:ListBucket",
							 | 
						|
											description:    "Bucket listing",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "get_bucket_acl",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{"acl": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_READ_ACP,
							 | 
						|
											expected:       "s3:GetBucketAcl",
							 | 
						|
											description:    "Bucket ACL retrieval",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "put_bucket_policy",
							 | 
						|
											method:         "PUT",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{"policy": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:       "s3:PutBucketPolicy",
							 | 
						|
											description:    "Bucket policy modification",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "delete_bucket",
							 | 
						|
											method:         "DELETE",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_DELETE_BUCKET,
							 | 
						|
											expected:       "s3:DeleteBucket",
							 | 
						|
											description:    "Bucket deletion",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "list_multipart_uploads",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "test-bucket",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{"uploads": ""},
							 | 
						|
											fallbackAction: s3_constants.ACTION_LIST,
							 | 
						|
											expected:       "s3:ListMultipartUploads",
							 | 
						|
											description:    "List multipart uploads in bucket",
							 | 
						|
										},
							 | 
						|
								
							 | 
						|
										// Fallback scenarios
							 | 
						|
										{
							 | 
						|
											name:           "legacy_read_fallback",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: s3_constants.ACTION_READ,
							 | 
						|
											expected:       "s3:GetObject",
							 | 
						|
											description:    "Legacy read action fallback",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:           "already_granular_action",
							 | 
						|
											method:         "GET",
							 | 
						|
											bucket:         "",
							 | 
						|
											objectKey:      "",
							 | 
						|
											queryParams:    map[string]string{},
							 | 
						|
											fallbackAction: "s3:GetBucketLocation", // Already granular
							 | 
						|
											expected:       "s3:GetBucketLocation",
							 | 
						|
											description:    "Already granular action passed through",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											// Create HTTP request with query parameters
							 | 
						|
											req := &http.Request{
							 | 
						|
												Method: tt.method,
							 | 
						|
												URL:    &url.URL{Path: "/" + tt.bucket + "/" + tt.objectKey},
							 | 
						|
											}
							 | 
						|
								
							 | 
						|
											// Add query parameters
							 | 
						|
											query := req.URL.Query()
							 | 
						|
											for key, value := range tt.queryParams {
							 | 
						|
												query.Set(key, value)
							 | 
						|
											}
							 | 
						|
											req.URL.RawQuery = query.Encode()
							 | 
						|
								
							 | 
						|
											// Test the granular action determination
							 | 
						|
											result := determineGranularS3Action(req, tt.fallbackAction, tt.bucket, tt.objectKey)
							 | 
						|
								
							 | 
						|
											assert.Equal(t, tt.expected, result,
							 | 
						|
												"Test %s failed: %s. Expected %s but got %s",
							 | 
						|
												tt.name, tt.description, tt.expected, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestMapLegacyActionToIAM tests the legacy action fallback mapping
							 | 
						|
								func TestMapLegacyActionToIAM(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name         string
							 | 
						|
										legacyAction Action
							 | 
						|
										expected     string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:         "read_action_fallback",
							 | 
						|
											legacyAction: s3_constants.ACTION_READ,
							 | 
						|
											expected:     "s3:GetObject",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:         "write_action_fallback",
							 | 
						|
											legacyAction: s3_constants.ACTION_WRITE,
							 | 
						|
											expected:     "s3:PutObject",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:         "admin_action_fallback",
							 | 
						|
											legacyAction: s3_constants.ACTION_ADMIN,
							 | 
						|
											expected:     "s3:*",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:         "granular_multipart_action",
							 | 
						|
											legacyAction: s3_constants.ACTION_CREATE_MULTIPART_UPLOAD,
							 | 
						|
											expected:     "s3:CreateMultipartUpload",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:         "unknown_action_with_s3_prefix",
							 | 
						|
											legacyAction: "s3:CustomAction",
							 | 
						|
											expected:     "s3:CustomAction",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:         "unknown_action_without_prefix",
							 | 
						|
											legacyAction: "CustomAction",
							 | 
						|
											expected:     "s3:CustomAction",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											result := mapLegacyActionToIAM(tt.legacyAction)
							 | 
						|
											assert.Equal(t, tt.expected, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestExtractSourceIP tests source IP extraction from requests
							 | 
						|
								func TestExtractSourceIP(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name       string
							 | 
						|
										setupReq   func() *http.Request
							 | 
						|
										expectedIP string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name: "X-Forwarded-For header",
							 | 
						|
											setupReq: func() *http.Request {
							 | 
						|
												req := httptest.NewRequest("GET", "/test", http.NoBody)
							 | 
						|
												req.Header.Set("X-Forwarded-For", "192.168.1.100, 10.0.0.1")
							 | 
						|
												return req
							 | 
						|
											},
							 | 
						|
											expectedIP: "192.168.1.100",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name: "X-Real-IP header",
							 | 
						|
											setupReq: func() *http.Request {
							 | 
						|
												req := httptest.NewRequest("GET", "/test", http.NoBody)
							 | 
						|
												req.Header.Set("X-Real-IP", "192.168.1.200")
							 | 
						|
												return req
							 | 
						|
											},
							 | 
						|
											expectedIP: "192.168.1.200",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name: "RemoteAddr fallback",
							 | 
						|
											setupReq: func() *http.Request {
							 | 
						|
												req := httptest.NewRequest("GET", "/test", http.NoBody)
							 | 
						|
												req.RemoteAddr = "192.168.1.300:12345"
							 | 
						|
												return req
							 | 
						|
											},
							 | 
						|
											expectedIP: "192.168.1.300",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											req := tt.setupReq()
							 | 
						|
											result := extractSourceIP(req)
							 | 
						|
											assert.Equal(t, tt.expectedIP, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestExtractRoleNameFromPrincipal tests role name extraction
							 | 
						|
								func TestExtractRoleNameFromPrincipal(t *testing.T) {
							 | 
						|
									tests := []struct {
							 | 
						|
										name      string
							 | 
						|
										principal string
							 | 
						|
										expected  string
							 | 
						|
									}{
							 | 
						|
										{
							 | 
						|
											name:      "valid assumed role ARN",
							 | 
						|
											principal: "arn:seaweed:sts::assumed-role/S3ReadOnlyRole/session-123",
							 | 
						|
											expected:  "S3ReadOnlyRole",
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:      "invalid format",
							 | 
						|
											principal: "invalid-principal",
							 | 
						|
											expected:  "", // Returns empty string to signal invalid format
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:      "missing session name",
							 | 
						|
											principal: "arn:seaweed:sts::assumed-role/TestRole",
							 | 
						|
											expected:  "TestRole", // Extracts role name even without session name
							 | 
						|
										},
							 | 
						|
										{
							 | 
						|
											name:      "empty principal",
							 | 
						|
											principal: "",
							 | 
						|
											expected:  "",
							 | 
						|
										},
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, tt := range tests {
							 | 
						|
										t.Run(tt.name, func(t *testing.T) {
							 | 
						|
											result := utils.ExtractRoleNameFromPrincipal(tt.principal)
							 | 
						|
											assert.Equal(t, tt.expected, result)
							 | 
						|
										})
							 | 
						|
									}
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// TestIAMIdentityIsAdmin tests the IsAdmin method
							 | 
						|
								func TestIAMIdentityIsAdmin(t *testing.T) {
							 | 
						|
									identity := &IAMIdentity{
							 | 
						|
										Name:         "test-identity",
							 | 
						|
										Principal:    "arn:seaweed:sts::assumed-role/TestRole/session",
							 | 
						|
										SessionToken: "test-token",
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// In our implementation, IsAdmin always returns false since admin status
							 | 
						|
									// is determined by policies, not identity
							 | 
						|
									result := identity.IsAdmin()
							 | 
						|
									assert.False(t, result)
							 | 
						|
								}
							 |