# Theta42 SSO Manager with OpenLDAP - All-in-One Dockerfile
# App + OpenLDAP in a single container, for development/testing or a
# self-contained single-node deployment. For production, run a dedicated
# LDAP server and configure the app via app_* env vars / mounted secrets.js.

# ── Git commit hash (build-time only) ────────────────────────────────────────
# The final image intentionally has no git binary and no .git directory (kept
# lean, per .dockerignore), so `git rev-parse` always fails at runtime and
# build_info.js silently fell back to "unknown". Resolve it here instead,
# where .git IS available (build context), and bake just the short hash into
# a file — this stage itself is discarded, only /commit.txt survives via the
# COPY --from below. Reuses the main base image (already pulled for the real
# build below) rather than a separate one, so this adds no extra image pull.
#
# GIT_COMMIT lets a caller override the resolved hash instead of computing it
# from .git in this build context. Needed when this repo is built as a git
# submodule (e.g. from theta-env): a submodule's .git is a pointer FILE, not
# a directory — the real object database lives in the superproject's
# .git/modules/, outside this repo's own directory and therefore outside
# Docker's build context entirely, so `git rev-parse` can never resolve it
# from in here no matter what. theta-env's setup.sh passes --build-arg
# GIT_COMMIT=$(git -C sso-manager-node rev-parse --short HEAD), computed on
# the host where the submodule resolves correctly.
ARG GIT_COMMIT=""
FROM node:20-alpine AS gitinfo
ARG GIT_COMMIT
WORKDIR /repo
COPY .git ./.git
RUN if [ -n "$GIT_COMMIT" ]; then \
        echo "$GIT_COMMIT" > /commit.txt; \
    else \
        { apk add --no-cache git \
        && git rev-parse --short HEAD > /commit.txt; } 2>/dev/null || echo unknown > /commit.txt; \
    fi

FROM node:20-alpine

# Install OpenLDAP and required packages.
# Alpine splits OpenLDAP into many small subpackages; there is no catch-all
# "openldap-overlays" package. We install exactly the backends/overlays/modules
# the app depends on:
#   openldap-back-mdb     : the mdb backend (slapd.conf uses `database mdb`)
#   openldap-overlay-ppolicy   : ppolicy module + overlay (account locking)
#   openldap-overlay-memberof  : reverse group membership
#   openldap-overlay-refint    : referential integrity on group members
#   openldap-passwd-sha2       : pw-sha2 module ({SSHA512} user password hashing)
# Note: Alpine does NOT ship a ppolicy.schema file — on OpenLDAP 2.6 the ppolicy
# schema is built into ppolicy.so and registered when the module loads, so
# docker-entrypoint.sh loads it via `moduleload ppolicy` (no schema include).
#   openssl : used by docker-entrypoint.sh to generate a JWT secret
RUN apk add --no-cache \
    openldap \
    openldap-clients \
    openldap-back-mdb \
    openldap-overlay-ppolicy \
    openldap-overlay-memberof \
    openldap-overlay-refint \
    openldap-overlay-syncprov \
    openldap-overlay-auditlog \
    openldap-passwd-sha2 \
    dumb-init \
    bash \
    openssl \
    redis \
    && rm -rf /var/cache/apk/*

# The openldap package already creates the `ldap` user/group, which slapd runs
# as (see -u ldap -g ldap in docker-entrypoint.sh). Nothing to add here.

WORKDIR /app

# Create required directories. slapd runs as the ldap user; the app process
# runs as root in this image (matches the bare-metal systemd unit).
RUN mkdir -p /var/lib/ldap /etc/ldap/sasl2 && \
    chown -R ldap:ldap /var/lib/ldap /etc/ldap/sasl2

# Copy application source and install production dependencies.
# .dockerignore excludes nodejs/node_modules so npm ci builds a clean tree.
COPY nodejs/package*.json ./
RUN npm ci --omit=dev

COPY nodejs/app.js ./
COPY nodejs/bin ./bin
COPY nodejs/conf ./conf
COPY nodejs/controller ./controller
COPY nodejs/middleware ./middleware
COPY nodejs/models ./models
COPY nodejs/routes ./routes
COPY nodejs/services ./services
COPY nodejs/utils ./utils
COPY nodejs/views ./views
COPY nodejs/public ./public

# routes/index.js reads path.join(__dirname, '../../tos.md') at boot. With the
# app flattened into /app, __dirname is /app/routes and ../../ resolves to /,
# so the file must exist at /tos.md (mirroring the repo where tos.md sits one
# level above the nodejs/ app dir). Without this the app crashes on startup.
COPY tos.md /tos.md

# Documentation, served in-app at /docs (routes/docs.js) so it's readable
# without internet access. Same flattened-path convention as tos.md above.
COPY README.md /README.md
COPY CHANGELOG.md /CHANGELOG.md
COPY DEPLOYMENT.md /DEPLOYMENT.md
COPY API.md /API.md
COPY directory_spec.md /directory_spec.md
COPY docs /docs

# Baked commit hash from the gitinfo stage (see build_info.js).
COPY --from=gitinfo /commit.txt ./.build_commit

# Copy startup script
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# Custom LDAP schemas the app's user model depends on. addPosixAccount
# (nodejs/models/user_ldap.js) tags every user entry with objectClasses
# theta42Person, sudoRole, and ldapPublicKey and writes dateOfBirth /
# sudoHost,sudoCommand,sudoUser / sshPublicKey — so the directory must know
# all three or user create/update fails (theta42Person: LDAP 0x15; sudoRole /
# ldapPublicKey: objectClassViolation 65). .dockerignore excludes ops/ from
# the build context except ops/schema/*.schema, which is why these COPYs work.
#   theta42.schema : dateOfBirth + theta42Person (mirrors ldap-setup.sh §5)
#   sudo.schema    : sudoRole (AUXILIARY — see file header) + sudo* attributes
#   openssh-lpk.schema : sshPublicKey + ldapPublicKey (AUXILIARY)
COPY ops/schema/theta42.schema    /etc/openldap/schema/theta42.schema
COPY ops/schema/sudo.schema       /etc/openldap/schema/sudo.schema
COPY ops/schema/openssh-lpk.schema /etc/openldap/schema/openssh-lpk.schema

# Expose ports
# 3001: SSO Manager web interface (HTTP — terminate TLS at the front proxy)
# 389:  LDAP  (plain + StartTLS) — used internally by the app; map to host only
#       if you want LAN clients to bind without TLS (not recommended).
# 636:  LDAPS — direct LDAP binds over the network (TLS): Linux host auth
#       (PAM/SSSD, sudo, SSH keys) and LDAP-native apps
EXPOSE 3001 389 636

# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
    CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1

# dumb-init reaps zombies and forwards signals to the node process the
# entrypoint execs into. Without it SIGTERM from `docker stop` is ignored
# and the container hits the 10s kill timeout.
ENTRYPOINT ["dumb-init", "/usr/local/bin/docker-entrypoint.sh"]

CMD ["node", "bin/www"]
