Skip to content

Commit de6128f

Browse files
committed
feat(identity-mapper): tool to apply user groups following incoming cluster admin policies
1 parent 8d3baea commit de6128f

33 files changed

Lines changed: 1401 additions & 1 deletion
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: Identity Mapper Container
2+
3+
on:
4+
workflow_call:
5+
6+
jobs:
7+
build:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
contents: read
11+
packages: write
12+
steps:
13+
- name: Checkout Code
14+
uses: actions/checkout@v6
15+
16+
- name: Generate Image Name
17+
run: echo IMAGE_REPOSITORY=ghcr.io/$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]' | tr '[_]' '[\-]')-identity-mapper >> $GITHUB_ENV
18+
19+
- name: Log in to GitHub Docker Registry
20+
if: github.event_name != 'pull_request'
21+
uses: docker/login-action@v4.1.0
22+
with:
23+
registry: ghcr.io
24+
username: ${{ github.actor }}
25+
password: ${{ secrets.GITHUB_TOKEN }}
26+
27+
- name: Extract Version from Tag
28+
id: tags
29+
run: echo version=$(echo "${{ github.ref }}" | awk -F '[@v]' '{print $3}') >> $GITHUB_OUTPUT
30+
31+
- name: Docker Metadata
32+
id: meta
33+
uses: docker/metadata-action@v5.10.0
34+
with:
35+
images: ${{ env.IMAGE_REPOSITORY }}
36+
tags: |
37+
type=ref,event=branch
38+
type=raw,value=latest,enable={{is_default_branch}}
39+
40+
- name: Set up Docker Buildx
41+
uses: docker/setup-buildx-action@v4.0.0
42+
with:
43+
driver-opts: network=host
44+
45+
- name: Build Image
46+
uses: docker/build-push-action@v6.18.0
47+
with:
48+
context: backend/identity-mapper
49+
push: ${{ github.event_name == 'push' }}
50+
load: false
51+
tags: ${{ steps.meta.outputs.tags }}
52+
labels: ${{ steps.meta.outputs.labels }}
53+
cache-from: type=gha
54+
cache-to: type=gha,mode=max

.github/workflows/_kyverno_policy.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ jobs:
3030
helm dep build charts/workflows
3131
helm dep build charts/sessionspaces
3232
helm dep build charts/sessionspaces/charts/database
33+
helm dep build charts/identity-mapper
3334
3435
- name: Install Kyverno
3536
run: |
@@ -39,6 +40,7 @@ jobs:
3940
- name: Install Workflows CRDs
4041
run: |
4142
helm template workflows charts/workflows --namespace workflows --create-namespace | yq e 'select(.kind == "CustomResourceDefinition")' | tee -a /tmp/crds.yaml | kubectl apply -f -
43+
kubectl apply -f charts/identity-mapper/crds/useridentity.yaml
4244
4345
- name: Wait for CRDs
4446
run: |
@@ -60,6 +62,7 @@ jobs:
6062
# To make testing work with this policy in place, it will be required to emulate the existence
6163
# of a POSIX uid label as part of request.userInfo.extra
6264
helm template workflows charts/workflows | yq e '. | select(.kind == "Policy" or .kind == "ClusterPolicy" or .kind == "GeneratingPolicy" or .kind == "ClusterRole" or .kind == "ClusterRoleBinding") | select(.metadata.name != "workflows-posix-uid-label")' | tee -a /tmp/policies.yaml | kubectl apply -f -
65+
helm template identity-mapper charts/identity-mapper | yq e '. | select(.kind == "Policy" or .kind == "ClusterPolicy" or .kind == "GeneratingPolicy" or .kind == "ClusterRole" or .kind == "ClusterRoleBinding")' | tee -a /tmp/policies.yaml | kubectl apply -f -
6366
cat /tmp/policies.yaml | yq e '. | select(.kind == "Policy" or .kind == "ClusterPolicy")' | kubectl wait --for=condition=Ready --timeout=120s -f -
6467
6568
- name: Wait for Kyverno

.github/workflows/ci.yaml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ on:
55
pull_request:
66

77
jobs:
8+
9+
detect_files_changed:
10+
runs-on: ubuntu-latest
11+
outputs:
12+
identity_mapper: ${{ steps.filter.outputs.identity_mapper }}
13+
steps:
14+
- uses: actions/checkout@v6
15+
16+
- name: Detect detect files changed
17+
id: filter
18+
uses: dorny/paths-filter@v3
19+
with:
20+
filters: |
21+
identity_mapper:
22+
- 'backend/identity-mapper/**'
23+
824
helm_lint:
925
# Deduplicate jobs from pull requests and branch pushes within the same repo.
1026
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
@@ -77,6 +93,14 @@ jobs:
7793
contents: read
7894
packages: write
7995

96+
identity_mapper_container:
97+
needs: detect_files_changed
98+
if: needs.detect_files_changed.outputs.identity_mapper == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository)
99+
uses: ./.github/workflows/_identity_mapper_container.yaml
100+
permissions:
101+
contents: read
102+
packages: write
103+
80104
auth_core_code:
81105
# Deduplicate jobs from pull requests and branch pushes within the same repo.
82106
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository

backend/identity-mapper/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__pycache__/
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.13

backend/identity-mapper/Dockerfile

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
FROM python:3.13-slim-trixie
2+
3+
# The installer requires curl (and certificates) to download the release archive
4+
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
5+
6+
# Download the latest installer
7+
ADD https://astral.sh/uv/0.11.16/install.sh /uv-installer.sh
8+
9+
# Run the installer then remove it
10+
RUN sh /uv-installer.sh && rm /uv-installer.sh
11+
12+
# Ensure the installed binary is on the `PATH`
13+
ENV PATH="/root/.local/bin/:$PATH"
14+
15+
# Copy the project into the image
16+
COPY . /app
17+
18+
# Disable development dependencies
19+
ENV UV_NO_DEV=1
20+
21+
# Sync the project into a new environment, asserting the lockfile is up to date
22+
WORKDIR /app
23+
RUN uv sync --locked
24+
25+
CMD ["uv", "run", "--no-sync", "identity-mapper"]

backend/identity-mapper/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# identity-mapper
2+
3+
This is an MVP to collect user group information to patch Argo Worflows and Pod securityContext based on LDAP information.
4+
5+
TODO: replace with a rust implementation.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
[project]
2+
name = "identity-mapper"
3+
version = "0.1.0"
4+
description = "Determines pod security context for Diamond Workflows"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "David Hadley", email = "davehadley@users.noreply.github.com" }
8+
]
9+
requires-python = ">=3.13"
10+
dependencies = [
11+
"kubernetes==35.0.0",
12+
"ldap3>=2.9.1",
13+
]
14+
15+
[project.scripts]
16+
identity-mapper = "identity_mapper:__main__._main"
17+
18+
[build-system]
19+
requires = ["uv_build>=0.11.15,<0.12.0"]
20+
build-backend = "uv_build"
21+
22+
[dependency-groups]
23+
dev = [
24+
"pytest>=9.0.3",
25+
"ruff>=0.15.13",
26+
]
27+
28+
[tool.ruff.lint]
29+
select = ["ALL"]
30+
ignore = ["N806", "D203", "D213", "D106", "S104", "D101", "D103", "D102", "COM812", "RET504", "C901"]
31+
32+
[tool.ruff.lint.per-file-ignores]
33+
"tests/**" = ["S101", "D103", "D100", "INP001", "PLR2004", ]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tool to synchronize Analysis Platform user information with LDAP."""
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Synchronize LDAP and Kubernetes user IDs and groups."""
2+
3+
import logging
4+
5+
import kubernetes
6+
import ldap3
7+
8+
from ._lookup_identities_in_kubernetes import lookup_identities_in_kubernetes
9+
from ._lookup_identities_in_ldap import lookup_identities_in_ldap
10+
from ._sync_ldap_to_kubernetes import sync_ldap_to_kubernetes
11+
12+
_logger = logging.getLogger(__name__)
13+
14+
15+
def _get_kubernetes_client() -> kubernetes.client.CustomObjectsApi:
16+
try:
17+
kubernetes.config.load_incluster_config()
18+
except kubernetes.config.ConfigException:
19+
kubernetes.config.load_kube_config()
20+
return kubernetes.client.CustomObjectsApi()
21+
22+
23+
def _main() -> None:
24+
_logger.info("Connecting to LDAP")
25+
ldap_server: str = "ldap://ldapmaster.diamond.ac.uk"
26+
server = ldap3.Server(ldap_server)
27+
ldap = ldap3.Connection(server, auto_bind=True)
28+
_logger.info("Initializing kubernetes client")
29+
kubectl = _get_kubernetes_client()
30+
_logger.info("Looking up identities in LDAP")
31+
ldap_identities = lookup_identities_in_ldap(ldap)
32+
_logger.info("Looking up identities in Kubernetes")
33+
kubernetes_identities = lookup_identities_in_kubernetes(kubectl)
34+
_logger.info("Syncronizing identities")
35+
sync_ldap_to_kubernetes(kubectl, ldap_identities, kubernetes_identities)
36+
_logger.info("Complete.")
37+
38+
39+
if __name__ == "__main__":
40+
_main()

0 commit comments

Comments
 (0)