diff --git a/adapters/opencmis_console_adapter.py b/adapters/opencmis_console_adapter.py index 3d718f9..fa86853 100644 --- a/adapters/opencmis_console_adapter.py +++ b/adapters/opencmis_console_adapter.py @@ -5,6 +5,7 @@ from __future__ import annotations import argparse import json +import os import subprocess from datetime import datetime, timezone from pathlib import Path @@ -45,6 +46,8 @@ def main() -> int: parser.add_argument("--artifact-dir", type=Path, required=True) parser.add_argument("--run-dir", type=Path) parser.add_argument("--extension-path", type=Path, default=Path(__file__).resolve().parents[1]) + parser.add_argument("--credentials-ref", default="") + parser.add_argument("--target-profile-dir", type=Path) parser.add_argument("--user") parser.add_argument("--password") parser.add_argument("--maven-executable", default="mvn") @@ -60,7 +63,8 @@ def main() -> int: def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: artifact_dir = args.artifact_dir.resolve() artifact_dir.mkdir(parents=True, exist_ok=True) - session_path = artifact_dir / "session.properties" + private_session_path = artifact_dir / "session-private.properties" + redacted_session_path = artifact_dir / "session.properties.redacted" groups_path = artifact_dir / "groups.txt" invocation_path = artifact_dir / "console-runner-invocation.json" console_stdout = artifact_dir / "console-runner-stdout.txt" @@ -78,25 +82,41 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: [], ) - _write_session_parameters(session_path, args) + credentials = _load_credentials(args) + if credentials["status"] == "blocked": + return _result( + "blocked", + credentials["observations"], + args, + group_classes, + args.run_dir, + artifact_dir, + [], + extra_facts={"blocked_reason": "credentials_unavailable"}, + ) + + _write_session_parameters(private_session_path, redacted_session_path, args, credentials) _write_groups(groups_path, group_classes) - command = _maven_command(args, session_path, groups_path) + command = _maven_command(args, private_session_path, groups_path) invocation = { "created_at": _now(), "command": command, "check_group": args.check_group, "group_classes": group_classes, - "session_parameters": str(session_path), + "session_parameters": str(redacted_session_path), + "private_session_parameters": str(private_session_path), "groups_file": str(groups_path), + "auth_mode": credentials["auth_mode"], } invocation_path.write_text(json.dumps(invocation, indent=2, sort_keys=True) + "\n", encoding="utf-8") artifact_refs = [ - str(session_path.relative_to(artifact_dir)), + str(redacted_session_path.relative_to(artifact_dir)), str(groups_path.relative_to(artifact_dir)), str(invocation_path.relative_to(artifact_dir)), ] if not group_classes: + private_session_path.unlink(missing_ok=True) return _result( "skipped", [f"No OpenCMIS TCK group classes are mapped for {args.check_group}; treated as a known-gap review group."], @@ -107,6 +127,7 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: artifact_refs, ) if args.dry_run: + private_session_path.unlink(missing_ok=True) return _result( "skipped", ["Dry run completed; OpenCMIS TCK ConsoleRunner was not invoked."], @@ -117,13 +138,16 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: artifact_refs, ) - completed = subprocess.run( - command, - capture_output=True, - text=True, - timeout=args.timeout_seconds, - check=False, - ) + try: + completed = subprocess.run( + command, + capture_output=True, + text=True, + timeout=args.timeout_seconds, + check=False, + ) + finally: + private_session_path.unlink(missing_ok=True) console_stdout.write_text(completed.stdout, encoding="utf-8") console_stderr.write_text(completed.stderr, encoding="utf-8") artifact_refs.extend( @@ -151,7 +175,12 @@ def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]: ) -def _write_session_parameters(path: Path, args: argparse.Namespace) -> None: +def _write_session_parameters( + private_path: Path, + redacted_path: Path, + args: argparse.Namespace, + credentials: dict[str, Any], +) -> None: values = { "org.apache.chemistry.opencmis.binding.spi.type": "browser", "org.apache.chemistry.opencmis.binding.browser.url": args.browser_url, @@ -160,14 +189,99 @@ def _write_session_parameters(path: Path, args: argparse.Namespace) -> None: "org.apache.chemistry.opencmis.binding.compression": "true", "org.apache.chemistry.opencmis.binding.cookies": "true", } - if args.user is not None: - values["org.apache.chemistry.opencmis.user"] = args.user - if args.password is not None: - values["org.apache.chemistry.opencmis.password"] = args.password - path.write_text( + user = credentials.get("user") + password = credentials.get("password") + if isinstance(user, str) and user: + values["org.apache.chemistry.opencmis.user"] = user + if isinstance(password, str) and password: + values["org.apache.chemistry.opencmis.password"] = password + private_path.write_text( "\n".join(f"{key}={value}" for key, value in values.items()) + "\n", encoding="utf-8", ) + redacted_values = dict(values) + if "org.apache.chemistry.opencmis.password" in redacted_values: + redacted_values["org.apache.chemistry.opencmis.password"] = "" + redacted_path.write_text( + "\n".join(f"{key}={value}" for key, value in redacted_values.items()) + "\n", + encoding="utf-8", + ) + + +def _load_credentials(args: argparse.Namespace) -> dict[str, Any]: + if args.user is not None or args.password is not None: + return { + "status": "available", + "auth_mode": "argv", + "user": args.user, + "password": args.password, + } + credentials_ref = (args.credentials_ref or "").strip() + if not credentials_ref: + return {"status": "available", "auth_mode": "anonymous"} + if credentials_ref.startswith("env:"): + return _load_env_credentials(credentials_ref) + if credentials_ref.startswith("file:"): + return _load_file_credentials(credentials_ref, args.target_profile_dir) + return { + "status": "blocked", + "auth_mode": "unknown", + "observations": [ + "Unsupported credentials_ref. Use env:USER_VAR,PASSWORD_VAR or file:/path/to/credentials.json." + ], + } + + +def _load_env_credentials(credentials_ref: str) -> dict[str, Any]: + names = credentials_ref.removeprefix("env:").split(",", 1) + if len(names) != 2 or not names[0] or not names[1]: + return { + "status": "blocked", + "auth_mode": "env", + "observations": [ + "Environment credentials_ref must be env:USER_VAR,PASSWORD_VAR." + ], + } + user = os.environ.get(names[0]) + password = os.environ.get(names[1]) + missing = [name for name, value in [(names[0], user), (names[1], password)] if not value] + if missing: + return { + "status": "blocked", + "auth_mode": "env", + "observations": [ + "Missing credential environment variable(s): " + ", ".join(missing) + "." + ], + } + return {"status": "available", "auth_mode": "env", "user": user, "password": password} + + +def _load_file_credentials( + credentials_ref: str, + target_profile_dir: Path | None, +) -> dict[str, Any]: + raw_path = credentials_ref.removeprefix("file:") + path = Path(raw_path).expanduser() + if not path.is_absolute() and target_profile_dir is not None: + path = target_profile_dir / path + if not path.exists(): + return { + "status": "blocked", + "auth_mode": "file", + "observations": [f"Credential file does not exist: {path}."], + } + payload = json.loads(path.read_text(encoding="utf-8")) + user = payload.get("user") + password = payload.get("password") + if not isinstance(user, str) or not isinstance(password, str): + return { + "status": "blocked", + "auth_mode": "file", + "observations": [ + "Credential file must contain string fields 'user' and 'password'." + ], + } + return {"status": "available", "auth_mode": "file", "user": user, "password": password} def _write_groups(path: Path, group_classes: list[str]) -> None: @@ -217,6 +331,7 @@ def _result( artifact_refs: list[str], cases: list[dict[str, str]] | None = None, returncode: int | None = None, + extra_facts: dict[str, Any] | None = None, ) -> dict[str, Any]: cases = cases or [] counts: dict[str, int] = {} @@ -224,18 +339,21 @@ def _result( counts[case["status"]] = counts.get(case["status"], 0) + 1 if not counts: counts[status] = 1 + facts = { + "adapter": "opencmis-console-runner", + "artifact_dir": str(artifact_dir), + "check_group": args.check_group, + "group_classes": group_classes, + "returncode": returncode, + "result_counts": counts, + } + if extra_facts: + facts.update(extra_facts) return { "result": status, "observations": observations, "tests": cases, - "facts": { - "adapter": "opencmis-console-runner", - "artifact_dir": str(artifact_dir), - "check_group": args.check_group, - "group_classes": group_classes, - "returncode": returncode, - "result_counts": counts, - }, + "facts": facts, "artifact_refs": [ _artifact_ref(artifact_dir / ref, run_dir, artifact_dir) for ref in artifact_refs diff --git a/docs/CMIS-PROFILES.md b/docs/CMIS-PROFILES.md index c8c8bf1..8e288b9 100644 --- a/docs/CMIS-PROFILES.md +++ b/docs/CMIS-PROFILES.md @@ -26,6 +26,53 @@ The CMIS target profile uses the guide-board `target-profile` schema: - `known_gaps`: list unsupported optional requirements with a stable gap ID, requirement refs, reason, and status such as `unsupported_by_design`. +Templates live under `profiles/targets/templates/`: + +- `cmis-browser-anonymous.json` +- `cmis-browser-basic-auth-env.json` +- `cmis-browser-basic-auth-file.json` + +## Credential References + +Secrets should not be committed to the repository or preserved in guide-board +artifacts. + +Supported credential reference forms: + +```text +null +env:CMIS_TCK_USER,CMIS_TCK_PASSWORD +file:/absolute/path/to/cmis-tck-credentials.json +``` + +Environment variables are useful for local runs: + +```sh +export CMIS_TCK_USER='alice' +export CMIS_TCK_PASSWORD='local-secret' +``` + +File credentials use JSON: + +```json +{ + "user": "alice", + "password": "local-secret" +} +``` + +The ConsoleRunner adapter writes a private session properties file only for the +duration of the run and retains `session.properties.redacted` as the artifact. + +Validate target profiles through guide-board before running: + +```sh +cd ../guide-board +PYTHONPATH=src python3 -m guide_board \ + profile validate-target \ + ../open-cmis-tck/profiles/targets/kontextual-cmis-compat.json +``` + ## Assessment Runtime Fields Repository selection and harness execution settings live in the assessment @@ -39,7 +86,28 @@ profile because they are run policy, not target identity: "opencmis_tck": { "repository_id": "compat-tck", "requires_java_maven": true, - "command": ["java", "-jar", "/assets/opencmis-tck-runner.jar"] + "command": [ + "python3", + "{extension_path}/adapters/opencmis_console_adapter.py", + "--browser-url", + "{browser_url}", + "--repository-id", + "{repository_id}", + "--check-group", + "{check_group}", + "--artifact-dir", + "{artifact_dir}", + "--run-dir", + "{run_dir}", + "--extension-path", + "{extension_path}", + "--credentials-ref", + "{credentials_ref}", + "--target-profile-dir", + "{target_profile_dir}", + "--timeout-seconds", + "{timeout_seconds}" + ] } } } diff --git a/docs/LOCAL-TCK-RUNTIME.md b/docs/LOCAL-TCK-RUNTIME.md index 4c382eb..a57e9a9 100644 --- a/docs/LOCAL-TCK-RUNTIME.md +++ b/docs/LOCAL-TCK-RUNTIME.md @@ -84,7 +84,7 @@ adapters/opencmis_console_adapter.py The adapter generates: -- `session.properties` +- `session.properties.redacted` - `groups.txt` - `console-runner-invocation.json` - `console-runner-stdout.txt` @@ -114,7 +114,23 @@ org.apache.chemistry.opencmis.binding.cookies=true ``` User and password fields are written only when explicitly supplied. Secret -storage is not implemented in this step. +values are written to a private session file for the ConsoleRunner process and +removed after use; guide-board retains only `session.properties.redacted`. + +Credential references come from the target profile: + +```text +credentials_ref: null +credentials_ref: env:CMIS_TCK_USER,CMIS_TCK_PASSWORD +credentials_ref: file:/absolute/path/to/cmis-tck-credentials.json +``` + +For an authenticated local run: + +```sh +export CMIS_TCK_USER='alice' +export CMIS_TCK_PASSWORD='local-secret' +``` ## Check Group Mapping diff --git a/profiles/assessments/cmis-browser-baseline.json b/profiles/assessments/cmis-browser-baseline.json index 992164b..2a319ad 100644 --- a/profiles/assessments/cmis-browser-baseline.json +++ b/profiles/assessments/cmis-browser-baseline.json @@ -47,6 +47,10 @@ "{run_dir}", "--extension-path", "{extension_path}", + "--credentials-ref", + "{credentials_ref}", + "--target-profile-dir", + "{target_profile_dir}", "--timeout-seconds", "{timeout_seconds}" ] diff --git a/profiles/targets/templates/cmis-browser-anonymous.json b/profiles/targets/templates/cmis-browser-anonymous.json new file mode 100644 index 0000000..6096aa3 --- /dev/null +++ b/profiles/targets/templates/cmis-browser-anonymous.json @@ -0,0 +1,23 @@ +{ + "id": "cmis-browser-anonymous-template", + "subject_type": "cmis-browser-binding-endpoint", + "subject_name": "Anonymous CMIS Browser Binding target", + "environment": "local", + "scope": [ + "CMIS 1.1 Browser Binding compatibility preparation" + ], + "endpoints": [ + { + "id": "browser-binding", + "url": "http://127.0.0.1:8080/cmis/browser", + "binding": "cmis-browser" + } + ], + "artifacts": [], + "credentials_ref": null, + "declared_capabilities": [ + "cmis.repository-info", + "cmis.type-definitions" + ], + "known_gaps": [] +} diff --git a/profiles/targets/templates/cmis-browser-basic-auth-env.json b/profiles/targets/templates/cmis-browser-basic-auth-env.json new file mode 100644 index 0000000..5bf6b2b --- /dev/null +++ b/profiles/targets/templates/cmis-browser-basic-auth-env.json @@ -0,0 +1,23 @@ +{ + "id": "cmis-browser-basic-auth-env-template", + "subject_type": "cmis-browser-binding-endpoint", + "subject_name": "Basic-auth CMIS Browser Binding target", + "environment": "local", + "scope": [ + "CMIS 1.1 Browser Binding compatibility preparation" + ], + "endpoints": [ + { + "id": "browser-binding", + "url": "http://127.0.0.1:8080/cmis/browser", + "binding": "cmis-browser" + } + ], + "artifacts": [], + "credentials_ref": "env:CMIS_TCK_USER,CMIS_TCK_PASSWORD", + "declared_capabilities": [ + "cmis.repository-info", + "cmis.type-definitions" + ], + "known_gaps": [] +} diff --git a/profiles/targets/templates/cmis-browser-basic-auth-file.json b/profiles/targets/templates/cmis-browser-basic-auth-file.json new file mode 100644 index 0000000..8f4d0a2 --- /dev/null +++ b/profiles/targets/templates/cmis-browser-basic-auth-file.json @@ -0,0 +1,23 @@ +{ + "id": "cmis-browser-basic-auth-file-template", + "subject_type": "cmis-browser-binding-endpoint", + "subject_name": "File-credential CMIS Browser Binding target", + "environment": "local", + "scope": [ + "CMIS 1.1 Browser Binding compatibility preparation" + ], + "endpoints": [ + { + "id": "browser-binding", + "url": "http://127.0.0.1:8080/cmis/browser", + "binding": "cmis-browser" + } + ], + "artifacts": [], + "credentials_ref": "file:/absolute/path/to/cmis-tck-credentials.json", + "declared_capabilities": [ + "cmis.repository-info", + "cmis.type-definitions" + ], + "known_gaps": [] +} diff --git a/profiles/targets/templates/cmis-tck-credentials.example.json b/profiles/targets/templates/cmis-tck-credentials.example.json new file mode 100644 index 0000000..1eda4fa --- /dev/null +++ b/profiles/targets/templates/cmis-tck-credentials.example.json @@ -0,0 +1,4 @@ +{ + "user": "cmis-user", + "password": "replace-with-local-secret" +} diff --git a/runners/opencmis_tck.py b/runners/opencmis_tck.py index 3a3c8ef..52ad155 100644 --- a/runners/opencmis_tck.py +++ b/runners/opencmis_tck.py @@ -138,6 +138,7 @@ def _run_configured_tck( "target_profile_id": context["target_profile"]["id"], "repository_id": _repository_id(context), "browser_binding_url": _browser_url(context), + "credentials_ref": _credentials_ref(context), }, ) @@ -424,6 +425,8 @@ def _expand_arg( .replace("{extension_path}", context["extension_path"]) .replace("{browser_url}", _browser_url(context) or "") .replace("{repository_id}", _repository_id(context) or "") + .replace("{credentials_ref}", _credentials_ref(context) or "") + .replace("{target_profile_dir}", _target_profile_dir(context) or "") .replace("{check_group}", selected_group or "") .replace("{target_id}", context["target_profile"]["id"]) .replace("{timeout_seconds}", str(int(_timeout_seconds(context)))) @@ -442,6 +445,18 @@ def _repository_id(context: dict[str, Any]) -> str | None: return value if isinstance(value, str) else None +def _credentials_ref(context: dict[str, Any]) -> str | None: + value = context["target_profile"].get("credentials_ref") + return value if isinstance(value, str) else None + + +def _target_profile_dir(context: dict[str, Any]) -> str | None: + path = context["plan"].get("profile_paths", {}).get("target_profile_path") + if not isinstance(path, str) or not path: + return None + return str(Path(path).resolve().parent) + + def _timeout_seconds(context: dict[str, Any]) -> float: runtime_policy = context["assessment_profile"].get("runtime_policy", {}) opencmis_policy = _opencmis_policy(context) diff --git a/src/open_cmis_tck/profile.py b/src/open_cmis_tck/profile.py index 71e18cc..d57473b 100644 --- a/src/open_cmis_tck/profile.py +++ b/src/open_cmis_tck/profile.py @@ -118,12 +118,25 @@ def validate_cmis_profile_config( repository_id = None if isinstance(opencmis_policy, dict): repository_id = opencmis_policy.get("repository_id") - if repository_id is not None and not isinstance(repository_id, str): + if repository_id is not None and not isinstance(repository_id, str): + diagnostics.append( + _diagnostic( + "error", + "runtime_policy.opencmis_tck.repository_id", + "repository_id must be a string when configured.", + ) + ) + + credentials_ref = target_profile.get("credentials_ref") + auth_mode = "anonymous" + if isinstance(credentials_ref, str) and credentials_ref: + auth_mode = _auth_mode(credentials_ref) + if auth_mode == "unknown": diagnostics.append( _diagnostic( - "error", - "runtime_policy.opencmis_tck.repository_id", - "repository_id must be a string when configured.", + "warning", + "credentials_ref", + "Use null, env:USER_VAR,PASSWORD_VAR, or file:/path/to/credentials.json.", ) ) @@ -134,7 +147,8 @@ def validate_cmis_profile_config( "cmis_config": { "browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None, "repository_id": repository_id, - "auth_mode": "anonymous" if target_profile.get("credentials_ref") is None else "credentials_ref", + "auth_mode": auth_mode, + "credentials_ref": credentials_ref, "declared_capabilities": sorted(declared), "known_gap_refs": sorted(known_gap_refs), "timeout_seconds": timeout, @@ -144,3 +158,11 @@ def validate_cmis_profile_config( def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]: return {"severity": severity, "field": field, "message": message} + + +def _auth_mode(credentials_ref: str) -> str: + if credentials_ref.startswith("env:"): + return "env" + if credentials_ref.startswith("file:"): + return "file" + return "unknown" diff --git a/tests/test_open_cmis_tck.py b/tests/test_open_cmis_tck.py index 7ab8867..3f4327d 100644 --- a/tests/test_open_cmis_tck.py +++ b/tests/test_open_cmis_tck.py @@ -2,6 +2,7 @@ from __future__ import annotations import http.client import json +import os import subprocess import sys import threading @@ -13,7 +14,11 @@ from tempfile import TemporaryDirectory from guide_board.discovery import discover_extensions from guide_board.execution import run_assessment -from guide_board.planning import build_run_plan, validate_assessment_profile +from guide_board.planning import ( + build_run_plan, + validate_assessment_profile, + validate_target_profile, +) from guide_board.retention import build_trend_summary from guide_board.service import ServiceHandle, start_service from open_cmis_tck.bootstrap import TCK_COORDINATE, check_runtime @@ -74,6 +79,11 @@ class OpenCmisTckExtensionTests(unittest.TestCase): self.assertEqual(diagnostics["cmis_config"]["repository_id"], "compat-tck") self.assertEqual(diagnostics["cmis_config"]["auth_mode"], "anonymous") + authenticated = dict(target) + authenticated["credentials_ref"] = "env:CMIS_TCK_USER,CMIS_TCK_PASSWORD" + authenticated_diagnostics = validate_cmis_profile_config(authenticated, assessment) + self.assertEqual(authenticated_diagnostics["cmis_config"]["auth_mode"], "env") + broken = dict(target) broken["endpoints"] = [] broken_diagnostics = validate_cmis_profile_config(broken, assessment) @@ -83,6 +93,17 @@ class OpenCmisTckExtensionTests(unittest.TestCase): broken_diagnostics["diagnostics"][0]["message"], ) + def test_target_profile_templates_validate(self) -> None: + template_dir = ROOT / "profiles" / "targets" / "templates" + for name in [ + "cmis-browser-anonymous.json", + "cmis-browser-basic-auth-env.json", + "cmis-browser-basic-auth-file.json", + ]: + with self.subTest(name=name): + profile = validate_target_profile(template_dir / name) + self.assertEqual(profile["subject_type"], "cmis-browser-binding-endpoint") + def test_bootstrap_reports_local_tck_runtime_posture(self) -> None: with TemporaryDirectory() as temporary_directory: output = Path(temporary_directory) / "runtime-summary.json" @@ -129,17 +150,65 @@ class OpenCmisTckExtensionTests(unittest.TestCase): self.assertEqual(completed.returncode, 0) self.assertEqual(result["result"], "skipped") self.assertIn( - "artifacts/open-cmis-tck/tck/repository-type/session.properties", + "artifacts/open-cmis-tck/tck/repository-type/session.properties.redacted", result["artifact_refs"], ) self.assertIn( "org.apache.chemistry.opencmis.binding.spi.type=browser", - (artifact_dir / "session.properties").read_text(encoding="utf-8"), + (artifact_dir / "session.properties.redacted").read_text(encoding="utf-8"), ) self.assertIn( "org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup", (artifact_dir / "groups.txt").read_text(encoding="utf-8"), ) + self.assertFalse((artifact_dir / "session-private.properties").exists()) + + def test_console_adapter_uses_env_credentials_without_persisting_secret(self) -> None: + with TemporaryDirectory() as temporary_directory: + temp_root = Path(temporary_directory) + run_dir = temp_root / "run" + artifact_dir = run_dir / "artifacts" / "open-cmis-tck" / "tck" / "repository-type" + env = { + **os.environ, + "CMIS_TEST_USER": "alice", + "CMIS_TEST_PASSWORD": "secret-password", + } + completed = subprocess.run( + [ + sys.executable, + str(ROOT / "adapters" / "opencmis_console_adapter.py"), + "--browser-url", + "http://127.0.0.1:8000/cmis/browser", + "--repository-id", + "local-test-repository", + "--check-group", + "repository-type", + "--artifact-dir", + str(artifact_dir), + "--run-dir", + str(run_dir), + "--extension-path", + str(ROOT), + "--credentials-ref", + "env:CMIS_TEST_USER,CMIS_TEST_PASSWORD", + "--dry-run", + ], + capture_output=True, + text=True, + check=False, + env=env, + ) + result = json.loads(completed.stdout) + redacted = (artifact_dir / "session.properties.redacted").read_text( + encoding="utf-8" + ) + + self.assertEqual(completed.returncode, 0) + self.assertEqual(result["result"], "skipped") + self.assertIn("org.apache.chemistry.opencmis.user=alice", redacted) + self.assertIn("org.apache.chemistry.opencmis.password=", redacted) + self.assertNotIn("secret-password", redacted) + self.assertFalse((artifact_dir / "session-private.properties").exists()) def test_runs_cmis_preflight_against_local_endpoint(self) -> None: server = HTTPServer(("127.0.0.1", 0), _CmisHandler) diff --git a/workplans/OPEN-CMIS-TCK-WP-0002-live-test-infrastructure.md b/workplans/OPEN-CMIS-TCK-WP-0002-live-test-infrastructure.md index 15dc80c..8524156 100644 --- a/workplans/OPEN-CMIS-TCK-WP-0002-live-test-infrastructure.md +++ b/workplans/OPEN-CMIS-TCK-WP-0002-live-test-infrastructure.md @@ -160,7 +160,7 @@ Progress: ```task id: OPEN-CMIS-TCK-WP-0002-T004 -status: todo +status: done priority: high state_hub_task_id: "c33a4d9a-a398-4a02-93c2-1d38e62a578f" ``` @@ -176,6 +176,16 @@ Acceptance: - Documentation explains how to run a profile validation before starting a TCK run. +Progress: + +- Added anonymous, environment-credential, and file-credential target profile + templates under `profiles/targets/templates/`. +- Added credential reference diagnostics to `open_cmis_tck.profile`. +- Wired `credentials_ref` and target profile directory expansion into the + OpenCMIS wrapper command placeholders. +- Updated the ConsoleRunner adapter to support `env:` and `file:` credential + refs without retaining raw passwords in evidence artifacts. + ## D2.5 - Real Result Normalization ```task