Skip to content

Commit 88775a8

Browse files
committed
doc: discovery examples in integration pages
1 parent 7acecad commit 88775a8

File tree

7 files changed

+366
-2
lines changed

7 files changed

+366
-2
lines changed

doc/guides/_examples/django_example.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@
1313
from scim2_models import Error
1414
from scim2_models import ListResponse
1515
from scim2_models import PatchOp
16+
from scim2_models import ResourceType
1617
from scim2_models import ResponseParameters
18+
from scim2_models import Schema
1719
from scim2_models import SearchRequest
1820
from scim2_models import UniquenessException
1921
from scim2_models import User
2022

2123
from .integrations import delete_record
2224
from .integrations import from_scim_user
2325
from .integrations import get_record
26+
from .integrations import get_resource_types
27+
from .integrations import get_schemas
2428
from .integrations import list_records
2529
from .integrations import save_record
30+
from .integrations import service_provider_config
2631
from .integrations import to_scim_user
2732

2833
# -- setup-start --
@@ -209,4 +214,116 @@ def post(self, request):
209214
path("scim/v2/Users/<user:app_record>", UserView.as_view(), name="scim_user"),
210215
]
211216
# -- collection-end --
217+
218+
219+
# -- discovery-start --
220+
# -- schemas-start --
221+
class SchemasView(View):
222+
"""Handle GET on the SCIM schemas collection."""
223+
224+
def get(self, request):
225+
try:
226+
req = SearchRequest.model_validate(request.GET.dict())
227+
except ValidationError as error:
228+
return scim_validation_error(error)
229+
230+
all_schemas = get_schemas()
231+
page = all_schemas[req.start_index_0 : req.stop_index_0]
232+
response = ListResponse[Schema](
233+
total_results=len(all_schemas),
234+
start_index=req.start_index or 1,
235+
items_per_page=len(page),
236+
resources=page,
237+
)
238+
return scim_response(
239+
response.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE)
240+
)
241+
242+
243+
class SchemaView(View):
244+
"""Handle GET on a single SCIM schema."""
245+
246+
def get(self, request, schema_id):
247+
for schema in get_schemas():
248+
if schema.id == schema_id:
249+
return scim_response(
250+
schema.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE)
251+
)
252+
scim_error = Error(status=404, detail=f"Schema {schema_id!r} not found")
253+
return scim_response(scim_error.model_dump_json(), HTTPStatus.NOT_FOUND)
254+
# -- schemas-end --
255+
256+
257+
# -- resource-types-start --
258+
class ResourceTypesView(View):
259+
"""Handle GET on the SCIM resource types collection."""
260+
261+
def get(self, request):
262+
try:
263+
req = SearchRequest.model_validate(request.GET.dict())
264+
except ValidationError as error:
265+
return scim_validation_error(error)
266+
267+
all_resource_types = get_resource_types()
268+
page = all_resource_types[req.start_index_0 : req.stop_index_0]
269+
response = ListResponse[ResourceType](
270+
total_results=len(all_resource_types),
271+
start_index=req.start_index or 1,
272+
items_per_page=len(page),
273+
resources=page,
274+
)
275+
return scim_response(
276+
response.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE)
277+
)
278+
279+
280+
class ResourceTypeView(View):
281+
"""Handle GET on a single SCIM resource type."""
282+
283+
def get(self, request, resource_type_id):
284+
for rt in get_resource_types():
285+
if rt.id == resource_type_id:
286+
return scim_response(
287+
rt.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE)
288+
)
289+
scim_error = Error(
290+
status=404, detail=f"ResourceType {resource_type_id!r} not found"
291+
)
292+
return scim_response(scim_error.model_dump_json(), HTTPStatus.NOT_FOUND)
293+
# -- resource-types-end --
294+
295+
296+
# -- service-provider-config-start --
297+
class ServiceProviderConfigView(View):
298+
"""Handle GET on the SCIM service provider configuration."""
299+
300+
def get(self, request):
301+
return scim_response(
302+
service_provider_config.model_dump_json(
303+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE
304+
)
305+
)
306+
# -- service-provider-config-end --
307+
308+
309+
discovery_urlpatterns = [
310+
path("scim/v2/Schemas", SchemasView.as_view(), name="scim_schemas"),
311+
path("scim/v2/Schemas/<path:schema_id>", SchemaView.as_view(), name="scim_schema"),
312+
path(
313+
"scim/v2/ResourceTypes",
314+
ResourceTypesView.as_view(),
315+
name="scim_resource_types",
316+
),
317+
path(
318+
"scim/v2/ResourceTypes/<resource_type_id>",
319+
ResourceTypeView.as_view(),
320+
name="scim_resource_type",
321+
),
322+
path(
323+
"scim/v2/ServiceProviderConfig",
324+
ServiceProviderConfigView.as_view(),
325+
name="scim_service_provider_config",
326+
),
327+
]
328+
# -- discovery-end --
212329
# -- endpoints-end --

doc/guides/_examples/flask_example.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@
1010
from scim2_models import Error
1111
from scim2_models import ListResponse
1212
from scim2_models import PatchOp
13+
from scim2_models import ResourceType
1314
from scim2_models import ResponseParameters
15+
from scim2_models import Schema
1416
from scim2_models import SearchRequest
1517
from scim2_models import UniquenessException
1618
from scim2_models import User
1719

1820
from .integrations import delete_record
1921
from .integrations import from_scim_user
2022
from .integrations import get_record
23+
from .integrations import get_resource_types
24+
from .integrations import get_schemas
2125
from .integrations import list_records
2226
from .integrations import save_record
27+
from .integrations import service_provider_config
2328
from .integrations import to_scim_user
2429

2530
# -- setup-start --
@@ -198,4 +203,87 @@ def create_user():
198203
)
199204
# -- create-user-end --
200205
# -- collection-end --
206+
207+
208+
# -- discovery-start --
209+
# -- schemas-start --
210+
@bp.get("/Schemas")
211+
def list_schemas():
212+
"""Return one page of SCIM schemas the server exposes."""
213+
req = SearchRequest.model_validate(request.args.to_dict())
214+
all_schemas = get_schemas()
215+
page = all_schemas[req.start_index_0 : req.stop_index_0]
216+
response = ListResponse[Schema](
217+
total_results=len(all_schemas),
218+
start_index=req.start_index or 1,
219+
items_per_page=len(page),
220+
resources=page,
221+
)
222+
return (
223+
response.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE),
224+
HTTPStatus.OK,
225+
)
226+
227+
228+
@bp.get("/Schemas/<path:schema_id>")
229+
def get_schema(schema_id):
230+
"""Return one SCIM schema by its URI identifier."""
231+
for schema in get_schemas():
232+
if schema.id == schema_id:
233+
return (
234+
schema.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE),
235+
HTTPStatus.OK,
236+
)
237+
scim_error = Error(status=404, detail=f"Schema {schema_id!r} not found")
238+
return scim_error.model_dump_json(), HTTPStatus.NOT_FOUND
239+
# -- schemas-end --
240+
241+
242+
# -- resource-types-start --
243+
@bp.get("/ResourceTypes")
244+
def list_resource_types():
245+
"""Return one page of SCIM resource types the server exposes."""
246+
req = SearchRequest.model_validate(request.args.to_dict())
247+
all_resource_types = get_resource_types()
248+
page = all_resource_types[req.start_index_0 : req.stop_index_0]
249+
response = ListResponse[ResourceType](
250+
total_results=len(all_resource_types),
251+
start_index=req.start_index or 1,
252+
items_per_page=len(page),
253+
resources=page,
254+
)
255+
return (
256+
response.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE),
257+
HTTPStatus.OK,
258+
)
259+
260+
261+
@bp.get("/ResourceTypes/<resource_type_id>")
262+
def get_resource_type(resource_type_id):
263+
"""Return one SCIM resource type by its identifier."""
264+
for rt in get_resource_types():
265+
if rt.id == resource_type_id:
266+
return (
267+
rt.model_dump_json(scim_ctx=Context.RESOURCE_QUERY_RESPONSE),
268+
HTTPStatus.OK,
269+
)
270+
scim_error = Error(
271+
status=404, detail=f"ResourceType {resource_type_id!r} not found"
272+
)
273+
return scim_error.model_dump_json(), HTTPStatus.NOT_FOUND
274+
# -- resource-types-end --
275+
276+
277+
# -- service-provider-config-start --
278+
@bp.get("/ServiceProviderConfig")
279+
def get_service_provider_config():
280+
"""Return the SCIM service provider configuration."""
281+
return (
282+
service_provider_config.model_dump_json(
283+
scim_ctx=Context.RESOURCE_QUERY_RESPONSE
284+
),
285+
HTTPStatus.OK,
286+
)
287+
# -- service-provider-config-end --
288+
# -- discovery-end --
201289
# -- endpoints-end --

doc/guides/_examples/integrations.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22

33
from uuid import uuid4
44

5+
from scim2_models import AuthenticationScheme
6+
from scim2_models import Bulk
7+
from scim2_models import ChangePassword
8+
from scim2_models import ETag
9+
from scim2_models import Filter
510
from scim2_models import Meta
11+
from scim2_models import Patch
12+
from scim2_models import ResourceType
13+
from scim2_models import ServiceProviderConfig
14+
from scim2_models import Sort
615
from scim2_models import User
716

817
# -- storage-start --
@@ -58,3 +67,35 @@ def from_scim_user(scim_user):
5867
"email": scim_user.emails[0].value if scim_user.emails else None,
5968
}
6069
# -- mapping-end --
70+
71+
72+
# -- discovery-start --
73+
RESOURCE_MODELS = [User]
74+
75+
76+
def get_schemas():
77+
"""Return a :class:`~scim2_models.Schema` for every resource the server exposes."""
78+
return [model.to_schema() for model in RESOURCE_MODELS]
79+
80+
81+
def get_resource_types():
82+
"""Return a :class:`~scim2_models.ResourceType` for every resource the server exposes."""
83+
return [ResourceType.from_resource(model) for model in RESOURCE_MODELS]
84+
85+
86+
service_provider_config = ServiceProviderConfig(
87+
patch=Patch(supported=True),
88+
bulk=Bulk(supported=False, max_operations=0, max_payload_size=0),
89+
filter=Filter(supported=False, max_results=0),
90+
change_password=ChangePassword(supported=False),
91+
sort=Sort(supported=False),
92+
etag=ETag(supported=False),
93+
authentication_schemes=[
94+
AuthenticationScheme(
95+
type=AuthenticationScheme.Type.httpbasic,
96+
name="HTTP Basic",
97+
description="Authentication via HTTP Basic",
98+
),
99+
],
100+
)
101+
# -- discovery-end --

doc/guides/django.rst

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,57 @@ The ``urlpatterns`` list wires both views to their routes.
144144
:start-after: # -- collection-start --
145145
:end-before: # -- collection-end --
146146

147+
Discovery endpoints
148+
===================
149+
150+
SCIM defines three read-only endpoints that let clients discover the server's capabilities
151+
and the resources it exposes (:rfc:`RFC 7644 §4 <7644#section-4>`).
152+
The shared :ref:`discovery helpers <discovery-helpers>` that build
153+
:class:`~scim2_models.Schema`, :class:`~scim2_models.ResourceType` and
154+
:class:`~scim2_models.ServiceProviderConfig` objects are defined in the :doc:`index` section.
155+
156+
GET /Schemas and GET /Schemas/<id>
157+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
158+
159+
Return all :class:`~scim2_models.Schema` objects or look one up by its URI.
160+
Schemas are built automatically from resource models with
161+
:meth:`~scim2_models.Resource.to_schema`.
162+
The collection endpoint parses pagination parameters with
163+
:class:`~scim2_models.SearchRequest`, following the same pattern as ``GET /Users``.
164+
165+
.. literalinclude:: _examples/django_example.py
166+
:language: python
167+
:start-after: # -- schemas-start --
168+
:end-before: # -- schemas-end --
169+
170+
GET /ResourceTypes and GET /ResourceTypes/<id>
171+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
172+
173+
Return all :class:`~scim2_models.ResourceType` objects or look one up by its identifier.
174+
Resource types are built automatically from resource models with
175+
:meth:`~scim2_models.ResourceType.from_resource`.
176+
The collection endpoint parses pagination parameters with
177+
:class:`~scim2_models.SearchRequest`, following the same pattern as ``GET /Users``.
178+
179+
.. literalinclude:: _examples/django_example.py
180+
:language: python
181+
:start-after: # -- resource-types-start --
182+
:end-before: # -- resource-types-end --
183+
184+
GET /ServiceProviderConfig
185+
^^^^^^^^^^^^^^^^^^^^^^^^^^
186+
187+
Return the :class:`~scim2_models.ServiceProviderConfig` singleton that describes the
188+
features the server supports (patch, bulk, filtering, etc.).
189+
190+
.. literalinclude:: _examples/django_example.py
191+
:language: python
192+
:start-after: # -- service-provider-config-start --
193+
:end-before: # -- service-provider-config-end --
194+
195+
The ``discovery_urlpatterns`` list wires the discovery views to their routes.
196+
Merge it with the resource ``urlpatterns`` in your root URLconf.
197+
147198
Complete example
148199
================
149200

0 commit comments

Comments
 (0)