mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
litellm already supports Google, Microsoft and generic OIDC SSO through fastapi-sso, which has no SAML support; AuthMethod.SAML existed only as an unused enum value. This adds real SAML 2.0 single sign-on for the admin UI. A new SAMLAuthHandler validates signed assertions with the OneLogin python3-saml toolkit and maps them onto a CustomOpenID, then reuses the shared post-login path every other provider goes through, so provisioning, role/team mapping and the UI session JWT are unchanged. Both SP-initiated and IdP-initiated HTTP-POST flows are supported. SP-initiated logins are bound to the browser that started them via an HttpOnly state cookie plus a cached AuthnRequest id, and the ACS rejects any response whose InResponseTo doesn't match; unsolicited (IdP-initiated) responses cannot be browser-bound so they are rejected unless SAML_ALLOW_UNSOLICITED=true. Replays are rejected by a consumed-assertion guard whose lifetime tracks each assertion's NotOnOrAfter, and both the replay guard and the login-state binding go through the proxy's shared in-memory + Redis cache for multi-instance deployments. The ACS honors DISABLE_ADMIN_UI and re-applies the free-SSO-user Enterprise gate after the assertion is validated, so an unvalidated POST can no longer drive the billable-user count query. SAML is configurable from the admin UI SSO settings (IdP metadata URL or inline XML, SP entity ID, and an allow-unsolicited toggle), which persists the SAML_* environment variables the handler reads, exactly like the Google, Microsoft and generic OIDC providers. python3-saml is kept as an optional saml extra; its xmlsec and lxml wheels bundle the native libraries so no system packages are required, and the import is guarded so the proxy still starts without the package with the SAML routes returning a clear 501. Resolves LIT-4016
195 lines
8.3 KiB
Text
195 lines
8.3 KiB
Text
# syntax=docker/dockerfile:1.7
|
|
|
|
# Base images
|
|
ARG LITELLM_BUILD_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
|
ARG LITELLM_RUNTIME_IMAGE=cgr.dev/chainguard/wolfi-base@sha256:42df77a9974d6ec8b17a5ee8bc23b532600a44d705acef2409e0933c1251b45f
|
|
ARG PROXY_EXTRAS_SOURCE=published
|
|
ARG UV_IMAGE=ghcr.io/astral-sh/uv:0.11.7@sha256:240fb85ab0f263ef12f492d8476aa3a2e4e1e333f7d67fbdd923d00a506a516a
|
|
# Pinned by digest like the other base images; bump explicitly on Node upgrades.
|
|
ARG UI_BUILD_IMAGE=node:20.18-alpine3.20@sha256:3488b10bf958af7125a176419d2d8a9937d895bf124012aae811651988d2ffe6
|
|
|
|
FROM $UV_IMAGE AS uvbin
|
|
|
|
# Admin UI builder. Pinned to the build platform so the architecture-independent
|
|
# Next.js static export compiles once natively even in a multi-arch build,
|
|
# instead of once per target arch under QEMU.
|
|
FROM --platform=$BUILDPLATFORM $UI_BUILD_IMAGE AS ui-builder
|
|
|
|
ENV NEXT_TELEMETRY_DISABLED=1 \
|
|
npm_config_fund=false \
|
|
npm_config_audit=false
|
|
|
|
WORKDIR /ui
|
|
|
|
COPY ui/litellm-dashboard/package.json ui/litellm-dashboard/package-lock.json ./
|
|
RUN --mount=type=cache,target=/root/.npm npm ci --prefer-offline
|
|
|
|
COPY ui/litellm-dashboard/ ./
|
|
RUN npm run build
|
|
|
|
FROM $LITELLM_BUILD_IMAGE AS builder
|
|
ARG PROXY_EXTRAS_SOURCE
|
|
WORKDIR /app
|
|
USER root
|
|
|
|
COPY --from=uvbin /uv /usr/local/bin/uv
|
|
COPY --from=uvbin /uvx /usr/local/bin/uvx
|
|
|
|
RUN for i in 1 2 3; do \
|
|
apk add --no-cache \
|
|
python3 \
|
|
python3-dev \
|
|
gcc \
|
|
rust \
|
|
bash \
|
|
coreutils \
|
|
curl \
|
|
openssl \
|
|
libsndfile \
|
|
nodejs \
|
|
npm && break || sleep 5; \
|
|
done
|
|
|
|
ENV UV_PROJECT_ENVIRONMENT=/app/.venv \
|
|
UV_LINK_MODE=copy \
|
|
PATH="/app/.venv/bin:${PATH}" \
|
|
LITELLM_NON_ROOT=true \
|
|
XDG_CACHE_HOME=/app/.cache
|
|
|
|
# Copy dependency metadata first for layer caching
|
|
COPY pyproject.toml uv.lock ./
|
|
COPY enterprise/pyproject.toml enterprise/
|
|
COPY litellm-proxy-extras/pyproject.toml litellm-proxy-extras/
|
|
|
|
# Install third-party dependencies (cached unless pyproject.toml/uv.lock change)
|
|
RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \
|
|
uv sync --frozen --no-install-project --no-install-workspace --no-default-groups --no-editable \
|
|
--extra proxy \
|
|
--extra proxy-runtime \
|
|
--extra extra_proxy \
|
|
--extra semantic-router \
|
|
--extra saml \
|
|
--python python3
|
|
|
|
# Copy full source tree
|
|
COPY . .
|
|
|
|
# Replace the committed UI bundle with the one built from this exact source.
|
|
# Clearing first drops the committed bundle's content-hashed chunks that COPY
|
|
# would otherwise leave behind alongside the fresh ones.
|
|
RUN rm -rf litellm/proxy/_experimental/out
|
|
COPY --from=ui-builder /ui/out/. litellm/proxy/_experimental/out/
|
|
|
|
# Set non-root flag for build time consistency
|
|
ENV LITELLM_NON_ROOT=true
|
|
|
|
RUN mkdir -p /var/lib/litellm/ui /var/lib/litellm/assets && \
|
|
cp -r /app/litellm/proxy/_experimental/out/. /var/lib/litellm/ui/ && \
|
|
cp /app/litellm/proxy/logo.jpg /var/lib/litellm/assets/logo.jpg && \
|
|
touch /var/lib/litellm/ui/.litellm_ui_ready
|
|
|
|
RUN --mount=type=cache,target=/app/.cache/uv,id=litellm-uv-cache \
|
|
if [ "$PROXY_EXTRAS_SOURCE" = "published" ]; then \
|
|
uv sync --frozen --no-default-groups --no-editable \
|
|
--extra proxy \
|
|
--extra proxy-runtime \
|
|
--extra extra_proxy \
|
|
--extra semantic-router \
|
|
--extra saml \
|
|
--python python3 \
|
|
--no-sources-package litellm-proxy-extras; \
|
|
else \
|
|
uv sync --frozen --no-default-groups --no-editable \
|
|
--extra proxy \
|
|
--extra proxy-runtime \
|
|
--extra extra_proxy \
|
|
--extra semantic-router \
|
|
--extra saml \
|
|
--python python3; \
|
|
fi
|
|
|
|
RUN HOME=/opt/prisma XDG_CACHE_HOME=/opt/prisma/.cache PRISMA_BINARY_CACHE_DIR=/opt/prisma/binaries \
|
|
npm_config_cache=/root/.npm \
|
|
prisma generate --schema=./schema.prisma
|
|
|
|
RUN sed -i 's/\r$//' docker/entrypoint.sh && chmod +x docker/entrypoint.sh && \
|
|
sed -i 's/\r$//' docker/prod_entrypoint.sh && chmod +x docker/prod_entrypoint.sh
|
|
|
|
FROM $LITELLM_RUNTIME_IMAGE AS runtime
|
|
ARG PROXY_EXTRAS_SOURCE
|
|
WORKDIR /app
|
|
USER root
|
|
|
|
RUN for i in 1 2 3; do \
|
|
apk upgrade --no-cache && break || sleep 5; \
|
|
done && \
|
|
for i in 1 2 3; do \
|
|
apk add --no-cache python3 bash openssl tzdata libsndfile nodejs && break || sleep 5; \
|
|
done
|
|
|
|
# Copy only what runtime needs. The application is installed inside the venv;
|
|
# the rest of the builder's /app is source and build metadata that must not
|
|
# ship (manifest-scanning tools attribute everything in it to this image).
|
|
# entrypoint.sh invokes litellm/proxy/prisma_migration.py by source path.
|
|
COPY --from=builder /app/.venv /app/.venv
|
|
COPY --from=builder /app/docker /app/docker
|
|
COPY --from=builder /app/schema.prisma /app/schema.prisma
|
|
COPY --from=builder /app/litellm/proxy/prisma_migration.py /app/litellm/proxy/prisma_migration.py
|
|
# enterprise/ is imported by source path at runtime (proxy_cli puts the
|
|
# working directory on sys.path; litellm/proxy/hooks resolves
|
|
# enterprise.enterprise_hooks from it)
|
|
COPY --from=builder /app/enterprise /app/enterprise
|
|
COPY --from=builder /app/litellm-proxy-extras /app/litellm-proxy-extras
|
|
# Prisma CLI + engines are baked under /opt/prisma, a fixed path every runtime
|
|
# uid can read and that no cache volume mount shadows (unlike /app/.cache or
|
|
# $HOME/.cache under readOnlyRootFilesystem + emptyDir or arbitrary-uid setups).
|
|
# PRISMA_CLI_QUERY_ENGINE_TYPE=binary makes the CLI use the baked binary query
|
|
# engine directly, so `prisma migrate deploy` on a fresh database needs no npm
|
|
# and no network access; without it the CLI looks for the library engine, which
|
|
# prisma stopped baking, and falls back to a download that fails offline or as a
|
|
# non-writable uid (#33650, #24554).
|
|
COPY --from=builder /opt/prisma /opt/prisma
|
|
COPY --from=builder /var/lib/litellm/ui /var/lib/litellm/ui
|
|
COPY --from=builder /var/lib/litellm/assets /var/lib/litellm/assets
|
|
|
|
# XDG_CACHE_HOME is intentionally left unset so it falls back to $HOME/.cache
|
|
# (/app/.cache, writable by the runtime uid). The prisma bake at the read-only
|
|
# /opt/prisma is anchored by PRISMA_BINARY_CACHE_DIR / PRISMA_CLI_PATH, so
|
|
# nothing needs XDG to point there; pointing it at the read-only bake would
|
|
# deny any XDG-aware library that writes a cache at runtime.
|
|
ENV PATH="/app/.venv/bin:${PATH}" \
|
|
PRISMA_BINARY_CACHE_DIR=/opt/prisma/binaries \
|
|
PRISMA_CLI_PATH=/opt/prisma/binaries/node_modules/.bin/prisma \
|
|
PRISMA_CLI_QUERY_ENGINE_TYPE=binary \
|
|
HOME=/app \
|
|
LITELLM_NON_ROOT=true \
|
|
PRISMA_SKIP_POSTINSTALL_GENERATE=1 \
|
|
PRISMA_HIDE_UPDATE_MESSAGE=1 \
|
|
PRISMA_ENGINES_CHECKSUM_IGNORE_MISSING=1 \
|
|
PRISMA_OFFLINE_MODE=true
|
|
|
|
RUN mkdir -p /nonexistent /app/.cache /var/lib/litellm/assets /var/lib/litellm/ui && \
|
|
chown -R nobody:nogroup /app /var/lib/litellm/ui /var/lib/litellm/assets /nonexistent && \
|
|
PRISMA_PATH=$(python -c "import os, prisma; print(os.path.dirname(prisma.__file__))") && \
|
|
chown -R nobody:nogroup "$PRISMA_PATH" && \
|
|
LITELLM_PKG_MIGRATIONS_PATH="$(python -c 'import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))' 2>/dev/null || echo '')/migrations" && \
|
|
[ -n "$LITELLM_PKG_MIGRATIONS_PATH" ] && chown -R nobody:nogroup "$LITELLM_PKG_MIGRATIONS_PATH" || true && \
|
|
LITELLM_PROXY_EXTRAS_PATH=$(python -c "import os, litellm_proxy_extras; print(os.path.dirname(litellm_proxy_extras.__file__))" 2>/dev/null || echo "") && \
|
|
chgrp -R 0 "$PRISMA_PATH" /var/lib/litellm/ui /var/lib/litellm/assets && \
|
|
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chgrp -R 0 "$LITELLM_PROXY_EXTRAS_PATH" || true && \
|
|
chmod -R g=u "$PRISMA_PATH" /var/lib/litellm/ui /var/lib/litellm/assets && \
|
|
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g=u "$LITELLM_PROXY_EXTRAS_PATH" || true && \
|
|
chmod -R g+w "$PRISMA_PATH" /var/lib/litellm/ui /var/lib/litellm/assets && \
|
|
[ -n "$LITELLM_PROXY_EXTRAS_PATH" ] && chmod -R g+w "$LITELLM_PROXY_EXTRAS_PATH" || true && \
|
|
chmod -R g+rX "$PRISMA_PATH" /var/lib/litellm/ui /var/lib/litellm/assets && \
|
|
chmod -R a+rX /opt/prisma && \
|
|
test -x /opt/prisma/binaries/node_modules/.bin/prisma && \
|
|
test -f /opt/prisma/binaries/node_modules/prisma/build/index.js && \
|
|
ls /opt/prisma/binaries/node_modules/@prisma/engines/query-engine-* >/dev/null 2>&1
|
|
|
|
USER 65534
|
|
|
|
EXPOSE 4000/tcp
|
|
|
|
ENTRYPOINT ["/app/docker/prod_entrypoint.sh"]
|
|
CMD ["--port", "4000"]
|