Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First pass on scim /Bulk API #1985

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,6 @@ storybook-static/

/**/.yarn/cache
.swc

# local ssl certs
certs/
3 changes: 1 addition & 2 deletions main/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from django.conf import settings
from django.conf.urls.static import static
from django.contrib import admin
from django.urls import include, path, re_path
from django.urls import include, re_path
from django.views.generic.base import RedirectView
from rest_framework.routers import DefaultRouter

Expand All @@ -41,7 +41,6 @@

urlpatterns = (
[ # noqa: RUF005
path("scim/v2/", include("django_scim.urls")),
re_path(r"^o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
re_path(r"^admin/", admin.site.urls),
re_path(r"", include("authentication.urls")),
Expand Down
Empty file added scim/__init__.py
Empty file.
Empty file added scim/apps.py
Empty file.
7 changes: 7 additions & 0 deletions scim/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""SCIM constants"""


class SchemaURI:
BULK_REQUEST = "urn:ietf:params:scim:api:messages:2.0:BulkRequest"

BULK_RESPONSE = "urn:ietf:params:scim:api:messages:2.0:BulkResponse"
10 changes: 10 additions & 0 deletions scim/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""URL configurations for profiles"""

from django.urls import include, re_path

from scim.views import BulkView

urlpatterns = (
re_path("^scim/v2/Bulk$", BulkView.as_view(), name="bulk"),
re_path("^scim/v2/", include("django_scim.urls")),
)
138 changes: 138 additions & 0 deletions scim/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
"""SCIM view customizations"""

import copy
import json

from django.http import HttpResponse
from django.urls.resolvers import get_resolver
from django_scim import constants as djs_constants
from django_scim import exceptions
from django_scim import views as djs_views

from scim import constants


class InMemoryHttpRequest(HttpRequest):
"""
A spoofed HttpRequest that only exists in-memory.
It does not implement all features of HttpRequest and is only used
for the bulk SCIM operations here so we can reuse view implementations.
"""

def __init__(self, request, path, method, body):
super().__init__()

self.META = copy.deepcopy(request.META)
self.path = path
self.method = method
self.content_type = djs_constants.SCIM_CONTENT_TYPE

# normally HttpRequest would read this in, but we already have the value
self._body = body


class BulkView(djs_views.SCIMView):
http_method_names = ["post"]

def post(self, request, *args, **kwargs):
body = self.load_body(request.body)

if body.get("schemas") != [constants.SchemaURI.BULK_REQUEST]:
raise exceptions.BadRequestError(
"Invalid schema uri. Must be SearchRequest."
)

fail_on_errors = body.get("failOnErrors", None)

if fail_on_errors is not None and isinstance(int, fail_on_errors):
raise exceptions.BaseRequestError(
"Invalid failOnErrors. Must be an integer."
)

operations = body.get("Operations")

results = self._attempt_operations(request, operations, fail_on_errors)

response = {
"schemas": [constants.SchemaURI.BATCH_RESPONSE],
"Operations": results,
}

content = json.dumps(response)

return HttpResponse(
content=content, content_type=djs_constants.SCIM_CONTENT_TYPE, status=200
)

def _attempt_operations(self, request, operations, fail_on_errors):
"""Attempt to run the operations that were passed"""
responses = []
num_errors = 0

for operation in operations:
# per-spec,if we've hit the error threshold stop processing and return
if fail_on_errors is not None and num_errors >= fail_on_errors:
break

op_response = self._attempt_operation(request, operation)

# if the operation returned a non-2xx status code, record it as a failure
if int(op_response.get("status")) >= 300:
num_errors += 1

responses.append(op_response)

return responses

def _attempt_operation(self, bulk_request, operation):
"""Attempt an operation as part of a bulk request"""

method = operation.get("method")
bulk_id = operation.get("bulkId")
path = operation.get("path")
data = operation.get("data")

if path.startswith("/Users/"):
return self._attempt_user_operation(
bulk_request, method, path, bulk_id, data
)
elif path.startswith("/Groups/"):
return self._attempt_group_operation(
bulk_request, method, path, bulk_id, data
)
else:
return self._operation_error(
bulk_id, 501, "Endpoint is not supported for /Bulk"
)

def _operation_error(self, method, bulk_id, status_code, detail):
"""Return a failure response"""
status_code = str(status_code)
return {
"method": method,
"status": status_code,
"bulkId": bulk_id,
"response": {
"schemas": [djs_constants.SchemaURI.ERROR],
"status": status_code,
"detail": detail,
},
}

def _attempt_user_operation(self, bulk_request, method, path, bulk_id, data):
op_request = InMemoryHttpRequest(bulk_request, path, method, data)
# resolve the operation's path against the django_scim urls
url_match = get_resolver("django_scim.urls").resolve(path)
response = djs_views.UserView.dispatch(
op_request, *url_match.args, **url_match.kwargs
)

return {
**response,
"bulkId": bulk_id,
}

def _attempt_group_operation(self, bulk_request, method, path, bulk_id, data):
return self._operation_error(
method, bulk_id, 501, "Group operations not implemented for /Bulk"
)
Loading