authentik.providers.scim.tests.test_client

SCIM Client tests

  1"""SCIM Client tests"""
  2
  3from django.core.cache import cache
  4from django.test import TestCase
  5from requests_mock import Mocker
  6
  7from authentik.blueprints.tests import apply_blueprint
  8from authentik.core.models import Application
  9from authentik.lib.generators import generate_id
 10from authentik.providers.scim.clients.base import SCIMClient
 11from authentik.providers.scim.models import SCIMMapping, SCIMProvider
 12from authentik.providers.scim.tasks import scim_sync
 13
 14
 15class SCIMClientTests(TestCase):
 16    """SCIM Client tests"""
 17
 18    @apply_blueprint("system/providers-scim.yaml")
 19    def setUp(self) -> None:
 20        # Clear cache before each test
 21        cache.clear()
 22        self.provider: SCIMProvider = SCIMProvider.objects.create(
 23            name=generate_id(),
 24            url="https://localhost",
 25            token=generate_id(),
 26        )
 27        self.app: Application = Application.objects.create(
 28            name=generate_id(),
 29            slug=generate_id(),
 30        )
 31        self.app.backchannel_providers.add(self.provider)
 32        self.provider.property_mappings.add(
 33            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
 34        )
 35        self.provider.property_mappings_group.add(
 36            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
 37        )
 38
 39    def test_config(self):
 40        """Test valid config:
 41        https://docs.aws.amazon.com/singlesignon/latest/developerguide/serviceproviderconfig.html"""
 42        with Mocker() as mock:
 43            mock: Mocker
 44            mock.get(
 45                "https://localhost/ServiceProviderConfig",
 46                json={
 47                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
 48                    "documentationUri": (
 49                        "https://docs.aws.amazon.com/singlesignon/latest/"
 50                        "userguide/manage-your-identity-source-idp.html"
 51                    ),
 52                    "authenticationSchemes": [
 53                        {
 54                            "type": "oauthbearertoken",
 55                            "name": "OAuth Bearer Token",
 56                            "description": (
 57                                "Authentication scheme using the OAuth Bearer Token Standard"
 58                            ),
 59                            "specUri": "https://www.rfc-editor.org/info/rfc6750",
 60                            "documentationUri": (
 61                                "https://docs.aws.amazon.com/singlesignon/latest/"
 62                                "userguide/provision-automatically.html"
 63                            ),
 64                            "primary": True,
 65                        }
 66                    ],
 67                    "patch": {"supported": True},
 68                    "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
 69                    "filter": {"supported": True, "maxResults": 50},
 70                    "changePassword": {"supported": False},
 71                    "sort": {"supported": False},
 72                    "etag": {"supported": False},
 73                },
 74            )
 75            SCIMClient(self.provider)
 76            self.assertEqual(mock.call_count, 1)
 77            self.assertEqual(mock.request_history[0].method, "GET")
 78
 79    def test_config_invalid(self):
 80        """Test invalid config"""
 81        with Mocker() as mock:
 82            mock: Mocker
 83            mock.get(
 84                "https://localhost/ServiceProviderConfig",
 85                json={},
 86            )
 87            SCIMClient(self.provider)
 88            self.assertEqual(mock.call_count, 1)
 89            self.assertEqual(mock.request_history[0].method, "GET")
 90
 91    def test_scim_sync(self):
 92        """test scim_sync task"""
 93        scim_sync.send(self.provider.pk).get_result()
 94
 95    def test_config_caching(self):
 96        """Test that ServiceProviderConfig is cached after first successful fetch"""
 97        config_json = {
 98            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
 99            "documentationUri": "https://example.com/docs",
100            "authenticationSchemes": [
101                {
102                    "type": "oauthbearertoken",
103                    "name": "OAuth Bearer Token",
104                    "description": "OAuth Bearer Token",
105                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
106                    "documentationUri": "https://example.com/auth",
107                    "primary": True,
108                }
109            ],
110            "patch": {"supported": True},
111            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
112            "filter": {"supported": True, "maxResults": 50},
113            "changePassword": {"supported": False},
114            "sort": {"supported": False},
115            "etag": {"supported": False},
116        }
117
118        with Mocker() as mock:
119            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
120
121            client = SCIMClient(self.provider)
122
123            # First call should hit the API
124            config1 = client.get_service_provider_config()
125            self.assertEqual(mock.call_count, 1)
126
127            # Second call should use cache, no additional API call
128            config2 = client.get_service_provider_config()
129            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
130
131            # Verify both configs are the same
132            self.assertEqual(config1, config2)
133
134    def test_config_caching_invalid(self):
135        """Test that default config is cached when remote config is invalid"""
136        with Mocker() as mock:
137            mock.get("https://localhost/ServiceProviderConfig", json={})
138
139            client = SCIMClient(self.provider)
140
141            # First call should hit the API and get invalid response
142            config1 = client.get_service_provider_config()
143            self.assertEqual(mock.call_count, 1)
144
145            # Second call should use cached default config, no additional API call
146            config2 = client.get_service_provider_config()
147            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
148
149            # Verify both configs are the same default
150            self.assertEqual(config1, config2)
151
152    def test_config_caching_error(self):
153        """Test that default config is cached when remote request fails"""
154        with Mocker() as mock:
155            mock.get("https://localhost/ServiceProviderConfig", status_code=500)
156
157            client = SCIMClient(self.provider)
158
159            # First call should hit the API and fail
160            config1 = client.get_service_provider_config()
161            self.assertEqual(mock.call_count, 1)
162
163            # Second call should use cached default config, no additional API call
164            config2 = client.get_service_provider_config()
165            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
166
167            # Verify both configs are the same default
168            self.assertEqual(config1, config2)
169
170    def test_config_cache_invalidation_on_save(self):
171        """Test that ServiceProviderConfig cache is cleared when provider is saved"""
172        config_json = {
173            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
174            "documentationUri": "https://example.com/docs",
175            "authenticationSchemes": [
176                {
177                    "type": "oauthbearertoken",
178                    "name": "OAuth Bearer Token",
179                    "description": "OAuth Bearer Token",
180                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
181                    "documentationUri": "https://example.com/auth",
182                    "primary": True,
183                }
184            ],
185            "patch": {"supported": True},
186            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
187            "filter": {"supported": True, "maxResults": 50},
188            "changePassword": {"supported": False},
189            "sort": {"supported": False},
190            "etag": {"supported": False},
191        }
192
193        with Mocker() as mock:
194            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
195
196            # First client instantiation caches the config
197            SCIMClient(self.provider)
198            self.assertEqual(mock.call_count, 1)
199
200            # Creating another client should use cached config
201            SCIMClient(self.provider)
202            self.assertEqual(mock.call_count, 1)  # Still 1, using cache
203
204            # Save the provider (e.g., changing compatibility mode)
205            self.provider.compatibility_mode = "aws"
206            self.provider.save()
207
208            # Creating a new client should now hit the API again since cache was cleared
209            SCIMClient(self.provider)
210            self.assertEqual(mock.call_count, 2)  # New API call after cache invalidation
class SCIMClientTests(django.test.testcases.TestCase):
 16class SCIMClientTests(TestCase):
 17    """SCIM Client tests"""
 18
 19    @apply_blueprint("system/providers-scim.yaml")
 20    def setUp(self) -> None:
 21        # Clear cache before each test
 22        cache.clear()
 23        self.provider: SCIMProvider = SCIMProvider.objects.create(
 24            name=generate_id(),
 25            url="https://localhost",
 26            token=generate_id(),
 27        )
 28        self.app: Application = Application.objects.create(
 29            name=generate_id(),
 30            slug=generate_id(),
 31        )
 32        self.app.backchannel_providers.add(self.provider)
 33        self.provider.property_mappings.add(
 34            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
 35        )
 36        self.provider.property_mappings_group.add(
 37            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
 38        )
 39
 40    def test_config(self):
 41        """Test valid config:
 42        https://docs.aws.amazon.com/singlesignon/latest/developerguide/serviceproviderconfig.html"""
 43        with Mocker() as mock:
 44            mock: Mocker
 45            mock.get(
 46                "https://localhost/ServiceProviderConfig",
 47                json={
 48                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
 49                    "documentationUri": (
 50                        "https://docs.aws.amazon.com/singlesignon/latest/"
 51                        "userguide/manage-your-identity-source-idp.html"
 52                    ),
 53                    "authenticationSchemes": [
 54                        {
 55                            "type": "oauthbearertoken",
 56                            "name": "OAuth Bearer Token",
 57                            "description": (
 58                                "Authentication scheme using the OAuth Bearer Token Standard"
 59                            ),
 60                            "specUri": "https://www.rfc-editor.org/info/rfc6750",
 61                            "documentationUri": (
 62                                "https://docs.aws.amazon.com/singlesignon/latest/"
 63                                "userguide/provision-automatically.html"
 64                            ),
 65                            "primary": True,
 66                        }
 67                    ],
 68                    "patch": {"supported": True},
 69                    "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
 70                    "filter": {"supported": True, "maxResults": 50},
 71                    "changePassword": {"supported": False},
 72                    "sort": {"supported": False},
 73                    "etag": {"supported": False},
 74                },
 75            )
 76            SCIMClient(self.provider)
 77            self.assertEqual(mock.call_count, 1)
 78            self.assertEqual(mock.request_history[0].method, "GET")
 79
 80    def test_config_invalid(self):
 81        """Test invalid config"""
 82        with Mocker() as mock:
 83            mock: Mocker
 84            mock.get(
 85                "https://localhost/ServiceProviderConfig",
 86                json={},
 87            )
 88            SCIMClient(self.provider)
 89            self.assertEqual(mock.call_count, 1)
 90            self.assertEqual(mock.request_history[0].method, "GET")
 91
 92    def test_scim_sync(self):
 93        """test scim_sync task"""
 94        scim_sync.send(self.provider.pk).get_result()
 95
 96    def test_config_caching(self):
 97        """Test that ServiceProviderConfig is cached after first successful fetch"""
 98        config_json = {
 99            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
100            "documentationUri": "https://example.com/docs",
101            "authenticationSchemes": [
102                {
103                    "type": "oauthbearertoken",
104                    "name": "OAuth Bearer Token",
105                    "description": "OAuth Bearer Token",
106                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
107                    "documentationUri": "https://example.com/auth",
108                    "primary": True,
109                }
110            ],
111            "patch": {"supported": True},
112            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
113            "filter": {"supported": True, "maxResults": 50},
114            "changePassword": {"supported": False},
115            "sort": {"supported": False},
116            "etag": {"supported": False},
117        }
118
119        with Mocker() as mock:
120            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
121
122            client = SCIMClient(self.provider)
123
124            # First call should hit the API
125            config1 = client.get_service_provider_config()
126            self.assertEqual(mock.call_count, 1)
127
128            # Second call should use cache, no additional API call
129            config2 = client.get_service_provider_config()
130            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
131
132            # Verify both configs are the same
133            self.assertEqual(config1, config2)
134
135    def test_config_caching_invalid(self):
136        """Test that default config is cached when remote config is invalid"""
137        with Mocker() as mock:
138            mock.get("https://localhost/ServiceProviderConfig", json={})
139
140            client = SCIMClient(self.provider)
141
142            # First call should hit the API and get invalid response
143            config1 = client.get_service_provider_config()
144            self.assertEqual(mock.call_count, 1)
145
146            # Second call should use cached default config, no additional API call
147            config2 = client.get_service_provider_config()
148            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
149
150            # Verify both configs are the same default
151            self.assertEqual(config1, config2)
152
153    def test_config_caching_error(self):
154        """Test that default config is cached when remote request fails"""
155        with Mocker() as mock:
156            mock.get("https://localhost/ServiceProviderConfig", status_code=500)
157
158            client = SCIMClient(self.provider)
159
160            # First call should hit the API and fail
161            config1 = client.get_service_provider_config()
162            self.assertEqual(mock.call_count, 1)
163
164            # Second call should use cached default config, no additional API call
165            config2 = client.get_service_provider_config()
166            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
167
168            # Verify both configs are the same default
169            self.assertEqual(config1, config2)
170
171    def test_config_cache_invalidation_on_save(self):
172        """Test that ServiceProviderConfig cache is cleared when provider is saved"""
173        config_json = {
174            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
175            "documentationUri": "https://example.com/docs",
176            "authenticationSchemes": [
177                {
178                    "type": "oauthbearertoken",
179                    "name": "OAuth Bearer Token",
180                    "description": "OAuth Bearer Token",
181                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
182                    "documentationUri": "https://example.com/auth",
183                    "primary": True,
184                }
185            ],
186            "patch": {"supported": True},
187            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
188            "filter": {"supported": True, "maxResults": 50},
189            "changePassword": {"supported": False},
190            "sort": {"supported": False},
191            "etag": {"supported": False},
192        }
193
194        with Mocker() as mock:
195            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
196
197            # First client instantiation caches the config
198            SCIMClient(self.provider)
199            self.assertEqual(mock.call_count, 1)
200
201            # Creating another client should use cached config
202            SCIMClient(self.provider)
203            self.assertEqual(mock.call_count, 1)  # Still 1, using cache
204
205            # Save the provider (e.g., changing compatibility mode)
206            self.provider.compatibility_mode = "aws"
207            self.provider.save()
208
209            # Creating a new client should now hit the API again since cache was cleared
210            SCIMClient(self.provider)
211            self.assertEqual(mock.call_count, 2)  # New API call after cache invalidation

SCIM Client tests

@apply_blueprint('system/providers-scim.yaml')
def setUp(self) -> None:
19    @apply_blueprint("system/providers-scim.yaml")
20    def setUp(self) -> None:
21        # Clear cache before each test
22        cache.clear()
23        self.provider: SCIMProvider = SCIMProvider.objects.create(
24            name=generate_id(),
25            url="https://localhost",
26            token=generate_id(),
27        )
28        self.app: Application = Application.objects.create(
29            name=generate_id(),
30            slug=generate_id(),
31        )
32        self.app.backchannel_providers.add(self.provider)
33        self.provider.property_mappings.add(
34            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
35        )
36        self.provider.property_mappings_group.add(
37            SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
38        )

Hook method for setting up the test fixture before exercising it.

def test_config(self):
40    def test_config(self):
41        """Test valid config:
42        https://docs.aws.amazon.com/singlesignon/latest/developerguide/serviceproviderconfig.html"""
43        with Mocker() as mock:
44            mock: Mocker
45            mock.get(
46                "https://localhost/ServiceProviderConfig",
47                json={
48                    "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
49                    "documentationUri": (
50                        "https://docs.aws.amazon.com/singlesignon/latest/"
51                        "userguide/manage-your-identity-source-idp.html"
52                    ),
53                    "authenticationSchemes": [
54                        {
55                            "type": "oauthbearertoken",
56                            "name": "OAuth Bearer Token",
57                            "description": (
58                                "Authentication scheme using the OAuth Bearer Token Standard"
59                            ),
60                            "specUri": "https://www.rfc-editor.org/info/rfc6750",
61                            "documentationUri": (
62                                "https://docs.aws.amazon.com/singlesignon/latest/"
63                                "userguide/provision-automatically.html"
64                            ),
65                            "primary": True,
66                        }
67                    ],
68                    "patch": {"supported": True},
69                    "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
70                    "filter": {"supported": True, "maxResults": 50},
71                    "changePassword": {"supported": False},
72                    "sort": {"supported": False},
73                    "etag": {"supported": False},
74                },
75            )
76            SCIMClient(self.provider)
77            self.assertEqual(mock.call_count, 1)
78            self.assertEqual(mock.request_history[0].method, "GET")
def test_config_invalid(self):
80    def test_config_invalid(self):
81        """Test invalid config"""
82        with Mocker() as mock:
83            mock: Mocker
84            mock.get(
85                "https://localhost/ServiceProviderConfig",
86                json={},
87            )
88            SCIMClient(self.provider)
89            self.assertEqual(mock.call_count, 1)
90            self.assertEqual(mock.request_history[0].method, "GET")

Test invalid config

def test_scim_sync(self):
92    def test_scim_sync(self):
93        """test scim_sync task"""
94        scim_sync.send(self.provider.pk).get_result()

test scim_sync task

def test_config_caching(self):
 96    def test_config_caching(self):
 97        """Test that ServiceProviderConfig is cached after first successful fetch"""
 98        config_json = {
 99            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
100            "documentationUri": "https://example.com/docs",
101            "authenticationSchemes": [
102                {
103                    "type": "oauthbearertoken",
104                    "name": "OAuth Bearer Token",
105                    "description": "OAuth Bearer Token",
106                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
107                    "documentationUri": "https://example.com/auth",
108                    "primary": True,
109                }
110            ],
111            "patch": {"supported": True},
112            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
113            "filter": {"supported": True, "maxResults": 50},
114            "changePassword": {"supported": False},
115            "sort": {"supported": False},
116            "etag": {"supported": False},
117        }
118
119        with Mocker() as mock:
120            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
121
122            client = SCIMClient(self.provider)
123
124            # First call should hit the API
125            config1 = client.get_service_provider_config()
126            self.assertEqual(mock.call_count, 1)
127
128            # Second call should use cache, no additional API call
129            config2 = client.get_service_provider_config()
130            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
131
132            # Verify both configs are the same
133            self.assertEqual(config1, config2)

Test that ServiceProviderConfig is cached after first successful fetch

def test_config_caching_invalid(self):
135    def test_config_caching_invalid(self):
136        """Test that default config is cached when remote config is invalid"""
137        with Mocker() as mock:
138            mock.get("https://localhost/ServiceProviderConfig", json={})
139
140            client = SCIMClient(self.provider)
141
142            # First call should hit the API and get invalid response
143            config1 = client.get_service_provider_config()
144            self.assertEqual(mock.call_count, 1)
145
146            # Second call should use cached default config, no additional API call
147            config2 = client.get_service_provider_config()
148            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
149
150            # Verify both configs are the same default
151            self.assertEqual(config1, config2)

Test that default config is cached when remote config is invalid

def test_config_caching_error(self):
153    def test_config_caching_error(self):
154        """Test that default config is cached when remote request fails"""
155        with Mocker() as mock:
156            mock.get("https://localhost/ServiceProviderConfig", status_code=500)
157
158            client = SCIMClient(self.provider)
159
160            # First call should hit the API and fail
161            config1 = client.get_service_provider_config()
162            self.assertEqual(mock.call_count, 1)
163
164            # Second call should use cached default config, no additional API call
165            config2 = client.get_service_provider_config()
166            self.assertEqual(mock.call_count, 1)  # Still 1, not 2
167
168            # Verify both configs are the same default
169            self.assertEqual(config1, config2)

Test that default config is cached when remote request fails

def test_config_cache_invalidation_on_save(self):
171    def test_config_cache_invalidation_on_save(self):
172        """Test that ServiceProviderConfig cache is cleared when provider is saved"""
173        config_json = {
174            "schemas": ["urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"],
175            "documentationUri": "https://example.com/docs",
176            "authenticationSchemes": [
177                {
178                    "type": "oauthbearertoken",
179                    "name": "OAuth Bearer Token",
180                    "description": "OAuth Bearer Token",
181                    "specUri": "https://www.rfc-editor.org/info/rfc6750",
182                    "documentationUri": "https://example.com/auth",
183                    "primary": True,
184                }
185            ],
186            "patch": {"supported": True},
187            "bulk": {"supported": False, "maxOperations": 1, "maxPayloadSize": 1048576},
188            "filter": {"supported": True, "maxResults": 50},
189            "changePassword": {"supported": False},
190            "sort": {"supported": False},
191            "etag": {"supported": False},
192        }
193
194        with Mocker() as mock:
195            mock.get("https://localhost/ServiceProviderConfig", json=config_json)
196
197            # First client instantiation caches the config
198            SCIMClient(self.provider)
199            self.assertEqual(mock.call_count, 1)
200
201            # Creating another client should use cached config
202            SCIMClient(self.provider)
203            self.assertEqual(mock.call_count, 1)  # Still 1, using cache
204
205            # Save the provider (e.g., changing compatibility mode)
206            self.provider.compatibility_mode = "aws"
207            self.provider.save()
208
209            # Creating a new client should now hit the API again since cache was cleared
210            SCIMClient(self.provider)
211            self.assertEqual(mock.call_count, 2)  # New API call after cache invalidation

Test that ServiceProviderConfig cache is cleared when provider is saved