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 <james@terminaloutcomes.com>
This commit is contained in:
Firstyear 2024-02-26 13:33:32 +10:00 committed by GitHub
parent 1a6400b58e
commit adb575947f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 120 additions and 20 deletions

View file

@ -12,9 +12,22 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: 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: kanidm_build:
name: Build kanidm Docker image name: Build kanidm Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: set_lower_case_name
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -23,7 +36,8 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
platforms: "linux/amd64" 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: | build-args: |
"KANIDM_FEATURES=" "KANIDM_FEATURES="
# "KANIDM_BUILD_OPTIONS=-j1" # "KANIDM_BUILD_OPTIONS=-j1"

View file

@ -12,9 +12,22 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: 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: kanidmd_build:
name: Build kanidmd Docker image name: Build kanidmd Docker image
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: set_lower_case_name
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
@ -41,7 +54,7 @@ jobs:
uses: docker/build-push-action@v5 uses: docker/build-push-action@v5
with: with:
platforms: "linux/amd64" 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: | # build-args: |
# "KANIDM_BUILD_OPTIONS=-j1" # "KANIDM_BUILD_OPTIONS=-j1"
file: server/Dockerfile file: server/Dockerfile

View file

@ -1,11 +1,37 @@
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
# ^ disabling this because pydantic models don't have public methods # ^ disabling this because pydantic models don't have public methods
import json
from typing import Dict, List, TypedDict from typing import Dict, List, TypedDict
from pydantic import BaseModel, ConfigDict, RootModel 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): class OAuth2Rs(BaseModel):
classes: List[str] classes: List[str]
displayname: str displayname: str
@ -14,7 +40,9 @@ class OAuth2Rs(BaseModel):
oauth2_rs_basic_secret: str oauth2_rs_basic_secret: str
oauth2_rs_origin: str oauth2_rs_origin: str
oauth2_rs_token_key: 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): class RawOAuth2Rs(BaseModel):
@ -38,6 +66,19 @@ class RawOAuth2Rs(BaseModel):
if len(self.attrs[field]) == 0: if len(self.attrs[field]) == 0:
raise ValueError(f"Empty field {field} in {self.attrs}") 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( return OAuth2Rs(
classes=self.attrs["class"], classes=self.attrs["class"],
displayname=self.attrs["displayname"][0], 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_basic_secret=self.attrs["oauth2_rs_basic_secret"][0],
oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0], oauth2_rs_origin=self.attrs["oauth2_rs_origin"][0],
oauth2_rs_token_key=self.attrs["oauth2_rs_token_key"][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]] Oauth2RsList = RootModel[List[RawOAuth2Rs]]

View file

@ -61,7 +61,7 @@ load-plugins = "pylint_pydantic,pylint_pytest"
[tool.ruff] [tool.ruff]
line-length = 150 line-length = 150
[tool.ruff.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"tests/*.py" = [ "tests/*.py" = [
"F401", # unused import, reused fixtures across all tests "F401", # unused import, reused fixtures across all tests
"F811", # pytest fixtures "F811", # pytest fixtures

View file

@ -1,5 +1,6 @@
import json import json
import logging import logging
import os
from pathlib import Path from pathlib import Path
from kanidm import KanidmClient from kanidm import KanidmClient
@ -23,9 +24,12 @@ async def test_oauth2_rs_list(client: KanidmClient) -> None:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
print(f"config: {client.config}") print(f"config: {client.config}")
username = "admin" username = "idm_admin"
# change this to be your admin password. # change this to be the password.
password = "pdf1Xz8q2QFsMTsvbv2jXNBaSEsDpW9h83ZRsH7dDfsJeJdM" 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( auth_resp = await client.authenticate_password(
username, password, update_internal_auth_token=True 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() resource_servers = await client.oauth2_rs_list()
print("content:") print("content:")
print(json.dumps(resource_servers, indent=4))
if resource_servers: if resource_servers:
for oauth_rs in 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: for mapping in oauth_rs.oauth2_rs_sup_scope_map:
print(f"oauth2_rs_sup_scope_map: {mapping}") print(f"oauth2_rs_sup_scope_map: {mapping}")
user, scopes = mapping.split(":")
scopes = scopes.replace("{", "[").replace("}", "]")
scopes = json.loads(scopes)
print(f"{user=} {scopes=}")

View file

@ -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" 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}" ${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 "Creating the OAuth2 RP Secondary Supplemental Crab-baite Scope Map.... wait, no that's not a thing."
echo "Checking the OAuth2 RP Exists" echo "Checking the OAuth2 RP Exists"

View file

@ -764,6 +764,27 @@ pub trait QueryServerTransaction<'a> {
}) })
.collect(); .collect();
v 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 { } else {
let v: Vec<_> = value.to_proto_string_clone_iter().collect(); let v: Vec<_> = value.to_proto_string_clone_iter().collect();
Ok(v) Ok(v)

View file

@ -1000,6 +1000,17 @@ pub enum OauthClaimMapJoin {
JsonArray, 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<DbValueOauthClaimMapJoinV1> for OauthClaimMapJoin { impl From<DbValueOauthClaimMapJoinV1> for OauthClaimMapJoin {
fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin { fn from(value: DbValueOauthClaimMapJoinV1) -> OauthClaimMapJoin {
match value { match value {

View file

@ -368,7 +368,6 @@ pub trait ValueSetT: std::fmt::Debug + DynClone {
} }
fn as_oauthclaim_map(&self) -> Option<&BTreeMap<String, OauthClaimMapping>> { fn as_oauthclaim_map(&self) -> Option<&BTreeMap<String, OauthClaimMapping>> {
debug_assert!(false);
None None
} }

View file

@ -637,12 +637,7 @@ impl ValueSetT for ValueSetOauthClaimMap {
fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> { fn to_proto_string_clone_iter(&self) -> Box<dyn Iterator<Item = String> + '_> {
Box::new(self.map.iter().flat_map(|(name, mapping)| { Box::new(self.map.iter().flat_map(|(name, mapping)| {
mapping.values.iter().map(move |(group, claims)| { mapping.values.iter().map(move |(group, claims)| {
let join_char = match mapping.join { let join_char = mapping.join.to_char();
OauthClaimMapJoin::CommaSeparatedValue => ',',
OauthClaimMapJoin::SpaceSeparatedValue => ' ',
// Should this be something else?
OauthClaimMapJoin::JsonArray => ';',
};
let joined = str_concat!(claims, join_char); let joined = str_concat!(claims, join_char);