authentik.admin.files.backends.tests.test_s3_backend
1from unittest import skipUnless 2 3from botocore.exceptions import UnsupportedSignatureVersionError 4from django.test import TestCase 5 6from authentik.admin.files.tests.utils import FileTestS3BackendMixin, s3_test_server_available 7from authentik.admin.files.usage import FileUsage 8from authentik.lib.config import CONFIG 9 10 11@skipUnless(s3_test_server_available(), "S3 test server not available") 12class TestS3Backend(FileTestS3BackendMixin, TestCase): 13 """Test S3 backend functionality""" 14 15 def setUp(self): 16 super().setUp() 17 18 def test_base_path(self): 19 """Test base_path property generates correct S3 key prefix""" 20 expected = "media/public" 21 self.assertEqual(self.media_s3_backend.base_path, expected) 22 23 def test_supports_file_path_s3(self): 24 """Test supports_file_path returns True for s3 backend""" 25 self.assertTrue(self.media_s3_backend.supports_file("path/to/any-file.png")) 26 self.assertTrue(self.media_s3_backend.supports_file("any-file.png")) 27 28 def test_list_files(self): 29 """Test list_files returns relative paths""" 30 self.media_s3_backend.client.put_object( 31 Bucket=self.media_s3_bucket_name, 32 Key="media/public/file1.png", 33 Body=b"test content", 34 ACL="private", 35 ) 36 self.media_s3_backend.client.put_object( 37 Bucket=self.media_s3_bucket_name, 38 Key="media/other/file1.png", 39 Body=b"test content", 40 ACL="private", 41 ) 42 43 files = list(self.media_s3_backend.list_files()) 44 45 self.assertEqual(len(files), 1) 46 self.assertIn("file1.png", files) 47 48 def test_list_files_empty(self): 49 """Test list_files with no files""" 50 files = list(self.media_s3_backend.list_files()) 51 52 self.assertEqual(len(files), 0) 53 54 def test_save_file(self): 55 """Test save_file uploads to S3""" 56 content = b"test file content" 57 self.media_s3_backend.save_file("test.png", content) 58 59 def test_save_file_stream(self): 60 """Test save_file_stream uploads to S3 using context manager""" 61 with self.media_s3_backend.save_file_stream("test.csv") as f: 62 f.write(b"header1,header2\n") 63 f.write(b"value1,value2\n") 64 65 def test_delete_file(self): 66 """Test delete_file removes from S3""" 67 self.media_s3_backend.client.put_object( 68 Bucket=self.media_s3_bucket_name, 69 Key="media/public/test.png", 70 Body=b"test content", 71 ACL="private", 72 ) 73 self.media_s3_backend.delete_file("test.png") 74 75 @CONFIG.patch("storage.s3.secure_urls", True) 76 @CONFIG.patch("storage.s3.custom_domain", None) 77 def test_file_url_basic(self): 78 """Test file_url generates presigned URL with AWS signature format""" 79 url = self.media_s3_backend.file_url("test.png") 80 81 self.assertIn("X-Amz-Algorithm=AWS4-HMAC-SHA256", url) 82 self.assertIn("X-Amz-Signature=", url) 83 self.assertIn("test.png", url) 84 85 def test_client_signature_version_default_v4(self): 86 """Test S3 client defaults to v4 signature when not configured.""" 87 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4") 88 89 @CONFIG.patch("storage.s3.signature_version", "s3") 90 def test_client_signature_version_global_override(self): 91 """Test S3 client respects globally configured signature version.""" 92 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3") 93 94 @CONFIG.patch("storage.s3.signature_version", "s3v4") 95 @CONFIG.patch("storage.media.s3.signature_version", "s3") 96 def test_client_signature_version_media_override(self): 97 """Test usage-specific signature version takes precedence over global.""" 98 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3") 99 100 @CONFIG.patch("storage.media.s3.signature_version", "not-a-real-signature") 101 def test_client_signature_version_unsupported(self): 102 """Test unsupported signature version raises botocore error.""" 103 with self.assertRaises(UnsupportedSignatureVersionError): 104 self.media_s3_backend.file_url("test.png", use_cache=False) 105 106 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 107 def test_file_exists_true(self): 108 """Test file_exists returns True for existing file""" 109 self.media_s3_backend.client.put_object( 110 Bucket=self.media_s3_bucket_name, 111 Key="media/public/test.png", 112 Body=b"test content", 113 ACL="private", 114 ) 115 116 exists = self.media_s3_backend.file_exists("test.png") 117 118 self.assertTrue(exists) 119 120 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 121 def test_file_exists_false(self): 122 """Test file_exists returns False for non-existent file""" 123 exists = self.media_s3_backend.file_exists("nonexistent.png") 124 125 self.assertFalse(exists) 126 127 def test_allowed_usages(self): 128 """Test that S3Backend supports all usage types""" 129 self.assertEqual(self.media_s3_backend.allowed_usages, list(FileUsage)) 130 131 def test_reports_usage(self): 132 """Test S3Backend with REPORTS usage""" 133 self.assertEqual(self.reports_s3_backend.usage, FileUsage.REPORTS) 134 self.assertEqual(self.reports_s3_backend.base_path, "reports/public") 135 136 @CONFIG.patch("storage.s3.secure_urls", True) 137 @CONFIG.patch("storage.s3.addressing_style", "path") 138 def test_file_url_custom_domain_with_bucket_no_duplicate(self): 139 """Test file_url doesn't duplicate bucket name when custom_domain includes bucket. 140 141 Regression test for https://github.com/goauthentik/authentik/issues/19521 142 143 When using: 144 - Path-style addressing (bucket name goes in URL path, not subdomain) 145 - Custom domain that includes the bucket name (e.g., s3.example.com/bucket-name) 146 147 The bucket name should NOT appear twice in the final URL. 148 149 Example of the bug: 150 - custom_domain = "s3.example.com/authentik-media" 151 - boto3 presigned URL = "http://s3.example.com/authentik-media/media/public/file.png?..." 152 - Buggy result = "https://s3.example.com/authentik-media/authentik-media/media/public/file.png?..." 153 """ 154 bucket_name = self.media_s3_bucket_name 155 156 # Custom domain includes the bucket name 157 custom_domain = f"localhost:8020/{bucket_name}" 158 159 with CONFIG.patch("storage.media.s3.custom_domain", custom_domain): 160 url = self.media_s3_backend.file_url("application-icons/test.svg", use_cache=False) 161 162 # The bucket name should appear exactly once in the URL path, not twice 163 bucket_occurrences = url.count(bucket_name) 164 self.assertEqual( 165 bucket_occurrences, 166 1, 167 f"Bucket name '{bucket_name}' appears {bucket_occurrences} times in URL, expected 1. " 168 f"URL: {url}", 169 ) 170 171 def test_themed_urls_without_theme_variable(self): 172 """Test themed_urls returns None when filename has no %(theme)s""" 173 result = self.media_s3_backend.themed_urls("logo.png") 174 self.assertIsNone(result) 175 176 def test_themed_urls_with_theme_variable(self): 177 """Test themed_urls returns dict of presigned URLs for each theme""" 178 result = self.media_s3_backend.themed_urls("logo-%(theme)s.png") 179 180 self.assertIsInstance(result, dict) 181 self.assertIn("light", result) 182 self.assertIn("dark", result) 183 184 # Check URLs are valid presigned URLs with correct file paths 185 self.assertIn("logo-light.png", result["light"]) 186 self.assertIn("logo-dark.png", result["dark"]) 187 self.assertIn("X-Amz-Signature=", result["light"]) 188 self.assertIn("X-Amz-Signature=", result["dark"]) 189 190 def test_themed_urls_multiple_theme_variables(self): 191 """Test themed_urls with multiple %(theme)s in path""" 192 result = self.media_s3_backend.themed_urls("%(theme)s/logo-%(theme)s.svg") 193 194 self.assertIsInstance(result, dict) 195 self.assertIn("light/logo-light.svg", result["light"]) 196 self.assertIn("dark/logo-dark.svg", result["dark"]) 197 198 def test_save_file_sets_content_type_svg(self): 199 """Test save_file sets correct ContentType for SVG files""" 200 self.media_s3_backend.save_file("test.svg", b"<svg></svg>") 201 202 response = self.media_s3_backend.client.head_object( 203 Bucket=self.media_s3_bucket_name, 204 Key="media/public/test.svg", 205 ) 206 self.assertEqual(response["ContentType"], "image/svg+xml") 207 208 def test_save_file_sets_content_type_png(self): 209 """Test save_file sets correct ContentType for PNG files""" 210 self.media_s3_backend.save_file("test.png", b"\x89PNG\r\n\x1a\n") 211 212 response = self.media_s3_backend.client.head_object( 213 Bucket=self.media_s3_bucket_name, 214 Key="media/public/test.png", 215 ) 216 self.assertEqual(response["ContentType"], "image/png") 217 218 def test_save_file_stream_sets_content_type(self): 219 """Test save_file_stream sets correct ContentType""" 220 with self.media_s3_backend.save_file_stream("test.css") as f: 221 f.write(b"body { color: red; }") 222 223 response = self.media_s3_backend.client.head_object( 224 Bucket=self.media_s3_bucket_name, 225 Key="media/public/test.css", 226 ) 227 self.assertEqual(response["ContentType"], "text/css") 228 229 def test_save_file_unknown_extension_octet_stream(self): 230 """Test save_file sets octet-stream for unknown extensions""" 231 self.media_s3_backend.save_file("test.unknownext123", b"data") 232 233 response = self.media_s3_backend.client.head_object( 234 Bucket=self.media_s3_bucket_name, 235 Key="media/public/test.unknownext123", 236 ) 237 self.assertEqual(response["ContentType"], "application/octet-stream")
12@skipUnless(s3_test_server_available(), "S3 test server not available") 13class TestS3Backend(FileTestS3BackendMixin, TestCase): 14 """Test S3 backend functionality""" 15 16 def setUp(self): 17 super().setUp() 18 19 def test_base_path(self): 20 """Test base_path property generates correct S3 key prefix""" 21 expected = "media/public" 22 self.assertEqual(self.media_s3_backend.base_path, expected) 23 24 def test_supports_file_path_s3(self): 25 """Test supports_file_path returns True for s3 backend""" 26 self.assertTrue(self.media_s3_backend.supports_file("path/to/any-file.png")) 27 self.assertTrue(self.media_s3_backend.supports_file("any-file.png")) 28 29 def test_list_files(self): 30 """Test list_files returns relative paths""" 31 self.media_s3_backend.client.put_object( 32 Bucket=self.media_s3_bucket_name, 33 Key="media/public/file1.png", 34 Body=b"test content", 35 ACL="private", 36 ) 37 self.media_s3_backend.client.put_object( 38 Bucket=self.media_s3_bucket_name, 39 Key="media/other/file1.png", 40 Body=b"test content", 41 ACL="private", 42 ) 43 44 files = list(self.media_s3_backend.list_files()) 45 46 self.assertEqual(len(files), 1) 47 self.assertIn("file1.png", files) 48 49 def test_list_files_empty(self): 50 """Test list_files with no files""" 51 files = list(self.media_s3_backend.list_files()) 52 53 self.assertEqual(len(files), 0) 54 55 def test_save_file(self): 56 """Test save_file uploads to S3""" 57 content = b"test file content" 58 self.media_s3_backend.save_file("test.png", content) 59 60 def test_save_file_stream(self): 61 """Test save_file_stream uploads to S3 using context manager""" 62 with self.media_s3_backend.save_file_stream("test.csv") as f: 63 f.write(b"header1,header2\n") 64 f.write(b"value1,value2\n") 65 66 def test_delete_file(self): 67 """Test delete_file removes from S3""" 68 self.media_s3_backend.client.put_object( 69 Bucket=self.media_s3_bucket_name, 70 Key="media/public/test.png", 71 Body=b"test content", 72 ACL="private", 73 ) 74 self.media_s3_backend.delete_file("test.png") 75 76 @CONFIG.patch("storage.s3.secure_urls", True) 77 @CONFIG.patch("storage.s3.custom_domain", None) 78 def test_file_url_basic(self): 79 """Test file_url generates presigned URL with AWS signature format""" 80 url = self.media_s3_backend.file_url("test.png") 81 82 self.assertIn("X-Amz-Algorithm=AWS4-HMAC-SHA256", url) 83 self.assertIn("X-Amz-Signature=", url) 84 self.assertIn("test.png", url) 85 86 def test_client_signature_version_default_v4(self): 87 """Test S3 client defaults to v4 signature when not configured.""" 88 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4") 89 90 @CONFIG.patch("storage.s3.signature_version", "s3") 91 def test_client_signature_version_global_override(self): 92 """Test S3 client respects globally configured signature version.""" 93 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3") 94 95 @CONFIG.patch("storage.s3.signature_version", "s3v4") 96 @CONFIG.patch("storage.media.s3.signature_version", "s3") 97 def test_client_signature_version_media_override(self): 98 """Test usage-specific signature version takes precedence over global.""" 99 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3") 100 101 @CONFIG.patch("storage.media.s3.signature_version", "not-a-real-signature") 102 def test_client_signature_version_unsupported(self): 103 """Test unsupported signature version raises botocore error.""" 104 with self.assertRaises(UnsupportedSignatureVersionError): 105 self.media_s3_backend.file_url("test.png", use_cache=False) 106 107 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 108 def test_file_exists_true(self): 109 """Test file_exists returns True for existing file""" 110 self.media_s3_backend.client.put_object( 111 Bucket=self.media_s3_bucket_name, 112 Key="media/public/test.png", 113 Body=b"test content", 114 ACL="private", 115 ) 116 117 exists = self.media_s3_backend.file_exists("test.png") 118 119 self.assertTrue(exists) 120 121 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 122 def test_file_exists_false(self): 123 """Test file_exists returns False for non-existent file""" 124 exists = self.media_s3_backend.file_exists("nonexistent.png") 125 126 self.assertFalse(exists) 127 128 def test_allowed_usages(self): 129 """Test that S3Backend supports all usage types""" 130 self.assertEqual(self.media_s3_backend.allowed_usages, list(FileUsage)) 131 132 def test_reports_usage(self): 133 """Test S3Backend with REPORTS usage""" 134 self.assertEqual(self.reports_s3_backend.usage, FileUsage.REPORTS) 135 self.assertEqual(self.reports_s3_backend.base_path, "reports/public") 136 137 @CONFIG.patch("storage.s3.secure_urls", True) 138 @CONFIG.patch("storage.s3.addressing_style", "path") 139 def test_file_url_custom_domain_with_bucket_no_duplicate(self): 140 """Test file_url doesn't duplicate bucket name when custom_domain includes bucket. 141 142 Regression test for https://github.com/goauthentik/authentik/issues/19521 143 144 When using: 145 - Path-style addressing (bucket name goes in URL path, not subdomain) 146 - Custom domain that includes the bucket name (e.g., s3.example.com/bucket-name) 147 148 The bucket name should NOT appear twice in the final URL. 149 150 Example of the bug: 151 - custom_domain = "s3.example.com/authentik-media" 152 - boto3 presigned URL = "http://s3.example.com/authentik-media/media/public/file.png?..." 153 - Buggy result = "https://s3.example.com/authentik-media/authentik-media/media/public/file.png?..." 154 """ 155 bucket_name = self.media_s3_bucket_name 156 157 # Custom domain includes the bucket name 158 custom_domain = f"localhost:8020/{bucket_name}" 159 160 with CONFIG.patch("storage.media.s3.custom_domain", custom_domain): 161 url = self.media_s3_backend.file_url("application-icons/test.svg", use_cache=False) 162 163 # The bucket name should appear exactly once in the URL path, not twice 164 bucket_occurrences = url.count(bucket_name) 165 self.assertEqual( 166 bucket_occurrences, 167 1, 168 f"Bucket name '{bucket_name}' appears {bucket_occurrences} times in URL, expected 1. " 169 f"URL: {url}", 170 ) 171 172 def test_themed_urls_without_theme_variable(self): 173 """Test themed_urls returns None when filename has no %(theme)s""" 174 result = self.media_s3_backend.themed_urls("logo.png") 175 self.assertIsNone(result) 176 177 def test_themed_urls_with_theme_variable(self): 178 """Test themed_urls returns dict of presigned URLs for each theme""" 179 result = self.media_s3_backend.themed_urls("logo-%(theme)s.png") 180 181 self.assertIsInstance(result, dict) 182 self.assertIn("light", result) 183 self.assertIn("dark", result) 184 185 # Check URLs are valid presigned URLs with correct file paths 186 self.assertIn("logo-light.png", result["light"]) 187 self.assertIn("logo-dark.png", result["dark"]) 188 self.assertIn("X-Amz-Signature=", result["light"]) 189 self.assertIn("X-Amz-Signature=", result["dark"]) 190 191 def test_themed_urls_multiple_theme_variables(self): 192 """Test themed_urls with multiple %(theme)s in path""" 193 result = self.media_s3_backend.themed_urls("%(theme)s/logo-%(theme)s.svg") 194 195 self.assertIsInstance(result, dict) 196 self.assertIn("light/logo-light.svg", result["light"]) 197 self.assertIn("dark/logo-dark.svg", result["dark"]) 198 199 def test_save_file_sets_content_type_svg(self): 200 """Test save_file sets correct ContentType for SVG files""" 201 self.media_s3_backend.save_file("test.svg", b"<svg></svg>") 202 203 response = self.media_s3_backend.client.head_object( 204 Bucket=self.media_s3_bucket_name, 205 Key="media/public/test.svg", 206 ) 207 self.assertEqual(response["ContentType"], "image/svg+xml") 208 209 def test_save_file_sets_content_type_png(self): 210 """Test save_file sets correct ContentType for PNG files""" 211 self.media_s3_backend.save_file("test.png", b"\x89PNG\r\n\x1a\n") 212 213 response = self.media_s3_backend.client.head_object( 214 Bucket=self.media_s3_bucket_name, 215 Key="media/public/test.png", 216 ) 217 self.assertEqual(response["ContentType"], "image/png") 218 219 def test_save_file_stream_sets_content_type(self): 220 """Test save_file_stream sets correct ContentType""" 221 with self.media_s3_backend.save_file_stream("test.css") as f: 222 f.write(b"body { color: red; }") 223 224 response = self.media_s3_backend.client.head_object( 225 Bucket=self.media_s3_bucket_name, 226 Key="media/public/test.css", 227 ) 228 self.assertEqual(response["ContentType"], "text/css") 229 230 def test_save_file_unknown_extension_octet_stream(self): 231 """Test save_file sets octet-stream for unknown extensions""" 232 self.media_s3_backend.save_file("test.unknownext123", b"data") 233 234 response = self.media_s3_backend.client.head_object( 235 Bucket=self.media_s3_bucket_name, 236 Key="media/public/test.unknownext123", 237 ) 238 self.assertEqual(response["ContentType"], "application/octet-stream")
Test S3 backend functionality
19 def test_base_path(self): 20 """Test base_path property generates correct S3 key prefix""" 21 expected = "media/public" 22 self.assertEqual(self.media_s3_backend.base_path, expected)
Test base_path property generates correct S3 key prefix
24 def test_supports_file_path_s3(self): 25 """Test supports_file_path returns True for s3 backend""" 26 self.assertTrue(self.media_s3_backend.supports_file("path/to/any-file.png")) 27 self.assertTrue(self.media_s3_backend.supports_file("any-file.png"))
Test supports_file_path returns True for s3 backend
29 def test_list_files(self): 30 """Test list_files returns relative paths""" 31 self.media_s3_backend.client.put_object( 32 Bucket=self.media_s3_bucket_name, 33 Key="media/public/file1.png", 34 Body=b"test content", 35 ACL="private", 36 ) 37 self.media_s3_backend.client.put_object( 38 Bucket=self.media_s3_bucket_name, 39 Key="media/other/file1.png", 40 Body=b"test content", 41 ACL="private", 42 ) 43 44 files = list(self.media_s3_backend.list_files()) 45 46 self.assertEqual(len(files), 1) 47 self.assertIn("file1.png", files)
Test list_files returns relative paths
49 def test_list_files_empty(self): 50 """Test list_files with no files""" 51 files = list(self.media_s3_backend.list_files()) 52 53 self.assertEqual(len(files), 0)
Test list_files with no files
55 def test_save_file(self): 56 """Test save_file uploads to S3""" 57 content = b"test file content" 58 self.media_s3_backend.save_file("test.png", content)
Test save_file uploads to S3
60 def test_save_file_stream(self): 61 """Test save_file_stream uploads to S3 using context manager""" 62 with self.media_s3_backend.save_file_stream("test.csv") as f: 63 f.write(b"header1,header2\n") 64 f.write(b"value1,value2\n")
Test save_file_stream uploads to S3 using context manager
66 def test_delete_file(self): 67 """Test delete_file removes from S3""" 68 self.media_s3_backend.client.put_object( 69 Bucket=self.media_s3_bucket_name, 70 Key="media/public/test.png", 71 Body=b"test content", 72 ACL="private", 73 ) 74 self.media_s3_backend.delete_file("test.png")
Test delete_file removes from S3
76 @CONFIG.patch("storage.s3.secure_urls", True) 77 @CONFIG.patch("storage.s3.custom_domain", None) 78 def test_file_url_basic(self): 79 """Test file_url generates presigned URL with AWS signature format""" 80 url = self.media_s3_backend.file_url("test.png") 81 82 self.assertIn("X-Amz-Algorithm=AWS4-HMAC-SHA256", url) 83 self.assertIn("X-Amz-Signature=", url) 84 self.assertIn("test.png", url)
Test file_url generates presigned URL with AWS signature format
86 def test_client_signature_version_default_v4(self): 87 """Test S3 client defaults to v4 signature when not configured.""" 88 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3v4")
Test S3 client defaults to v4 signature when not configured.
90 @CONFIG.patch("storage.s3.signature_version", "s3") 91 def test_client_signature_version_global_override(self): 92 """Test S3 client respects globally configured signature version.""" 93 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
Test S3 client respects globally configured signature version.
95 @CONFIG.patch("storage.s3.signature_version", "s3v4") 96 @CONFIG.patch("storage.media.s3.signature_version", "s3") 97 def test_client_signature_version_media_override(self): 98 """Test usage-specific signature version takes precedence over global.""" 99 self.assertEqual(self.media_s3_backend.client.meta.config.signature_version, "s3")
Test usage-specific signature version takes precedence over global.
101 @CONFIG.patch("storage.media.s3.signature_version", "not-a-real-signature") 102 def test_client_signature_version_unsupported(self): 103 """Test unsupported signature version raises botocore error.""" 104 with self.assertRaises(UnsupportedSignatureVersionError): 105 self.media_s3_backend.file_url("test.png", use_cache=False)
Test unsupported signature version raises botocore error.
107 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 108 def test_file_exists_true(self): 109 """Test file_exists returns True for existing file""" 110 self.media_s3_backend.client.put_object( 111 Bucket=self.media_s3_bucket_name, 112 Key="media/public/test.png", 113 Body=b"test content", 114 ACL="private", 115 ) 116 117 exists = self.media_s3_backend.file_exists("test.png") 118 119 self.assertTrue(exists)
Test file_exists returns True for existing file
121 @CONFIG.patch("storage.s3.bucket_name", "test-bucket") 122 def test_file_exists_false(self): 123 """Test file_exists returns False for non-existent file""" 124 exists = self.media_s3_backend.file_exists("nonexistent.png") 125 126 self.assertFalse(exists)
Test file_exists returns False for non-existent file
128 def test_allowed_usages(self): 129 """Test that S3Backend supports all usage types""" 130 self.assertEqual(self.media_s3_backend.allowed_usages, list(FileUsage))
Test that S3Backend supports all usage types
132 def test_reports_usage(self): 133 """Test S3Backend with REPORTS usage""" 134 self.assertEqual(self.reports_s3_backend.usage, FileUsage.REPORTS) 135 self.assertEqual(self.reports_s3_backend.base_path, "reports/public")
Test S3Backend with REPORTS usage
137 @CONFIG.patch("storage.s3.secure_urls", True) 138 @CONFIG.patch("storage.s3.addressing_style", "path") 139 def test_file_url_custom_domain_with_bucket_no_duplicate(self): 140 """Test file_url doesn't duplicate bucket name when custom_domain includes bucket. 141 142 Regression test for https://github.com/goauthentik/authentik/issues/19521 143 144 When using: 145 - Path-style addressing (bucket name goes in URL path, not subdomain) 146 - Custom domain that includes the bucket name (e.g., s3.example.com/bucket-name) 147 148 The bucket name should NOT appear twice in the final URL. 149 150 Example of the bug: 151 - custom_domain = "s3.example.com/authentik-media" 152 - boto3 presigned URL = "http://s3.example.com/authentik-media/media/public/file.png?..." 153 - Buggy result = "https://s3.example.com/authentik-media/authentik-media/media/public/file.png?..." 154 """ 155 bucket_name = self.media_s3_bucket_name 156 157 # Custom domain includes the bucket name 158 custom_domain = f"localhost:8020/{bucket_name}" 159 160 with CONFIG.patch("storage.media.s3.custom_domain", custom_domain): 161 url = self.media_s3_backend.file_url("application-icons/test.svg", use_cache=False) 162 163 # The bucket name should appear exactly once in the URL path, not twice 164 bucket_occurrences = url.count(bucket_name) 165 self.assertEqual( 166 bucket_occurrences, 167 1, 168 f"Bucket name '{bucket_name}' appears {bucket_occurrences} times in URL, expected 1. " 169 f"URL: {url}", 170 )
Test file_url doesn't duplicate bucket name when custom_domain includes bucket.
Regression test for https://github.com/goauthentik/authentik/issues/19521
When using:
- Path-style addressing (bucket name goes in URL path, not subdomain)
- Custom domain that includes the bucket name (e.g., s3.example.com/bucket-name)
The bucket name should NOT appear twice in the final URL.
Example of the bug:
- custom_domain = "s3.example.com/authentik-media"
- boto3 presigned URL = "http://s3.example.com/authentik-media/media/public/file.png?..."
- Buggy result = "https://s3.example.com/authentik-media/authentik-media/media/public/file.png?..."
172 def test_themed_urls_without_theme_variable(self): 173 """Test themed_urls returns None when filename has no %(theme)s""" 174 result = self.media_s3_backend.themed_urls("logo.png") 175 self.assertIsNone(result)
Test themed_urls returns None when filename has no %(theme)s
177 def test_themed_urls_with_theme_variable(self): 178 """Test themed_urls returns dict of presigned URLs for each theme""" 179 result = self.media_s3_backend.themed_urls("logo-%(theme)s.png") 180 181 self.assertIsInstance(result, dict) 182 self.assertIn("light", result) 183 self.assertIn("dark", result) 184 185 # Check URLs are valid presigned URLs with correct file paths 186 self.assertIn("logo-light.png", result["light"]) 187 self.assertIn("logo-dark.png", result["dark"]) 188 self.assertIn("X-Amz-Signature=", result["light"]) 189 self.assertIn("X-Amz-Signature=", result["dark"])
Test themed_urls returns dict of presigned URLs for each theme
191 def test_themed_urls_multiple_theme_variables(self): 192 """Test themed_urls with multiple %(theme)s in path""" 193 result = self.media_s3_backend.themed_urls("%(theme)s/logo-%(theme)s.svg") 194 195 self.assertIsInstance(result, dict) 196 self.assertIn("light/logo-light.svg", result["light"]) 197 self.assertIn("dark/logo-dark.svg", result["dark"])
Test themed_urls with multiple %(theme)s in path
199 def test_save_file_sets_content_type_svg(self): 200 """Test save_file sets correct ContentType for SVG files""" 201 self.media_s3_backend.save_file("test.svg", b"<svg></svg>") 202 203 response = self.media_s3_backend.client.head_object( 204 Bucket=self.media_s3_bucket_name, 205 Key="media/public/test.svg", 206 ) 207 self.assertEqual(response["ContentType"], "image/svg+xml")
Test save_file sets correct ContentType for SVG files
209 def test_save_file_sets_content_type_png(self): 210 """Test save_file sets correct ContentType for PNG files""" 211 self.media_s3_backend.save_file("test.png", b"\x89PNG\r\n\x1a\n") 212 213 response = self.media_s3_backend.client.head_object( 214 Bucket=self.media_s3_bucket_name, 215 Key="media/public/test.png", 216 ) 217 self.assertEqual(response["ContentType"], "image/png")
Test save_file sets correct ContentType for PNG files
219 def test_save_file_stream_sets_content_type(self): 220 """Test save_file_stream sets correct ContentType""" 221 with self.media_s3_backend.save_file_stream("test.css") as f: 222 f.write(b"body { color: red; }") 223 224 response = self.media_s3_backend.client.head_object( 225 Bucket=self.media_s3_bucket_name, 226 Key="media/public/test.css", 227 ) 228 self.assertEqual(response["ContentType"], "text/css")
Test save_file_stream sets correct ContentType
230 def test_save_file_unknown_extension_octet_stream(self): 231 """Test save_file sets octet-stream for unknown extensions""" 232 self.media_s3_backend.save_file("test.unknownext123", b"data") 233 234 response = self.media_s3_backend.client.head_object( 235 Bucket=self.media_s3_bucket_name, 236 Key="media/public/test.unknownext123", 237 ) 238 self.assertEqual(response["ContentType"], "application/octet-stream")
Test save_file sets octet-stream for unknown extensions