From adb575947faffd312d1d6c8188439a7fb978fd29 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Mon, 26 Feb 2024 13:33:32 +1000 Subject: [PATCH] Adjust output of claim maps for better parsing (#2566) * Adjust output of claim maps for better parsing * Update python tests for OAuth2 bits * fixing workflows for container builds --------- Co-authored-by: James Hodgkinson --- .github/workflows/docker_build_kanidm.yml | 16 +++++++- .github/workflows/docker_build_kanidmd.yml | 15 ++++++- pykanidm/kanidm/models/oauth2_rs.py | 48 +++++++++++++++++++++- pykanidm/pyproject.toml | 2 +- pykanidm/tests/test_oauth2.py | 16 ++++---- scripts/setup_dev_environment.sh | 3 ++ server/lib/src/server/mod.rs | 21 ++++++++++ server/lib/src/value.rs | 11 +++++ server/lib/src/valueset/mod.rs | 1 - server/lib/src/valueset/oauth.rs | 7 +--- 10 files changed, 120 insertions(+), 20 deletions(-) diff --git a/.github/workflows/docker_build_kanidm.yml b/.github/workflows/docker_build_kanidm.yml index b19d760cf..b0001888b 100644 --- a/.github/workflows/docker_build_kanidm.yml +++ b/.github/workflows/docker_build_kanidm.yml @@ -12,9 +12,22 @@ concurrency: cancel-in-progress: true jobs: + set_lower_case_name: + runs-on: ubuntu-latest + name: set lower case owner name + steps: + - id: step1 + run: | + echo "OWNER_LC=${OWNER,,}" >> "${GITHUB_OUTPUT}" + env: + OWNER: '${{ github.repository_owner }}' + outputs: + owner_lc: ${{ steps.step1.outputs.OWNER_LC }} + kanidm_build: name: Build kanidm Docker image runs-on: ubuntu-latest + needs: set_lower_case_name steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx @@ -23,7 +36,8 @@ jobs: uses: docker/build-push-action@v5 with: platforms: "linux/amd64" - tags: ghcr.io/${{ github.repository_owner }}/kanidm:devel + tags: ghcr.io/${{ needs.set_lower_case_name.outputs.owner_lc }}/kanidm:devel + build-args: | "KANIDM_FEATURES=" # "KANIDM_BUILD_OPTIONS=-j1" diff --git a/.github/workflows/docker_build_kanidmd.yml b/.github/workflows/docker_build_kanidmd.yml index 617746fc1..50c00934b 100644 --- a/.github/workflows/docker_build_kanidmd.yml +++ b/.github/workflows/docker_build_kanidmd.yml @@ -12,9 +12,22 @@ concurrency: cancel-in-progress: true jobs: + set_lower_case_name: + runs-on: ubuntu-latest + name: set lower case owner name + steps: + - id: step1 + run: | + echo "OWNER_LC=${OWNER,,}" >> "${GITHUB_OUTPUT}" + env: + OWNER: '${{ github.repository_owner }}' + outputs: + owner_lc: ${{ steps.step1.outputs.OWNER_LC }} + kanidmd_build: name: Build kanidmd Docker image runs-on: ubuntu-latest + needs: set_lower_case_name steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx @@ -41,7 +54,7 @@ jobs: uses: docker/build-push-action@v5 with: platforms: "linux/amd64" - tags: ghcr.io/${{ github.repository_owner }}/kanidmd:devel + tags: ghcr.io/${{ needs.set_lower_case_name.outputs.owner_lc }}/kanidmd:devel # build-args: | # "KANIDM_BUILD_OPTIONS=-j1" file: server/Dockerfile diff --git a/pykanidm/kanidm/models/oauth2_rs.py b/pykanidm/kanidm/models/oauth2_rs.py index 1dd836a84..e2e194881 100644 --- a/pykanidm/kanidm/models/oauth2_rs.py +++ b/pykanidm/kanidm/models/oauth2_rs.py @@ -1,11 +1,37 @@ # pylint: disable=too-few-public-methods # ^ disabling this because pydantic models don't have public methods +import json from typing import Dict, List, TypedDict from pydantic import BaseModel, ConfigDict, RootModel +class OAuth2RsClaimMap(BaseModel): + name: str + group: str + join: str + values: List[str] + + @classmethod + def from_entry(cls, entry: str) -> "OAuth2RsClaimMap": + name, group, join, values = entry.split(":") + values = json.loads(values).split(",") + return cls(name=name, group=group, join=join, values=values) + + +class OAuth2RsScopeMap(BaseModel): + group: str + values: List[str] + + @classmethod + def from_entry(cls, entry: str) -> "OAuth2RsScopeMap": + group, values = entry.split(":") + values = values.replace("{", "[").replace("}", "]") + values = json.loads(values.strip()) + return cls(group=group, values=values) + + class OAuth2Rs(BaseModel): classes: List[str] displayname: str @@ -14,7 +40,9 @@ class OAuth2Rs(BaseModel): oauth2_rs_basic_secret: str oauth2_rs_origin: str oauth2_rs_token_key: str - oauth2_rs_sup_scope_map: List[str] + oauth2_rs_scope_map: List[OAuth2RsScopeMap] + oauth2_rs_sup_scope_map: List[OAuth2RsScopeMap] + oauth2_rs_claim_map: List[OAuth2RsClaimMap] class RawOAuth2Rs(BaseModel): @@ -38,6 +66,19 @@ class RawOAuth2Rs(BaseModel): if len(self.attrs[field]) == 0: raise ValueError(f"Empty field {field} in {self.attrs}") + oauth2_rs_scope_map = [ + OAuth2RsScopeMap.from_entry(entry) + for entry in self.attrs.get("oauth2_rs_scope_map", []) + ] + oauth2_rs_sup_scope_map = [ + OAuth2RsScopeMap.from_entry(entry) + for entry in self.attrs.get("oauth2_rs_sup_scope_map", []) + ] + oauth2_rs_claim_map = [ + OAuth2RsClaimMap.from_entry(entry) + for entry in self.attrs.get("oauth2_rs_claim_map", []) + ] + return OAuth2Rs( classes=self.attrs["class"], displayname=self.attrs["displayname"][0], @@ -46,9 +87,12 @@ class RawOAuth2Rs(BaseModel): oauth2_rs_basic_secret=self.attrs["oauth2_rs_basic_secret"][0], oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0], oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][0], - oauth2_rs_sup_scope_map=self.attrs.get("oauth2_rs_sup_scope_map", []), + oauth2_rs_scope_map=oauth2_rs_scope_map, + oauth2_rs_sup_scope_map=oauth2_rs_sup_scope_map, + oauth2_rs_claim_map=oauth2_rs_claim_map, ) + Oauth2RsList = RootModel[List[RawOAuth2Rs]] diff --git a/pykanidm/pyproject.toml b/pykanidm/pyproject.toml index 243caae32..a7eb47c1c 100644 --- a/pykanidm/pyproject.toml +++ b/pykanidm/pyproject.toml @@ -61,7 +61,7 @@ load-plugins = "pylint_pydantic,pylint_pytest" [tool.ruff] line-length = 150 -[tool.ruff.per-file-ignores] +[tool.ruff.lint.per-file-ignores] "tests/*.py" = [ "F401", # unused import, reused fixtures across all tests "F811", # pytest fixtures diff --git a/pykanidm/tests/test_oauth2.py b/pykanidm/tests/test_oauth2.py index 3c669145c..5d4a2dcdb 100644 --- a/pykanidm/tests/test_oauth2.py +++ b/pykanidm/tests/test_oauth2.py @@ -1,5 +1,6 @@ import json import logging +import os from pathlib import Path from kanidm import KanidmClient @@ -23,9 +24,12 @@ async def test_oauth2_rs_list(client: KanidmClient) -> None: logging.basicConfig(level=logging.DEBUG) print(f"config: {client.config}") - username = "admin" - # change this to be your admin password. - password = "pdf1Xz8q2QFsMTsvbv2jXNBaSEsDpW9h83ZRsH7dDfsJeJdM" + username = "idm_admin" + # change this to be the password. + password = os.getenv("KANIDM_PASSWORD") + if password is None: + print("No KANIDM_PASSWORD env var set for testing") + raise pytest.skip("No KANIDM_PASSWORD env var set for testing") auth_resp = await client.authenticate_password( username, password, update_internal_auth_token=True @@ -41,13 +45,9 @@ async def test_oauth2_rs_list(client: KanidmClient) -> None: resource_servers = await client.oauth2_rs_list() print("content:") - print(json.dumps(resource_servers, indent=4)) if resource_servers: for oauth_rs in resource_servers: + print(json.dumps(oauth_rs.model_dump(), indent=4, default=str)) for mapping in oauth_rs.oauth2_rs_sup_scope_map: print(f"oauth2_rs_sup_scope_map: {mapping}") - user, scopes = mapping.split(":") - scopes = scopes.replace("{", "[").replace("}", "]") - scopes = json.loads(scopes) - print(f"{user=} {scopes=}") diff --git a/scripts/setup_dev_environment.sh b/scripts/setup_dev_environment.sh index a122452a7..1a9ddb547 100755 --- a/scripts/setup_dev_environment.sh +++ b/scripts/setup_dev_environment.sh @@ -126,6 +126,9 @@ ${KANIDM} system oauth2 update-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" openi echo "Creating the ${OAUTH2_RP_ID} OAuth2 RP Supplemental Scope Map" ${KANIDM} system oauth2 update-sup-scope-map "${OAUTH2_RP_ID}" "${TEST_GROUP}" admin -D "${IDM_ADMIN_USER}" +echo "Creating a claim map for RS ${OAUTH2_RP_ID}" +${KANIDM} system oauth2 update-claim-map "${OAUTH2_RP_ID}" testclaim "${TEST_GROUP}" foo bar + echo "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing." echo "Checking the OAuth2 RP Exists" diff --git a/server/lib/src/server/mod.rs b/server/lib/src/server/mod.rs index 692f2972a..d2acdecf0 100644 --- a/server/lib/src/server/mod.rs +++ b/server/lib/src/server/mod.rs @@ -764,6 +764,27 @@ pub trait QueryServerTransaction<'a> { }) .collect(); v + } else if let Some(r_map) = value.as_oauthclaim_map() { + let mut v = Vec::new(); + for (claim_name, mapping) in r_map.iter() { + for (group_ref, claims) in mapping.values() { + let join_char = mapping.join().to_char(); + + let nv = self.uuid_to_spn(*group_ref)?; + let resolved_id = match nv { + Some(v) => v.to_proto_string_clone(), + None => uuid_to_proto_string(*group_ref), + }; + + let joined = str_concat!(claims, ','); + + v.push(format!( + "{}:{}:{}:{:?}", + claim_name, resolved_id, join_char, joined + )) + } + } + Ok(v) } else { let v: Vec<_> = value.to_proto_string_clone_iter().collect(); Ok(v) diff --git a/server/lib/src/value.rs b/server/lib/src/value.rs index b75de4295..8a0e88fe9 100644 --- a/server/lib/src/value.rs +++ b/server/lib/src/value.rs @@ -1000,6 +1000,17 @@ pub enum OauthClaimMapJoin { JsonArray, } +impl OauthClaimMapJoin { + pub(crate) fn to_char(&self) -> char { + match self { + OauthClaimMapJoin::CommaSeparatedValue => ',', + OauthClaimMapJoin::SpaceSeparatedValue => ' ', + // Should this be something else? + OauthClaimMapJoin::JsonArray => ';', + } + } +} + impl From for OauthClaimMapJoin { fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin { match value { diff --git a/server/lib/src/valueset/mod.rs b/server/lib/src/valueset/mod.rs index 440bd204e..c79f791ea 100644 --- a/server/lib/src/valueset/mod.rs +++ b/server/lib/src/valueset/mod.rs @@ -368,7 +368,6 @@ pub trait ValueSetT: std::fmt::Debug + DynClone { } fn as_oauthclaim_map(&self) -> Option<&BTreeMap> { - debug_assert!(false); None } diff --git a/server/lib/src/valueset/oauth.rs b/server/lib/src/valueset/oauth.rs index 00d276a30..af8bbd49f 100644 --- a/server/lib/src/valueset/oauth.rs +++ b/server/lib/src/valueset/oauth.rs @@ -637,12 +637,7 @@ impl ValueSetT for ValueSetOauthClaimMap { fn to_proto_string_clone_iter(&self) -> Box + '_> { Box::new(self.map.iter().flat_map(|(name, mapping)| { mapping.values.iter().map(move |(group, claims)| { - let join_char = match mapping.join { - OauthClaimMapJoin::CommaSeparatedValue => ',', - OauthClaimMapJoin::SpaceSeparatedValue => ' ', - // Should this be something else? - OauthClaimMapJoin::JsonArray => ';', - }; + let join_char = mapping.join.to_char(); let joined = str_concat!(claims, join_char);