Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# syntax=docker/dockerfile:1
# omnibioai-toolserver/Dockerfile.new
FROM python:3.12-slim-bookworm

Expand All @@ -6,13 +7,33 @@ LABEL org.opencontainers.image.source=https://github.com/man4ish/omnibioai
WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl \
ca-certificates curl git \
&& rm -rf /var/lib/apt/lists/*

ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# HIPAA-V2-019: requirements.txt pins omnibioai-iam-client (private repo)
# as a git+https dependency, so pip needs git plus a GitHub credential.
# Same pattern as omnibioai-model-registry/Dockerfile: a BuildKit secret
# mount (never an ARG, which BuildKit echoes into build logs) read through
# an ephemeral GIT_ASKPASS helper, so git never builds a credentialed URL
# that a failed clone could print. Without this the image could not be
# built at all, which is how the delegated-auth fix (a4cdf8a) never
# reached the running deployment.
RUN --mount=type=secret,id=github_token \
set -eu; \
printf '%s\n' \
'#!/bin/sh' \
'case "$1" in' \
' *Username*) printf "%s\n" "x-access-token" ;;' \
' *Password*) cat /run/secrets/github_token ;;' \
' *) exit 1 ;;' \
'esac' > /tmp/git-askpass; \
chmod 700 /tmp/git-askpass; \
trap 'rm -f /tmp/git-askpass' EXIT; \
GIT_ASKPASS=/tmp/git-askpass GIT_TERMINAL_PROMPT=0 \
pip install --no-cache-dir -r requirements.txt

COPY toolserver/ ./toolserver/
COPY toolserver_app.py .
Expand Down
27 changes: 5 additions & 22 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,28 +21,11 @@ dependencies = [
# redis + python-jose[cryptography] transitively (see that package's own
# pyproject.toml).
#
# REMOTE BUILD PENDING TAG PUSH -- v0.1.4 is the correct, intended target.
# It is a real, existing, annotated Git tag as of this change (commit
# 54f4d61, "Release v0.1.4: add delegated execution identity support"),
# independently verified by omnibioai-iam-client's own release-readiness
# review: 129/129 tests pass, the tag builds a real sdist/wheel containing
# iam_client/delegated.py, and a clean non-editable install from that exact
# tag (via `git+file://` locally) successfully imports
# DelegatedExecutionIdentity/validate_delegated_execution/
# require_delegated_execution/require_delegated_permission and passes all
# 41 delegated tests -- see omnibioai-iam-client's own
# omnibioai-docs/security/hipaa_v2_019_service_identity_design.md entry.
#
# However the tag exists ONLY LOCALLY -- it has not been pushed to
# github.com/OmniBioAI/omnibioai-iam-client. The `git+https://...@v0.1.4`
# reference below will 404 for any checkout/CI/Docker build that doesn't
# already have this exact local repository (i.e. everywhere except this
# machine) until that push happens. This is a deliberate, reported state
# (per this task's explicit "do not misrepresent remote resolvability"
# instruction), not an oversight. Local verification against the real
# local tag (not an editable install) is documented in
# requirements-dev.txt.
"omnibioai-iam-client @ git+https://github.com/OmniBioAI/omnibioai-iam-client.git@v0.1.4",
# v0.1.5 adds iam_client.registration (require_toolserver_registration),
# used by toolserver/security.py::require_tes_registration for the
# service-only TES -> POST /register_tools credential. Private repo:
# Docker builds pass a GitHub token as a BuildKit secret (see Dockerfile).
"omnibioai-iam-client @ git+https://github.com/OmniBioAI/omnibioai-iam-client.git@v0.1.5",
]

[project.optional-dependencies]
Expand Down
16 changes: 4 additions & 12 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,7 @@ pyyaml
# HIPAA-V2-019: toolserver/security.py's delegated-execution authentication
# needs omnibioai-iam-client's iam_client.delegated API.
#
# REMOTE BUILD PENDING TAG PUSH -- v0.1.4 is a real, verified, annotated
# Git tag (commit 54f4d61) as of this change, but it exists only in the
# local omnibioai-iam-client checkout on this machine, not on
# github.com/OmniBioAI/omnibioai-iam-client. This Dockerfile's `pip
# install -r requirements.txt` will 404 on this line in any environment
# other than this one until the tag is pushed -- that is the single
# remaining release action, not a bug in this line. See pyproject.toml's
# matching entry and
# omnibioai-docs/security/hipaa_v2_019_service_identity_design.md for the
# full release-readiness evidence (129/129 IAM tests, clean non-editable
# tag install, 41/41 delegated tests against that install).
omnibioai-iam-client @ git+https://github.com/OmniBioAI/omnibioai-iam-client.git@v0.1.4
# v0.1.5 adds iam_client.registration (require_toolserver_registration) for
# the service-only TES -> POST /register_tools credential. Private repo: the
# Dockerfile passes a GitHub token as a BuildKit secret.
omnibioai-iam-client @ git+https://github.com/OmniBioAI/omnibioai-iam-client.git@v0.1.5
13 changes: 7 additions & 6 deletions tests/test_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -619,16 +619,17 @@ def test_e2e_failed_run_results_not_ready(self, monkeypatch, tmp_path):
# test_toolserver_app_coverage.py's register_tools tests.

class TestRegisterToolsDisabled:
"""POST /register_tools is unconditionally disabled, with no delegated permission able to unlock it."""
"""POST /register_tools is not unlocked by delegated execution authority."""

def test_register_tools_returns_501_even_when_authenticated(self, ctx):
"""An authenticated caller (this file's fixtures always are, via
conftest.py's dependency override) gets the exact same denial as
an unauthenticated one -- there is no delegated permission that
can unlock this endpoint today."""
"""This file's fixtures are authenticated for /runs and /validate
(conftest.py overrides the delegated dependencies), but that
authority does not extend to registration, which requires TES's
separate service-only credential: a request without one is 401.
(Name kept for history; the endpoint now answers 401, not 501.)"""
client, _ = ctx
resp = client.post("/register_tools", json={"tools": [{"tool_id": "stub_exec_tool"}]})
assert resp.status_code == 501
assert resp.status_code == 401

def test_register_tools_disabled_leaves_tool_unregistered(self, ctx):
"""Since /register_tools is a no-op, a "registered" tool_id never
Expand Down
48 changes: 15 additions & 33 deletions tests/test_toolserver_app_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,14 @@ def test_create_app_yaml_not_found(capsys):
# ── Line 120→122: get_results when state != COMPLETED ───────────────────────
# Replace test_get_results_not_ready and add test for line 160

# ── /register_tools: REGISTER_TOOLS_AUTHORIZATION_MODEL_UNRESOLVED ──────────
# HIPAA-V2-019: this endpoint used to register arbitrary caller-supplied
# HTTP-tool execution definitions with zero authentication and zero
# permission model -- a code-execution-adjacent administrative capability
# that Auth's delegated-execution permission set (workflow.execute,
# runs.read) does not cover. Per the HIPAA-V2-019 follow-up, it is now
# unconditionally disabled (fails closed, 501) rather than left
# reachable under an ill-fitting permission -- see toolserver_app.py's
# own comment on register_tools_endpoint. These four tests, which
# previously asserted successful anonymous registration, are replaced by
# tests asserting the new fail-closed behavior; the stub/http-handler
# registration code paths they used to cover are now unreachable by
# design, not merely untested.
def test_register_tools_disabled_returns_501():
"""Reject a well-formed HTTP-tool registration with 501 and the REGISTER_TOOLS_AUTHORIZATION_MODEL_UNRESOLVED code."""
# ── /register_tools: anonymous callers are rejected ──────────────────────────
# HIPAA-V2-019: registration requires Auth's service-only
# toolserver_registration credential from an allowlisted service identity
# (TES). The full authenticated contract is in
# tests/test_toolserver_registration_auth.py; these keep the anonymous cases
# next to the rest of the app coverage.
def test_register_tools_anonymous_is_rejected():
"""Reject an anonymous, well-formed HTTP-tool registration with 401."""
from toolserver_app import create_app
client = TestClient(create_app())

Expand All @@ -61,37 +54,26 @@ def test_register_tools_disabled_returns_501():
"method": "POST"
}
}]})
assert resp.status_code == 501
assert "REGISTER_TOOLS_AUTHORIZATION_MODEL_UNRESOLVED" in resp.json()["detail"]
assert resp.status_code == 401


def test_register_tools_disabled_regardless_of_payload_shape():
"""Reject a minimal/stub-shaped registration payload with 501 just like a full one."""
from toolserver_app import create_app
client = TestClient(create_app())

resp = client.post("/register_tools", json={"tools": [{"tool_id": "my_stub_tool"}]})
assert resp.status_code == 501


def test_register_tools_disabled_for_empty_tools_list():
"""Reject registration with 501 even when the tools list is empty."""
def test_register_tools_anonymous_rejected_for_empty_tools_list():
"""Reject anonymous registration with 401 even when the tools list is empty."""
from toolserver_app import create_app
client = TestClient(create_app())

resp = client.post("/register_tools", json={"tools": []})
assert resp.status_code == 501
assert resp.status_code == 401


def test_register_tools_disabled_does_not_mutate_registry():
"""No tool_def -- valid or not -- can be registered through this
endpoint anymore; nothing about a request body changes that."""
def test_register_tools_anonymous_does_not_mutate_registry():
"""An anonymous request can never add a tool, whatever its body."""
from toolserver_app import create_app
client = TestClient(create_app())

client.post("/register_tools", json={"tools": [
{},
{"tool_id": "should_never_register"},
{"tool_id": "should_never_register", "http": {"url": "http://example.com"}},
]})
caps = client.get("/capabilities").json()
tool_ids = {t["tool_id"] for t in caps["tools"]}
Expand Down
27 changes: 13 additions & 14 deletions tests/test_toolserver_delegated_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ def _mock_iam(
fake_client.validate_delegated_execution = AsyncMock(side_effect=side_effect)
else:
fake_client.validate_delegated_execution = AsyncMock(return_value=return_value)
# A delegated credential is never a registration credential (Auth's
# registration introspection answers valid=false for it).
fake_client.validate_toolserver_registration = AsyncMock(return_value=None)
monkeypatch.setattr(security_mod, "get_iam_client", lambda: fake_client)
return fake_client

Expand Down Expand Up @@ -636,29 +639,25 @@ def test_route_denies_without_any_credential(self, client, monkeypatch, method,
# /register_tools: unconditionally disabled, regardless of credential
# ===========================================================================

class TestRegisterToolsUnresolved:
"""POST /register_tools stays unconditionally disabled (501) under
the new auth layer too -- neither a valid delegated credential nor
the absence of one changes that outcome, since the endpoint is
disabled before any permission check runs."""
class TestRegisterToolsNotUnlockedByDelegation:
"""POST /register_tools accepts only TES's service-only registration
credential (tests/test_toolserver_registration_auth.py). A delegated
user credential -- even one holding both execution permissions -- and
no credential at all are both rejected (401), before any tool is
registered."""

def test_register_tools_denied_even_with_valid_credential(self, client, monkeypatch):
"""A request carrying a fully valid, both-permissions delegated
credential still gets 501 from /register_tools -- the disabled
endpoint is reached before any permission check, so no
credential can unlock it."""
"""A fully valid, both-permissions delegated credential cannot register tools."""
_mock_iam(monkeypatch, return_value=BOTH_IDENTITY)
resp = client.post(
"/register_tools", json={"tools": [{"tool_id": "x"}]}, headers=_bearer(),
)
assert resp.status_code == 501
assert resp.status_code == 401

def test_register_tools_denied_without_any_credential(self, client):
"""A request with no credential at all also gets 501, the same
as an authenticated one -- proving the 501 comes from the
endpoint being disabled, not from an auth failure."""
"""A request with no credential at all is rejected with 401."""
resp = client.post("/register_tools", json={"tools": [{"tool_id": "x"}]})
assert resp.status_code == 501
assert resp.status_code == 401


# ===========================================================================
Expand Down
Loading
Loading