scim2-cli 0.1.1__tar.gz → 0.1.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.1
2
2
  Name: scim2-cli
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: SCIM application development CLI
5
5
  License: MIT
6
6
  Keywords: scim,scim2,provisioning
@@ -20,13 +20,21 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Programming Language :: Python :: 3.9
21
21
  Classifier: Programming Language :: Python :: Implementation :: CPython
22
22
  Requires-Dist: click (>=8.1.7,<9.0.0)
23
- Requires-Dist: scim2-client (>=0.1.4,<0.2.0)
23
+ Requires-Dist: scim2-client (>=0.1.6,<0.2.0)
24
+ Requires-Dist: scim2-tester (>=0.1.3,<0.2.0)
24
25
  Requires-Dist: sphinx-click-rst-to-ansi-formatter (>=0.1.0,<0.2.0)
25
26
  Description-Content-Type: text/markdown
26
27
 
27
28
  # scim2-cli
28
29
 
29
- An utility command line to help you perform requests against a SCIM server, while validating input and response payloads.
30
+ An utility command line to help you perform requests against a SCIM server, while validating input and response payloads. It also uses [scim2-tester](https://scim2-tester.readthedocs.io) to perform a [SCIM server compliance test](https://scim2-cli.readthedocs.io/en/latest/reference.html#scim-url-test).
31
+
32
+ ## What's SCIM anyway?
33
+
34
+ SCIM stands for System for Cross-domain Identity Management, and it is a provisioning protocol.
35
+ Provisioning is the action of managing a set of resources across different services, usually users and groups.
36
+ SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
37
+ It allows users and groups creations, modifications and deletions to be synchronized between applications.
30
38
 
31
39
  ## Installation
32
40
 
@@ -41,7 +49,7 @@ Check the [reference](https://scim2-cli.readthedocs.io/en/latest/reference.html)
41
49
  Here is an example of resource creation:
42
50
 
43
51
  ```shell
44
- $ scim2 https://auth.example create user << EOL
52
+ $ scim2 https://auth.example --headers "Authorization: Bearer 12345" create user << EOL
45
53
  {
46
54
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
47
55
  "userName": "bjensen@example.com"
@@ -64,7 +72,7 @@ EOL
64
72
  Here is an example of resource query:
65
73
 
66
74
  ```shell
67
- $ scim2 https://auth.example query user 2819c223-7f76-453a-919d-413861904646
75
+ $ scim2 https://auth.example --header "Authorization: Bearer 12345" query user 2819c223-7f76-453a-919d-413861904646
68
76
  {
69
77
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
70
78
  "id": "2819c223-7f76-453a-919d-413861904646",
@@ -79,3 +87,8 @@ $ scim2 https://auth.example query user 2819c223-7f76-453a-919d-413861904646
79
87
  }
80
88
  ```
81
89
 
90
+ scim2-cli belongs in a collection of SCIM tools developed by [Yaal Coop](https://yaal.coop),
91
+ with [scim2-models](https://github.com/yaal-coop/scim2-models),
92
+ [scim2-client](https://github.com/yaal-coop/scim2-client) and
93
+ [scim2-tester](https://github.com/yaal-coop/scim2-tester)
94
+
@@ -1,6 +1,13 @@
1
1
  # scim2-cli
2
2
 
3
- An utility command line to help you perform requests against a SCIM server, while validating input and response payloads.
3
+ An utility command line to help you perform requests against a SCIM server, while validating input and response payloads. It also uses [scim2-tester](https://scim2-tester.readthedocs.io) to perform a [SCIM server compliance test](https://scim2-cli.readthedocs.io/en/latest/reference.html#scim-url-test).
4
+
5
+ ## What's SCIM anyway?
6
+
7
+ SCIM stands for System for Cross-domain Identity Management, and it is a provisioning protocol.
8
+ Provisioning is the action of managing a set of resources across different services, usually users and groups.
9
+ SCIM is often used between Identity Providers and applications in completion of standards like OAuth2 and OpenID Connect.
10
+ It allows users and groups creations, modifications and deletions to be synchronized between applications.
4
11
 
5
12
  ## Installation
6
13
 
@@ -15,7 +22,7 @@ Check the [reference](https://scim2-cli.readthedocs.io/en/latest/reference.html)
15
22
  Here is an example of resource creation:
16
23
 
17
24
  ```shell
18
- $ scim2 https://auth.example create user << EOL
25
+ $ scim2 https://auth.example --headers "Authorization: Bearer 12345" create user << EOL
19
26
  {
20
27
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
21
28
  "userName": "bjensen@example.com"
@@ -38,7 +45,7 @@ EOL
38
45
  Here is an example of resource query:
39
46
 
40
47
  ```shell
41
- $ scim2 https://auth.example query user 2819c223-7f76-453a-919d-413861904646
48
+ $ scim2 https://auth.example --header "Authorization: Bearer 12345" query user 2819c223-7f76-453a-919d-413861904646
42
49
  {
43
50
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
44
51
  "id": "2819c223-7f76-453a-919d-413861904646",
@@ -52,3 +59,8 @@ $ scim2 https://auth.example query user 2819c223-7f76-453a-919d-413861904646
52
59
  }
53
60
  }
54
61
  ```
62
+
63
+ scim2-cli belongs in a collection of SCIM tools developed by [Yaal Coop](https://yaal.coop),
64
+ with [scim2-models](https://github.com/yaal-coop/scim2-models),
65
+ [scim2-client](https://github.com/yaal-coop/scim2-client) and
66
+ [scim2-tester](https://github.com/yaal-coop/scim2-tester)
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "scim2-cli"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "SCIM application development CLI"
9
9
  authors = ["Yaal Coop <contact@yaal.coop>"]
10
10
  license = "MIT"
@@ -27,7 +27,8 @@ classifiers = [
27
27
  [tool.poetry.dependencies]
28
28
  python = "^3.10"
29
29
  click = "^8.1.7"
30
- scim2-client = "^0.1.4"
30
+ scim2-client = "^0.1.6"
31
+ scim2-tester = "^0.1.3"
31
32
  sphinx-click-rst-to-ansi-formatter = "^0.1.0"
32
33
 
33
34
  [tool.poetry.group.doc]
@@ -3,7 +3,6 @@ import json
3
3
  import click
4
4
  from httpx import Client
5
5
  from scim2_client import SCIMClient
6
- from scim2_models import EnterpriseUser
7
6
  from scim2_models import Group
8
7
  from scim2_models import User
9
8
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
@@ -13,6 +12,7 @@ from .delete import delete_cli
13
12
  from .query import query_cli
14
13
  from .replace import replace_cli
15
14
  from .search import search_cli
15
+ from .test import test_cli
16
16
  from .utils import DOC_URL
17
17
 
18
18
 
@@ -25,9 +25,7 @@ def cli(ctx, url):
25
25
  ctx.ensure_object(dict)
26
26
  ctx.obj["URL"] = url
27
27
  client = Client(base_url=ctx.obj["URL"])
28
- ctx.obj["client"] = SCIMClient(
29
- client, resource_types=(User, User[EnterpriseUser], Group)
30
- )
28
+ ctx.obj["client"] = SCIMClient(client, resource_types=(User, Group))
31
29
  ctx.obj["resource_types"] = {
32
30
  resource_type.__name__.lower(): resource_type
33
31
  for resource_type in ctx.obj["client"].resource_types
@@ -47,3 +45,4 @@ cli.add_command(query_cli)
47
45
  cli.add_command(replace_cli)
48
46
  cli.add_command(delete_cli)
49
47
  cli.add_command(search_cli)
48
+ cli.add_command(test_cli)
@@ -1,12 +1,13 @@
1
+ import sys
2
+
1
3
  import click
2
- import httpx
3
4
  from click import ClickException
4
- from pydantic import ValidationError
5
5
  from scim2_client import SCIMClientError
6
6
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
7
 
8
8
  from .utils import DOC_URL
9
9
  from .utils import formatted_payload
10
+ from .utils import split_headers
10
11
 
11
12
 
12
13
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="create")
@@ -17,7 +18,10 @@ from .utils import formatted_payload
17
18
  default=True,
18
19
  help="Indent JSON response payloads.",
19
20
  )
20
- def create_cli(ctx, indent):
21
+ @click.option(
22
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
23
+ )
24
+ def create_cli(ctx, indent, headers):
21
25
  """Perform a `SCIM POST <https://www.rfc-editor.org/rfc/rfc7644#section-3.3>`_ request
22
26
  on resources endpoint.
23
27
 
@@ -34,15 +38,16 @@ def create_cli(ctx, indent):
34
38
  raise ClickException("Input data is missing")
35
39
 
36
40
  try:
37
- response = ctx.obj["client"].create(payload)
38
-
39
- except (httpx.HTTPError, SCIMClientError) as exc:
40
- raise ClickException(exc) from exc
41
-
42
- except ValidationError as exc:
43
- payload = formatted_payload(exc.response_payload, indent)
44
- message = f"The server response is invalid:\n{payload}\n{exc}"
45
- raise ClickException(message) from exc
41
+ response = ctx.obj["client"].create(payload, headers=split_headers(headers))
42
+
43
+ except SCIMClientError as scim_exc:
44
+ message = str(scim_exc)
45
+ if sys.version_info >= (3, 11) and hasattr(
46
+ scim_exc, "__notes__"
47
+ ): # pragma: no cover
48
+ for note in scim_exc.__notes__:
49
+ message = f"{message}\n{note}"
50
+ raise ClickException(message) from scim_exc
46
51
 
47
52
  payload = formatted_payload(response.model_dump(), indent)
48
53
  click.echo(payload)
@@ -1,16 +1,23 @@
1
+ import sys
2
+
1
3
  import click
2
- import httpx
3
4
  from click import ClickException
4
5
  from scim2_client import SCIMClientError
6
+ from scim2_models import Message
7
+ from scim2_models import Resource
5
8
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
6
9
 
7
10
  from .utils import DOC_URL
8
11
  from .utils import formatted_payload
12
+ from .utils import split_headers
9
13
 
10
14
 
11
15
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="delete")
12
16
  @click.argument("resource-type", required=True)
13
17
  @click.argument("id", required=True)
18
+ @click.option(
19
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
20
+ )
14
21
  @click.option(
15
22
  "--indent/--no-indent",
16
23
  is_flag=True,
@@ -18,7 +25,7 @@ from .utils import formatted_payload
18
25
  help="Indent JSON response payloads.",
19
26
  )
20
27
  @click.pass_context
21
- def delete_cli(ctx, resource_type, id, indent):
28
+ def delete_cli(ctx, resource_type, id, headers, indent):
22
29
  """Perform a `SCIM DELETE query <https://www.rfc-editor.org/rfc/rfc7644#section-3.6>`_ request.
23
30
 
24
31
  .. code-block:: bash
@@ -35,11 +42,24 @@ def delete_cli(ctx, resource_type, id, indent):
35
42
  )
36
43
 
37
44
  try:
38
- response = ctx.obj["client"].delete(resource_type, id)
45
+ response = ctx.obj["client"].delete(
46
+ resource_type, id, headers=split_headers(headers)
47
+ )
39
48
 
40
- except (httpx.HTTPError, SCIMClientError) as exc:
41
- raise ClickException(exc) from exc
49
+ except SCIMClientError as scim_exc:
50
+ message = str(scim_exc)
51
+ if sys.version_info >= (3, 11) and hasattr(
52
+ scim_exc, "__notes__"
53
+ ): # pragma: no cover
54
+ for note in scim_exc.__notes__:
55
+ message = f"{message}\n{note}"
56
+ raise ClickException(message) from scim_exc
42
57
 
43
58
  if response:
44
- payload = formatted_payload(response.model_dump(), indent)
59
+ payload = (
60
+ response.model_dump()
61
+ if isinstance(response, (Resource, Message))
62
+ else response
63
+ )
64
+ payload = formatted_payload(payload, indent)
45
65
  click.echo(payload)
@@ -1,16 +1,16 @@
1
+ import sys
1
2
  from typing import List
2
3
  from typing import Optional
3
4
 
4
5
  import click
5
- import httpx
6
6
  from click import ClickException
7
- from pydantic import ValidationError
8
7
  from scim2_client import SCIMClientError
9
8
  from scim2_models import SearchRequest
10
9
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
11
10
 
12
11
  from .utils import DOC_URL
13
12
  from .utils import formatted_payload
13
+ from .utils import split_headers
14
14
 
15
15
 
16
16
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="query")
@@ -48,6 +48,9 @@ from .utils import formatted_payload
48
48
  "--sort-order",
49
49
  help="A string indicating the order in which the “sortBy” parameter is applied.",
50
50
  )
51
+ @click.option(
52
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
53
+ )
51
54
  @click.option(
52
55
  "--indent/--no-indent",
53
56
  is_flag=True,
@@ -65,6 +68,7 @@ def query_cli(
65
68
  filter: str,
66
69
  sort_by: str,
67
70
  sort_order: str,
71
+ headers: List[str],
68
72
  indent: bool,
69
73
  ):
70
74
  """Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request
@@ -107,26 +111,22 @@ def query_cli(
107
111
  )
108
112
 
109
113
  try:
110
- if resource_type:
111
- response = ctx.obj["client"].query(
112
- resource_type,
113
- id,
114
- search_request=payload,
115
- check_request_payload=check_request_payload,
116
- )
117
-
118
- else:
119
- response = ctx.obj["client"].query_all(
120
- search_request=payload, check_request_payload=check_request_payload
121
- )
122
-
123
- except (httpx.HTTPError, SCIMClientError) as exc:
124
- raise ClickException(exc) from exc
114
+ response = ctx.obj["client"].query(
115
+ resource_type,
116
+ id,
117
+ search_request=payload,
118
+ check_request_payload=check_request_payload,
119
+ headers=split_headers(headers),
120
+ )
125
121
 
126
- except ValidationError as exc:
127
- payload = formatted_payload(exc.response_payload, indent)
128
- message = f"The server response is invalid:\n{payload}\n{exc}"
129
- raise ClickException(message) from exc
122
+ except SCIMClientError as scim_exc:
123
+ message = str(scim_exc)
124
+ if sys.version_info >= (3, 11) and hasattr(
125
+ scim_exc, "__notes__"
126
+ ): # pragma: no cover
127
+ for note in scim_exc.__notes__:
128
+ message = f"{message}\n{note}"
129
+ raise ClickException(message) from scim_exc
130
130
 
131
131
  payload = formatted_payload(response.model_dump(), indent)
132
132
  click.echo(payload)
@@ -1,23 +1,27 @@
1
+ import sys
2
+
1
3
  import click
2
- import httpx
3
4
  from click import ClickException
4
- from pydantic import ValidationError
5
5
  from scim2_client import SCIMClientError
6
6
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
7
7
 
8
8
  from .utils import DOC_URL
9
9
  from .utils import formatted_payload
10
+ from .utils import split_headers
10
11
 
11
12
 
12
13
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="replace")
13
14
  @click.pass_context
15
+ @click.option(
16
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
17
+ )
14
18
  @click.option(
15
19
  "--indent/--no-indent",
16
20
  is_flag=True,
17
21
  default=True,
18
22
  help="Indent JSON response payloads.",
19
23
  )
20
- def replace_cli(ctx, indent):
24
+ def replace_cli(ctx, headers, indent):
21
25
  """Perform a `SCIM PUT <https://www.rfc-editor.org/rfc/rfc7644#section-3.5.1>`_ request
22
26
  on the resources endpoint.
23
27
 
@@ -34,15 +38,16 @@ def replace_cli(ctx, indent):
34
38
  raise ClickException("Input data is missing")
35
39
 
36
40
  try:
37
- response = ctx.obj["client"].replace(payload)
38
-
39
- except (httpx.HTTPError, SCIMClientError) as exc:
40
- raise ClickException(exc) from exc
41
-
42
- except ValidationError as exc:
43
- payload = formatted_payload(exc.response_payload, indent)
44
- message = f"The server response is invalid:\n{payload}\n{exc}"
45
- raise ClickException(message) from exc
41
+ response = ctx.obj["client"].replace(payload, headers=split_headers(headers))
42
+
43
+ except SCIMClientError as scim_exc:
44
+ message = str(scim_exc)
45
+ if sys.version_info >= (3, 11) and hasattr(
46
+ scim_exc, "__notes__"
47
+ ): # pragma: no cover
48
+ for note in scim_exc.__notes__:
49
+ message = f"{message}\n{note}"
50
+ raise ClickException(message) from scim_exc
46
51
 
47
52
  payload = formatted_payload(response.model_dump(), indent)
48
53
  click.echo(payload)
@@ -1,15 +1,15 @@
1
+ import sys
1
2
  from typing import List
2
3
 
3
4
  import click
4
- import httpx
5
5
  from click import ClickException
6
- from pydantic import ValidationError
7
6
  from scim2_client import SCIMClientError
8
7
  from scim2_models import SearchRequest
9
8
  from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
10
9
 
11
10
  from .utils import DOC_URL
12
11
  from .utils import formatted_payload
12
+ from .utils import split_headers
13
13
 
14
14
 
15
15
  @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="search")
@@ -45,6 +45,9 @@ from .utils import formatted_payload
45
45
  "--sort-order",
46
46
  help="A string indicating the order in which the “sortBy” parameter is applied.",
47
47
  )
48
+ @click.option(
49
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
50
+ )
48
51
  @click.option(
49
52
  "--indent/--no-indent",
50
53
  is_flag=True,
@@ -60,6 +63,7 @@ def search_cli(
60
63
  filter: str,
61
64
  sort_by: str,
62
65
  sort_order: str,
66
+ headers: List[str],
63
67
  indent: bool,
64
68
  ):
65
69
  """Perform a `SCIM GET <https://www.rfc-editor.org/rfc/rfc7644#section-3.4.1>`_ request
@@ -92,15 +96,17 @@ def search_cli(
92
96
  response = ctx.obj["client"].search(
93
97
  search_request=payload,
94
98
  check_request_payload=check_request_payload,
99
+ headers=split_headers(headers),
95
100
  )
96
101
 
97
- except (httpx.HTTPError, SCIMClientError) as exc:
98
- raise ClickException(exc) from exc
99
-
100
- except ValidationError as exc:
101
- payload = formatted_payload(exc.response_payload, indent)
102
- message = f"The server response is invalid:\n{payload}\n{exc}"
103
- raise ClickException(message) from exc
102
+ except SCIMClientError as scim_exc:
103
+ message = str(scim_exc)
104
+ if sys.version_info >= (3, 11) and hasattr(
105
+ scim_exc, "__notes__"
106
+ ): # pragma: no cover
107
+ for note in scim_exc.__notes__:
108
+ message = f"{message}\n{note}"
109
+ raise ClickException(message) from scim_exc
104
110
 
105
111
  payload = formatted_payload(response.model_dump(), indent)
106
112
  click.echo(payload)
@@ -0,0 +1,40 @@
1
+ import click
2
+ from scim2_tester import Status
3
+ from scim2_tester import check_server
4
+ from sphinx_click.rst_to_ansi_formatter import make_rst_to_ansi_formatter
5
+
6
+ from .utils import DOC_URL
7
+ from .utils import Color
8
+ from .utils import split_headers
9
+
10
+
11
+ @click.command(cls=make_rst_to_ansi_formatter(DOC_URL), name="test")
12
+ @click.pass_context
13
+ @click.option(
14
+ "-h", "--headers", multiple=True, help="Header to pass in the HTTP requests."
15
+ )
16
+ @click.option("-v", "--verbose", is_flag=True, help="Enables verbose mode")
17
+ def test_cli(ctx, headers, verbose):
18
+ """Perform a server SCIM compliance check using :doc:`scim2-tester
19
+ <scim2-tester:index>`.
20
+
21
+ .. code-block:: bash
22
+
23
+ scim https://scim.example test
24
+ """
25
+
26
+ client = ctx.obj["client"]
27
+ client.client.headers.update(split_headers(headers))
28
+ results = check_server(client)
29
+ click.echo(f"Performing a SCIM compliance check on {client.client.base_url} ...")
30
+ for result in results:
31
+ if result.status == Status.SUCCESS:
32
+ status = click.style(result.status.name, fg=Color.green)
33
+ else:
34
+ status = click.style(result.status.name, fg=Color.red)
35
+ click.echo(f"{status} {result.title}")
36
+
37
+ if result.reason:
38
+ click.echo(f" {result.reason}")
39
+ if verbose and result.data:
40
+ click.echo(f" {result.data}")
@@ -0,0 +1,36 @@
1
+ import json
2
+ from enum import Enum
3
+
4
+ DOC_URL = "https://scim2-cli.readthedocs.io/"
5
+ INDENTATION_SIZE = 4
6
+
7
+
8
+ class Color(str, Enum):
9
+ black = "black"
10
+ red = "red"
11
+ green = "green"
12
+ yellow = "yellow"
13
+ blue = "blue"
14
+ magenta = "magenta"
15
+ cyan = "cyan"
16
+ white = "white"
17
+ bright_black = "bright_black"
18
+ bright_red = "bright_red"
19
+ bright_green = "bright_green"
20
+ bright_yellow = "bright_yellow"
21
+ bright_blue = "bright_blue"
22
+ bright_magenta = "bright_magenta"
23
+ bright_cyan = "bright_cyan"
24
+ bright_white = "bright_white"
25
+
26
+
27
+ def formatted_payload(obj, indent):
28
+ indent = INDENTATION_SIZE if indent else False
29
+ return json.dumps(obj, indent=indent)
30
+
31
+
32
+ def split_headers(headers):
33
+ return {
34
+ header[: header.index(":")].strip(): header[header.index(":") + 1 :].strip()
35
+ for header in headers
36
+ }
@@ -1,9 +0,0 @@
1
- import json
2
-
3
- DOC_URL = "https://scim2-cli.readthedocs.io/"
4
- INDENTATION_SIZE = 4
5
-
6
-
7
- def formatted_payload(obj, indent):
8
- indent = INDENTATION_SIZE if indent else False
9
- return json.dumps(obj, indent=indent)