diff --git a/.dockerignore b/.dockerignore index cf1f6d3..d19c105 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,4 +8,3 @@ !pyproject.toml !LICENSE !README.md -!client_secrets.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 862699d..19878eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## v0.4.1 (2026-07-20) + +- Load OIDC client configuration from a mounted file instead of the image. + ## v0.4.0 (2026-07-14) - Add parallel execution for `viz-workflow` recipes. diff --git a/README.md b/README.md index d83d50c..220ce30 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Cite this software as: > Trey Stafford, Robyn Marowitz, Rushiraj Nenuji, Matthew B. Jones, Twila > Moon, Jeanette Clark. 2026. Open Geospatial Data Cloud (OGDC) Runner: Client tool and API for > scalable and repeatable geospatial data transformation recipes. (version -> 0.4.0). Arctic Data Center. +> 0.4.1). Arctic Data Center. > [doi:10.18739/A2P26Q57N](https://doi.org/doi:10.18739/A2P26Q57N) diff --git a/client_secrets.json b/client_secrets.json deleted file mode 100644 index b21ccfc..0000000 --- a/client_secrets.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "server_metadata_url": "https://auth.dataone.org/realms/dataone/.well-known/openid-configuration", - "client_id": "d1-confidential", - "client_secret": "", - "redirect_uris": ["https://ogdc.test.dataone.org/api/authorize"] -} diff --git a/docs/architecture/service.md b/docs/architecture/service.md index 29d5c63..0166a5f 100644 --- a/docs/architecture/service.md +++ b/docs/architecture/service.md @@ -63,6 +63,10 @@ New users can be given access via the keycloak administrator user interface. - `OGDC_DB_NAME` (optional): database name, defaults to `ogdc`. - `OGDC_SECRET_KEY` (optional): value of the secret key used to encode/decode cookies used for authentication. +- `ACCESS_MODE` (optional): one of `authenticated`, `read_only`, or `open`; + defaults to `authenticated`. +- `OIDC_CLIENT_SECRETS_FILE`: mounted OIDC client JSON path. Required in + authenticated mode; changes require a service restart. - `OGDC_WORKFLOW_PVC_NAME` (optional): name of the PersistentVolumeClaim mounted into workflow pods, defaults to `cephfs-qgnet-ogdc-workflow-pvc`. The ogdc-helm chart sets this to `cephfs-${RELEASE_NAME}-workflow-pvc`. diff --git a/pyproject.toml b/pyproject.toml index a844fb3..f7c7ba1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ authors = [ { name = "Robyn Marowitz", email = "robyn.marowitz@colorado.edu" }, { name = "Jeanette Clark", email = "clark@nceas.ucsb.edu" }, ] -version = "0.4.0" +version = "0.4.1" description = "Defines OGDC recipe API(s) and submits recipes to OGDC for execution" readme = "README.md" license.file = "LICENSE" @@ -94,7 +94,7 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.bumpversion] -current_version = "0.4.0" +current_version = "0.4.1" commit = false tag = false diff --git a/src/ogdc_runner/__init__.py b/src/ogdc_runner/__init__.py index 38311b6..5c32524 100644 --- a/src/ogdc_runner/__init__.py +++ b/src/ogdc_runner/__init__.py @@ -14,6 +14,6 @@ ) # Use your package's name as registered on PyPI except PackageNotFoundError: # This block handles cases where the package is imported but not yet installed (e.g., development mode) - __version__ = "0.4.0" + __version__ = "0.4.1" __all__ = ["__version__"] diff --git a/src/ogdc_runner/service/auth.py b/src/ogdc_runner/service/auth.py index 0f8e0a0..aa1a246 100644 --- a/src/ogdc_runner/service/auth.py +++ b/src/ogdc_runner/service/auth.py @@ -3,12 +3,35 @@ from __future__ import annotations import os +from typing import Any -from dataone.auth import AuthFactory, load_client_secrets +from dataone.auth import ( + ACCESS_MODE_AUTHENTICATED, + AuthFactory, + ConfigurationError, + get_access_mode, + load_client_secrets, +) -SCOPE_ADMIN = os.getenv("OGDC_SCOPE_ADMIN", "odgc:admin") +SCOPE_ADMIN = os.getenv("OGDC_SCOPE_ADMIN", "ogdc:admin") scopes = [SCOPE_ADMIN] -# load secrets and instantiate the client -secrets = load_client_secrets() + +def load_oidc_client_secrets() -> dict[str, Any]: + """Load mounted OIDC credentials when authentication is enabled.""" + if get_access_mode() != ACCESS_MODE_AUTHENTICATED: + return {} + + secrets_file = os.getenv("OIDC_CLIENT_SECRETS_FILE") + if not secrets_file: + msg = ( + "OIDC_CLIENT_SECRETS_FILE must point to a client secrets JSON file " + "when ACCESS_MODE=authenticated" + ) + raise ConfigurationError(msg) + + return load_client_secrets(secrets_file) + + +secrets = load_oidc_client_secrets() auth_client = AuthFactory.create_client("fastapi", secrets, scopes) diff --git a/src/ogdc_runner/service/auth_routes.py b/src/ogdc_runner/service/auth_routes.py index 84701df..7096057 100644 --- a/src/ogdc_runner/service/auth_routes.py +++ b/src/ogdc_runner/service/auth_routes.py @@ -18,7 +18,7 @@ bearer_schema = HTTPBearer(auto_error=False) -SCOPE_ADMIN = os.getenv("OGDC_SCOPE_ADMIN", "odgc:admin") +SCOPE_ADMIN = os.getenv("OGDC_SCOPE_ADMIN", "ogdc:admin") router = APIRouter( # Require that all routes in this module be authenticated via an access diff --git a/src/ogdc_runner/workflow/viz_workflow.py b/src/ogdc_runner/workflow/viz_workflow.py index f5be638..21beba6 100644 --- a/src/ogdc_runner/workflow/viz_workflow.py +++ b/src/ogdc_runner/workflow/viz_workflow.py @@ -58,7 +58,7 @@ # Viz worker container image. Override via VIZ_WORKFLOW_IMAGE env var. VIZ_WORKFLOW_IMAGE: str = os.environ.get( "VIZ_WORKFLOW_IMAGE", - "ghcr.io/permafrostdiscoverygateway/viz-workflow:latest", + "ghcr.io/permafrostdiscoverygateway/pdgworkflow:latest", ) VIZ_WORKFLOW_IMAGE_PULL_POLICY: str = os.environ.get( "VIZ_WORKFLOW_IMAGE_PULL_POLICY", diff --git a/tests/conftest.py b/tests/conftest.py index 60c594e..6e1322c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,11 +1,15 @@ from __future__ import annotations +import os from functools import cache from pathlib import Path import pytest from sqlmodel import StaticPool, create_engine +# OIDC-specific tests provide their own temporary credentials. +os.environ.setdefault("ACCESS_MODE", "open") + from ogdc_runner.service import db SHELL_RECIPE_TEST_PATH = Path(__file__).parent / "test_shell_workflow_recipe_dir" diff --git a/tests/integration/test_ogdc_viz_recipe.py b/tests/integration/test_ogdc_viz_recipe.py index 1a5a3d8..625cb77 100644 --- a/tests/integration/test_ogdc_viz_recipe.py +++ b/tests/integration/test_ogdc_viz_recipe.py @@ -16,7 +16,7 @@ from ogdc_runner.workflow import viz_workflow DEFAULT_VIZ_WORKFLOW_TEST_IMAGE = ( - "ghcr.io/permafrostdiscoverygateway/viz-workflow:1.1.0-dev-2" + "ghcr.io/permafrostdiscoverygateway/pdgworkflow:latest" ) diff --git a/tests/unit/test_auth_config.py b/tests/unit/test_auth_config.py new file mode 100644 index 0000000..ea061d6 --- /dev/null +++ b/tests/unit/test_auth_config.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import json + +import pytest +from dataone.auth import ConfigurationError + +from ogdc_runner.service import auth + + +def test_authenticated_mode_loads_explicit_secrets_file(tmp_path, monkeypatch): + secrets_path = tmp_path / "client_secrets.json" + expected = { + "server_metadata_url": "https://auth.example/realms/test/.well-known/openid-configuration", + "client_id": "ogdc-test", + "client_secret": "not-a-real-secret", + "redirect_uris": ["https://ogdc.example/api/authorize"], + } + secrets_path.write_text(json.dumps(expected)) + monkeypatch.setenv("ACCESS_MODE", "authenticated") + monkeypatch.setenv("OIDC_CLIENT_SECRETS_FILE", str(secrets_path)) + + assert auth.load_oidc_client_secrets() == expected + + +def test_authenticated_mode_requires_explicit_secrets_file(monkeypatch): + monkeypatch.setenv("ACCESS_MODE", "authenticated") + monkeypatch.delenv("OIDC_CLIENT_SECRETS_FILE", raising=False) + + with pytest.raises(ConfigurationError, match="OIDC_CLIENT_SECRETS_FILE"): + auth.load_oidc_client_secrets() + + +@pytest.mark.parametrize("access_mode", ["open", "read_only"]) +def test_unauthenticated_modes_do_not_require_oidc_secret(access_mode, monkeypatch): + monkeypatch.setenv("ACCESS_MODE", access_mode) + monkeypatch.delenv("OIDC_CLIENT_SECRETS_FILE", raising=False) + + assert auth.load_oidc_client_secrets() == {}