diff --git a/.github/workflows/publish_docker.yaml b/.github/workflows/publish_docker.yaml new file mode 100644 index 0000000..7a3a61a --- /dev/null +++ b/.github/workflows/publish_docker.yaml @@ -0,0 +1,44 @@ +--- +name: Build and publish a Docker image + +# Run workflow on pushes to matching branches +on: # yamllint disable-line rule:truthy + push: + branches: ["main"] + tags: ["*"] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-publish-image: + name: Build Docker image and publish to GitHub container repository + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + steps: + - name: Check out the repo + uses: actions/checkout@v3 + + - name: Log in to the Container registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ github.repository }} + + - name: Build and publish Docker images + uses: docker/build-push-action@v4 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0033914 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM python:3.11-alpine + +WORKDIR /app + +RUN apk add --update --no-cache \ + gcc libc-dev libffi-dev + +# Upload and install Python package and dependencies +COPY ./apricot apricot +COPY ./pyproject.toml . +COPY ./README.md . +RUN pip install --upgrade hatch pip +# Initialise environment with hatch +RUN hatch run true + +# Install executable files and set permissions +COPY ./docker/entrypoint.sh . +COPY ./run.py . +RUN chmod ugo+x ./entrypoint.sh + +# Open appropriate ports +EXPOSE 1389 + +# Run the server +ENTRYPOINT ["./entrypoint.sh"] diff --git a/README.md b/README.md index 46e6c99..59638f2 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,22 @@ The name is a slightly tortured acronym for: LD**A**P **pr**oxy for Open**I**D * ## Usage -Start the `Apricot` server on port 8080 by running: +Start the `Apricot` server on port 1389 by running: ```bash -python run.py --client-id "" --client-secret "" --backend "" --port 8080 --domain "" +python run.py --client-id "" --client-secret "" --backend "" --port 1389 --domain "" ``` +Alternatively, you can run in Docker by editing `docker/docker-compose.yaml` and running: + +```bash +docker-compose up +``` + +from the `docker` directory. + +## Outputs + This will create an LDAP tree that looks like this: ```ldif diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 0000000..2d13d7a --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,16 @@ +--- +version: "3" +services: + apricot: + container_name: apricot + image: apricot + build: . + environment: + BACKEND: "MicrosoftEntra" + CLIENT_ID: "" + DOMAIN: "" + ENTRA_TENANT_ID: "" + ports: + - "1389:1389" + restart: always diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..6655fdd --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,38 @@ +#! /bin/sh +# shellcheck disable=SC2086 +# shellcheck disable=SC2089 + +if [ -z "${BACKEND}" ]; then + echo "BACKEND environment variable is not set" + exit 1 +fi + +if [ -z "${CLIENT_ID}" ]; then + echo "CLIENT_ID environment variable is not set" + exit 1 +fi + +if [ -z "${CLIENT_SECRET}" ]; then + echo "CLIENT_SECRET environment variable is not set" + exit 1 +fi + +if [ -z "${DOMAIN}" ]; then + echo "DOMAIN environment variable is not set" + exit 1 +fi + +# Optional arguments +EXTRA_OPTS="" +if [ -n "${ENTRA_TENANT_ID}" ]; then + EXTRA_OPTS="${EXTRA_OPTS} --entra-tenant-id $ENTRA_TENANT_ID" +fi + +# Run the server +hatch run python run.py \ + --backend "$BACKEND" \ + --client-id "$CLIENT_ID" \ + --client-secret "$CLIENT_SECRET" \ + --domain "$DOMAIN" \ + --port 1389 \ + $EXTRA_OPTS diff --git a/pyproject.toml b/pyproject.toml index b8b1a4d..3cdbf16 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,8 @@ classifiers = [ ] dependencies = [ "ldaptor~=21.2.0", + "oauthlib~=3.2.2", + "requests-oauthlib~=1.3.1", "Twisted~=23.8.0", "zope.interface~=6.0", ] diff --git a/run.py b/run.py index 1d9e7dd..466958a 100644 --- a/run.py +++ b/run.py @@ -4,21 +4,28 @@ from apricot.oauth import OAuthBackend if __name__ == "__main__": - parser = argparse.ArgumentParser( - prog="Apricot", - description="Apricot is a proxy for delegating LDAP requests to an OpenID Connect backend.", - ) - # Common options needed for all backends - parser.add_argument("-b", "--backend", type=OAuthBackend, help="Which OAuth backend to use.") - parser.add_argument("-d", "--domain", type=str, help="Which domain users belong to.") - parser.add_argument("-p", "--port", type=int, default=8080, help="Port to run on.") - parser.add_argument("-i", "--client-id", type=str, help="OAuth client ID.") - parser.add_argument("-s", "--client-secret", type=str, help="OAuth client secret.") - # Options for Microsoft Entra backend - parser.add_argument("-t", "--entra-tenant-id", type=str, help="Microsoft Entra tenant ID.", required=False) - # Parse arguments - args = parser.parse_args() + try: + parser = argparse.ArgumentParser( + prog="Apricot", + description="Apricot is a proxy for delegating LDAP requests to an OpenID Connect backend.", + ) + # Common options needed for all backends + parser.add_argument("-b", "--backend", type=OAuthBackend, help="Which OAuth backend to use.") + parser.add_argument("-d", "--domain", type=str, help="Which domain users belong to.") + parser.add_argument("-p", "--port", type=int, default=1389, help="Port to run on.") + parser.add_argument("-i", "--client-id", type=str, help="OAuth client ID.") + parser.add_argument("-s", "--client-secret", type=str, help="OAuth client secret.") + # Options for Microsoft Entra backend + group = parser.add_argument_group("Microsoft Entra") + group.add_argument("-t", "--entra-tenant-id", type=str, help="Microsoft Entra tenant ID.", required=False) + # Parse arguments + args = parser.parse_args() - # Create the Apricot server - reactor = ApricotServer(**vars(args)) + # Create the Apricot server + reactor = ApricotServer(**vars(args)) + except Exception: + msg = "Unable to initialise Apricot server from provided command line arguments." + raise ValueError(msg) + + # Run the Apricot server reactor.run()