Skip to content
Merged
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
6 changes: 4 additions & 2 deletions descope/management/_outbound_scim_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

from typing import Any, Optional

from descope.management.outbound_scim_types import OutboundSCIMConfigurationData


class OutboundSCIMBase:
@staticmethod
def _compose_create_body(
app_id: str,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
body: dict[str, Any] = {
"appId": app_id,
Expand All @@ -20,7 +22,7 @@ def _compose_create_body(
def _compose_update_body(
app_id: str,
version: int,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
body: dict[str, Any] = {
"appId": app_id,
Expand Down
5 changes: 3 additions & 2 deletions descope/management/outbound_scim.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from descope._http_base import HTTPBase
from descope.management._outbound_scim_base import OutboundSCIMBase
from descope.management.common import MgmtV1
from descope.management.outbound_scim_types import OutboundSCIMConfigurationData


class OutboundSCIM(OutboundSCIMBase, HTTPBase):
def create_configuration(
self,
app_id: str,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
"""
Create a new outbound SCIM configuration on the federated SSO application
Expand Down Expand Up @@ -41,7 +42,7 @@ def update_configuration(
self,
app_id: str,
version: int,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
"""
Update the outbound SCIM configuration attached to the given federated SSO app.
Expand Down
5 changes: 3 additions & 2 deletions descope/management/outbound_scim_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from descope._http_base import AsyncHTTPBase
from descope.management._outbound_scim_base import OutboundSCIMBase
from descope.management.common import MgmtV1
from descope.management.outbound_scim_types import OutboundSCIMConfigurationData


class OutboundSCIMAsync(OutboundSCIMBase, AsyncHTTPBase):
Expand All @@ -13,7 +14,7 @@ class OutboundSCIMAsync(OutboundSCIMBase, AsyncHTTPBase):
async def create_configuration(
self,
app_id: str,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
"""
Create a new outbound SCIM configuration on the federated SSO application
Expand Down Expand Up @@ -43,7 +44,7 @@ async def update_configuration(
self,
app_id: str,
version: int,
configuration: Optional[dict] = None,
configuration: Optional[OutboundSCIMConfigurationData] = None,
) -> dict:
"""
Update the outbound SCIM configuration attached to the given federated SSO app.
Expand Down
140 changes: 140 additions & 0 deletions descope/management/outbound_scim_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"""Typed shapes for outbound-SCIM configuration.

Mirror the SCIM connector template
(``content/connectors/templates/scim/metadata.json``) so callers get IDE/mypy
completion when constructing an ``OutboundSCIMConfigurationData`` dict.
Optional-field TypedDicts are declared with ``total=False``; the backend
requires ``baseUrl`` on Create but that's enforced at the call site, not in
the type. Secret-typed fields (``hmacSecret``, ``awsAccessKeyId``,
``awsSecretAccessKey``, ``rfc9421PrivateKey``) are stored encrypted
server-side and returned masked on Load — never plaintext.
"""

from __future__ import annotations

# TypedDict, Literal, and List are all stdlib since Python 3.8; the project's
# declared floor is 3.9 so no version guard is needed. All TypedDicts here
# declare fields via ``total=False`` rather than ``NotRequired``, so no
# ``typing_extensions`` import is needed either.
from typing import List, Literal, TypedDict

OutboundSCIMHTTPAuthMethod = Literal[
"none",
"bearerToken",
"apiKey",
"basicAuth",
"oauth2ClientCredentials",
]


class OutboundSCIMUserMapping(TypedDict):
"""One entry in the user attribute mapping. ``srcKey`` may be a dot-path
(e.g. ``"customAttributes.foo"``)."""

srcKey: str
namespace: str
destKey: str


class OutboundSCIMHeader(TypedDict, total=False):
"""One extra HTTP header sent with every SCIM request. ``secret``-flagged
headers are stored encrypted server-side."""

key: str
value: str
secret: bool


class OutboundSCIMAPIKeyAuth(TypedDict):
"""API key credential. ``key`` is the header name, ``token`` is the value."""

key: str
token: str


class OutboundSCIMBasicAuth(TypedDict):
"""HTTP basic-auth credentials."""

username: str
password: str


class OutboundSCIMOAuth2RequestHeader(TypedDict):
"""One extra header sent to the OAuth2 token endpoint."""

name: str
value: str


class OutboundSCIMOAuth2ClientCredentials(TypedDict, total=False):
"""OAuth2 client-credentials grant configuration. ``scopes`` is
space-separated. ``authStyle`` is one of ``"header"`` (default) or
``"body"``."""

clientId: str
clientSecret: str
authUrl: str
scopes: str
authStyle: Literal["header", "body"]
tokenRequestHeaders: List[OutboundSCIMOAuth2RequestHeader]


class OutboundSCIMHTTPAuth(TypedDict, total=False):
"""Flat HTTP auth config: ``method`` is the discriminator; the sub-field
matching ``method`` carries the credentials. Only the sub-field named by
``method`` is used at request time — others are ignored server-side."""

method: OutboundSCIMHTTPAuthMethod
bearerToken: str
apiKey: OutboundSCIMAPIKeyAuth
basicAuth: OutboundSCIMBasicAuth
oauth2ClientCredentials: OutboundSCIMOAuth2ClientCredentials


class OutboundSCIMConfigurationData(TypedDict, total=False):
"""Provider-specific configuration blob for an outbound SCIM connector.

Field names mirror the SCIM connector template
(``content/connectors/templates/scim/metadata.json``). ``baseUrl`` is
required by the backend on Create. Secret-typed fields (``hmacSecret``,
``awsAccessKeyId``, ``awsSecretAccessKey``, ``rfc9421PrivateKey``) are
stored encrypted server-side and returned masked on Load — never plaintext.
"""

# SCIM SP root URL, e.g. "https://scim.example.com". Required on Create.
baseUrl: str
# Drop unverified phone numbers from outgoing SCIM payloads.
ignoreUnverifiedPhones: bool
# Drop unverified emails from outgoing SCIM payloads.
ignoreUnverifiedEmails: bool
# Maps Descope user attributes to SCIM attributes.
userMapping: List[OutboundSCIMUserMapping]
# HTTP auth used for every SCIM request.
authentication: OutboundSCIMHTTPAuth
# Extra HTTP headers sent with every SCIM request. Values may be secret-typed.
headers: List[OutboundSCIMHeader]
# HMAC secret; signs the base64-encoded payload — the signature is
# delivered in the "x-descope-webhook-s256" header. Secret-typed.
hmacSecret: str
# AWS Signature V4 signing mode. One of "none" (default) or "credentials".
awsAuthType: Literal["none", "credentials"]
# Required when awsAuthType == "credentials". Secret-typed.
awsAccessKeyId: str
# Required when awsAuthType == "credentials". Secret-typed.
awsSecretAccessKey: str
# AWS service to target (e.g. "lambda", "execute-api"). Required when
# awsAuthType == "credentials".
awsService: str
# Turn on RFC 9421 HTTP Message Signatures.
rfc9421SigningEnabled: bool
# PEM private key (ECDSA / Ed25519 / RSA) or HMAC secret. Secret-typed.
rfc9421PrivateKey: str
# Key id included in the signature metadata.
rfc9421KeyId: str
# HTTP message components covered by the signature (comma-separated, e.g.
# "@method,@target-uri,@authority"). Empty means defaults.
rfc9421Components: str
# How long the signature is valid, in seconds. Default 300.
rfc9421SignatureTTL: int
# Disable TLS certificate verification. Do not use in production.
insecure: bool
20 changes: 16 additions & 4 deletions tests/management/test_outbound_scim.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ async def test_create_configuration_success(self, client_factory):
response = await client.invoke(
client.mgmt.outbound_scim.create_configuration(
"app1",
{"baseUrl": "https://scim.example.com"},
{
"baseUrl": "https://scim.example.com",
"authentication": {"method": "bearerToken", "bearerToken": "shh"},
},
)
)
assert response == CONFIG_RESPONSE
Expand All @@ -46,7 +49,10 @@ async def test_create_configuration_success(self, client_factory):
params=None,
json={
"appId": "app1",
"configuration": {"baseUrl": "https://scim.example.com"},
"configuration": {
"baseUrl": "https://scim.example.com",
"authentication": {"method": "bearerToken", "bearerToken": "shh"},
},
},
follow_redirects=False,
)
Expand Down Expand Up @@ -82,7 +88,10 @@ async def test_update_configuration_success(self, client_factory):
client.mgmt.outbound_scim.update_configuration(
"app1",
3,
{"baseUrl": "https://scim.example.com"},
{
"baseUrl": "https://scim.example.com",
"authentication": {"method": "bearerToken", "bearerToken": "shh"},
},
)
)
assert response == CONFIG_RESPONSE
Expand All @@ -95,7 +104,10 @@ async def test_update_configuration_success(self, client_factory):
json={
"appId": "app1",
"version": 3,
"configuration": {"baseUrl": "https://scim.example.com"},
"configuration": {
"baseUrl": "https://scim.example.com",
"authentication": {"method": "bearerToken", "bearerToken": "shh"},
},
},
follow_redirects=False,
)
Expand Down
Loading