providers/scim: add webex compatibility mode (#21208)

* providers/scim: add webex compatibility mode

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* migrate

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2026-03-27 20:39:39 +00:00
committed by GitHub
parent d4590f15e7
commit 1a43ac1dc2
10 changed files with 157 additions and 10 deletions
+12 -7
View File
@@ -54,6 +54,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
["group", "provider", "connection"],
)
def _create_group_member(self, id: str) -> GroupMember:
member = GroupMember(value=id)
# https://developer.webex.com/admin/docs/api/v1/scim-2-groups/create-a-group
if self.provider.compatibility_mode == SCIMCompatibilityMode.WEBEX:
member.type = "user"
return member
def to_schema(self, obj: Group, connection: SCIMProviderGroup) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = super().to_schema(obj, connection)
@@ -77,9 +84,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
members = []
for user in connections:
members.append(
GroupMember(
value=user.scim_id,
)
self._create_group_member(user.scim_id),
)
if members:
scim_group.members = members
@@ -322,7 +327,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
op=PatchOp.add,
path="members",
value=[
GroupMember(value=x).model_dump(
self._create_group_member(x).model_dump(
mode="json",
exclude_unset=True,
)
@@ -335,7 +340,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
op=PatchOp.remove,
path="members",
value=[
GroupMember(value=x).model_dump(
self._create_group_member(x).model_dump(
mode="json",
exclude_unset=True,
)
@@ -363,7 +368,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
op=PatchOp.add,
path="members",
value=[
GroupMember(value=x).model_dump(
self._create_group_member(x).model_dump(
mode="json",
exclude_unset=True,
)
@@ -391,7 +396,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMProviderGroup, SCIMGroupSchema]):
op=PatchOp.remove,
path="members",
value=[
GroupMember(value=x).model_dump(
self._create_group_member(x).model_dump(
mode="json",
exclude_unset=True,
)
@@ -84,4 +84,21 @@ class Migration(migrations.Migration):
to="authentik_core.group",
),
),
migrations.AlterField(
model_name="scimprovider",
name="compatibility_mode",
field=models.CharField(
choices=[
("default", "Default"),
("aws", "AWS"),
("slack", "Slack"),
("sfdc", "Salesforce"),
("webex", "Webex"),
],
default="default",
help_text="Alter authentik behavior for vendor-specific SCIM implementations.",
max_length=30,
verbose_name="SCIM Compatibility Mode",
),
),
]
+1
View File
@@ -82,6 +82,7 @@ class SCIMCompatibilityMode(models.TextChoices):
AWS = "aws", _("AWS")
SLACK = "slack", _("Slack")
SALESFORCE = "sfdc", _("Salesforce")
WEBEX = "webex", _("Webex")
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
@@ -7,7 +7,7 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.models import SCIMCompatibilityMode, SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.tenants.models import Tenant
@@ -26,12 +26,13 @@ class SCIMMembershipTests(TestCase):
Tenant.objects.update(avatars="none")
@apply_blueprint("system/providers-scim.yaml")
def configure(self) -> None:
def configure(self, **kwargs) -> None:
"""Configure provider"""
self.provider: SCIMProvider = SCIMProvider.objects.create(
name=generate_id(),
url="https://localhost",
token=generate_id(),
**kwargs,
)
self.app: Application = Application.objects.create(
name=generate_id(),
@@ -353,3 +354,113 @@ class SCIMMembershipTests(TestCase):
]
},
)
def test_member_add_save_compat_webex(self):
"""Test member add + save"""
config = ServiceProviderConfiguration.default()
config.patch.supported = True
user_scim_id = generate_id()
group_scim_id = generate_id()
uid = generate_id()
group = Group.objects.create(
name=uid,
)
user = User.objects.create(username=generate_id())
# Test initial sync of group creation
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.model_dump(),
)
mocker.post(
"https://localhost/Users",
json={
"id": user_scim_id,
},
)
mocker.post(
"https://localhost/Groups",
json={
"id": group_scim_id,
},
)
self.configure(compatibility_mode=SCIMCompatibilityMode.WEBEX)
scim_sync.send(self.provider.pk)
self.assertEqual(mocker.call_count, 3)
self.assertEqual(mocker.request_history[0].method, "GET")
self.assertEqual(mocker.request_history[1].method, "POST")
self.assertEqual(mocker.request_history[2].method, "POST")
self.assertJSONEqual(
mocker.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"emails": [],
"active": True,
"externalId": user.uid,
"name": {"familyName": " ", "formatted": " ", "givenName": ""},
"displayName": "",
"userName": user.username,
},
)
self.assertJSONEqual(
mocker.request_history[2].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
"displayName": group.name,
},
)
with Mocker() as mocker:
mocker.get(
"https://localhost/ServiceProviderConfig",
json=config.model_dump(),
)
mocker.get(
f"https://localhost/Groups/{group_scim_id}",
json={},
)
mocker.patch(
f"https://localhost/Groups/{group_scim_id}",
json={},
)
group.users.add(user)
group.save()
self.assertEqual(mocker.call_count, 3)
self.assertEqual(mocker.request_history[0].method, "PATCH")
self.assertEqual(mocker.request_history[1].method, "PATCH")
self.assertEqual(mocker.request_history[2].method, "GET")
self.assertJSONEqual(
mocker.request_history[0].body,
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [{"value": user_scim_id, "type": "user"}],
}
],
},
)
self.assertJSONEqual(
mocker.request_history[1].body,
{
"Operations": [
{
"op": "replace",
"value": {
"id": group_scim_id,
"displayName": group.name,
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:Group"],
"externalId": str(group.pk),
},
}
]
},
)
+2 -1
View File
@@ -11057,7 +11057,8 @@
"default",
"aws",
"slack",
"sfdc"
"sfdc",
"webex"
],
"title": "SCIM Compatibility Mode",
"description": "Alter authentik behavior for vendor-specific SCIM implementations."
@@ -25,6 +25,7 @@ const (
COMPATIBILITYMODEENUM_AWS CompatibilityModeEnum = "aws"
COMPATIBILITYMODEENUM_SLACK CompatibilityModeEnum = "slack"
COMPATIBILITYMODEENUM_SFDC CompatibilityModeEnum = "sfdc"
COMPATIBILITYMODEENUM_WEBEX CompatibilityModeEnum = "webex"
)
// All allowed values of CompatibilityModeEnum enum
@@ -33,6 +34,7 @@ var AllowedCompatibilityModeEnumEnumValues = []CompatibilityModeEnum{
"aws",
"slack",
"sfdc",
"webex",
}
func (v *CompatibilityModeEnum) UnmarshalJSON(src []byte) error {
@@ -21,6 +21,8 @@ pub enum CompatibilityModeEnum {
Slack,
#[serde(rename = "sfdc")]
Sfdc,
#[serde(rename = "webex")]
Webex,
}
impl std::fmt::Display for CompatibilityModeEnum {
@@ -30,6 +32,7 @@ impl std::fmt::Display for CompatibilityModeEnum {
Self::Aws => write!(f, "aws"),
Self::Slack => write!(f, "slack"),
Self::Sfdc => write!(f, "sfdc"),
Self::Webex => write!(f, "webex"),
}
}
}
@@ -22,6 +22,7 @@ export const CompatibilityModeEnum = {
Aws: 'aws',
Slack: 'slack',
Sfdc: 'sfdc',
Webex: 'webex',
UnknownDefaultOpenApi: '11184809'
} as const;
export type CompatibilityModeEnum = typeof CompatibilityModeEnum[keyof typeof CompatibilityModeEnum];
+1
View File
@@ -35773,6 +35773,7 @@ components:
- aws
- slack
- sfdc
- webex
type: string
Config:
type: object
@@ -198,6 +198,11 @@ export function renderForm({ provider = {}, errors = {}, update }: SCIMProviderF
value: CompatibilityModeEnum.Sfdc,
description: html`${msg("Altered behavior for usage with Salesforce.")}`,
},
{
label: msg("Webex"),
value: CompatibilityModeEnum.Webex,
description: html`${msg("Altered behavior for usage with Cisco Webex.")}`,
},
]}
help=${msg(
"Alter authentik's behavior for vendor-specific SCIM implementations.",