scim2-server 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: scim2-server
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Lightweight SCIM2 server prototype
5
5
  Keywords: scim,scim2,provisioning,rfc7643,rfc7644
6
6
  Author: Yaal Coop, Christian Friedrich Coors
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Lightweight SCIM2 server prototype"
9
9
  readme = "README.md"
10
10
  keywords = [
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "scim2-server"
7
- version = "0.3.2"
7
+ version = "0.3.3"
8
8
  description = "Lightweight SCIM2 server prototype"
9
9
  authors = [
10
10
  {name="Yaal Coop", email="contact@yaal.coop"},
@@ -36,6 +36,7 @@ from werkzeug import Response
36
36
  from werkzeug.datastructures import ETags
37
37
  from werkzeug.exceptions import Forbidden
38
38
  from werkzeug.exceptions import HTTPException
39
+ from werkzeug.exceptions import MethodNotAllowed
39
40
  from werkzeug.exceptions import NotFound
40
41
  from werkzeug.exceptions import NotImplemented as WerkzeugNotImplemented
41
42
  from werkzeug.exceptions import PreconditionFailed
@@ -43,6 +44,7 @@ from werkzeug.exceptions import RequestEntityTooLarge
43
44
  from werkzeug.exceptions import Unauthorized
44
45
  from werkzeug.http import parse_etags
45
46
  from werkzeug.http import unquote_etag
47
+ from werkzeug.routing import BaseConverter
46
48
  from werkzeug.routing import Map
47
49
  from werkzeug.routing import Rule
48
50
  from werkzeug.routing.exceptions import RequestRedirect
@@ -79,6 +81,20 @@ BULK_SUCCESS_STATUS = {
79
81
  }
80
82
 
81
83
 
84
+ class ResourceEndpointConverter(BaseConverter):
85
+ """Match a resource endpoint, but not the endpoints RFC 7644 reserves nor the version prefix.
86
+
87
+ A request with a method a reserved endpoint does not support then gets a
88
+ 405 answer, instead of being routed to a resource type of that name.
89
+ """
90
+
91
+ # A reserved name followed by the end of the path segment is refused.
92
+ regex = (
93
+ r"(?!(?:ServiceProviderConfig|ResourceTypes|Schemas|Bulk|Me|v2)(?![^/]))[^/]+"
94
+ )
95
+ part_isolating = True
96
+
97
+
82
98
  class SCIMApplication:
83
99
  """A WSGI application implementing a SCIM provider (server)."""
84
100
 
@@ -123,17 +139,17 @@ class SCIMApplication:
123
139
  methods=("GET", "POST", "PUT", "PATCH", "DELETE"),
124
140
  ),
125
141
  Rule(
126
- f"{prefix}/<string:resource_endpoint>",
142
+ f"{prefix}/<resource_endpoint:resource_endpoint>",
127
143
  endpoint="resource",
128
144
  methods=("GET", "POST"),
129
145
  ),
130
146
  Rule(
131
- f"{prefix}/<string:resource_endpoint>/.search",
147
+ f"{prefix}/<resource_endpoint:resource_endpoint>/.search",
132
148
  endpoint="resource_search",
133
149
  methods=("POST",),
134
150
  ),
135
151
  Rule(
136
- f"{prefix}/<string:resource_endpoint>/<string:resource_id>",
152
+ f"{prefix}/<resource_endpoint:resource_endpoint>/<string:resource_id>",
137
153
  endpoint="single_resource",
138
154
  methods=("GET", "PUT", "PATCH", "DELETE"),
139
155
  ),
@@ -148,7 +164,9 @@ class SCIMApplication:
148
164
  for prefix in ("", "/v2")
149
165
  )
150
166
 
151
- self.url_map = Map(rules)
167
+ self.url_map = Map(
168
+ rules, converters={"resource_endpoint": ResourceEndpointConverter}
169
+ )
152
170
 
153
171
  def get_model(self, resource_type: ResourceType) -> type[Resource[Any]]:
154
172
  """Return the model of a resource type, its extensions included."""
@@ -811,7 +829,11 @@ class SCIMApplication:
811
829
  self.log.exception(e)
812
830
  return e.get_response(environ)
813
831
  except Exception as e:
814
- return self.make_error(self.error_from(e))
832
+ response = self.make_error(self.error_from(e))
833
+ if isinstance(e, MethodNotAllowed) and e.valid_methods:
834
+ # RFC 9110 §15.5.6: a 405 answer lists the supported methods.
835
+ response.headers["Allow"] = ", ".join(sorted(e.valid_methods))
836
+ return response
815
837
 
816
838
  def __call__(
817
839
  self, environ: "WSGIEnvironment", start_response: "StartResponse"
File without changes
File without changes