import uuid
from scim2_client import SCIMClientError
from scim2_models import Error
from scim2_models import Schema
from ..utils import CheckContext
from ..utils import CheckResult
from ..utils import Status
from ..utils import check_result
from ..utils import checker
from ._discovery_utils import _test_discovery_endpoint_methods
def _schemas_endpoint(context: CheckContext) -> list[CheckResult]:
"""Orchestrate validation of the Schemas discovery endpoint.
Runs comprehensive tests on the ``/Schemas`` endpoint including listing
all schemas, querying individual schemas by ID, and error handling for
invalid schema requests.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: All sub-checks completed successfully
- :attr:`~scim2_tester.Status.ERROR`: One or more sub-checks failed
.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`
"An HTTP GET to this endpoint is used to retrieve information about
resource schemas supported by a SCIM service provider."
"Service providers MUST provide this endpoint."
"""
schemas_result_list = query_all_schemas(context)
results = []
results.extend(schemas_result_list)
schemas_result = schemas_result_list[0] # Get the first (and only) result
if schemas_result.status == Status.SUCCESS:
results.extend(access_schema_by_id(context))
results.extend(access_invalid_schema(context))
return results
[docs]
@checker("*", "discovery", "schemas")
def query_all_schemas(context: CheckContext) -> list[CheckResult]:
"""Validate retrieval of all available schemas.
Tests that the ``/Schemas`` endpoint returns a complete list of all supported
schemas including core schemas, extensions, and custom schemas.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: Endpoint returns valid list of :class:`~scim2_models.Schema` objects
- :attr:`~scim2_tester.Status.ERROR`: Endpoint is inaccessible or returns invalid response
.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`
"An HTTP GET to this endpoint is used to retrieve information about
resource schemas supported by a SCIM service provider."
"""
response = context.client.query(
Schema, expected_status_codes=context.conf.expected_status_codes or [200]
)
available = ", ".join([f"'{resource.name}'" for resource in response.resources])
return [
check_result(
context,
status=Status.SUCCESS,
reason=f"Schemas available are: {available}",
data=response.resources,
)
]
[docs]
@checker("discovery", "schemas")
def access_invalid_schema(context: CheckContext) -> list[CheckResult]:
"""Validate error handling for non-existent schema IDs.
Tests that accessing ``/Schemas/{invalid_id}`` with a non-existent schema
ID returns appropriate :class:`~scim2_models.Error` object with 404 status.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: Server returns :class:`~scim2_models.Error` object with 404 status
- :attr:`~scim2_tester.Status.ERROR`: Server returns non-:class:`~scim2_models.Error` object or incorrect status
.. pull-quote:: :rfc:`RFC 7644 Section 3.12 - Error Response Handling <7644#section-3.12>`
"When returning HTTP error status codes, the server SHOULD return
a SCIM error response."
"""
probably_invalid_id = str(uuid.uuid4())
response = context.client.query(
Schema,
probably_invalid_id,
expected_status_codes=context.conf.expected_status_codes or [404],
raise_scim_errors=False,
)
if not isinstance(response, Error):
return [
check_result(
context,
status=Status.ERROR,
reason=f"/Schemas/{probably_invalid_id} invalid URL did not return an Error object",
data=response,
)
]
if response.status != 404:
return [
check_result(
context,
status=Status.ERROR,
reason=f"/Schemas/{probably_invalid_id} invalid URL did return an Error object, but the status code is {response.status}",
data=response,
)
]
return [
check_result(
context,
status=Status.SUCCESS,
reason=f"/Schemas/{probably_invalid_id} invalid URL correctly returned a 404 error",
data=response,
)
]
[docs]
@checker("discovery", "schemas")
def schemas_endpoint_methods(
context: CheckContext,
) -> list[CheckResult]:
"""Validate that unsupported HTTP methods return 405 Method Not Allowed.
Tests that POST, PUT, PATCH, and DELETE methods on the ``/Schemas``
endpoint correctly return HTTP 405 Method Not Allowed status, as only GET is supported.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: All unsupported methods return 405 status
- :attr:`~scim2_tester.Status.ERROR`: One or more methods return unexpected status
.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`
"An HTTP GET to this endpoint is used to retrieve information about
resource schemas supported by a SCIM service provider."
Only GET method is specified, other methods should return appropriate errors.
"""
return _test_discovery_endpoint_methods(context, "/Schemas")
[docs]
@checker("discovery", "schemas")
def access_schema_by_id(
context: CheckContext,
) -> list[CheckResult]:
"""Validate individual schema retrieval by ID.
Tests that all schemas can be retrieved using GET requests to
``/Schemas/{id}`` with their complete attribute definitions and metadata.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: All schemas retrieved successfully with valid data
- :attr:`~scim2_tester.Status.ERROR`: One or more schemas failed to retrieve
.. pull-quote:: :rfc:`RFC 7644 Section 7 - Schema Definition <7644#section-7>`
"Each schema specifies the name of the resource, the resource's base URI,
and any attributes (including sub-attributes) of the resource."
"""
schemas_response = context.client.query(
Schema, expected_status_codes=context.conf.expected_status_codes or [200]
)
results = []
for schema in schemas_response.resources:
if schema.id:
try:
response = context.client.query(
Schema,
schema.id,
expected_status_codes=context.conf.expected_status_codes or [200],
)
results.append(
check_result(
context,
status=Status.SUCCESS,
reason=f"Successfully accessed schema: {schema.id}",
data=response,
)
)
except SCIMClientError as e:
results.append(
check_result(
context,
status=Status.ERROR,
reason=f"Failed to access schema {schema.id}: {str(e)}",
)
)
if not results:
results.append(
check_result(
context,
status=Status.SUCCESS,
reason="No schemas with IDs found to test",
)
)
return results
[docs]
@checker("discovery", "schemas")
def core_schemas_validation(
context: CheckContext,
) -> list[CheckResult]:
"""Validate that mandatory core schemas are provided.
Tests that the ``/Schemas`` endpoint provides the three mandatory core schemas:
ResourceType, ServiceProviderConfig, and Schema schemas themselves.
**Status:**
- :attr:`~scim2_tester.Status.SUCCESS`: All mandatory core schemas are present
- :attr:`~scim2_tester.Status.ERROR`: One or more mandatory schemas are missing
.. pull-quote:: :rfc:`RFC 7644 Section 4 - Discovery <7644#section-4>`
"Service providers MUST provide this endpoint."
The core schemas for ResourceType, ServiceProviderConfig, and Schema
objects are fundamental to SCIM operation and should always be available.
"""
response = context.client.query(
Schema, expected_status_codes=context.conf.expected_status_codes or [200]
)
required_schemas = {
"ResourceType": False,
"ServiceProviderConfig": False,
"Schema": False,
}
for schema in response.resources:
schema_name = getattr(schema, "name", "")
if schema_name in required_schemas:
required_schemas[schema_name] = True
missing_schemas = [name for name, found in required_schemas.items() if not found]
if missing_schemas:
return [
check_result(
context,
status=Status.ERROR,
reason=f"Missing mandatory core schemas: {', '.join(missing_schemas)}",
data=response,
)
]
return [
check_result(
context,
status=Status.SUCCESS,
reason="All mandatory core schemas (ResourceType, ServiceProviderConfig, Schema) are present",
data=response,
)
]