generated from coulomb/repo-seed
Setting up Open CMIS TCK
This commit is contained in:
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -45,6 +46,8 @@ def main() -> int:
|
|||||||
parser.add_argument("--artifact-dir", type=Path, required=True)
|
parser.add_argument("--artifact-dir", type=Path, required=True)
|
||||||
parser.add_argument("--run-dir", type=Path)
|
parser.add_argument("--run-dir", type=Path)
|
||||||
parser.add_argument("--extension-path", type=Path, default=Path(__file__).resolve().parents[1])
|
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("--user")
|
||||||
parser.add_argument("--password")
|
parser.add_argument("--password")
|
||||||
parser.add_argument("--maven-executable", default="mvn")
|
parser.add_argument("--maven-executable", default="mvn")
|
||||||
@@ -60,7 +63,8 @@ def main() -> int:
|
|||||||
def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
|
def run_console_adapter(args: argparse.Namespace) -> dict[str, Any]:
|
||||||
artifact_dir = args.artifact_dir.resolve()
|
artifact_dir = args.artifact_dir.resolve()
|
||||||
artifact_dir.mkdir(parents=True, exist_ok=True)
|
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"
|
groups_path = artifact_dir / "groups.txt"
|
||||||
invocation_path = artifact_dir / "console-runner-invocation.json"
|
invocation_path = artifact_dir / "console-runner-invocation.json"
|
||||||
console_stdout = artifact_dir / "console-runner-stdout.txt"
|
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)
|
_write_groups(groups_path, group_classes)
|
||||||
command = _maven_command(args, session_path, groups_path)
|
command = _maven_command(args, private_session_path, groups_path)
|
||||||
invocation = {
|
invocation = {
|
||||||
"created_at": _now(),
|
"created_at": _now(),
|
||||||
"command": command,
|
"command": command,
|
||||||
"check_group": args.check_group,
|
"check_group": args.check_group,
|
||||||
"group_classes": group_classes,
|
"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),
|
"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")
|
invocation_path.write_text(json.dumps(invocation, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
artifact_refs = [
|
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(groups_path.relative_to(artifact_dir)),
|
||||||
str(invocation_path.relative_to(artifact_dir)),
|
str(invocation_path.relative_to(artifact_dir)),
|
||||||
]
|
]
|
||||||
if not group_classes:
|
if not group_classes:
|
||||||
|
private_session_path.unlink(missing_ok=True)
|
||||||
return _result(
|
return _result(
|
||||||
"skipped",
|
"skipped",
|
||||||
[f"No OpenCMIS TCK group classes are mapped for {args.check_group}; treated as a known-gap review group."],
|
[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,
|
artifact_refs,
|
||||||
)
|
)
|
||||||
if args.dry_run:
|
if args.dry_run:
|
||||||
|
private_session_path.unlink(missing_ok=True)
|
||||||
return _result(
|
return _result(
|
||||||
"skipped",
|
"skipped",
|
||||||
["Dry run completed; OpenCMIS TCK ConsoleRunner was not invoked."],
|
["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,
|
artifact_refs,
|
||||||
)
|
)
|
||||||
|
|
||||||
completed = subprocess.run(
|
try:
|
||||||
command,
|
completed = subprocess.run(
|
||||||
capture_output=True,
|
command,
|
||||||
text=True,
|
capture_output=True,
|
||||||
timeout=args.timeout_seconds,
|
text=True,
|
||||||
check=False,
|
timeout=args.timeout_seconds,
|
||||||
)
|
check=False,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
private_session_path.unlink(missing_ok=True)
|
||||||
console_stdout.write_text(completed.stdout, encoding="utf-8")
|
console_stdout.write_text(completed.stdout, encoding="utf-8")
|
||||||
console_stderr.write_text(completed.stderr, encoding="utf-8")
|
console_stderr.write_text(completed.stderr, encoding="utf-8")
|
||||||
artifact_refs.extend(
|
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 = {
|
values = {
|
||||||
"org.apache.chemistry.opencmis.binding.spi.type": "browser",
|
"org.apache.chemistry.opencmis.binding.spi.type": "browser",
|
||||||
"org.apache.chemistry.opencmis.binding.browser.url": args.browser_url,
|
"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.compression": "true",
|
||||||
"org.apache.chemistry.opencmis.binding.cookies": "true",
|
"org.apache.chemistry.opencmis.binding.cookies": "true",
|
||||||
}
|
}
|
||||||
if args.user is not None:
|
user = credentials.get("user")
|
||||||
values["org.apache.chemistry.opencmis.user"] = args.user
|
password = credentials.get("password")
|
||||||
if args.password is not None:
|
if isinstance(user, str) and user:
|
||||||
values["org.apache.chemistry.opencmis.password"] = args.password
|
values["org.apache.chemistry.opencmis.user"] = user
|
||||||
path.write_text(
|
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",
|
"\n".join(f"{key}={value}" for key, value in values.items()) + "\n",
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
redacted_values = dict(values)
|
||||||
|
if "org.apache.chemistry.opencmis.password" in redacted_values:
|
||||||
|
redacted_values["org.apache.chemistry.opencmis.password"] = "<redacted>"
|
||||||
|
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:
|
def _write_groups(path: Path, group_classes: list[str]) -> None:
|
||||||
@@ -217,6 +331,7 @@ def _result(
|
|||||||
artifact_refs: list[str],
|
artifact_refs: list[str],
|
||||||
cases: list[dict[str, str]] | None = None,
|
cases: list[dict[str, str]] | None = None,
|
||||||
returncode: int | None = None,
|
returncode: int | None = None,
|
||||||
|
extra_facts: dict[str, Any] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
cases = cases or []
|
cases = cases or []
|
||||||
counts: dict[str, int] = {}
|
counts: dict[str, int] = {}
|
||||||
@@ -224,18 +339,21 @@ def _result(
|
|||||||
counts[case["status"]] = counts.get(case["status"], 0) + 1
|
counts[case["status"]] = counts.get(case["status"], 0) + 1
|
||||||
if not counts:
|
if not counts:
|
||||||
counts[status] = 1
|
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 {
|
return {
|
||||||
"result": status,
|
"result": status,
|
||||||
"observations": observations,
|
"observations": observations,
|
||||||
"tests": cases,
|
"tests": cases,
|
||||||
"facts": {
|
"facts": facts,
|
||||||
"adapter": "opencmis-console-runner",
|
|
||||||
"artifact_dir": str(artifact_dir),
|
|
||||||
"check_group": args.check_group,
|
|
||||||
"group_classes": group_classes,
|
|
||||||
"returncode": returncode,
|
|
||||||
"result_counts": counts,
|
|
||||||
},
|
|
||||||
"artifact_refs": [
|
"artifact_refs": [
|
||||||
_artifact_ref(artifact_dir / ref, run_dir, artifact_dir)
|
_artifact_ref(artifact_dir / ref, run_dir, artifact_dir)
|
||||||
for ref in artifact_refs
|
for ref in artifact_refs
|
||||||
|
|||||||
@@ -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,
|
- `known_gaps`: list unsupported optional requirements with a stable gap ID,
|
||||||
requirement refs, reason, and status such as `unsupported_by_design`.
|
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
|
## Assessment Runtime Fields
|
||||||
|
|
||||||
Repository selection and harness execution settings live in the assessment
|
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": {
|
"opencmis_tck": {
|
||||||
"repository_id": "compat-tck",
|
"repository_id": "compat-tck",
|
||||||
"requires_java_maven": true,
|
"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}"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ adapters/opencmis_console_adapter.py
|
|||||||
|
|
||||||
The adapter generates:
|
The adapter generates:
|
||||||
|
|
||||||
- `session.properties`
|
- `session.properties.redacted`
|
||||||
- `groups.txt`
|
- `groups.txt`
|
||||||
- `console-runner-invocation.json`
|
- `console-runner-invocation.json`
|
||||||
- `console-runner-stdout.txt`
|
- `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
|
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
|
## Check Group Mapping
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,10 @@
|
|||||||
"{run_dir}",
|
"{run_dir}",
|
||||||
"--extension-path",
|
"--extension-path",
|
||||||
"{extension_path}",
|
"{extension_path}",
|
||||||
|
"--credentials-ref",
|
||||||
|
"{credentials_ref}",
|
||||||
|
"--target-profile-dir",
|
||||||
|
"{target_profile_dir}",
|
||||||
"--timeout-seconds",
|
"--timeout-seconds",
|
||||||
"{timeout_seconds}"
|
"{timeout_seconds}"
|
||||||
]
|
]
|
||||||
|
|||||||
23
profiles/targets/templates/cmis-browser-anonymous.json
Normal file
23
profiles/targets/templates/cmis-browser-anonymous.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
23
profiles/targets/templates/cmis-browser-basic-auth-env.json
Normal file
23
profiles/targets/templates/cmis-browser-basic-auth-env.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
23
profiles/targets/templates/cmis-browser-basic-auth-file.json
Normal file
23
profiles/targets/templates/cmis-browser-basic-auth-file.json
Normal file
@@ -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": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"user": "cmis-user",
|
||||||
|
"password": "replace-with-local-secret"
|
||||||
|
}
|
||||||
@@ -138,6 +138,7 @@ def _run_configured_tck(
|
|||||||
"target_profile_id": context["target_profile"]["id"],
|
"target_profile_id": context["target_profile"]["id"],
|
||||||
"repository_id": _repository_id(context),
|
"repository_id": _repository_id(context),
|
||||||
"browser_binding_url": _browser_url(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("{extension_path}", context["extension_path"])
|
||||||
.replace("{browser_url}", _browser_url(context) or "")
|
.replace("{browser_url}", _browser_url(context) or "")
|
||||||
.replace("{repository_id}", _repository_id(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("{check_group}", selected_group or "")
|
||||||
.replace("{target_id}", context["target_profile"]["id"])
|
.replace("{target_id}", context["target_profile"]["id"])
|
||||||
.replace("{timeout_seconds}", str(int(_timeout_seconds(context))))
|
.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
|
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:
|
def _timeout_seconds(context: dict[str, Any]) -> float:
|
||||||
runtime_policy = context["assessment_profile"].get("runtime_policy", {})
|
runtime_policy = context["assessment_profile"].get("runtime_policy", {})
|
||||||
opencmis_policy = _opencmis_policy(context)
|
opencmis_policy = _opencmis_policy(context)
|
||||||
|
|||||||
@@ -118,12 +118,25 @@ def validate_cmis_profile_config(
|
|||||||
repository_id = None
|
repository_id = None
|
||||||
if isinstance(opencmis_policy, dict):
|
if isinstance(opencmis_policy, dict):
|
||||||
repository_id = opencmis_policy.get("repository_id")
|
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(
|
diagnostics.append(
|
||||||
_diagnostic(
|
_diagnostic(
|
||||||
"error",
|
"warning",
|
||||||
"runtime_policy.opencmis_tck.repository_id",
|
"credentials_ref",
|
||||||
"repository_id must be a string when configured.",
|
"Use null, env:USER_VAR,PASSWORD_VAR, or file:/path/to/credentials.json.",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -134,7 +147,8 @@ def validate_cmis_profile_config(
|
|||||||
"cmis_config": {
|
"cmis_config": {
|
||||||
"browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None,
|
"browser_binding_url": browser_endpoints[0]["url"] if browser_endpoints else None,
|
||||||
"repository_id": repository_id,
|
"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),
|
"declared_capabilities": sorted(declared),
|
||||||
"known_gap_refs": sorted(known_gap_refs),
|
"known_gap_refs": sorted(known_gap_refs),
|
||||||
"timeout_seconds": timeout,
|
"timeout_seconds": timeout,
|
||||||
@@ -144,3 +158,11 @@ def validate_cmis_profile_config(
|
|||||||
|
|
||||||
def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]:
|
def _diagnostic(severity: str, field: str, message: str) -> dict[str, str]:
|
||||||
return {"severity": severity, "field": field, "message": message}
|
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"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import http.client
|
import http.client
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -13,7 +14,11 @@ from tempfile import TemporaryDirectory
|
|||||||
|
|
||||||
from guide_board.discovery import discover_extensions
|
from guide_board.discovery import discover_extensions
|
||||||
from guide_board.execution import run_assessment
|
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.retention import build_trend_summary
|
||||||
from guide_board.service import ServiceHandle, start_service
|
from guide_board.service import ServiceHandle, start_service
|
||||||
from open_cmis_tck.bootstrap import TCK_COORDINATE, check_runtime
|
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"]["repository_id"], "compat-tck")
|
||||||
self.assertEqual(diagnostics["cmis_config"]["auth_mode"], "anonymous")
|
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 = dict(target)
|
||||||
broken["endpoints"] = []
|
broken["endpoints"] = []
|
||||||
broken_diagnostics = validate_cmis_profile_config(broken, assessment)
|
broken_diagnostics = validate_cmis_profile_config(broken, assessment)
|
||||||
@@ -83,6 +93,17 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
|
|||||||
broken_diagnostics["diagnostics"][0]["message"],
|
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:
|
def test_bootstrap_reports_local_tck_runtime_posture(self) -> None:
|
||||||
with TemporaryDirectory() as temporary_directory:
|
with TemporaryDirectory() as temporary_directory:
|
||||||
output = Path(temporary_directory) / "runtime-summary.json"
|
output = Path(temporary_directory) / "runtime-summary.json"
|
||||||
@@ -129,17 +150,65 @@ class OpenCmisTckExtensionTests(unittest.TestCase):
|
|||||||
self.assertEqual(completed.returncode, 0)
|
self.assertEqual(completed.returncode, 0)
|
||||||
self.assertEqual(result["result"], "skipped")
|
self.assertEqual(result["result"], "skipped")
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"artifacts/open-cmis-tck/tck/repository-type/session.properties",
|
"artifacts/open-cmis-tck/tck/repository-type/session.properties.redacted",
|
||||||
result["artifact_refs"],
|
result["artifact_refs"],
|
||||||
)
|
)
|
||||||
self.assertIn(
|
self.assertIn(
|
||||||
"org.apache.chemistry.opencmis.binding.spi.type=browser",
|
"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(
|
self.assertIn(
|
||||||
"org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup",
|
"org.apache.chemistry.opencmis.tck.tests.basics.BasicsTestGroup",
|
||||||
(artifact_dir / "groups.txt").read_text(encoding="utf-8"),
|
(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>", redacted)
|
||||||
|
self.assertNotIn("secret-password", redacted)
|
||||||
|
self.assertFalse((artifact_dir / "session-private.properties").exists())
|
||||||
|
|
||||||
def test_runs_cmis_preflight_against_local_endpoint(self) -> None:
|
def test_runs_cmis_preflight_against_local_endpoint(self) -> None:
|
||||||
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
|
server = HTTPServer(("127.0.0.1", 0), _CmisHandler)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ Progress:
|
|||||||
|
|
||||||
```task
|
```task
|
||||||
id: OPEN-CMIS-TCK-WP-0002-T004
|
id: OPEN-CMIS-TCK-WP-0002-T004
|
||||||
status: todo
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "c33a4d9a-a398-4a02-93c2-1d38e62a578f"
|
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
|
- Documentation explains how to run a profile validation before starting a TCK
|
||||||
run.
|
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
|
## D2.5 - Real Result Normalization
|
||||||
|
|
||||||
```task
|
```task
|
||||||
|
|||||||
Reference in New Issue
Block a user