diff --git a/.env.example b/.env.example index 11a55cf..8be1548 100644 --- a/.env.example +++ b/.env.example @@ -46,6 +46,8 @@ KEYNETRA_RATE_LIMIT_WINDOW_SECONDS=60 KEYNETRA_SERVICE_MODE=all KEYNETRA_AUTO_SEED_SAMPLE_DATA=true KEYNETRA_OTEL_ENABLED=false +# Enforce explicit tenant resolution (no implicit fallback) +KEYNETRA_STRICT_TENANCY=false # Server defaults for CLI config mode KEYNETRA_SERVER_HOST=0.0.0.0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bc8ce77..8cc8b0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: CI +name: CI Pipeline on: push: @@ -8,51 +8,197 @@ on: permissions: contents: read +env: + PYTHONUNBUFFERED: "1" + KEYNETRA_DATABASE_URL: sqlite+pysqlite:///./.keynetra-ci.db + KEYNETRA_API_KEYS: testkey + KEYNETRA_RATE_LIMIT_PER_MINUTE: "5000" + KEYNETRA_RATE_LIMIT_BURST: "5000" + jobs: + + # ------------------------------- + # Stage 1: Security Scan + # ------------------------------- + secret-scan: + name: ๐Ÿ” Secret Scan (Gitleaks) + runs-on: ubuntu-latest + + steps: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: ๐Ÿ” Run Gitleaks + run: | + docker run --rm \ + -v ${{ github.workspace }}:/repo \ + ghcr.io/gitleaks/gitleaks:latest \ + detect --source /repo --verbose --exit-code 1 + + + # ------------------------------- + # Stage 2: Lint & Static Checks + # ------------------------------- + lint: + name: ๐Ÿงน Lint & Formatting + runs-on: ubuntu-latest + needs: secret-scan + + steps: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@v4 + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: ๐Ÿ“ฆ Install dependencies + run: | + pip install -r requirements.lock + pip install -r requirements-dev.lock + + - name: ๐Ÿงช Run linters + run: | + ruff check . + black --check . + isort --check-only . + lint-imports --config .importlinter + + + # ------------------------------- + # Stage 3: Security Dependencies + # ------------------------------- + security-deps: + name: ๐Ÿ›ก Dependency Security Scan + runs-on: ubuntu-latest + needs: lint + + steps: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@v4 + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: ๐Ÿ“ฆ Install dependencies + run: | + pip install -r requirements.lock + pip install pip-audit + + - name: ๐Ÿ” Run pip-audit + run: | + pip-audit + mkdir -p artifacts + pip-audit -f cyclonedx-json -o artifacts/sbom.cdx.json + + - name: ๐Ÿ“ค Upload SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom + path: artifacts/sbom.cdx.json + + + # ------------------------------- + # Stage 4: Tests (Matrix) + # ------------------------------- test: name: CI / test (${{ matrix.python-version }}) runs-on: ubuntu-latest + needs: security-deps strategy: - fail-fast: false + fail-fast: true matrix: python-version: ["3.11", "3.12", "3.13", "3.14"] - env: - KEYNETRA_DATABASE_URL: sqlite+pysqlite:///./.keynetra-ci.db - KEYNETRA_API_KEYS: testkey - PYTHONUNBUFFERED: "1" - steps: - - name: Checkout repository + - name: ๐Ÿ“ฅ Checkout repository uses: actions/checkout@v4 - - name: Set up Python ${{ matrix.python-version }} + - name: ๐Ÿ Setup Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - cache: "pip" + cache: pip - - name: Install dependencies + - name: ๐Ÿ“ฆ Install dependencies run: | - python -m pip install --upgrade pip - python -m pip install -r requirements.txt - python -m pip install -r requirements-dev.txt - python -m pip install -e . + pip install --upgrade pip + pip install -r requirements.lock + pip install -r requirements-dev.lock + pip install -e . + + if [ -d ./keynetra-client-python ]; then + pip install -e ./keynetra-client-python + fi - - name: Lint + - name: ๐Ÿ”„ OpenAPI drift check + run: keynetra check-openapi + + - name: ๐Ÿ—„ Migration check + run: keynetra migrate --confirm-destructive + + - name: ๐Ÿงช Run tests with coverage run: | - ruff check . - black --check . - isort --check-only . + pytest -q \ + --cov=keynetra \ + --cov-fail-under=80 \ + --cov-report=term \ + --cov-report=json + + if [ -d ./keynetra-client-python/tests ]; then + pytest -q keynetra-client-python/tests + fi + + python scripts/check_coverage.py + + + # ------------------------------- + # Stage 5: Load Test + # ------------------------------- + load-test: + name: ๐Ÿšฆ Load Smoke Test + runs-on: ubuntu-latest + needs: test + + steps: + - name: ๐Ÿ“ฅ Checkout repository + uses: actions/checkout@v4 + + - name: ๐Ÿ Setup Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip - - name: Migration check - env: - PYTHONPATH: ${{ github.workspace }} - run: python -m keynetra.cli migrate --confirm-destructive + - name: ๐Ÿ“ฆ Install dependencies + run: | + pip install -r requirements.lock + pip install locust uvicorn + + - name: ๐Ÿš€ Start API + run: | + python -m uvicorn keynetra.api.main:app --host 127.0.0.1 --port 8000 & + sleep 3 - - name: Tests and coverage - env: - PYTHONPATH: ${{ github.workspace }} + - name: โšก Run Locust run: | - python -m pytest -q --cov=keynetra --cov-fail-under=80 \ No newline at end of file + locust \ + -f locustfile.py \ + --host http://127.0.0.1:8000 \ + --headless \ + -u 10 \ + -r 2 \ + -t 20s \ + --csv /tmp/locust \ + --only-summary + + - name: ๐Ÿ“Š Validate load budget + run: python scripts/check_load_budget.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d09a355..2bc5214 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Release +name: ๐Ÿš€ Release on: push: @@ -7,36 +7,67 @@ on: permissions: contents: write + packages: write jobs: release: + name: ๐Ÿ“ฆ Python Release runs-on: ubuntu-latest env: KEYNETRA_DATABASE_URL: sqlite+pysqlite:///./.keynetra-release.db KEYNETRA_API_KEYS: testkey PYTHONUNBUFFERED: "1" steps: - - name: Checkout repository + - name: ๐Ÿ“ฅ Checkout repository uses: actions/checkout@v4 + with: + fetch-depth: 0 - - name: Set up Python 3.11 + - name: ๐Ÿ Set up Python 3.11 uses: actions/setup-python@v5 with: python-version: "3.11" + cache: "pip" - - name: Install dependencies + - name: ๐Ÿ“ฆ Install dependencies run: | python -m pip install --upgrade pip python -m pip install -r requirements.txt python -m pip install -r requirements-dev.txt + python -m pip install build twine + + - name: ๐Ÿ” Lint & Security + run: | + ruff check . + black --check . + pip-audit + safety check + + - name: โœ… Run tests & coverage + run: | + pytest -q \ + --cov=keynetra \ + --cov-fail-under=85 \ + --cov-report=term \ + --cov-report=json:coverage.json - - name: Build Python package - run: python -m build + - name: ๐Ÿ“Š Coverage Summary + if: always() + run: | + echo "## ๐Ÿ“ˆ Test Coverage" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ -f coverage.json ]; then + cat coverage.json >> $GITHUB_STEP_SUMMARY + else + echo "No coverage.json found" >> $GITHUB_STEP_SUMMARY + fi - - name: Run tests - run: pytest -q --cov=keynetra --cov-fail-under=80 + - name: ๐Ÿ—๏ธ Build Python package + run: | + python -m build + twine check dist/* - - name: Attach release artifacts + - name: ๐Ÿ“ค Upload release artifacts uses: actions/upload-artifact@v4 with: name: keynetra-release-artifacts @@ -44,14 +75,101 @@ jobs: dist/*.tar.gz dist/*.whl - - name: Publish GitHub release + - name: ๐ŸŽ‰ Publish GitHub release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} uses: softprops/action-gh-release@v2 with: name: KeyNetra ${{ github.ref_name }} body: | - Initial public release of the KeyNetra authorization engine. + # KeyNetra ${{ github.ref_name }} + + ๐Ÿš€ **New release of the KeyNetra authorization engine!** - Includes support for RBAC, ABAC, ACL, and ReBAC with a compiled authorization engine, distributed caching, policy simulation, impact analysis, and observability. + ## โœจ Features + - RBAC, ABAC, ACL, ReBAC support + - Compiled authorization engine + - Distributed caching + - Policy simulation & impact analysis + - Full observability + + ## ๐Ÿ“ฆ Artifacts + - Python wheel & sdist + - Docker images (below) + + **Changelog:** [See commits](https://github.com/keynetra/keynetra/compare/${{ github.ref_previous }}...${{ github.ref }}) files: | dist/*.tar.gz dist/*.whl + generate_release_notes: true + + docker: + name: ๐Ÿณ Docker Multi-Platform + runs-on: ubuntu-latest + needs: release + permissions: + contents: read + packages: write + strategy: + matrix: + platform: [linux/amd64, linux/arm64] + steps: + - name: ๐Ÿ“ฅ Checkout repo + uses: actions/checkout@v4 + + - name: ๐Ÿ Set up Python (for build) + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: ๐Ÿ“ฆ Install build deps + run: | + python -m pip install -r requirements.txt + + - name: โš™๏ธ Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: ๐Ÿ” Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: ๐Ÿ” Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: ๐Ÿ“‹ Extract metadata + id: meta + uses: docker/metadata-action@v6 + with: + images: | + keynetra/keynetra + ghcr.io/${{ github.repository }}/keynetra + tags: | + type=ref,event=tag + type=sha,prefix={{branch}}- + + - name: ๐Ÿณ Build & Push Multi-Platform + uses: docker/build-push-action@v6 + with: + context: . + platforms: ${{ matrix.platform }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: ๐Ÿ“Š Docker Summary + if: always() + run: | + echo "## ๐Ÿณ Docker Images Published" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Registry | Tags | Platforms |" >> $GITHUB_STEP_SUMMARY + echo "|----------|------|-----------|" >> $GITHUB_STEP_SUMMARY + echo "| Docker Hub | \`${{ steps.meta.outputs.tags }}\` | ${{ matrix.platform }} |" >> $GITHUB_STEP_SUMMARY + echo "| GHCR | \`${{ steps.meta.outputs.tags }}\` | ${{ matrix.platform }} |" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.gitignore b/.gitignore index cf73dc8..32069b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,69 @@ +# Python bytecode and caches __pycache__/ -*.pyc -.env -.venv -.vscode -.idea -dist/ +*.py[cod] +*$py.class + +# Build and packaging artifacts build/ +dist/ +site/ +*.egg-info/ +.eggs/ +pip-wheel-metadata/ + +# Virtual environments +.venv/ +venv/ +env/ +ENV/ + +# Test/coverage/type/lint caches .coverage +.coverage.* +coverage.xml htmlcov/ .pytest_cache/ -node_modules/ -.ruff_cache/ .mypy_cache/ +.ruff_cache/ +.import_linter_cache/ +.tox/ +.nox/ +coverage/ + +# Local environment files +.env +.env.local + +# Logs and temp files +*.log +*.tmp +*.swp + +# Local databases *.db *.sqlite *.sqlite3 + +# Terraform state and overrides +.terraform/ +*.tfstate +*.tfstate.* +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraform.lock.hcl + +# Editor/IDE settings +.vscode/ +.idea/ + +# OS artifacts .DS_Store -docs-site \ No newline at end of file +Thumbs.db + +# JS/build cache +node_modules/ + +# Project-specific +docs-site diff --git a/.importlinter b/.importlinter new file mode 100644 index 0000000..23608da --- /dev/null +++ b/.importlinter @@ -0,0 +1,53 @@ +[importlinter] +root_package = keynetra + +[importlinter:contract:services-no-api] +name = Services must not import API layer +type = forbidden +source_modules = + keynetra.services +forbidden_modules = + keynetra.api + +[importlinter:contract:engine-no-runtime-layers] +name = Engine must not import API, services, or infrastructure +type = forbidden +source_modules = + keynetra.engine +forbidden_modules = + keynetra.api + keynetra.services + keynetra.infrastructure + +[importlinter:contract:infrastructure-no-api-or-services] +name = Infrastructure must not import API +type = forbidden +source_modules = + keynetra.infrastructure +forbidden_modules = + keynetra.api + +[importlinter:contract:domain-no-infra] +name = Domain must not import infrastructure +type = forbidden +source_modules = + keynetra.domain +forbidden_modules = + keynetra.infrastructure + +[importlinter:contract:domain-no-api-services] +name = Domain must not import API or services +type = forbidden +source_modules = + keynetra.domain +forbidden_modules = + keynetra.api + keynetra.services + +[importlinter:contract:config-no-api-routes] +name = Config must not import API routes +type = forbidden +source_modules = + keynetra.config +forbidden_modules = + keynetra.api.routes diff --git a/.safety-policy.yml b/.safety-policy.yml new file mode 100644 index 0000000..fbdbca4 --- /dev/null +++ b/.safety-policy.yml @@ -0,0 +1,5 @@ +ignore-vulnerabilities: + 64459: + reason: "Side-channel vulnerability in ecdsa not exploitable in this project" + 64396: + reason: "ecdsa library limitation; project does not use ECDSA private key operations" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6f2125f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,31 @@ +# Code of Conduct + +## Our Commitment + +KeyNetra contributors are committed to a respectful, harassment-free environment for everyone, regardless of age, body size, disability, ethnicity, identity, experience level, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Expected Behavior + +- Be respectful and constructive. +- Assume good intent and ask clarifying questions before escalating. +- Focus feedback on code, design, and behavior, not people. +- Help new contributors onboard with clear guidance. + +## Unacceptable Behavior + +- Harassment, intimidation, or discriminatory language. +- Personal attacks, trolling, or sustained disruption. +- Sharing private information without explicit permission. +- Any conduct that is inappropriate in a professional open-source setting. + +## Enforcement + +Project maintainers are responsible for clarifying and enforcing standards. They may remove, edit, or reject contributions, comments, or issues that violate this code of conduct. + +## Reporting + +Report incidents privately to the maintainers through a security/contact channel listed in [SECURITY.md](./SECURITY.md). Reports will be reviewed promptly and handled confidentially. + +## Scope + +This code of conduct applies in all project spaces, including issues, pull requests, discussions, and community channels. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b93685a..8e90098 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ export KEYNETRA_API_KEYS=devkey Start the API locally: ```bash -python -m keynetra.cli serve +keynetra serve ``` ## Run tests diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md new file mode 100644 index 0000000..144958a --- /dev/null +++ b/DEPLOYMENT.md @@ -0,0 +1,85 @@ +# Deployment Guide + +This guide documents supported deployment paths for KeyNetra v0.1.0-beta. + +## Prerequisites + +- Python 3.11+ +- Container runtime (Docker or compatible) +- PostgreSQL 16+ (recommended for production) +- Redis 7+ (recommended for distributed cache invalidation) + +## Environment Variables + +Minimum required variables: + +- `KEYNETRA_DATABASE_URL` +- `KEYNETRA_API_KEYS` or `KEYNETRA_API_KEY_HASHES` +- `KEYNETRA_JWT_SECRET` +- `KEYNETRA_STRICT_TENANCY=true` (recommended for multi-tenant production) + +Optional: + +- `KEYNETRA_REDIS_URL` +- `KEYNETRA_RATE_LIMIT_PER_MINUTE` +- `KEYNETRA_RATE_LIMIT_BURST` +- `KEYNETRA_RUN_MIGRATIONS` +- `KEYNETRA_AUTO_SEED_SAMPLE_DATA` + +See [.env.example](./.env.example) for a complete list. + +## Docker (Single Container) + +```bash +docker build -t keynetra:test . +docker run --rm -p 8080:8080 --env-file .env keynetra:test +``` + +Health and observability endpoints: + +- `GET http://localhost:8080/health` +- `GET http://localhost:8080/docs` +- `GET http://localhost:8080/metrics` + +## Docker Compose (Full Stack) + +```bash +docker compose up --build +``` + +Services: + +- KeyNetra API +- PostgreSQL +- Redis +- Prometheus +- Grafana +- node-exporter +- Loki + +## Kubernetes Manifests + +```bash +kubectl apply -f deploy/kubernetes/ +``` + +Included resources: + +- `deployment.yaml` +- `service.yaml` +- `configmap.yaml` +- `secret.yaml` +- `ingress.yaml` +- `hpa.yaml` + +## Helm + +```bash +helm install keynetra ./deploy/helm/keynetra +``` + +Render-only validation: + +```bash +helm template keynetra deploy/helm/keynetra +``` diff --git a/Dockerfile b/Dockerfile index 499cf05..c065d5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,14 +15,16 @@ RUN pip install --no-cache-dir -r /app/requirements.txt COPY alembic.ini /app/alembic.ini COPY alembic /app/alembic COPY keynetra /app/keynetra -COPY infra/docker/start.sh /usr/local/bin/start-keynetra - -RUN chmod +x /usr/local/bin/start-keynetra && chown -R appuser:appuser /app +COPY contracts /app/contracts +COPY pyproject.toml /app/pyproject.toml +COPY README.md /app/README.md +RUN pip install --no-cache-dir /app +RUN chown -R appuser:appuser /app USER appuser -EXPOSE 8000 +EXPOSE 8080 HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=3)" + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=3)" -ENTRYPOINT ["start-keynetra"] +CMD ["keynetra", "serve", "--host", "0.0.0.0", "--port", "8080"] diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..d934a57 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,20 @@ +# Governance + +## Maintainers +- Core maintainers are responsible for release management, security triage, and architectural direction. +- Repository write access is restricted to maintainers and trusted release engineers. + +## Decision Process +- API and schema changes require a documented rationale in pull requests. +- Breaking changes require a deprecation window and changelog migration notes. +- Security-sensitive changes require at least one maintainer security review. + +## Release Cadence +- Patch releases: weekly or as needed for security/bug fixes. +- Minor releases: every 4-6 weeks. +- Emergency security releases: out-of-band. + +## Contribution Workflow +- Fork + pull request workflow. +- CI must be green (tests, lint, typing, security scans, contract checks). +- At least one maintainer approval required before merge. diff --git a/Makefile b/Makefile index 73e3a43..0840193 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,14 @@ PYTHON ?= python3.11 +VENV ?= .venv -.PHONY: install test lint format migrate run +.PHONY: install test lint format migrate run bootstrap smoke install: - $(PYTHON) -m pip install -r requirements.txt -r requirements-dev.txt + @if [ -f requirements.lock ] && [ -f requirements-dev.lock ]; then \ + $(PYTHON) -m pip install -r requirements-dev.lock; \ + else \ + $(PYTHON) -m pip install -r requirements.txt -r requirements-dev.txt; \ + fi test: $(PYTHON) -m pytest -q @@ -12,13 +17,31 @@ lint: $(PYTHON) -m ruff check . $(PYTHON) -m black --check . $(PYTHON) -m isort --check-only . + lint-imports --config .importlinter format: $(PYTHON) -m black . $(PYTHON) -m isort . migrate: - $(PYTHON) -m keynetra.cli migrate --confirm-destructive + keynetra migrate --confirm-destructive run: $(PYTHON) -m uvicorn keynetra.api.main:app --host 0.0.0.0 --port 8000 + +bootstrap: + $(PYTHON) -m venv $(VENV) + $(VENV)/bin/python -m pip install --upgrade pip + @if [ -f requirements.lock ] && [ -f requirements-dev.lock ]; then \ + $(VENV)/bin/python -m pip install -r requirements-dev.lock; \ + else \ + $(VENV)/bin/python -m pip install -r requirements.txt -r requirements-dev.txt; \ + fi + $(VENV)/bin/python -m pip install -e . + @if [ ! -f .env ]; then cp .env.example .env; fi + $(VENV)/bin/keynetra migrate --confirm-destructive + $(VENV)/bin/keynetra config doctor + $(VENV)/bin/python -m pytest -q tests/test_api_contract.py tests/test_compiled_policies.py + +smoke: + $(PYTHON) -m pytest -q tests/test_api_contract.py tests/test_compiled_policies.py tests/test_doctor.py diff --git a/README.md b/README.md index 98e040e..ce0196e 100644 --- a/README.md +++ b/README.md @@ -1,385 +1,238 @@ +

+ KeyNetra Logo +

+

+ KeyNetra animated typing banner +

- KeyNetra banner -

- CI - Python - License - OpenAPI - Docs -

+[![CI](https://github.com/keynetra/keynetra/actions/workflows/ci.yml/badge.svg)](https://github.com/keynetra/keynetra/actions/workflows/ci.yml) +[![Release](https://github.com/keynetra/keynetra/actions/workflows/release.yml/badge.svg)](https://github.com/keynetra/keynetra/actions/workflows/release.yml) +[![Docker Hub](https://img.shields.io/docker/pulls/keynetra/keynetra?label=docker%20pulls)](https://hub.docker.com/r/keynetra/keynetra) +[![Python](https://img.shields.io/badge/python-3.11%2B-blue)](./pyproject.toml) +[![License](https://img.shields.io/badge/license-Apache--2.0-green)](./LICENSE) +[![OpenAPI](https://img.shields.io/badge/OpenAPI-3.1-orange)](./contracts/openapi/openapi.json) - Typing animation
-

- Policy-driven authorization and access control engine for modern applications. -

+# KeyNetra + +Policy-driven authorization control plane for applications that need deterministic, explainable access decisions across RBAC, ACL, and ReBAC. + +## What KeyNetra Provides + +- Authorization engine with deterministic evaluation and explain traces +- FastAPI API server and operational CLI +- Multi-tenant policy evaluation with strict tenancy controls +- Policy lifecycle operations (validation, compile, simulation, impact analysis) +- Caching and access indexing for low-latency checks +- Structured logging, metrics, and dashboard-ready monitoring +- Deployment assets for Docker, Kubernetes, and Helm + +## Architecture -KeyNetra is an open-source authorization core built for teams that need Stripe/Keycloak/Casbin-level operational clarity while keeping architecture and deployment under their control. - -## Why KeyNetra - -- Deterministic evaluation pipeline with explain traces. -- Multiple authorization models in one runtime: - - RBAC - - ACL - - ReBAC - - schema-permission checks - - compiled policy graph evaluation -- Headless-first operation: - - HTTP API - - CLI - - embedded Python engine -- Production-focused defaults: - - migrations - - cache layers - - observability metrics - - Docker/Kubernetes deployment assets - -## Table Of Contents - -- [Quick Start](#quick-start) -- [Core Capabilities](#core-capabilities) -- [Usage Modes](#usage-modes) -- [Architecture](#architecture) -- [API Surface](#api-surface) -- [Configuration](#configuration) -- [Security](#security) -- [Caching and Consistency](#caching-and-consistency) -- [Observability](#observability) -- [Deployment](#deployment) -- [Development](#development) -- [Documentation](#documentation) -- [Release and Compatibility](#release-and-compatibility) -- [Citation](#citation) -- [Contributing](#contributing) -- [License](#license) - -## Quick Start - -### 1) Install +Layering is enforced through import contracts: + +- `keynetra.api` -> transport only +- `keynetra.services` -> orchestration and runtime flow +- `keynetra.engine` -> pure policy decision logic +- `keynetra.domain` -> shared models/schemas +- `keynetra.infrastructure` -> repositories, storage, cache adapters +- `keynetra.config` -> configuration loading and guardrails + +Detailed architecture notes: [`ARCHITECTURE.md`](./ARCHITECTURE.md) + +## Quick Start (Local) + +### 1) Setup ```bash python3.11 -m venv .venv source .venv/bin/activate +pip install --upgrade pip pip install -r requirements.txt -r requirements-dev.txt +pip install -e . cp .env.example .env ``` -### 2) Start API +### 2) Run API ```bash -python -m keynetra.cli serve --config examples/keynetra.yaml +keynetra serve --host 0.0.0.0 --port 8080 ``` -### 3) Verify health +### 3) Health and Docs ```bash -curl -i http://localhost:8000/health/ready +curl -i http://localhost:8080/health/ready +open http://localhost:8080/docs ``` -### 4) Run first authorization check +### 4) First Authorization Check ```bash -curl -s -X POST http://localhost:8000/check-access \ +curl -s -X POST http://localhost:8080/check-access \ -H "Content-Type: application/json" \ -H "X-API-Key: devkey" \ + -H "X-Tenant-Id: acme" \ -d '{ - "user": {"id": 1, "role": "manager"}, - "action": "approve_payment", - "resource": {"amount": 5000}, + "user": {"id": "u1", "role": "admin"}, + "action": "read", + "resource": {"resource_type": "document", "resource_id": "doc-1"}, "context": {} }' ``` -## Core Capabilities - -| Capability | Details | -| --- | --- | -| RBAC | Roles, permissions, role-permission bindings | -| ACL | Subject/resource/action-level allow/deny | -| ReBAC | Relationship tuples and index-assisted checks | -| Compiled policy graph | Deterministic policy evaluation stage | -| Auth modeling | Schema parser + validator + compiler | -| Simulation | `/simulate-policy` and `/impact-analysis` | -| Cache layers | Policy, decision, relationship, ACL, access index | -| Observability | Prometheus metrics + structured logs | -| Runtime modes | API, CLI, embedded Python | +## CLI Usage -## Usage Modes - -### API Server Mode +Entrypoint is standardized to `keynetra`: ```bash -python -m keynetra.cli serve --config examples/keynetra.yaml +keynetra --help +keynetra check-openapi +keynetra migrate --confirm-destructive +keynetra doctor --service core ``` -### CLI Mode +## API Surface (Core) -```bash -python -m keynetra.cli help-cli -python -m keynetra.cli check --config examples/keynetra.yaml --api-key devkey --action read --user '{"id":"u1"}' --resource '{"resource_type":"document","resource_id":"doc-1"}' -python -m keynetra.cli compile-policies --config examples/keynetra.yaml -python -m keynetra.cli doctor --service core --config examples/keynetra.yaml -``` +- `POST /check-access` +- `POST /check-access-batch` +- `POST /simulate` +- `POST /simulate-policy` +- `POST /impact-analysis` +- `GET /health`, `GET /health/ready`, `GET /metrics` -### Embedded Python Mode +OpenAPI contracts: -```python -from keynetra import KeyNetra +- [`contracts/openapi/openapi.json`](./contracts/openapi/openapi.json) +- [`contracts/openapi/keynetra-v0.1.0.yaml`](./contracts/openapi/keynetra-v0.1.0.yaml) -engine = KeyNetra.from_config("examples/keynetra.yaml") -engine.load_policies("examples/policies") -engine.load_model("examples/auth-model.yaml") +## Multi-Tenant and Security -decision = engine.check_access( - subject="user:1", - action="read", - resource="document:abc", - context={}, -) -print(decision.allowed) -``` +- Tenant-aware request flow and storage isolation +- Strict tenancy mode available via `KEYNETRA_STRICT_TENANCY=true` +- API key and JWT auth support +- Admin auth flow for privileged operations +- Rate limiting and request correlation IDs -### Pure Engine Import +See [`SECURITY.md`](./SECURITY.md) for security policy and reporting. -```python -from keynetra.engine import KeyNetraEngine +## Observability and Monitoring -engine = KeyNetraEngine( - [{"action": "read", "effect": "allow", "priority": 1, "conditions": {}}] -) -decision = engine.check_access( - subject="user:123", - action="read", - resource="document:abc", - context={}, -) -print(decision.allowed) -``` +KeyNetra exposes Prometheus metrics at `GET /metrics` including: -## Architecture +- HTTP request count/latency/error metrics +- Authorization decision and stage latency metrics +- Cache hit/miss metrics +- DB query latency metrics +- Tenant activity dimensions -Layered boundaries: +Monitoring assets: -- `keynetra/engine`: deterministic decision logic only -- `keynetra/services`: orchestration, hydration, consistency handling -- `keynetra/infrastructure`: DB/cache/repository side effects -- `keynetra/api`: transport, middleware, and route wiring +- Prometheus config: [`monitoring/prometheus/prometheus.yml`](./monitoring/prometheus/prometheus.yml) +- Grafana dashboard: [`monitoring/grafana/dashboards/keynetra-overview.json`](./monitoring/grafana/dashboards/keynetra-overview.json) +- Grafana provisioning: [`monitoring/grafana/provisioning`](./monitoring/grafana/provisioning) -```mermaid -flowchart LR - A[Request] --> B[AuthorizationService] - B --> C[KeyNetraEngine] - C --> D[Decision + Explain Trace] - B --> E[(Decision Cache)] - B --> F[(Audit Log)] -``` +## Deployment -Engine evaluation order: - -1. direct user permissions -2. ACL checks -3. RBAC role permissions -4. relationship index checks -5. schema permission checks -6. compiled policy graph checks -7. default deny - -## API Surface - -OpenAPI contract: [`contracts/openapi/keynetra-v0.1.0.yaml`](./contracts/openapi/keynetra-v0.1.0.yaml) - -Key endpoints: - -- Decisions: - - `POST /check-access` - - `POST /check-access-batch` - - `POST /simulate` -- Modeling: - - `POST /auth-model` - - `GET /auth-model` -- ACL: - - `POST /acl` - - `GET /acl/{resource_type}/{resource_id}` - - `DELETE /acl/{acl_id}` -- Simulation: - - `POST /simulate-policy` - - `POST /impact-analysis` -- Health and metrics: - - `GET /health` - - `GET /health/live` - - `GET /health/ready` - - `GET /metrics` -- Admin auth: - - `POST /admin/login` - -## Configuration - -KeyNetra supports YAML, JSON, and TOML config files: +### Docker ```bash -python -m keynetra.cli serve --config examples/keynetra.yaml +docker build -t keynetra:test . +docker run --rm -p 8080:8080 --env-file .env keynetra:test ``` -Example (`examples/keynetra.yaml`): - -```yaml -database: - url: sqlite+pysqlite:///./keynetra.db - -redis: - url: redis://localhost:6379/0 - -policies: - path: ./examples/policies - -models: - path: ./examples/auth-model.yaml +### Docker Compose (Full Dev/Obs Stack) -seed_data: true - -server: - host: 0.0.0.0 - port: 8080 +```bash +docker compose up --build ``` -Policy/model file support: - -- policies: `.yaml`, `.json`, `.polar` -- auth models: `.yaml`, `.json`, `.toml` (plus raw schema/text) - -## Security +Includes: -- API key auth (`X-API-Key`) -- JWT bearer auth -- admin login endpoint (`/admin/login`) -- management role enforcement (`viewer`, `developer`, `admin`) -- idempotency middleware for write safety -- API version negotiation (`X-API-Version`) - -For disclosure policy, see [`SECURITY.md`](./SECURITY.md). - -## Caching and Consistency - -Cache layers: - -- policy cache -- decision cache -- relationship cache -- ACL cache -- access-index cache - -Distribution and invalidation: - -- Redis backend with in-memory fallback -- namespace bump invalidation strategy -- policy distribution via Redis Pub/Sub - -## Observability - -- Prometheus metrics at `GET /metrics` -- structured logging (JSON) and rich colored logs -- explain traces and audit records for decision transparency - -Docker monitoring stack includes: - -- Prometheus: `http://localhost:9090` -- Grafana: `http://localhost:3000` - -## Deployment +- KeyNetra API +- PostgreSQL +- Redis +- Prometheus +- Grafana +- node-exporter +- Loki -### Docker Compose (default) +### Kubernetes ```bash -docker compose up --build +kubectl apply -f deploy/kubernetes/ ``` -### Docker Compose (development) +### Helm ```bash -docker compose -f docker-compose.dev.yml up --build +helm install keynetra ./deploy/helm/keynetra ``` -Services included in stack: +More deployment detail: [`DEPLOYMENT.md`](./DEPLOYMENT.md) -- KeyNetra API -- PostgreSQL -- Redis -- Prometheus -- Grafana +## SDKs -Kubernetes baseline: +SDKs are maintained separately from this engine repository. -- Helm chart at `infra/k8s/helm/keynetra` +- Python SDK package: `keynetra-client` +- SDK guide: [`SDK_GUIDE.md`](./SDK_GUIDE.md) -## Development +Example (Python SDK): -```bash -make install -make lint -make test -make migrate -make run +```python +from keynetra_client import KeyNetraClient + +client = KeyNetraClient("http://localhost:8080") +decision = client.check_access( + user={"id": "alice"}, + action="read", + resource={"type": "document", "id": "doc-1"}, +) +print(decision.allowed) ``` -Policy and diagnostics: +## Developer Workflow ```bash -python -m keynetra.cli test-policy examples/policy_tests.yaml -python -m keynetra.cli explain --user u1 --resource r1 --action read -python -m keynetra.cli benchmark --api-key devkey +ruff check . +black --check . +pytest +lint-imports --config .importlinter ``` -## Documentation +Convenience commands are available in [`Makefile`](./Makefile). -- docs index: [`docs/README.md`](./docs/README.md) -- architecture notes: [`architecture.md`](./architecture.md) -- Docusaurus site app: [`docs-site/`](./docs-site/) -- sidebar config: [`docs-site/sidebars.ts`](./docs-site/sidebars.ts) -- Docusaurus config: [`docs-site/docusaurus.config.ts`](./docs-site/docusaurus.config.ts) +## Documentation Index -## Release and Compatibility +- [`ARCHITECTURE.md`](./ARCHITECTURE.md) +- [`DEPLOYMENT.md`](./DEPLOYMENT.md) +- [`SDK_GUIDE.md`](./SDK_GUIDE.md) +- [`CONTRIBUTING.md`](./CONTRIBUTING.md) +- [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md) +- [`SECURITY.md`](./SECURITY.md) +- [`CHANGELOG.md`](./CHANGELOG.md) +- [`docs/README.md`](./docs/README.md) -Current version: `0.1.0` +## Contributing -- package version: [`pyproject.toml`](./pyproject.toml) -- runtime version: [`keynetra/version.py`](./keynetra/version.py) -- release notes: [`CHANGELOG.md`](./CHANGELOG.md) +Contributions are welcome. Start with [`CONTRIBUTING.md`](./CONTRIBUTING.md) and [`CODE_OF_CONDUCT.md`](./CODE_OF_CONDUCT.md). -Compatibility: +## License -- Python `3.11+` -- DB: PostgreSQL, SQLite -- Cache: Redis optional -- Deployment: Docker Compose, Helm baseline +Apache-2.0. See [`LICENSE`](./LICENSE). ## Citation ```bibtex -@software{keynetra_v0_1_0, - title = {KeyNetra: Policy-driven Authorization and Access Control Engine}, +@software{keynetra_2026, + title = {KeyNetra}, author = {KeyNetra Community}, year = {2026}, - version = {0.1.0}, - url = {https://github.com/keynetra/keynetra-core} + version = {0.1.0-beta}, + url = {https://github.com/keynetra/keynetra} } ``` - -## Contributing - -Contributions are welcome. - -- contribution guide: [`CONTRIBUTING.md`](./CONTRIBUTING.md) -- security policy: [`SECURITY.md`](./SECURITY.md) - -## License - -Apache License 2.0. See [`LICENSE`](./LICENSE). - ---- - -

- Made with love for the KeyNetra Community. -

+

Made with love โค๏ธ for KeyNetra Community.

\ No newline at end of file diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..dc3dfec --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,16 @@ +# Roadmap + +## 2026 H1 +- Harden production defaults and configuration safety checks. +- Expand CI quality gates: typing, security scans, contract drift, load smoke budgets. +- Introduce policy lifecycle states (`draft`, `active`, `archived`) with canary evaluation. + +## 2026 H2 +- Add first-class OpenFGA and OPA/Rego adapters. +- Publish Terraform provider for policy resources. +- Expand async data-path options for higher-concurrency deployments. + +## Backlog +- Deeper mutation testing strategy for policy/rule evaluation. +- Per-tenant async worker model and queue-based authorization batch processing. +- Additional policy authoring UX and governance tooling. diff --git a/SDK_GUIDE.md b/SDK_GUIDE.md new file mode 100644 index 0000000..a4f65e9 --- /dev/null +++ b/SDK_GUIDE.md @@ -0,0 +1,44 @@ +# SDK Guide + +KeyNetra SDKs are versioned independently from the core engine. + +## Repository Scope + +This repository contains the authorization engine, API server, CLI, and deployment assets. + +SDK implementations are maintained in separate repositories and released on their own cadence. + +## Python SDK + +- Package: `keynetra-client` +- Install: `pip install keynetra-client` + +Basic usage: + +```python +from keynetra_client import KeyNetraClient + +client = KeyNetraClient("http://localhost:8080") +decision = client.check_access( + user={"id": "alice"}, + action="read", + resource={"type": "document", "id": "doc-1"}, +) +print(decision.allowed) +``` + +## API Contract Compatibility + +- Server OpenAPI contract: `contracts/openapi/openapi.json` +- Validate API consistency from this repository: + +```bash +keynetra check-openapi +``` + +## Planned SDKs + +- Python +- TypeScript +- Go +- Java diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000..c683a86 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,18 @@ +# Support + +## Triage SLA +- Security reports: acknowledge within 24 hours. +- Bug reports: acknowledge within 3 business days. +- Feature requests: acknowledge within 5 business days. + +## Support Channels +- GitHub Issues for bugs and feature requests. +- Security issues via the disclosure process in `SECURITY.md`. + +## Release Support Policy +- Latest minor release is fully supported. +- Previous minor release receives security and critical bug fixes for 60 days. + +## Incident Escalation +- Production-impacting authorization regressions are prioritized as P0. +- Maintainers publish mitigation guidance and recovery steps in issue updates and release notes. diff --git a/alembic/versions/20260404_000001_init.py b/alembic/versions/20260404_000001_init.py deleted file mode 100644 index d9e4e77..0000000 --- a/alembic/versions/20260404_000001_init.py +++ /dev/null @@ -1,66 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260404_000001" -down_revision = None -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "users", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("external_id", sa.String(length=128), nullable=True), - ) - op.create_index("ix_users_external_id", "users", ["external_id"], unique=False) - - op.create_table( - "roles", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("name", sa.String(length=64), nullable=False, unique=True), - ) - - op.create_table( - "permissions", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("action", sa.String(length=128), nullable=False), - sa.UniqueConstraint("action", name="uq_permissions_action"), - ) - - op.create_table( - "user_roles", - sa.Column( - "user_id", sa.Integer(), sa.ForeignKey("users.id", ondelete="CASCADE"), primary_key=True - ), - sa.Column( - "role_id", sa.Integer(), sa.ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True - ), - ) - - op.create_table( - "role_permissions", - sa.Column( - "role_id", sa.Integer(), sa.ForeignKey("roles.id", ondelete="CASCADE"), primary_key=True - ), - sa.Column( - "permission_id", - sa.Integer(), - sa.ForeignKey("permissions.id", ondelete="CASCADE"), - primary_key=True, - ), - ) - - # policies are created in 20260404_000002 (versioned policy schema) - - -def downgrade() -> None: - op.drop_table("role_permissions") - op.drop_table("user_roles") - op.drop_table("permissions") - op.drop_table("roles") - op.drop_index("ix_users_external_id", table_name="users") - op.drop_table("users") diff --git a/alembic/versions/20260404_000002_tenants_versioning_audit.py b/alembic/versions/20260404_000002_tenants_versioning_audit.py deleted file mode 100644 index b5f33ca..0000000 --- a/alembic/versions/20260404_000002_tenants_versioning_audit.py +++ /dev/null @@ -1,99 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260404_000002" -down_revision = "20260404_000001" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "tenants", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("tenant_key", sa.String(length=64), nullable=False, unique=True), - ) - - op.create_table( - "audit_logs", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("principal_type", sa.String(length=32), nullable=False), - sa.Column("principal_id", sa.String(length=128), nullable=False), - sa.Column("user", sa.JSON(), nullable=False, server_default=sa.text("'{}'")), - sa.Column("action", sa.String(length=128), nullable=False), - sa.Column("resource", sa.JSON(), nullable=False, server_default=sa.text("'{}'")), - sa.Column("decision", sa.String(length=8), nullable=False), - sa.Column("matched_policies", sa.JSON(), nullable=False, server_default=sa.text("'[]'")), - sa.Column("reason", sa.String(length=256), nullable=True), - sa.Column( - "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() - ), - ) - op.create_index("ix_audit_logs_tenant_id", "audit_logs", ["tenant_id"], unique=False) - - op.create_table( - "policies", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("policy_key", sa.String(length=64), nullable=False), - sa.Column("current_version", sa.Integer(), nullable=False, server_default="1"), - sa.UniqueConstraint("tenant_id", "policy_key", name="uq_policies_tenant_key"), - ) - op.create_index("ix_policies_tenant_id", "policies", ["tenant_id"], unique=False) - - op.create_table( - "policy_versions", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column( - "policy_id", - sa.Integer(), - sa.ForeignKey("policies.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("version", sa.Integer(), nullable=False), - sa.Column("action", sa.String(length=128), nullable=False), - sa.Column("effect", sa.String(length=16), nullable=False, server_default="deny"), - sa.Column("priority", sa.Integer(), nullable=False, server_default="100"), - sa.Column("conditions", sa.JSON(), nullable=False, server_default=sa.text("'{}'")), - sa.Column( - "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() - ), - sa.Column("created_by", sa.String(length=128), nullable=True), - sa.UniqueConstraint("policy_id", "version", name="uq_policy_versions_policy_version"), - ) - op.create_index( - "ix_policy_versions_tenant_action_priority", - "policy_versions", - ["tenant_id", "action", "priority"], - unique=False, - ) - - -def downgrade() -> None: - op.drop_index("ix_policy_versions_tenant_action_priority", table_name="policy_versions") - op.drop_table("policy_versions") - op.drop_index("ix_policies_tenant_id", table_name="policies") - op.drop_table("policies") - op.drop_index("ix_audit_logs_tenant_id", table_name="audit_logs") - op.drop_table("audit_logs") - op.drop_table("tenants") diff --git a/alembic/versions/20260404_000003_tenant_policy_version.py b/alembic/versions/20260404_000003_tenant_policy_version.py deleted file mode 100644 index 0c7c3ab..0000000 --- a/alembic/versions/20260404_000003_tenant_policy_version.py +++ /dev/null @@ -1,20 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260404_000003" -down_revision = "20260404_000002" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.add_column( - "tenants", sa.Column("policy_version", sa.Integer(), nullable=False, server_default="1") - ) - - -def downgrade() -> None: - op.drop_column("tenants", "policy_version") diff --git a/alembic/versions/20260404_000004_relationships.py b/alembic/versions/20260404_000004_relationships.py deleted file mode 100644 index 0a31fbc..0000000 --- a/alembic/versions/20260404_000004_relationships.py +++ /dev/null @@ -1,50 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260404_000004" -down_revision = "20260404_000003" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "relationships", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("subject_type", sa.String(length=32), nullable=False), - sa.Column("subject_id", sa.String(length=128), nullable=False), - sa.Column("relation", sa.String(length=64), nullable=False), - sa.Column("object_type", sa.String(length=32), nullable=False), - sa.Column("object_id", sa.String(length=128), nullable=False), - sa.UniqueConstraint( - "tenant_id", - "subject_type", - "subject_id", - "relation", - "object_type", - "object_id", - name="uq_relationships_tuple", - ), - ) - op.create_index( - "ix_relationships_lookup", - "relationships", - ["tenant_id", "subject_type", "subject_id", "relation"], - unique=False, - ) - op.create_index("ix_relationships_tenant_id", "relationships", ["tenant_id"], unique=False) - - -def downgrade() -> None: - op.drop_index("ix_relationships_tenant_id", table_name="relationships") - op.drop_index("ix_relationships_lookup", table_name="relationships") - op.drop_table("relationships") diff --git a/alembic/versions/20260404_000005_audit_explainability.py b/alembic/versions/20260404_000005_audit_explainability.py deleted file mode 100644 index b744a42..0000000 --- a/alembic/versions/20260404_000005_audit_explainability.py +++ /dev/null @@ -1,26 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260404_000005" -down_revision = "20260404_000004" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.add_column( - "audit_logs", - sa.Column("evaluated_rules", sa.JSON(), nullable=False, server_default=sa.text("'[]'")), - ) - op.add_column( - "audit_logs", - sa.Column("failed_conditions", sa.JSON(), nullable=False, server_default=sa.text("'[]'")), - ) - - -def downgrade() -> None: - op.drop_column("audit_logs", "failed_conditions") - op.drop_column("audit_logs", "evaluated_rules") diff --git a/alembic/versions/20260405_000006_idempotency.py b/alembic/versions/20260405_000006_idempotency.py deleted file mode 100644 index e6629a3..0000000 --- a/alembic/versions/20260405_000006_idempotency.py +++ /dev/null @@ -1,32 +0,0 @@ -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260405_000006" -down_revision = "20260404_000005" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "idempotency_records", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column("scope", sa.String(length=256), nullable=False), - sa.Column("idempotency_key", sa.String(length=128), nullable=False), - sa.Column("request_hash", sa.String(length=64), nullable=False), - sa.Column("response_status_code", sa.Integer(), nullable=True), - sa.Column("response_body", sa.Text(), nullable=True), - sa.Column("response_content_type", sa.String(length=128), nullable=True), - sa.Column( - "created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now() - ), - sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), - sa.UniqueConstraint("scope", "idempotency_key", name="uq_idempotency_records_scope_key"), - ) - - -def downgrade() -> None: - op.drop_table("idempotency_records") diff --git a/alembic/versions/20260405_000007_resource_acl.py b/alembic/versions/20260405_000007_resource_acl.py deleted file mode 100644 index 73902e7..0000000 --- a/alembic/versions/20260405_000007_resource_acl.py +++ /dev/null @@ -1,52 +0,0 @@ -"""add resource acl table - -Revision ID: 20260405_000007 -Revises: 20260405_000006 -Create Date: 2026-04-05 00:07:00.000000 -""" - -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -# revision identifiers, used by Alembic. -revision = "20260405_000007" -down_revision = "20260405_000006" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.create_table( - "resource_acl", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - ), - sa.Column("subject_type", sa.String(length=32), nullable=False), - sa.Column("subject_id", sa.String(length=128), nullable=False), - sa.Column("resource_type", sa.String(length=64), nullable=False), - sa.Column("resource_id", sa.String(length=128), nullable=False), - sa.Column("action", sa.String(length=128), nullable=False), - sa.Column("effect", sa.String(length=16), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=False), - ) - op.create_index( - "ix_resource_acl_lookup", - "resource_acl", - ["tenant_id", "resource_type", "resource_id", "action"], - ) - op.create_index( - "ix_resource_acl_subject", "resource_acl", ["tenant_id", "subject_type", "subject_id"] - ) - - -def downgrade() -> None: - op.drop_index("ix_resource_acl_subject", table_name="resource_acl") - op.drop_index("ix_resource_acl_lookup", table_name="resource_acl") - op.drop_table("resource_acl") diff --git a/alembic/versions/20260405_000008_auth_model_revision.py b/alembic/versions/20260405_000008_auth_model_revision.py deleted file mode 100644 index fe9fb8a..0000000 --- a/alembic/versions/20260405_000008_auth_model_revision.py +++ /dev/null @@ -1,46 +0,0 @@ -"""add auth model storage and authorization revision - -Revision ID: 20260405_000008 -Revises: 20260405_000007 -Create Date: 2026-04-05 00:08:00.000000 -""" - -from __future__ import annotations - -import sqlalchemy as sa - -from alembic import op - -revision = "20260405_000008" -down_revision = "20260405_000007" -branch_labels = None -depends_on = None - - -def upgrade() -> None: - op.add_column( - "tenants", - sa.Column("authorization_revision", sa.Integer(), nullable=False, server_default="1"), - ) - op.create_table( - "auth_models", - sa.Column("id", sa.Integer(), primary_key=True), - sa.Column( - "tenant_id", - sa.Integer(), - sa.ForeignKey("tenants.id", ondelete="CASCADE"), - nullable=False, - unique=True, - ), - sa.Column("schema_text", sa.Text(), nullable=False), - sa.Column("schema_json", sa.JSON(), nullable=False), - sa.Column("compiled_json", sa.JSON(), nullable=False), - sa.Column("created_at", sa.DateTime(), nullable=False), - sa.Column("updated_at", sa.DateTime(), nullable=False), - sa.UniqueConstraint("tenant_id", name="uq_auth_models_tenant"), - ) - - -def downgrade() -> None: - op.drop_table("auth_models") - op.drop_column("tenants", "authorization_revision") diff --git a/alembic/versions/20260407_000001_initial_schema_v0.py b/alembic/versions/20260407_000001_initial_schema_v0.py new file mode 100644 index 0000000..5dac529 --- /dev/null +++ b/alembic/versions/20260407_000001_initial_schema_v0.py @@ -0,0 +1,36 @@ +"""initial_schema_v0 + +Revision ID: 20260407_000001 +Revises: +Create Date: 2026-04-07 +""" + +from __future__ import annotations + +from alembic import op + +# Ensure all models are registered on Base metadata. +from keynetra.domain.models import acl as _acl # noqa: F401 +from keynetra.domain.models import audit as _audit # noqa: F401 +from keynetra.domain.models import auth_model as _auth_model # noqa: F401 +from keynetra.domain.models import idempotency as _idempotency # noqa: F401 +from keynetra.domain.models import policy_versioning as _policy_versioning # noqa: F401 +from keynetra.domain.models import rbac as _rbac # noqa: F401 +from keynetra.domain.models import relationship as _relationship # noqa: F401 +from keynetra.domain.models import tenant as _tenant # noqa: F401 +from keynetra.domain.models.base import Base + +revision = "20260407_000001" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + bind = op.get_bind() + Base.metadata.create_all(bind=bind) + + +def downgrade() -> None: + bind = op.get_bind() + Base.metadata.drop_all(bind=bind) diff --git a/contracts/openapi/keynetra-v0.1.0.yaml b/contracts/openapi/keynetra-v0.1.0.yaml index 9f388c0..da568a6 100644 --- a/contracts/openapi/keynetra-v0.1.0.yaml +++ b/contracts/openapi/keynetra-v0.1.0.yaml @@ -48,12 +48,23 @@ paths: - access summary: Check Access operationId: check_access_check_access_post + security: + - HTTPBearer: [] + - APIKeyHeader: [] + parameters: + - name: policy_set + in: query + required: false + schema: + type: string + default: active + title: Policy Set requestBody: + required: true content: application/json: schema: $ref: '#/components/schemas/AccessRequest' - required: true responses: '200': description: Successful Response @@ -67,9 +78,6 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - security: - - HTTPBearer: [] - - APIKeyHeader: [] /simulate: post: tags: @@ -104,12 +112,23 @@ paths: - access summary: Check Access Batch operationId: check_access_batch_check_access_batch_post + security: + - HTTPBearer: [] + - APIKeyHeader: [] + parameters: + - name: policy_set + in: query + required: false + schema: + type: string + default: active + title: Policy Set requestBody: + required: true content: application/json: schema: $ref: '#/components/schemas/BatchAccessRequest' - required: true responses: '200': description: Successful Response @@ -123,9 +142,6 @@ paths: application/json: schema: $ref: '#/components/schemas/HTTPValidationError' - security: - - HTTPBearer: [] - - APIKeyHeader: [] /admin/login: post: tags: @@ -1338,6 +1354,11 @@ components: principal_id: type: string title: Principal Id + correlation_id: + anyOf: + - type: string + - type: 'null' + title: Correlation Id user: additionalProperties: true type: object @@ -1653,6 +1674,10 @@ components: type: integer title: Priority default: 100 + state: + type: string + title: State + default: active conditions: additionalProperties: true type: object @@ -1675,6 +1700,10 @@ components: priority: type: integer title: Priority + state: + type: string + title: State + default: active conditions: additionalProperties: true type: object diff --git a/contracts/openapi/openapi.json b/contracts/openapi/openapi.json new file mode 100644 index 0000000..49856dd --- /dev/null +++ b/contracts/openapi/openapi.json @@ -0,0 +1,3587 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "KeyNetra", + "version": "0.1.0" + }, + "paths": { + "/health": { + "get": { + "tags": [ + "health" + ], + "summary": "Health", + "operationId": "health_health_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__str__" + } + } + } + } + } + } + }, + "/health/live": { + "get": { + "tags": [ + "health" + ], + "summary": "Liveness", + "operationId": "liveness_health_live_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__str__" + } + } + } + } + } + } + }, + "/health/ready": { + "get": { + "tags": [ + "health" + ], + "summary": "Readiness", + "operationId": "readiness_health_ready_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__object__" + } + } + } + } + } + } + }, + "/check-access": { + "post": { + "tags": [ + "access" + ], + "summary": "Check Access", + "operationId": "check_access_check_access_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "policy_set", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "active", + "title": "Policy Set" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_AccessDecisionResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/simulate": { + "post": { + "tags": [ + "access" + ], + "summary": "Simulate", + "operationId": "simulate_simulate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AccessRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_SimulationResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/check-access-batch": { + "post": { + "tags": [ + "access" + ], + "summary": "Check Access Batch", + "operationId": "check_access_batch_check_access_batch_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "policy_set", + "in": "query", + "required": false, + "schema": { + "type": "string", + "default": "active", + "title": "Policy Set" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BatchAccessRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_BatchAccessResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/admin/login": { + "post": { + "tags": [ + "auth", + "auth" + ], + "summary": "Admin Login", + "operationId": "admin_login_admin_login_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AdminLoginRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_AdminLoginResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/policies": { + "get": { + "tags": [ + "management" + ], + "summary": "List Policies", + "operationId": "list_policies_policies_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_PolicyOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "management" + ], + "summary": "Create Policy", + "operationId": "create_policy_policies_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_PolicyOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/policies/{policy_key}": { + "put": { + "tags": [ + "management" + ], + "summary": "Update Policy", + "operationId": "update_policy_policies__policy_key__put", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "policy_key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Policy Key" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicyCreate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_PolicyOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "management" + ], + "summary": "Delete Policy", + "operationId": "delete_policy_policies__policy_key__delete", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "policy_key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Policy Key" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__str__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/policies/dsl": { + "post": { + "tags": [ + "management" + ], + "summary": "Create Policy From Dsl", + "operationId": "create_policy_from_dsl_policies_dsl_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "dsl", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Dsl" + } + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_PolicyOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/policies/{policy_key}/rollback/{version}": { + "post": { + "tags": [ + "management" + ], + "summary": "Rollback Policy", + "operationId": "rollback_policy_policies__policy_key__rollback__version__post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "policy_key", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Policy Key" + } + }, + { + "name": "version", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Version" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__Union_int__str___" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/acl": { + "post": { + "tags": [ + "management" + ], + "summary": "Create Acl Entry", + "operationId": "create_acl_entry_acl_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ACLCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_ACLOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/acl/{resource_type}/{resource_id}": { + "get": { + "tags": [ + "management" + ], + "summary": "List Acl Entries", + "operationId": "list_acl_entries_acl__resource_type___resource_id__get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "resource_type", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Resource Type" + } + }, + { + "name": "resource_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Resource Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_ACLOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/acl/{acl_id}": { + "delete": { + "tags": [ + "management" + ], + "summary": "Delete Acl Entry", + "operationId": "delete_acl_entry_acl__acl_id__delete", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "acl_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Acl Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__int__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/auth-model": { + "get": { + "tags": [ + "management" + ], + "summary": "Get Auth Model", + "operationId": "get_auth_model_auth_model_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_AuthModelOut_" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + }, + "post": { + "tags": [ + "management" + ], + "summary": "Create Auth Model", + "operationId": "create_auth_model_auth_model_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthModelCreate" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_AuthModelOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/simulate-policy": { + "post": { + "tags": [ + "management" + ], + "summary": "Simulate Policy", + "operationId": "simulate_policy_simulate_policy_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PolicySimulationRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_PolicySimulationResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/impact-analysis": { + "post": { + "tags": [ + "management" + ], + "summary": "Impact Analysis", + "operationId": "impact_analysis_impact_analysis_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ImpactAnalysisRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_ImpactAnalysisResponse_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/roles": { + "get": { + "tags": [ + "management" + ], + "summary": "List Roles", + "operationId": "list_roles_roles_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_RoleOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "management" + ], + "summary": "Create Role", + "operationId": "create_role_roles_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/roles/{role_id}": { + "put": { + "tags": [ + "management" + ], + "summary": "Update Role", + "operationId": "update_role_roles__role_id__put", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Role Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RoleOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "management" + ], + "summary": "Delete Role", + "operationId": "delete_role_roles__role_id__delete", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Role Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__int__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/roles/{role_id}/permissions": { + "get": { + "tags": [ + "management" + ], + "summary": "List Role Permissions", + "operationId": "list_role_permissions_roles__role_id__permissions_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Role Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_PermissionOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/roles/{role_id}/permissions/{permission_id}": { + "post": { + "tags": [ + "management" + ], + "summary": "Add Permission To Role", + "operationId": "add_permission_to_role_roles__role_id__permissions__permission_id__post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Role Id" + } + }, + { + "name": "permission_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Permission Id" + } + } + ], + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_PermissionOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "management" + ], + "summary": "Remove Permission From Role", + "operationId": "remove_permission_from_role_roles__role_id__permissions__permission_id__delete", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "role_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Role Id" + } + }, + { + "name": "permission_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Permission Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__int__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/permissions": { + "get": { + "tags": [ + "management" + ], + "summary": "List Permissions", + "operationId": "list_permissions_permissions_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_PermissionOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "management" + ], + "summary": "Create Permission", + "operationId": "create_permission_permissions_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/permissions/{permission_id}": { + "put": { + "tags": [ + "management" + ], + "summary": "Update Permission", + "operationId": "update_permission_permissions__permission_id__put", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "permission_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Permission Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionUpdate" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PermissionOut" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "management" + ], + "summary": "Delete Permission", + "operationId": "delete_permission_permissions__permission_id__delete", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "permission_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Permission Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__int__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/permissions/{permission_id}/roles": { + "get": { + "tags": [ + "management" + ], + "summary": "List Permission Roles", + "operationId": "list_permission_roles_permissions__permission_id__roles_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "permission_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Permission Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_RoleOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/relationships": { + "get": { + "tags": [ + "management" + ], + "summary": "List Relationships", + "operationId": "list_relationships_relationships_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "subject_type", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Subject Type" + } + }, + { + "name": "subject_id", + "in": "query", + "required": true, + "schema": { + "type": "string", + "title": "Subject Id" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_dict_str__str___" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "post": { + "tags": [ + "management" + ], + "summary": "Create Relationship", + "operationId": "create_relationship_relationships_post", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RelationshipCreate" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_RelationshipOut_" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/audit": { + "get": { + "tags": [ + "management" + ], + "summary": "List Audit Logs", + "operationId": "list_audit_logs_audit_get", + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "default": 50, + "title": "Limit" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Cursor" + } + }, + { + "name": "user_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "User Id" + } + }, + { + "name": "resource_id", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Resource Id" + } + }, + { + "name": "decision", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Decision" + } + }, + { + "name": "start_time", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Start Time" + } + }, + { + "name": "end_time", + "in": "query", + "required": false, + "schema": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "End Time" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_list_AuditRecordOut__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/playground/evaluate": { + "post": { + "tags": [ + "playground" + ], + "summary": "Evaluate", + "operationId": "evaluate_playground_evaluate_post", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaygroundEvaluateRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__Any__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + }, + "security": [ + { + "HTTPBearer": [] + }, + { + "APIKeyHeader": [] + } + ] + } + }, + "/dev/sample-data": { + "get": { + "tags": [ + "dev" + ], + "summary": "Get Sample Data", + "operationId": "get_sample_data_dev_sample_data_get", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__object__" + } + } + } + } + } + } + }, + "/dev/sample-data/seed": { + "post": { + "tags": [ + "dev" + ], + "summary": "Seed Sample Data", + "operationId": "seed_sample_data_dev_sample_data_seed_post", + "parameters": [ + { + "name": "reset", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "description": "Clear the sample dataset before reseeding it.", + "default": false, + "title": "Reset" + }, + "description": "Clear the sample dataset before reseeding it." + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SuccessResponse_dict_str__object__" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "ACLCreate": { + "properties": { + "subject_type": { + "type": "string", + "title": "Subject Type" + }, + "subject_id": { + "type": "string", + "title": "Subject Id" + }, + "resource_type": { + "type": "string", + "title": "Resource Type" + }, + "resource_id": { + "type": "string", + "title": "Resource Id" + }, + "action": { + "type": "string", + "title": "Action" + }, + "effect": { + "type": "string", + "title": "Effect" + } + }, + "type": "object", + "required": [ + "subject_type", + "subject_id", + "resource_type", + "resource_id", + "action", + "effect" + ], + "title": "ACLCreate" + }, + "ACLOut": { + "properties": { + "subject_type": { + "type": "string", + "title": "Subject Type" + }, + "subject_id": { + "type": "string", + "title": "Subject Id" + }, + "resource_type": { + "type": "string", + "title": "Resource Type" + }, + "resource_id": { + "type": "string", + "title": "Resource Id" + }, + "action": { + "type": "string", + "title": "Action" + }, + "effect": { + "type": "string", + "title": "Effect" + }, + "id": { + "type": "integer", + "title": "Id" + }, + "tenant_id": { + "type": "integer", + "title": "Tenant Id" + }, + "created_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Created At" + } + }, + "type": "object", + "required": [ + "subject_type", + "subject_id", + "resource_type", + "resource_id", + "action", + "effect", + "id", + "tenant_id" + ], + "title": "ACLOut" + }, + "AccessDecisionResponse": { + "properties": { + "allowed": { + "type": "boolean", + "title": "Allowed" + }, + "decision": { + "type": "string", + "title": "Decision" + }, + "matched_policies": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Matched Policies" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "policy_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Policy Id" + }, + "explain_trace": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Explain Trace" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "allowed", + "decision" + ], + "title": "AccessDecisionResponse" + }, + "AccessRequest": { + "properties": { + "user": { + "additionalProperties": true, + "type": "object", + "title": "User" + }, + "action": { + "type": "string", + "title": "Action" + }, + "resource": { + "additionalProperties": true, + "type": "object", + "title": "Resource" + }, + "context": { + "additionalProperties": true, + "type": "object", + "title": "Context" + }, + "consistency": { + "type": "string", + "title": "Consistency", + "default": "eventual" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "AccessRequest", + "description": "Explicit authorization request passed through the API boundary." + }, + "AdminLoginRequest": { + "properties": { + "username": { + "type": "string", + "title": "Username" + }, + "password": { + "type": "string", + "title": "Password" + } + }, + "type": "object", + "required": [ + "username", + "password" + ], + "title": "AdminLoginRequest" + }, + "AdminLoginResponse": { + "properties": { + "access_token": { + "type": "string", + "title": "Access Token" + }, + "token_type": { + "type": "string", + "title": "Token Type", + "default": "bearer" + }, + "expires_in": { + "type": "integer", + "title": "Expires In" + }, + "role": { + "type": "string", + "title": "Role", + "default": "admin" + }, + "tenant_key": { + "type": "string", + "title": "Tenant Key" + } + }, + "type": "object", + "required": [ + "access_token", + "expires_in", + "tenant_key" + ], + "title": "AdminLoginResponse" + }, + "AuditRecordOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "principal_type": { + "type": "string", + "title": "Principal Type" + }, + "principal_id": { + "type": "string", + "title": "Principal Id" + }, + "correlation_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Correlation Id" + }, + "user": { + "additionalProperties": true, + "type": "object", + "title": "User" + }, + "action": { + "type": "string", + "title": "Action" + }, + "resource": { + "additionalProperties": true, + "type": "object", + "title": "Resource" + }, + "decision": { + "type": "string", + "title": "Decision" + }, + "matched_policies": { + "items": {}, + "type": "array", + "title": "Matched Policies" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "evaluated_rules": { + "items": {}, + "type": "array", + "title": "Evaluated Rules" + }, + "failed_conditions": { + "items": {}, + "type": "array", + "title": "Failed Conditions" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + } + }, + "type": "object", + "required": [ + "id", + "principal_type", + "principal_id", + "user", + "action", + "resource", + "decision", + "matched_policies", + "evaluated_rules", + "failed_conditions", + "created_at" + ], + "title": "AuditRecordOut" + }, + "AuthModelCreate": { + "properties": { + "schema": { + "type": "string", + "title": "Schema" + } + }, + "type": "object", + "required": [ + "schema" + ], + "title": "AuthModelCreate" + }, + "AuthModelOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "tenant_id": { + "type": "integer", + "title": "Tenant Id" + }, + "schema": { + "type": "string", + "title": "Schema" + }, + "parsed": { + "additionalProperties": true, + "type": "object", + "title": "Parsed" + }, + "compiled": { + "additionalProperties": true, + "type": "object", + "title": "Compiled" + } + }, + "type": "object", + "required": [ + "id", + "tenant_id", + "schema", + "parsed", + "compiled" + ], + "title": "AuthModelOut" + }, + "BatchAccessItem": { + "properties": { + "action": { + "type": "string", + "title": "Action" + }, + "resource": { + "additionalProperties": true, + "type": "object", + "title": "Resource" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "BatchAccessItem" + }, + "BatchAccessRequest": { + "properties": { + "user": { + "additionalProperties": true, + "type": "object", + "title": "User" + }, + "items": { + "items": { + "$ref": "#/components/schemas/BatchAccessItem" + }, + "type": "array", + "title": "Items" + }, + "consistency": { + "type": "string", + "title": "Consistency", + "default": "eventual" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "items" + ], + "title": "BatchAccessRequest" + }, + "BatchAccessResponse": { + "properties": { + "results": { + "items": { + "$ref": "#/components/schemas/BatchAccessResult" + }, + "type": "array", + "title": "Results" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "results" + ], + "title": "BatchAccessResponse" + }, + "BatchAccessResult": { + "properties": { + "action": { + "type": "string", + "title": "Action" + }, + "allowed": { + "type": "boolean", + "title": "Allowed" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "action", + "allowed" + ], + "title": "BatchAccessResult" + }, + "HTTPValidationError": { + "properties": { + "detail": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Detail" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "ImpactAnalysisRequest": { + "properties": { + "policy_change": { + "type": "string", + "title": "Policy Change" + } + }, + "type": "object", + "required": [ + "policy_change" + ], + "title": "ImpactAnalysisRequest" + }, + "ImpactAnalysisResponse": { + "properties": { + "gained_access": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Gained Access" + }, + "lost_access": { + "items": { + "type": "integer" + }, + "type": "array", + "title": "Lost Access" + } + }, + "type": "object", + "title": "ImpactAnalysisResponse" + }, + "MetaBody": { + "properties": { + "request_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Request Id" + }, + "limit": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Limit" + }, + "next_cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Next Cursor" + }, + "extra": { + "additionalProperties": true, + "type": "object", + "title": "Extra" + } + }, + "type": "object", + "title": "MetaBody" + }, + "PermissionCreate": { + "properties": { + "action": { + "type": "string", + "title": "Action" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "PermissionCreate" + }, + "PermissionOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "action": { + "type": "string", + "title": "Action" + } + }, + "type": "object", + "required": [ + "id", + "action" + ], + "title": "PermissionOut" + }, + "PermissionUpdate": { + "properties": { + "action": { + "type": "string", + "title": "Action" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "PermissionUpdate" + }, + "PlaygroundEvaluateRequest": { + "properties": { + "policies": { + "items": { + "$ref": "#/components/schemas/PlaygroundPolicy" + }, + "type": "array", + "title": "Policies" + }, + "input": { + "$ref": "#/components/schemas/PlaygroundInput" + } + }, + "type": "object", + "required": [ + "policies", + "input" + ], + "title": "PlaygroundEvaluateRequest" + }, + "PlaygroundInput": { + "properties": { + "user": { + "additionalProperties": true, + "type": "object", + "title": "User" + }, + "resource": { + "additionalProperties": true, + "type": "object", + "title": "Resource" + }, + "action": { + "type": "string", + "title": "Action", + "default": "" + }, + "context": { + "additionalProperties": true, + "type": "object", + "title": "Context" + } + }, + "type": "object", + "title": "PlaygroundInput" + }, + "PlaygroundPolicy": { + "properties": { + "action": { + "type": "string", + "title": "Action" + }, + "effect": { + "type": "string", + "title": "Effect", + "default": "allow" + }, + "priority": { + "type": "integer", + "title": "Priority", + "default": 100 + }, + "policy_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Policy Id" + }, + "conditions": { + "additionalProperties": true, + "type": "object", + "title": "Conditions" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "PlaygroundPolicy" + }, + "PolicyCreate": { + "properties": { + "action": { + "type": "string", + "title": "Action" + }, + "effect": { + "type": "string", + "title": "Effect", + "default": "allow" + }, + "priority": { + "type": "integer", + "title": "Priority", + "default": 100 + }, + "state": { + "type": "string", + "title": "State", + "default": "active" + }, + "conditions": { + "additionalProperties": true, + "type": "object", + "title": "Conditions" + } + }, + "type": "object", + "required": [ + "action" + ], + "title": "PolicyCreate" + }, + "PolicyOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "action": { + "type": "string", + "title": "Action" + }, + "effect": { + "type": "string", + "title": "Effect" + }, + "priority": { + "type": "integer", + "title": "Priority" + }, + "state": { + "type": "string", + "title": "State", + "default": "active" + }, + "conditions": { + "additionalProperties": true, + "type": "object", + "title": "Conditions" + } + }, + "type": "object", + "required": [ + "id", + "action", + "effect", + "priority", + "conditions" + ], + "title": "PolicyOut" + }, + "PolicySimulationInput": { + "properties": { + "policy_change": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Policy Change" + }, + "relationship_change": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Relationship Change" + }, + "role_change": { + "anyOf": [ + { + "additionalProperties": true, + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Role Change" + } + }, + "type": "object", + "title": "PolicySimulationInput" + }, + "PolicySimulationRequest": { + "properties": { + "simulate": { + "$ref": "#/components/schemas/PolicySimulationInput" + }, + "request": { + "additionalProperties": true, + "type": "object", + "title": "Request" + } + }, + "type": "object", + "title": "PolicySimulationRequest" + }, + "PolicySimulationResponse": { + "properties": { + "decision_before": { + "additionalProperties": true, + "type": "object", + "title": "Decision Before" + }, + "decision_after": { + "additionalProperties": true, + "type": "object", + "title": "Decision After" + } + }, + "type": "object", + "required": [ + "decision_before", + "decision_after" + ], + "title": "PolicySimulationResponse" + }, + "RelationshipCreate": { + "properties": { + "subject_type": { + "type": "string", + "title": "Subject Type" + }, + "subject_id": { + "type": "string", + "title": "Subject Id" + }, + "relation": { + "type": "string", + "title": "Relation" + }, + "object_type": { + "type": "string", + "title": "Object Type" + }, + "object_id": { + "type": "string", + "title": "Object Id" + } + }, + "type": "object", + "required": [ + "subject_type", + "subject_id", + "relation", + "object_type", + "object_id" + ], + "title": "RelationshipCreate" + }, + "RelationshipOut": { + "properties": { + "subject_type": { + "type": "string", + "title": "Subject Type" + }, + "subject_id": { + "type": "string", + "title": "Subject Id" + }, + "relation": { + "type": "string", + "title": "Relation" + }, + "object_type": { + "type": "string", + "title": "Object Type" + }, + "object_id": { + "type": "string", + "title": "Object Id" + }, + "id": { + "type": "integer", + "title": "Id" + } + }, + "type": "object", + "required": [ + "subject_type", + "subject_id", + "relation", + "object_type", + "object_id", + "id" + ], + "title": "RelationshipOut" + }, + "RoleCreate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "RoleCreate" + }, + "RoleOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "id", + "name" + ], + "title": "RoleOut" + }, + "RoleUpdate": { + "properties": { + "name": { + "type": "string", + "title": "Name" + } + }, + "type": "object", + "required": [ + "name" + ], + "title": "RoleUpdate" + }, + "SimulationResponse": { + "properties": { + "decision": { + "type": "string", + "title": "Decision" + }, + "matched_policies": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Matched Policies" + }, + "reason": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Reason" + }, + "policy_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Policy Id" + }, + "explain_trace": { + "items": { + "additionalProperties": true, + "type": "object" + }, + "type": "array", + "title": "Explain Trace" + }, + "failed_conditions": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Failed Conditions" + }, + "revision": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "title": "Revision" + } + }, + "type": "object", + "required": [ + "decision", + "matched_policies" + ], + "title": "SimulationResponse" + }, + "SuccessResponse_ACLOut_": { + "properties": { + "data": { + "$ref": "#/components/schemas/ACLOut" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[ACLOut]" + }, + "SuccessResponse_AccessDecisionResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/AccessDecisionResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[AccessDecisionResponse]" + }, + "SuccessResponse_AdminLoginResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/AdminLoginResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[AdminLoginResponse]" + }, + "SuccessResponse_AuthModelOut_": { + "properties": { + "data": { + "$ref": "#/components/schemas/AuthModelOut" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[AuthModelOut]" + }, + "SuccessResponse_BatchAccessResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/BatchAccessResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[BatchAccessResponse]" + }, + "SuccessResponse_ImpactAnalysisResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/ImpactAnalysisResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[ImpactAnalysisResponse]" + }, + "SuccessResponse_PermissionOut_": { + "properties": { + "data": { + "$ref": "#/components/schemas/PermissionOut" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[PermissionOut]" + }, + "SuccessResponse_PolicyOut_": { + "properties": { + "data": { + "$ref": "#/components/schemas/PolicyOut" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[PolicyOut]" + }, + "SuccessResponse_PolicySimulationResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/PolicySimulationResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[PolicySimulationResponse]" + }, + "SuccessResponse_RelationshipOut_": { + "properties": { + "data": { + "$ref": "#/components/schemas/RelationshipOut" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[RelationshipOut]" + }, + "SuccessResponse_SimulationResponse_": { + "properties": { + "data": { + "$ref": "#/components/schemas/SimulationResponse" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[SimulationResponse]" + }, + "SuccessResponse_dict_str__Any__": { + "properties": { + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[dict[str, Any]]" + }, + "SuccessResponse_dict_str__Union_int__str___": { + "properties": { + "data": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "type": "object", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[dict[str, Union[int, str]]]" + }, + "SuccessResponse_dict_str__int__": { + "properties": { + "data": { + "additionalProperties": { + "type": "integer" + }, + "type": "object", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[dict[str, int]]" + }, + "SuccessResponse_dict_str__object__": { + "properties": { + "data": { + "additionalProperties": true, + "type": "object", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[dict[str, object]]" + }, + "SuccessResponse_dict_str__str__": { + "properties": { + "data": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[dict[str, str]]" + }, + "SuccessResponse_list_ACLOut__": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/ACLOut" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[ACLOut]]" + }, + "SuccessResponse_list_AuditRecordOut__": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/AuditRecordOut" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[AuditRecordOut]]" + }, + "SuccessResponse_list_PermissionOut__": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/PermissionOut" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[PermissionOut]]" + }, + "SuccessResponse_list_PolicyOut__": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/PolicyOut" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[PolicyOut]]" + }, + "SuccessResponse_list_RoleOut__": { + "properties": { + "data": { + "items": { + "$ref": "#/components/schemas/RoleOut" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[RoleOut]]" + }, + "SuccessResponse_list_dict_str__str___": { + "properties": { + "data": { + "items": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "type": "array", + "title": "Data" + }, + "meta": { + "$ref": "#/components/schemas/MetaBody" + }, + "error": { + "type": "null", + "title": "Error" + } + }, + "type": "object", + "required": [ + "data" + ], + "title": "SuccessResponse[list[dict[str, str]]]" + }, + "ValidationError": { + "properties": { + "loc": { + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "integer" + } + ] + }, + "type": "array", + "title": "Location" + }, + "msg": { + "type": "string", + "title": "Message" + }, + "type": { + "type": "string", + "title": "Error Type" + }, + "input": { + "title": "Input" + }, + "ctx": { + "type": "object", + "title": "Context" + } + }, + "type": "object", + "required": [ + "loc", + "msg", + "type" + ], + "title": "ValidationError" + } + }, + "securitySchemes": { + "HTTPBearer": { + "type": "http", + "scheme": "bearer" + }, + "APIKeyHeader": { + "type": "apiKey", + "in": "header", + "name": "X-API-Key" + } + } + } +} \ No newline at end of file diff --git a/coverage.json b/coverage.json new file mode 100644 index 0000000..3b75279 --- /dev/null +++ b/coverage.json @@ -0,0 +1 @@ +{"meta": {"format": 3, "version": "7.13.5", "timestamp": "2026-04-07T02:24:08.009199", "branch_coverage": false, "show_contexts": false}, "files": {"keynetra/__init__.py": {"executed_lines": [3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/dependencies.py": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 56, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 80, 81, 97, 104, 109, 115], "summary": {"covered_lines": 69, "num_statements": 69, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"build_services": {"executed_lines": [61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 80, 81, 97, 104, 109, 115], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 56}, "": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 56], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ServiceContainer": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 35}, "": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 56, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 80, 81, 97, 104, 109, 115], "summary": {"covered_lines": 69, "num_statements": 69, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/errors.py": {"executed_lines": [3, 5, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 24, 27, 28, 29, 30, 31], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"ApiError.__init__": {"executed_lines": [27, 28, 29, 30, 31], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24}, "": {"executed_lines": [3, 5, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 24], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ApiErrorCode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 9}, "ApiError": {"executed_lines": [27, 28, 29, 30, 31], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "": {"executed_lines": [3, 5, 6, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 24], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/main.py": {"executed_lines": [1, 2, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34, 35, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 64, 66, 67, 69, 82, 85, 108, 109, 110, 111, 112, 113, 114, 116, 117, 118, 120, 121, 123, 124, 125, 126, 127, 128, 129, 130, 131, 140, 141, 142, 143, 154, 164, 165, 166, 167, 168, 170, 171, 173, 174, 176, 177, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 208], "summary": {"covered_lines": 96, "num_statements": 157, "percent_covered": 61.146496815286625, "percent_covered_display": "61", "missing_lines": 61, "excluded_lines": 0, "percent_statements_covered": 61.146496815286625, "percent_statements_covered_display": "61"}, "missing_lines": [36, 37, 38, 39, 40, 42, 70, 71, 73, 74, 75, 76, 77, 78, 79, 80, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 105, 132, 133, 138, 144, 145, 146, 147, 148, 149, 150, 151, 155, 156, 157, 158, 159, 160, 161, 169, 175, 181, 182, 183, 184, 188, 205], "excluded_lines": [], "functions": {"_lifespan": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [36, 37, 38, 39, 40, 42], "excluded_lines": [], "start_line": 35}, "create_app": {"executed_lines": [46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 64, 66, 67, 69, 82], "summary": {"covered_lines": 15, "num_statements": 25, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [70, 71, 73, 74, 75, 76, 77, 78, 79, 80], "excluded_lines": [], "start_line": 45}, "_run_startup": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 19, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 19, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 105], "excluded_lines": [], "start_line": 85}, "_start_policy_subscriber": {"executed_lines": [109, 110, 111, 112, 113, 114, 116, 117, 118, 120, 121, 123, 140, 141, 142, 143], "summary": {"covered_lines": 16, "num_statements": 24, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [144, 145, 146, 147, 148, 149, 150, 151], "excluded_lines": [], "start_line": 108}, "_start_policy_subscriber.run": {"executed_lines": [124, 125, 126, 127, 128, 129, 130, 131], "summary": {"covered_lines": 8, "num_statements": 11, "percent_covered": 72.72727272727273, "percent_covered_display": "73", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 72.72727272727273, "percent_statements_covered_display": "73"}, "missing_lines": [132, 133, 138], "excluded_lines": [], "start_line": 123}, "_stop_policy_subscriber": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [155, 156, 157, 158, 159, 160, 161], "excluded_lines": [], "start_line": 154}, "_bootstrap_file_backed_model": {"executed_lines": [165, 166, 167, 168, 170, 171, 173, 174, 176, 177], "summary": {"covered_lines": 10, "num_statements": 17, "percent_covered": 58.8235294117647, "percent_covered_display": "59", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 58.8235294117647, "percent_statements_covered_display": "59"}, "missing_lines": [169, 175, 181, 182, 183, 184, 188], "excluded_lines": [], "start_line": 164}, "_bootstrap_file_backed_policies": {"executed_lines": [192, 193, 194, 195, 196, 197, 198, 199, 200, 201], "summary": {"covered_lines": 10, "num_statements": 11, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91"}, "missing_lines": [205], "excluded_lines": [], "start_line": 191}, "": {"executed_lines": [1, 2, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34, 35, 45, 85, 108, 154, 164, 191, 208], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 2, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 34, 35, 45, 46, 47, 48, 50, 51, 52, 53, 54, 55, 56, 64, 66, 67, 69, 82, 85, 108, 109, 110, 111, 112, 113, 114, 116, 117, 118, 120, 121, 123, 124, 125, 126, 127, 128, 129, 130, 131, 140, 141, 142, 143, 154, 164, 165, 166, 167, 168, 170, 171, 173, 174, 176, 177, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 208], "summary": {"covered_lines": 96, "num_statements": 157, "percent_covered": 61.146496815286625, "percent_covered_display": "61", "missing_lines": 61, "excluded_lines": 0, "percent_statements_covered": 61.146496815286625, "percent_statements_covered_display": "61"}, "missing_lines": [36, 37, 38, 39, 40, 42, 70, 71, 73, 74, 75, 76, 77, 78, 79, 80, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 105, 132, 133, 138, 144, 145, 146, 147, 148, 149, 150, 151, 155, 156, 157, 158, 159, 160, 161, 169, 175, 181, 182, 183, 184, 188, 205], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/errors.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16, 19, 20, 23, 24, 26, 27, 28, 29, 37, 41, 43, 44, 45, 53, 61, 62, 64, 65, 79, 80], "summary": {"covered_lines": 33, "num_statements": 43, "percent_covered": 76.74418604651163, "percent_covered_display": "77", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 76.74418604651163, "percent_statements_covered_display": "77"}, "missing_lines": [68, 69, 77, 81, 82, 83, 91, 99, 100, 101], "excluded_lines": [], "functions": {"_request_id": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "register_error_handlers": {"executed_lines": [24, 26, 27, 43, 44, 64, 65, 79, 80], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "register_error_handlers.api_exception_handler": {"executed_lines": [28, 29, 37, 41], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "register_error_handlers.http_exception_handler": {"executed_lines": [45, 53, 61, 62], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44}, "register_error_handlers.validation_exception_handler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [68, 69, 77], "excluded_lines": [], "start_line": 65}, "register_error_handlers.unhandled_exception_handler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [81, 82, 83, 91, 99, 100, 101], "excluded_lines": [], "start_line": 80}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16, 19, 23], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16, 19, 20, 23, 24, 26, 27, 28, 29, 37, 41, 43, 44, 45, 53, 61, 62, 64, 65, 79, 80], "summary": {"covered_lines": 33, "num_statements": 43, "percent_covered": 76.74418604651163, "percent_covered_display": "77", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 76.74418604651163, "percent_statements_covered_display": "77"}, "missing_lines": [68, 69, 77, 81, 82, 83, 91, 99, 100, 101], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/idempotency.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 27, 28, 29, 30, 32, 35, 36, 38, 39, 40, 42, 43, 44, 46, 47, 48, 49, 52, 53, 64, 76, 77, 82, 83, 84, 86, 88, 89, 90, 91, 92, 93, 94, 101, 102, 103, 106, 109, 110, 112, 113, 114, 115, 118, 119, 120], "summary": {"covered_lines": 58, "num_statements": 60, "percent_covered": 96.66666666666667, "percent_covered_display": "97", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 96.66666666666667, "percent_statements_covered_display": "97"}, "missing_lines": [65, 111], "excluded_lines": [], "functions": {"IdempotencyMiddleware.__init__": {"executed_lines": [28, 29, 30], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "IdempotencyMiddleware.dispatch": {"executed_lines": [35, 36, 38, 39, 40, 42, 43, 44, 46, 47, 48, 49, 52, 53, 64, 76, 77, 82, 83, 84, 86, 88, 89, 90, 91, 92, 93, 94, 101, 102, 103, 106], "summary": {"covered_lines": 32, "num_statements": 33, "percent_covered": 96.96969696969697, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.96969696969697, "percent_statements_covered_display": "97"}, "missing_lines": [65], "excluded_lines": [], "start_line": 32}, "_collect_body": {"executed_lines": [110, 112, 113, 114, 115], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [111], "excluded_lines": [], "start_line": 109}, "_clone_response": {"executed_lines": [119, 120], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 118}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 27, 32, 109, 118], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"IdempotencyMiddleware": {"executed_lines": [28, 29, 30, 35, 36, 38, 39, 40, 42, 43, 44, 46, 47, 48, 49, 52, 53, 64, 76, 77, 82, 83, 84, 86, 88, 89, 90, 91, 92, 93, 94, 101, 102, 103, 106], "summary": {"covered_lines": 35, "num_statements": 36, "percent_covered": 97.22222222222223, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.22222222222223, "percent_statements_covered_display": "97"}, "missing_lines": [65], "excluded_lines": [], "start_line": 18}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 27, 32, 109, 110, 112, 113, 114, 115, 118, 119, 120], "summary": {"covered_lines": 23, "num_statements": 24, "percent_covered": 95.83333333333333, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 95.83333333333333, "percent_statements_covered_display": "96"}, "missing_lines": [111], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/logging.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 11, 12, 13, 16, 19, 20, 21, 23, 26, 27, 28, 29, 30, 40, 47], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"RequestLoggingMiddleware.__init__": {"executed_lines": [20, 21], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "RequestLoggingMiddleware.dispatch": {"executed_lines": [26, 27, 28, 29, 30, 40, 47], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 11, 12, 13, 16, 19, 23], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RequestLoggingMiddleware": {"executed_lines": [20, 21, 26, 27, 28, 29, 30, 40, 47], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 11, 12, 13, 16, 19, 23], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/request_id.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 10, 13, 21, 23, 26, 27, 28, 29, 30, 31, 32, 34], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"RequestIdMiddleware.dispatch": {"executed_lines": [26, 27, 28, 29, 30, 31, 32, 34], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 10, 13, 21, 23], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RequestIdMiddleware": {"executed_lines": [26, 27, 28, 29, 30, 31, 32, 34], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 10, 13, 21, 23], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/tenant.py": {"executed_lines": [3, 5, 7, 8, 9, 11, 14, 17, 19, 22, 25, 26, 27, 28, 30, 31, 44, 45], "summary": {"covered_lines": 18, "num_statements": 19, "percent_covered": 94.73684210526316, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 94.73684210526316, "percent_statements_covered_display": "95"}, "missing_lines": [32], "excluded_lines": [], "functions": {"TenantResolverMiddleware.dispatch": {"executed_lines": [22, 25, 26, 27, 28, 30, 31, 44, 45], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [32], "excluded_lines": [], "start_line": 19}, "": {"executed_lines": [3, 5, 7, 8, 9, 11, 14, 17, 19], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"TenantResolverMiddleware": {"executed_lines": [22, 25, 26, 27, 28, 30, 31, 44, 45], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [32], "excluded_lines": [], "start_line": 14}, "": {"executed_lines": [3, 5, 7, 8, 9, 11, 14, 17, 19], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/middleware/versioning.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 16, 19, 20, 21, 23, 26, 30, 31, 46, 47, 48, 49, 50, 51, 60, 61, 62], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"ApiVersionMiddleware.dispatch": {"executed_lines": [26, 30, 31, 46, 47, 48, 49, 50, 51, 60, 61, 62], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 16, 19, 20, 21, 23], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ApiVersionMiddleware": {"executed_lines": [26, 30, 31, 46, 47, 48, 49, 50, 51, 60, 61, 62], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 16, 19, 20, 21, 23], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/pagination.py": {"executed_lines": [3, 5, 7, 8, 9, 12, 13, 16, 19, 20, 21, 22, 23, 24, 30], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"encode_cursor": {"executed_lines": [13], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "decode_cursor": {"executed_lines": [19, 20, 21, 22, 23, 24, 30], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "": {"executed_lines": [3, 5, 7, 8, 9, 12, 16], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 7, 8, 9, 12, 13, 16, 19, 20, 21, 22, 23, 24, 30], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/responses.py": {"executed_lines": [3, 5, 7, 10, 18, 27, 28], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"success_response": {"executed_lines": [18], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 10}, "request_id_from_state": {"executed_lines": [28], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "": {"executed_lines": [3, 5, 7, 10, 27], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 7, 10, 18, 27, 28], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/access.py": {"executed_lines": [7, 9, 11, 12, 14, 15, 16, 17, 18, 24, 32, 33, 34, 36, 37, 40, 41, 44, 50, 51, 54, 55, 56, 58, 59, 60, 61, 63, 64, 67, 68, 75, 78, 79, 80, 90, 95, 103, 104, 105, 106, 112, 113, 126, 148, 156, 170, 175, 182, 183, 184, 185, 197, 216, 223, 237, 242, 250, 251, 252, 253, 259, 260, 271, 291, 297], "summary": {"covered_lines": 66, "num_statements": 84, "percent_covered": 78.57142857142857, "percent_covered_display": "79", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79"}, "missing_lines": [82, 107, 114, 137, 138, 143, 144, 186, 205, 206, 211, 212, 254, 261, 280, 281, 286, 287], "excluded_lines": [], "functions": {"_legacy_service_override": {"executed_lines": [41], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40}, "_resolve_tenant_key": {"executed_lines": [50, 51, 54, 55, 56, 58, 59, 60, 61, 63, 64, 67, 68, 75, 78, 79, 80], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 94.44444444444444, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 94.44444444444444, "percent_statements_covered_display": "94"}, "missing_lines": [82], "excluded_lines": [], "start_line": 44}, "check_access": {"executed_lines": [103, 104, 105, 106, 112, 113, 126, 148, 156], "summary": {"covered_lines": 9, "num_statements": 15, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [107, 114, 137, 138, 143, 144], "excluded_lines": [], "start_line": 95}, "simulate": {"executed_lines": [182, 183, 184, 185, 197, 216, 223], "summary": {"covered_lines": 7, "num_statements": 12, "percent_covered": 58.333333333333336, "percent_covered_display": "58", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 58.333333333333336, "percent_statements_covered_display": "58"}, "missing_lines": [186, 205, 206, 211, 212], "excluded_lines": [], "start_line": 175}, "check_access_batch": {"executed_lines": [250, 251, 252, 253, 259, 260, 271, 291, 297], "summary": {"covered_lines": 9, "num_statements": 15, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [254, 261, 280, 281, 286, 287], "excluded_lines": [], "start_line": 242}, "": {"executed_lines": [7, 9, 11, 12, 14, 15, 16, 17, 18, 24, 32, 33, 34, 36, 37, 40, 44, 90, 95, 170, 175, 237, 242], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [7, 9, 11, 12, 14, 15, 16, 17, 18, 24, 32, 33, 34, 36, 37, 40, 41, 44, 50, 51, 54, 55, 56, 58, 59, 60, 61, 63, 64, 67, 68, 75, 78, 79, 80, 90, 95, 103, 104, 105, 106, 112, 113, 126, 148, 156, 170, 175, 182, 183, 184, 185, 197, 216, 223, 237, 242, 250, 251, 252, 253, 259, 260, 271, 291, 297], "summary": {"covered_lines": 66, "num_statements": 84, "percent_covered": 78.57142857142857, "percent_covered_display": "79", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79"}, "missing_lines": [82, 107, 114, 137, 138, 143, 144, 186, 205, 206, 211, 212, 254, 261, 280, 281, 286, 287], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/acl.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 18, 19, 25, 26, 32, 33, 42, 43, 48, 49, 54, 65, 66, 73, 74, 75, 82, 101, 102, 108, 109, 110, 111, 112, 113, 118, 119, 124], "summary": {"covered_lines": 40, "num_statements": 47, "percent_covered": 85.1063829787234, "percent_covered_display": "85", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 85.1063829787234, "percent_statements_covered_display": "85"}, "missing_lines": [27, 50, 51, 78, 79, 120, 121], "excluded_lines": [], "functions": {"create_acl_entry": {"executed_lines": [25, 26, 32, 33, 42, 43, 48, 49, 54], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [27, 50, 51], "excluded_lines": [], "start_line": 19}, "list_acl_entries": {"executed_lines": [73, 74, 75, 82], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [78, 79], "excluded_lines": [], "start_line": 66}, "delete_acl_entry": {"executed_lines": [108, 109, 110, 111, 112, 113, 118, 119, 124], "summary": {"covered_lines": 9, "num_statements": 11, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82"}, "missing_lines": [120, 121], "excluded_lines": [], "start_line": 102}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 18, 19, 65, 66, 101, 102], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 15, 18, 19, 25, 26, 32, 33, 42, 43, 48, 49, 54, 65, 66, 73, 74, 75, 82, 101, 102, 108, 109, 110, 111, 112, 113, 118, 119, 124], "summary": {"covered_lines": 40, "num_statements": 47, "percent_covered": 85.1063829787234, "percent_covered_display": "85", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 85.1063829787234, "percent_statements_covered_display": "85"}, "missing_lines": [27, 50, 51, 78, 79, 120, 121], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/admin_auth.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 17, 20, 21, 26, 27, 28, 30, 37, 38, 39, 42, 43, 44, 45, 51, 52, 63], "summary": {"covered_lines": 29, "num_statements": 32, "percent_covered": 90.625, "percent_covered_display": "91", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.625, "percent_statements_covered_display": "91"}, "missing_lines": [31, 40, 41], "excluded_lines": [], "functions": {"admin_login": {"executed_lines": [26, 27, 28, 30, 37, 38, 39, 42, 43, 44, 45, 51, 52, 63], "summary": {"covered_lines": 14, "num_statements": 17, "percent_covered": 82.3529411764706, "percent_covered_display": "82", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 82.3529411764706, "percent_statements_covered_display": "82"}, "missing_lines": [31, 40, 41], "excluded_lines": [], "start_line": 21}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 17, 20, 21], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 8, 10, 11, 12, 13, 14, 15, 17, 20, 21, 26, 27, 28, 30, 37, 38, 39, 42, 43, 44, 45, 51, 52, 63], "summary": {"covered_lines": 29, "num_statements": 32, "percent_covered": 90.625, "percent_covered_display": "91", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.625, "percent_statements_covered_display": "91"}, "missing_lines": [31, 40, 41], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/audit.py": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 19, 20, 32, 38, 39, 40, 54], "summary": {"covered_lines": 19, "num_statements": 22, "percent_covered": 86.36363636363636, "percent_covered_display": "86", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 86.36363636363636, "percent_statements_covered_display": "86"}, "missing_lines": [33, 50, 51], "excluded_lines": [], "functions": {"list_audit_logs": {"executed_lines": [32, 38, 39, 40, 54], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 62.5, "percent_statements_covered_display": "62"}, "missing_lines": [33, 50, 51], "excluded_lines": [], "start_line": 20}, "": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 19, 20], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 16, 19, 20, 32, 38, 39, 40, 54], "summary": {"covered_lines": 19, "num_statements": 22, "percent_covered": 86.36363636363636, "percent_covered_display": "86", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 86.36363636363636, "percent_statements_covered_display": "86"}, "missing_lines": [33, 50, 51], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/auth_model.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 19, 21, 24, 25, 31, 32, 33, 34, 35, 36, 47, 50, 59, 71, 72, 77, 78, 79, 81], "summary": {"covered_lines": 31, "num_statements": 36, "percent_covered": 86.11111111111111, "percent_covered_display": "86", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 86.11111111111111, "percent_statements_covered_display": "86"}, "missing_lines": [51, 52, 55, 56, 80], "excluded_lines": [], "functions": {"create_auth_model": {"executed_lines": [31, 32, 33, 34, 35, 36, 47, 50, 59], "summary": {"covered_lines": 9, "num_statements": 13, "percent_covered": 69.23076923076923, "percent_covered_display": "69", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 69.23076923076923, "percent_statements_covered_display": "69"}, "missing_lines": [51, 52, 55, 56], "excluded_lines": [], "start_line": 25}, "get_auth_model": {"executed_lines": [77, 78, 79, 81], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [80], "excluded_lines": [], "start_line": 72}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 19, 21, 24, 25, 71, 72], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 19, 21, 24, 25, 31, 32, 33, 34, 35, 36, 47, 50, 59, 71, 72, 77, 78, 79, 81], "summary": {"covered_lines": 31, "num_statements": 36, "percent_covered": 86.11111111111111, "percent_covered_display": "86", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 86.11111111111111, "percent_statements_covered_display": "86"}, "missing_lines": [51, 52, 55, 56, 80], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/dev.py": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 16, 17, 18, 21, 22, 26, 27, 32, 33, 39, 40, 41], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"_require_local_dev": {"executed_lines": [17, 18], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "get_sample_data": {"executed_lines": [26, 27], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "seed_sample_data": {"executed_lines": [39, 40, 41], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 33}, "": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 16, 21, 22, 32, 33], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 13, 16, 17, 18, 21, 22, 26, 27, 32, 33, 39, 40, 41], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/health.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 16, 17, 18, 21, 22, 23, 26, 27, 32, 33, 34, 38, 48, 54, 55, 56, 57, 62, 63, 64], "summary": {"covered_lines": 30, "num_statements": 40, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [58, 59, 66, 67, 68, 70, 71, 72, 73, 74], "excluded_lines": [], "functions": {"health": {"executed_lines": [18], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "liveness": {"executed_lines": [23], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "readiness": {"executed_lines": [32, 33, 34, 38, 48], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "_check_database": {"executed_lines": [55, 56, 57], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [58, 59], "excluded_lines": [], "start_line": 54}, "_check_redis": {"executed_lines": [63, 64], "summary": {"covered_lines": 2, "num_statements": 10, "percent_covered": 20.0, "percent_covered_display": "20", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 20.0, "percent_statements_covered_display": "20"}, "missing_lines": [66, 67, 68, 70, 71, 72, 73, 74], "excluded_lines": [], "start_line": 62}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 16, 17, 21, 22, 26, 27, 54, 62], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 16, 17, 18, 21, 22, 23, 26, 27, 32, 33, 34, 38, 48, 54, 55, 56, 57, 62, 63, 64], "summary": {"covered_lines": 30, "num_statements": 40, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [58, 59, 66, 67, 68, 70, 71, 72, 73, 74], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/metrics.py": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 12], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"metrics": {"executed_lines": [12], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "": {"executed_lines": [1, 3, 4, 5, 7, 10, 11], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 10, 11, 12], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/permissions.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 21, 23, 26, 27, 34, 35, 36, 41, 42, 43, 52, 57, 58, 59, 62, 70, 71, 76, 77, 80, 81, 84, 85, 86, 87, 88, 89, 90, 94, 97, 98, 104, 105, 106, 107, 108, 117, 121, 122, 123, 124, 125, 126, 130, 133, 134, 140, 141, 146, 147, 148, 149, 152, 153, 154, 155, 159, 164, 165, 171, 172, 173, 175, 180], "summary": {"covered_lines": 77, "num_statements": 89, "percent_covered": 86.51685393258427, "percent_covered_display": "87", "missing_lines": 12, "excluded_lines": 0, "percent_statements_covered": 86.51685393258427, "percent_statements_covered_display": "87"}, "missing_lines": [44, 91, 92, 93, 118, 127, 128, 129, 156, 157, 158, 174], "excluded_lines": [], "functions": {"list_permissions": {"executed_lines": [34, 35, 36, 41, 42, 43, 52, 57, 58, 59, 62], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [44], "excluded_lines": [], "start_line": 27}, "create_permission": {"executed_lines": [76, 77, 80, 81, 84, 85, 86, 87, 88, 89, 90, 94], "summary": {"covered_lines": 12, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [91, 92, 93], "excluded_lines": [], "start_line": 71}, "update_permission": {"executed_lines": [104, 105, 106, 107, 108, 117, 121, 122, 123, 124, 125, 126, 130], "summary": {"covered_lines": 13, "num_statements": 17, "percent_covered": 76.47058823529412, "percent_covered_display": "76", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 76.47058823529412, "percent_statements_covered_display": "76"}, "missing_lines": [118, 127, 128, 129], "excluded_lines": [], "start_line": 98}, "delete_permission": {"executed_lines": [140, 141, 146, 147, 148, 149, 152, 153, 154, 155, 159], "summary": {"covered_lines": 11, "num_statements": 14, "percent_covered": 78.57142857142857, "percent_covered_display": "79", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79"}, "missing_lines": [156, 157, 158], "excluded_lines": [], "start_line": 134}, "list_permission_roles": {"executed_lines": [171, 172, 173, 175, 180], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [174], "excluded_lines": [], "start_line": 165}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 21, 23, 26, 27, 70, 71, 97, 98, 133, 134, 164, 165], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 12, 13, 14, 15, 21, 23, 26, 27, 34, 35, 36, 41, 42, 43, 52, 57, 58, 59, 62, 70, 71, 76, 77, 80, 81, 84, 85, 86, 87, 88, 89, 90, 94, 97, 98, 104, 105, 106, 107, 108, 117, 121, 122, 123, 124, 125, 126, 130, 133, 134, 140, 141, 146, 147, 148, 149, 152, 153, 154, 155, 159, 164, 165, 171, 172, 173, 175, 180], "summary": {"covered_lines": 77, "num_statements": 89, "percent_covered": 86.51685393258427, "percent_covered_display": "87", "missing_lines": 12, "excluded_lines": 0, "percent_statements_covered": 86.51685393258427, "percent_statements_covered_display": "87"}, "missing_lines": [44, 91, 92, 93, 118, 127, 128, 129, 156, 157, 158, 174], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/playground.py": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 25, 26, 27, 28, 29, 32, 33, 34, 37, 40, 41, 46, 47, 53, 54], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"evaluate": {"executed_lines": [46, 47, 53, 54], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 25, 26, 27, 28, 29, 32, 33, 34, 37, 40, 41], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"PlaygroundPolicy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "PlaygroundInput": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "PlaygroundEvaluateRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 32}, "": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 14, 17, 18, 19, 20, 21, 22, 25, 26, 27, 28, 29, 32, 33, 34, 37, 40, 41, 46, 47, 53, 54], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/policies.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 21, 22, 29, 30, 35, 36, 37, 40, 41, 46, 55, 56, 63, 64, 70, 76, 77, 91, 94, 108, 109, 117, 123, 129, 130, 144, 147, 161, 162, 169, 170, 171, 172, 190, 191, 197, 198, 203, 208, 211, 218, 219, 224, 225], "summary": {"covered_lines": 57, "num_statements": 73, "percent_covered": 78.08219178082192, "percent_covered_display": "78", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 78.08219178082192, "percent_statements_covered_display": "78"}, "missing_lines": [42, 43, 65, 71, 87, 88, 118, 124, 140, 141, 175, 199, 200, 226, 227, 230], "excluded_lines": [], "functions": {"list_policies": {"executed_lines": [29, 30, 35, 36, 37, 40, 41, 46], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [42, 43], "excluded_lines": [], "start_line": 22}, "create_policy": {"executed_lines": [63, 64, 70, 76, 77, 91, 94], "summary": {"covered_lines": 7, "num_statements": 11, "percent_covered": 63.63636363636363, "percent_covered_display": "64", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 63.63636363636363, "percent_statements_covered_display": "64"}, "missing_lines": [65, 71, 87, 88], "excluded_lines": [], "start_line": 56}, "update_policy": {"executed_lines": [117, 123, 129, 130, 144, 147], "summary": {"covered_lines": 6, "num_statements": 10, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [118, 124, 140, 141], "excluded_lines": [], "start_line": 109}, "create_policy_from_dsl": {"executed_lines": [169, 170, 171, 172], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [175], "excluded_lines": [], "start_line": 162}, "delete_policy": {"executed_lines": [197, 198, 203], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [199, 200], "excluded_lines": [], "start_line": 191}, "rollback_policy": {"executed_lines": [218, 219, 224, 225], "summary": {"covered_lines": 4, "num_statements": 7, "percent_covered": 57.142857142857146, "percent_covered_display": "57", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 57.142857142857146, "percent_statements_covered_display": "57"}, "missing_lines": [226, 227, 230], "excluded_lines": [], "start_line": 211}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 21, 22, 55, 56, 108, 109, 161, 162, 190, 191, 208, 211], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 18, 21, 22, 29, 30, 35, 36, 37, 40, 41, 46, 55, 56, 63, 64, 70, 76, 77, 91, 94, 108, 109, 117, 123, 129, 130, 144, 147, 161, 162, 169, 170, 171, 172, 190, 191, 197, 198, 203, 208, 211, 218, 219, 224, 225], "summary": {"covered_lines": 57, "num_statements": 73, "percent_covered": 78.08219178082192, "percent_covered_display": "78", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 78.08219178082192, "percent_statements_covered_display": "78"}, "missing_lines": [42, 43, 65, 71, 87, 88, 118, 124, 140, 141, 175, 199, 200, 226, 227, 230], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/relationships.py": {"executed_lines": [3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 17, 20, 21, 22, 23, 24, 25, 28, 29, 32, 33, 42, 43, 48, 49, 60, 68, 71, 77, 78, 81, 82, 89], "summary": {"covered_lines": 34, "num_statements": 38, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 89.47368421052632, "percent_statements_covered_display": "89"}, "missing_lines": [56, 57, 85, 86], "excluded_lines": [], "functions": {"list_relationships": {"executed_lines": [42, 43, 48, 49, 60], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71"}, "missing_lines": [56, 57], "excluded_lines": [], "start_line": 33}, "create_relationship": {"executed_lines": [77, 78, 81, 82, 89], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71"}, "missing_lines": [85, 86], "excluded_lines": [], "start_line": 71}, "": {"executed_lines": [3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 17, 20, 21, 22, 23, 24, 25, 28, 29, 32, 33, 68, 71], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RelationshipCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20}, "RelationshipOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28}, "": {"executed_lines": [3, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 17, 20, 21, 22, 23, 24, 25, 28, 29, 32, 33, 42, 43, 48, 49, 60, 68, 71, 77, 78, 81, 82, 89], "summary": {"covered_lines": 34, "num_statements": 38, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 89.47368421052632, "percent_statements_covered_display": "89"}, "missing_lines": [56, 57, 85, 86], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/roles.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 22, 23, 30, 31, 37, 38, 39, 40, 46, 49, 50, 51, 54, 62, 63, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 82, 85, 86, 92, 93, 94, 95, 96, 101, 103, 104, 105, 106, 107, 108, 112, 115, 116, 122, 123, 133, 134, 135, 136, 137, 138, 139, 140, 141, 145, 150, 151, 157, 158, 163, 165, 174, 179, 186, 187, 188, 189, 191, 193, 194, 195, 196, 197, 198, 204, 210, 213, 220, 221, 222, 223, 225, 227, 228, 229, 230, 231, 232, 238], "summary": {"covered_lines": 106, "num_statements": 128, "percent_covered": 82.8125, "percent_covered_display": "83", "missing_lines": 22, "excluded_lines": 0, "percent_statements_covered": 82.8125, "percent_statements_covered_display": "83"}, "missing_lines": [32, 79, 80, 81, 102, 109, 110, 111, 142, 143, 144, 164, 190, 192, 199, 200, 201, 224, 226, 233, 234, 235], "excluded_lines": [], "functions": {"list_roles": {"executed_lines": [30, 31, 37, 38, 39, 40, 46, 49, 50, 51, 54], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [32], "excluded_lines": [], "start_line": 23}, "create_role": {"executed_lines": [68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 82], "summary": {"covered_lines": 12, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [79, 80, 81], "excluded_lines": [], "start_line": 63}, "update_role": {"executed_lines": [92, 93, 94, 95, 96, 101, 103, 104, 105, 106, 107, 108, 112], "summary": {"covered_lines": 13, "num_statements": 17, "percent_covered": 76.47058823529412, "percent_covered_display": "76", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 76.47058823529412, "percent_statements_covered_display": "76"}, "missing_lines": [102, 109, 110, 111], "excluded_lines": [], "start_line": 86}, "delete_role": {"executed_lines": [122, 123, 133, 134, 135, 136, 137, 138, 139, 140, 141, 145], "summary": {"covered_lines": 12, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [142, 143, 144], "excluded_lines": [], "start_line": 116}, "list_role_permissions": {"executed_lines": [157, 158, 163, 165], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [164], "excluded_lines": [], "start_line": 151}, "add_permission_to_role": {"executed_lines": [186, 187, 188, 189, 191, 193, 194, 195, 196, 197, 198, 204], "summary": {"covered_lines": 12, "num_statements": 17, "percent_covered": 70.58823529411765, "percent_covered_display": "71", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 70.58823529411765, "percent_statements_covered_display": "71"}, "missing_lines": [190, 192, 199, 200, 201], "excluded_lines": [], "start_line": 179}, "remove_permission_from_role": {"executed_lines": [220, 221, 222, 223, 225, 227, 228, 229, 230, 231, 232, 238], "summary": {"covered_lines": 12, "num_statements": 17, "percent_covered": 70.58823529411765, "percent_covered_display": "71", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 70.58823529411765, "percent_statements_covered_display": "71"}, "missing_lines": [224, 226, 233, 234, 235], "excluded_lines": [], "start_line": 213}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 22, 23, 62, 63, 85, 86, 115, 116, 150, 151, 174, 179, 210, 213], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 19, 22, 23, 30, 31, 37, 38, 39, 40, 46, 49, 50, 51, 54, 62, 63, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 82, 85, 86, 92, 93, 94, 95, 96, 101, 103, 104, 105, 106, 107, 108, 112, 115, 116, 122, 123, 133, 134, 135, 136, 137, 138, 139, 140, 141, 145, 150, 151, 157, 158, 163, 165, 174, 179, 186, 187, 188, 189, 191, 193, 194, 195, 196, 197, 198, 204, 210, 213, 220, 221, 222, 223, 225, 227, 228, 229, 230, 231, 232, 238], "summary": {"covered_lines": 106, "num_statements": 128, "percent_covered": 82.8125, "percent_covered_display": "83", "missing_lines": 22, "excluded_lines": 0, "percent_statements_covered": 82.8125, "percent_statements_covered_display": "83"}, "missing_lines": [32, 79, 80, 81, 102, 109, 110, 111, 142, 143, 144, 164, 190, 192, 199, 200, 201, 224, 226, 233, 234, 235], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/routes/simulation.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 18, 21, 22, 28, 29, 30, 34, 35, 51, 70, 71, 77, 78, 89, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 108, 110, 112, 113, 114, 115, 116], "summary": {"covered_lines": 40, "num_statements": 51, "percent_covered": 78.43137254901961, "percent_covered_display": "78", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 78.43137254901961, "percent_statements_covered_display": "78"}, "missing_lines": [31, 43, 44, 47, 48, 81, 82, 85, 86, 109, 111], "excluded_lines": [], "functions": {"simulate_policy": {"executed_lines": [28, 29, 30, 34, 35, 51], "summary": {"covered_lines": 6, "num_statements": 11, "percent_covered": 54.54545454545455, "percent_covered_display": "55", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 54.54545454545455, "percent_statements_covered_display": "55"}, "missing_lines": [31, 43, 44, 47, 48], "excluded_lines": [], "start_line": 22}, "impact_analysis": {"executed_lines": [77, 78, 89], "summary": {"covered_lines": 3, "num_statements": 7, "percent_covered": 42.857142857142854, "percent_covered_display": "43", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 42.857142857142854, "percent_statements_covered_display": "43"}, "missing_lines": [81, 82, 85, 86], "excluded_lines": [], "start_line": 71}, "_normalize_request": {"executed_lines": [96, 97, 98, 99, 100, 101, 102, 103, 104, 108, 110, 112, 113, 114, 115, 116], "summary": {"covered_lines": 16, "num_statements": 18, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89"}, "missing_lines": [109, 111], "excluded_lines": [], "start_line": 95}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 18, 21, 22, 70, 71, 95], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 8, 9, 10, 11, 18, 21, 22, 28, 29, 30, 34, 35, 51, 70, 71, 77, 78, 89, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 108, 110, 112, 113, 114, 115, 116], "summary": {"covered_lines": 40, "num_statements": 51, "percent_covered": 78.43137254901961, "percent_covered_display": "78", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 78.43137254901961, "percent_statements_covered_display": "78"}, "missing_lines": [31, 43, 44, 47, 48, 81, 82, 85, 86, 109, 111], "excluded_lines": [], "start_line": 1}}}, "keynetra/api/service_modes.py": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 45], "summary": {"covered_lines": 37, "num_statements": 38, "percent_covered": 97.36842105263158, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.36842105263158, "percent_statements_covered_display": "97"}, "missing_lines": [43], "excluded_lines": [], "functions": {"router_for_mode": {"executed_lines": [22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 45], "summary": {"covered_lines": 20, "num_statements": 21, "percent_covered": 95.23809523809524, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 95.23809523809524, "percent_statements_covered_display": "95"}, "missing_lines": [43], "excluded_lines": [], "start_line": 21}, "": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 21, 22, 23, 24, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 45], "summary": {"covered_lines": 37, "num_statements": 38, "percent_covered": 97.36842105263158, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.36842105263158, "percent_statements_covered_display": "97"}, "missing_lines": [43], "excluded_lines": [], "start_line": 1}}}, "keynetra/cli.py": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, 45, 48, 54, 55, 56, 57, 58, 59, 60, 63, 64, 70, 72, 75, 76, 77, 78, 79, 80, 83, 84, 85, 86, 87, 88, 90, 93, 94, 95, 96, 99, 100, 101, 102, 103, 104, 105, 106, 108, 111, 112, 121, 122, 123, 124, 131, 132, 141, 142, 143, 144, 151, 154, 156, 157, 164, 165, 166, 174, 175, 178, 181, 182, 183, 184, 185, 186, 187, 188, 193, 194, 197, 198, 199, 201, 202, 206, 220, 222, 229, 230, 231, 232, 233, 234, 235, 237, 244, 245, 246, 247, 248, 249, 251, 258, 259, 260, 261, 262, 264, 265, 266, 275, 284, 295, 304, 305, 308, 311, 312, 321, 322, 323, 329, 330, 333, 334, 337, 384, 385, 394, 396, 397, 399, 400, 401, 402, 403, 404, 405, 406, 407, 413, 414, 417, 418, 426, 428, 429, 430, 431, 432, 433, 435, 437, 453, 454, 465, 466, 468, 469, 470, 471, 472, 473, 479, 480, 483, 484, 491, 492, 493, 494, 497, 498, 502, 503, 504, 507, 508, 519, 520, 521, 530, 536, 537, 540, 541, 548, 549, 550, 556, 557, 560, 561, 570, 572, 573, 574, 575, 576, 577, 578, 588, 590, 605, 606, 613, 614, 615, 617, 618, 619, 624, 625, 628, 629, 640, 641, 642, 643, 644, 646, 647, 648, 668, 669, 693, 694, 734, 735, 741, 743, 744, 745, 746, 754, 756, 757, 758, 761, 762, 798, 806, 807, 808, 810, 811, 812, 813, 814, 815, 816, 818, 819, 822, 823, 825, 826, 827, 828, 829, 830, 833, 834, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 862, 865, 866, 875, 876, 877, 878, 879, 880, 881, 882, 883, 892, 895, 896, 897, 899, 902, 903, 908, 909, 910, 911, 912, 913, 916, 918, 921, 922, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 942, 943, 944, 946, 949, 950, 951, 952, 958, 959, 962, 963, 964, 980, 981, 984, 985, 988], "summary": {"covered_lines": 362, "num_statements": 431, "percent_covered": 83.9907192575406, "percent_covered_display": "84", "missing_lines": 69, "excluded_lines": 0, "percent_statements_covered": 83.9907192575406, "percent_statements_covered_display": "84"}, "missing_lines": [71, 89, 107, 189, 190, 191, 203, 204, 294, 408, 409, 410, 411, 412, 650, 652, 653, 675, 677, 678, 679, 680, 681, 682, 684, 685, 686, 687, 689, 690, 702, 704, 705, 706, 707, 708, 709, 711, 712, 713, 714, 715, 717, 718, 719, 720, 730, 731, 747, 748, 749, 750, 751, 752, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 783, 794, 795, 824, 989], "excluded_lines": [], "functions": {"cli_root": {"executed_lines": [70, 72], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [71], "excluded_lines": [], "start_line": 64}, "_load_config": {"executed_lines": [76, 77, 78, 79, 80], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 75}, "_effective_config_path": {"executed_lines": [84, 85, 86, 87, 88, 90], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [89], "excluded_lines": [], "start_line": 83}, "_maybe_load_config": {"executed_lines": [94, 95, 96], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "_resolve_url": {"executed_lines": [100, 101, 102, 103, 104, 105, 106, 108], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 88.88888888888889, "percent_statements_covered_display": "89"}, "missing_lines": [107], "excluded_lines": [], "start_line": 99}, "start": {"executed_lines": [121, 122, 123, 124], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 112}, "serve": {"executed_lines": [141, 142, 143, 144], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 132}, "_run_server": {"executed_lines": [154, 156, 157, 164, 165, 166, 174, 175], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 151}, "_render_startup_screen": {"executed_lines": [181, 182, 183, 184, 185, 186, 187, 188, 193, 194, 197, 198, 199, 201, 202, 206, 220, 222, 229, 230, 231, 232, 233, 234, 235, 237, 244, 245, 246, 247, 248, 249, 251, 258, 259, 260, 261, 262, 264, 265, 266, 275, 284, 295], "summary": {"covered_lines": 44, "num_statements": 50, "percent_covered": 88.0, "percent_covered_display": "88", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 88.0, "percent_statements_covered_display": "88"}, "missing_lines": [189, 190, 191, 203, 204, 294], "excluded_lines": [], "start_line": 178}, "version": {"executed_lines": [308], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 305}, "admin_login": {"executed_lines": [321, 322, 323, 329, 330], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 312}, "help_cli": {"executed_lines": [337], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 334}, "migrate": {"executed_lines": [394, 396, 397, 399, 400, 401, 402, 403, 404, 405, 406, 407, 413, 414], "summary": {"covered_lines": 14, "num_statements": 19, "percent_covered": 73.6842105263158, "percent_covered_display": "74", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 73.6842105263158, "percent_statements_covered_display": "74"}, "missing_lines": [408, 409, 410, 411, 412], "excluded_lines": [], "start_line": 385}, "seed_data": {"executed_lines": [426, 428, 429, 430, 431, 432, 433, 435, 437], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 418}, "check": {"executed_lines": [465, 466, 468, 469, 470, 471, 472, 473, 479, 480], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 454}, "model_apply": {"executed_lines": [491, 492, 493, 494], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 484}, "model_show": {"executed_lines": [502, 503, 504], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 498}, "simulate": {"executed_lines": [519, 520, 521, 530, 536, 537], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 508}, "impact": {"executed_lines": [548, 549, 550, 556, 557], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 541}, "explain": {"executed_lines": [570, 572, 573, 574, 575, 576, 577, 578, 588, 590], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 561}, "test_policy": {"executed_lines": [613, 614, 615, 617, 618, 619, 624, 625], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 606}, "compile_policies": {"executed_lines": [640, 641, 642, 643, 644, 646, 647, 648], "summary": {"covered_lines": 8, "num_statements": 11, "percent_covered": 72.72727272727273, "percent_covered_display": "73", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 72.72727272727273, "percent_statements_covered_display": "73"}, "missing_lines": [650, 652, 653], "excluded_lines": [], "start_line": 629}, "generate_openapi": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 13, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [675, 677, 678, 679, 680, 681, 682, 684, 685, 686, 687, 689, 690], "excluded_lines": [], "start_line": 669}, "check_openapi": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [702, 704, 705, 706, 707, 708, 709, 711, 712, 713, 714, 715, 717, 718, 719, 720, 730, 731], "excluded_lines": [], "start_line": 694}, "doctor": {"executed_lines": [741, 743, 744, 745, 746, 754, 756, 757, 758], "summary": {"covered_lines": 9, "num_statements": 15, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [747, 748, 749, 750, 751, 752], "excluded_lines": [], "start_line": 735}, "config_doctor": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 13, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 783, 794, 795], "excluded_lines": [], "start_line": 762}, "_run_benchmark": {"executed_lines": [806, 807, 808, 810, 818, 819], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 798}, "_run_benchmark.send_request": {"executed_lines": [811, 812, 813, 814, 815, 816], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 810}, "_percentile": {"executed_lines": [823, 825, 826, 827, 828, 829, 830], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [824], "excluded_lines": [], "start_line": 822}, "benchmark": {"executed_lines": [843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 862], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 834}, "acl_add": {"executed_lines": [875, 876, 877, 878, 879, 880, 881, 882, 883, 892, 895, 896, 897, 899], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 866}, "acl_list": {"executed_lines": [908, 909, 910, 911, 912, 913, 916, 918], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 903}, "acl_remove": {"executed_lines": [926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 942, 943, 944, 946], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 922}, "_read_applied_revisions": {"executed_lines": [950, 951, 952, 958, 959], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 949}, "_build_authorization_service": {"executed_lines": [963, 964], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 962}, "_coerce_scalar": {"executed_lines": [981], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 980}, "main": {"executed_lines": [985], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 984}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, 45, 48, 54, 55, 56, 57, 58, 59, 60, 63, 64, 75, 83, 93, 99, 111, 112, 131, 132, 151, 178, 304, 305, 311, 312, 333, 334, 384, 385, 417, 418, 453, 454, 483, 484, 497, 498, 507, 508, 540, 541, 560, 561, 605, 606, 628, 629, 668, 669, 693, 694, 734, 735, 761, 762, 798, 822, 833, 834, 865, 866, 902, 903, 921, 922, 949, 962, 980, 984, 988], "summary": {"covered_lines": 105, "num_statements": 106, "percent_covered": 99.05660377358491, "percent_covered_display": "99", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 99.05660377358491, "percent_statements_covered_display": "99"}, "missing_lines": [989], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 40, 41, 42, 43, 44, 45, 48, 54, 55, 56, 57, 58, 59, 60, 63, 64, 70, 72, 75, 76, 77, 78, 79, 80, 83, 84, 85, 86, 87, 88, 90, 93, 94, 95, 96, 99, 100, 101, 102, 103, 104, 105, 106, 108, 111, 112, 121, 122, 123, 124, 131, 132, 141, 142, 143, 144, 151, 154, 156, 157, 164, 165, 166, 174, 175, 178, 181, 182, 183, 184, 185, 186, 187, 188, 193, 194, 197, 198, 199, 201, 202, 206, 220, 222, 229, 230, 231, 232, 233, 234, 235, 237, 244, 245, 246, 247, 248, 249, 251, 258, 259, 260, 261, 262, 264, 265, 266, 275, 284, 295, 304, 305, 308, 311, 312, 321, 322, 323, 329, 330, 333, 334, 337, 384, 385, 394, 396, 397, 399, 400, 401, 402, 403, 404, 405, 406, 407, 413, 414, 417, 418, 426, 428, 429, 430, 431, 432, 433, 435, 437, 453, 454, 465, 466, 468, 469, 470, 471, 472, 473, 479, 480, 483, 484, 491, 492, 493, 494, 497, 498, 502, 503, 504, 507, 508, 519, 520, 521, 530, 536, 537, 540, 541, 548, 549, 550, 556, 557, 560, 561, 570, 572, 573, 574, 575, 576, 577, 578, 588, 590, 605, 606, 613, 614, 615, 617, 618, 619, 624, 625, 628, 629, 640, 641, 642, 643, 644, 646, 647, 648, 668, 669, 693, 694, 734, 735, 741, 743, 744, 745, 746, 754, 756, 757, 758, 761, 762, 798, 806, 807, 808, 810, 811, 812, 813, 814, 815, 816, 818, 819, 822, 823, 825, 826, 827, 828, 829, 830, 833, 834, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 862, 865, 866, 875, 876, 877, 878, 879, 880, 881, 882, 883, 892, 895, 896, 897, 899, 902, 903, 908, 909, 910, 911, 912, 913, 916, 918, 921, 922, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 942, 943, 944, 946, 949, 950, 951, 952, 958, 959, 962, 963, 964, 980, 981, 984, 985, 988], "summary": {"covered_lines": 362, "num_statements": 431, "percent_covered": 83.9907192575406, "percent_covered_display": "84", "missing_lines": 69, "excluded_lines": 0, "percent_statements_covered": 83.9907192575406, "percent_statements_covered_display": "84"}, "missing_lines": [71, 89, 107, 189, 190, 191, 203, 204, 294, 408, 409, 410, 411, 412, 650, 652, 653, 675, 677, 678, 679, 680, 681, 682, 684, 685, 686, 687, 689, 690, 702, 704, 705, 706, 707, 708, 709, 711, 712, 713, 714, 715, 717, 718, 719, 720, 730, 731, 747, 748, 749, 750, 751, 752, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 783, 794, 795, 824, 989], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/admin_auth.py": {"executed_lines": [1, 3, 4, 6, 8, 9, 10, 11, 13, 16, 17, 18, 19, 20, 23, 24, 25, 27, 32, 33, 34, 37, 38, 39, 45, 46, 56, 57, 58, 59, 61, 64, 65, 66, 67, 68, 69, 70, 71, 73, 74, 76, 77, 80, 81, 82, 83, 84, 85, 86, 89, 90, 91, 92, 93, 95, 96, 99, 101, 102, 104, 105, 106, 108, 109, 110, 111, 112, 117, 120, 121, 124, 125, 127, 128, 129, 131, 132, 139, 140], "summary": {"covered_lines": 80, "num_statements": 86, "percent_covered": 93.02325581395348, "percent_covered_display": "93", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 93.02325581395348, "percent_statements_covered_display": "93"}, "missing_lines": [72, 78, 94, 100, 114, 142], "excluded_lines": [], "functions": {"require_management_role": {"executed_lines": [24, 25, 27, 61], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "require_management_role.dependency": {"executed_lines": [32, 33, 34, 37, 38, 39, 45, 46, 56, 57, 58, 59], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "_resolve_tenant_role": {"executed_lines": [65, 66, 67, 68, 69, 70, 71, 73, 74, 76, 77, 80, 81, 82, 83, 84, 85, 86, 89, 90, 91, 92, 93, 95, 96, 99, 101, 102, 104, 105, 106, 108, 109, 110, 111, 112], "summary": {"covered_lines": 36, "num_statements": 41, "percent_covered": 87.8048780487805, "percent_covered_display": "88", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 87.8048780487805, "percent_statements_covered_display": "88"}, "missing_lines": [72, 78, 94, 100, 114], "excluded_lines": [], "start_line": 64}, "_resolve_request_tenant_key": {"executed_lines": [120, 121, 124, 125, 127, 128, 129, 131, 132, 139, 140], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [142], "excluded_lines": [], "start_line": 117}, "": {"executed_lines": [1, 3, 4, 6, 8, 9, 10, 11, 13, 16, 17, 18, 19, 20, 23, 64, 117], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AdminAccess": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "": {"executed_lines": [1, 3, 4, 6, 8, 9, 10, 11, 13, 16, 17, 18, 19, 20, 23, 24, 25, 27, 32, 33, 34, 37, 38, 39, 45, 46, 56, 57, 58, 59, 61, 64, 65, 66, 67, 68, 69, 70, 71, 73, 74, 76, 77, 80, 81, 82, 83, 84, 85, 86, 89, 90, 91, 92, 93, 95, 96, 99, 101, 102, 104, 105, 106, 108, 109, 110, 111, 112, 117, 120, 121, 124, 125, 127, 128, 129, 131, 132, 139, 140], "summary": {"covered_lines": 80, "num_statements": 86, "percent_covered": 93.02325581395348, "percent_covered_display": "93", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 93.02325581395348, "percent_statements_covered_display": "93"}, "missing_lines": [72, 78, 94, 100, 114, 142], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/config_loader.py": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 30, 32, 33, 35, 36, 37, 38, 39, 41, 42, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 66, 67, 68, 69, 70, 71, 72, 73, 75, 86, 89, 90, 92, 93, 94, 95, 96, 97, 98, 100, 103, 104, 105, 106, 107, 110, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 128, 129, 130], "summary": {"covered_lines": 88, "num_statements": 104, "percent_covered": 84.61538461538461, "percent_covered_display": "85", "missing_lines": 16, "excluded_lines": 2, "percent_statements_covered": 84.61538461538461, "percent_statements_covered_display": "85"}, "missing_lines": [34, 43, 91, 99, 116, 124, 125, 131, 132, 133, 134, 135, 136, 137, 138, 139], "excluded_lines": [12, 13], "functions": {"load_config_file": {"executed_lines": [28, 29, 30, 32, 33, 35, 36, 37, 38, 39, 41, 42, 44, 45, 46], "summary": {"covered_lines": 15, "num_statements": 17, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 88.23529411764706, "percent_statements_covered_display": "88"}, "missing_lines": [34, 43], "excluded_lines": [], "start_line": 27}, "apply_config_to_environment": {"executed_lines": [50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "_normalize_config": {"executed_lines": [67, 68, 69, 70, 71, 72, 73, 75], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 66}, "_paths_from_payload": {"executed_lines": [89, 90, 92, 93, 94, 95, 96, 97, 98, 100], "summary": {"covered_lines": 10, "num_statements": 12, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [91, 99], "excluded_lines": [], "start_line": 86}, "_nested": {"executed_lines": [104, 105, 106, 107], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 103}, "_as_str": {"executed_lines": [111, 112, 113, 114, 115], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [116], "excluded_lines": [], "start_line": 110}, "_as_int": {"executed_lines": [120, 121, 122, 123], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [124, 125], "excluded_lines": [], "start_line": 119}, "_as_bool": {"executed_lines": [129, 130], "summary": {"covered_lines": 2, "num_statements": 11, "percent_covered": 18.181818181818183, "percent_covered_display": "18", "missing_lines": 9, "excluded_lines": 0, "percent_statements_covered": 18.181818181818183, "percent_statements_covered_display": "18"}, "missing_lines": [131, 132, 133, 134, 135, 136, 137, 138, 139], "excluded_lines": [], "start_line": 128}, "": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 24, 27, 49, 66, 86, 103, 110, 119, 128], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [12, 13], "start_line": 1}}, "classes": {"KeyNetraFileConfig": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 16, 17, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 30, 32, 33, 35, 36, 37, 38, 39, 41, 42, 44, 45, 46, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 66, 67, 68, 69, 70, 71, 72, 73, 75, 86, 89, 90, 92, 93, 94, 95, 96, 97, 98, 100, 103, 104, 105, 106, 107, 110, 111, 112, 113, 114, 115, 119, 120, 121, 122, 123, 128, 129, 130], "summary": {"covered_lines": 88, "num_statements": 104, "percent_covered": 84.61538461538461, "percent_covered_display": "85", "missing_lines": 16, "excluded_lines": 2, "percent_statements_covered": 84.61538461538461, "percent_statements_covered_display": "85"}, "missing_lines": [34, 43, 91, 99, 116, 124, 125, 131, 132, 133, 134, 135, 136, 137, 138, 139], "excluded_lines": [12, 13], "start_line": 1}}}, "keynetra/config/file_loaders.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 14, 15, 16, 17, 18, 19, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 53, 54, 55, 56, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 80, 81, 82, 83, 86, 87, 89, 90, 91, 92, 95, 97, 98, 99, 100, 101, 102, 103, 104, 107, 108, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 133, 136, 137, 138, 139, 143, 144, 145, 146, 147, 148, 149, 150, 151, 160, 161, 162, 163, 164, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 191, 194, 195, 196, 198, 199, 200, 201, 202, 203, 204, 205, 206, 209, 210, 211, 212, 213, 214, 215, 216, 218, 219, 221, 222, 223, 224, 225, 228, 229, 230, 232, 233, 234, 235, 236, 239, 240, 242, 243, 244, 246], "summary": {"covered_lines": 160, "num_statements": 178, "percent_covered": 89.88764044943821, "percent_covered_display": "90", "missing_lines": 18, "excluded_lines": 2, "percent_statements_covered": 89.88764044943821, "percent_statements_covered_display": "90"}, "missing_lines": [42, 57, 58, 59, 60, 61, 62, 77, 88, 93, 109, 117, 141, 142, 165, 197, 226, 237], "excluded_lines": [10, 11], "functions": {"load_policies_from_paths": {"executed_lines": [15, 16, 17, 18, 19, 27, 28, 29, 30, 31, 32], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 14}, "load_policies_from_file": {"executed_lines": [36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 92.85714285714286, "percent_statements_covered_display": "93"}, "missing_lines": [42], "excluded_lines": [], "start_line": 35}, "load_authorization_model_from_paths": {"executed_lines": [54, 55, 56, 63, 64, 65, 66, 67], "summary": {"covered_lines": 8, "num_statements": 14, "percent_covered": 57.142857142857146, "percent_covered_display": "57", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 57.142857142857146, "percent_statements_covered_display": "57"}, "missing_lines": [57, 58, 59, 60, 61, 62], "excluded_lines": [], "start_line": 53}, "_load_model_file_if_supported": {"executed_lines": [71, 72, 73, 74, 75, 76], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [77], "excluded_lines": [], "start_line": 70}, "load_authorization_model_from_file": {"executed_lines": [81, 82, 83, 86, 87, 89, 90, 91, 92, 95, 97, 98, 99, 100, 101, 102, 103, 104], "summary": {"covered_lines": 18, "num_statements": 20, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [88, 93], "excluded_lines": [], "start_line": 80}, "_normalize_policy_payload": {"executed_lines": [108, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 133], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [109, 117], "excluded_lines": [], "start_line": 107}, "_policy_from_effect_block": {"executed_lines": [137, 138, 139, 143, 144, 145, 146, 147, 148, 149, 150, 151], "summary": {"covered_lines": 12, "num_statements": 14, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [141, 142], "excluded_lines": [], "start_line": 136}, "_parse_polar_policy_lines": {"executed_lines": [161, 162, 163, 164, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 191], "summary": {"covered_lines": 22, "num_statements": 23, "percent_covered": 95.65217391304348, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 95.65217391304348, "percent_statements_covered_display": "96"}, "missing_lines": [165], "excluded_lines": [], "start_line": 160}, "_coerce_scalar": {"executed_lines": [195, 196, 198, 199, 200, 201, 202, 203, 204, 205, 206], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [197], "excluded_lines": [], "start_line": 194}, "_model_mapping_to_schema": {"executed_lines": [210, 211, 212, 213, 214, 215, 216, 218, 219, 221, 222, 223, 224, 225, 228, 229, 230, 232, 233, 234, 235, 236, 239, 240, 242, 243, 244, 246], "summary": {"covered_lines": 28, "num_statements": 30, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93"}, "missing_lines": [226, 237], "excluded_lines": [], "start_line": 209}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 14, 35, 53, 70, 80, 107, 136, 160, 194, 209], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [10, 11], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 14, 15, 16, 17, 18, 19, 27, 28, 29, 30, 31, 32, 35, 36, 37, 38, 40, 41, 43, 44, 45, 46, 47, 48, 49, 50, 53, 54, 55, 56, 63, 64, 65, 66, 67, 70, 71, 72, 73, 74, 75, 76, 80, 81, 82, 83, 86, 87, 89, 90, 91, 92, 95, 97, 98, 99, 100, 101, 102, 103, 104, 107, 108, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 133, 136, 137, 138, 139, 143, 144, 145, 146, 147, 148, 149, 150, 151, 160, 161, 162, 163, 164, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 191, 194, 195, 196, 198, 199, 200, 201, 202, 203, 204, 205, 206, 209, 210, 211, 212, 213, 214, 215, 216, 218, 219, 221, 222, 223, 224, 225, 228, 229, 230, 232, 233, 234, 235, 236, 239, 240, 242, 243, 244, 246], "summary": {"covered_lines": 160, "num_statements": 178, "percent_covered": 89.88764044943821, "percent_covered_display": "90", "missing_lines": 18, "excluded_lines": 2, "percent_statements_covered": 89.88764044943821, "percent_statements_covered_display": "90"}, "missing_lines": [42, 57, 58, 59, 60, 61, 62, 77, 88, 93, 109, 117, 141, 142, 165, 197, 226, 237], "excluded_lines": [10, 11], "start_line": 1}}}, "keynetra/config/policies.py": {"executed_lines": [3, 5, 7], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 5, 7], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 7], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/rate_limit.py": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 21, 24, 25, 26, 27, 30, 31, 32, 33, 73, 74, 75, 76, 77, 78, 80, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 119, 120, 121, 122, 123, 124, 127, 128, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 150, 151, 170, 171, 172, 173, 174], "summary": {"covered_lines": 85, "num_statements": 85, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"RateLimitMiddleware.__init__": {"executed_lines": [75, 76, 77, 78], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 74}, "RateLimitMiddleware.dispatch": {"executed_lines": [81, 82, 84, 85, 86, 87, 88, 89, 90, 91], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 80}, "RateLimitMiddleware._consume": {"executed_lines": [94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 119, 120, 121, 122, 123, 124, 127, 128, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "RateLimitMiddleware._limited_response": {"executed_lines": [151], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 150}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 21, 24, 25, 26, 27, 30, 31, 32, 33, 73, 74, 80, 93, 150, 170, 171, 172, 173, 174], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"_LocalBucket": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "RateLimitMiddleware": {"executed_lines": [75, 76, 77, 78, 81, 82, 84, 85, 86, 87, 88, 89, 90, 91, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 106, 107, 108, 109, 119, 120, 121, 122, 123, 124, 127, 128, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 151], "summary": {"covered_lines": 52, "num_statements": 52, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 73}, "_BucketDecision": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 171}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 17, 18, 19, 21, 24, 25, 26, 27, 30, 31, 32, 33, 73, 74, 80, 93, 150, 170, 171, 172, 173, 174], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/redis_client.py": {"executed_lines": [1, 3, 4, 6, 7, 11, 14, 15, 16, 17, 18, 19], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 9], "functions": {"get_redis": {"executed_lines": [16, 17, 18, 19], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "": {"executed_lines": [1, 3, 4, 6, 7, 11, 14, 15], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 9], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 11, 14, 15, 16, 17, 18, 19], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 9], "start_line": 1}}}, "keynetra/config/sample_data.py": {"executed_lines": [1, 3, 4, 6, 8, 13, 17, 22, 32, 49, 60], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [61], "excluded_lines": [], "functions": {"sample_bootstrap_document": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [61], "excluded_lines": [], "start_line": 60}, "": {"executed_lines": [1, 3, 4, 6, 8, 13, 17, 22, 32, 49, 60], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 8, 13, 17, 22, 32, 49, 60], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [61], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/security.py": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 27, 43, 44, 47, 48, 49, 62, 63, 64, 67, 68, 69, 70, 75, 115, 121, 122, 123, 124, 125, 126, 127, 128, 129, 134, 135, 140, 142, 147, 148, 150, 151, 152, 153, 161, 164, 165, 166, 167, 168, 170, 171], "summary": {"covered_lines": 62, "num_statements": 106, "percent_covered": 58.490566037735846, "percent_covered_display": "58", "missing_lines": 44, "excluded_lines": 0, "percent_statements_covered": 58.490566037735846, "percent_statements_covered_display": "58"}, "missing_lines": [28, 29, 30, 31, 32, 33, 34, 35, 38, 39, 40, 76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 110, 111, 112, 141, 154], "excluded_lines": [], "functions": {"_decode_with_jwks": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [28, 29, 30, 31, 32, 33, 34, 35, 38, 39, 40], "excluded_lines": [], "start_line": 27}, "_unauthorized": {"executed_lines": [44], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43}, "_log_failed_auth": {"executed_lines": [48, 49], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "_matches_api_key": {"executed_lines": [63, 64], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 62}, "_scopes_are_defined": {"executed_lines": [68, 69, 70], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 67}, "_get_jwks": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 31, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 31, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 110, 111, 112], "excluded_lines": [], "start_line": 75}, "get_principal": {"executed_lines": [121, 122, 123, 124, 125, 126, 127, 128, 129, 134, 135, 140, 142, 147, 148, 150, 151, 152, 153, 161, 164, 165, 166, 167, 168, 170, 171], "summary": {"covered_lines": 27, "num_statements": 29, "percent_covered": 93.10344827586206, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 93.10344827586206, "percent_statements_covered_display": "93"}, "missing_lines": [141, 154], "excluded_lines": [], "start_line": 115}, "": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 27, 43, 47, 62, 67, 75, 115], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 6, 7, 8, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 23, 24, 27, 43, 44, 47, 48, 49, 62, 63, 64, 67, 68, 69, 70, 75, 115, 121, 122, 123, 124, 125, 126, 127, 128, 129, 134, 135, 140, 142, 147, 148, 150, 151, 152, 153, 161, 164, 165, 166, 167, 168, 170, 171], "summary": {"covered_lines": 62, "num_statements": 106, "percent_covered": 58.490566037735846, "percent_covered_display": "58", "missing_lines": 44, "excluded_lines": 0, "percent_statements_covered": 58.490566037735846, "percent_statements_covered_display": "58"}, "missing_lines": [28, 29, 30, 31, 32, 33, 34, 35, 38, 39, 40, 76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 90, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 110, 111, 112, 141, 154], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/settings.py": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 11, 13, 14, 17, 18, 20, 21, 23, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65, 68, 69, 70, 71, 72, 74, 75, 76, 77, 78, 80, 82, 83, 84, 85, 87, 89, 90, 91, 92, 94, 96, 97, 98, 99, 101, 103, 104, 105, 106, 108, 110, 111, 112, 113, 114, 115, 117, 119, 120, 121, 122, 124, 126, 127, 128, 129, 131, 133, 134, 135, 140, 141, 145, 147, 151, 154, 155, 158, 160, 162, 163, 164, 165, 171, 173, 174, 178, 181, 183, 184, 185, 186, 188, 189, 191, 193, 194, 195, 196, 198, 199, 200, 201, 203, 204, 205, 206, 207, 210, 212, 213, 214, 216, 217, 218, 219, 226, 228, 229, 231, 232, 234, 236, 237, 238, 239, 242, 243, 244, 245, 249, 250, 251, 254, 255], "summary": {"covered_lines": 165, "num_statements": 194, "percent_covered": 85.05154639175258, "percent_covered_display": "85", "missing_lines": 29, "excluded_lines": 0, "percent_statements_covered": 85.05154639175258, "percent_statements_covered_display": "85"}, "missing_lines": [79, 86, 93, 100, 107, 116, 123, 130, 142, 146, 148, 152, 156, 159, 166, 168, 169, 170, 175, 176, 179, 190, 208, 209, 211, 215, 233, 240, 246], "excluded_lines": [], "functions": {"Settings._validate_environment": {"executed_lines": [77, 78, 80], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [79], "excluded_lines": [], "start_line": 76}, "Settings._validate_service_timeout": {"executed_lines": [85, 87], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [86], "excluded_lines": [], "start_line": 84}, "Settings._validate_retry_attempts": {"executed_lines": [92, 94], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [93], "excluded_lines": [], "start_line": 91}, "Settings._validate_rate_limit_per_minute": {"executed_lines": [99, 101], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [100], "excluded_lines": [], "start_line": 98}, "Settings._validate_rate_limit_window_seconds": {"executed_lines": [106, 108], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [107], "excluded_lines": [], "start_line": 105}, "Settings._validate_rate_limit_burst": {"executed_lines": [113, 114, 115, 117], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [116], "excluded_lines": [], "start_line": 112}, "Settings._validate_jwks_cache_ttl_seconds": {"executed_lines": [122, 124], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [123], "excluded_lines": [], "start_line": 121}, "Settings._validate_jwks_backoff_max_seconds": {"executed_lines": [129, 131], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [130], "excluded_lines": [], "start_line": 128}, "Settings._validate_security_profile": {"executed_lines": [135, 140, 141, 145, 147, 151, 154, 155, 158, 160], "summary": {"covered_lines": 10, "num_statements": 16, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 62.5, "percent_statements_covered_display": "62"}, "missing_lines": [142, 146, 148, 152, 156, 159], "excluded_lines": [], "start_line": 134}, "Settings.load_policies": {"executed_lines": [163, 164, 165, 171, 173, 174, 178, 181], "summary": {"covered_lines": 8, "num_statements": 15, "percent_covered": 53.333333333333336, "percent_covered_display": "53", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 53.333333333333336, "percent_statements_covered_display": "53"}, "missing_lines": [166, 168, 169, 170, 175, 176, 179], "excluded_lines": [], "start_line": 162}, "Settings.parsed_policy_paths": {"executed_lines": [184, 185, 186], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 183}, "Settings.parsed_model_paths": {"executed_lines": [189, 191], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [190], "excluded_lines": [], "start_line": 188}, "Settings.parsed_api_keys": {"executed_lines": [194, 195, 196], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 193}, "Settings.parsed_api_key_hashes": {"executed_lines": [199, 200, 201], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 198}, "Settings.parsed_api_key_scopes": {"executed_lines": [204, 205, 206, 207, 210, 212, 213, 214, 216, 217, 218, 219, 226], "summary": {"covered_lines": 13, "num_statements": 17, "percent_covered": 76.47058823529412, "percent_covered_display": "76", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 76.47058823529412, "percent_statements_covered_display": "76"}, "missing_lines": [208, 209, 211, 215], "excluded_lines": [], "start_line": 203}, "Settings.is_development": {"executed_lines": [229], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 228}, "Settings.parsed_cors_allow_origins": {"executed_lines": [232, 234], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [233], "excluded_lines": [], "start_line": 231}, "Settings.parsed_cors_allow_methods": {"executed_lines": [237, 238, 239], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [240], "excluded_lines": [], "start_line": 236}, "Settings.parsed_cors_allow_headers": {"executed_lines": [243, 244, 245], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [246], "excluded_lines": [], "start_line": 242}, "get_settings": {"executed_lines": [251], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 250}, "reset_settings_cache": {"executed_lines": [255], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 254}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 11, 13, 14, 17, 18, 20, 21, 23, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65, 68, 69, 70, 71, 72, 74, 75, 76, 82, 83, 84, 89, 90, 91, 96, 97, 98, 103, 104, 105, 110, 111, 112, 119, 120, 121, 126, 127, 128, 133, 134, 162, 183, 188, 193, 198, 203, 228, 231, 236, 242, 249, 250, 254], "summary": {"covered_lines": 93, "num_statements": 93, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"Settings": {"executed_lines": [77, 78, 80, 85, 87, 92, 94, 99, 101, 106, 108, 113, 114, 115, 117, 122, 124, 129, 131, 135, 140, 141, 145, 147, 151, 154, 155, 158, 160, 163, 164, 165, 171, 173, 174, 178, 181, 184, 185, 186, 189, 191, 194, 195, 196, 199, 200, 201, 204, 205, 206, 207, 210, 212, 213, 214, 216, 217, 218, 219, 226, 229, 232, 234, 237, 238, 239, 243, 244, 245], "summary": {"covered_lines": 70, "num_statements": 99, "percent_covered": 70.70707070707071, "percent_covered_display": "71", "missing_lines": 29, "excluded_lines": 0, "percent_statements_covered": 70.70707070707071, "percent_statements_covered_display": "71"}, "missing_lines": [79, 86, 93, 100, 107, 116, 123, 130, 142, 146, 148, 152, 156, 159, 166, 168, 169, 170, 175, 176, 179, 190, 208, 209, 211, 215, 233, 240, 246], "excluded_lines": [], "start_line": 17}, "": {"executed_lines": [1, 3, 4, 5, 6, 8, 9, 11, 13, 14, 17, 18, 20, 21, 23, 26, 28, 29, 30, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 65, 68, 69, 70, 71, 72, 74, 75, 76, 82, 83, 84, 89, 90, 91, 96, 97, 98, 103, 104, 105, 110, 111, 112, 119, 120, 121, 126, 127, 128, 133, 134, 162, 183, 188, 193, 198, 203, 228, 231, 236, 242, 249, 250, 251, 254, 255], "summary": {"covered_lines": 95, "num_statements": 95, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/config/tenancy.py": {"executed_lines": [1, 3, 4, 6, 7, 8, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 26, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 53, 56, 57, 58], "summary": {"covered_lines": 43, "num_statements": 44, "percent_covered": 97.72727272727273, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.72727272727273, "percent_statements_covered_display": "98"}, "missing_lines": [22], "excluded_lines": [], "functions": {"get_tenant_key": {"executed_lines": [12], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "normalize_tenant_key": {"executed_lines": [16, 17, 18, 19, 20, 21, 23], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [22], "excluded_lines": [], "start_line": 15}, "tenant_from_principal": {"executed_lines": [27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 53], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "tenant_for_logs": {"executed_lines": [57, 58], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 56}, "": {"executed_lines": [1, 3, 4, 6, 7, 8, 11, 15, 26, 56], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 8, 11, 12, 15, 16, 17, 18, 19, 20, 21, 23, 26, 27, 28, 29, 30, 31, 32, 33, 35, 36, 37, 39, 40, 41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 53, 56, 57, 58], "summary": {"covered_lines": 43, "num_statements": 44, "percent_covered": 97.72727272727273, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.72727272727273, "percent_statements_covered_display": "98"}, "missing_lines": [22], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/acl.py": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 20, 21, 22, 23, 27], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 20, 21, 22, 23, 27], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ResourceACL": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 20, 21, 22, 23, 27], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/audit.py": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 21, 22, 23, 25, 26, 27, 28, 29, 31], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 21, 22, 23, 25, 26, 27, 28, 29, 31], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuditLog": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 17, 18, 19, 21, 22, 23, 25, 26, 27, 28, 29, 31], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/auth_model.py": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 18, 19, 20, 21, 24, 28], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 18, 19, 20, 21, 24, 28], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuthorizationModel": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 18, 19, 20, 21, 24, 28], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/base.py": {"executed_lines": [1, 3, 6, 7], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 6, 7], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"Base": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 6}, "": {"executed_lines": [1, 3, 6, 7], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/idempotency.py": {"executed_lines": [1, 3, 5, 6, 8, 11, 14, 16, 17, 18, 19, 20, 21, 22, 23, 26, 28], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 6, 8, 11, 14, 16, 17, 18, 19, 20, 21, 22, 23, 26, 28], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"IdempotencyRecord": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "": {"executed_lines": [1, 3, 5, 6, 8, 11, 14, 16, 17, 18, 19, 20, 21, 22, 23, 26, 28], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/policy_versioning.py": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 16, 17, 18, 20, 23, 24, 26, 27, 28, 31, 33, 34, 35, 36, 38, 41, 42, 44], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 16, 17, 18, 20, 23, 24, 26, 27, 28, 31, 33, 34, 35, 36, 38, 41, 42, 44], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"Policy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "PolicyVersion": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [1, 3, 5, 6, 8, 11, 12, 14, 15, 16, 17, 18, 20, 23, 24, 26, 27, 28, 31, 33, 34, 35, 36, 38, 41, 42, 44], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/rbac.py": {"executed_lines": [1, 3, 4, 6, 8, 15, 25, 26, 28, 29, 31, 33, 36, 37, 39, 40, 42, 43, 48, 49, 51, 52, 54, 58], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 4, 6, 8, 15, 25, 26, 28, 29, 31, 33, 36, 37, 39, 40, 42, 43, 48, 49, 51, 52, 54, 58], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"User": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "Role": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 36}, "Permission": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48}, "": {"executed_lines": [1, 3, 4, 6, 8, 15, 25, 26, 28, 29, 31, 33, 36, 37, 39, 40, 42, 43, 48, 49, 51, 52, 54, 58], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/relationship.py": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"Relationship": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 9}, "": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 15, 16, 17, 18, 19, 21], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/models/tenant.py": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 14, 15], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 14, 15], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"Tenant": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 9}, "": {"executed_lines": [1, 3, 4, 6, 9, 10, 12, 13, 14, 15], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/pagination.py": {"executed_lines": [3, 5, 6, 7, 10, 11, 12, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93"}, "missing_lines": [17], "excluded_lines": [], "functions": {"encode_cursor": {"executed_lines": [11, 12], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 10}, "decode_cursor": {"executed_lines": [16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [17], "excluded_lines": [], "start_line": 15}, "": {"executed_lines": [3, 5, 6, 7, 10, 15], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 7, 10, 11, 12, 15, 16, 18, 19, 20, 21, 22], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93"}, "missing_lines": [17], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/schemas/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/schemas/access.py": {"executed_lines": [1, 3, 5, 8, 11, 12, 13, 14, 15, 16, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 35, 36, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 52, 55, 56, 57, 58, 61, 62, 63], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 8, 11, 12, 13, 14, 15, 16, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 35, 36, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 52, 55, 56, 57, 58, 61, 62, 63], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AccessRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 8}, "AccessResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "AccessDecisionResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "SimulationResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 33}, "BatchAccessItem": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43}, "BatchAccessRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48}, "BatchAccessResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 55}, "BatchAccessResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 61}, "": {"executed_lines": [1, 3, 5, 8, 11, 12, 13, 14, 15, 16, 19, 20, 23, 24, 25, 26, 27, 28, 29, 30, 33, 34, 35, 36, 37, 38, 39, 40, 43, 44, 45, 48, 49, 50, 51, 52, 55, 56, 57, 58, 61, 62, 63], "summary": {"covered_lines": 43, "num_statements": 43, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/schemas/api.py": {"executed_lines": [3, 5, 7, 9, 12, 13, 14, 15, 18, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 5, 7, 9, 12, 13, 14, 15, 18, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ErrorBody": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "MetaBody": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "SuccessResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "ErrorResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "": {"executed_lines": [3, 5, 7, 9, 12, 13, 14, 15, 18, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/schemas/management.py": {"executed_lines": [1, 3, 4, 6, 9, 10, 13, 14, 17, 18, 19, 22, 23, 26, 27, 30, 31, 32, 35, 36, 37, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 54, 57, 58, 59, 60, 61, 62, 63, 66, 67, 68, 69, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 93, 94, 95, 96, 97, 98], "summary": {"covered_lines": 68, "num_statements": 68, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 4, 6, 9, 10, 13, 14, 17, 18, 19, 22, 23, 26, 27, 30, 31, 32, 35, 36, 37, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 54, 57, 58, 59, 60, 61, 62, 63, 66, 67, 68, 69, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 93, 94, 95, 96, 97, 98], "summary": {"covered_lines": 68, "num_statements": 68, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RoleCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 9}, "RoleUpdate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 13}, "RoleOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "PermissionCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "PermissionUpdate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "PermissionOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "RolePermissionOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 35}, "PolicyCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 40}, "PolicyOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48}, "ACLCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 57}, "ACLOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 66}, "AuditRecordOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 72}, "AdminLoginRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 88}, "AdminLoginResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "": {"executed_lines": [1, 3, 4, 6, 9, 10, 13, 14, 17, 18, 19, 22, 23, 26, 27, 30, 31, 32, 35, 36, 37, 40, 41, 42, 43, 44, 45, 48, 49, 50, 51, 52, 53, 54, 57, 58, 59, 60, 61, 62, 63, 66, 67, 68, 69, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 88, 89, 90, 93, 94, 95, 96, 97, 98], "summary": {"covered_lines": 68, "num_statements": 68, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/domain/schemas/modeling.py": {"executed_lines": [1, 3, 5, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19, 22, 23, 24, 25, 28, 29, 30, 33, 34, 35, 38, 39, 42, 43, 44], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 5, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19, 22, 23, 24, 25, 28, 29, 30, 33, 34, 35, 38, 39, 42, 43, 44], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuthModelCreate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 8}, "AuthModelOut": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 13}, "PolicySimulationInput": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "PolicySimulationRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28}, "PolicySimulationResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 33}, "ImpactAnalysisRequest": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 38}, "ImpactAnalysisResponse": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "": {"executed_lines": [1, 3, 5, 8, 9, 10, 13, 14, 15, 16, 17, 18, 19, 22, 23, 24, 25, 28, 29, 30, 33, 34, 35, 38, 39, 42, 43, 44], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/__init__.py": {"executed_lines": [3, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 11], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/compiled/__init__.py": {"executed_lines": [1, 6, 8], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 6, 8], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 6, 8], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/compiled/decision_graph.py": {"executed_lines": [3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 31, 32, 33, 34, 35, 36, 37, 38, 43, 44, 45, 46, 47, 50, 53, 54, 55, 57, 58, 59, 61, 62, 63, 65, 66, 67, 68, 69, 72], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"DecisionGraph.evaluate": {"executed_lines": [32, 33, 34, 35, 36, 37, 38, 43, 44, 45, 46, 47], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "CompiledPolicyStore.__init__": {"executed_lines": [54, 55], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 53}, "CompiledPolicyStore.get": {"executed_lines": [58, 59], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 57}, "CompiledPolicyStore.set": {"executed_lines": [62, 63], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 61}, "CompiledPolicyStore.invalidate": {"executed_lines": [66, 67, 68, 69], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 65}, "": {"executed_lines": [3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 31, 50, 53, 57, 61, 65, 72], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"GraphDecision": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "CompiledPolicyNode": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "DecisionGraph": {"executed_lines": [32, 33, 34, 35, 36, 37, 38, 43, 44, 45, 46, 47], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28}, "CompiledPolicyStore": {"executed_lines": [54, 55, 58, 59, 62, 63, 66, 67, 68, 69], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 50}, "": {"executed_lines": [3, 5, 6, 7, 8, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23, 24, 27, 28, 29, 31, 50, 53, 57, 61, 65, 72], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/compiled/policy_compiler.py": {"executed_lines": [3, 5, 6, 8, 9, 12, 13, 14, 15, 16, 17, 18, 21, 25, 28, 30, 31, 32, 33, 35, 36, 37, 38, 40, 49, 52, 62, 66, 67], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 96.66666666666667, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.66666666666667, "percent_statements_covered_display": "97"}, "missing_lines": [34], "excluded_lines": [], "functions": {"compile_policy_ast": {"executed_lines": [25, 28, 30, 40], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "compile_policy_ast.evaluate": {"executed_lines": [31, 32, 33, 35, 36, 37, 38], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [34], "excluded_lines": [], "start_line": 30}, "compile_policy_graph": {"executed_lines": [52, 62, 66, 67], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 13, 14, 15, 16, 17, 18, 21, 49], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"PolicyAST": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 13, 14, 15, 16, 17, 18, 21, 25, 28, 30, 31, 32, 33, 35, 36, 37, 38, 40, 49, 52, 62, 66, 67], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 96.66666666666667, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.66666666666667, "percent_statements_covered_display": "97"}, "missing_lines": [34], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/keynetra_engine.py": {"executed_lines": [8, 10, 11, 12, 13, 14, 16, 17, 18, 19, 27, 28, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 50, 51, 52, 53, 54, 56, 57, 58, 67, 68, 71, 72, 73, 74, 76, 77, 85, 86, 93, 94, 95, 96, 97, 98, 99, 101, 102, 105, 108, 111, 114, 126, 129, 130, 131, 132, 133, 134, 136, 139, 140, 141, 144, 146, 149, 151, 152, 153, 154, 156, 159, 161, 162, 163, 164, 166, 167, 180, 195, 198, 200, 201, 202, 203, 204, 205, 207, 210, 211, 214, 215, 216, 218, 223, 227, 230, 236, 240, 243, 244, 257, 259, 272, 275, 277, 291, 292, 293, 302, 309, 310, 319, 320, 321, 327, 328, 330, 331, 338, 339, 340, 342, 343, 345, 346, 347, 354, 356, 357, 358, 363, 364, 373, 374, 375, 380, 381, 390, 391, 392, 397, 398, 407, 408, 411, 416, 417, 426, 427, 428, 433, 434, 443, 444, 445, 450, 451, 460, 461, 466, 467, 477, 487, 488, 489, 490, 491, 492, 500, 505, 509, 519, 522, 525, 526, 534, 535, 540, 542, 549, 550, 551, 556, 557, 558, 559, 564, 565, 566, 569, 570, 571, 572, 573, 581, 582, 587, 588, 589, 591, 594, 595, 596, 604, 605, 608, 610, 617, 618, 619, 620, 625, 626, 627, 629, 634, 636, 637, 639, 640, 648, 653, 658, 660, 663, 664, 665, 666, 667, 672, 673, 681, 683, 686, 687, 688, 693, 694, 695, 696, 703, 704, 712, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 728, 729, 730, 735, 736, 737, 738, 740, 741, 742, 743, 744, 745, 747, 748, 755, 756, 758, 766, 771, 773, 774, 775, 777, 780, 782, 792, 794, 820, 828], "summary": {"covered_lines": 290, "num_statements": 354, "percent_covered": 81.92090395480226, "percent_covered_display": "82", "missing_lines": 64, "excluded_lines": 0, "percent_statements_covered": 81.92090395480226, "percent_statements_covered_display": "82"}, "missing_lines": [117, 118, 119, 120, 121, 122, 123, 124, 142, 143, 150, 160, 165, 168, 169, 170, 171, 172, 173, 174, 175, 177, 178, 184, 185, 186, 187, 188, 189, 190, 193, 199, 208, 212, 217, 224, 311, 312, 315, 322, 323, 324, 325, 329, 341, 628, 633, 635, 638, 739, 770, 772, 781, 783, 786, 791, 801, 802, 810, 823, 824, 825, 826, 829], "excluded_lines": [], "functions": {"PolicyDefinition.from_dict": {"executed_lines": [58], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 57}, "ExplainTraceStep.to_dict": {"executed_lines": [77], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 76}, "AuthorizationDecision.evaluated_rules": {"executed_lines": [105], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 102}, "ConditionEvaluator.evaluate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [117, 118, 119, 120, 121, 122, 123, 124], "excluded_lines": [], "start_line": 114}, "ConditionEvaluator.handle_role": {"executed_lines": [129, 130, 131, 132, 133, 134], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 126}, "ConditionEvaluator.handle_max_amount": {"executed_lines": [139, 140, 141, 144], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [142, 143], "excluded_lines": [], "start_line": 136}, "ConditionEvaluator.handle_owner_only": {"executed_lines": [149, 151, 152, 153, 154], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [150], "excluded_lines": [], "start_line": 146}, "ConditionEvaluator.handle_time_range": {"executed_lines": [159, 161, 162, 163, 164, 166, 167], "summary": {"covered_lines": 7, "num_statements": 19, "percent_covered": 36.8421052631579, "percent_covered_display": "37", "missing_lines": 12, "excluded_lines": 0, "percent_statements_covered": 36.8421052631579, "percent_statements_covered_display": "37"}, "missing_lines": [160, 165, 168, 169, 170, 171, 172, 173, 174, 175, 177, 178], "excluded_lines": [], "start_line": 156}, "ConditionEvaluator.handle_geo_match": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [184, 185, 186, 187, 188, 189, 190, 193], "excluded_lines": [], "start_line": 180}, "ConditionEvaluator.handle_has_relation": {"executed_lines": [198, 200, 201, 202, 203, 204, 205, 207, 210, 211, 214, 215, 216, 218, 223], "summary": {"covered_lines": 15, "num_statements": 20, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [199, 208, 212, 217, 224], "excluded_lines": [], "start_line": 195}, "KeyNetraEngine.__init__": {"executed_lines": [236, 240, 243, 244, 257], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 230}, "KeyNetraEngine.decide": {"executed_lines": [272, 275], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 259}, "KeyNetraEngine.check_access": {"executed_lines": [291, 292, 293], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 277}, "KeyNetraEngine._normalize_input": {"executed_lines": [309, 310], "summary": {"covered_lines": 2, "num_statements": 5, "percent_covered": 40.0, "percent_covered_display": "40", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 40.0, "percent_statements_covered_display": "40"}, "missing_lines": [311, 312, 315], "excluded_lines": [], "start_line": 302}, "KeyNetraEngine._normalize_subject": {"executed_lines": [320, 321], "summary": {"covered_lines": 2, "num_statements": 6, "percent_covered": 33.333333333333336, "percent_covered_display": "33", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 33.333333333333336, "percent_statements_covered_display": "33"}, "missing_lines": [322, 323, 324, 325], "excluded_lines": [], "start_line": 319}, "KeyNetraEngine._normalize_resource": {"executed_lines": [328, 330, 331], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [329], "excluded_lines": [], "start_line": 327}, "KeyNetraEngine._parse_descriptor": {"executed_lines": [339, 340, 342, 343], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [341], "excluded_lines": [], "start_line": 338}, "KeyNetraEngine._decide_structured": {"executed_lines": [346, 347, 354, 356, 357, 358, 363, 364, 373, 374, 375, 380, 381, 390, 391, 392, 397, 398, 407, 408, 411, 416, 417, 426, 427, 428, 433, 434, 443, 444, 445, 450, 451, 460, 461, 466, 467], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 345}, "KeyNetraEngine._decision_from_stage": {"executed_lines": [487, 488, 489, 490, 491, 492, 500, 505, 509], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 477}, "KeyNetraEngine._evaluate_direct_permissions": {"executed_lines": [522, 525, 526, 534, 535, 540], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 519}, "KeyNetraEngine._evaluate_acl": {"executed_lines": [549, 550, 551, 556, 557, 558, 559, 564, 565, 566, 569, 570, 571, 572, 573, 581, 582, 587, 588, 589], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 542}, "KeyNetraEngine._evaluate_role_permissions": {"executed_lines": [594, 595, 596, 604, 605, 608], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 591}, "KeyNetraEngine._evaluate_relationship_index": {"executed_lines": [617, 618, 619, 620, 625, 626, 627, 629, 634, 636, 637, 639, 640, 648, 653, 658], "summary": {"covered_lines": 16, "num_statements": 20, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [628, 633, 635, 638], "excluded_lines": [], "start_line": 610}, "KeyNetraEngine._evaluate_compiled_policies": {"executed_lines": [663, 664, 665, 666, 667, 672, 673, 681], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 660}, "KeyNetraEngine._evaluate_permission_graph": {"executed_lines": [686, 687, 688, 693, 694, 695, 696, 703, 704, 712], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 683}, "KeyNetraEngine._subject_descriptors": {"executed_lines": [715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 728, 729, 730, 735, 736, 737, 738, 740, 741, 742, 743, 744, 745], "summary": {"covered_lines": 24, "num_statements": 25, "percent_covered": 96.0, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.0, "percent_statements_covered_display": "96"}, "missing_lines": [739], "excluded_lines": [], "start_line": 714}, "KeyNetraEngine._resource_identity": {"executed_lines": [748, 755, 756], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 747}, "KeyNetraEngine._acl_matches": {"executed_lines": [766, 771, 773, 774, 775], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71"}, "missing_lines": [770, 772], "excluded_lines": [], "start_line": 758}, "KeyNetraEngine._acl_subject_matches": {"executed_lines": [780, 782, 792], "summary": {"covered_lines": 3, "num_statements": 7, "percent_covered": 42.857142857142854, "percent_covered_display": "43", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 42.857142857142854, "percent_statements_covered_display": "43"}, "missing_lines": [781, 783, 786, 791], "excluded_lines": [], "start_line": 777}, "KeyNetraEngine._decision_from_policy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [801, 802, 810], "excluded_lines": [], "start_line": 794}, "KeyNetraEngine._best_reason": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [823, 824, 825, 826], "excluded_lines": [], "start_line": 820}, "KeyNetraEngine._policy_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [829], "excluded_lines": [], "start_line": 828}, "": {"executed_lines": [8, 10, 11, 12, 13, 14, 16, 17, 18, 19, 27, 28, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 50, 51, 52, 53, 54, 56, 57, 67, 68, 71, 72, 73, 74, 76, 85, 86, 93, 94, 95, 96, 97, 98, 99, 101, 102, 108, 111, 114, 126, 136, 146, 156, 180, 195, 227, 230, 259, 277, 302, 319, 327, 338, 345, 477, 519, 542, 591, 610, 660, 683, 714, 747, 758, 777, 794, 820, 828], "summary": {"covered_lines": 82, "num_statements": 82, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuthorizationInput": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 32}, "PolicyDefinition": {"executed_lines": [58], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "ExplainTraceStep": {"executed_lines": [77], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 68}, "AuthorizationDecision": {"executed_lines": [105], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 86}, "ConditionEvaluator": {"executed_lines": [129, 130, 131, 132, 133, 134, 139, 140, 141, 144, 149, 151, 152, 153, 154, 159, 161, 162, 163, 164, 166, 167, 198, 200, 201, 202, 203, 204, 205, 207, 210, 211, 214, 215, 216, 218, 223], "summary": {"covered_lines": 37, "num_statements": 73, "percent_covered": 50.68493150684932, "percent_covered_display": "51", "missing_lines": 36, "excluded_lines": 0, "percent_statements_covered": 50.68493150684932, "percent_statements_covered_display": "51"}, "missing_lines": [117, 118, 119, 120, 121, 122, 123, 124, 142, 143, 150, 160, 165, 168, 169, 170, 171, 172, 173, 174, 175, 177, 178, 184, 185, 186, 187, 188, 189, 190, 193, 199, 208, 212, 217, 224], "excluded_lines": [], "start_line": 111}, "KeyNetraEngine": {"executed_lines": [236, 240, 243, 244, 257, 272, 275, 291, 292, 293, 309, 310, 320, 321, 328, 330, 331, 339, 340, 342, 343, 346, 347, 354, 356, 357, 358, 363, 364, 373, 374, 375, 380, 381, 390, 391, 392, 397, 398, 407, 408, 411, 416, 417, 426, 427, 428, 433, 434, 443, 444, 445, 450, 451, 460, 461, 466, 467, 487, 488, 489, 490, 491, 492, 500, 505, 509, 522, 525, 526, 534, 535, 540, 549, 550, 551, 556, 557, 558, 559, 564, 565, 566, 569, 570, 571, 572, 573, 581, 582, 587, 588, 589, 594, 595, 596, 604, 605, 608, 617, 618, 619, 620, 625, 626, 627, 629, 634, 636, 637, 639, 640, 648, 653, 658, 663, 664, 665, 666, 667, 672, 673, 681, 686, 687, 688, 693, 694, 695, 696, 703, 704, 712, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 728, 729, 730, 735, 736, 737, 738, 740, 741, 742, 743, 744, 745, 748, 755, 756, 766, 771, 773, 774, 775, 780, 782, 792], "summary": {"covered_lines": 168, "num_statements": 196, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 28, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [311, 312, 315, 322, 323, 324, 325, 329, 341, 628, 633, 635, 638, 739, 770, 772, 781, 783, 786, 791, 801, 802, 810, 823, 824, 825, 826, 829], "excluded_lines": [], "start_line": 227}, "": {"executed_lines": [8, 10, 11, 12, 13, 14, 16, 17, 18, 19, 27, 28, 31, 32, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 50, 51, 52, 53, 54, 56, 57, 67, 68, 71, 72, 73, 74, 76, 85, 86, 93, 94, 95, 96, 97, 98, 99, 101, 102, 108, 111, 114, 126, 136, 146, 156, 180, 195, 227, 230, 259, 277, 302, 319, 327, 338, 345, 477, 519, 542, 591, 610, 660, 683, 714, 747, 758, 777, 794, 820, 828], "summary": {"covered_lines": 82, "num_statements": 82, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/model_graph/__init__.py": {"executed_lines": [1, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/engine/model_graph/permission_graph.py": {"executed_lines": [3, 5, 6, 7, 9, 12, 13, 14, 15, 16, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 41, 47, 48, 49, 52, 53, 56, 57, 60, 61, 62, 64, 65, 67, 68, 69, 71, 73, 74, 77, 78, 79, 81, 82, 83, 84, 86, 88, 90, 92, 93, 96, 99, 100, 101, 103, 104, 105, 107, 108, 109, 111, 116], "summary": {"covered_lines": 68, "num_statements": 78, "percent_covered": 87.17948717948718, "percent_covered_display": "87", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 87.17948717948718, "percent_statements_covered_display": "87"}, "missing_lines": [70, 72, 75, 80, 85, 87, 89, 91, 112, 113], "excluded_lines": [], "functions": {"CompiledPermissionGraph.evaluate": {"executed_lines": [25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 41], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24}, "CompiledPermissionGraph._resource_identity": {"executed_lines": [48, 49, 52, 53], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "_PermissionEvaluator.__init__": {"executed_lines": [60, 61, 62], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 57}, "_PermissionEvaluator.evaluate": {"executed_lines": [65, 67, 68, 69, 71, 73, 74], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 70.0, "percent_statements_covered_display": "70"}, "missing_lines": [70, 72, 75], "excluded_lines": [], "start_line": 64}, "_PermissionEvaluator._has_relation": {"executed_lines": [78, 79, 81, 82, 83, 84, 86, 88, 90, 92, 93], "summary": {"covered_lines": 11, "num_statements": 16, "percent_covered": 68.75, "percent_covered_display": "69", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 68.75, "percent_statements_covered_display": "69"}, "missing_lines": [80, 85, 87, 89, 91], "excluded_lines": [], "start_line": 77}, "PermissionGraphStore.__init__": {"executed_lines": [100, 101], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 99}, "PermissionGraphStore.get": {"executed_lines": [104, 105], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 103}, "PermissionGraphStore.set": {"executed_lines": [108, 109], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 107}, "PermissionGraphStore.invalidate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [112, 113], "excluded_lines": [], "start_line": 111}, "": {"executed_lines": [3, 5, 6, 7, 9, 12, 13, 14, 15, 16, 19, 20, 21, 22, 24, 47, 56, 57, 64, 77, 96, 99, 103, 107, 111, 116], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuthorizationGraphDecision": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 13}, "CompiledPermissionGraph": {"executed_lines": [25, 26, 27, 28, 29, 30, 31, 34, 35, 36, 41, 48, 49, 52, 53], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20}, "_PermissionEvaluator": {"executed_lines": [60, 61, 62, 65, 67, 68, 69, 71, 73, 74, 78, 79, 81, 82, 83, 84, 86, 88, 90, 92, 93], "summary": {"covered_lines": 21, "num_statements": 29, "percent_covered": 72.41379310344827, "percent_covered_display": "72", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 72.41379310344827, "percent_statements_covered_display": "72"}, "missing_lines": [70, 72, 75, 80, 85, 87, 89, 91], "excluded_lines": [], "start_line": 56}, "PermissionGraphStore": {"executed_lines": [100, 101, 104, 105, 108, 109], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [112, 113], "excluded_lines": [], "start_line": 96}, "": {"executed_lines": [3, 5, 6, 7, 9, 12, 13, 14, 15, 16, 19, 20, 21, 22, 24, 47, 56, 57, 64, 77, 96, 99, 103, 107, 111, 116], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/headless.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 13, 14, 19, 20, 23, 24, 27, 28, 30, 31, 32, 33, 34, 36, 37, 38, 42, 44, 45, 46, 47, 50, 51, 52, 57, 65, 66, 67, 77, 78, 79, 80, 81, 82, 85, 86, 88, 89, 97, 98, 99, 100, 101, 102], "summary": {"covered_lines": 50, "num_statements": 53, "percent_covered": 94.33962264150944, "percent_covered_display": "94", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 94.33962264150944, "percent_statements_covered_display": "94"}, "missing_lines": [48, 83, 87], "excluded_lines": [], "functions": {"KeyNetra.from_config": {"executed_lines": [32, 33, 34, 36, 37, 38, 42], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "KeyNetra.load_policies": {"executed_lines": [45, 46, 47], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [48], "excluded_lines": [], "start_line": 44}, "KeyNetra.load_model": {"executed_lines": [51, 52], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 50}, "KeyNetra.check_access": {"executed_lines": [65, 66, 67], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 57}, "KeyNetra._subject_to_user": {"executed_lines": [78, 79, 80, 81, 82], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [83], "excluded_lines": [], "start_line": 77}, "KeyNetra._resource_to_payload": {"executed_lines": [86, 88, 89], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [87], "excluded_lines": [], "start_line": 85}, "_parse_descriptor": {"executed_lines": [98, 99, 100, 101, 102], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 97}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 13, 14, 19, 20, 23, 24, 27, 28, 30, 31, 44, 50, 57, 77, 85, 97], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"KeyNetra": {"executed_lines": [32, 33, 34, 36, 37, 38, 42, 45, 46, 47, 51, 52, 65, 66, 67, 78, 79, 80, 81, 82, 86, 88, 89], "summary": {"covered_lines": 23, "num_statements": 26, "percent_covered": 88.46153846153847, "percent_covered_display": "88", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 88.46153846153847, "percent_statements_covered_display": "88"}, "missing_lines": [48, 83, 87], "excluded_lines": [], "start_line": 24}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 13, 14, 19, 20, 23, 24, 27, 28, 30, 31, 44, 50, 57, 77, 85, 97, 98, 99, 100, 101, 102], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/access_index_cache.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 17, 18, 20, 23, 31, 32, 33, 34, 35, 39, 42, 43, 44, 45, 47, 48, 50, 77, 79, 88, 102, 113, 114, 120, 121, 123, 124, 126, 127, 137, 139, 140, 143, 144], "summary": {"covered_lines": 42, "num_statements": 49, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [36, 37, 38, 40, 41, 46, 49], "excluded_lines": [], "functions": {"RedisBackedAccessIndexCache.__init__": {"executed_lines": [17, 18], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "RedisBackedAccessIndexCache.get": {"executed_lines": [23, 31, 32, 33, 34, 35, 39, 42, 43, 44, 45, 47, 48, 50, 77], "summary": {"covered_lines": 15, "num_statements": 22, "percent_covered": 68.18181818181819, "percent_covered_display": "68", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 68.18181818181819, "percent_statements_covered_display": "68"}, "missing_lines": [36, 37, 38, 40, 41, 46, 49], "excluded_lines": [], "start_line": 20}, "RedisBackedAccessIndexCache.set": {"executed_lines": [88, 102], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 79}, "RedisBackedAccessIndexCache.invalidate": {"executed_lines": [114], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 113}, "RedisBackedAccessIndexCache.invalidate_tenant": {"executed_lines": [121], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 120}, "RedisBackedAccessIndexCache.invalidate_global": {"executed_lines": [124], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 123}, "RedisBackedAccessIndexCache._key": {"executed_lines": [127, 137], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 126}, "RedisBackedAccessIndexCache._namespace_key": {"executed_lines": [140], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 139}, "build_access_index_cache": {"executed_lines": [144], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 143}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 20, 79, 113, 120, 123, 126, 139, 143], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RedisBackedAccessIndexCache": {"executed_lines": [17, 18, 23, 31, 32, 33, 34, 35, 39, 42, 43, 44, 45, 47, 48, 50, 77, 88, 102, 114, 121, 124, 127, 137, 140], "summary": {"covered_lines": 25, "num_statements": 32, "percent_covered": 78.125, "percent_covered_display": "78", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 78.125, "percent_statements_covered_display": "78"}, "missing_lines": [36, 37, 38, 40, 41, 46, 49], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 20, 79, 113, 120, 123, 126, 139, 143, 144], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/acl_cache.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 17, 18, 20, 23, 31, 32, 33, 62, 71, 72, 83, 84, 90, 93, 94, 103, 105, 106, 109, 110], "summary": {"covered_lines": 28, "num_statements": 44, "percent_covered": 63.63636363636363, "percent_covered_display": "64", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 63.63636363636363, "percent_statements_covered_display": "64"}, "missing_lines": [34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 60, 91], "excluded_lines": [], "functions": {"RedisBackedACLCache.__init__": {"executed_lines": [17, 18], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "RedisBackedACLCache.get": {"executed_lines": [23, 31, 32, 33], "summary": {"covered_lines": 4, "num_statements": 19, "percent_covered": 21.05263157894737, "percent_covered_display": "21", "missing_lines": 15, "excluded_lines": 0, "percent_statements_covered": 21.05263157894737, "percent_statements_covered_display": "21"}, "missing_lines": [34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 60], "excluded_lines": [], "start_line": 20}, "RedisBackedACLCache.set": {"executed_lines": [71, 72], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 62}, "RedisBackedACLCache.invalidate": {"executed_lines": [84], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 83}, "RedisBackedACLCache.invalidate_global": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [91], "excluded_lines": [], "start_line": 90}, "RedisBackedACLCache._key": {"executed_lines": [94, 103], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "RedisBackedACLCache._namespace_key": {"executed_lines": [106], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 105}, "build_acl_cache": {"executed_lines": [110], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 109}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 20, 62, 83, 90, 93, 105, 109], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RedisBackedACLCache": {"executed_lines": [17, 18, 23, 31, 32, 33, 71, 72, 84, 94, 103, 106], "summary": {"covered_lines": 12, "num_statements": 28, "percent_covered": 42.857142857142854, "percent_covered_display": "43", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 42.857142857142854, "percent_statements_covered_display": "43"}, "missing_lines": [34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 60, 91], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 20, 62, 83, 90, 93, 105, 109, 110], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/backends.py": {"executed_lines": [7, 9, 10, 11, 13, 15, 18, 30, 33, 34, 36, 37, 38, 39, 40, 41, 42, 43, 44, 46, 47, 48, 50, 51, 53, 54, 55, 56, 57, 60, 63, 64, 66, 67, 68, 69, 70, 71, 72, 73, 76, 77, 78, 81, 82, 83, 84, 86, 87, 88, 89, 90, 91, 93, 94, 95, 96, 97, 98, 101, 104, 107, 108, 109], "summary": {"covered_lines": 64, "num_statements": 66, "percent_covered": 96.96969696969697, "percent_covered_display": "97", "missing_lines": 2, "excluded_lines": 10, "percent_statements_covered": 96.96969696969697, "percent_statements_covered_display": "97"}, "missing_lines": [74, 79], "excluded_lines": [20, 21, 22, 23, 24, 25, 26, 27, 28, 29], "functions": {"CacheBackend.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [21], "start_line": 21}, "CacheBackend.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [23], "start_line": 23}, "CacheBackend.delete": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [25], "start_line": 25}, "CacheBackend.incr": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [27], "start_line": 27}, "InMemoryCacheBackend.__init__": {"executed_lines": [34], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 33}, "InMemoryCacheBackend.get": {"executed_lines": [37, 38, 39, 40, 41, 42, 43, 44], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 36}, "InMemoryCacheBackend.set": {"executed_lines": [47, 48], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 46}, "InMemoryCacheBackend.delete": {"executed_lines": [51], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 50}, "InMemoryCacheBackend.incr": {"executed_lines": [54, 55, 56, 57], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 53}, "RedisCacheBackend.__init__": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 63}, "RedisCacheBackend.get": {"executed_lines": [67, 68, 69, 70, 71, 72, 73], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [74], "excluded_lines": [], "start_line": 66}, "RedisCacheBackend.set": {"executed_lines": [77, 78, 81, 82, 83, 84], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [79], "excluded_lines": [], "start_line": 76}, "RedisCacheBackend.delete": {"executed_lines": [87, 88, 89, 90, 91], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 86}, "RedisCacheBackend.incr": {"executed_lines": [94, 95, 96, 97, 98], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "build_cache_backend": {"executed_lines": [107, 108, 109], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 104}, "": {"executed_lines": [7, 9, 10, 11, 13, 15, 18, 30, 33, 36, 46, 50, 53, 60, 63, 66, 76, 86, 93, 101, 104], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [20, 22, 24, 26, 28, 29], "start_line": 1}}, "classes": {"CacheBackend": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [21, 23, 25, 27], "start_line": 18}, "InMemoryCacheBackend": {"executed_lines": [34, 37, 38, 39, 40, 41, 42, 43, 44, 47, 48, 51, 54, 55, 56, 57], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "RedisCacheBackend": {"executed_lines": [64, 67, 68, 69, 70, 71, 72, 73, 77, 78, 81, 82, 83, 84, 87, 88, 89, 90, 91, 94, 95, 96, 97, 98], "summary": {"covered_lines": 24, "num_statements": 26, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92"}, "missing_lines": [74, 79], "excluded_lines": [], "start_line": 60}, "": {"executed_lines": [7, 9, 10, 11, 13, 15, 18, 30, 33, 36, 46, 50, 53, 60, 63, 66, 76, 86, 93, 101, 104, 107, 108, 109], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [20, 22, 24, 26, 28, 29], "start_line": 1}}}, "keynetra/infrastructure/cache/decision_cache.py": {"executed_lines": [8, 10, 11, 12, 14, 15, 16, 19, 20, 23, 26, 27, 29, 30, 31, 32, 33, 34, 37, 61, 62, 71, 73, 81, 82, 91, 92, 94, 95, 97, 98, 99, 101, 102, 105, 108], "summary": {"covered_lines": 36, "num_statements": 38, "percent_covered": 94.73684210526316, "percent_covered_display": "95", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 94.73684210526316, "percent_statements_covered_display": "95"}, "missing_lines": [35, 36], "excluded_lines": [], "functions": {"_stable_json": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "RedisBackedDecisionCache.__init__": {"executed_lines": [27], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "RedisBackedDecisionCache.get": {"executed_lines": [30, 31, 32, 33, 34, 37], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [35, 36], "excluded_lines": [], "start_line": 29}, "RedisBackedDecisionCache.set": {"executed_lines": [62, 71], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 61}, "RedisBackedDecisionCache.make_key": {"executed_lines": [81, 82, 91, 92], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 73}, "RedisBackedDecisionCache.bump_namespace": {"executed_lines": [95], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 94}, "RedisBackedDecisionCache._tenant_namespace": {"executed_lines": [98, 99], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 97}, "RedisBackedDecisionCache._namespace_key": {"executed_lines": [102], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 101}, "build_decision_cache": {"executed_lines": [108], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 105}, "": {"executed_lines": [8, 10, 11, 12, 14, 15, 16, 19, 23, 26, 29, 61, 73, 94, 97, 101, 105], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RedisBackedDecisionCache": {"executed_lines": [27, 30, 31, 32, 33, 34, 37, 62, 71, 81, 82, 91, 92, 95, 98, 99, 102], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 89.47368421052632, "percent_statements_covered_display": "89"}, "missing_lines": [35, 36], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [8, 10, 11, 12, 14, 15, 16, 19, 20, 23, 26, 29, 61, 73, 94, 97, 101, 105, 108], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/policy_cache.py": {"executed_lines": [7, 9, 10, 12, 13, 14, 17, 20, 21, 23, 24, 25, 26, 27, 28, 29, 32, 34, 35, 36, 38, 44, 46, 47, 48, 61, 63, 64, 66, 67, 68, 70, 71, 74, 77], "summary": {"covered_lines": 35, "num_statements": 39, "percent_covered": 89.74358974358974, "percent_covered_display": "90", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 89.74358974358974, "percent_statements_covered_display": "90"}, "missing_lines": [30, 31, 33, 37], "excluded_lines": [], "functions": {"RedisBackedPolicyCache.__init__": {"executed_lines": [21], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20}, "RedisBackedPolicyCache.get": {"executed_lines": [24, 25, 26, 27, 28, 29, 32, 34, 35, 36, 38, 44], "summary": {"covered_lines": 12, "num_statements": 16, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [30, 31, 33, 37], "excluded_lines": [], "start_line": 23}, "RedisBackedPolicyCache.set": {"executed_lines": [47, 48, 61], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 46}, "RedisBackedPolicyCache.invalidate": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 63}, "RedisBackedPolicyCache._cache_key": {"executed_lines": [67, 68], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 66}, "RedisBackedPolicyCache._namespace_key": {"executed_lines": [71], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 70}, "build_policy_cache": {"executed_lines": [77], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 74}, "": {"executed_lines": [7, 9, 10, 12, 13, 14, 17, 20, 23, 46, 63, 66, 70, 74], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RedisBackedPolicyCache": {"executed_lines": [21, 24, 25, 26, 27, 28, 29, 32, 34, 35, 36, 38, 44, 47, 48, 61, 64, 67, 68, 71], "summary": {"covered_lines": 20, "num_statements": 24, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [30, 31, 33, 37], "excluded_lines": [], "start_line": 17}, "": {"executed_lines": [7, 9, 10, 12, 13, 14, 17, 20, 23, 46, 63, 66, 70, 74, 77], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/policy_distribution.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 15, 16, 17, 18, 20, 24, 25, 26, 27, 41, 44, 45, 47, 48], "summary": {"covered_lines": 23, "num_statements": 29, "percent_covered": 79.3103448275862, "percent_covered_display": "79", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 79.3103448275862, "percent_statements_covered_display": "79"}, "missing_lines": [21, 28, 29, 30, 31, 38], "excluded_lines": [], "functions": {"PolicyUpdateEvent.to_json": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [21], "excluded_lines": [], "start_line": 20}, "publish_policy_update": {"executed_lines": [25, 26, 27], "summary": {"covered_lines": 3, "num_statements": 8, "percent_covered": 37.5, "percent_covered_display": "38", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 37.5, "percent_statements_covered_display": "38"}, "missing_lines": [28, 29, 30, 31, 38], "excluded_lines": [], "start_line": 24}, "RedisPolicyEventPublisher.__init__": {"executed_lines": [45], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 44}, "RedisPolicyEventPublisher.publish_policy_update": {"executed_lines": [48], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 15, 16, 17, 18, 20, 24, 41, 44, 47], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"PolicyUpdateEvent": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [21], "excluded_lines": [], "start_line": 16}, "RedisPolicyEventPublisher": {"executed_lines": [45, 48], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 41}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 12, 15, 16, 17, 18, 20, 24, 25, 26, 27, 41, 44, 47], "summary": {"covered_lines": 21, "num_statements": 26, "percent_covered": 80.76923076923077, "percent_covered_display": "81", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 80.76923076923077, "percent_statements_covered_display": "81"}, "missing_lines": [28, 29, 30, 31, 38], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/relationship_cache.py": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 16, 17, 19, 22, 25, 26, 27, 28, 31, 33, 34, 35, 37, 46, 48, 56, 57, 63, 64, 68, 69, 72, 75], "summary": {"covered_lines": 30, "num_statements": 34, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 88.23529411764706, "percent_statements_covered_display": "88"}, "missing_lines": [29, 30, 32, 36], "excluded_lines": [], "functions": {"RedisBackedRelationshipCache.__init__": {"executed_lines": [16, 17], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "RedisBackedRelationshipCache.get": {"executed_lines": [22, 25, 26, 27, 28, 31, 33, 34, 35, 37, 46], "summary": {"covered_lines": 11, "num_statements": 15, "percent_covered": 73.33333333333333, "percent_covered_display": "73", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 73.33333333333333, "percent_statements_covered_display": "73"}, "missing_lines": [29, 30, 32, 36], "excluded_lines": [], "start_line": 19}, "RedisBackedRelationshipCache.set": {"executed_lines": [56, 57], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 48}, "RedisBackedRelationshipCache.invalidate": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 63}, "RedisBackedRelationshipCache._key": {"executed_lines": [69], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 68}, "build_relationship_cache": {"executed_lines": [75], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 72}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 19, 48, 63, 68, 72], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RedisBackedRelationshipCache": {"executed_lines": [16, 17, 22, 25, 26, 27, 28, 31, 33, 34, 35, 37, 46, 56, 57, 64, 69], "summary": {"covered_lines": 17, "num_statements": 21, "percent_covered": 80.95238095238095, "percent_covered_display": "81", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 80.95238095238095, "percent_statements_covered_display": "81"}, "missing_lines": [29, 30, 32, 36], "excluded_lines": [], "start_line": 12}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 19, 48, 63, 68, 72, 75], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/cache/user_cache.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 36, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 36, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [1, 3, 4, 5, 7, 8, 9, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 28, 29, 30, 31, 32, 33, 34, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 58], "excluded_lines": [], "functions": {"get_cached_user_context": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 17, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 17, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [15, 16, 17, 18, 19, 20, 21, 22, 28, 29, 30, 31, 32, 33, 34, 40, 41], "excluded_lines": [], "start_line": 14}, "set_cached_user_context": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [45, 46, 47, 48, 49, 50, 51, 52, 58], "excluded_lines": [], "start_line": 44}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [1, 3, 4, 5, 7, 8, 9, 11, 14, 44], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 36, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 36, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [1, 3, 4, 5, 7, 8, 9, 11, 14, 15, 16, 17, 18, 19, 20, 21, 22, 28, 29, 30, 31, 32, 33, 34, 40, 41, 44, 45, 46, 47, 48, 49, 50, 51, 52, 58], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/errors.py": {"executed_lines": [1, 4, 8, 12], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 4, 8, 12], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"KeyNetraError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 4}, "BootstrapError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 8}, "ConfigurationError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "": {"executed_lines": [1, 4, 8, 12], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/logging.py": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 18, 19, 21, 22, 24, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 61, 62, 65, 71, 72, 73, 74, 75, 78, 79, 80, 81, 84, 85, 88, 89, 92, 93], "summary": {"covered_lines": 55, "num_statements": 62, "percent_covered": 88.70967741935483, "percent_covered_display": "89", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 88.70967741935483, "percent_statements_covered_display": "89"}, "missing_lines": [53, 54, 55, 56, 57, 58, 59], "excluded_lines": [], "functions": {"JsonLogFormatter.format": {"executed_lines": [21, 22, 24, 25, 26, 27, 28], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "configure_json_logging": {"executed_lines": [32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 31}, "configure_rich_logging": {"executed_lines": [47, 48, 49, 50, 51, 52, 61, 62, 65, 71, 72, 73, 74, 75], "summary": {"covered_lines": 14, "num_statements": 21, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [53, 54, 55, 56, 57, 58, 59], "excluded_lines": [], "start_line": 46}, "log_event": {"executed_lines": [79, 80, 81], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 78}, "set_correlation_id": {"executed_lines": [85], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 84}, "reset_correlation_id": {"executed_lines": [89], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 88}, "get_correlation_id": {"executed_lines": [93], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 92}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 18, 19, 31, 46, 78, 84, 88, 92], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"JsonLogFormatter": {"executed_lines": [21, 22, 24, 25, 26, 27, 28], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 10, 12, 18, 19, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 46, 47, 48, 49, 50, 51, 52, 61, 62, 65, 71, 72, 73, 74, 75, 78, 79, 80, 81, 84, 85, 88, 89, 92, 93], "summary": {"covered_lines": 48, "num_statements": 55, "percent_covered": 87.27272727272727, "percent_covered_display": "87", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 87.27272727272727, "percent_statements_covered_display": "87"}, "missing_lines": [53, 54, 55, 56, 57, 58, 59], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/metrics.py": {"executed_lines": [3, 5, 11], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 5, 11], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 11], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/__init__.py": {"executed_lines": [3, 4, 5, 6, 7, 8, 10], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [3, 4, 5, 6, 7, 8, 10], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 4, 5, 6, 7, 8, 10], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/acl.py": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 16, 18, 29, 38, 39, 40, 41, 43, 46, 57, 59, 60, 69, 71, 79, 91, 93, 94, 99, 101, 102], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"SqlACLRepository.__init__": {"executed_lines": [16], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "SqlACLRepository.create_acl_entry": {"executed_lines": [29, 38, 39, 40, 41], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "SqlACLRepository.list_resource_acl": {"executed_lines": [46, 57], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 43}, "SqlACLRepository.get_acl_entry": {"executed_lines": [60, 69], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59}, "SqlACLRepository.find_matching_acl": {"executed_lines": [79, 91], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 71}, "SqlACLRepository.delete_acl_entry": {"executed_lines": [94, 99], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 93}, "SqlACLRepository._to_record": {"executed_lines": [102], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 101}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 18, 43, 59, 71, 93, 101], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlACLRepository": {"executed_lines": [16, 29, 38, 39, 40, 41, 46, 57, 60, 69, 79, 91, 94, 99, 102], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 18, 43, 59, 71, 93, 101], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/audit.py": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 16, 19, 20, 22, 32, 46, 47, 49, 61, 62, 63, 64, 65, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 87, 94, 95, 96, 101, 103, 104, 105, 107, 109, 110, 111], "summary": {"covered_lines": 43, "num_statements": 44, "percent_covered": 97.72727272727273, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.72727272727273, "percent_statements_covered_display": "98"}, "missing_lines": [106], "excluded_lines": [], "functions": {"SqlAuditRepository.__init__": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "SqlAuditRepository.write": {"executed_lines": [32, 46, 47], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "SqlAuditRepository.list_page": {"executed_lines": [61, 62, 63, 64, 65, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 87, 94, 95, 96, 101], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "SqlAuditRepository._json_field": {"executed_lines": [104, 105, 107], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [106], "excluded_lines": [], "start_line": 103}, "SqlAuditRepository._to_item": {"executed_lines": [111], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 110}, "": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 16, 19, 22, 49, 103, 109, 110], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlAuditRepository": {"executed_lines": [20, 32, 46, 47, 61, 62, 63, 64, 65, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 87, 94, 95, 96, 101, 104, 105, 107, 111], "summary": {"covered_lines": 28, "num_statements": 29, "percent_covered": 96.55172413793103, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.55172413793103, "percent_statements_covered_display": "97"}, "missing_lines": [106], "excluded_lines": [], "start_line": 16}, "": {"executed_lines": [3, 5, 7, 8, 10, 11, 12, 13, 16, 19, 22, 49, 103, 109, 110], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/auth_models.py": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 16, 18, 19, 26, 28, 36, 43, 44, 50, 55, 56, 57, 59, 60], "summary": {"covered_lines": 21, "num_statements": 24, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [52, 53, 54], "excluded_lines": [], "functions": {"SqlAuthModelRepository.__init__": {"executed_lines": [16], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "SqlAuthModelRepository.get_model": {"executed_lines": [19, 26], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "SqlAuthModelRepository.upsert_model": {"executed_lines": [36, 43, 44, 50, 55, 56, 57], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 70.0, "percent_statements_covered_display": "70"}, "missing_lines": [52, 53, 54], "excluded_lines": [], "start_line": 28}, "SqlAuthModelRepository._to_record": {"executed_lines": [60], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 18, 28, 59], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlAuthModelRepository": {"executed_lines": [16, 19, 26, 36, 43, 44, 50, 55, 56, 57, 60], "summary": {"covered_lines": 11, "num_statements": 14, "percent_covered": 78.57142857142857, "percent_covered_display": "79", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79"}, "missing_lines": [52, 53, 54], "excluded_lines": [], "start_line": 12}, "": {"executed_lines": [3, 5, 6, 8, 9, 12, 15, 18, 28, 59], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/idempotency.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 15, 16, 19, 20, 21, 22, 23, 26, 29, 30, 32, 35, 38, 39, 40, 41, 42, 43, 44, 45, 46, 48, 49, 50, 52, 60, 68, 69, 71, 72, 73, 74, 75, 77, 78], "summary": {"covered_lines": 42, "num_statements": 45, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 93.33333333333333, "percent_statements_covered_display": "93"}, "missing_lines": [47, 51, 70], "excluded_lines": [], "functions": {"SqlIdempotencyRepository.__init__": {"executed_lines": [30], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 29}, "SqlIdempotencyRepository.start": {"executed_lines": [35, 38, 39, 40, 41, 42, 43, 44, 45, 46, 48, 49, 50, 52], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [47, 51], "excluded_lines": [], "start_line": 32}, "SqlIdempotencyRepository.complete": {"executed_lines": [68, 69, 71, 72, 73, 74, 75], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 87.5, "percent_statements_covered_display": "88"}, "missing_lines": [70], "excluded_lines": [], "start_line": 60}, "SqlIdempotencyRepository._get": {"executed_lines": [78], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 77}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 15, 16, 19, 20, 21, 22, 23, 26, 29, 32, 60, 77], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"IdempotencyStartResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "SqlIdempotencyRepository": {"executed_lines": [30, 35, 38, 39, 40, 41, 42, 43, 44, 45, 46, 48, 49, 50, 52, 68, 69, 71, 72, 73, 74, 75, 78], "summary": {"covered_lines": 23, "num_statements": 26, "percent_covered": 88.46153846153847, "percent_covered_display": "88", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 88.46153846153847, "percent_statements_covered_display": "88"}, "missing_lines": [47, 51, 70], "excluded_lines": [], "start_line": 26}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 15, 16, 19, 20, 21, 22, 23, 26, 29, 32, 60, 77], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/policies.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 22, 24, 27, 28, 31, 32, 33, 47, 48, 60, 62, 65, 66, 69, 70, 71, 84, 85, 96, 98, 105, 112, 122, 125, 126, 127, 139, 140, 141, 142, 143, 145, 157, 166, 167, 168, 169, 170, 172, 173, 175, 186, 187, 188, 204, 205, 214, 215, 224, 226, 236, 237, 243, 244, 253, 255, 260, 261, 263, 266, 267, 274, 275, 278, 282], "summary": {"covered_lines": 77, "num_statements": 109, "percent_covered": 70.64220183486239, "percent_covered_display": "71", "missing_lines": 32, "excluded_lines": 0, "percent_statements_covered": 70.64220183486239, "percent_statements_covered_display": "71"}, "missing_lines": [29, 30, 34, 46, 67, 68, 72, 83, 113, 189, 191, 192, 202, 203, 225, 238, 239, 240, 241, 254, 277, 283, 296, 297, 298, 299, 300, 301, 302, 303, 304, 316], "excluded_lines": [], "functions": {"SqlPolicyRepository.__init__": {"executed_lines": [22], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "SqlPolicyRepository.list_current_policies": {"executed_lines": [27, 28, 31, 32, 33, 47, 48, 60], "summary": {"covered_lines": 8, "num_statements": 12, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [29, 30, 34, 46], "excluded_lines": [], "start_line": 24}, "SqlPolicyRepository.list_current_policy_views": {"executed_lines": [65, 66, 69, 70, 71, 84, 85, 96], "summary": {"covered_lines": 8, "num_statements": 12, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [67, 68, 72, 83], "excluded_lines": [], "start_line": 62}, "SqlPolicyRepository.list_current_policy_page": {"executed_lines": [105, 112, 122, 125, 126, 127, 139, 140, 141, 142, 143], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 91.66666666666667, "percent_statements_covered_display": "92"}, "missing_lines": [113], "excluded_lines": [], "start_line": 98}, "SqlPolicyRepository.create_policy_version": {"executed_lines": [157, 166, 167, 168, 169, 170, 172, 173, 175, 186, 187, 188, 204, 205], "summary": {"covered_lines": 14, "num_statements": 19, "percent_covered": 73.6842105263158, "percent_covered_display": "74", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 73.6842105263158, "percent_statements_covered_display": "74"}, "missing_lines": [189, 191, 192, 202, 203], "excluded_lines": [], "start_line": 145}, "SqlPolicyRepository.rollback_policy": {"executed_lines": [215, 224, 226, 236, 237], "summary": {"covered_lines": 5, "num_statements": 10, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [225, 238, 239, 240, 241], "excluded_lines": [], "start_line": 214}, "SqlPolicyRepository.delete_policy": {"executed_lines": [244, 253, 255, 260, 261], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [254], "excluded_lines": [], "start_line": 243}, "SqlPolicyRepository._current_policy_rows": {"executed_lines": [266, 267, 274, 275, 278], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [277], "excluded_lines": [], "start_line": 263}, "SqlPolicyRepository._legacy_current_policy_rows": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [283, 296, 297, 298, 299, 300, 301, 302, 303, 304, 316], "excluded_lines": [], "start_line": 282}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 24, 62, 98, 145, 214, 243, 263, 282], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlPolicyRepository": {"executed_lines": [22, 27, 28, 31, 32, 33, 47, 48, 60, 65, 66, 69, 70, 71, 84, 85, 96, 105, 112, 122, 125, 126, 127, 139, 140, 141, 142, 143, 157, 166, 167, 168, 169, 170, 172, 173, 175, 186, 187, 188, 204, 205, 215, 224, 226, 236, 237, 244, 253, 255, 260, 261, 266, 267, 274, 275, 278], "summary": {"covered_lines": 57, "num_statements": 89, "percent_covered": 64.04494382022472, "percent_covered_display": "64", "missing_lines": 32, "excluded_lines": 0, "percent_statements_covered": 64.04494382022472, "percent_statements_covered_display": "64"}, "missing_lines": [29, 30, 34, 46, 67, 68, 72, 83, 113, 189, 191, 192, 202, 203, 225, 238, 239, 240, 241, 254, 277, 283, 296, 297, 298, 299, 300, 301, 302, 303, 304, 316], "excluded_lines": [], "start_line": 18}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 12, 13, 14, 15, 18, 21, 24, 62, 98, 145, 214, 243, 263, 282], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/relationships.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 17, 19, 22, 38, 49, 58, 64, 85, 97, 98, 99, 109, 110, 120, 122, 125, 141, 152, 155, 156, 189, 228, 238, 246, 247, 248, 249], "summary": {"covered_lines": 35, "num_statements": 51, "percent_covered": 68.62745098039215, "percent_covered_display": "69", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 68.62745098039215, "percent_statements_covered_display": "69"}, "missing_lines": [65, 111, 112, 157, 174, 177, 178, 187, 192, 193, 194, 198, 215, 216, 217, 226], "excluded_lines": [], "functions": {"SqlRelationshipRepository.__init__": {"executed_lines": [17], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "SqlRelationshipRepository.list_for_subject": {"executed_lines": [22, 38], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "SqlRelationshipRepository.list_for_subject_page": {"executed_lines": [58, 64, 85, 97, 98, 99, 109, 110, 120], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [65, 111, 112], "excluded_lines": [], "start_line": 49}, "SqlRelationshipRepository.list_for_object": {"executed_lines": [125, 141], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 122}, "SqlRelationshipRepository.list_for_subjects": {"executed_lines": [155, 156], "summary": {"covered_lines": 2, "num_statements": 7, "percent_covered": 28.571428571428573, "percent_covered_display": "29", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 28.571428571428573, "percent_statements_covered_display": "29"}, "missing_lines": [157, 174, 177, 178, 187], "excluded_lines": [], "start_line": 152}, "SqlRelationshipRepository.list_for_objects": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [192, 193, 194, 198, 215, 216, 217, 226], "excluded_lines": [], "start_line": 189}, "SqlRelationshipRepository.create": {"executed_lines": [238, 246, 247, 248, 249], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 228}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 19, 49, 122, 152, 189, 228], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlRelationshipRepository": {"executed_lines": [17, 22, 38, 58, 64, 85, 97, 98, 99, 109, 110, 120, 125, 141, 155, 156, 238, 246, 247, 248, 249], "summary": {"covered_lines": 21, "num_statements": 37, "percent_covered": 56.75675675675676, "percent_covered_display": "57", "missing_lines": 16, "excluded_lines": 0, "percent_statements_covered": 56.75675675675676, "percent_statements_covered_display": "57"}, "missing_lines": [65, 111, 112, 157, 174, 177, 178, 187, 192, 193, 194, 198, 215, 216, 217, 226], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 13, 16, 19, 49, 122, 152, 189, 228], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/tenants.py": {"executed_lines": [7, 9, 10, 12, 13, 16, 19, 20, 22, 30, 31, 36, 37, 38, 39, 40, 41, 42, 44, 45, 46, 48, 49, 50, 51, 53, 54, 55, 57, 58, 59, 60, 62, 63], "summary": {"covered_lines": 34, "num_statements": 40, "percent_covered": 85.0, "percent_covered_display": "85", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 85.0, "percent_statements_covered_display": "85"}, "missing_lines": [23, 26, 27, 28, 47, 56], "excluded_lines": [], "functions": {"SqlTenantRepository.__init__": {"executed_lines": [20], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "SqlTenantRepository.get_by_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [23, 26, 27, 28], "excluded_lines": [], "start_line": 22}, "SqlTenantRepository.get_or_create": {"executed_lines": [31, 36, 37, 38, 39, 40, 41, 42], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "SqlTenantRepository.bump_policy_version": {"executed_lines": [45, 46, 48, 49, 50, 51], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [47], "excluded_lines": [], "start_line": 44}, "SqlTenantRepository.bump_revision": {"executed_lines": [54, 55, 57, 58, 59, 60], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 85.71428571428571, "percent_statements_covered_display": "86"}, "missing_lines": [56], "excluded_lines": [], "start_line": 53}, "SqlTenantRepository._to_record": {"executed_lines": [63], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 62}, "": {"executed_lines": [7, 9, 10, 12, 13, 16, 19, 22, 30, 44, 53, 62], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlTenantRepository": {"executed_lines": [20, 31, 36, 37, 38, 39, 40, 41, 42, 45, 46, 48, 49, 50, 51, 54, 55, 57, 58, 59, 60, 63], "summary": {"covered_lines": 22, "num_statements": 28, "percent_covered": 78.57142857142857, "percent_covered_display": "79", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 78.57142857142857, "percent_statements_covered_display": "79"}, "missing_lines": [23, 26, 27, 28, 47, 56], "excluded_lines": [], "start_line": 16}, "": {"executed_lines": [7, 9, 10, 12, 13, 16, 19, 22, 30, 44, 53, 62], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/repositories/users.py": {"executed_lines": [3, 5, 7, 8, 10, 13, 16, 17, 19, 20, 29, 30, 45, 46, 47, 49, 50, 51], "summary": {"covered_lines": 18, "num_statements": 37, "percent_covered": 48.648648648648646, "percent_covered_display": "49", "missing_lines": 19, "excluded_lines": 0, "percent_statements_covered": 48.648648648648646, "percent_statements_covered_display": "49"}, "missing_lines": [31, 32, 33, 34, 35, 36, 37, 38, 52, 61, 62, 63, 64, 65, 66, 67, 68, 69, 75], "excluded_lines": [], "functions": {"SqlUserRepository.__init__": {"executed_lines": [17], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "SqlUserRepository.get_user_context": {"executed_lines": [20, 29, 30], "summary": {"covered_lines": 3, "num_statements": 11, "percent_covered": 27.272727272727273, "percent_covered_display": "27", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 27.272727272727273, "percent_statements_covered_display": "27"}, "missing_lines": [31, 32, 33, 34, 35, 36, 37, 38], "excluded_lines": [], "start_line": 19}, "SqlUserRepository.list_user_ids": {"executed_lines": [46, 47], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 45}, "SqlUserRepository.get_user_contexts": {"executed_lines": [50, 51], "summary": {"covered_lines": 2, "num_statements": 13, "percent_covered": 15.384615384615385, "percent_covered_display": "15", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 15.384615384615385, "percent_statements_covered_display": "15"}, "missing_lines": [52, 61, 62, 63, 64, 65, 66, 67, 68, 69, 75], "excluded_lines": [], "start_line": 49}, "": {"executed_lines": [3, 5, 7, 8, 10, 13, 16, 19, 45, 49], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SqlUserRepository": {"executed_lines": [17, 20, 29, 30, 46, 47, 50, 51], "summary": {"covered_lines": 8, "num_statements": 27, "percent_covered": 29.62962962962963, "percent_covered_display": "30", "missing_lines": 19, "excluded_lines": 0, "percent_statements_covered": 29.62962962962963, "percent_statements_covered_display": "30"}, "missing_lines": [31, 32, 33, 34, 35, 36, 37, 38, 52, 61, 62, 63, 64, 65, 66, 67, 68, 69, 75], "excluded_lines": [], "start_line": 13}, "": {"executed_lines": [3, 5, 7, 8, 10, 13, 16, 19, 45, 49], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/storage/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/infrastructure/storage/session.py": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 14, 17, 18, 19, 42, 43, 45, 55, 58, 59, 60, 61, 64, 65, 66, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, 80, 83, 84, 85, 86, 87, 88, 89, 91], "summary": {"covered_lines": 44, "num_statements": 46, "percent_covered": 95.65217391304348, "percent_covered_display": "96", "missing_lines": 2, "excluded_lines": 16, "percent_statements_covered": 95.65217391304348, "percent_statements_covered_display": "96"}, "missing_lines": [49, 67], "excluded_lines": [22, 23, 24, 25, 26, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39], "functions": {"_operation_name": {"executed_lines": [18, 19], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "_before_cursor_execute": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [26], "start_line": 23}, "_after_cursor_execute": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 7, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [33, 34, 35, 36, 37, 38, 39], "start_line": 30}, "create_engine_for_url": {"executed_lines": [45, 55], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [49], "excluded_lines": [], "start_line": 43}, "create_session_factory": {"executed_lines": [60, 61], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 59}, "initialize_database": {"executed_lines": [66, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, 80], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 92.3076923076923, "percent_statements_covered_display": "92"}, "missing_lines": [67], "excluded_lines": [], "start_line": 65}, "get_db": {"executed_lines": [84, 85, 86, 87, 88, 89, 91], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 83}, "": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 14, 17, 42, 43, 58, 59, 64, 65, 83], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 8, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [22, 23, 24, 25, 29, 30, 31, 32], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 5, 7, 8, 9, 10, 11, 13, 14, 17, 18, 19, 42, 43, 45, 55, 58, 59, 60, 61, 64, 65, 66, 69, 70, 71, 72, 73, 74, 75, 76, 77, 79, 80, 83, 84, 85, 86, 87, 88, 89, 91], "summary": {"covered_lines": 44, "num_statements": 46, "percent_covered": 95.65217391304348, "percent_covered_display": "96", "missing_lines": 2, "excluded_lines": 16, "percent_statements_covered": 95.65217391304348, "percent_statements_covered_display": "96"}, "missing_lines": [49, 67], "excluded_lines": [22, 23, 24, 25, 26, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39], "start_line": 1}}}, "keynetra/main.py": {"executed_lines": [6, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [6, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [6, 8], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/migrations.py": {"executed_lines": [3, 5, 6, 7, 9, 10, 13, 14, 15, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31], "summary": {"covered_lines": 21, "num_statements": 23, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 91.30434782608695, "percent_statements_covered_display": "91"}, "missing_lines": [16, 17], "excluded_lines": [], "functions": {"parse_revision_file": {"executed_lines": [14, 15, 18, 19, 20, 21], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [16, 17], "excluded_lines": [], "start_line": 13}, "find_destructive_revisions": {"executed_lines": [25, 26, 27, 28, 29, 30, 31], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24}, "": {"executed_lines": [3, 5, 6, 7, 9, 10, 13, 24], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 7, 9, 10, 13, 14, 15, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31], "summary": {"covered_lines": 21, "num_statements": 23, "percent_covered": 91.30434782608695, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 91.30434782608695, "percent_statements_covered_display": "91"}, "missing_lines": [16, 17], "excluded_lines": [], "start_line": 1}}}, "keynetra/modeling/__init__.py": {"executed_lines": [1, 2, 3, 5], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2, 3, 5], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 2, 3, 5], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/modeling/model_validator.py": {"executed_lines": [3, 5, 14, 15, 17, 19, 21, 23, 24, 26, 27, 29, 30, 32, 35, 36, 37, 39, 40, 43, 44, 45, 46], "summary": {"covered_lines": 23, "num_statements": 34, "percent_covered": 67.6470588235294, "percent_covered_display": "68", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 67.6470588235294, "percent_statements_covered_display": "68"}, "missing_lines": [16, 18, 20, 22, 25, 28, 31, 38, 41, 42, 47], "excluded_lines": [], "functions": {"validate_authorization_schema": {"executed_lines": [15, 17, 19, 21, 23, 24, 26, 27, 29, 30, 32], "summary": {"covered_lines": 11, "num_statements": 18, "percent_covered": 61.111111111111114, "percent_covered_display": "61", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 61.111111111111114, "percent_statements_covered_display": "61"}, "missing_lines": [16, 18, 20, 22, 25, 28, 31], "excluded_lines": [], "start_line": 14}, "_validate_expr": {"executed_lines": [36, 37, 39, 40, 43, 44, 45, 46], "summary": {"covered_lines": 8, "num_statements": 12, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [38, 41, 42, 47], "excluded_lines": [], "start_line": 35}, "": {"executed_lines": [3, 5, 14, 35], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 14, 15, 17, 19, 21, 23, 24, 26, 27, 29, 30, 32, 35, 36, 37, 39, 40, 43, 44, 45, 46], "summary": {"covered_lines": 23, "num_statements": 34, "percent_covered": 67.6470588235294, "percent_covered_display": "68", "missing_lines": 11, "excluded_lines": 0, "percent_statements_covered": 67.6470588235294, "percent_statements_covered_display": "68"}, "missing_lines": [16, 18, 20, 22, 25, 28, 31, 38, 41, 42, 47], "excluded_lines": [], "start_line": 1}}}, "keynetra/modeling/permission_compiler.py": {"executed_lines": [3, 5, 6, 8, 9, 20, 21, 22, 23, 26, 27, 28, 29, 30, 31, 32, 34, 35, 47, 50, 53, 54, 58, 67, 68, 69, 70, 72, 74, 75], "summary": {"covered_lines": 30, "num_statements": 33, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.9090909090909, "percent_statements_covered_display": "91"}, "missing_lines": [71, 73, 76], "excluded_lines": [], "functions": {"CompiledAuthorizationModel.to_dict": {"executed_lines": [35], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "compile_authorization_schema": {"executed_lines": [50, 53, 54, 58], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "_expr_to_dict": {"executed_lines": [68, 69, 70, 72, 74, 75], "summary": {"covered_lines": 6, "num_statements": 9, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [71, 73, 76], "excluded_lines": [], "start_line": 67}, "": {"executed_lines": [3, 5, 6, 8, 9, 20, 21, 22, 23, 26, 27, 28, 29, 30, 31, 32, 34, 47, 67], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"CompiledPermission": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "CompiledAuthorizationModel": {"executed_lines": [35], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "": {"executed_lines": [3, 5, 6, 8, 9, 20, 21, 22, 23, 26, 27, 28, 29, 30, 31, 32, 34, 47, 50, 53, 54, 58, 67, 68, 69, 70, 72, 74, 75], "summary": {"covered_lines": 29, "num_statements": 32, "percent_covered": 90.625, "percent_covered_display": "91", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.625, "percent_statements_covered_display": "91"}, "missing_lines": [71, 73, 76], "excluded_lines": [], "start_line": 1}}}, "keynetra/modeling/schema_parser.py": {"executed_lines": [3, 5, 6, 7, 10, 11, 12, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29, 32, 35, 36, 37, 38, 39, 40, 41, 44, 47, 48, 49, 50, 53, 54, 55, 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 82, 91, 92, 94, 95, 96, 97, 99, 101, 102, 104, 107, 108, 110, 111, 112, 113, 115, 116, 117, 119, 122, 123, 124, 126, 129, 130, 131, 132, 133, 134, 137, 138, 139, 142, 145, 146, 148, 149, 150, 153, 158, 160], "summary": {"covered_lines": 98, "num_statements": 119, "percent_covered": 82.3529411764706, "percent_covered_display": "82", "missing_lines": 21, "excluded_lines": 0, "percent_statements_covered": 82.3529411764706, "percent_statements_covered_display": "82"}, "missing_lines": [51, 56, 80, 93, 98, 100, 103, 109, 114, 118, 125, 140, 141, 147, 151, 152, 154, 155, 156, 157, 159], "excluded_lines": [], "functions": {"parse_authorization_schema": {"executed_lines": [48, 49, 50, 53, 54, 55, 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 82], "summary": {"covered_lines": 28, "num_statements": 31, "percent_covered": 90.3225806451613, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.3225806451613, "percent_statements_covered_display": "90"}, "missing_lines": [51, 56, 80], "excluded_lines": [], "start_line": 47}, "_parse_relation": {"executed_lines": [92, 94, 95, 96, 97, 99, 101, 102, 104], "summary": {"covered_lines": 9, "num_statements": 13, "percent_covered": 69.23076923076923, "percent_covered_display": "69", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 69.23076923076923, "percent_statements_covered_display": "69"}, "missing_lines": [93, 98, 100, 103], "excluded_lines": [], "start_line": 91}, "_parse_permission": {"executed_lines": [108, 110, 111, 112, 113, 115, 116, 117, 119], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [109, 114, 118], "excluded_lines": [], "start_line": 107}, "_tokenize": {"executed_lines": [123, 124, 126], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [125], "excluded_lines": [], "start_line": 122}, "_parse_expr": {"executed_lines": [130, 131, 132, 133, 134], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 129}, "_parse_term": {"executed_lines": [138, 139, 142], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 60.0, "percent_statements_covered_display": "60"}, "missing_lines": [140, 141], "excluded_lines": [], "start_line": 137}, "_parse_factor": {"executed_lines": [146, 148, 149, 150, 153, 158, 160], "summary": {"covered_lines": 7, "num_statements": 15, "percent_covered": 46.666666666666664, "percent_covered_display": "47", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 46.666666666666664, "percent_statements_covered_display": "47"}, "missing_lines": [147, 151, 152, 154, 155, 156, 157, 159], "excluded_lines": [], "start_line": 145}, "": {"executed_lines": [3, 5, 6, 7, 10, 11, 12, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29, 32, 35, 36, 37, 38, 39, 40, 41, 44, 47, 91, 107, 122, 129, 137, 145], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"IdentifierExpr": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 11}, "NotExpr": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "AndExpr": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "OrExpr": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "AuthorizationSchema": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 36}, "": {"executed_lines": [3, 5, 6, 7, 10, 11, 12, 15, 16, 17, 20, 21, 22, 23, 26, 27, 28, 29, 32, 35, 36, 37, 38, 39, 40, 41, 44, 47, 48, 49, 50, 53, 54, 55, 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 82, 91, 92, 94, 95, 96, 97, 99, 101, 102, 104, 107, 108, 110, 111, 112, 113, 115, 116, 117, 119, 122, 123, 124, 126, 129, 130, 131, 132, 133, 134, 137, 138, 139, 142, 145, 146, 148, 149, 150, 153, 158, 160], "summary": {"covered_lines": 98, "num_statements": 119, "percent_covered": 82.3529411764706, "percent_covered_display": "82", "missing_lines": 21, "excluded_lines": 0, "percent_statements_covered": 82.3529411764706, "percent_statements_covered_display": "82"}, "missing_lines": [51, 56, 80, 93, 98, 100, 103, 109, 114, 118, 125, 140, 141, 147, 151, 152, 154, 155, 156, 157, 159], "excluded_lines": [], "start_line": 1}}}, "keynetra/observability/__init__.py": {"executed_lines": [1, 3, 19], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 19], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 19], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/observability/http_metrics.py": {"executed_lines": [3, 5, 6, 11, 12, 17, 27, 30, 31, 32, 33, 35, 36, 42, 43], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [7, 8, 9, 22, 23, 24], "functions": {"record_http_request": {"executed_lines": [30, 31, 32, 33, 35, 36, 42, 43], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 27}, "": {"executed_lines": [3, 5, 6, 11, 12, 17, 27], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [7, 8, 9, 22, 23, 24], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 11, 12, 17, 27, 30, 31, 32, 33, 35, 36, 42, 43], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [7, 8, 9, 22, 23, 24], "start_line": 1}}}, "keynetra/observability/metrics.py": {"executed_lines": [3, 5, 6, 11, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 123, 124, 125, 128, 129, 130, 137, 138, 139, 142, 143, 144, 147, 148, 149, 152, 153, 154, 157, 158, 159, 162, 163, 164, 167, 168, 169, 174, 175, 176, 177, 180, 181, 182, 183, 186, 187, 188, 189, 190, 191, 193, 194, 196, 197, 201, 202, 203, 206, 207, 208, 211, 212, 213, 216, 217, 218, 221, 226, 227, 228, 231, 232, 233], "summary": {"covered_lines": 86, "num_statements": 90, "percent_covered": 95.55555555555556, "percent_covered_display": "96", "missing_lines": 4, "excluded_lines": 22, "percent_statements_covered": 95.55555555555556, "percent_statements_covered_display": "96"}, "missing_lines": [192, 198, 222, 223], "excluded_lines": [7, 8, 9, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120], "functions": {"_tenant_label": {"executed_lines": [124, 125], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 123}, "_cache_type_label": {"executed_lines": [129, 130], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 128}, "record_access_check": {"executed_lines": [138, 139], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 137}, "record_acl_match": {"executed_lines": [143, 144], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 142}, "record_policy_evaluation": {"executed_lines": [148, 149], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 147}, "record_relationship_traversal": {"executed_lines": [153, 154], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 152}, "record_policy_compilation": {"executed_lines": [158, 159], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 157}, "record_revision_update": {"executed_lines": [163, 164], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 162}, "observe_access_check_latency": {"executed_lines": [168, 169], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 167}, "record_cache_hit": {"executed_lines": [175, 176, 177], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 174}, "record_cache_miss": {"executed_lines": [181, 182, 183], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 180}, "record_cache_event": {"executed_lines": [187, 188, 189, 190, 191, 193, 194, 196, 197], "summary": {"covered_lines": 9, "num_statements": 11, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82"}, "missing_lines": [192, 198], "excluded_lines": [], "start_line": 186}, "observe_decision_latency": {"executed_lines": [202, 203], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 201}, "record_api_error": {"executed_lines": [207, 208], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 206}, "record_bootstrap_failure": {"executed_lines": [212, 213], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 211}, "record_auth_failure": {"executed_lines": [217, 218], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 216}, "record_jwks_fetch": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [222, 223], "excluded_lines": [], "start_line": 221}, "record_access_index_rebuild": {"executed_lines": [227, 228], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 226}, "observe_db_query_latency": {"executed_lines": [232, 233], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 231}, "": {"executed_lines": [3, 5, 6, 11, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 123, 128, 137, 142, 147, 152, 157, 162, 167, 174, 180, 186, 201, 206, 211, 216, 221, 226, 231], "summary": {"covered_lines": 41, "num_statements": 41, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 22, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [7, 8, 9, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 11, 12, 17, 22, 27, 32, 37, 42, 47, 52, 57, 62, 67, 72, 77, 82, 87, 92, 97, 123, 124, 125, 128, 129, 130, 137, 138, 139, 142, 143, 144, 147, 148, 149, 152, 153, 154, 157, 158, 159, 162, 163, 164, 167, 168, 169, 174, 175, 176, 177, 180, 181, 182, 183, 186, 187, 188, 189, 190, 191, 193, 194, 196, 197, 201, 202, 203, 206, 207, 208, 211, 212, 213, 216, 217, 218, 221, 226, 227, 228, 231, 232, 233], "summary": {"covered_lines": 86, "num_statements": 90, "percent_covered": 95.55555555555556, "percent_covered_display": "96", "missing_lines": 4, "excluded_lines": 22, "percent_statements_covered": 95.55555555555556, "percent_statements_covered_display": "96"}, "missing_lines": [192, 198, 222, 223], "excluded_lines": [7, 8, 9, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120], "start_line": 1}}}, "keynetra/services/__init__.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/access_indexer.py": {"executed_lines": [3, 5, 6, 7, 8, 10, 11, 21, 22, 23, 24, 26, 30, 36, 39, 47, 48, 49, 50, 51, 52, 53, 54, 56, 64, 70, 71, 72, 73, 74, 83, 89, 90, 92, 100, 101, 107, 108, 114, 122, 128, 142, 143, 164, 171, 173, 203, 204, 205, 206, 207, 214, 219, 220, 222, 223, 226, 229, 230, 235, 238, 245, 274, 275], "summary": {"covered_lines": 64, "num_statements": 114, "percent_covered": 56.14035087719298, "percent_covered_display": "56", "missing_lines": 50, "excluded_lines": 0, "percent_statements_covered": 56.14035087719298, "percent_statements_covered_display": "56"}, "missing_lines": [27, 31, 75, 81, 176, 177, 178, 179, 180, 182, 183, 184, 190, 191, 193, 194, 196, 201, 208, 209, 210, 211, 212, 236, 239, 240, 241, 242, 243, 246, 247, 248, 249, 250, 251, 252, 255, 256, 257, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272], "excluded_lines": [], "functions": {"AccessSubject.to_descriptor": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [27], "excluded_lines": [], "start_line": 26}, "relationship_descriptor": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [31], "excluded_lines": [], "start_line": 30}, "AccessIndexer.__init__": {"executed_lines": [47, 48, 49, 50, 51, 52, 53, 54], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 39}, "AccessIndexer.build_resource_index": {"executed_lines": [64, 70, 71, 72, 73, 74, 83, 89, 90], "summary": {"covered_lines": 9, "num_statements": 11, "percent_covered": 81.81818181818181, "percent_covered_display": "82", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 81.81818181818181, "percent_statements_covered_display": "82"}, "missing_lines": [75, 81], "excluded_lines": [], "start_line": 56}, "AccessIndexer._rebuild_resource_index": {"executed_lines": [100, 101, 107, 108, 114, 122, 128, 142, 143, 164, 171], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 92}, "AccessIndexer._schedule_background_refresh": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [176, 177, 178, 179, 180, 182, 196, 201], "excluded_lines": [], "start_line": 173}, "AccessIndexer._schedule_background_refresh.run": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [183, 184, 190, 191, 193, 194], "excluded_lines": [], "start_line": 182}, "AccessIndexer._memo_get": {"executed_lines": [204, 205, 206, 207], "summary": {"covered_lines": 4, "num_statements": 9, "percent_covered": 44.44444444444444, "percent_covered_display": "44", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 44.44444444444444, "percent_statements_covered_display": "44"}, "missing_lines": [208, 209, 210, 211, 212], "excluded_lines": [], "start_line": 203}, "AccessIndexer._memo_set": {"executed_lines": [219, 220], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 214}, "AccessIndexer.invalidate_resource": {"executed_lines": [223, 226, 229, 230, 235], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 83.33333333333333, "percent_statements_covered_display": "83"}, "missing_lines": [236], "excluded_lines": [], "start_line": 222}, "AccessIndexer.invalidate_tenant": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [239, 240, 241, 242, 243], "excluded_lines": [], "start_line": 238}, "AccessIndexer.subject_descriptors": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 21, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 21, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [246, 247, 248, 249, 250, 251, 252, 255, 256, 257, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272], "excluded_lines": [], "start_line": 245}, "AccessIndexer._subject_descriptor": {"executed_lines": [275], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 274}, "": {"executed_lines": [3, 5, 6, 7, 8, 10, 11, 21, 22, 23, 24, 26, 30, 36, 39, 56, 92, 173, 203, 214, 222, 238, 245, 274], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AccessSubject": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [27], "excluded_lines": [], "start_line": 22}, "AccessIndexer": {"executed_lines": [47, 48, 49, 50, 51, 52, 53, 54, 64, 70, 71, 72, 73, 74, 83, 89, 90, 100, 101, 107, 108, 114, 122, 128, 142, 143, 164, 171, 204, 205, 206, 207, 219, 220, 223, 226, 229, 230, 235, 275], "summary": {"covered_lines": 40, "num_statements": 88, "percent_covered": 45.45454545454545, "percent_covered_display": "45", "missing_lines": 48, "excluded_lines": 0, "percent_statements_covered": 45.45454545454545, "percent_statements_covered_display": "45"}, "missing_lines": [75, 81, 176, 177, 178, 179, 180, 182, 183, 184, 190, 191, 193, 194, 196, 201, 208, 209, 210, 211, 212, 236, 239, 240, 241, 242, 243, 246, 247, 248, 249, 250, 251, 252, 255, 256, 257, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272], "excluded_lines": [], "start_line": 36}, "": {"executed_lines": [3, 5, 6, 7, 8, 10, 11, 21, 22, 23, 24, 26, 30, 36, 39, 56, 92, 173, 203, 214, 222, 238, 245, 274], "summary": {"covered_lines": 24, "num_statements": 25, "percent_covered": 96.0, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 96.0, "percent_statements_covered_display": "96"}, "missing_lines": [31], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/attribute_validation.py": {"executed_lines": [1, 3, 6, 7, 10, 11, 13, 15, 17, 18, 20, 22, 26, 27, 30, 31], "summary": {"covered_lines": 16, "num_statements": 22, "percent_covered": 72.72727272727273, "percent_covered_display": "73", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 72.72727272727273, "percent_statements_covered_display": "73"}, "missing_lines": [12, 14, 16, 19, 21, 23], "excluded_lines": [], "functions": {"_validate_dict": {"executed_lines": [11, 13, 15, 17, 18, 20, 22], "summary": {"covered_lines": 7, "num_statements": 13, "percent_covered": 53.84615384615385, "percent_covered_display": "54", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 53.84615384615385, "percent_statements_covered_display": "54"}, "missing_lines": [12, 14, 16, 19, 21, 23], "excluded_lines": [], "start_line": 10}, "validate_user": {"executed_lines": [27], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 26}, "validate_resource": {"executed_lines": [31], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "": {"executed_lines": [1, 3, 6, 7, 10, 26, 30], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AttributeValidationError": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 6}, "": {"executed_lines": [1, 3, 6, 7, 10, 11, 13, 15, 17, 18, 20, 22, 26, 27, 30, 31], "summary": {"covered_lines": 16, "num_statements": 22, "percent_covered": 72.72727272727273, "percent_covered_display": "73", "missing_lines": 6, "excluded_lines": 0, "percent_statements_covered": 72.72727272727273, "percent_statements_covered_display": "73"}, "missing_lines": [12, 14, 16, 19, 21, 23], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/audit.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [7, 9], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [7, 9], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [7, 9], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/authorization.py": {"executed_lines": [7, 9, 10, 11, 12, 13, 14, 16, 17, 18, 24, 25, 26, 27, 28, 29, 44, 45, 48, 49, 52, 53, 54, 57, 60, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 105, 107, 121, 122, 123, 124, 131, 132, 139, 140, 143, 144, 146, 147, 148, 153, 154, 160, 161, 162, 165, 171, 177, 178, 179, 180, 181, 189, 209, 211, 239, 251, 252, 253, 254, 255, 256, 260, 261, 292, 293, 296, 297, 298, 299, 300, 301, 306, 309, 317, 322, 323, 324, 330, 331, 335, 336, 339, 341, 349, 353, 355, 379, 389, 398, 400, 401, 403, 421, 430, 431, 432, 436, 437, 449, 459, 460, 461, 462, 463, 464, 470, 471, 472, 473, 479, 480, 488, 499, 500, 501, 502, 503, 504, 505, 506, 510, 511, 512, 513, 514, 517, 518, 526, 532, 533, 535, 538, 539, 542, 547, 549, 550, 551, 554, 556, 558, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 574, 575, 601, 602, 612, 613, 623, 626, 627, 628, 629, 632, 656, 657, 658, 660, 661, 662, 676, 677, 679, 680, 681, 690, 691, 699, 700, 701, 715, 716, 718, 721, 722, 738, 739, 740, 756, 759, 761, 764, 765, 786, 787, 794, 795, 797, 798, 799], "summary": {"covered_lines": 217, "num_statements": 255, "percent_covered": 85.09803921568627, "percent_covered_display": "85", "missing_lines": 38, "excluded_lines": 0, "percent_statements_covered": 85.09803921568627, "percent_statements_covered_display": "85"}, "missing_lines": [190, 191, 201, 225, 267, 268, 274, 307, 332, 333, 338, 367, 412, 419, 465, 467, 468, 469, 651, 652, 666, 667, 668, 675, 705, 706, 707, 714, 729, 730, 746, 747, 748, 755, 777, 778, 806, 807], "excluded_lines": [], "functions": {"AuthorizationService.__init__": {"executed_lines": [78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 105], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 60}, "AuthorizationService.authorize": {"executed_lines": [121, 122, 123, 124, 131, 132, 139, 140, 143, 144, 146, 147, 148, 153, 154, 160, 161, 162, 165, 171, 177, 178, 179, 180, 181, 189, 209], "summary": {"covered_lines": 27, "num_statements": 30, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [190, 191, 201], "excluded_lines": [], "start_line": 107}, "AuthorizationService.authorize_async": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [225], "excluded_lines": [], "start_line": 211}, "AuthorizationService.authorize_batch": {"executed_lines": [251, 252, 253, 254, 255, 256, 260, 261, 292, 293, 296, 297, 298, 299, 300, 301, 306, 309, 317, 322, 323, 324, 330, 331, 335, 336, 339, 341, 349, 353], "summary": {"covered_lines": 30, "num_statements": 37, "percent_covered": 81.08108108108108, "percent_covered_display": "81", "missing_lines": 7, "excluded_lines": 0, "percent_statements_covered": 81.08108108108108, "percent_statements_covered_display": "81"}, "missing_lines": [267, 268, 274, 307, 332, 333, 338], "excluded_lines": [], "start_line": 239}, "AuthorizationService.authorize_batch_async": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [367], "excluded_lines": [], "start_line": 355}, "AuthorizationService.simulate": {"executed_lines": [389, 398], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 379}, "AuthorizationService.get_revision": {"executed_lines": [401], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 400}, "AuthorizationService.build_input": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [412, 419], "excluded_lines": [], "start_line": 403}, "AuthorizationService._build_input": {"executed_lines": [430, 431, 432, 436, 437], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 421}, "AuthorizationService._build_authorization_input": {"executed_lines": [459, 460, 461, 462, 463, 464, 470, 471, 472, 473, 479, 480, 488], "summary": {"covered_lines": 13, "num_statements": 17, "percent_covered": 76.47058823529412, "percent_covered_display": "76", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 76.47058823529412, "percent_statements_covered_display": "76"}, "missing_lines": [465, 467, 468, 469], "excluded_lines": [], "start_line": 449}, "AuthorizationService._hydrate_user": {"executed_lines": [500, 501, 502, 503, 504, 505, 506, 510, 511, 512, 513, 514, 517, 518, 526, 532, 533], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 499}, "AuthorizationService._build_engine": {"executed_lines": [538, 539, 542, 547, 549, 558, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 535}, "AuthorizationService._build_engine._load_current_policies": {"executed_lines": [550, 551, 554, 556], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 549}, "AuthorizationService._decision_from_cache": {"executed_lines": [575], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 574}, "AuthorizationService._safe_deny": {"executed_lines": [602], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 601}, "AuthorizationService._safe_allow": {"executed_lines": [613], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 612}, "AuthorizationService._fallback_decision": {"executed_lines": [626, 627, 628, 629, 632, 656, 657, 658], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [651, 652], "excluded_lines": [], "start_line": 623}, "AuthorizationService._safe_cache_get": {"executed_lines": [661, 662, 676, 677], "summary": {"covered_lines": 4, "num_statements": 8, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [666, 667, 668, 675], "excluded_lines": [], "start_line": 660}, "AuthorizationService._safe_cache_set": {"executed_lines": [680, 681, 690, 691], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 679}, "AuthorizationService._safe_policy_cache_get": {"executed_lines": [700, 701, 715, 716], "summary": {"covered_lines": 4, "num_statements": 8, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [705, 706, 707, 714], "excluded_lines": [], "start_line": 699}, "AuthorizationService._safe_policy_cache_set": {"executed_lines": [721, 722], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [729, 730], "excluded_lines": [], "start_line": 718}, "AuthorizationService._safe_relationship_cache_get": {"executed_lines": [739, 740, 756, 759], "summary": {"covered_lines": 4, "num_statements": 8, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [746, 747, 748, 755], "excluded_lines": [], "start_line": 738}, "AuthorizationService._safe_relationship_cache_set": {"executed_lines": [764, 765], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [777, 778], "excluded_lines": [], "start_line": 761}, "AuthorizationService._resource_identity": {"executed_lines": [787, 794, 795], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 786}, "AuthorizationService._safe_audit_write": {"executed_lines": [798, 799], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 50.0, "percent_statements_covered_display": "50"}, "missing_lines": [806, 807], "excluded_lines": [], "start_line": 797}, "": {"executed_lines": [7, 9, 10, 11, 12, 13, 14, 16, 17, 18, 24, 25, 26, 27, 28, 29, 44, 45, 48, 49, 52, 53, 54, 57, 60, 107, 211, 239, 355, 379, 400, 403, 421, 449, 499, 535, 574, 601, 612, 623, 660, 679, 699, 718, 738, 761, 786, 797], "summary": {"covered_lines": 48, "num_statements": 48, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"AuthorizationResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 49}, "AuthorizationService": {"executed_lines": [78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 105, 121, 122, 123, 124, 131, 132, 139, 140, 143, 144, 146, 147, 148, 153, 154, 160, 161, 162, 165, 171, 177, 178, 179, 180, 181, 189, 209, 251, 252, 253, 254, 255, 256, 260, 261, 292, 293, 296, 297, 298, 299, 300, 301, 306, 309, 317, 322, 323, 324, 330, 331, 335, 336, 339, 341, 349, 353, 389, 398, 401, 430, 431, 432, 436, 437, 459, 460, 461, 462, 463, 464, 470, 471, 472, 473, 479, 480, 488, 500, 501, 502, 503, 504, 505, 506, 510, 511, 512, 513, 514, 517, 518, 526, 532, 533, 538, 539, 542, 547, 549, 550, 551, 554, 556, 558, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 575, 602, 613, 626, 627, 628, 629, 632, 656, 657, 658, 661, 662, 676, 677, 680, 681, 690, 691, 700, 701, 715, 716, 721, 722, 739, 740, 756, 759, 764, 765, 787, 794, 795, 798, 799], "summary": {"covered_lines": 169, "num_statements": 207, "percent_covered": 81.64251207729468, "percent_covered_display": "82", "missing_lines": 38, "excluded_lines": 0, "percent_statements_covered": 81.64251207729468, "percent_statements_covered_display": "82"}, "missing_lines": [190, 191, 201, 225, 267, 268, 274, 307, 332, 333, 338, 367, 412, 419, 465, 467, 468, 469, 651, 652, 666, 667, 668, 675, 705, 706, 707, 714, 729, 730, 746, 747, 748, 755, 777, 778, 806, 807], "excluded_lines": [], "start_line": 57}, "": {"executed_lines": [7, 9, 10, 11, 12, 13, 14, 16, 17, 18, 24, 25, 26, 27, 28, 29, 44, 45, 48, 49, 52, 53, 54, 57, 60, 107, 211, 239, 355, 379, 400, 403, 421, 449, 499, 535, 574, 601, 612, 623, 660, 679, 699, 718, 738, 761, 786, 797], "summary": {"covered_lines": 48, "num_statements": 48, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/doctor.py": {"executed_lines": [7, 9, 10, 11, 12, 14, 15, 17, 18, 19, 22, 23, 26, 27, 28, 29, 32, 35, 41, 42, 50, 53, 57, 58, 59, 60, 61, 62, 65, 68, 73, 74, 76, 77, 78, 79, 82, 84, 88, 90, 111, 114, 115, 116, 117, 118, 130, 133, 134, 135, 138, 139, 140, 147, 150, 151, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 171, 172], "summary": {"covered_lines": 70, "num_statements": 78, "percent_covered": 89.74358974358974, "percent_covered_display": "90", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 89.74358974358974, "percent_statements_covered_display": "90"}, "missing_lines": [75, 83, 85, 89, 124, 125, 141, 142], "excluded_lines": [], "functions": {"run_core_doctor": {"executed_lines": [35, 41, 42], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 32}, "_check_env": {"executed_lines": [53, 57, 58, 59, 60, 61, 62, 65, 68, 73, 74, 76, 77, 78, 79, 82, 84, 88, 90], "summary": {"covered_lines": 19, "num_statements": 23, "percent_covered": 82.6086956521739, "percent_covered_display": "83", "missing_lines": 4, "excluded_lines": 0, "percent_statements_covered": 82.6086956521739, "percent_statements_covered_display": "83"}, "missing_lines": [75, 83, 85, 89], "excluded_lines": [], "start_line": 50}, "_check_database": {"executed_lines": [114, 115, 116, 117, 118], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 71.42857142857143, "percent_statements_covered_display": "71"}, "missing_lines": [124, 125], "excluded_lines": [], "start_line": 111}, "_check_redis": {"executed_lines": [133, 134, 135, 138, 139, 140], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [141, 142], "excluded_lines": [], "start_line": 130}, "_check_migrations": {"executed_lines": [150, 151, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 171, 172], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 147}, "": {"executed_lines": [7, 9, 10, 11, 12, 14, 15, 17, 18, 19, 22, 23, 26, 27, 28, 29, 32, 50, 111, 130, 147], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"DoctorCheck": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 23}, "": {"executed_lines": [7, 9, 10, 11, 12, 14, 15, 17, 18, 19, 22, 23, 26, 27, 28, 29, 32, 35, 41, 42, 50, 53, 57, 58, 59, 60, 61, 62, 65, 68, 73, 74, 76, 77, 78, 79, 82, 84, 88, 90, 111, 114, 115, 116, 117, 118, 130, 133, 134, 135, 138, 139, 140, 147, 150, 151, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 171, 172], "summary": {"covered_lines": 70, "num_statements": 78, "percent_covered": 89.74358974358974, "percent_covered_display": "90", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 89.74358974358974, "percent_statements_covered_display": "90"}, "missing_lines": [75, 83, 85, 89, 124, 125, 141, 142], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/impact_analysis.py": {"executed_lines": [3, 5, 6, 8, 9, 15, 18, 19, 20, 21, 24, 25, 33, 34, 35, 36, 38, 39, 40, 41, 42, 43, 56, 57, 58, 59, 60, 63, 64, 65, 68, 69, 70, 81, 82, 83, 94, 95, 96, 101, 106, 107, 112, 117, 118, 119, 120, 121, 123, 124, 127, 130, 138, 140, 147, 148, 149, 150, 155, 161], "summary": {"covered_lines": 60, "num_statements": 73, "percent_covered": 82.1917808219178, "percent_covered_display": "82", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 82.1917808219178, "percent_statements_covered_display": "82"}, "missing_lines": [61, 62, 66, 67, 79, 80, 92, 93, 104, 105, 131, 151, 154], "excluded_lines": [], "functions": {"ImpactAnalyzer.__init__": {"executed_lines": [33, 34, 35, 36], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "ImpactAnalyzer.analyze_policy_change": {"executed_lines": [39, 40, 41, 42, 43, 56, 57, 58, 59, 60, 63, 64, 65, 68, 69, 70, 81, 82, 83, 94, 95, 96, 101, 106, 107, 112, 117, 118, 119, 120, 121], "summary": {"covered_lines": 31, "num_statements": 41, "percent_covered": 75.60975609756098, "percent_covered_display": "76", "missing_lines": 10, "excluded_lines": 0, "percent_statements_covered": 75.60975609756098, "percent_statements_covered_display": "76"}, "missing_lines": [61, 62, 66, 67, 79, 80, 92, 93, 104, 105], "excluded_lines": [], "start_line": 38}, "ImpactAnalyzer._candidate_resources": {"executed_lines": [124, 127, 130, 138], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 80.0, "percent_statements_covered_display": "80"}, "missing_lines": [131], "excluded_lines": [], "start_line": 123}, "ImpactAnalyzer._enrich_user_with_relationships": {"executed_lines": [147, 148, 149, 150, 155, 161], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [151, 154], "excluded_lines": [], "start_line": 140}, "": {"executed_lines": [3, 5, 6, 8, 9, 15, 18, 19, 20, 21, 24, 25, 38, 123, 140], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"ImpactResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "ImpactAnalyzer": {"executed_lines": [33, 34, 35, 36, 39, 40, 41, 42, 43, 56, 57, 58, 59, 60, 63, 64, 65, 68, 69, 70, 81, 82, 83, 94, 95, 96, 101, 106, 107, 112, 117, 118, 119, 120, 121, 124, 127, 130, 138, 147, 148, 149, 150, 155, 161], "summary": {"covered_lines": 45, "num_statements": 58, "percent_covered": 77.58620689655173, "percent_covered_display": "78", "missing_lines": 13, "excluded_lines": 0, "percent_statements_covered": 77.58620689655173, "percent_statements_covered_display": "78"}, "missing_lines": [61, 62, 66, 67, 79, 80, 92, 93, 104, 105, 131, 151, 154], "excluded_lines": [], "start_line": 24}, "": {"executed_lines": [3, 5, 6, 8, 9, 15, 18, 19, 20, 21, 24, 25, 38, 123, 140], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/interfaces.py": {"executed_lines": [7, 9, 10, 12, 19, 20, 23, 24, 25, 26, 29, 30, 33, 34, 35, 36, 37, 39, 40, 49, 50, 53, 54, 55, 56, 57, 58, 59, 60, 61, 63, 64, 81, 82, 85, 86, 89, 90, 93, 94, 95, 96, 97, 98, 101, 102, 105, 106, 107, 108, 109, 110, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 130, 131, 134, 135, 136, 137, 138, 139, 140, 143, 144, 147, 148, 149, 150, 151, 152, 153, 155, 156, 157, 168, 180, 217, 232, 240, 277, 305, 315, 332, 365, 385, 386, 389, 390, 391, 392, 393, 394, 395, 396, 397, 400, 424, 432, 451], "summary": {"covered_lines": 113, "num_statements": 113, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 238, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 234, 235, 236, 237, 238, 239, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 307, 308, 309, 310, 311, 312, 313, 314, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 426, 427, 428, 429, 430, 431, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 453, 454, 455], "functions": {"RelationshipRecord.to_dict": {"executed_lines": [40], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 39}, "ACLRecord.to_dict": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 63}, "CachedDecision.from_decision": {"executed_lines": [157], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 156}, "TenantRepository.get_or_create": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [171], "start_line": 171}, "TenantRepository.get_by_id": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [173], "start_line": 173}, "TenantRepository.bump_policy_version": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [175], "start_line": 175}, "TenantRepository.bump_revision": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [177], "start_line": 177}, "PolicyRepository.list_current_policies": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [185], "start_line": 183}, "PolicyRepository.list_current_policy_views": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [187], "start_line": 187}, "PolicyRepository.list_current_policy_page": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [195], "start_line": 189}, "PolicyRepository.create_policy_version": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [208], "start_line": 197}, "PolicyRepository.rollback_policy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [212], "start_line": 210}, "PolicyRepository.delete_policy": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [214], "start_line": 214}, "AuthModelRepository.get_model": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [220], "start_line": 220}, "AuthModelRepository.upsert_model": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [229], "start_line": 222}, "UserRepository.get_user_context": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [235], "start_line": 235}, "UserRepository.list_user_ids": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [237], "start_line": 237}, "RelationshipRepository.list_for_subject": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [245], "start_line": 243}, "RelationshipRepository.list_for_subject_page": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [255], "start_line": 247}, "RelationshipRepository.list_for_object": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [263], "start_line": 257}, "RelationshipRepository.create": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [274], "start_line": 265}, "AuditRepository.write": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [289], "start_line": 280}, "AuditRepository.list_page": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [302], "start_line": 291}, "PolicyCache.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [308], "start_line": 308}, "PolicyCache.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [310], "start_line": 310}, "PolicyCache.invalidate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [312], "start_line": 312}, "RelationshipCache.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [320], "start_line": 318}, "RelationshipCache.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [329], "start_line": 322}, "ACLRepository.create_acl_entry": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [345], "start_line": 335}, "ACLRepository.list_resource_acl": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [349], "start_line": 347}, "ACLRepository.get_acl_entry": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [351], "start_line": 351}, "ACLRepository.find_matching_acl": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [360], "start_line": 353}, "ACLRepository.delete_acl_entry": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [362], "start_line": 362}, "ACLCache.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [370], "start_line": 368}, "ACLCache.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [380], "start_line": 372}, "ACLCache.invalidate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [382], "start_line": 382}, "AccessIndexCache.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [405], "start_line": 403}, "AccessIndexCache.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [415], "start_line": 407}, "AccessIndexCache.invalidate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [417], "start_line": 417}, "AccessIndexCache.invalidate_tenant": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [419], "start_line": 419}, "AccessIndexCache.invalidate_global": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [421], "start_line": 421}, "RoleBindingRepository.list_user_ids": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [427], "start_line": 427}, "RoleBindingRepository.invalidate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [429], "start_line": 429}, "DecisionCache.get": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [435], "start_line": 435}, "DecisionCache.set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [437], "start_line": 437}, "DecisionCache.make_key": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [446], "start_line": 439}, "DecisionCache.bump_namespace": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [448], "start_line": 448}, "PolicyEventPublisher.publish_policy_update": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [454], "start_line": 454}, "": {"executed_lines": [7, 9, 10, 12, 19, 20, 23, 24, 25, 26, 29, 30, 33, 34, 35, 36, 37, 39, 49, 50, 53, 54, 55, 56, 57, 58, 59, 60, 61, 63, 81, 82, 85, 86, 89, 90, 93, 94, 95, 96, 97, 98, 101, 102, 105, 106, 107, 108, 109, 110, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 130, 131, 134, 135, 136, 137, 138, 139, 140, 143, 144, 147, 148, 149, 150, 151, 152, 153, 155, 156, 168, 180, 217, 232, 240, 277, 305, 315, 332, 365, 385, 386, 389, 390, 391, 392, 393, 394, 395, 396, 397, 400, 424, 432, 451], "summary": {"covered_lines": 110, "num_statements": 110, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 192, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [170, 172, 174, 176, 178, 179, 183, 184, 186, 188, 189, 190, 191, 192, 193, 194, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 209, 210, 211, 213, 215, 216, 219, 221, 222, 223, 224, 225, 226, 227, 228, 230, 231, 234, 236, 238, 239, 243, 244, 246, 247, 248, 249, 250, 251, 252, 253, 254, 256, 257, 258, 259, 260, 261, 262, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 275, 276, 280, 281, 282, 283, 284, 285, 286, 287, 288, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 303, 304, 307, 309, 311, 313, 314, 318, 319, 321, 322, 323, 324, 325, 326, 327, 328, 330, 331, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 346, 347, 348, 350, 352, 353, 354, 355, 356, 357, 358, 359, 361, 363, 364, 368, 369, 371, 372, 373, 374, 375, 376, 377, 378, 379, 381, 383, 384, 403, 404, 406, 407, 408, 409, 410, 411, 412, 413, 414, 416, 418, 420, 422, 423, 426, 428, 430, 431, 434, 436, 438, 439, 440, 441, 442, 443, 444, 445, 447, 449, 450, 453], "start_line": 1}}, "classes": {"TenantRecord": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20}, "RelationshipRecord": {"executed_lines": [40], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 30}, "ACLRecord": {"executed_lines": [64], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 50}, "PolicyRecord": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 82}, "PolicyMutationResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 90}, "PolicyListItem": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 102}, "AuditListItem": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 114}, "AuthModelRecord": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 131}, "CachedDecision": {"executed_lines": [157], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 144}, "TenantRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [171, 173, 175, 177], "start_line": 168}, "PolicyRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 6, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [185, 187, 195, 208, 212, 214], "start_line": 180}, "AuthModelRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [220, 229], "start_line": 217}, "UserRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [235, 237], "start_line": 232}, "RelationshipRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [245, 255, 263, 274], "start_line": 240}, "AuditRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [289, 302], "start_line": 277}, "PolicyCache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [308, 310, 312], "start_line": 305}, "RelationshipCache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [320, 329], "start_line": 315}, "ACLRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [345, 349, 351, 360, 362], "start_line": 332}, "ACLCache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 3, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [370, 380, 382], "start_line": 365}, "AccessIndexEntry": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 386}, "AccessIndexCache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 5, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [405, 415, 417, 419, 421], "start_line": 400}, "RoleBindingRepository": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [427, 429], "start_line": 424}, "DecisionCache": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 4, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [435, 437, 446, 448], "start_line": 432}, "PolicyEventPublisher": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 1, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [454], "start_line": 451}, "": {"executed_lines": [7, 9, 10, 12, 19, 20, 23, 24, 25, 26, 29, 30, 33, 34, 35, 36, 37, 39, 49, 50, 53, 54, 55, 56, 57, 58, 59, 60, 61, 63, 81, 82, 85, 86, 89, 90, 93, 94, 95, 96, 97, 98, 101, 102, 105, 106, 107, 108, 109, 110, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 130, 131, 134, 135, 136, 137, 138, 139, 140, 143, 144, 147, 148, 149, 150, 151, 152, 153, 155, 156, 168, 180, 217, 232, 240, 277, 305, 315, 332, 365, 385, 386, 389, 390, 391, 392, 393, 394, 395, 396, 397, 400, 424, 432, 451], "summary": {"covered_lines": 110, "num_statements": 110, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 192, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [170, 172, 174, 176, 178, 179, 183, 184, 186, 188, 189, 190, 191, 192, 193, 194, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 209, 210, 211, 213, 215, 216, 219, 221, 222, 223, 224, 225, 226, 227, 228, 230, 231, 234, 236, 238, 239, 243, 244, 246, 247, 248, 249, 250, 251, 252, 253, 254, 256, 257, 258, 259, 260, 261, 262, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 275, 276, 280, 281, 282, 283, 284, 285, 286, 287, 288, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 303, 304, 307, 309, 311, 313, 314, 318, 319, 321, 322, 323, 324, 325, 326, 327, 328, 330, 331, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 346, 347, 348, 350, 352, 353, 354, 355, 356, 357, 358, 359, 361, 363, 364, 368, 369, 371, 372, 373, 374, 375, 376, 377, 378, 379, 381, 383, 384, 403, 404, 406, 407, 408, 409, 410, 411, 412, 413, 414, 416, 418, 420, 422, 423, 426, 428, 430, 431, 434, 436, 438, 439, 440, 441, 442, 443, 444, 445, 447, 449, 450, 453], "start_line": 1}}}, "keynetra/services/policies.py": {"executed_lines": [3, 5, 6, 7, 8, 16, 19, 22, 31, 32, 33, 34, 35, 36, 38, 39, 40, 41, 42, 43, 44, 45, 47, 54, 55, 58, 59, 60, 61, 62, 63, 65, 77, 78, 79, 89, 90, 99, 100, 101, 102, 103, 104, 107, 111, 113, 114, 115, 118, 119, 120, 121, 122, 123, 126, 130, 132, 133, 134, 135, 136, 137, 138, 139, 140, 143, 148, 149, 150, 164], "summary": {"covered_lines": 70, "num_statements": 70, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"PolicyService.__init__": {"executed_lines": [31, 32, 33, 34, 35, 36], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 22}, "PolicyService.list_policies": {"executed_lines": [39, 40, 41, 42, 43, 44, 45], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 38}, "PolicyService.list_policies_page": {"executed_lines": [54, 55, 58, 59, 60, 61, 62, 63], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 47}, "PolicyService.create_policy": {"executed_lines": [77, 78, 79, 89, 90, 99, 100, 101, 102, 103, 104, 107, 111], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 65}, "PolicyService.rollback_policy": {"executed_lines": [114, 115, 118, 119, 120, 121, 122, 123, 126, 130], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 113}, "PolicyService.delete_policy": {"executed_lines": [133, 134, 135, 136, 137, 138, 139, 140, 143], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 132}, "PolicyService._compile_and_store": {"executed_lines": [149, 150, 164], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 148}, "": {"executed_lines": [3, 5, 6, 7, 8, 16, 19, 22, 38, 47, 65, 113, 132, 148], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"PolicyService": {"executed_lines": [31, 32, 33, 34, 35, 36, 39, 40, 41, 42, 43, 44, 45, 54, 55, 58, 59, 60, 61, 62, 63, 77, 78, 79, 89, 90, 99, 100, 101, 102, 103, 104, 107, 111, 114, 115, 118, 119, 120, 121, 122, 123, 126, 130, 133, 134, 135, 136, 137, 138, 139, 140, 143, 149, 150, 164], "summary": {"covered_lines": 56, "num_statements": 56, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "": {"executed_lines": [3, 5, 6, 7, 8, 16, 19, 22, 38, 47, 65, 113, 132, 148], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/policy_dsl.py": {"executed_lines": [1, 3, 4, 6, 7, 12, 23, 28, 29, 31, 32, 33, 34, 35, 36, 40, 43, 44, 47, 48, 50, 53, 54, 56], "summary": {"covered_lines": 24, "num_statements": 29, "percent_covered": 82.75862068965517, "percent_covered_display": "83", "missing_lines": 5, "excluded_lines": 2, "percent_statements_covered": 82.75862068965517, "percent_statements_covered_display": "83"}, "missing_lines": [38, 41, 45, 49, 51], "excluded_lines": [8, 9], "functions": {"dsl_to_policy": {"executed_lines": [23, 28, 29, 31, 32, 33, 34, 35, 36, 40, 43, 44, 47, 48, 50, 53, 54, 56], "summary": {"covered_lines": 18, "num_statements": 23, "percent_covered": 78.26086956521739, "percent_covered_display": "78", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 78.26086956521739, "percent_statements_covered_display": "78"}, "missing_lines": [38, 41, 45, 49, 51], "excluded_lines": [], "start_line": 12}, "": {"executed_lines": [1, 3, 4, 6, 7, 12], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [8, 9], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 3, 4, 6, 7, 12, 23, 28, 29, 31, 32, 33, 34, 35, 36, 40, 43, 44, 47, 48, 50, 53, 54, 56], "summary": {"covered_lines": 24, "num_statements": 29, "percent_covered": 82.75862068965517, "percent_covered_display": "83", "missing_lines": 5, "excluded_lines": 2, "percent_statements_covered": 82.75862068965517, "percent_statements_covered_display": "83"}, "missing_lines": [38, 41, 45, 49, 51], "excluded_lines": [8, 9], "start_line": 1}}}, "keynetra/services/policy_lint.py": {"executed_lines": [3, 5, 6, 7, 9, 10, 12, 13, 16, 17, 18, 21, 24, 25, 26, 28, 29, 30, 31, 33, 34, 35, 37, 38, 39, 40, 42, 48, 49, 50, 51, 52, 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 75], "summary": {"covered_lines": 45, "num_statements": 48, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 93.75, "percent_statements_covered_display": "94"}, "missing_lines": [66, 67, 71], "excluded_lines": [], "functions": {"PolicyLintService.__init__": {"executed_lines": [25, 26], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 24}, "PolicyLintService.lint": {"executed_lines": [29, 30, 31, 33, 34, 35], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 28}, "PolicyLintService._serialize_conditions": {"executed_lines": [39, 40], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 38}, "PolicyLintService._collect_unused_role_warnings": {"executed_lines": [48, 49, 50, 51, 52, 53, 54], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "PolicyLintService._collect_duplicate_warnings": {"executed_lines": [57, 58, 59, 60, 61, 62, 63, 64, 65, 75], "summary": {"covered_lines": 10, "num_statements": 13, "percent_covered": 76.92307692307692, "percent_covered_display": "77", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 76.92307692307692, "percent_statements_covered_display": "77"}, "missing_lines": [66, 67, 71], "excluded_lines": [], "start_line": 56}, "": {"executed_lines": [3, 5, 6, 7, 9, 10, 12, 13, 16, 17, 18, 21, 24, 28, 37, 38, 42, 56], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"PolicyLintWarning": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 17}, "PolicyLintService": {"executed_lines": [25, 26, 29, 30, 31, 33, 34, 35, 39, 40, 48, 49, 50, 51, 52, 53, 54, 57, 58, 59, 60, 61, 62, 63, 64, 65, 75], "summary": {"covered_lines": 27, "num_statements": 30, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 90.0, "percent_statements_covered_display": "90"}, "missing_lines": [66, 67, 71], "excluded_lines": [], "start_line": 21}, "": {"executed_lines": [3, 5, 6, 7, 9, 10, 12, 13, 16, 17, 18, 21, 24, 28, 37, 38, 42, 56], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/policy_simulator.py": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 14, 15, 16, 17, 20, 21, 28, 29, 30, 32, 42, 43, 50, 60, 61, 62, 74, 75], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"PolicySimulator.__init__": {"executed_lines": [28, 29, 30], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 21}, "PolicySimulator.simulate_policy_change": {"executed_lines": [42, 43, 50, 60, 61, 62, 74, 75], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 32}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 14, 15, 16, 17, 20, 21, 32], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SimulationResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "PolicySimulator": {"executed_lines": [28, 29, 30, 42, 43, 50, 60, 61, 62, 74, 75], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 20}, "": {"executed_lines": [3, 5, 6, 8, 9, 10, 11, 14, 15, 16, 17, 20, 21, 32], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/policy_testing.py": {"executed_lines": [9, 11, 12, 13, 15, 16, 18, 19, 24, 25, 28, 29, 30, 33, 34, 37, 38, 41, 42, 45, 46, 47, 48, 49, 50, 51, 54, 57, 58, 61, 62, 63, 65, 68, 71, 72, 75, 78, 79, 80, 81, 82, 93, 96, 99, 100, 103, 104, 105, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 120, 121, 122, 124, 126, 127, 128, 130, 131, 133, 142, 143, 145, 146, 147, 148, 150, 152, 155, 156, 157, 158, 159, 161, 163, 165, 168, 180, 181, 182], "summary": {"covered_lines": 89, "num_statements": 107, "percent_covered": 83.17757009345794, "percent_covered_display": "83", "missing_lines": 18, "excluded_lines": 2, "percent_statements_covered": 83.17757009345794, "percent_statements_covered_display": "83"}, "missing_lines": [59, 64, 66, 106, 119, 123, 125, 129, 132, 144, 149, 151, 153, 160, 162, 164, 166, 183], "excluded_lines": [20, 21], "functions": {"parse_policy_test_suite": {"executed_lines": [57, 58, 61, 62, 63, 65, 68, 71, 72], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0, "percent_statements_covered": 75.0, "percent_statements_covered_display": "75"}, "missing_lines": [59, 64, 66], "excluded_lines": [], "start_line": 54}, "run_policy_test_suite": {"executed_lines": [78, 79, 80, 81, 82, 93], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 75}, "validate_policy_test_suite": {"executed_lines": [99, 100], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 96}, "_load_document": {"executed_lines": [104, 105], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [106], "excluded_lines": [], "start_line": 103}, "_parse_policy_entry": {"executed_lines": [110, 111, 112, 113, 114, 115, 116, 117, 118, 120, 121, 122, 124, 126, 127, 128, 130, 131, 133], "summary": {"covered_lines": 19, "num_statements": 24, "percent_covered": 79.16666666666667, "percent_covered_display": "79", "missing_lines": 5, "excluded_lines": 0, "percent_statements_covered": 79.16666666666667, "percent_statements_covered_display": "79"}, "missing_lines": [119, 123, 125, 129, 132], "excluded_lines": [], "start_line": 109}, "_parse_test_case": {"executed_lines": [143, 145, 146, 147, 148, 150, 152, 155, 156, 157, 158, 159, 161, 163, 165, 168], "summary": {"covered_lines": 16, "num_statements": 24, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 8, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [144, 149, 151, 153, 160, 162, 164, 166], "excluded_lines": [], "start_line": 142}, "_dump_document": {"executed_lines": [181, 182], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 66.66666666666667, "percent_statements_covered_display": "67"}, "missing_lines": [183], "excluded_lines": [], "start_line": 180}, "": {"executed_lines": [9, 11, 12, 13, 15, 16, 18, 19, 24, 25, 28, 29, 30, 33, 34, 37, 38, 41, 42, 45, 46, 47, 48, 49, 50, 51, 54, 75, 96, 103, 109, 142, 180], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 2, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [20, 21], "start_line": 1}}, "classes": {"PolicyTestCase": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "PolicyTestSuite": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "PolicyTestResult": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 42}, "": {"executed_lines": [9, 11, 12, 13, 15, 16, 18, 19, 24, 25, 28, 29, 30, 33, 34, 37, 38, 41, 42, 45, 46, 47, 48, 49, 50, 51, 54, 57, 58, 61, 62, 63, 65, 68, 71, 72, 75, 78, 79, 80, 81, 82, 93, 96, 99, 100, 103, 104, 105, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 120, 121, 122, 124, 126, 127, 128, 130, 131, 133, 142, 143, 145, 146, 147, 148, 150, 152, 155, 156, 157, 158, 159, 161, 163, 165, 168, 180, 181, 182], "summary": {"covered_lines": 89, "num_statements": 107, "percent_covered": 83.17757009345794, "percent_covered_display": "83", "missing_lines": 18, "excluded_lines": 2, "percent_statements_covered": 83.17757009345794, "percent_statements_covered_display": "83"}, "missing_lines": [59, 64, 66, 106, 119, 123, 125, 129, 132, 144, 149, 151, 153, 160, 162, 164, 166, 183], "excluded_lines": [20, 21], "start_line": 1}}}, "keynetra/services/relationships.py": {"executed_lines": [3, 5, 12, 15, 18, 27, 28, 29, 30, 31, 32, 34, 37, 38, 41, 42, 43, 48, 54, 56, 65, 66, 73, 75, 85, 86, 94, 97, 98, 99, 100, 101], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"RelationshipService.__init__": {"executed_lines": [27, 28, 29, 30, 31, 32], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 18}, "RelationshipService.list_relationships": {"executed_lines": [37, 38, 41, 42, 43, 48, 54], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 34}, "RelationshipService.list_relationships_page": {"executed_lines": [65, 66, 73], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 56}, "RelationshipService.create_relationship": {"executed_lines": [85, 86, 94, 97, 98, 99, 100, 101], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 75}, "": {"executed_lines": [3, 5, 12, 15, 18, 34, 56, 75], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RelationshipService": {"executed_lines": [27, 28, 29, 30, 31, 32, 37, 38, 41, 42, 43, 48, 54, 65, 66, 73, 85, 86, 94, 97, 98, 99, 100, 101], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "": {"executed_lines": [3, 5, 12, 15, 18, 34, 56, 75], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/resilience.py": {"executed_lines": [3, 5, 6, 7, 8, 9, 11, 13, 16, 17, 18, 19, 20, 21, 22, 25, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"with_timeout": {"executed_lines": [17, 18, 19, 20, 21, 22], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 16}, "retry": {"executed_lines": [28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "": {"executed_lines": [3, 5, 6, 7, 8, 9, 11, 13, 16, 25], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [3, 5, 6, 7, 8, 9, 11, 13, 16, 17, 18, 19, 20, 21, 22, 25, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/revisions.py": {"executed_lines": [3, 5, 6, 9, 12, 13, 15, 16, 17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"RevisionService.__init__": {"executed_lines": [13], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 12}, "RevisionService.get_revision": {"executed_lines": [16, 17], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 15}, "RevisionService.bump_revision": {"executed_lines": [20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 19}, "": {"executed_lines": [3, 5, 6, 9, 12, 15, 19], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"RevisionService": {"executed_lines": [13, 16, 17, 20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 9}, "": {"executed_lines": [3, 5, 6, 9, 12, 15, 19], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}, "keynetra/services/seeding.py": {"executed_lines": [3, 5, 7, 8, 10, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 35, 45, 46, 47, 49, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 64, 66, 67, 68, 71, 72, 73, 74, 75, 76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 88, 101, 102, 112, 114, 115, 125, 126, 137, 183, 193, 202, 203, 205, 206, 207, 208, 220], "summary": {"covered_lines": 71, "num_statements": 94, "percent_covered": 75.53191489361703, "percent_covered_display": "76", "missing_lines": 23, "excluded_lines": 0, "percent_statements_covered": 75.53191489361703, "percent_statements_covered_display": "76"}, "missing_lines": [50, 138, 139, 140, 142, 143, 144, 154, 155, 157, 158, 160, 161, 167, 168, 169, 170, 171, 172, 173, 178, 179, 180], "excluded_lines": [], "functions": {"seed_demo_data": {"executed_lines": [45, 46, 47, 49, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 64, 66, 67, 68, 71, 72, 73, 74, 75, 76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 88, 101, 102, 112, 114, 115, 125, 126], "summary": {"covered_lines": 42, "num_statements": 43, "percent_covered": 97.67441860465117, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0, "percent_statements_covered": 97.67441860465117, "percent_statements_covered_display": "98"}, "missing_lines": [50], "excluded_lines": [], "start_line": 35}, "_clear_sample_data": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 22, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 22, "excluded_lines": 0, "percent_statements_covered": 0.0, "percent_statements_covered_display": "0"}, "missing_lines": [138, 139, 140, 142, 143, 144, 154, 155, 157, 158, 160, 161, 167, 168, 169, 170, 171, 172, 173, 178, 179, 180], "excluded_lines": [], "start_line": 137}, "_ensure_policy": {"executed_lines": [193, 202, 203, 205, 206, 207, 208, 220], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 183}, "": {"executed_lines": [3, 5, 7, 8, 10, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 35, 137, 183], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"SeedSummary": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 25}, "": {"executed_lines": [3, 5, 7, 8, 10, 18, 19, 20, 21, 24, 25, 26, 27, 28, 29, 30, 31, 32, 35, 45, 46, 47, 49, 52, 53, 54, 55, 56, 57, 59, 60, 61, 62, 63, 64, 66, 67, 68, 71, 72, 73, 74, 75, 76, 77, 79, 80, 81, 82, 83, 84, 85, 86, 88, 101, 102, 112, 114, 115, 125, 126, 137, 183, 193, 202, 203, 205, 206, 207, 208, 220], "summary": {"covered_lines": 71, "num_statements": 94, "percent_covered": 75.53191489361703, "percent_covered_display": "76", "missing_lines": 23, "excluded_lines": 0, "percent_statements_covered": 75.53191489361703, "percent_statements_covered_display": "76"}, "missing_lines": [50, 138, 139, 140, 142, 143, 144, 154, 155, 157, 158, 160, 161, 167, 168, 169, 170, 171, 172, 173, 178, 179, 180], "excluded_lines": [], "start_line": 1}}}, "keynetra/version.py": {"executed_lines": [1, 2, 4], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2, 4], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}, "classes": {"": {"executed_lines": [1, 2, 4], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0, "percent_statements_covered": 100.0, "percent_statements_covered_display": "100"}, "missing_lines": [], "excluded_lines": [], "start_line": 1}}}}, "totals": {"covered_lines": 4791, "num_statements": 5613, "percent_covered": 85.35542490646714, "percent_covered_display": "85", "missing_lines": 822, "excluded_lines": 302, "percent_statements_covered": 85.35542490646714, "percent_statements_covered_display": "85"}} \ No newline at end of file diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..de027b2 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,27 @@ +# Deploy Assets + +Production-ready deployment manifests are generated in this directory. + +## Structure + +- `deploy/docker/`: container image and compose stack +- `deploy/kubernetes/`: raw Kubernetes manifests +- `deploy/helm/keynetra/`: installable Helm chart + +## Required Environment Variables + +- `KEYNETRA_DATABASE_URL`: SQLAlchemy DSN +- `KEYNETRA_REDIS_URL`: Redis URL for cache and invalidation +- `KEYNETRA_API_KEYS` or `KEYNETRA_API_KEY_HASHES` +- `KEYNETRA_API_KEY_SCOPES_JSON`: role/permission scopes for API keys +- `KEYNETRA_JWT_SECRET`: non-default outside development +- `KEYNETRA_STRICT_TENANCY=true`: required for strict multi-tenant behavior + +## Quick Commands + +- Docker compose: + - `docker compose -f deploy/docker/docker-compose.yml up --build` +- Kubernetes: + - `kubectl apply -f deploy/kubernetes/` +- Helm: + - `helm install keynetra ./deploy/helm/keynetra` diff --git a/deploy/docker/Dockerfile b/deploy/docker/Dockerfile new file mode 100644 index 0000000..186286e --- /dev/null +++ b/deploy/docker/Dockerfile @@ -0,0 +1,30 @@ +FROM python:3.11-slim + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /app + +RUN adduser --disabled-password --gecos "" --uid 10001 keynetra + +COPY requirements.lock requirements.lock +RUN pip install --upgrade pip && pip install -r requirements.lock + +COPY alembic.ini alembic.ini +COPY alembic alembic +COPY keynetra keynetra +COPY contracts contracts +COPY pyproject.toml pyproject.toml +COPY README.md README.md + +RUN pip install . && chown -R keynetra:keynetra /app + +USER keynetra + +EXPOSE 8000 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=3)" + +CMD ["keynetra", "serve", "--host", "0.0.0.0", "--port", "8000"] diff --git a/deploy/docker/README.md b/deploy/docker/README.md new file mode 100644 index 0000000..70017c5 --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,39 @@ +# Docker Deploy + +Docker assets: + +- `deploy/docker/Dockerfile` +- `deploy/docker/docker-compose.yml` + +## Local Docker Compose + +From repository root: + +```bash +docker compose -f deploy/docker/docker-compose.yml up --build +``` + +Default compose setup expects: + +- API on `http://localhost:8000` +- Postgres/Redis services from `deploy/docker/docker-compose.yml` + +## Minimal Environment Example + +```env +KEYNETRA_DATABASE_URL=postgresql+psycopg://postgres:postgres@db:5432/keynetra +KEYNETRA_REDIS_URL=redis://redis:6379/0 +KEYNETRA_API_KEYS=devkey +KEYNETRA_API_KEY_SCOPES_JSON={"devkey":{"tenant":"default","role":"admin","permissions":["*"]}} +KEYNETRA_JWT_SECRET=replace-with-strong-secret +KEYNETRA_ENVIRONMENT=prod +KEYNETRA_STRICT_TENANCY=true +``` + +## Scale API Replicas + +```bash +docker compose -f deploy/docker/docker-compose.yml up --scale keynetra=3 +``` + +Use a reverse proxy/load balancer in front of replicas for production traffic. diff --git a/deploy/docker/docker-compose.yml b/deploy/docker/docker-compose.yml new file mode 100644 index 0000000..92d88ff --- /dev/null +++ b/deploy/docker/docker-compose.yml @@ -0,0 +1,57 @@ +name: keynetra + +services: + keynetra: + build: + context: ../.. + dockerfile: deploy/docker/Dockerfile + restart: unless-stopped + ports: + - "8000:8000" + environment: + KEYNETRA_ENVIRONMENT: ${KEYNETRA_ENVIRONMENT:-development} + KEYNETRA_DATABASE_URL: ${KEYNETRA_DATABASE_URL:-postgresql+psycopg://keynetra:keynetra@postgres:5432/keynetra} + KEYNETRA_REDIS_URL: ${KEYNETRA_REDIS_URL:-redis://redis:6379/0} + KEYNETRA_API_KEYS: ${KEYNETRA_API_KEYS:-devkey} + KEYNETRA_JWT_SECRET: ${KEYNETRA_JWT_SECRET:-change-me} + KEYNETRA_STRICT_TENANCY: ${KEYNETRA_STRICT_TENANCY:-false} + KEYNETRA_AUTO_SEED_SAMPLE_DATA: ${KEYNETRA_AUTO_SEED_SAMPLE_DATA:-false} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=3)"] + interval: 30s + timeout: 5s + retries: 5 + start_period: 20s + + postgres: + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${KEYNETRA_POSTGRES_USER:-keynetra} + POSTGRES_PASSWORD: ${KEYNETRA_POSTGRES_PASSWORD:-keynetra} + POSTGRES_DB: ${KEYNETRA_POSTGRES_DB:-keynetra} + volumes: + - keynetra_postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] + interval: 10s + timeout: 5s + retries: 5 + + redis: + image: redis:7-alpine + restart: unless-stopped + command: ["redis-server", "--save", "", "--appendonly", "no"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + keynetra_postgres_data: diff --git a/deploy/helm/README.md b/deploy/helm/README.md new file mode 100644 index 0000000..dc594d3 --- /dev/null +++ b/deploy/helm/README.md @@ -0,0 +1,24 @@ +# Helm Deploy + +Chart path: + +- `deploy/helm/keynetra` + +## Install + +```bash +helm upgrade --install keynetra ./deploy/helm/keynetra +``` + +## Override Examples + +```bash +helm upgrade --install keynetra ./deploy/helm/keynetra \ + --set image.repository=ghcr.io/keynetra/keynetra \ + --set image.tag=v0.1.0 \ + --set env.KEYNETRA_DATABASE_URL=postgresql+psycopg://... \ + --set env.KEYNETRA_REDIS_URL=redis://... \ + --set env.KEYNETRA_STRICT_TENANCY=true \ + --set secretEnv.KEYNETRA_API_KEY_HASHES= \ + --set secretEnv.KEYNETRA_JWT_SECRET= +``` diff --git a/infra/k8s/helm/keynetra/Chart.yaml b/deploy/helm/keynetra/Chart.yaml similarity index 56% rename from infra/k8s/helm/keynetra/Chart.yaml rename to deploy/helm/keynetra/Chart.yaml index 80e28b1..cf4ddb6 100644 --- a/infra/k8s/helm/keynetra/Chart.yaml +++ b/deploy/helm/keynetra/Chart.yaml @@ -1,6 +1,6 @@ apiVersion: v2 name: keynetra -description: Helm chart for self-hosted KeyNetra OSS deployments +description: Helm chart for KeyNetra authorization control plane type: application version: 0.1.0 appVersion: "0.1.0" diff --git a/deploy/helm/keynetra/templates/_helpers.tpl b/deploy/helm/keynetra/templates/_helpers.tpl new file mode 100644 index 0000000..079df65 --- /dev/null +++ b/deploy/helm/keynetra/templates/_helpers.tpl @@ -0,0 +1,11 @@ +{{- define "keynetra.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- define "keynetra.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name (include "keynetra.name" .) | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} diff --git a/deploy/helm/keynetra/templates/configmap.yaml b/deploy/helm/keynetra/templates/configmap.yaml new file mode 100644 index 0000000..5c35512 --- /dev/null +++ b/deploy/helm/keynetra/templates/configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "keynetra.fullname" . }}-config +data: +{{- range $key, $value := .Values.env }} + {{ $key }}: {{ $value | quote }} +{{- end }} diff --git a/deploy/helm/keynetra/templates/deployment.yaml b/deploy/helm/keynetra/templates/deployment.yaml new file mode 100644 index 0000000..ed77436 --- /dev/null +++ b/deploy/helm/keynetra/templates/deployment.yaml @@ -0,0 +1,42 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "keynetra.fullname" . }} + labels: + app.kubernetes.io/name: {{ include "keynetra.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "keynetra.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "keynetra.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + spec: + containers: + - name: keynetra + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: ["keynetra", "serve", "--host", "0.0.0.0", "--port", "8000"] + ports: + - name: http + containerPort: 8000 + envFrom: + - configMapRef: + name: {{ include "keynetra.fullname" . }}-config + - secretRef: + name: {{ include "keynetra.fullname" . }}-secret + resources: +{{ toYaml .Values.resources | indent 12 }} + readinessProbe: + httpGet: + path: /health/ready + port: http + livenessProbe: + httpGet: + path: /health/live + port: http diff --git a/deploy/helm/keynetra/templates/hpa.yaml b/deploy/helm/keynetra/templates/hpa.yaml new file mode 100644 index 0000000..b285e0d --- /dev/null +++ b/deploy/helm/keynetra/templates/hpa.yaml @@ -0,0 +1,20 @@ +{{- if .Values.hpa.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "keynetra.fullname" . }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "keynetra.fullname" . }} + minReplicas: {{ .Values.hpa.minReplicas }} + maxReplicas: {{ .Values.hpa.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.hpa.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/deploy/helm/keynetra/templates/ingress.yaml b/deploy/helm/keynetra/templates/ingress.yaml new file mode 100644 index 0000000..39eb693 --- /dev/null +++ b/deploy/helm/keynetra/templates/ingress.yaml @@ -0,0 +1,19 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "keynetra.fullname" . }} +spec: + ingressClassName: {{ .Values.ingress.className }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: {{ .Values.ingress.path }} + pathType: Prefix + backend: + service: + name: {{ include "keynetra.fullname" . }} + port: + number: {{ .Values.service.port }} +{{- end }} diff --git a/deploy/helm/keynetra/templates/secret.yaml b/deploy/helm/keynetra/templates/secret.yaml new file mode 100644 index 0000000..fec2fed --- /dev/null +++ b/deploy/helm/keynetra/templates/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "keynetra.fullname" . }}-secret +type: Opaque +stringData: +{{- range $key, $value := .Values.secretEnv }} + {{ $key }}: {{ $value | quote }} +{{- end }} diff --git a/deploy/helm/keynetra/templates/service.yaml b/deploy/helm/keynetra/templates/service.yaml new file mode 100644 index 0000000..4d03573 --- /dev/null +++ b/deploy/helm/keynetra/templates/service.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "keynetra.fullname" . }} +spec: + type: {{ .Values.service.type }} + selector: + app.kubernetes.io/name: {{ include "keynetra.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http diff --git a/deploy/helm/keynetra/values.yaml b/deploy/helm/keynetra/values.yaml new file mode 100644 index 0000000..7877929 --- /dev/null +++ b/deploy/helm/keynetra/values.yaml @@ -0,0 +1,41 @@ +replicaCount: 2 + +image: + repository: ghcr.io/keynetra/keynetra + tag: v0.1.0 + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8000 + +resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 1000m + memory: 1Gi + +ingress: + enabled: false + className: nginx + host: keynetra.example.com + path: / + +hpa: + enabled: true + minReplicas: 2 + maxReplicas: 10 + targetCPUUtilizationPercentage: 70 + +env: + KEYNETRA_ENVIRONMENT: prod + KEYNETRA_STRICT_TENANCY: "true" + KEYNETRA_DATABASE_URL: postgresql+psycopg://keynetra:keynetra@postgres:5432/keynetra + KEYNETRA_REDIS_URL: redis://redis:6379/0 + +secretEnv: + KEYNETRA_API_KEY_HASHES: replace-with-sha256-hashes + KEYNETRA_API_KEY_SCOPES_JSON: '{"replace":{"tenant":"default","role":"admin","permissions":["*"]}}' + KEYNETRA_JWT_SECRET: replace-with-strong-secret diff --git a/deploy/kubernetes/README.md b/deploy/kubernetes/README.md new file mode 100644 index 0000000..d51c063 --- /dev/null +++ b/deploy/kubernetes/README.md @@ -0,0 +1,22 @@ +# Kubernetes Deploy + +Manifests are in `deploy/kubernetes/`: + +- `configmap.yaml` +- `secret.yaml` +- `deployment.yaml` +- `service.yaml` +- `horizontal-pod-autoscaler.yaml` +- `ingress.yaml` + +## Apply + +```bash +kubectl apply -f deploy/kubernetes/ +``` + +## Notes + +- Set secure values in `secret.yaml` before applying. +- `KEYNETRA_STRICT_TENANCY` defaults to `true` in this deployment. +- Probes are configured for `/health/live` and `/health/ready`. diff --git a/deploy/kubernetes/configmap.yaml b/deploy/kubernetes/configmap.yaml new file mode 100644 index 0000000..e492b9a --- /dev/null +++ b/deploy/kubernetes/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: keynetra-config +data: + KEYNETRA_ENVIRONMENT: "prod" + KEYNETRA_STRICT_TENANCY: "true" + KEYNETRA_DATABASE_URL: "postgresql+psycopg://keynetra:keynetra@postgres:5432/keynetra" + KEYNETRA_REDIS_URL: "redis://redis:6379/0" diff --git a/deploy/kubernetes/deployment.yaml b/deploy/kubernetes/deployment.yaml new file mode 100644 index 0000000..a1486f7 --- /dev/null +++ b/deploy/kubernetes/deployment.yaml @@ -0,0 +1,50 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keynetra + labels: + app: keynetra +spec: + replicas: 2 + selector: + matchLabels: + app: keynetra + template: + metadata: + labels: + app: keynetra + spec: + containers: + - name: keynetra + image: ghcr.io/keynetra/keynetra:v0.1.0 + imagePullPolicy: IfNotPresent + args: ["keynetra", "serve", "--host", "0.0.0.0", "--port", "8000"] + ports: + - containerPort: 8000 + name: http + envFrom: + - configMapRef: + name: keynetra-config + - secretRef: + name: keynetra-secret + resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "1000m" + memory: "1Gi" + readinessProbe: + httpGet: + path: /health/ready + port: http + initialDelaySeconds: 10 + periodSeconds: 10 + timeoutSeconds: 3 + livenessProbe: + httpGet: + path: /health/live + port: http + initialDelaySeconds: 20 + periodSeconds: 20 + timeoutSeconds: 3 diff --git a/deploy/kubernetes/hpa.yaml b/deploy/kubernetes/hpa.yaml new file mode 100644 index 0000000..a120e0e --- /dev/null +++ b/deploy/kubernetes/hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: keynetra +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: keynetra + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 diff --git a/deploy/kubernetes/ingress.yaml b/deploy/kubernetes/ingress.yaml new file mode 100644 index 0000000..d8d7820 --- /dev/null +++ b/deploy/kubernetes/ingress.yaml @@ -0,0 +1,18 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: keynetra + annotations: + kubernetes.io/ingress.class: nginx +spec: + rules: + - host: keynetra.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: keynetra + port: + number: 8000 diff --git a/deploy/kubernetes/secret.yaml b/deploy/kubernetes/secret.yaml new file mode 100644 index 0000000..a23c6dd --- /dev/null +++ b/deploy/kubernetes/secret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: keynetra-secret +type: Opaque +stringData: + KEYNETRA_API_KEY_HASHES: "replace-with-sha256-hashes" + KEYNETRA_API_KEY_SCOPES_JSON: '{"replace":{"tenant":"default","role":"admin","permissions":["*"]}}' + KEYNETRA_JWT_SECRET: "replace-with-strong-secret" diff --git a/deploy/kubernetes/service.yaml b/deploy/kubernetes/service.yaml new file mode 100644 index 0000000..d1a876e --- /dev/null +++ b/deploy/kubernetes/service.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Service +metadata: + name: keynetra + labels: + app: keynetra +spec: + selector: + app: keynetra + ports: + - name: http + port: 8000 + targetPort: http + type: ClusterIP diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index b262470..4d2c21e 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,7 +1,7 @@ name: keynetra-dev services: - keynetra: + keynetra-api: build: context: . dockerfile: Dockerfile @@ -10,12 +10,12 @@ services: command: > uvicorn keynetra.api.main:app --host 0.0.0.0 - --port 8000 + --port 8080 --reload --proxy-headers --forwarded-allow-ips "*" ports: - - "8000:8000" + - "8000:8080" volumes: - .:/app environment: @@ -32,6 +32,7 @@ services: KEYNETRA_RATE_LIMIT_PER_MINUTE: ${KEYNETRA_RATE_LIMIT_PER_MINUTE:-120} KEYNETRA_RATE_LIMIT_BURST: ${KEYNETRA_RATE_LIMIT_BURST:-120} KEYNETRA_RUN_MIGRATIONS: ${KEYNETRA_RUN_MIGRATIONS:-1} + KEYNETRA_SERVER_PORT: ${KEYNETRA_SERVER_PORT:-8080} depends_on: postgres: condition: service_healthy @@ -76,10 +77,10 @@ services: ports: - "9090:9090" volumes: - - ./infra/docker/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus_data:/prometheus depends_on: - keynetra: + keynetra-api: condition: service_started grafana: @@ -93,13 +94,30 @@ services: - "3000:3000" volumes: - grafana_data:/var/lib/grafana - - ./infra/docker/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - - ./infra/docker/monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro depends_on: prometheus: condition: service_started + node-exporter: + image: prom/node-exporter:v1.8.2 + restart: unless-stopped + ports: + - "9100:9100" + + loki: + image: grafana/loki:3.2.1 + restart: unless-stopped + command: -config.file=/etc/loki/loki-config.yml + ports: + - "3100:3100" + volumes: + - ./monitoring/loki/loki-config.yml:/etc/loki/loki-config.yml:ro + - loki_data:/loki + volumes: postgres_data: prometheus_data: grafana_data: + loki_data: diff --git a/docker-compose.yml b/docker-compose.yml index cbfb33b..e91b35e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,6 +16,7 @@ x-keynetra-common: &keynetra-common KEYNETRA_RATE_LIMIT_BURST: ${KEYNETRA_RATE_LIMIT_BURST:-120} KEYNETRA_RUN_MIGRATIONS: ${KEYNETRA_RUN_MIGRATIONS:-1} KEYNETRA_AUTO_SEED_SAMPLE_DATA: ${KEYNETRA_AUTO_SEED_SAMPLE_DATA:-1} + KEYNETRA_SERVER_PORT: ${KEYNETRA_SERVER_PORT:-8080} depends_on: postgres: condition: service_healthy @@ -23,13 +24,13 @@ x-keynetra-common: &keynetra-common condition: service_healthy services: - # Production/default API service. - keynetra: + keynetra-api: <<: *keynetra-common + command: ["keynetra", "serve", "--host", "0.0.0.0", "--port", "8080"] ports: - - "8000:8000" + - "8000:8080" healthcheck: - test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=3)"] + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8080/health/ready', timeout=3)"] interval: 30s timeout: 5s retries: 5 @@ -73,10 +74,10 @@ services: ports: - "9090:9090" volumes: - - ./infra/docker/monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - ./monitoring/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus_data:/prometheus depends_on: - keynetra: + keynetra-api: condition: service_started grafana: @@ -90,13 +91,30 @@ services: - "3000:3000" volumes: - grafana_data:/var/lib/grafana - - ./infra/docker/monitoring/grafana/provisioning:/etc/grafana/provisioning:ro - - ./infra/docker/monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro + - ./monitoring/grafana/provisioning:/etc/grafana/provisioning:ro + - ./monitoring/grafana/dashboards:/var/lib/grafana/dashboards:ro depends_on: prometheus: condition: service_started + node-exporter: + image: prom/node-exporter:v1.8.2 + restart: unless-stopped + ports: + - "9100:9100" + + loki: + image: grafana/loki:3.2.1 + restart: unless-stopped + command: -config.file=/etc/loki/loki-config.yml + ports: + - "3100:3100" + volumes: + - ./monitoring/loki/loki-config.yml:/etc/loki/loki-config.yml:ro + - loki_data:/loki + volumes: postgres_data: prometheus_data: grafana_data: + loki_data: diff --git a/docs/README.md b/docs/README.md index 9c361b0..aa3cc4d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,69 +1,12 @@ -# KeyNetra Documentation - -This documentation set is organized like an OSS project handbook: quick onboarding, architecture references, operations runbooks, and executable examples. - -## Recommended Reading Order - -1. [Project Overview](getting-started/overview.md) -2. [Installation](getting-started/installation.md) -3. [Quickstart](getting-started/quickstart.md) -4. [Example Files](examples/example-files.md) -5. [API Reference](reference/api-reference.md) - -## Documentation Map - -Getting Started: - -- [Overview](getting-started/overview.md) -- [Installation](getting-started/installation.md) -- [Quickstart](getting-started/quickstart.md) -- [Runtime Modes](getting-started/runtime-modes.md) - -Examples: - -- [Example Files](examples/example-files.md) -- [End-to-End API Flow](examples/end-to-end-api-flow.md) -- [CLI Workflows](examples/cli-workflows.md) -- [Policy Patterns](examples/policy-patterns.md) - -Core Concepts: - -- [Authorization Models](core-concepts/authorization-models.md) -- [Request Evaluation Lifecycle](core-concepts/request-evaluation-lifecycle.md) -- [Consistency and Revisions](core-concepts/consistency-and-revisions.md) - -Architecture: - -- [System Architecture](architecture/system-architecture.md) -- [Authorization Pipeline](architecture/authorization-pipeline.md) -- [Caching and Consistency](architecture/caching-and-consistency.md) -- [Data Models](architecture/data-models.md) - -Reference: - -- [API Reference](reference/api-reference.md) -- [CLI Reference](reference/cli-reference.md) -- [Configuration Files](reference/configuration-files.md) -- [Environment Variables](reference/environment-variables.md) -- [Policy File Formats](reference/policy-files.md) -- [Authorization Model Files](reference/auth-model-files.md) - -Operations: - -- [Docker Deployment](operations/deployment-docker.md) -- [Kubernetes Deployment](operations/deployment-kubernetes.md) -- [Observability](operations/observability.md) -- [Security](operations/security.md) -- [Troubleshooting](operations/troubleshooting.md) - -Development: - -- [Local Development](development/local-development.md) -- [Migrations](development/migrations.md) -- [Testing](development/testing.md) -- [CI/CD and Release](development/ci-cd-release.md) -- [Contributing](development/contributing.md) - -## Source of Truth - -When documentation and code diverge, use implementation in `keynetra/` and contracts in `contracts/openapi/` as source of truth. +# Documentation Index + +Primary project documentation: + +- [README](../README.md) +- [ARCHITECTURE](../ARCHITECTURE.md) +- [DEPLOYMENT](../DEPLOYMENT.md) +- [SDK Guide](../SDK_GUIDE.md) +- [CONTRIBUTING](../CONTRIBUTING.md) +- [SECURITY](../SECURITY.md) +- [CODE_OF_CONDUCT](../CODE_OF_CONDUCT.md) +- [CHANGELOG](../CHANGELOG.md) diff --git a/docs/api-endpoints.md b/docs/api-endpoints.md deleted file mode 100644 index afe29ee..0000000 --- a/docs/api-endpoints.md +++ /dev/null @@ -1,344 +0,0 @@ -# API Endpoints (Beginner Guide) - -All endpoints below are active in this repository and are the primary integration surface. - -Base URL: - -- `http://localhost:8000` - -Auth header: - -- `X-API-Key: ` - -Example setup: - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve -``` - ---- - -## POST /check-access - -Purpose: - -- Evaluate one authorization request and return allow/deny with explanation. - -Code path: - -- Route: `keynetra/api/routes/access.py::check_access` -- Service call: `AuthorizationService.authorize(...)` -- Engine call: `KeyNetraEngine.decide(...)` - -Request body: - -```json -{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance"}, - "consistency": "eventual", - "revision": null -} -``` - -Example request: - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance"} - }' | jq . -``` - -Example response: - -```json -{ - "data": { - "allowed": true, - "decision": "allow", - "matched_policies": ["rbac:permissions"], - "reason": "explicit permission grant", - "policy_id": "rbac:permissions", - "explain_trace": [], - "revision": 1 - }, - "meta": {"request_id": "...", "limit": null, "next_cursor": null, "extra": {}}, - "error": null -} -``` - -Common use cases: - -- Check access before serving a protected API -- Add audit trail context for allow/deny decisions -- Return explanation details to internal admin tools - ---- - -## POST /check-access-batch - -Purpose: - -- Evaluate multiple actions/resources for the same user in one call. - -Code path: - -- Route: `keynetra/api/routes/access.py::check_access_batch` -- Service call: `AuthorizationService.authorize_batch(...)` -- Engine call per item: `KeyNetraEngine.decide(...)` - -Request body: - -```json -{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "items": [ - {"action": "approve_payment", "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}}, - {"action": "delete", "resource": {"resource_type": "payment", "resource_id": "pay-900"}} - ], - "consistency": "eventual", - "revision": null -} -``` - -Example request: - -```bash -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "items": [ - {"action": "approve_payment", "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}}, - {"action": "delete", "resource": {"resource_type": "payment", "resource_id": "pay-900"}} - ] - }' | jq . -``` - -Example response: - -```json -{ - "data": { - "results": [ - {"action": "approve_payment", "allowed": true, "revision": 1}, - {"action": "delete", "allowed": false, "revision": 1} - ], - "revision": 1 - }, - "meta": {"request_id": "...", "limit": null, "next_cursor": null, "extra": {}}, - "error": null -} -``` - -Common use cases: - -- Render UI permissions for many buttons/tabs at once -- Reduce network calls from gateway/backend-for-frontend - ---- - -## POST /simulate - -Purpose: - -- Run a non-persisted decision with full trace and failed conditions. - -Code path: - -- Route: `keynetra/api/routes/access.py::simulate` -- Service call: `AuthorizationService.simulate(...)` -- Internally uses `authorize(...)` with standard evaluation pipeline - -Request body: - -- Same shape as `/check-access` - -Example request: - -```bash -curl -s -X POST http://localhost:8000/simulate \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "manager-1", "role": "manager"}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 120000}, - "context": {"department": "finance"} - }' | jq . -``` - -Example response: - -```json -{ - "data": { - "decision": "deny", - "matched_policies": [], - "reason": "default deny", - "policy_id": null, - "explain_trace": [], - "failed_conditions": ["max_amount"], - "revision": 1 - }, - "meta": {"request_id": "...", "limit": null, "next_cursor": null, "extra": {}}, - "error": null -} -``` - -Common use cases: - -- Debug policy behavior without changing state -- Build policy authoring tools with explainability - ---- - -## POST /simulate-policy - -Purpose: - -- Compare decision before and after a proposed policy change. - -Code path: - -- Route: `keynetra/api/routes/simulation.py::simulate_policy` -- Simulator: `PolicySimulator.simulate_policy_change(...)` -- DSL parser: `keynetra/services/policy_dsl.py::dsl_to_policy` - -Note: - -- Requires management role (`viewer` or higher). API key auth works as admin in this repo. - -Request body: - -```json -{ - "simulate": { - "policy_change": "allow:\n action: share_document\n priority: 1\n policy_key: share-admin\n when:\n role: admin" - }, - "request": { - "user": {"id": "root-admin", "role": "admin", "roles": ["admin"]}, - "action": "share_document", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } -} -``` - -Example request: - -```bash -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "simulate": { - "policy_change": "allow:\n action: share_document\n priority: 1\n policy_key: share-admin\n when:\n role: admin" - }, - "request": { - "user": {"id": "root-admin", "role": "admin", "roles": ["admin"]}, - "action": "share_document", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } - }' | jq . -``` - -Example response: - -```json -{ - "data": { - "decision_before": { - "allowed": false, - "decision": "deny", - "reason": "no matching policy", - "policy_id": null - }, - "decision_after": { - "allowed": true, - "decision": "allow", - "reason": "policy change grants access", - "policy_id": "share-admin" - } - }, - "meta": {"request_id": "...", "limit": null, "next_cursor": null, "extra": {}}, - "error": null -} -``` - -Common use cases: - -- Review policy change impact during PRs -- Safety-check production policy updates - ---- - -## POST /impact-analysis - -Purpose: - -- Estimate which users gain or lose access from a proposed policy change. - -Code path: - -- Route: `keynetra/api/routes/simulation.py::impact_analysis` -- Analyzer: `ImpactAnalyzer.analyze_policy_change(...)` -- Compares `before_engine` and `after_engine` decisions per user/resource candidate - -Request body: - -```json -{ - "policy_change": "deny:\n action: export_payment\n priority: 1\n policy_key: deny-export-contractors\n when:\n role: external" -} -``` - -Example request: - -```bash -curl -s -X POST http://localhost:8000/impact-analysis \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "policy_change": "deny:\n action: export_payment\n priority: 1\n policy_key: deny-export-contractors\n when:\n role: external" - }' | jq . -``` - -Example response: - -```json -{ - "data": { - "gained_access": [101, 204], - "lost_access": [302] - }, - "meta": {"request_id": "...", "limit": null, "next_cursor": null, "extra": {}}, - "error": null -} -``` - -Common use cases: - -- Change approvals for security/governance -- Alerting on high-impact policy changes - ---- - -## Errors to expect - -- `401 unauthorized`: missing/invalid API key or token -- `403 forbidden`: principal lacks required management role -- `422 validation_error`: payload format or values are invalid -- `429 too_many_requests`: rate limit exceeded -- `500 database_error`: storage issue diff --git a/docs/architecture.md b/docs/architecture.md deleted file mode 100644 index 67df62f..0000000 --- a/docs/architecture.md +++ /dev/null @@ -1,51 +0,0 @@ -# Architecture Guide - -This page explains how an authorization request flows through KeyNetra. - -## Core components - -- API layer: FastAPI routes in `keynetra/api/routes/` -- Service layer: orchestration in `keynetra/services/` -- Engine layer: policy evaluation in `keynetra/engine/` -- Data layer: repositories in `keynetra/infrastructure/repositories/` -- Cache layer: in-memory/Redis caches in `keynetra/infrastructure/cache/` -- Observability: metrics/logging/audit support - -## Request flow - -```text -Client request - -> API auth (API key or JWT) - -> Request validation - -> AuthorizationService.authorize(...) - -> Load policies / relationships / ACL / model graph - -> Evaluate decision (RBAC/ABAC/ACL/ReBAC) - -> Build explain_trace + reason + policy_id - -> Write audit / update metrics - -> Return response envelope -``` - -## Policy evaluation flow (simplified) - -1. Read request (`user`, `action`, `resource`, `context`) -2. Evaluate explicit allows/denies (policies and ACL where applicable) -3. Evaluate relationship-based grants (ReBAC model graph) -4. Apply priority and first-match logic -5. Return `allow` or `deny` -6. If no policy matches, deny by default - -## Consistency and revision tokens - -Responses include `revision` values. -Use revision tokens when you need stronger consistency between write and read operations. - -## Caching behavior - -KeyNetra uses cache adapters to reduce repeated policy and relationship lookups. -When policies or relationships change, namespaces/entries are invalidated. - -## Where to read next - -- [API Endpoints](api-endpoints.md) -- [Policy Guide](policies.md) -- [Best Practices](best-practices.md) diff --git a/docs/architecture/authorization-pipeline.md b/docs/architecture/authorization-pipeline.md deleted file mode 100644 index e609e69..0000000 --- a/docs/architecture/authorization-pipeline.md +++ /dev/null @@ -1,89 +0,0 @@ ---- -title: Authorization Pipeline ---- - -# Authorization Pipeline - -`KeyNetraEngine` evaluates authorization in deterministic order. - -Source of truth: - -- `keynetra/engine/keynetra_engine.py` - -## Evaluation Order - -1. Direct user permissions -2. ACL checks -3. RBAC role permissions -4. Relationship index checks -5. Schema permission graph checks -6. Compiled policy graph evaluation -7. Default deny - -This order is fixed by engine implementation and is important when multiple models can match the same request. - -## Input Contract - -Engine accepts an explicit `AuthorizationInput` object: - -- `user` -- `action` -- `resource` -- `context` -- hydrated ACL/relationship/index/model graph fields from service layer - -No hidden data fetch occurs inside the engine. - -The service layer pre-hydrates policy data, relationships, ACL data, and optional compiled model graphs before the engine runs. - -## Decision Output - -`AuthorizationDecision` includes: - -- `allowed` -- `decision` (`allow` or `deny`) -- `reason` -- `policy_id` -- `matched_policies` -- `failed_conditions` -- `explain_trace` - -`explain_trace` is designed for debugging and auditability of decision paths. - -## Service Responsibilities - -Service constructs full input and handles: - -- policy retrieval and compilation lookup -- relationship and ACL hydration -- decision caching -- revision-aware consistency -- audit writes - -Primary file: - -- `keynetra/services/authorization.py` - -## Example Decision Call - -```python -from keynetra.engine import KeyNetraEngine - -engine = KeyNetraEngine([ - {"action": "read", "effect": "allow", "priority": 10, "conditions": {"role": "admin"}} -]) - -decision = engine.check_access( - subject="user:123", - action="read", - resource="document:abc", - context={"role": "admin"}, -) -``` - -## Related Pages - -- [Data Models and Storage](data-models.md) -- [Caching and Consistency](caching-and-consistency.md) -- [Policy File Formats](../reference/policy-files.md) -- [Authorization Model Files](../reference/auth-model-files.md) diff --git a/docs/architecture/caching-and-consistency.md b/docs/architecture/caching-and-consistency.md deleted file mode 100644 index 7a66585..0000000 --- a/docs/architecture/caching-and-consistency.md +++ /dev/null @@ -1,63 +0,0 @@ ---- -title: Caching and Consistency ---- - -# Caching and Consistency - -KeyNetra uses layered caching with Redis backend and in-memory fallback. - -Caching is implemented per concern (policy, decision, ACL, relationship, and access index) to reduce latency while preserving deterministic decisions. - -## Cache Layers - -- Policy cache: `keynetra/infrastructure/cache/policy_cache.py` -- Relationship cache: `keynetra/infrastructure/cache/relationship_cache.py` -- Decision cache: `keynetra/infrastructure/cache/decision_cache.py` -- ACL cache: `keynetra/infrastructure/cache/acl_cache.py` -- Access index cache: `keynetra/infrastructure/cache/access_index_cache.py` - -Backend abstraction: - -- `keynetra/infrastructure/cache/backends.py` - -If Redis is unavailable, KeyNetra falls back to shared in-memory cache adapters in-process. - -## Invalidation Model - -- Tenant namespace bump for decision cache invalidation. -- Resource/subject scoped invalidation for ACL and relationship changes. -- Policy updates invalidate policy cache and publish distribution events. - -This keeps cache behavior predictable across policy and relationship mutations. - -## Policy Distribution - -Redis pub/sub channel is used for policy update fan-out: - -- Event publisher: `keynetra/infrastructure/cache/policy_distribution.py` -- Subscriber startup: `keynetra/api/main.py` (`_start_policy_subscriber`) -- Channel config: `KEYNETRA_POLICY_EVENTS_CHANNEL` - -## Consistency Controls - -Access requests support consistency modes and revisions: - -- eventual cached reads (default) -- fully consistent bypass behavior where configured in service -- revision-driven keying in decision cache - -Implementation references: - -- `keynetra/services/authorization.py` -- `keynetra/services/revisions.py` - -## Operational Notes - -- For horizontally scaled deployments, configure Redis to share cache and policy events. -- For local development, in-memory fallback works without Redis. - -## Related Pages - -- [Authorization Pipeline](authorization-pipeline.md) -- [Observability](../operations/observability.md) -- [Troubleshooting](../operations/troubleshooting.md) diff --git a/docs/architecture/data-models.md b/docs/architecture/data-models.md deleted file mode 100644 index 6949a15..0000000 --- a/docs/architecture/data-models.md +++ /dev/null @@ -1,59 +0,0 @@ ---- -title: Data Models and Storage ---- - -# Data Models and Storage - -KeyNetra persists state in relational tables with Alembic migration control. - -This page maps high-level authorization concepts to concrete database tables. - -## Core Tables - -Defined in `keynetra/domain/models/`: - -- `tenant.py`: `tenants` -- `rbac.py`: `users`, `roles`, `permissions`, `user_roles`, `role_permissions` -- `relationship.py`: `relationships` -- `acl.py`: `resource_acl` -- `policy_versioning.py`: `policies`, `policy_versions` -- `auth_model.py`: `auth_models` -- `audit.py`: `audit_logs` -- `idempotency.py`: `idempotency_records` - -## Concept to Table Mapping - -- Tenancy and revisions: `tenants` -- RBAC: `users`, `roles`, `permissions`, `user_roles`, `role_permissions` -- ReBAC edges: `relationships` -- ACL rules: `resource_acl` -- Policy history: `policies`, `policy_versions` -- Schema modeling: `auth_models` -- Decision audit: `audit_logs` -- Idempotent write replay: `idempotency_records` - -## Migration System - -- Alembic config: `alembic.ini` -- Runtime env: `alembic/env.py` -- Revisions: `alembic/versions/*.py` - -Current revision history includes baseline plus tenant policy versioning, relationships, ACL, auth model, audit explainability, and idempotency support. - -See [Migrations](../development/migrations.md) for execution details. - -## Repository Pattern - -Storage access is routed through repository implementations in: - -- `keynetra/infrastructure/repositories/` - -Services use protocol interfaces from: - -- `keynetra/services/interfaces.py` - -## Related Pages - -- [Migrations](../development/migrations.md) -- [API Reference](../reference/api-reference.md) -- [Authorization Pipeline](authorization-pipeline.md) diff --git a/docs/architecture/system-architecture.md b/docs/architecture/system-architecture.md deleted file mode 100644 index 82f1198..0000000 --- a/docs/architecture/system-architecture.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: System Architecture ---- - -# System Architecture - -KeyNetra follows a layered architecture with strict boundary control. - -## Layers - -Key principle: the engine layer remains pure and deterministic, while side effects stay in service/infrastructure layers. - -## Engine Layer - -- Location: `keynetra/engine/` -- Contains deterministic authorization logic. -- No DB, cache, HTTP, or external state access. - -Primary engine implementation: - -- `keynetra/engine/keynetra_engine.py` -- `keynetra/engine/compiled/` -- `keynetra/engine/model_graph/` - -## Service Layer - -- Location: `keynetra/services/` -- Orchestrates repositories, cache, revision consistency, and resilience. - -Main orchestrator: - -- `keynetra/services/authorization.py` - -## Infrastructure Layer - -- Location: `keynetra/infrastructure/` -- Owns cache backends, repositories, DB session handling, and transport adapters. - -Examples: - -- `keynetra/infrastructure/cache/` -- `keynetra/infrastructure/repositories/` -- `keynetra/infrastructure/storage/session.py` - -## API Layer - -- Location: `keynetra/api/` -- FastAPI routes and middleware only. -- Delegates decision logic to services. - -Entry point: - -- `keynetra/api/main.py` - -## Configuration Layer - -- Location: `keynetra/config/` -- Environment settings, security, tenancy, and file-based config loading. - -## Domain Layer - -- Location: `keynetra/domain/` -- SQLAlchemy data models and API schema contracts. - -## Request Lifecycle - -1. API receives request and authenticates principal. -2. Service hydrates tenant context and evaluation input. -3. Engine evaluates with deterministic decision order. -4. Service handles cache/audit/revision side effects. -5. API returns normalized response envelope. - -## Architecture Guardrails - -- `keynetra/` code does not depend on `infra/`. -- Route handlers avoid business logic and delegate to services. -- Engine evaluations use explicit inputs only, with no hidden lookups. - -## Related Pages - -- [Authorization Pipeline](authorization-pipeline.md) -- [Caching and Consistency](caching-and-consistency.md) -- [API Reference](../reference/api-reference.md) -- [Data Models and Storage](data-models.md) diff --git a/docs/best-practices.md b/docs/best-practices.md deleted file mode 100644 index 1e19c17..0000000 --- a/docs/best-practices.md +++ /dev/null @@ -1,52 +0,0 @@ -# Best Practices - -## 1) Deny by default - -Treat unmatched requests as deny. -Do not create broad fallback allow rules. - -## 2) Apply least privilege - -- Grant only required actions -- Prefer narrower resource scopes -- Review and remove stale grants regularly - -## 3) Use policy versioning discipline - -- Track policy changes in source control -- Require review for policy edits -- Use `policy_id` naming that reflects intent and version - -## 4) Keep tenant boundaries explicit - -- Include tenant checks in policies/attributes -- Prevent cross-tenant reads by default -- Test multi-tenant edge cases with batch checks - -## 5) Validate before deployment - -Always run both: - -- `/simulate-policy` for before/after behavior -- `/impact-analysis` for affected user scope - -## 6) Use explainability in production support - -Persist or log these fields from decisions: - -- `decision` -- `reason` -- `policy_id` -- `revision` -- `explain_trace` - -## 7) Keep ACL usage controlled - -Use ACL for explicit exceptions, not as the primary model for the whole system. - -## 8) Add policy tests for critical flows - -- Payment approvals -- Admin operations -- Cross-tenant access -- Data export operations diff --git a/docs/cli.md b/docs/cli.md deleted file mode 100644 index fb2feec..0000000 --- a/docs/cli.md +++ /dev/null @@ -1,70 +0,0 @@ -# CLI Guide - -KeyNetra CLI lets you run and validate authorization without UI. - -Main entry point: - -```bash -python -m keynetra.cli --help -``` - -## Start server - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve -``` - -## Load models - -Apply a model file to API: - -```bash -python -m keynetra.cli model apply ./path/to/auth-model.yaml --api-key devkey -``` - -Show current model: - -```bash -python -m keynetra.cli model show --api-key devkey -``` - -## Run access checks - -```bash -python -m keynetra.cli check \ - --api-key devkey \ - --user '{"id":"alice","role":"manager"}' \ - --action approve_payment \ - --resource '{"resource_type":"payment","resource_id":"pay-900","amount":5000}' \ - --context '{"department":"finance"}' -``` - -## Simulate policy changes - -```bash -python -m keynetra.cli simulate \ - --api-key devkey \ - --policy-change 'allow:\n action: share_document\n priority: 1\n policy_key: share-admin\n when:\n role: admin' \ - --user '{"id":"root-admin","role":"admin","roles":["admin"]}' \ - --action share_document \ - --resource '{"resource_type":"document","resource_id":"doc-1"}' -``` - -## Run impact analysis - -```bash -python -m keynetra.cli impact \ - --api-key devkey \ - --policy-change 'deny:\n action: export_payment\n priority: 1\n policy_key: deny-export-contractors\n when:\n role: external' -``` - -## Helpful developer commands - -```bash -python -m keynetra.cli test-policy ./path/to/policy_tests.yaml -python -m keynetra.cli compile-policies --path ./policies -python -m keynetra.cli explain --user alice --resource doc-1 --action read -python -m keynetra.cli doctor --service core -python -m keynetra.cli version -``` diff --git a/docs/configuration.md b/docs/configuration.md deleted file mode 100644 index 1d6b48b..0000000 --- a/docs/configuration.md +++ /dev/null @@ -1,79 +0,0 @@ -# Configuration Guide - -KeyNetra supports two practical configuration styles: - -1. Environment variables (fastest) -2. YAML/JSON/TOML config file passed to CLI with `--config` - -## Environment variable setup - -```bash -export KEYNETRA_API_KEYS=devkey -export KEYNETRA_DATABASE_URL=sqlite+pysqlite:///./keynetra.db -export KEYNETRA_REDIS_URL= -export KEYNETRA_POLICY_PATHS=./policies -export KEYNETRA_MODEL_PATHS=./models -python -m keynetra.cli serve -``` - -## YAML config file - -Example `keynetra.yaml`: - -```yaml -database: - url: sqlite+pysqlite:///./keynetra.db -redis: - url: null -policies: - path: ./policies -models: - path: ./models -server: - host: 0.0.0.0 - port: 8000 -seed_data: false -``` - -Run with config file: - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve --config ./keynetra.yaml -``` - -Note: - -- API keys are still configured via environment (`KEYNETRA_API_KEYS`). - -## JSON config file - -```json -{ - "database": {"url": "sqlite+pysqlite:///./keynetra.db"}, - "redis": {"url": null}, - "policy_paths": ["./policies"], - "model_paths": ["./models"], - "server": {"host": "0.0.0.0", "port": 8000}, - "seed_data": false -} -``` - -Run: - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve --config ./keynetra.json -``` - -## Most useful env vars - -- `KEYNETRA_API_KEYS` -- `KEYNETRA_DATABASE_URL` -- `KEYNETRA_REDIS_URL` -- `KEYNETRA_POLICY_PATHS` -- `KEYNETRA_MODEL_PATHS` -- `KEYNETRA_RATE_LIMIT_PER_MINUTE` -- `KEYNETRA_RATE_LIMIT_BURST` -- `KEYNETRA_SERVER_HOST` -- `KEYNETRA_SERVER_PORT` diff --git a/docs/core-concepts/authorization-models.md b/docs/core-concepts/authorization-models.md deleted file mode 100644 index 73de158..0000000 --- a/docs/core-concepts/authorization-models.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -title: Authorization Models ---- - -# Authorization Models - -KeyNetra supports multiple authorization models that can be composed in a single decision flow. - -## RBAC - -Role-Based Access Control is implemented through users, roles, permissions, and role-permission bindings. - -Related implementation: - -- `keynetra/domain/models/rbac.py` -- `keynetra/api/routes/roles.py` -- `keynetra/api/routes/permissions.py` - -## ACL - -Access Control Lists provide resource-scoped, subject-specific allow/deny entries. - -Related implementation: - -- `keynetra/domain/models/acl.py` -- `keynetra/api/routes/acl.py` - -## ReBAC - -Relationship-Based Access Control uses relationship edges between subjects and objects. - -Related implementation: - -- `keynetra/domain/models/relationship.py` -- `keynetra/api/routes/relationships.py` - -## Policy Graph Evaluation - -Policy rules are compiled and evaluated as part of the deterministic engine pipeline. - -Related implementation: - -- `keynetra/engine/compiled/decision_graph.py` -- `keynetra/services/policies.py` - -## Schema-Based Authorization Modeling - -Authorization models can be defined as schema files and compiled into permission graphs. - -Related implementation: - -- `keynetra/modeling/schema_parser.py` -- `keynetra/modeling/model_validator.py` -- `keynetra/modeling/permission_compiler.py` - -## Related Pages - -- [Authorization Pipeline](../architecture/authorization-pipeline.md) -- [Policy File Formats](../reference/policy-files.md) -- [Authorization Model Files](../reference/auth-model-files.md) - diff --git a/docs/core-concepts/consistency-and-revisions.md b/docs/core-concepts/consistency-and-revisions.md deleted file mode 100644 index af851f9..0000000 --- a/docs/core-concepts/consistency-and-revisions.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Consistency and Revisions ---- - -# Consistency and Revisions - -KeyNetra uses tenant revisions and cache namespace strategies to keep authorization decisions coherent during policy and relationship changes. - -## Consistency Modes - -Access requests can use different consistency behavior, including eventual cached reads and stricter consistency paths. - -Primary implementation: - -- `keynetra/services/authorization.py` - -## Revision Tracking - -Tenant revisions and policy versions are used to isolate stale decisions. - -Primary implementation: - -- `keynetra/services/revisions.py` -- `keynetra/domain/models/tenant.py` - -## Cache Namespace Bumping - -When policies, ACL entries, or relationships change, relevant cache namespaces are bumped and stale decision keys become invalid. - -Related caches: - -- policy cache -- relationship cache -- ACL cache -- access index cache -- decision cache - -## Distributed Invalidation - -In multi-instance deployments, policy invalidations are distributed through Redis Pub/Sub. - -Related implementation: - -- `keynetra/infrastructure/cache/policy_distribution.py` -- `keynetra/api/main.py` (`_start_policy_subscriber`) - -## Related Pages - -- [Caching and Consistency](../architecture/caching-and-consistency.md) -- [Observability](../operations/observability.md) -- [Troubleshooting](../operations/troubleshooting.md) - diff --git a/docs/core-concepts/request-evaluation-lifecycle.md b/docs/core-concepts/request-evaluation-lifecycle.md deleted file mode 100644 index 1d6c9c0..0000000 --- a/docs/core-concepts/request-evaluation-lifecycle.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Request Evaluation Lifecycle ---- - -# Request Evaluation Lifecycle - -This page explains what happens from request intake to final authorization decision. - -## 1) Request Intake - -An access request includes: - -- `user` -- `action` -- `resource` -- optional `context` - -Transport entry points: - -- `POST /check-access` -- `POST /check-access-batch` - -## 2) Service Hydration - -The authorization service resolves tenant state, policies, relationships, ACL data, and cached decision candidates. - -Key implementation: - -- `keynetra/services/authorization.py` - -## 3) Engine Evaluation - -The engine performs deterministic evaluation across direct permissions, ACL, RBAC, relationships, schema permissions, policy graph, and default deny. - -Key implementation: - -- `keynetra/engine/keynetra_engine.py` - -## 4) Decision Output - -The system returns: - -- decision (`allow` or `deny`) -- reason and optional policy ID -- explain trace entries for audit/debugging - -## 5) Side Effects - -After decision calculation, the service may: - -- write audit records -- update decision cache -- apply revision/consistency behavior - -## Related Pages - -- [Authorization Pipeline](../architecture/authorization-pipeline.md) -- [Caching and Consistency](../architecture/caching-and-consistency.md) -- [API Reference](../reference/api-reference.md) - diff --git a/docs/deep-dive/code-walkthrough.md b/docs/deep-dive/code-walkthrough.md deleted file mode 100644 index 32e09ac..0000000 --- a/docs/deep-dive/code-walkthrough.md +++ /dev/null @@ -1,130 +0,0 @@ -# Code Walkthrough (Line-by-Line Concepts) - -This guide explains key classes and methods with implementation context. - -## A) `keynetra/api/routes/access.py` - -### `check_access(...)` - -What it does: - -1. Accepts validated `AccessRequest` -2. Calls `AuthorizationService.authorize(...)` -3. Converts service output to API schema (`AccessDecisionResponse`) -4. Returns standardized success envelope - -Why this design: - -- Route layer is transport-focused (HTTP validation/serialization) -- Business logic stays in service/engine layers - -### `check_access_batch(...)` - -What it does: - -- Maps `BatchAccessRequest.items` into service input -- Returns per-item allow/deny results with revision - -### `simulate(...)` - -What it does: - -- Calls `service.simulate(...)` -- Returns diagnostic fields like `failed_conditions` - -## B) `keynetra/services/authorization.py` - -### `AuthorizationService.__init__(...)` - -Dependency injection of: - -- repositories (tenants, policies, users, relationships, audit, ACL, model) -- caches (policy, relationship, decision, ACL, access index) -- settings (timeouts, resilience mode, etc.) - -Benefit: - -- easy testing with fake repositories/caches -- clear boundary between domain logic and storage - -### `authorize(...)` - -Notable behavior: - -- Builds fallback input early for resilience path -- Uses decision cache unless `fully_consistent` -- Writes audit after decision -- Returns stable response even when backend fails (via fallback behavior) - -### `_build_authorization_input(...)` - -Adds optional data into `AuthorizationInput`: - -- `acl_entries` -- `access_index_entries` -- `permission_graph` - -This allows engine to evaluate multiple models in one run. - -## C) `keynetra/engine/keynetra_engine.py` - -### `AuthorizationInput` - -Everything required for deterministic decision is explicit in this object. -No hidden external calls happen in the engine. - -### `PolicyDefinition` - -Normalized policy object with: - -- `action` -- `effect` -- `conditions` -- `priority` -- `policy_id` - -### `KeyNetraEngine.decide(...)` - -Supports two call styles: - -- new style: pass `AuthorizationInput` -- legacy style: `decide(user, action, resource)` - -### `_decide_structured(...)` - -This is the decision pipeline and the most important method to understand. -It appends trace steps for each stage and exits on first decisive stage. - -## D) `keynetra/services/policy_simulator.py` - -### `simulate_policy_change(...)` - -- Computes before decision from current state -- Parses policy DSL (`dsl_to_policy`) -- Evaluates after decision in temporary engine -- Returns both decisions for direct comparison - -## E) `keynetra/services/impact_analysis.py` - -### `analyze_policy_change(...)` - -- Compares before/after engines across user-resource candidates -- Reports changed user sets: - - `gained_access` - - `lost_access` - -Interpretation tip: - -- Large changed sets mean high blast radius; review carefully. - -## F) `keynetra/cli.py` - -Commands to map with API features: - -- `check` -> `/check-access` -- `simulate` -> `/simulate-policy` -- `impact` -> `/impact-analysis` -- `test-policy` -> policy regression tests -- `compile-policies` -> policy compile/validation summary - -Use CLI for reproducible scripts and CI jobs. diff --git a/docs/deep-dive/developer-manual.md b/docs/deep-dive/developer-manual.md deleted file mode 100644 index bce915a..0000000 --- a/docs/deep-dive/developer-manual.md +++ /dev/null @@ -1,219 +0,0 @@ -# Developer Manual (Detailed) - -This manual explains how KeyNetra works from request entry to final decision. -It is intended for developers integrating KeyNetra into real services. - -## 1) Mental model - -At runtime, KeyNetra does this for every authorization check: - -1. Accept request from API or CLI -2. Authenticate principal (`X-API-Key` or JWT) -3. Build normalized `AuthorizationInput` -4. Enrich user/resource context (roles, permissions, relationships) -5. Evaluate decision using deterministic engine stages -6. Return decision envelope with reason and explain trace - -Core types: - -- `AuthorizationInput` in `keynetra/engine/keynetra_engine.py` -- `AuthorizationDecision` in `keynetra/engine/keynetra_engine.py` -- `AuthorizationResult` in `keynetra/services/authorization.py` - -## 2) API entry points and code path - -Main route handlers: - -- `POST /check-access` -> `keynetra/api/routes/access.py::check_access` -- `POST /check-access-batch` -> `keynetra/api/routes/access.py::check_access_batch` -- `POST /simulate` -> `keynetra/api/routes/access.py::simulate` -- `POST /simulate-policy` -> `keynetra/api/routes/simulation.py::simulate_policy` -- `POST /impact-analysis` -> `keynetra/api/routes/simulation.py::impact_analysis` - -Service construction: - -- `get_authorization_service()` wires repositories + caches in `access.py` -- `get_simulation_services()` wires simulator/analyzer in `simulation.py` - -## 3) AuthorizationService internals - -File: `keynetra/services/authorization.py` - -Primary methods: - -- `authorize(...)` -- `authorize_batch(...)` -- `simulate(...)` -- `get_revision(...)` - -### 3.1 `authorize(...)` flow - -`authorize()` does more than engine evaluation. It orchestrates: - -1. Input validation via `validate_user` and `validate_resource` -2. Tenant lookup via `TenantRepository` -3. User hydration (`_hydrate_user`) to include persisted roles/relationships -4. Decision cache lookup (unless `consistency=fully_consistent`) -5. Engine construction (`_build_engine`) using current policy version -6. Pure engine call: `engine.decide(authorization_input)` -7. Cache write, audit write, and metrics reporting -8. Resilience fallback if dependencies fail - -Why this matters: - -- API behavior is stable even when cache or storage temporarily fails -- Decisions remain explainable because fallback still returns structured traces - -### 3.2 `authorize_batch(...)` - -`authorize_batch()`: - -- Reuses tenant and engine setup once -- Evaluates items concurrently using `ThreadPoolExecutor` -- Preserves per-item allow/deny results with revision - -Use this when frontends need many permission checks in one request. - -### 3.3 `simulate(...)` - -`simulate()` calls `authorize()` and returns `decision` directly. - -Key difference from `/check-access`: - -- API response includes `failed_conditions` and trace details for diagnostics - -### 3.4 How input enrichment works - -`_hydrate_user(...)` adds: - -- `roles` -- `role_permissions` -- `relations` -- `direct_permissions` - -This enables mixed RBAC/ABAC/ReBAC decisions from one normalized input. - -## 4) Engine internals and stage ordering - -File: `keynetra/engine/keynetra_engine.py` - -`KeyNetraEngine._decide_structured(...)` evaluates in fixed order: - -1. Direct permissions (`rbac:permissions`) -2. ACL match -3. Role permissions (`rbac:role`) -4. Relationship index check (`relationship:index`) -5. Compiled authorization model graph (`permission_graph`) -6. Compiled policy graph (`policy_graph`) -7. Default deny - -This ordering is important: earlier matches can short-circuit later stages. - -### 4.1 Traceability - -Every stage appends an `ExplainTraceStep`. -Response traces are deterministic and include: - -- `step` -- `outcome` -- `detail` -- `policy_id` - -This is the core debugging feature for production support. - -### 4.2 Condition evaluation - -`ConditionEvaluator` implements handlers such as: - -- `handle_role` -- `handle_max_amount` -- `handle_owner_only` -- `handle_time_range` -- `handle_geo_match` -- `handle_has_relation` - -Unknown condition keys fail safely (`unknown condition: `). - -## 5) Simulation and impact analysis internals - -### 5.1 Policy simulation - -File: `keynetra/services/policy_simulator.py` - -`simulate_policy_change(...)`: - -1. Builds "before" decision via `AuthorizationService` -2. Parses proposed DSL with `dsl_to_policy` -3. Appends changed policy to current policy list -4. Builds temporary engine and computes "after" decision - -Output: `SimulationResult(decision_before, decision_after)` - -### 5.2 Impact analysis - -File: `keynetra/services/impact_analysis.py` - -`analyze_policy_change(...)`: - -1. Loads current policies -2. Builds `before_engine` and `after_engine` -3. Iterates users and candidate resources -4. Compares before/after decision for target action -5. Returns `gained_access` and `lost_access` - -Use this to estimate blast radius before deploying policy updates. - -## 6) Caching and consistency details - -Cache adapters used by service layer: - -- Policy cache -- Relationship cache -- Decision cache -- ACL/access index caches - -Consistency knobs in access APIs: - -- `consistency: eventual` (default; uses decision cache) -- `consistency: fully_consistent` (bypasses decision cache) -- optional `revision` token for stronger control - -## 7) Example: full request lifecycle - -Request: - -```json -{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance"} -} -``` - -Potential stage path: - -- Direct permission stage matches `approve_payment` -- Engine returns allow with `policy_id=rbac:permissions` -- Service wraps response + revision + request metadata - -## 8) Integration checklist - -Before integrating in production: - -1. Use `/check-access-batch` where N checks happen per request -2. Log `decision`, `reason`, `policy_id`, `revision` -3. Add policy simulation in CI review for policy changes -4. Add impact analysis for sensitive policy operations -5. Keep deny-by-default and least-privilege policies - -## 9) Source map (quick links) - -- API app bootstrap: `keynetra/api/main.py` -- Access routes: `keynetra/api/routes/access.py` -- Simulation routes: `keynetra/api/routes/simulation.py` -- Service orchestrator: `keynetra/services/authorization.py` -- Engine core: `keynetra/engine/keynetra_engine.py` -- Policy simulator: `keynetra/services/policy_simulator.py` -- Impact analysis: `keynetra/services/impact_analysis.py` -- CLI: `keynetra/cli.py` diff --git a/docs/deep-dive/integration-cookbook.md b/docs/deep-dive/integration-cookbook.md deleted file mode 100644 index 56698a0..0000000 --- a/docs/deep-dive/integration-cookbook.md +++ /dev/null @@ -1,118 +0,0 @@ -# Integration Cookbook (Practical) - -This page gives end-to-end integration patterns with copy-paste examples. - -## 1) Backend middleware pattern - -Use KeyNetra before protected handlers. - -Pseudo-flow: - -1. Build request payload from authenticated user + route context -2. Call `/check-access` -3. Deny with 403 when `allowed=false` -4. Log `reason` + `policy_id` for debugging - -Example payload: - -```json -{ - "user": {"id": "u-42", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance", "request_id": "req-123"} -} -``` - -## 2) Frontend permission matrix pattern - -When UI needs many permissions (buttons, tabs, actions), call one batch endpoint. - -Example: - -```bash -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "u-42", "role": "manager", "permissions": ["approve_payment"]}, - "items": [ - {"action": "approve_payment", "resource": {"resource_type": "payment", "resource_id": "pay-1", "amount": 500}}, - {"action": "approve_payment", "resource": {"resource_type": "payment", "resource_id": "pay-2", "amount": 500000}}, - {"action": "read", "resource": {"resource_type": "document", "resource_id": "doc-1"}} - ] - }' | jq . -``` - -## 3) Safe policy rollout pattern - -For policy PRs or release pipelines: - -1. Run `/simulate-policy` with representative cases -2. Run `/impact-analysis` -3. Require explicit approval for high-impact changes - -### Step A: simulate one critical flow - -```bash -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "simulate": { - "policy_change": "deny:\n action: approve_payment\n priority: 1\n policy_key: emergency-freeze\n when:\n department: finance" - }, - "request": { - "user": {"id": "u-42", "role": "manager", "department": "finance"}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 1000}, - "context": {} - } - }' | jq . -``` - -### Step B: analyze blast radius - -```bash -curl -s -X POST http://localhost:8000/impact-analysis \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "policy_change": "deny:\n action: approve_payment\n priority: 1\n policy_key: emergency-freeze\n when:\n department: finance" - }' | jq . -``` - -## 4) Incident-debug pattern - -If an expected allow becomes deny in production: - -1. Replay request through `/simulate` -2. Inspect `failed_conditions` -3. Inspect `explain_trace` -4. Confirm latest `revision` - -Example: - -```bash -curl -s -X POST http://localhost:8000/simulate \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "u-42", "role": "manager"}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 250000}, - "context": {"department": "finance"} - }' | jq . -``` - -## 5) Language-agnostic response contract - -Always parse these fields from responses: - -- `data.allowed` or `data.decision` -- `data.reason` -- `data.policy_id` -- `data.revision` -- `meta.request_id` - -These fields are enough for product behavior, logging, and support triage. diff --git a/docs/development/ci-cd-release.md b/docs/development/ci-cd-release.md deleted file mode 100644 index 73d0430..0000000 --- a/docs/development/ci-cd-release.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: CI/CD and Release ---- - -# CI/CD and Release - -GitHub Actions workflows: - -- `.github/workflows/ci.yml` -- `.github/workflows/release.yml` - -## CI Workflow - -Triggered on pushes and pull requests. - -Stages: - -1. Setup Python 3.11 -2. Install dependencies -3. Lint (`ruff`, `black --check`, `isort --check-only`) -4. Migration check (`python -m keynetra.cli migrate --confirm-destructive`) -5. Tests + coverage (`--cov-fail-under=80`) - -CI currently runs on Python 3.11. - -## Release Workflow - -Triggered on tags matching `v*`. - -Stages: - -1. Build package (`python -m build`) -2. Run tests with coverage -3. Upload artifacts (`.whl`, `.tar.gz`) -4. Publish GitHub release - -## Recommended Release Steps - -1. ensure version alignment (`pyproject.toml`, `keynetra/version.py`, OpenAPI info) -2. run lint, migrations, and full tests locally -3. confirm changelog and release notes -4. push release tag (`vX.Y.Z`) - -## Version and Contract Alignment - -Version `0.1.0` is currently represented in: - -- `pyproject.toml` -- `keynetra/version.py` -- `contracts/openapi/keynetra-v0.1.0.yaml` - -## Release Hygiene Checklist - -- tests pass locally and in CI -- OpenAPI contract synced with implemented routes -- migrations apply cleanly -- docs and examples updated -- changelog updated - -## Related Pages - -- [Testing Strategy](testing.md) -- [Contributing](contributing.md) -- [Migrations](migrations.md) diff --git a/docs/development/contributing.md b/docs/development/contributing.md deleted file mode 100644 index 09d65c2..0000000 --- a/docs/development/contributing.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Contributing ---- - -# Contributing - -Primary contribution guidance comes from: - -- `CONTRIBUTING.md` - -## Standards - -- Python 3.11 -- Black formatting -- Isort import order -- Ruff lint rules -- tests and coverage maintained -- architecture boundaries respected (`keynetra/` does not depend on `infra/`) - -## Documentation Expectations - -- update docs for behavior changes -- keep examples runnable and version-aligned -- maintain internal links across pages - -## Typical Workflow - -1. Create branch -2. Implement focused change -3. Add/update tests -4. Run lint + tests -5. Update docs/migrations as needed -6. Open PR - -## Useful Commands - -```bash -make lint -make test -make migrate -``` - -## Related Pages - -- [Local Development](local-development.md) -- [CI/CD and Release](ci-cd-release.md) -- [Testing Strategy](testing.md) diff --git a/docs/development/local-development.md b/docs/development/local-development.md deleted file mode 100644 index a9ef2ac..0000000 --- a/docs/development/local-development.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Local Development ---- - -# Local Development - -This page describes the recommended development workflow for contributors and maintainers. - -## Setup - -```bash -python3.11 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -r requirements-dev.txt -cp .env.example .env -``` - -Optional: run local services via Docker while running the app locally. - -## Core Commands - -From `Makefile`: - -- `make install` -- `make test` -- `make lint` -- `make format` -- `make migrate` -- `make run` - -## Run API - -```bash -make run -``` - -or - -```bash -uvicorn keynetra.api.main:app --reload -``` - -Or use CLI: - -```bash -python -m keynetra.cli serve --config ./keynetra.yaml -``` - -## Seed Sample Data - -```bash -python -m keynetra.cli seed-data --reset -``` - -## Developer-Facing Endpoints - -In development/local environment (`KEYNETRA_ENVIRONMENT=development`), sample endpoints are available: - -- `GET /dev/sample-data` -- `POST /dev/sample-data/seed` - -## Related Pages - -- [Testing Strategy](testing.md) -- [Migrations](migrations.md) -- [Contributing](contributing.md) diff --git a/docs/development/migrations.md b/docs/development/migrations.md deleted file mode 100644 index 029b81b..0000000 --- a/docs/development/migrations.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Migrations ---- - -# Migrations - -KeyNetra uses Alembic for schema migrations. - -All schema changes should be tracked with migration files under `alembic/versions/`. - -## Files - -- `alembic.ini` -- `alembic/env.py` -- `alembic/versions/*.py` -- `keynetra/migrations.py` (destructive migration detection utility) - -## Run Migrations - -```bash -python -m keynetra.cli migrate -``` - -If destructive revisions exist and are intentional: - -```bash -python -m keynetra.cli migrate --confirm-destructive -``` - -## Migration Safety - -`keynetra/migrations.py` detects unapplied destructive operations (drop table/column) and blocks execution unless explicitly confirmed. - -## Migration Coverage - -Revision files currently include schema for: - -- RBAC tables -- tenant and policy versioning -- relationships -- audit explainability fields -- idempotency records -- ACL entries -- authorization model revisions - -## Docker Migrations - -Container startup script runs migrations when: - -- `KEYNETRA_RUN_MIGRATIONS=1` - -Reference: - -- `infra/docker/start.sh` - -## Related Pages - -- [Data Models and Storage](../architecture/data-models.md) -- [Troubleshooting](../operations/troubleshooting.md) -- [CI/CD and Release](ci-cd-release.md) diff --git a/docs/development/testing.md b/docs/development/testing.md deleted file mode 100644 index 6793b12..0000000 --- a/docs/development/testing.md +++ /dev/null @@ -1,72 +0,0 @@ ---- -title: Testing Strategy ---- - -# Testing Strategy - -Test suite location: - -- `tests/` - -## Run Tests - -```bash -pytest -q -pytest -q --cov=keynetra --cov-fail-under=80 -``` - -For quick local iteration, run targeted test modules: - -```bash -pytest -q tests/test_engine.py -pytest -q tests/test_api.py -``` - -## Coverage Areas - -Current test modules validate: - -- engine behavior and explainability -- API contract and route behavior -- ACL operations -- relationship indexing -- compiled policies and policy simulation -- impact analysis -- auth model parsing/validation/compile flow -- revision consistency and caching behavior -- metrics endpoint output -- admin login flow -- migration safety utilities -- release hardening checks -- headless and CLI modes - -Representative files: - -- `tests/test_engine.py` -- `tests/test_api.py` -- `tests/test_api_contract.py` -- `tests/test_acl.py` -- `tests/test_auth_model.py` -- `tests/test_policy_simulation.py` -- `tests/test_impact_analysis.py` -- `tests/test_metrics_endpoint.py` -- `tests/test_services_caching.py` -- `tests/test_headless_modes.py` - -## Policy Test Suites - -Policy-specific deterministic testing via CLI: - -```bash -python -m keynetra.cli test-policy ./policy_tests.yaml -``` - -## CI Expectations - -CI validates lint, migration application, and coverage thresholds. Match those checks locally before opening a PR. - -## Related Pages - -- [CLI Reference](../reference/cli-reference.md) -- [CI/CD and Release](ci-cd-release.md) -- [Contributing](contributing.md) diff --git a/docs/examples/assets/auth-model.yaml b/docs/examples/assets/auth-model.yaml deleted file mode 100644 index b4681f6..0000000 --- a/docs/examples/assets/auth-model.yaml +++ /dev/null @@ -1,13 +0,0 @@ -model: - schema_version: 1 - type: document - relations: - owner: user - editor: - - user - viewer: - - user - permissions: - read: owner or editor or viewer - write: owner or editor - delete: owner diff --git a/docs/examples/assets/keynetra.yaml b/docs/examples/assets/keynetra.yaml deleted file mode 100644 index 7305b0b..0000000 --- a/docs/examples/assets/keynetra.yaml +++ /dev/null @@ -1,18 +0,0 @@ -database: - url: sqlite+pysqlite:///./keynetra.db - -redis: - url: redis://localhost:6379/0 - -policies: - paths: - - ./docs/examples/assets/policies - -models: - path: ./docs/examples/assets/auth-model.yaml - -seed_data: true - -server: - host: 0.0.0.0 - port: 8000 diff --git a/docs/examples/assets/policies/document_access.yaml b/docs/examples/assets/policies/document_access.yaml deleted file mode 100644 index dac5e3d..0000000 --- a/docs/examples/assets/policies/document_access.yaml +++ /dev/null @@ -1,34 +0,0 @@ -policies: - - action: read - effect: allow - priority: 10 - policy_id: document-read-admin - conditions: - role: admin - resource_type: document - - - action: read - effect: allow - priority: 20 - policy_id: document-read-editor - conditions: - relation: editor - resource_type: document - same_tenant: true - - - action: write - effect: allow - priority: 30 - policy_id: document-write-owner - conditions: - relation: owner - resource_type: document - owner_only: true - - - action: delete - effect: deny - priority: 40 - policy_id: document-delete-protected - conditions: - resource_type: document - resource_attr: { classification: legal_hold } diff --git a/docs/examples/assets/policies/finance_rules.json b/docs/examples/assets/policies/finance_rules.json deleted file mode 100644 index 49cf70b..0000000 --- a/docs/examples/assets/policies/finance_rules.json +++ /dev/null @@ -1,23 +0,0 @@ -[ - { - "action": "approve_payment", - "effect": "allow", - "priority": 35, - "policy_id": "payment-approve-manager", - "conditions": { - "role": "manager", - "department": "finance", - "max_amount": 10000 - } - }, - { - "action": "approve_payment", - "effect": "deny", - "priority": 95, - "policy_id": "payment-approve-high-risk", - "conditions": { - "department": "finance", - "risk_level": "high" - } - } -] diff --git a/docs/examples/assets/policies/ops_rules.polar b/docs/examples/assets/policies/ops_rules.polar deleted file mode 100644 index 6424bfe..0000000 --- a/docs/examples/assets/policies/ops_rules.polar +++ /dev/null @@ -1,5 +0,0 @@ -# Polar-like flat rules supported by KeyNetra loader -allow action=deploy priority=20 policy_id=ops-deploy-allow role=ops environment=staging -allow action=restart_service priority=30 policy_id=ops-restart-allow role=sre - -deny action=deploy priority=90 policy_id=ops-deploy-deny-prod role=contractor environment=production diff --git a/docs/examples/assets/policy_tests.yaml b/docs/examples/assets/policy_tests.yaml deleted file mode 100644 index 6a09c8a..0000000 --- a/docs/examples/assets/policy_tests.yaml +++ /dev/null @@ -1,42 +0,0 @@ -policies: - - action: read - effect: allow - priority: 10 - policy_id: document-read-admin - conditions: - role: admin - resource_type: document - - - action: read - effect: deny - priority: 80 - policy_id: document-read-legal-hold - conditions: - resource_type: document - classification: legal_hold - -tests: - - name: admin can read normal document - expect: allow - input: - user: - id: alice - role: admin - action: read - resource: - resource_type: document - resource_id: doc-1 - context: {} - - - name: legal hold document denied for admin - expect: deny - input: - user: - id: alice - role: admin - action: read - resource: - resource_type: document - resource_id: doc-2 - classification: legal_hold - context: {} diff --git a/docs/examples/cli-workflows.md b/docs/examples/cli-workflows.md deleted file mode 100644 index c80bb47..0000000 --- a/docs/examples/cli-workflows.md +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: CLI Workflows ---- - -# CLI Workflows - -This page provides operational CLI recipes for development and release workflows. - -## Local Bootstrap - -```bash -python -m keynetra.cli migrate -python -m keynetra.cli seed-data --reset -python -m keynetra.cli serve -``` - -## API Decision via CLI - -```bash -python -m keynetra.cli check \ - --api-key devkey \ - --user '{"id":"alice","role":"manager"}' \ - --action approve_payment \ - --resource '{"resource_type":"payment","resource_id":"pay-900","amount":5000}' -``` - -## Policy Validation Pipeline - -```bash -python -m keynetra.cli compile-policies --config docs/examples/assets/keynetra.yaml -python -m keynetra.cli test-policy docs/examples/assets/policy_tests.yaml -python -m keynetra.cli doctor --service core --config docs/examples/assets/keynetra.yaml -``` - -## Runtime Debug Flow - -```bash -python -m keynetra.cli explain \ - --user u1 \ - --resource doc-1 \ - --action read \ - --context '{"department":"finance"}' -``` - -## Performance Smoke Test - -```bash -python -m keynetra.cli benchmark \ - --url http://localhost:8000/check-access \ - --requests 200 \ - --concurrency 20 \ - --api-key devkey -``` - -## ACL Maintenance - -```bash -python -m keynetra.cli acl add \ - --subject-type user \ - --subject-id alice \ - --resource-type document \ - --resource-id doc-1 \ - --action read \ - --effect allow - -python -m keynetra.cli acl list --resource-type document --resource-id doc-1 -python -m keynetra.cli acl remove --acl-id 1 -``` - -## Related Pages - -- [CLI Reference](../reference/cli-reference.md) -- [Quickstart](../getting-started/quickstart.md) diff --git a/docs/examples/end-to-end-api-flow.md b/docs/examples/end-to-end-api-flow.md deleted file mode 100644 index c0133f4..0000000 --- a/docs/examples/end-to-end-api-flow.md +++ /dev/null @@ -1,97 +0,0 @@ ---- -title: End-to-End API Flow ---- - -# End-to-End API Flow - -This walkthrough covers a practical management-to-decision flow using HTTP APIs. - -For file-based bootstrapping, use [Example Files](example-files.md) in `docs/examples/assets/`. - -## Goal - -- Create a policy -- Validate access decision -- Simulate a policy change -- Review audit records - -## 1. Start KeyNetra - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve -``` - -## 2. Create Policy - -```bash -curl -s -X POST http://localhost:8000/policies \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "action": "read", - "effect": "allow", - "priority": 50, - "conditions": { - "policy_key": "allow-read-admin", - "role": "admin" - } - }' | jq . -``` - -## 3. Evaluate Access - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "u1", "role": "admin"}, - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - }' | jq . -``` - -You should see `data.allowed=true` when policy and payload conditions match. - -## 4. Simulate Deny Override - -```bash -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "simulate": { - "policy_change": "deny:\n action: read\n priority: 100\n policy_key: deny-read-admin-temp\n when:\n role: admin" - }, - "request": { - "user": {"id": "u1", "role": "admin"}, - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } - }' | jq . -``` - -Use this output to confirm behavior before persisting policy updates. - -## 5. Read Audit Trail - -```bash -curl -s "http://localhost:8000/audit?user_id=u1&resource_id=doc-1&limit=10" \ - -H "X-API-Key: devkey" | jq . -``` - -## 6. Cleanup Policy - -```bash -curl -s -X DELETE http://localhost:8000/policies/allow-read-admin \ - -H "X-API-Key: devkey" | jq . -``` - -## Related Pages - -- [API Reference](../reference/api-reference.md) -- [Policy File Formats](../reference/policy-files.md) -- [Policy Patterns](policy-patterns.md) diff --git a/docs/examples/example-files.md b/docs/examples/example-files.md deleted file mode 100644 index ef454c4..0000000 --- a/docs/examples/example-files.md +++ /dev/null @@ -1,143 +0,0 @@ ---- -title: Example Files ---- - -# Example Files - -All core examples are embedded directly here so you can copy/paste without browsing file paths. - -## Runtime Config Example - -```yaml -database: - url: sqlite+pysqlite:///./keynetra.db -redis: - url: redis://localhost:6379/0 -policies: - paths: - - ./docs/examples/assets/policies -models: - path: ./docs/examples/assets/auth-model.yaml -seed_data: true -server: - host: 0.0.0.0 - port: 8000 -``` - -## Authorization Model Example - -```yaml -model: - schema_version: 1 - type: document - relations: - owner: user - editor: - - user - viewer: - - user - permissions: - read: owner or editor or viewer - write: owner or editor - delete: owner -``` - -## Policy Examples - -YAML: - -```yaml -policies: - - action: read - effect: allow - priority: 10 - policy_id: document-read-admin - conditions: - role: admin - resource_type: document - - - action: delete - effect: deny - priority: 40 - policy_id: document-delete-protected - conditions: - resource_type: document - resource_attr: { classification: legal_hold } -``` - -JSON: - -```json -[ - { - "action": "approve_payment", - "effect": "allow", - "priority": 35, - "policy_id": "payment-approve-manager", - "conditions": { - "role": "manager", - "department": "finance", - "max_amount": 10000 - } - } -] -``` - -Polar-like: - -```text -allow action=deploy priority=20 policy_id=ops-deploy-allow role=ops environment=staging -deny action=deploy priority=90 policy_id=ops-deploy-deny-prod role=contractor environment=production -``` - -## Policy Test Suite Example - -```yaml -policies: - - action: read - effect: allow - priority: 10 - policy_id: document-read-admin - conditions: - role: admin - resource_type: document - -tests: - - name: admin can read normal document - expect: allow - input: - user: - id: alice - role: admin - action: read - resource: - resource_type: document - resource_id: doc-1 - context: {} -``` - -## Quick Validation Flow - -```bash -# server -python -m keynetra.cli serve --config docs/examples/assets/keynetra.yaml - -# compile and test -python -m keynetra.cli compile-policies --config docs/examples/assets/keynetra.yaml -python -m keynetra.cli test-policy docs/examples/assets/policy_tests.yaml - -# apply model -python -m keynetra.cli model apply docs/examples/assets/auth-model.yaml --api-key devkey -``` - -## Why Embedded Examples - -- Show supported file formats (`yaml`, `json`, `polar`). -- Give copy-paste examples directly inside docs. -- Keep docs and runnable assets aligned. - -## Related Pages - -- [Project Overview](../getting-started/overview.md) -- [Policy File Formats](../reference/policy-files.md) -- [Authorization Model Files](../reference/auth-model-files.md) diff --git a/docs/examples/policy-patterns.md b/docs/examples/policy-patterns.md deleted file mode 100644 index 57075de..0000000 --- a/docs/examples/policy-patterns.md +++ /dev/null @@ -1,84 +0,0 @@ ---- -title: Policy Patterns ---- - -# Policy Patterns - -These patterns are aligned with KeyNetra policy parsing and decision priority behavior. - -## Pattern 1: Explicit Admin Allow - -```yaml -policies: - - policy_id: allow-read-admin - action: read - effect: allow - priority: 20 - conditions: - role: admin -``` - -Use when a role should have stable baseline access. - -## Pattern 2: Deny Override for High-Risk Context - -```yaml -policies: - - policy_id: deny-export-external - action: export - effect: deny - priority: 100 - conditions: - role: external -``` - -Use high priority deny rules for risk boundaries. - -## Pattern 3: Amount Guardrail - -```yaml -policies: - - policy_id: allow-approve-manager-low-value - action: approve_payment - effect: allow - priority: 40 - conditions: - role: manager - max_amount: 10000 -``` - -Pair with request payload context such as `amount` to enforce transaction limits. - -## Pattern 4: Department Scope - -```yaml -policies: - - policy_id: allow-finance-read - action: read_payment - effect: allow - priority: 30 - conditions: - department: finance -``` - -Use contextual fields from `context` payload for scoped permissions. - -## Pattern 5: Progressive Rollout - -1. Create policy in low priority allow mode. -2. Run `simulate-policy` for representative users/resources. -3. Run `impact-analysis` to estimate changed decisions. -4. Increase priority after validation. - -## Validation Checklist - -- Every rule has an explicit `action`, `effect`, and `priority`. -- `policy_id` or `policy_key` is stable for rollback/audit. -- Condition keys match request schema fields. -- Run `compile-policies` and `test-policy` before deployment. - -## Related Pages - -- [Policy File Formats](../reference/policy-files.md) -- [CLI Workflows](cli-workflows.md) -- [End-to-End API Flow](end-to-end-api-flow.md) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md deleted file mode 100644 index 214c0ac..0000000 --- a/docs/getting-started/installation.md +++ /dev/null @@ -1,69 +0,0 @@ ---- -title: Installation ---- - -# Installation - -This page covers local and Docker-based installation paths for KeyNetra. - -## Prerequisites - -- Python 3.11 -- `pip` -- Optional for production/local parity: Docker + Docker Compose - -Implementation references: - -- `pyproject.toml` -- `requirements.txt` -- `requirements-dev.txt` -- `Dockerfile` - -## Local Python Setup - -```bash -python3.11 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -r requirements-dev.txt -cp .env.example .env -``` - -## Verify Installation - -```bash -python -m keynetra.cli version -python -m keynetra.cli help-cli -``` - -Expected behavior: - -- `version` prints the current package version (for example, `0.1.0`) -- `help-cli` prints the operational command reference - -## Optional Docker Setup - -```bash -docker compose up --build -``` - -Development compose: - -```bash -docker compose -f docker-compose.dev.yml up --build -``` - -## Verify Runtime - -After startup, run: - -```bash -curl -i http://localhost:8000/health/ready -``` - -You should receive an HTTP `200` response. - -## Next - -- [Quickstart](quickstart.md) -- [Configuration Files](../reference/configuration-files.md) -- [Environment Variables](../reference/environment-variables.md) diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md deleted file mode 100644 index c6b5cbc..0000000 --- a/docs/getting-started/overview.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Project Overview ---- - -# Project Overview - -KeyNetra is a Python authorization platform that combines a deterministic policy engine with API, CLI, and embedded usage modes. - -It is designed for self-hosted, headless-first deployments where policy evaluation must remain deterministic and auditable. - -## Repository Scope - -Primary implementation lives in: - -- `keynetra/engine`: pure authorization engine -- `keynetra/services`: orchestration layer for policy loading, cache, audit, and resilience -- `keynetra/api`: FastAPI transport and middleware -- `keynetra/infrastructure`: DB/cache repositories, logging, and metrics integrations -- `keynetra/domain`: SQLAlchemy models and Pydantic schemas -- `keynetra/config`: settings, security, tenancy, and config file loading -- `alembic/`: database migrations -- `infra/`: Docker and Kubernetes deployment assets -- `contracts/openapi/keynetra-v0.1.0.yaml`: OpenAPI contract -- `examples/`: config, policy, and model examples - -## Core Capabilities - -- RBAC: users, roles, permissions, and role-permission binding -- ACL: resource-level allow/deny entries -- ReBAC: relationship graph checks -- ABAC-style policies: compiled decision graph from policy definitions -- Authorization modeling: schema parser, validator, and permission compiler -- Policy simulation and impact analysis -- Revision and consistency controls -- Redis-backed distributed cache with in-memory fallback -- Prometheus metrics and structured logging - -## Usage Modes - -KeyNetra supports three primary operating modes: - -- HTTP API server mode -- CLI operational mode -- Embedded engine mode inside Python applications - -See [Runtime Modes](runtime-modes.md) for concrete examples. - -## Who This Is For - -- Platform/backend engineers embedding authorization in services -- DevOps/SRE operators deploying KeyNetra in Docker or Kubernetes -- Application teams integrating with management and decision APIs - -## Related Pages - -- [Runtime Modes](runtime-modes.md) -- [Example Files](../examples/example-files.md) -- [System Architecture](../architecture/system-architecture.md) -- [API Reference](../reference/api-reference.md) -- [Docker Deployment](../operations/deployment-docker.md) diff --git a/docs/getting-started/quickstart.md b/docs/getting-started/quickstart.md deleted file mode 100644 index da1157a..0000000 --- a/docs/getting-started/quickstart.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Quickstart ---- - -# Quickstart - -This guide validates a full local KeyNetra flow: install, run server, execute a decision request, and inspect results. - -## Prerequisites - -- Python 3.11+ -- `pip` -- `curl` -- Optional: `jq` for pretty JSON output - -## 1. Install Dependencies - -From repository root: - -```bash -python3.11 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -r requirements-dev.txt -``` - -## 2. Configure API Access Key - -```bash -export KEYNETRA_API_KEYS=devkey -``` - -Optional but useful for first run: - -```bash -export KEYNETRA_ENVIRONMENT=development -export KEYNETRA_AUTO_SEED_SAMPLE_DATA=true -``` - -## 3. Start the API Server - -```bash -python -m keynetra.cli serve --host 0.0.0.0 --port 8000 -``` - -Server entrypoint is `keynetra/api/main.py` and default URL is `http://localhost:8000`. - -## 4. Verify Health and Readiness - -```bash -curl -s http://localhost:8000/health | jq . -curl -s http://localhost:8000/health/ready | jq . -``` - -Expected status is `ok` for healthy local setup. - -## 5. Run Your First Access Decision - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance"} - }' | jq . -``` - -Key fields to review in the response: - -- `data.allowed`: final allow/deny boolean -- `data.decision`: normalized decision string -- `data.matched_policies`: rules that produced the outcome -- `request_id`: request correlation id from middleware - -## 6. Run a Batch Check - -```bash -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager"}, - "items": [ - { - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - }, - { - "action": "delete", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } - ] - }' | jq . -``` - -Use this endpoint when a single user needs multiple action checks in one network call. - -## 7. Simulate a Policy Change - -```bash -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "simulate": { - "policy_change": "allow:\n action: read\n priority: 10\n when:\n role: admin" - }, - "request": { - "user": {"id": "u1", "role": "admin"}, - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } - }' | jq . -``` - -This lets you validate policy behavior before persisting the change. - -## 8. Stop the Server - -Use `Ctrl+C` in the terminal running `serve`. - -## Next Steps - -- [Runtime Modes](runtime-modes.md) -- [API Reference](../reference/api-reference.md) -- [CLI Reference](../reference/cli-reference.md) -- [End-to-End API Example](../examples/end-to-end-api-flow.md) diff --git a/docs/getting-started/runtime-modes.md b/docs/getting-started/runtime-modes.md deleted file mode 100644 index ffaa769..0000000 --- a/docs/getting-started/runtime-modes.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Runtime Modes ---- - -# Runtime Modes - -KeyNetra can run in three modes. - -## 1) API server mode - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve -``` - -Use when other services call authorization over HTTP. - -## 2) CLI mode - -```bash -python -m keynetra.cli check \ - --api-key devkey \ - --user '{"id":"alice","role":"manager"}' \ - --action approve_payment \ - --resource '{"resource_type":"payment","resource_id":"pay-900","amount":5000}' -``` - -Use for local testing, scripts, and operations. - -## 3) Embedded Python mode - -```python -from keynetra import KeyNetra - -engine = KeyNetra.from_config("./keynetra.yaml") -decision = engine.check_access( - subject="user:alice", - action="read", - resource="document:doc-1", - context={} -) -print(decision.allowed) -``` - -Use when you want in-process authorization in Python applications. diff --git a/docs/models/README.md b/docs/models/README.md deleted file mode 100644 index 20e0805..0000000 --- a/docs/models/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Authorization Models - -If you are new to authorization, this is the quickest mental model: - -- RBAC answers: "What can this role do?" -- ABAC answers: "Do attributes satisfy policy conditions?" -- ACL answers: "Is this exact user/group explicitly allowed or denied on this resource?" -- ReBAC answers: "Does a relationship path grant access?" - -KeyNetra supports all four and can combine them in a single decision. - -## How to choose - -- Start with RBAC for coarse permissions -- Add ABAC for dynamic constraints (department, time, amount) -- Add ACL for exceptions on specific resources -- Add ReBAC for sharing/collaboration graphs (owner/editor/member) - -## Example model (document system) - -```yaml -model: - type: document - relations: - owner: user - editor: user - viewer: user - permissions: - read: owner or editor or viewer - write: owner or editor - delete: owner -``` - -## Read next - -- [RBAC](rbac.md) -- [ABAC](abac.md) -- [ACL](acl.md) -- [ReBAC](rebac.md) diff --git a/docs/models/abac.md b/docs/models/abac.md deleted file mode 100644 index 47b4edc..0000000 --- a/docs/models/abac.md +++ /dev/null @@ -1,37 +0,0 @@ -# ABAC (Attribute-Based Access Control) - -ABAC evaluates attributes from user, resource, and context. - -## Simple idea - -A request is allowed when conditions match attributes. - -Examples of attributes: - -- User: `department`, `employment_type` -- Resource: `owner_id`, `classification`, `amount` -- Context: `time`, `ip`, `region` - -## Example - -Policy condition concept: - -```yaml -conditions: - role: manager - max_amount: 100000 -``` - -Request resource: - -```json -{"amount": 45000} -``` - -Result: allowed for a manager under threshold. - -## When ABAC works well - -- Financial approvals -- Geo/time-based restrictions -- Department-scoped access diff --git a/docs/models/acl.md b/docs/models/acl.md deleted file mode 100644 index 294961a..0000000 --- a/docs/models/acl.md +++ /dev/null @@ -1,30 +0,0 @@ -# ACL (Access Control List) - -ACL stores explicit allow/deny entries per resource. - -## Simple idea - -You can override generic rules for one resource. - -Example entry: - -```json -{ - "subject_type": "user", - "subject_id": "charlie", - "resource_type": "document", - "resource_id": "doc-1", - "action": "share", - "effect": "deny" -} -``` - -## When ACL is useful - -- One-off exceptions -- Sensitive records requiring explicit grants/denies -- Temporary access overrides - -## Caution - -Avoid relying only on ACL for large systems. Combine with RBAC/ABAC. diff --git a/docs/models/rbac.md b/docs/models/rbac.md deleted file mode 100644 index d7f3930..0000000 --- a/docs/models/rbac.md +++ /dev/null @@ -1,35 +0,0 @@ -# RBAC (Role-Based Access Control) - -RBAC grants access based on roles assigned to users. - -## Simple idea - -- Users have roles (`admin`, `manager`, `viewer`) -- Roles map to allowed actions - -## Example - -User: - -```json -{"id": "alice", "role": "manager", "permissions": ["approve_payment"]} -``` - -Request: - -```json -{"action": "approve_payment"} -``` - -If the role/permissions include the action, decision is `allow`. - -## When RBAC works well - -- Standard SaaS dashboards -- Internal admin tooling -- Stable permission catalogs - -## Limitation - -RBAC alone cannot express dynamic constraints like "amount < 100000". -Use ABAC for that. diff --git a/docs/models/rebac.md b/docs/models/rebac.md deleted file mode 100644 index 1684ba1..0000000 --- a/docs/models/rebac.md +++ /dev/null @@ -1,30 +0,0 @@ -# ReBAC (Relationship-Based Access Control) - -ReBAC grants permissions from relationships between subjects and resources. - -## Simple idea - -If relationship exists, access may be allowed. - -Examples: - -- `user:alice` is `owner` of `document:doc-1` -- `user:bob` is `editor` of `document:doc-1` - -Model permission: - -```yaml -permissions: - read: owner or editor or viewer - write: owner or editor -``` - -## When ReBAC works well - -- Document sharing -- Team collaboration tools -- Hierarchical organizations and graph permissions - -## Benefit - -ReBAC keeps sharing logic out of application code and inside explicit relationship data. diff --git a/docs/operations/deployment-docker.md b/docs/operations/deployment-docker.md deleted file mode 100644 index c81bf9d..0000000 --- a/docs/operations/deployment-docker.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Docker Deployment ---- - -# Docker Deployment - -This page covers the Docker deployment assets shipped in this repository. - -Docker assets: - -- `Dockerfile` -- `docker-compose.yml` -- `docker-compose.dev.yml` -- `infra/docker/start.sh` - -## Default Stack - -```bash -docker compose up --build -``` - -Services: - -- `keynetra` API -- PostgreSQL -- Redis -- Prometheus -- Grafana - -Default exposed ports: - -- API: `8000` -- Postgres: `5432` -- Redis: `6379` -- Prometheus: `9090` -- Grafana: `3000` - -## Development Stack - -```bash -docker compose -f docker-compose.dev.yml up --build -``` - -Includes source mount and Uvicorn reload. - -Use this stack for iterative local development when you need auto-reload behavior. - -## Startup Behavior - -Container entrypoint script: - -1. Runs Alembic migrations if `KEYNETRA_RUN_MIGRATIONS=1` -2. Renders startup dashboard when enabled -3. Exports rich logging defaults -4. Starts Uvicorn workers - -Implementation: `infra/docker/start.sh` - -## Useful Environment Values - -- `KEYNETRA_DATABASE_URL` -- `KEYNETRA_REDIS_URL` -- `KEYNETRA_API_KEYS` -- `KEYNETRA_ADMIN_USERNAME` -- `KEYNETRA_ADMIN_PASSWORD` -- `KEYNETRA_UVICORN_WORKERS` -- `KEYNETRA_LOG_FORMAT=rich` -- `KEYNETRA_FORCE_COLOR=1` - -Example override: - -```bash -KEYNETRA_API_KEYS=devkey KEYNETRA_AUTO_SEED_SAMPLE_DATA=1 docker compose up --build -``` - -## Health Endpoints - -- `GET /health` -- `GET /health/live` -- `GET /health/ready` - -## Related Pages - -- [Observability](observability.md) -- [Troubleshooting](troubleshooting.md) -- [Configuration Files](../reference/configuration-files.md) diff --git a/docs/operations/deployment-kubernetes.md b/docs/operations/deployment-kubernetes.md deleted file mode 100644 index a14d3e2..0000000 --- a/docs/operations/deployment-kubernetes.md +++ /dev/null @@ -1,55 +0,0 @@ ---- -title: Kubernetes and Helm ---- - -# Kubernetes and Helm - -Kubernetes assets are under `infra/k8s/`. - -The included chart is intentionally minimal and should be extended for production environments. - -## Helm Chart - -Location: - -- `infra/k8s/helm/keynetra/` - -Key files: - -- `Chart.yaml` -- `values.yaml` -- `templates/deployment.yaml` - -`values.yaml` currently defines image repository/tag and service port. Deployment template provides baseline single-deployment rollout. - -## What To Extend Before Production - -- environment variables and secret references -- readiness/liveness probes -- resource limits/requests -- rolling update strategy -- ingress and TLS -- external database/redis service wiring - -## Terraform Directory - -`infra/k8s/terraform/README.md` documents intended scope: - -- self-hosted modules only -- no SaaS control-plane infrastructure in this repository - -## Production Considerations - -For production Kubernetes usage, extend chart values for: - -- environment variables and secrets -- liveness/readiness probes -- resource requests/limits -- ingress/network policy -- external Postgres and Redis connectivity - -## Related Pages - -- [Docker Deployment](deployment-docker.md) -- [Security](security.md) -- [Environment Variables](../reference/environment-variables.md) diff --git a/docs/operations/observability.md b/docs/operations/observability.md deleted file mode 100644 index 236660d..0000000 --- a/docs/operations/observability.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: Observability ---- - -# Observability - -KeyNetra includes first-party metrics and structured logging for operational visibility. - -Observability components: - -- Metrics definitions: `keynetra/observability/metrics.py` -- Metrics endpoint: `keynetra/api/routes/metrics.py` -- Logging config: `keynetra/infrastructure/logging.py` -- Request logging middleware: `keynetra/api/middleware/logging.py` - -## Metrics Endpoint - -`GET /metrics` returns Prometheus text format (`text/plain; version=0.0.4`). - -## Metric Families - -From implementation, key metrics include: - -- `keynetra_access_checks_total` -- `keynetra_acl_matches_total` -- `keynetra_policy_evaluations_total` -- `keynetra_relationship_traversals_total` -- `keynetra_policy_compilations_total` -- `keynetra_revision_updates_total` -- `keynetra_access_check_latency_seconds` -- `keynetra_decision_latency_seconds` -- `keynetra_cache_hits_total` -- `keynetra_cache_misses_total` -- `keynetra_cache_events_total` -- `keynetra_api_errors_total` - -These metrics cover authorization decisions, cache behavior, policy/model lifecycle, and API error rates. - -## Logging Modes - -- JSON logs by default -- Rich colored logs when `KEYNETRA_LOG_FORMAT=rich` - -Docker startup script sets rich mode by default. - -Use JSON mode for log aggregation pipelines and rich mode for local operator readability. - -## Prometheus and Grafana - -Compose stack includes monitoring: - -- Prometheus config: `infra/docker/monitoring/prometheus/prometheus.yml` -- Grafana provisioning: `infra/docker/monitoring/grafana/provisioning/` -- Dashboards: `infra/docker/monitoring/grafana/dashboards/` - -## Quick Validation - -```bash -curl -s http://localhost:8000/metrics | head -``` - -## Related Pages - -- [Docker Deployment](deployment-docker.md) -- [Troubleshooting](troubleshooting.md) -- [API Reference](../reference/api-reference.md) diff --git a/docs/operations/security.md b/docs/operations/security.md deleted file mode 100644 index 0585bd8..0000000 --- a/docs/operations/security.md +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: Security ---- - -# Security - -Security behavior is implemented across config, middleware, and route dependencies. - -This page documents the security mechanisms currently implemented in the repository. - -## Authentication Methods - -- API key header (`X-API-Key`) -- JWT bearer token -- Optional OIDC/JWKS token verification -- Admin login endpoint (`/admin/login`) issuing JWT - -Key implementation files: - -- `keynetra/config/security.py` -- `keynetra/config/admin_auth.py` -- `keynetra/api/routes/admin_auth.py` - -## Authorization for Management APIs - -Management endpoints enforce tenant role levels: - -- viewer -- developer -- admin - -Role checks are wired through `require_management_role(...)`. - -API keys are treated as admin-level principals for management paths by default behavior in current implementation. - -## Rate Limiting and Idempotency - -- Rate limiting middleware: `keynetra/config/rate_limit.py` -- Idempotency middleware: `keynetra/api/middleware/idempotency.py` -- Idempotency storage: `keynetra/domain/models/idempotency.py` - -## API Version and Request Tracking - -- Version negotiation: `X-API-Version` middleware -- Request IDs and structured request completion logs - -## Recommended Operational Baselines - -- rotate API keys and JWT secrets regularly -- use hashed API key mode (`KEYNETRA_API_KEY_HASHES`) in production -- avoid default admin credentials outside local development -- run behind TLS-terminating proxy or gateway - -## Disclosure Policy - -See repository policy: - -- `SECURITY.md` - -## Related Pages - -- [API Reference](../reference/api-reference.md) -- [Environment Variables](../reference/environment-variables.md) -- [Troubleshooting](troubleshooting.md) diff --git a/docs/operations/troubleshooting.md b/docs/operations/troubleshooting.md deleted file mode 100644 index 8a9c6ac..0000000 --- a/docs/operations/troubleshooting.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -title: Troubleshooting ---- - -# Troubleshooting - -Use this page for common local and container runtime issues. - -## Server Starts Then Exits in Docker - -Check: - -- `KEYNETRA_DATABASE_URL` connectivity -- migration failures in `infra/docker/start.sh` -- worker count and Uvicorn startup logs - -Commands: - -```bash -docker compose logs keynetra --tail=200 -docker compose ps -``` - -Also verify `KEYNETRA_UVICORN_WORKERS`; high values can fail in constrained environments. - -## No Colors in Logs - -Set: - -- `KEYNETRA_LOG_FORMAT=rich` -- `KEYNETRA_FORCE_COLOR=1` - -For Docker, confirm env values are set in compose service environment. - -If output is piped to a non-TTY, some terminals may suppress ANSI colors. - -## Startup Screen Not Visible - -Startup banner rendering is in `infra/docker/start.sh` and can be disabled with `KEYNETRA_STARTUP_SCREEN=0`. - -## Auth Failures - -Verify: - -- `KEYNETRA_API_KEYS` or `KEYNETRA_API_KEY_HASHES` -- JWT secret/algorithm match -- admin credentials (`KEYNETRA_ADMIN_USERNAME`, `KEYNETRA_ADMIN_PASSWORD`) - -For API-key authentication, ensure the header name is exactly `X-API-Key`. - -## Migration Failures - -Run manually: - -```bash -python -m keynetra.cli migrate --confirm-destructive -``` - -Review: - -- `alembic/env.py` -- `alembic/versions/` - -## Config File Not Applied - -Confirm command includes: - -```bash -python -m keynetra.cli serve --config ./keynetra.yaml -``` - -Supported file types are YAML/JSON/TOML only. - -If CLI still uses old values, verify no conflicting `KEYNETRA_*` variables are exported in your shell. - -## Metrics Endpoint Not Available - -Verify that service mode includes observability routes and check: - -```bash -curl -i http://localhost:8000/metrics -``` - -## Related Pages - -- [Docker Deployment](deployment-docker.md) -- [Configuration Files](../reference/configuration-files.md) -- [Observability](observability.md) diff --git a/docs/package-lock.json b/docs/package-lock.json deleted file mode 100644 index 6ab6825..0000000 --- a/docs/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "docs", - "lockfileVersion": 3, - "requires": true, - "packages": {} -} diff --git a/docs/policies.md b/docs/policies.md deleted file mode 100644 index 5ed605f..0000000 --- a/docs/policies.md +++ /dev/null @@ -1,73 +0,0 @@ -# Policy Guide - -This guide explains policy structure in plain language. - -## Policy structure - -Key fields you will use most: - -- `action`: what operation the policy targets -- `effect`: `allow` or `deny` -- `priority`: lower numbers are evaluated first -- `policy_id` (or key): identifier shown in decision responses -- `conditions`: attribute checks required for a match - -## Example - -```yaml -policies: - - action: approve_payment - effect: allow - priority: 10 - policy_id: finance-approve-manager-under-limit - conditions: - role: manager - max_amount: 100000 - - - action: approve_payment - effect: deny - priority: 20 - policy_id: finance-maker-checker-deny - conditions: - owner_only: true -``` - -## Allow and deny logic - -- Policies are checked by priority. -- First matching policy determines outcome. -- If nothing matches, system returns deny (safe default). - -## Priority rules - -- Smaller number = higher priority -- Use this to place explicit safety denies before broad allows - -Example: - -- Priority `1`: deny risky operation -- Priority `10`: allow common trusted flow - -## Conditions and attributes - -Conditions are matched against request data: - -- `user` attributes (`role`, `permissions`) -- `resource` attributes (`amount`, `owner_id`, `resource_type`) -- `context` attributes (`department`, `time`) - -## Practical tips - -- Keep policies small and focused -- Use clear `policy_id` names so traces are readable -- Prefer explicit denies for high-risk operations -- Validate changes with `/simulate-policy` before deployment -- Run `/impact-analysis` for high-blast-radius updates - -## Example workflow - -1. Draft policy in YAML -2. Run `python -m keynetra.cli test-policy ` -3. Run `/simulate-policy` with representative request -4. Run `/impact-analysis` to measure user impact -5. Deploy policy diff --git a/docs/quickstart.md b/docs/quickstart.md deleted file mode 100644 index ee1f6ff..0000000 --- a/docs/quickstart.md +++ /dev/null @@ -1,92 +0,0 @@ -# 5-Minute Quickstart - -This quickstart is designed for developers who have never used an authorization engine. - -## What you will do - -1. Start KeyNetra locally -2. Send one access request -3. Read the decision and reason - -## Prerequisites - -- Python 3.11 -- `curl` - -## 1) Install and activate environment - -```bash -python3.11 -m venv .venv -source .venv/bin/activate -pip install -r requirements.txt -r requirements-dev.txt -``` - -## 2) Set an API key and start server - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve -``` - -Server runs on `http://localhost:8000`. - -## 3) Check health - -```bash -curl -s http://localhost:8000/health | jq . -``` - -Expected shape: - -```json -{ - "data": {"status": "ok"}, - "meta": {"request_id": "..."}, - "error": null -} -``` - -## 4) Run first authorization check - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "action": "approve_payment", - "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 5000}, - "context": {"department": "finance"} - }' | jq . -``` - -Typical response fields: - -- `data.allowed`: `true` or `false` -- `data.decision`: `allow` or `deny` -- `data.reason`: human-readable reason -- `data.policy_id`: policy that made the decision -- `data.explain_trace`: decision trace for debugging -- `data.revision`: revision token for consistency - -## 5) Run a batch check - -```bash -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "alice", "role": "manager", "permissions": ["approve_payment"]}, - "items": [ - {"action": "approve_payment", "resource": {"resource_type": "payment", "resource_id": "pay-900", "amount": 1000}}, - {"action": "delete", "resource": {"resource_type": "payment", "resource_id": "pay-900"}} - ] - }' | jq . -``` - -## Next - -- [API Endpoints](api-endpoints.md) -- [Authorization Models](models/README.md) -- [Policies](policies.md) -- [CLI Guide](cli.md) diff --git a/docs/reference/api-reference.md b/docs/reference/api-reference.md deleted file mode 100644 index f0913e3..0000000 --- a/docs/reference/api-reference.md +++ /dev/null @@ -1,226 +0,0 @@ ---- -title: API Reference ---- - -# API Reference - -This page documents the implemented HTTP API surface in this repository. - -Implementation entrypoints: - -- `keynetra/api/main.py` -- `keynetra/api/service_modes.py` -- `keynetra/api/routes/*` - -OpenAPI contract: - -- `contracts/openapi/keynetra-v0.1.0.yaml` - -## Base URL - -Local default: - -```text -http://localhost:8000 -``` - -## Authentication - -Supported request auth: - -- `X-API-Key: ` -- `Authorization: Bearer ` -- Admin login via `POST /admin/login` - -Many management endpoints require elevated roles enforced in route dependencies. - -## Service Modes and Endpoint Availability - -Configured via `KEYNETRA_SERVICE_MODE`: - -- `all`: exposes access and management APIs -- `access-api`: exposes health/metrics + access endpoints -- `policy-store`: exposes health/metrics + management endpoints -- `policy-engine`: exposes health/metrics + access endpoints - -If an endpoint is missing in runtime, verify the service mode first. - -## Response Envelope - -Most endpoints return the standard envelope defined in `keynetra/domain/schemas/api.py`. - -Typical success shape: - -```json -{ - "success": true, - "data": {}, - "request_id": "..." -} -``` - -## Endpoint Groups - -### Health and Observability - -- `GET /health` -- `GET /health/live` -- `GET /health/ready` -- `GET /metrics` - -Example: - -```bash -curl -s http://localhost:8000/health/ready | jq . -``` - -### Access Decision - -- `POST /check-access` -- `POST /check-access-batch` -- `POST /simulate` - -Single decision example: - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "u1", "role": "admin"}, - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - }' | jq . -``` - -Batch decision example: - -```bash -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "user": {"id": "u1", "role": "admin"}, - "items": [ - {"action": "read", "resource": {"resource_type": "document", "resource_id": "doc-1"}, "context": {}}, - {"action": "write", "resource": {"resource_type": "document", "resource_id": "doc-1"}, "context": {}} - ] - }' | jq . -``` - -### Policy Simulation and Impact - -- `POST /simulate-policy` -- `POST /impact-analysis` - -Example: - -```bash -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "simulate": { - "policy_change": "allow:\n action: read\n priority: 10\n policy_key: read-admin\n when:\n role: admin" - }, - "request": { - "user": {"id": "u1", "role": "admin"}, - "action": "read", - "resource": {"resource_type": "document", "resource_id": "doc-1"}, - "context": {} - } - }' | jq . -``` - -### Policy Management - -- `GET /policies` -- `POST /policies` -- `PUT /policies/{policy_key}` -- `DELETE /policies/{policy_key}` -- `POST /policies/dsl` -- `POST /policies/{policy_key}/rollback/{version}` - -Create policy example: - -```bash -curl -s -X POST http://localhost:8000/policies \ - -H "Content-Type: application/json" \ - -H "X-API-Key: devkey" \ - -d '{ - "action": "read", - "effect": "allow", - "priority": 20, - "conditions": {"policy_key": "document-read-admin", "role": "admin"} - }' | jq . -``` - -### RBAC, ACL, Relationships, and Models - -RBAC endpoints: - -- `GET /roles` -- `POST /roles` -- `PUT /roles/{role_id}` -- `DELETE /roles/{role_id}` -- `GET /roles/{role_id}/permissions` -- `POST /roles/{role_id}/permissions` -- `DELETE /roles/{role_id}/permissions/{permission_id}` -- `GET /permissions` -- `POST /permissions` -- `PUT /permissions/{permission_id}` -- `DELETE /permissions/{permission_id}` -- `GET /permissions/{permission_id}/roles` - -ACL endpoints: - -- `POST /acl` -- `GET /acl/{resource_type}/{resource_id}` -- `DELETE /acl/{acl_id}` - -Relationship endpoints: - -- `GET /relationships` -- `POST /relationships` - -Authorization model endpoints: - -- `POST /auth-model` -- `GET /auth-model` - -### Audit, Playground, and Dev Utilities - -- `GET /audit` -- `POST /playground/evaluate` -- `GET /dev/sample-data` -- `POST /dev/sample-data/seed` - -## Common Error Cases - -- `401`: missing or invalid API key/JWT -- `403`: authenticated but insufficient management role -- `422`: payload validation error -- `500`: database or internal processing failure - -Inspect `request_id` in error responses to trace logs. - -## Versioning and Middleware - -Versioning middleware: - -- `keynetra/api/middleware/versioning.py` - -Other key middleware: - -- request id: `keynetra/api/middleware/request_id.py` -- rate limit: `keynetra/config/rate_limit.py` -- idempotency: `keynetra/api/middleware/idempotency.py` -- structured error envelope: `keynetra/api/middleware/errors.py` - -## Related Pages - -- [CLI Reference](cli-reference.md) -- [Configuration Files](configuration-files.md) -- [End-to-End API Example](../examples/end-to-end-api-flow.md) -- [Security](../operations/security.md) diff --git a/docs/reference/auth-model-files.md b/docs/reference/auth-model-files.md deleted file mode 100644 index 3a3ce36..0000000 --- a/docs/reference/auth-model-files.md +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: Authorization Model Files ---- - -# Authorization Model Files - -Authorization schema model support is implemented in: - -- `keynetra/config/file_loaders.py` -- `keynetra/modeling/schema_parser.py` -- `keynetra/modeling/model_validator.py` -- `keynetra/modeling/permission_compiler.py` - -Supported file formats: - -- `.yaml` / `.yml` -- `.json` -- `.toml` -- `.schema` / `.txt` (raw schema DSL) - -These files define relation and permission semantics used by the schema permission stage in authorization evaluation. - -## YAML Example - -```yaml -model: - schema_version: 1 - type: document - relations: - owner: user - editor: user - permissions: - read: owner or editor - write: owner -``` - -## Generated DSL Shape - -Files are normalized to schema DSL with sections like: - -```text -model schema 1 -type user -type document -relations -owner: [user] -editor: [user] -permissions -read = owner or editor -write = owner -``` - -## Runtime Integration - -- API startup auto-load via configured `model_paths`. -- `POST /auth-model` stores and compiles model per tenant. -- Embedded usage via `KeyNetra.load_model(...)`. - -## Minimal DSL Example - -```text -model schema 1 -type user -type document -relations -owner: [user] -permissions -read = owner -``` - -## Validation Rules - -The compiler/validator enforces: - -- schema version must be `>= 1` -- at least one type and permission must exist -- `user` type must exist -- relation subjects must reference defined types -- permission expressions must reference known relations/permissions - -## Related Pages - -- [Configuration Files](configuration-files.md) -- [Authorization Pipeline](../architecture/authorization-pipeline.md) -- [API Reference](api-reference.md) -- [Policy File Formats](policy-files.md) diff --git a/docs/reference/cli-reference.md b/docs/reference/cli-reference.md deleted file mode 100644 index 67be36b..0000000 --- a/docs/reference/cli-reference.md +++ /dev/null @@ -1,164 +0,0 @@ ---- -title: CLI Reference ---- - -# CLI Reference - -KeyNetra CLI is implemented in `keynetra/cli.py` and built with Typer. - -Entrypoint: - -```bash -python -m keynetra.cli --help -``` - -## Global Option - -- `--config `: load YAML/JSON/TOML configuration before executing a command. - -## Command Summary - -Server and runtime: - -- `serve` -- `start` (backward-compatible alias) -- `version` -- `help-cli` - -Auth and operations: - -- `admin-login` -- `migrate` -- `seed-data` -- `doctor` - -Decision workflows: - -- `check` -- `simulate` -- `impact` -- `explain` -- `benchmark` - -Policy/model tooling: - -- `test-policy` -- `compile-policies` -- `model apply` -- `model show` - -ACL tooling: - -- `acl add` -- `acl list` -- `acl remove` - -## Core Workflows - -### Start server - -```bash -export KEYNETRA_API_KEYS=devkey -python -m keynetra.cli serve --host 0.0.0.0 --port 8000 -``` - -### Check one access request - -```bash -python -m keynetra.cli check \ - --api-key devkey \ - --user '{"id":"alice","role":"manager"}' \ - --action approve_payment \ - --resource '{"resource_type":"payment","resource_id":"pay-900","amount":5000}' \ - --context '{"department":"finance"}' -``` - -### Simulate a policy change before rollout - -```bash -python -m keynetra.cli simulate \ - --api-key devkey \ - --policy-change 'allow:\n action: read\n priority: 10\n policy_key: read-admin\n when:\n role: admin' \ - --user '{"id":"u1","role":"admin"}' \ - --action read \ - --resource '{"resource_type":"document","resource_id":"doc-1"}' -``` - -### Estimate policy impact - -```bash -python -m keynetra.cli impact \ - --api-key devkey \ - --policy-change 'deny:\n action: export_payment\n priority: 5\n policy_key: deny-export-external\n when:\n role: external' -``` - -### Compile policies from configured paths - -```bash -python -m keynetra.cli compile-policies --config docs/examples/assets/keynetra.yaml -``` - -### Validate policy tests - -```bash -python -m keynetra.cli test-policy docs/examples/assets/policy_tests.yaml -``` - -### Local readiness checks - -```bash -python -m keynetra.cli doctor --service core --config docs/examples/assets/keynetra.yaml -``` - -## Model Commands - -Apply a schema model: - -```bash -python -m keynetra.cli model apply docs/examples/assets/auth-model.yaml --api-key devkey -``` - -Read current model: - -```bash -python -m keynetra.cli model show --api-key devkey -``` - -## ACL Commands - -Add ACL: - -```bash -python -m keynetra.cli acl add \ - --subject-type user \ - --subject-id alice \ - --resource-type document \ - --resource-id doc-1 \ - --action read \ - --effect allow -``` - -List ACL for resource: - -```bash -python -m keynetra.cli acl list --resource-type document --resource-id doc-1 -``` - -Remove ACL entry: - -```bash -python -m keynetra.cli acl remove --acl-id 1 -``` - -## Exit Behavior - -- Commands raise non-zero exit code on HTTP failure, validation failure, or readiness failure. -- `test-policy` exits non-zero if any policy test fails. -- `doctor` exits non-zero when `ok=false`. - -## Related Pages - -- [Quickstart](../getting-started/quickstart.md) -- [API Reference](api-reference.md) -- [Policy File Formats](policy-files.md) -- [CLI Workflows](../examples/cli-workflows.md) diff --git a/docs/reference/configuration-files.md b/docs/reference/configuration-files.md deleted file mode 100644 index 7ecab47..0000000 --- a/docs/reference/configuration-files.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -title: Configuration Files ---- - -# Configuration Files - -KeyNetra supports YAML, JSON, and TOML configuration files. - -Loader implementation: - -- `keynetra/config/config_loader.py` - -## Precedence - -When multiple configuration sources are used, effective settings follow this order: - -1. CLI flags (`--host`, `--port`, command-specific options) -2. Environment variables (`KEYNETRA_*`) -3. Config file values loaded via `--config` -4. Built-in defaults in `keynetra/config/settings.py` - -## Supported Keys - -Top-level keys currently mapped by loader: - -- `database.url` -- `redis.url` -- `policies.path` and `policies.paths` -- `models.path` and `models.paths` -- `policy_paths` -- `model_paths` -- `seed_data` -- `server.host` -- `server.port` - -These are transformed into `KEYNETRA_*` environment variables. - -## Field Mapping - -| Config Field | Type | Purpose | Mapped Environment Variable | -| --- | --- | --- | --- | -| `database.url` | string | SQLAlchemy database URL | `KEYNETRA_DATABASE_URL` | -| `redis.url` | string | Redis connection URL | `KEYNETRA_REDIS_URL` | -| `policies.path` / `policies.paths` | string/list | Policy file or directory inputs | `KEYNETRA_POLICY_PATHS` | -| `policy_paths` | list | Alternate explicit policy list | `KEYNETRA_POLICY_PATHS` | -| `models.path` / `models.paths` | string/list | Auth model files | `KEYNETRA_MODEL_PATHS` | -| `model_paths` | list | Alternate explicit model list | `KEYNETRA_MODEL_PATHS` | -| `seed_data` | bool | Auto-seed sample data in local mode | `KEYNETRA_AUTO_SEED_SAMPLE_DATA` | -| `server.host` | string | API bind host | `KEYNETRA_SERVER_HOST` | -| `server.port` | int | API bind port | `KEYNETRA_SERVER_PORT` | - -## Example YAML - -```yaml -database: - url: postgresql+psycopg://keynetra:keynetra@localhost:5432/keynetra - -redis: - url: redis://localhost:6379/0 - -policies: - paths: - - ./docs/examples/assets/policies - -models: - path: ./docs/examples/assets/auth-model.yaml - -seed_data: false - -server: - host: 0.0.0.0 - port: 8000 -``` - -## Example JSON - -```json -{ - "database": { "url": "sqlite+pysqlite:///./keynetra.db" }, - "redis": { "url": "redis://localhost:6379/0" }, - "policy_paths": ["./docs/examples/assets/policies"], - "model_paths": ["./docs/examples/assets/auth-model.yaml"], - "seed_data": true, - "server": { "host": "0.0.0.0", "port": 8000 } -} -``` - -## Example TOML - -```toml -[database] -url = "sqlite+pysqlite:///./keynetra.db" - -[redis] -url = "redis://localhost:6379/0" - -[policies] -path = "./docs/examples/assets/policies" - -[models] -path = "./docs/examples/assets/auth-model.yaml" - -seed_data = true - -[server] -host = "0.0.0.0" -port = 8000 -``` - -## Runtime Usage - -API server: - -```bash -python -m keynetra.cli serve --config ./docs/examples/assets/keynetra.yaml -``` - -Decision check using the same config: - -```bash -python -m keynetra.cli check \ - --config ./docs/examples/assets/keynetra.yaml \ - --api-key devkey \ - --user '{"id":"u1","role":"admin"}' \ - --action read \ - --resource '{"resource_type":"document","resource_id":"doc-1"}' -``` - -## Validation Tips - -- Use absolute paths in containerized environments. -- Keep policy/model paths under version control for repeatable deployments. -- Run `compile-policies` after any policy path change. -- Run `doctor --service core` before production rollout. - -## Related Pages - -- [Environment Variables](environment-variables.md) -- [Policy File Formats](policy-files.md) -- [Authorization Model Files](auth-model-files.md) -- [CLI Reference](cli-reference.md) diff --git a/docs/reference/environment-variables.md b/docs/reference/environment-variables.md deleted file mode 100644 index 00e11b0..0000000 --- a/docs/reference/environment-variables.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -title: Environment Variables ---- - -# Environment Variables - -Runtime settings are defined in `keynetra/config/settings.py`. `.env.example` provides baseline values. - -This page summarizes runtime variables and gives a production-oriented example block. - -## Core Runtime - -- `KEYNETRA_ENVIRONMENT` -- `KEYNETRA_DEBUG` -- `KEYNETRA_SERVICE_MODE` -- `KEYNETRA_SERVER_HOST` -- `KEYNETRA_SERVER_PORT` -- `KEYNETRA_AUTO_SEED_SAMPLE_DATA` - -Purpose: - -- environment mode, server bindings, routing mode, and local bootstrap behavior - -## Data Stores - -- `KEYNETRA_DATABASE_URL` -- `KEYNETRA_REDIS_URL` - -Purpose: - -- configure primary persistence (database) and optional distributed cache/event backend (Redis) - -## Authentication and Security - -- `KEYNETRA_API_KEYS` -- `KEYNETRA_API_KEY_HASHES` -- `KEYNETRA_JWT_SECRET` -- `KEYNETRA_JWT_ALGORITHM` -- `KEYNETRA_ADMIN_USERNAME` -- `KEYNETRA_ADMIN_PASSWORD` -- `KEYNETRA_ADMIN_TOKEN_EXPIRY_MINUTES` - -Purpose: - -- configure API auth methods and admin login token behavior - -## CORS - -- `KEYNETRA_CORS_ALLOW_ORIGINS` -- `KEYNETRA_CORS_ALLOW_ORIGIN_REGEX` -- `KEYNETRA_CORS_ALLOW_CREDENTIALS` -- `KEYNETRA_CORS_ALLOW_METHODS` -- `KEYNETRA_CORS_ALLOW_HEADERS` - -Purpose: - -- browser-origin controls for web clients - -## Policy and Model Loading - -- `KEYNETRA_POLICIES_JSON` -- `KEYNETRA_POLICY_PATHS` -- `KEYNETRA_MODEL_PATHS` - -Purpose: - -- configure inline policies or load policies/models from file paths - -## Caching and Resilience - -- `KEYNETRA_DECISION_CACHE_TTL_SECONDS` -- `KEYNETRA_SERVICE_TIMEOUT_SECONDS` -- `KEYNETRA_CRITICAL_RETRY_ATTEMPTS` -- `KEYNETRA_RESILIENCE_MODE` -- `KEYNETRA_RESILIENCE_FALLBACK_BEHAVIOR` -- `KEYNETRA_POLICY_EVENTS_CHANNEL` - -Purpose: - -- decision-cache tuning, service timeout/retry behavior, and policy event distribution - -## Rate Limiting - -- `KEYNETRA_RATE_LIMIT_PER_MINUTE` -- `KEYNETRA_RATE_LIMIT_BURST` -- `KEYNETRA_RATE_LIMIT_WINDOW_SECONDS` - -Purpose: - -- configure API request throttling defaults - -## OTel and OIDC - -- `KEYNETRA_OTEL_ENABLED` -- `KEYNETRA_OIDC_JWKS_URL` -- `KEYNETRA_OIDC_AUDIENCE` -- `KEYNETRA_OIDC_ISSUER` - -## Logging - -- `KEYNETRA_LOG_FORMAT` (`json` or `rich`) -- `KEYNETRA_FORCE_COLOR` (`1`/`0`) - -## Docker Startup Helpers - -- `KEYNETRA_RUN_MIGRATIONS` -- `KEYNETRA_STARTUP_SCREEN` -- `KEYNETRA_HOST` -- `KEYNETRA_PORT` -- `KEYNETRA_UVICORN_WORKERS` - -## Example `.env` - -```bash -KEYNETRA_ENVIRONMENT=production -KEYNETRA_DATABASE_URL=postgresql+psycopg://keynetra:keynetra@postgres:5432/keynetra -KEYNETRA_REDIS_URL=redis://redis:6379/0 -KEYNETRA_API_KEYS=devkey -KEYNETRA_JWT_SECRET=change-me -KEYNETRA_ADMIN_USERNAME=admin -KEYNETRA_ADMIN_PASSWORD=admin123 -KEYNETRA_POLICY_PATHS=./docs/examples/assets/policies -KEYNETRA_MODEL_PATHS=./docs/examples/assets/auth-model.yaml -KEYNETRA_SERVICE_MODE=all -KEYNETRA_SERVER_HOST=0.0.0.0 -KEYNETRA_SERVER_PORT=8000 -KEYNETRA_LOG_FORMAT=rich -KEYNETRA_FORCE_COLOR=1 -``` - -## Related Pages - -- [Configuration Files](configuration-files.md) -- [Troubleshooting](../operations/troubleshooting.md) -- [Security](../operations/security.md) diff --git a/docs/reference/policy-files.md b/docs/reference/policy-files.md deleted file mode 100644 index cc93d20..0000000 --- a/docs/reference/policy-files.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -title: Policy File Formats ---- - -# Policy File Formats - -Policy file loaders are implemented in: - -- `keynetra/config/file_loaders.py` - -Supported policy formats: - -- `.yaml` / `.yml` -- `.json` -- `.polar` - -Policy files can be loaded from individual files or recursively scanned directories. - -## YAML - -```yaml -policies: - - action: read - effect: allow - priority: 10 - policy_id: document-read-admin - conditions: - role: admin -``` - -Also supported: - -```yaml -allow: - action: read - priority: 10 - when: - role: admin -``` - -## JSON - -```json -[ - { - "action": "approve_payment", - "effect": "allow", - "priority": 5, - "conditions": { "role": "manager", "max_amount": 10000 } - } -] -``` - -## Polar-like Flat Rules - -```text -allow action=deploy priority=15 role=ops -deny action=deploy priority=100 -``` - -## Loading from Paths - -Configured `policy_paths` can be files or directories. Directory paths are scanned recursively for supported extensions. - -Priority and conditions are preserved as loaded and compiled into the decision graph. - -Runtime hooks: - -- CLI compile: `python -m keynetra.cli compile-policies --config ...` -- API startup bootstrap: `keynetra/api/main.py` (`_bootstrap_file_backed_policies`) -- Embedded usage: `KeyNetra.load_policies(...)` - -## Validation Tips - -- Ensure each rule has a non-empty `action`. -- Use explicit `priority` values for deterministic precedence. -- Keep condition keys consistent with request payload fields. - -## Related Pages - -- [Configuration Files](configuration-files.md) -- [Authorization Pipeline](../architecture/authorization-pipeline.md) -- [CLI Reference](cli-reference.md) diff --git a/docs/resources.md b/docs/resources.md deleted file mode 100644 index a5a5759..0000000 --- a/docs/resources.md +++ /dev/null @@ -1,32 +0,0 @@ -# Documentation Resources - -KeyNetra docs share a unified visual identity. The same `data/imgs/logo.png` graphic anchors: - -- `README.md` hero banner -- `docs/README.md` header overview -- Every quickstart/reference guide that embeds the logo via `` - -Use this file as the entry point for doc sources, templates, and branding assets. - -## Branding asset - -- File: `data/imgs/logo.png` -- Use: hero banner, doc headers, quickstart references -- Recommended alt text: "KeyNetra Logo" - -## Doc sources - -- `README.md`: top-level landing -- `docs/api-endpoints.md`: HTTP contract details -- `docs/models/`: authorization model explanations -- `docs/policies.md`: policy structure guidance -- `docs/use-cases.md`: real-world example scenarios -- `docs/deep-dive/`: developer manual, code walkthrough, integration cookbook - -Each markdown includes the same logo to keep visual continuity. - -## When adding new docs - -1. Save art in `data/imgs/` and reference via relative path `data/imgs/logo.png`. -2. Reuse the same hero markup `

KeyNetra Logo

` for brand consistency. -3. Keep doc resources structured under `docs/` so the documentation site can render them uniformly. diff --git a/docs/testing-guide.md b/docs/testing-guide.md deleted file mode 100644 index e838842..0000000 --- a/docs/testing-guide.md +++ /dev/null @@ -1,154 +0,0 @@ -# KeyNetra Verification Guide - -This guide verifies KeyNetra end-to-end without any UI. - -## 1) Run the test suite - -```bash -PYTHONPATH=. python3.11 -m pytest -q -``` - -Coverage audited in `tests/` (the repository does not contain `core/tests/`): - -- authorization engine -- RBAC, ABAC, ACL, relationship-based access (ReBAC) -- authorization modeling and compiled policy evaluation -- policy simulation and impact analysis -- revision tokens and consistency behavior -- metrics endpoint and cache behavior -- API contracts - -Additional endpoint-level coverage added for: - -- `POST /check-access-batch` -- `POST /simulate` -- `POST /simulate-policy` -- `POST /impact-analysis` - -## 2) Real-world authorization scenarios - -Use: - -- `examples/scenarios/real_world_authorization_scenarios.yaml` - -Included scenarios: - -- Document management system -- SaaS multi-tenant access -- Financial approval workflow -- Team collaboration -- Admin privilege delegation - -Each scenario defines subjects, resources, actions, relationships, roles, policies, and ACL entries. - -## 3) Authorization models - -Use model examples from: - -- `examples/models/document_model.yaml` -- `examples/models/saas_tenant_model.yaml` -- `examples/models/finance_model.yaml` -- `examples/models/team_collaboration_model.yaml` -- `examples/models/admin_delegation_model.yaml` - -## 4) Policy examples - -Use policy files from: - -- `examples/policies/document_access.yaml` -- `examples/policies/finance_policy.yaml` -- `examples/policies/team_access.yaml` - -## 5) API request examples - -Request payloads for all required endpoints: - -- `examples/requests/api_requests.json` - -Expected responses: - -- `examples/responses/api_expected_responses.json` - -### Example calls - -```bash -curl -s -X POST http://localhost:8000/check-access \ - -H "Content-Type: application/json" \ - -H "X-API-Key: testkey" \ - -d @<(jq '.["check-access"]' examples/requests/api_requests.json) - -curl -s -X POST http://localhost:8000/check-access-batch \ - -H "Content-Type: application/json" \ - -H "X-API-Key: testkey" \ - -d @<(jq '.["check-access-batch"]' examples/requests/api_requests.json) - -curl -s -X POST http://localhost:8000/simulate \ - -H "Content-Type: application/json" \ - -H "X-API-Key: testkey" \ - -d @<(jq '.["simulate"]' examples/requests/api_requests.json) - -curl -s -X POST http://localhost:8000/simulate-policy \ - -H "Content-Type: application/json" \ - -H "X-API-Key: testkey" \ - -d @<(jq '.["simulate-policy"]' examples/requests/api_requests.json) - -curl -s -X POST http://localhost:8000/impact-analysis \ - -H "Content-Type: application/json" \ - -H "X-API-Key: testkey" \ - -d @<(jq '.["impact-analysis"]' examples/requests/api_requests.json) -``` - -## 6) CLI verification examples - -Use: - -- `examples/requests/cli_examples.sh` - -Direct commands: - -```bash -keynetra check \ - --api-key testkey \ - --user '{"id":"alice","role":"editor","permissions":["approve_payment"]}' \ - --action read \ - --resource '{"resource_type":"document","resource_id":"doc-123"}' - -keynetra simulate \ - --api-key testkey \ - --policy-change 'allow:\n action: share_document\n priority: 1\n policy_key: share-admin\n when:\n role: admin' \ - --user '{"id":"root-admin","role":"admin","roles":["admin"]}' \ - --action share_document \ - --resource '{"resource_type":"document","resource_id":"doc-123"}' - -keynetra impact \ - --api-key testkey \ - --policy-change 'deny:\n action: export_payment\n priority: 1\n policy_key: deny-export-contractors\n when:\n role: external' -``` - -## 7) Developer verification forms - -Use structured forms from: - -- `examples/forms/developer_verification_forms.json` - -Fill one form per test case and compare actual decision vs `expected`. - -## 8) Example test datasets - -Use these datasets to seed and validate real-world flows: - -- `examples/data/users.json` -- `examples/data/roles.json` -- `examples/data/relationships.json` -- `examples/data/acl_entries.json` - -## 9) No-UI developer workflow - -1. Start API: `keynetra serve` -2. Run tests: `PYTHONPATH=. python3.11 -m pytest -q` -3. Replay API payloads from `examples/requests/api_requests.json` -4. Compare responses to `examples/responses/api_expected_responses.json` -5. Run CLI checks from `examples/requests/cli_examples.sh` -6. Validate scenario decisions using `examples/forms/developer_verification_forms.json` - -This provides repeatable verification through API, CLI, config files, and datasets only. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md deleted file mode 100644 index 12b5a7f..0000000 --- a/docs/troubleshooting.md +++ /dev/null @@ -1,80 +0,0 @@ -# Troubleshooting - -## 1) `401 unauthorized` on every request - -Cause: - -- Missing or wrong API key - -Fix: - -```bash -export KEYNETRA_API_KEYS=devkey -curl -H "X-API-Key: devkey" http://localhost:8000/health -``` - -## 2) `403 forbidden` on simulation endpoints - -Cause: - -- Principal does not have required management role - -Fix: - -- Use API key auth for local testing (`X-API-Key`) -- Or provide JWT with management claims - -## 3) `429 too_many_requests` - -Cause: - -- Rate limit exceeded - -Fix: - -```bash -export KEYNETRA_RATE_LIMIT_PER_MINUTE=1000 -export KEYNETRA_RATE_LIMIT_BURST=1000 -``` - -## 4) Database errors at startup - -Cause: - -- Bad `KEYNETRA_DATABASE_URL` -- Missing local DB permissions - -Fix: - -```bash -export KEYNETRA_DATABASE_URL=sqlite+pysqlite:///./keynetra.db -python -m keynetra.cli serve -``` - -## 5) Policy change does not seem to apply - -Cause: - -- Cache still serving old state -- Policy not loaded from expected path - -Fix: - -- Confirm policy path/config values -- Restart server for local debugging -- Use `/simulate-policy` to confirm new policy behavior - -## 6) Hard to understand deny responses - -Fix: - -- Use `/simulate` for `failed_conditions` -- Inspect `reason`, `policy_id`, and `explain_trace` - -## 7) CLI command cannot find model/policy file - -Fix: - -- Use absolute paths first -- Confirm working directory is repository root -- Check file extension and content format diff --git a/docs/use-cases.md b/docs/use-cases.md deleted file mode 100644 index 24cff2d..0000000 --- a/docs/use-cases.md +++ /dev/null @@ -1,83 +0,0 @@ -# Real-World Use Cases - -This page maps common product scenarios to KeyNetra concepts. - -## 1) Document Management System - -Typical requirements: - -- Owners can read/write/delete -- Editors can read/write -- Viewers can only read -- Specific users can be denied sharing for sensitive docs - -How KeyNetra helps: - -- ReBAC for owner/editor/viewer relationships -- ACL for per-document exceptions -- Policy trace for support/debugging - -## 2) SaaS Multi-Tenant Platform - -Typical requirements: - -- User can only access resources in their tenant -- Tenant admins manage tenant settings -- Cross-tenant access is denied by default - -How KeyNetra helps: - -- ABAC (`same_tenant`) checks -- RBAC for `tenant_admin` vs `tenant_member` -- Batch checks for dashboards with many widgets - -## 3) Financial Approval Workflow - -Typical requirements: - -- Managers can approve up to a threshold -- Finance admins approve above threshold -- Maker-checker separation (owner cannot self-approve) - -How KeyNetra helps: - -- ABAC for amount-based limits -- Explicit deny for maker-checker guardrail -- `/simulate-policy` before rolling out new thresholds - -## 4) Team Collaboration System - -Typical requirements: - -- Maintainers can merge -- Contributors can comment/read -- External users cannot merge even if they can view - -How KeyNetra helps: - -- ReBAC for maintainer/contributor relationships -- RBAC for external role restrictions -- ACL exceptions for temporary project access - -## 5) Admin Delegation - -Typical requirements: - -- Root admin can delegate limited rights -- Delegated admins can grant but not perform all root operations -- Read-only support users should never mutate policy - -How KeyNetra helps: - -- RBAC + ABAC for delegated constraints -- ACL deny entries for protected policy operations -- Impact analysis before changing admin policies - -## Suggested validation process for any use case - -1. Define model relations and permissions -2. Write baseline policies -3. Add ACL exceptions only where needed -4. Run `/simulate` and `/simulate-policy` -5. Run `/impact-analysis` -6. Add tests for critical rules diff --git a/infra/docker/Dockerfile b/infra/docker/Dockerfile deleted file mode 100644 index 8d4b1b3..0000000 --- a/infra/docker/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM python:3.11-slim - -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PIP_NO_CACHE_DIR=1 \ - PYTHONPATH=/app - -WORKDIR /app - -RUN useradd --create-home --uid 10001 appuser - -COPY core/requirements.txt /app/requirements.txt -RUN pip install --no-cache-dir -r /app/requirements.txt - -COPY core/alembic.ini /app/alembic.ini -COPY core/alembic /app/alembic -COPY core/keynetra /app/keynetra -COPY core/infra/docker/start.sh /usr/local/bin/start-keynetra - -RUN chmod +x /usr/local/bin/start-keynetra && chown -R appuser:appuser /app - -USER appuser -EXPOSE 8000 - -HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=5 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/ready', timeout=3)" - -ENTRYPOINT ["start-keynetra"] diff --git a/infra/docker/monitoring/grafana/dashboards/keynetra-overview.json b/infra/docker/monitoring/grafana/dashboards/keynetra-overview.json deleted file mode 100644 index 34634d9..0000000 --- a/infra/docker/monitoring/grafana/dashboards/keynetra-overview.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "uid": "keynetra-overview", - "title": "KeyNetra Overview", - "schemaVersion": 39, - "version": 1, - "refresh": "30s", - "timezone": "browser", - "panels": [ - { - "type": "stat", - "title": "Access Checks/s", - "gridPos": { "h": 8, "w": 8, "x": 0, "y": 0 }, - "datasource": "Prometheus", - "targets": [ - { - "expr": "sum(rate(keynetra_access_checks_total[5m]))", - "refId": "A" - } - ], - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value_and_name" - } - }, - { - "type": "stat", - "title": "Cache Hits/s", - "gridPos": { "h": 8, "w": 8, "x": 8, "y": 0 }, - "datasource": "Prometheus", - "targets": [ - { - "expr": "sum(rate(keynetra_cache_hits_total[5m]))", - "refId": "A" - } - ], - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value_and_name" - } - }, - { - "type": "stat", - "title": "Revision Updates/s", - "gridPos": { "h": 8, "w": 8, "x": 16, "y": 0 }, - "datasource": "Prometheus", - "targets": [ - { - "expr": "sum(rate(keynetra_revision_updates_total[5m]))", - "refId": "A" - } - ], - "options": { - "colorMode": "value", - "graphMode": "area", - "justifyMode": "center", - "textMode": "value_and_name" - } - } - ] -} diff --git a/infra/docker/monitoring/prometheus/prometheus.yml b/infra/docker/monitoring/prometheus/prometheus.yml deleted file mode 100644 index 1414b6b..0000000 --- a/infra/docker/monitoring/prometheus/prometheus.yml +++ /dev/null @@ -1,11 +0,0 @@ -global: - scrape_interval: 15s - evaluation_interval: 15s - -scrape_configs: - - job_name: keynetra - metrics_path: /metrics - static_configs: - - targets: - - keynetra:8000 - diff --git a/infra/docker/start.sh b/infra/docker/start.sh deleted file mode 100644 index 1d92c53..0000000 --- a/infra/docker/start.sh +++ /dev/null @@ -1,38 +0,0 @@ -#!/bin/sh -set -eu - -cd /app - -if [ "${KEYNETRA_RUN_MIGRATIONS:-1}" = "1" ]; then - alembic -c /app/alembic.ini upgrade head -fi - -# Docker uses uvicorn directly, so render the startup dashboard explicitly. -if [ "${KEYNETRA_STARTUP_SCREEN:-1}" = "1" ]; then - python - <<'PY' -import os -from keynetra.cli import _render_startup_screen -from keynetra.config.settings import get_settings - -host = os.getenv("KEYNETRA_HOST", "0.0.0.0") -port = int(os.getenv("KEYNETRA_PORT", "8000")) -settings = get_settings() -_render_startup_screen( - host=host, - port=port, - reload=False, - settings=settings, - config_path=os.getenv("KEYNETRA_CONFIG"), -) -PY -fi - -export KEYNETRA_LOG_FORMAT="${KEYNETRA_LOG_FORMAT:-rich}" -export KEYNETRA_FORCE_COLOR="${KEYNETRA_FORCE_COLOR:-1}" - -exec uvicorn keynetra.api.main:app \ - --host "${KEYNETRA_HOST:-0.0.0.0}" \ - --port "${KEYNETRA_PORT:-8000}" \ - --proxy-headers \ - --forwarded-allow-ips "*" \ - --workers "${KEYNETRA_UVICORN_WORKERS:-2}" diff --git a/infra/k8s/helm/keynetra/templates/deployment.yaml b/infra/k8s/helm/keynetra/templates/deployment.yaml deleted file mode 100644 index 0b19942..0000000 --- a/infra/k8s/helm/keynetra/templates/deployment.yaml +++ /dev/null @@ -1,19 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: keynetra -spec: - replicas: 1 - selector: - matchLabels: - app: keynetra - template: - metadata: - labels: - app: keynetra - spec: - containers: - - name: keynetra - image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - ports: - - containerPort: 8000 diff --git a/infra/k8s/helm/keynetra/templates/service.yaml b/infra/k8s/helm/keynetra/templates/service.yaml deleted file mode 100644 index 397ae5a..0000000 --- a/infra/k8s/helm/keynetra/templates/service.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: Service -metadata: - name: keynetra -spec: - selector: - app: keynetra - ports: - - port: {{ .Values.service.port }} - targetPort: 8000 diff --git a/infra/k8s/helm/keynetra/values.yaml b/infra/k8s/helm/keynetra/values.yaml deleted file mode 100644 index f4b6ec6..0000000 --- a/infra/k8s/helm/keynetra/values.yaml +++ /dev/null @@ -1,7 +0,0 @@ -image: - repository: ghcr.io/keynetra/core - tag: "0.1.0" - -service: - type: ClusterIP - port: 8000 diff --git a/infra/k8s/terraform/README.md b/infra/k8s/terraform/README.md deleted file mode 100644 index f8fedba..0000000 --- a/infra/k8s/terraform/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# KeyNetra Core Terraform - -This directory is reserved for self-hosted infrastructure modules only. - -Allowed examples: - -- single-tenant VM deployments -- self-hosted Kubernetes clusters -- customer-managed databases and caches - -Do not place SaaS control plane or managed multi-tenant infrastructure here. diff --git a/integrations/__init__.py b/integrations/__init__.py new file mode 100644 index 0000000..5053a30 --- /dev/null +++ b/integrations/__init__.py @@ -0,0 +1 @@ +"""External integration adapters for KeyNetra policy ecosystem.""" diff --git a/integrations/interfaces.py b/integrations/interfaces.py new file mode 100644 index 0000000..a63c158 --- /dev/null +++ b/integrations/interfaces.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Protocol + + +@dataclass(frozen=True) +class TupleRecord: + subject: str + relation: str + object: str + + +class TupleStoreAdapter(Protocol): + def import_tuples(self, tuples: list[TupleRecord]) -> int: ... + + def export_tuples(self) -> list[TupleRecord]: ... + + +class PolicyAdapter(Protocol): + def import_policies(self, payload: str) -> int: ... + + def export_policies(self) -> str: ... + + +class TerraformResourceAdapter(Protocol): + def plan(self) -> dict[str, object]: ... + + def apply(self) -> dict[str, object]: ... diff --git a/integrations/opa_rego_adapter.py b/integrations/opa_rego_adapter.py new file mode 100644 index 0000000..c37a180 --- /dev/null +++ b/integrations/opa_rego_adapter.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from integrations.interfaces import PolicyAdapter + + +@dataclass +class OPARegoPolicyAdapter(PolicyAdapter): + """Minimal OPA/Rego policy adapter scaffold.""" + + _rego: str = "" + + def import_policies(self, payload: str) -> int: + self._rego = payload + return len(payload.splitlines()) if payload else 0 + + def export_policies(self) -> str: + return self._rego diff --git a/integrations/openfga_adapter.py b/integrations/openfga_adapter.py new file mode 100644 index 0000000..35d7fe9 --- /dev/null +++ b/integrations/openfga_adapter.py @@ -0,0 +1,19 @@ +from __future__ import annotations + +from dataclasses import dataclass, field + +from integrations.interfaces import TupleRecord, TupleStoreAdapter + + +@dataclass +class InMemoryOpenFGATupleAdapter(TupleStoreAdapter): + """Starter adapter for OpenFGA tuple import/export workflows.""" + + tuples: list[TupleRecord] = field(default_factory=list) + + def import_tuples(self, tuples: list[TupleRecord]) -> int: + self.tuples.extend(tuples) + return len(tuples) + + def export_tuples(self) -> list[TupleRecord]: + return list(self.tuples) diff --git a/integrations/terraform_provider.py b/integrations/terraform_provider.py new file mode 100644 index 0000000..43c43f5 --- /dev/null +++ b/integrations/terraform_provider.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from integrations.interfaces import TerraformResourceAdapter + + +@dataclass +class TerraformPolicyResourceAdapter(TerraformResourceAdapter): + """Placeholder adapter boundary for Terraform-managed policy resources.""" + + policy_count: int = 0 + + def plan(self) -> dict[str, object]: + return {"changes": self.policy_count, "resource": "keynetra_policy"} + + def apply(self) -> dict[str, object]: + return {"applied": True, "resource_count": self.policy_count} diff --git a/keynetra/api/dependencies.py b/keynetra/api/dependencies.py new file mode 100644 index 0000000..c43c887 --- /dev/null +++ b/keynetra/api/dependencies.py @@ -0,0 +1,140 @@ +from __future__ import annotations + +from dataclasses import dataclass + +from fastapi import Depends, Request +from sqlalchemy.orm import Session + +from keynetra.config.redis_client import get_redis +from keynetra.config.settings import Settings, get_settings +from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache +from keynetra.infrastructure.cache.acl_cache import build_acl_cache +from keynetra.infrastructure.cache.decision_cache import build_decision_cache +from keynetra.infrastructure.cache.policy_cache import build_policy_cache +from keynetra.infrastructure.cache.policy_distribution import RedisPolicyEventPublisher +from keynetra.infrastructure.cache.relationship_cache import build_relationship_cache +from keynetra.infrastructure.repositories.acl import SqlACLRepository +from keynetra.infrastructure.repositories.audit import SqlAuditRepository +from keynetra.infrastructure.repositories.auth_models import SqlAuthModelRepository +from keynetra.infrastructure.repositories.policies import SqlPolicyRepository +from keynetra.infrastructure.repositories.relationships import SqlRelationshipRepository +from keynetra.infrastructure.repositories.tenants import SqlTenantRepository +from keynetra.infrastructure.repositories.users import SqlUserRepository +from keynetra.infrastructure.storage.session import get_db +from keynetra.services.access_indexer import AccessIndexer +from keynetra.services.authorization import AuthorizationService +from keynetra.services.impact_analysis import ImpactAnalyzer +from keynetra.services.interfaces import DecisionCache +from keynetra.services.policies import PolicyService +from keynetra.services.policy_lint import PolicyLintService +from keynetra.services.policy_simulator import PolicySimulator +from keynetra.services.relationships import RelationshipService + + +@dataclass(frozen=True) +class ServiceContainer: + db: Session + settings: Settings + tenant_repo: SqlTenantRepository + policy_repo: SqlPolicyRepository + user_repo: SqlUserRepository + relationship_repo: SqlRelationshipRepository + acl_repo: SqlACLRepository + audit_repo: SqlAuditRepository + auth_model_repo: SqlAuthModelRepository + authorization_service: AuthorizationService + policy_service: PolicyService + policy_lint_service: PolicyLintService + relationship_service: RelationshipService + access_indexer: AccessIndexer + access_index_cache: object + decision_cache: DecisionCache + policy_simulator: PolicySimulator + impact_analyzer: ImpactAnalyzer + + +def build_services( + request: Request, + settings: Settings = Depends(get_settings), + db: Session = Depends(get_db), +) -> ServiceContainer: + redis_client = get_redis() + decision_cache = build_decision_cache(redis_client) + policy_cache = build_policy_cache(redis_client) + relationship_cache = build_relationship_cache(redis_client) + acl_cache = build_acl_cache(redis_client) + access_index_cache = build_access_index_cache(redis_client) + tenant_repo = SqlTenantRepository(db) + policy_repo = SqlPolicyRepository(db) + user_repo = SqlUserRepository(db) + relationship_repo = SqlRelationshipRepository(db) + acl_repo = SqlACLRepository(db) + audit_repo = SqlAuditRepository(db) + auth_model_repo = SqlAuthModelRepository(db) + access_indexer = AccessIndexer( + acl_repository=acl_repo, + acl_cache=acl_cache, + access_index_cache=access_index_cache, + relationships=relationship_repo, + ) + request_id = getattr(request.state, "request_id", None) + authorization_service = AuthorizationService( + settings=settings, + tenants=tenant_repo, + policies=policy_repo, + users=user_repo, + relationships=relationship_repo, + audit=audit_repo, + policy_cache=policy_cache, + relationship_cache=relationship_cache, + decision_cache=decision_cache, + acl_repository=acl_repo, + acl_cache=acl_cache, + access_index_cache=access_index_cache, + auth_model_repository=auth_model_repo, + request_id=request_id, + ) + policy_service = PolicyService( + tenants=tenant_repo, + policies=policy_repo, + policy_cache=policy_cache, + decision_cache=decision_cache, + publisher=RedisPolicyEventPublisher(settings), + ) + policy_simulator = PolicySimulator( + tenants=tenant_repo, + policies=policy_repo, + authorization_service=authorization_service, + ) + impact_analyzer = ImpactAnalyzer( + tenants=tenant_repo, + policies=policy_repo, + users=user_repo, + relationships=relationship_repo, + ) + return ServiceContainer( + db=db, + settings=settings, + tenant_repo=tenant_repo, + policy_repo=policy_repo, + user_repo=user_repo, + relationship_repo=relationship_repo, + acl_repo=acl_repo, + audit_repo=audit_repo, + auth_model_repo=auth_model_repo, + authorization_service=authorization_service, + policy_service=policy_service, + policy_lint_service=PolicyLintService(session=db, policies=policy_repo), + relationship_service=RelationshipService( + tenants=tenant_repo, + relationships=relationship_repo, + relationship_cache=relationship_cache, + decision_cache=decision_cache, + access_index_cache=access_index_cache, + ), + access_indexer=access_indexer, + access_index_cache=access_index_cache, + decision_cache=decision_cache, + policy_simulator=policy_simulator, + impact_analyzer=impact_analyzer, + ) diff --git a/keynetra/api/main.py b/keynetra/api/main.py index 0d262a5..f5cee3d 100644 --- a/keynetra/api/main.py +++ b/keynetra/api/main.py @@ -1,40 +1,56 @@ +import logging +from collections.abc import AsyncIterator +from contextlib import asynccontextmanager + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from keynetra.api.middleware.admin import AdminAuthorizationContextMiddleware from keynetra.api.middleware.errors import register_error_handlers from keynetra.api.middleware.idempotency import IdempotencyMiddleware from keynetra.api.middleware.logging import RequestLoggingMiddleware from keynetra.api.middleware.request_id import RequestIdMiddleware +from keynetra.api.middleware.tenant import TenantResolverMiddleware from keynetra.api.middleware.versioning import ApiVersionMiddleware from keynetra.api.service_modes import router_for_mode from keynetra.config.rate_limit import RateLimitMiddleware from keynetra.config.redis_client import get_redis -from keynetra.config.settings import get_settings +from keynetra.config.settings import Settings, get_settings from keynetra.config.tenancy import DEFAULT_TENANT_KEY from keynetra.engine.compiled.decision_graph import COMPILED_POLICY_STORE from keynetra.engine.keynetra_engine import KeyNetraEngine from keynetra.engine.model_graph.permission_graph import MODEL_GRAPH_STORE, CompiledPermissionGraph from keynetra.infrastructure.cache.policy_cache import build_policy_cache -from keynetra.infrastructure.logging import configure_json_logging -from keynetra.infrastructure.storage.session import ( - create_session_factory, - initialize_database, -) +from keynetra.infrastructure.errors import BootstrapError +from keynetra.infrastructure.logging import configure_json_logging, log_event +from keynetra.infrastructure.storage.session import create_session_factory, initialize_database from keynetra.modeling.permission_compiler import compile_authorization_schema +from keynetra.observability.metrics import record_bootstrap_failure from keynetra.services.seeding import seed_demo_data from keynetra.version import version as keynetra_version +_bootstrap_logger = logging.getLogger("keynetra.bootstrap") + + +@asynccontextmanager +async def _lifespan(app: FastAPI) -> AsyncIterator[None]: + settings = get_settings() + _run_startup(settings) + _start_policy_subscriber(app, settings=settings) + try: + yield + finally: + _stop_policy_subscriber(app) + def create_app() -> FastAPI: configure_json_logging() - app = FastAPI(title="KeyNetra", version=keynetra_version) + app = FastAPI(title="KeyNetra", version=keynetra_version, lifespan=_lifespan) settings = get_settings() app.add_middleware(RequestIdMiddleware) + app.add_middleware(TenantResolverMiddleware) app.add_middleware(ApiVersionMiddleware) app.add_middleware(RequestLoggingMiddleware) - app.add_middleware(AdminAuthorizationContextMiddleware) app.add_middleware(RateLimitMiddleware, settings=settings) app.add_middleware(IdempotencyMiddleware, settings=settings) app.add_middleware( @@ -55,33 +71,43 @@ def create_app() -> FastAPI: from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor FastAPIInstrumentor.instrument_app(app) - except Exception: - pass - - @app.on_event("startup") - def _bootstrap_sample_data() -> None: - initialize_database(settings.database_url) - _bootstrap_file_backed_policies() - _bootstrap_file_backed_model() - if settings.environment.strip().lower() not in {"development", "dev", "local"}: - return - if not getattr(settings, "auto_seed_sample_data", False): - return - mode = getattr(settings, "service_mode", "all").strip().lower() - if mode not in {"all", "policy-store"}: - return - db = create_session_factory(settings.database_url)() - try: - seed_demo_data(db) - finally: - db.close() + except ImportError: + log_event(_bootstrap_logger, event="otel_disabled", reason="instrumentor_not_installed") + except RuntimeError as exc: + record_bootstrap_failure(stage="otel") + log_event(_bootstrap_logger, event="otel_init_failed", reason=repr(exc)) + if settings.environment in {"prod", "production"}: + raise BootstrapError("failed to initialize OTel instrumentation") from exc - _start_policy_subscriber(app) return app -def _start_policy_subscriber(app: FastAPI) -> None: - settings = get_settings() +def _run_startup(settings: Settings) -> None: + try: + initialize_database(settings.database_url) + except Exception as exc: + record_bootstrap_failure(stage="database") + log_event(_bootstrap_logger, event="bootstrap_database_failed", reason=repr(exc)) + raise BootstrapError("database initialization failed") from exc + _bootstrap_file_backed_policies(settings) + _bootstrap_file_backed_model(settings) + if settings.environment.strip().lower() not in {"development", "dev", "local"}: + return + if not getattr(settings, "auto_seed_sample_data", False): + return + mode = getattr(settings, "service_mode", "all").strip().lower() + if mode not in {"all", "policy-store"}: + return + db = create_session_factory(settings.database_url)() + try: + seed_demo_data(db) + finally: + db.close() + + +def _start_policy_subscriber(app: FastAPI, *, settings: Settings | None = None) -> None: + if settings is None: + settings = get_settings() policy_cache = build_policy_cache(get_redis()) try: import json @@ -103,18 +129,41 @@ def run() -> None: tenant_key = payload.get("tenant_key") if isinstance(tenant_key, str): policy_cache.invalidate(tenant_key) - except Exception: + except (TypeError, ValueError) as exc: + log_event( + _bootstrap_logger, + event="policy_subscriber_message_invalid", + reason=repr(exc), + ) continue t = threading.Thread(target=run, name="policy-subscriber", daemon=True) t.start() + app.state.policy_pubsub = pubsub app.state.policy_subscriber = t - except Exception: + except ImportError as exc: + log_event(_bootstrap_logger, event="policy_subscriber_unavailable", reason=repr(exc)) return + except RuntimeError as exc: + record_bootstrap_failure(stage="policy_subscriber") + log_event(_bootstrap_logger, event="policy_subscriber_failed", reason=repr(exc)) + if settings.environment in {"prod", "production"}: + raise BootstrapError("policy subscriber startup failed") from exc -def _bootstrap_file_backed_model() -> None: - settings = get_settings() +def _stop_policy_subscriber(app: FastAPI) -> None: + pubsub = getattr(app.state, "policy_pubsub", None) + if pubsub is None: + return + try: + pubsub.close() + except (RuntimeError, OSError, ValueError) as exc: + log_event(_bootstrap_logger, event="policy_subscriber_close_failed", reason=repr(exc)) + + +def _bootstrap_file_backed_model(settings: Settings | None = None) -> None: + if settings is None: + settings = get_settings() model_paths = settings.parsed_model_paths() if not model_paths: return @@ -129,18 +178,31 @@ def _bootstrap_file_backed_model() -> None: DEFAULT_TENANT_KEY, CompiledPermissionGraph(tenant_key=DEFAULT_TENANT_KEY, model=compiled), ) - except Exception: - return - - -def _bootstrap_file_backed_policies() -> None: - settings = get_settings() + except (ValueError, RuntimeError) as exc: + record_bootstrap_failure(stage="model_bootstrap") + log_event(_bootstrap_logger, event="model_bootstrap_failed", reason=repr(exc)) + if str(getattr(settings, "environment", "development")).strip().lower() in { + "prod", + "production", + }: + raise BootstrapError("authorization model bootstrap failed") from exc + + +def _bootstrap_file_backed_policies(settings: Settings | None = None) -> None: + if settings is None: + settings = get_settings() try: policies = settings.load_policies() engine = KeyNetraEngine(policies) COMPILED_POLICY_STORE.set(DEFAULT_TENANT_KEY, 1, engine._compiled_graph) - except Exception: - return + except (ValueError, RuntimeError) as exc: + record_bootstrap_failure(stage="policy_bootstrap") + log_event(_bootstrap_logger, event="policy_bootstrap_failed", reason=repr(exc)) + if str(getattr(settings, "environment", "development")).strip().lower() in { + "prod", + "production", + }: + raise BootstrapError("policy bootstrap failed") from exc app = create_app() diff --git a/keynetra/api/middleware/admin.py b/keynetra/api/middleware/admin.py deleted file mode 100644 index 7d8e680..0000000 --- a/keynetra/api/middleware/admin.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Administrative request context middleware.""" - -from __future__ import annotations - -from starlette.middleware.base import BaseHTTPMiddleware - - -class AdminAuthorizationContextMiddleware(BaseHTTPMiddleware): - _PREFIXES = ("/policies", "/roles", "/permissions", "/relationships", "/playground", "/audit") - - async def dispatch(self, request, call_next): # type: ignore[override] - request.state.requested_tenant_key = "default" - request.state.is_management_api = any( - request.url.path.startswith(prefix) for prefix in self._PREFIXES - ) - return await call_next(request) diff --git a/keynetra/api/middleware/errors.py b/keynetra/api/middleware/errors.py index 1cc3a00..74029a3 100644 --- a/keynetra/api/middleware/errors.py +++ b/keynetra/api/middleware/errors.py @@ -11,6 +11,7 @@ from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.config.settings import Settings +from keynetra.config.tenancy import tenant_for_logs from keynetra.infrastructure.logging import log_event from keynetra.infrastructure.metrics import record_api_error @@ -31,7 +32,7 @@ async def api_exception_handler(request: Request, exc: ApiError) -> JSONResponse code=str(exc.code), message=exc.message, request_id=_request_id(request), - tenant_id="default", + tenant_id=tenant_for_logs(request), ) payload: dict[str, Any] = { "data": None, @@ -83,7 +84,7 @@ async def unhandled_exception_handler(request: Request, exc: Exception) -> JSONR logger, event="unhandled_exception", request_id=rid, - tenant_id="default", + tenant_id=tenant_for_logs(request), error=repr(exc), traceback="".join(traceback.format_exception(type(exc), exc, exc.__traceback__)), ) diff --git a/keynetra/api/middleware/idempotency.py b/keynetra/api/middleware/idempotency.py index 8421057..1ca7a85 100644 --- a/keynetra/api/middleware/idempotency.py +++ b/keynetra/api/middleware/idempotency.py @@ -3,7 +3,7 @@ from __future__ import annotations import hashlib -from typing import Callable +from collections.abc import Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request diff --git a/keynetra/api/middleware/logging.py b/keynetra/api/middleware/logging.py index a5f35be..d343170 100644 --- a/keynetra/api/middleware/logging.py +++ b/keynetra/api/middleware/logging.py @@ -2,13 +2,15 @@ import logging import time -from typing import Callable +from collections.abc import Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response +from keynetra.config.tenancy import tenant_for_logs from keynetra.infrastructure.logging import log_event +from keynetra.observability.http_metrics import record_http_request class RequestLoggingMiddleware(BaseHTTPMiddleware): @@ -23,14 +25,23 @@ async def dispatch( ) -> Response: start = time.perf_counter() response = await call_next(request) + duration_seconds = time.perf_counter() - start + tenant_id = tenant_for_logs(request) log_event( self._logger, event="request_completed", method=request.method, path=request.url.path, status_code=response.status_code, - duration_ms=round((time.perf_counter() - start) * 1000, 3), + duration_ms=round(duration_seconds * 1000, 3), request_id=getattr(request.state, "request_id", None), - tenant_id="default", + tenant_id=tenant_id, + ) + record_http_request( + tenant=tenant_id, + endpoint=request.url.path, + method=request.method, + status=response.status_code, + duration_seconds=duration_seconds, ) return response diff --git a/keynetra/api/middleware/request_id.py b/keynetra/api/middleware/request_id.py index 3fa6267..f13390d 100644 --- a/keynetra/api/middleware/request_id.py +++ b/keynetra/api/middleware/request_id.py @@ -1,12 +1,14 @@ from __future__ import annotations import secrets -from typing import Callable +from collections.abc import Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import Response +from keynetra.infrastructure.logging import reset_correlation_id, set_correlation_id + class RequestIdMiddleware(BaseHTTPMiddleware): """ @@ -23,6 +25,10 @@ async def dispatch( ) -> Response: request_id = request.headers.get(self.header_name) or secrets.token_urlsafe(10) request.state.request_id = request_id - response = await call_next(request) - response.headers[self.header_name] = request_id - return response + token = set_correlation_id(request_id) + try: + response = await call_next(request) + response.headers[self.header_name] = request_id + return response + finally: + reset_correlation_id(token) diff --git a/keynetra/api/middleware/tenant.py b/keynetra/api/middleware/tenant.py new file mode 100644 index 0000000..84c2db1 --- /dev/null +++ b/keynetra/api/middleware/tenant.py @@ -0,0 +1,45 @@ +"""Tenant resolution middleware.""" + +from __future__ import annotations + +from collections.abc import Callable + +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.requests import Request +from starlette.responses import JSONResponse, Response + +from keynetra.config.tenancy import TENANT_HEADER_NAME, normalize_tenant_key + + +class TenantResolverMiddleware(BaseHTTPMiddleware): + """Resolves and validates tenant header into request state.""" + + _PREFIXES = ("/policies", "/roles", "/permissions", "/relationships", "/playground", "/audit") + + async def dispatch( + self, request: Request, call_next: Callable[[Request], Response] + ) -> Response: + request.state.is_management_api = any( + request.url.path.startswith(prefix) for prefix in self._PREFIXES + ) + requested = request.headers.get(TENANT_HEADER_NAME) + if requested is None: + request.state.requested_tenant_key = None + return await call_next(request) + + tenant_key = normalize_tenant_key(requested) + if tenant_key is None: + return JSONResponse( + status_code=422, + content={ + "data": None, + "error": { + "code": "validation_error", + "message": "invalid tenant header", + "details": {"header": TENANT_HEADER_NAME}, + }, + }, + ) + + request.state.requested_tenant_key = tenant_key + return await call_next(request) diff --git a/keynetra/api/middleware/versioning.py b/keynetra/api/middleware/versioning.py index 319e59e..0a0f46b 100644 --- a/keynetra/api/middleware/versioning.py +++ b/keynetra/api/middleware/versioning.py @@ -3,12 +3,13 @@ from __future__ import annotations import logging -from typing import Callable +from collections.abc import Callable from starlette.middleware.base import BaseHTTPMiddleware from starlette.requests import Request from starlette.responses import JSONResponse, Response +from keynetra.config.tenancy import TENANT_HEADER_NAME, normalize_tenant_key, tenant_for_logs from keynetra.infrastructure.logging import log_event @@ -43,6 +44,10 @@ async def dispatch( ) request.state.api_version = requested_version + if getattr(request.state, "requested_tenant_key", None) is None: + header_tenant = normalize_tenant_key(request.headers.get(TENANT_HEADER_NAME)) + if header_tenant: + request.state.requested_tenant_key = header_tenant log_event( logging.getLogger("keynetra.api_version"), event="api_version_used", @@ -50,7 +55,7 @@ async def dispatch( path=request.url.path, method=request.method, request_id=getattr(request.state, "request_id", None), - tenant_id="default", + tenant_id=tenant_for_logs(request), ) response = await call_next(request) response.headers[self.header_name] = requested_version diff --git a/keynetra/api/pagination.py b/keynetra/api/pagination.py index 7b757d0..6cd0868 100644 --- a/keynetra/api/pagination.py +++ b/keynetra/api/pagination.py @@ -2,18 +2,15 @@ from __future__ import annotations -import base64 -import json from typing import Any from keynetra.api.errors import ApiError, ApiErrorCode +from keynetra.domain.pagination import decode_cursor as _decode_cursor +from keynetra.domain.pagination import encode_cursor as _encode_cursor def encode_cursor(payload: dict[str, Any]) -> str: - """Encode an opaque cursor payload.""" - - raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8") - return base64.urlsafe_b64encode(raw).decode("ascii") + return _encode_cursor(payload) def decode_cursor(cursor: str | None) -> dict[str, Any] | None: @@ -22,8 +19,7 @@ def decode_cursor(cursor: str | None) -> dict[str, Any] | None: if not cursor: return None try: - raw = base64.urlsafe_b64decode(cursor.encode("ascii")) - decoded = json.loads(raw.decode("utf-8")) + decoded = _decode_cursor(cursor) except Exception as exc: raise ApiError( status_code=422, @@ -31,11 +27,4 @@ def decode_cursor(cursor: str | None) -> dict[str, Any] | None: message="invalid cursor", details={"cursor": cursor}, ) from exc - if not isinstance(decoded, dict): - raise ApiError( - status_code=422, - code=ApiErrorCode.VALIDATION_ERROR, - message="invalid cursor", - details={"cursor": cursor}, - ) return decoded diff --git a/keynetra/api/routes/access.py b/keynetra/api/routes/access.py index 216e603..89243c2 100644 --- a/keynetra/api/routes/access.py +++ b/keynetra/api/routes/access.py @@ -10,14 +10,17 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.responses import request_id_from_state, success_response -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal -from keynetra.config.settings import Settings, get_settings -from keynetra.config.tenancy import DEFAULT_TENANT_KEY +from keynetra.config.tenancy import ( + DEFAULT_TENANT_KEY, + TENANT_HEADER_NAME, + normalize_tenant_key, + tenant_from_principal, +) from keynetra.domain.schemas.access import ( AccessDecisionResponse, AccessRequest, @@ -27,19 +30,6 @@ SimulationResponse, ) from keynetra.domain.schemas.api import SuccessResponse -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.cache.acl_cache import build_acl_cache -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.cache.policy_cache import build_policy_cache -from keynetra.infrastructure.cache.relationship_cache import build_relationship_cache -from keynetra.infrastructure.repositories.acl import SqlACLRepository -from keynetra.infrastructure.repositories.audit import SqlAuditRepository -from keynetra.infrastructure.repositories.auth_models import SqlAuthModelRepository -from keynetra.infrastructure.repositories.policies import SqlPolicyRepository -from keynetra.infrastructure.repositories.relationships import SqlRelationshipRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.repositories.users import SqlUserRepository -from keynetra.infrastructure.storage.session import get_db from keynetra.services.attribute_validation import AttributeValidationError from keynetra.services.authorization import AuthorizationService @@ -47,27 +37,53 @@ logger = logging.getLogger("keynetra.access") -def get_authorization_service( - settings: Settings = Depends(get_settings), - db: Session = Depends(get_db), -) -> AuthorizationService: - """Create the request-scoped authorization service.""" - - redis_client = get_redis() - return AuthorizationService( - settings=settings, - tenants=SqlTenantRepository(db), - policies=SqlPolicyRepository(db), - users=SqlUserRepository(db), - relationships=SqlRelationshipRepository(db), - audit=SqlAuditRepository(db), - policy_cache=build_policy_cache(redis_client), - relationship_cache=build_relationship_cache(redis_client), - decision_cache=build_decision_cache(redis_client), - acl_repository=SqlACLRepository(db), - acl_cache=build_acl_cache(redis_client), - access_index_cache=build_access_index_cache(redis_client), - auth_model_repository=SqlAuthModelRepository(db), +def _legacy_service_override() -> AuthorizationService | None: + return None + + +def _resolve_tenant_key( + *, + request: Request, + principal: dict[str, str], + services: ServiceContainer, +) -> str: + headers = getattr(request, "headers", {}) + requested = normalize_tenant_key( + headers.get(TENANT_HEADER_NAME) or getattr(request.state, "requested_tenant_key", None) + ) + if requested: + request.state.requested_tenant_key = requested + return requested + + principal_tenant = tenant_from_principal(principal) + if principal_tenant: + request.state.requested_tenant_key = principal_tenant + return principal_tenant + + settings = getattr(services, "settings", None) + strict_tenancy = ( + bool(getattr(settings, "strict_tenancy", False)) if settings is not None else False + ) + if strict_tenancy: + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="tenant is required", + details={"header": TENANT_HEADER_NAME}, + ) + + is_development = ( + bool(getattr(settings, "is_development", lambda: True)()) if settings is not None else True + ) + if is_development: + request.state.requested_tenant_key = DEFAULT_TENANT_KEY + return DEFAULT_TENANT_KEY + + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="tenant is required", + details={"header": TENANT_HEADER_NAME}, ) @@ -76,23 +92,48 @@ def get_authorization_service( response_model=SuccessResponse[AccessDecisionResponse], dependencies=[Depends(get_principal)], ) -def check_access( +async def check_access( payload: AccessRequest, request: Request, - service: AuthorizationService = Depends(get_authorization_service), + service: AuthorizationService | None = Depends(_legacy_service_override), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), + policy_set: str = "active", ) -> dict[str, object]: - try: - result = service.authorize( - tenant_key=DEFAULT_TENANT_KEY, - principal=principal, - user=payload.user, - action=payload.action, - resource=payload.resource, - context=payload.context, - consistency=payload.consistency, - revision=payload.revision, + effective_service = service or services.authorization_service + tenant_key = _resolve_tenant_key(request=request, principal=principal, services=services) + normalized_policy_set = policy_set.strip().lower() + if normalized_policy_set not in {"active", "draft", "archived"}: + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="policy_set must be one of active, draft, archived", ) + try: + if services.settings.async_authorization_enabled: + result = await effective_service.authorize_async( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + action=payload.action, + resource=payload.resource, + context=payload.context, + consistency=payload.consistency, + revision=payload.revision, + policy_set=normalized_policy_set, + ) + else: + result = effective_service.authorize( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + action=payload.action, + resource=payload.resource, + context=payload.context, + consistency=payload.consistency, + revision=payload.revision, + policy_set=normalized_policy_set, + ) except AttributeValidationError as error: raise ApiError( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -131,21 +172,36 @@ def check_access( response_model=SuccessResponse[SimulationResponse], dependencies=[Depends(get_principal)], ) -def simulate( +async def simulate( payload: AccessRequest, request: Request, - service: AuthorizationService = Depends(get_authorization_service), + service: AuthorizationService | None = Depends(_legacy_service_override), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), ) -> dict[str, object]: + effective_service = service or services.authorization_service + tenant_key = _resolve_tenant_key(request=request, principal=principal, services=services) try: - decision = service.simulate( - tenant_key=DEFAULT_TENANT_KEY, - principal=principal, - user=payload.user, - action=payload.action, - resource=payload.resource, - context=payload.context, - ) + if services.settings.async_authorization_enabled: + decision = ( + await effective_service.authorize_async( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + action=payload.action, + resource=payload.resource, + context=payload.context, + ) + ).decision + else: + decision = effective_service.simulate( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + action=payload.action, + resource=payload.resource, + context=payload.context, + ) except AttributeValidationError as error: raise ApiError( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, @@ -172,7 +228,7 @@ def simulate( policy_id=decision.policy_id, explain_trace=[step.to_dict() for step in decision.explain_trace], failed_conditions=list(decision.failed_conditions), - revision=service.get_revision(tenant_key=DEFAULT_TENANT_KEY), + revision=effective_service.get_revision(tenant_key=tenant_key), ).model_dump(), request_id=request_id_from_state(request.state), ) @@ -183,21 +239,44 @@ def simulate( response_model=SuccessResponse[BatchAccessResponse], dependencies=[Depends(get_principal)], ) -def check_access_batch( +async def check_access_batch( payload: BatchAccessRequest, request: Request, - service: AuthorizationService = Depends(get_authorization_service), + service: AuthorizationService | None = Depends(_legacy_service_override), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), + policy_set: str = "active", ) -> dict[str, object]: - try: - results = service.authorize_batch( - tenant_key=DEFAULT_TENANT_KEY, - principal=principal, - user=payload.user, - items=[item.model_dump() for item in payload.items], - consistency=payload.consistency, - revision=payload.revision, + effective_service = service or services.authorization_service + tenant_key = _resolve_tenant_key(request=request, principal=principal, services=services) + normalized_policy_set = policy_set.strip().lower() + if normalized_policy_set not in {"active", "draft", "archived"}: + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="policy_set must be one of active, draft, archived", ) + try: + if services.settings.async_authorization_enabled: + results = await effective_service.authorize_batch_async( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + items=[item.model_dump() for item in payload.items], + consistency=payload.consistency, + revision=payload.revision, + policy_set=normalized_policy_set, + ) + else: + results = effective_service.authorize_batch( + tenant_key=tenant_key, + principal=principal, + user=payload.user, + items=[item.model_dump() for item in payload.items], + consistency=payload.consistency, + revision=payload.revision, + policy_set=normalized_policy_set, + ) except AttributeValidationError as error: raise ApiError( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, diff --git a/keynetra/api/routes/acl.py b/keynetra/api/routes/acl.py index 6cdaf63..3227da8 100644 --- a/keynetra/api/routes/acl.py +++ b/keynetra/api/routes/acl.py @@ -2,54 +2,27 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.management import ACLCreate, ACLOut -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.cache.acl_cache import build_acl_cache -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.repositories.acl import SqlACLRepository -from keynetra.infrastructure.repositories.relationships import SqlRelationshipRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db -from keynetra.services.access_indexer import AccessIndexer from keynetra.services.revisions import RevisionService router = APIRouter(prefix="/acl", dependencies=[Depends(get_principal)]) -def get_acl_dependencies( - db: Session = Depends(get_db), -) -> tuple[SqlTenantRepository, SqlACLRepository, AccessIndexer]: - redis_client = get_redis() - tenant_repo = SqlTenantRepository(db) - acl_repo = SqlACLRepository(db) - indexer = AccessIndexer( - acl_repository=acl_repo, - acl_cache=build_acl_cache(redis_client), - access_index_cache=build_access_index_cache(redis_client), - relationships=SqlRelationshipRepository(db), - ) - return tenant_repo, acl_repo, indexer - - @router.post("", response_model=SuccessResponse[ACLOut], status_code=status.HTTP_201_CREATED) def create_acl_entry( payload: ACLCreate, request: Request, - deps: tuple[SqlTenantRepository, SqlACLRepository, AccessIndexer] = Depends( - get_acl_dependencies - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: - tenant_repo, acl_repo, indexer = deps - tenant = tenant_repo.get_or_create(access.tenant_key) + tenant = services.tenant_repo.get_or_create(access.tenant_key) if payload.effect not in {"allow", "deny"}: raise ApiError( status_code=422, @@ -57,7 +30,7 @@ def create_acl_entry( message="effect must be allow or deny", ) try: - acl_id = acl_repo.create_acl_entry( + acl_id = services.acl_repo.create_acl_entry( tenant_id=tenant.id, subject_type=payload.subject_type, subject_id=payload.subject_id, @@ -66,14 +39,14 @@ def create_acl_entry( action=payload.action, effect=payload.effect, ) - created = acl_repo.get_acl_entry(tenant_id=tenant.id, acl_id=acl_id) - indexer.invalidate_resource( + created = services.acl_repo.get_acl_entry(tenant_id=tenant.id, acl_id=acl_id) + services.access_indexer.invalidate_resource( tenant_id=tenant.id, resource_type=payload.resource_type, resource_id=payload.resource_id, ) - build_decision_cache(get_redis()).bump_namespace(tenant.tenant_key) - RevisionService(tenant_repo).bump_revision(tenant_key=tenant.tenant_key) + services.decision_cache.bump_namespace(tenant.tenant_key) + RevisionService(services.tenant_repo).bump_revision(tenant_key=tenant.tenant_key) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" @@ -94,15 +67,12 @@ def list_acl_entries( resource_type: str, resource_id: str, request: Request, - deps: tuple[SqlTenantRepository, SqlACLRepository, AccessIndexer] = Depends( - get_acl_dependencies - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: - tenant_repo, acl_repo, _ = deps - tenant = tenant_repo.get_or_create(access.tenant_key) + tenant = services.tenant_repo.get_or_create(access.tenant_key) try: - rows = acl_repo.list_resource_acl( + rows = services.acl_repo.list_resource_acl( tenant_id=tenant.id, resource_type=resource_type, resource_id=resource_id ) except SQLAlchemyError as error: @@ -132,24 +102,21 @@ def list_acl_entries( def delete_acl_entry( acl_id: int, request: Request, - deps: tuple[SqlTenantRepository, SqlACLRepository, AccessIndexer] = Depends( - get_acl_dependencies - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("admin")), ) -> dict[str, object]: - tenant_repo, acl_repo, indexer = deps - tenant = tenant_repo.get_or_create(access.tenant_key) + tenant = services.tenant_repo.get_or_create(access.tenant_key) try: - target = acl_repo.get_acl_entry(tenant_id=tenant.id, acl_id=acl_id) - acl_repo.delete_acl_entry(tenant_id=tenant.id, acl_id=acl_id) + target = services.acl_repo.get_acl_entry(tenant_id=tenant.id, acl_id=acl_id) + services.acl_repo.delete_acl_entry(tenant_id=tenant.id, acl_id=acl_id) if target is not None: - indexer.invalidate_resource( + services.access_indexer.invalidate_resource( tenant_id=tenant.id, resource_type=target.resource_type, resource_id=target.resource_id, ) - build_decision_cache(get_redis()).bump_namespace(tenant.tenant_key) - RevisionService(tenant_repo).bump_revision(tenant_key=tenant.tenant_key) + services.decision_cache.bump_namespace(tenant.tenant_key) + RevisionService(services.tenant_repo).bump_revision(tenant_key=tenant.tenant_key) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" diff --git a/keynetra/api/routes/admin_auth.py b/keynetra/api/routes/admin_auth.py index 5ff6c2c..a00bf77 100644 --- a/keynetra/api/routes/admin_auth.py +++ b/keynetra/api/routes/admin_auth.py @@ -1,5 +1,6 @@ from __future__ import annotations +import hashlib import hmac from datetime import UTC, datetime, timedelta @@ -24,8 +25,9 @@ def admin_login( ) -> dict[str, object]: username = settings.admin_username password = settings.admin_password + password_hash = settings.admin_password_hash - if not username or not password: + if not username or (not password and not password_hash): raise ApiError( status_code=status.HTTP_403_FORBIDDEN, code=ApiErrorCode.FORBIDDEN, @@ -33,7 +35,12 @@ def admin_login( ) valid_username = hmac.compare_digest(payload.username, username) - valid_password = hmac.compare_digest(payload.password, password) + valid_password = False + if password_hash: + candidate_hash = hashlib.sha256(payload.password.encode("utf-8")).hexdigest() + valid_password = hmac.compare_digest(candidate_hash, password_hash) + elif password: + valid_password = hmac.compare_digest(payload.password, password) if not (valid_username and valid_password): raise ApiError( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/keynetra/api/routes/audit.py b/keynetra/api/routes/audit.py index ef201fb..982f4ed 100644 --- a/keynetra/api/routes/audit.py +++ b/keynetra/api/routes/audit.py @@ -4,17 +4,14 @@ from fastapi import APIRouter, Depends, Request from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.pagination import decode_cursor from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.management import AuditRecordOut -from keynetra.infrastructure.repositories.audit import SqlAuditRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db router = APIRouter(prefix="/audit") @@ -22,7 +19,7 @@ @router.get("", response_model=SuccessResponse[list[AuditRecordOut]]) def list_audit_logs( request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), limit: int = 50, cursor: str | None = None, @@ -38,9 +35,9 @@ def list_audit_logs( code=ApiErrorCode.VALIDATION_ERROR, message="limit must be between 1 and 100", ) - tenant = SqlTenantRepository(db).get_or_create(access.tenant_key) + tenant = services.tenant_repo.get_or_create(access.tenant_key) try: - items, next_cursor = SqlAuditRepository(db).list_page( + items, next_cursor = services.audit_repo.list_page( tenant_id=tenant.id, limit=limit, cursor=decode_cursor(cursor), diff --git a/keynetra/api/routes/auth_model.py b/keynetra/api/routes/auth_model.py index f03526b..660e2e7 100644 --- a/keynetra/api/routes/auth_model.py +++ b/keynetra/api/routes/auth_model.py @@ -2,8 +2,8 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role @@ -11,9 +11,6 @@ from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.modeling import AuthModelCreate, AuthModelOut from keynetra.engine.model_graph.permission_graph import MODEL_GRAPH_STORE, CompiledPermissionGraph -from keynetra.infrastructure.repositories.auth_models import SqlAuthModelRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db from keynetra.modeling import ( compile_authorization_schema, parse_authorization_schema, @@ -28,17 +25,15 @@ def create_auth_model( payload: AuthModelCreate, request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: - tenant_repo = SqlTenantRepository(db) - repo = SqlAuthModelRepository(db) - tenant = tenant_repo.get_or_create(access.tenant_key) + tenant = services.tenant_repo.get_or_create(access.tenant_key) try: schema = parse_authorization_schema(payload.schema_text) validate_authorization_schema(schema) compiled = compile_authorization_schema(schema) - record = repo.upsert_model( + record = services.auth_model_repo.upsert_model( tenant_id=tenant.id, schema_text=payload.schema_text, schema_json={ @@ -52,7 +47,7 @@ def create_auth_model( MODEL_GRAPH_STORE.set( access.tenant_key, CompiledPermissionGraph(tenant_key=access.tenant_key, model=compiled) ) - RevisionService(tenant_repo).bump_revision(tenant_key=access.tenant_key) + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except ValueError as error: raise ApiError( status_code=422, code=ApiErrorCode.VALIDATION_ERROR, message=str(error) @@ -76,13 +71,11 @@ def create_auth_model( @router.get("", response_model=SuccessResponse[AuthModelOut]) def get_auth_model( request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: - tenant_repo = SqlTenantRepository(db) - repo = SqlAuthModelRepository(db) - tenant = tenant_repo.get_or_create(access.tenant_key) - record = repo.get_model(tenant_id=tenant.id) + tenant = services.tenant_repo.get_or_create(access.tenant_key) + record = services.auth_model_repo.get_model(tenant_id=tenant.id) if record is None: raise ApiError(status_code=404, code=ApiErrorCode.NOT_FOUND, message="auth model not found") return success_response( diff --git a/keynetra/api/routes/dev.py b/keynetra/api/routes/dev.py index f7e816d..00ad28d 100644 --- a/keynetra/api/routes/dev.py +++ b/keynetra/api/routes/dev.py @@ -1,14 +1,13 @@ from __future__ import annotations from fastapi import APIRouter, Depends, Query, Request -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.sample_data import sample_bootstrap_document from keynetra.config.settings import Settings, get_settings from keynetra.domain.schemas.api import SuccessResponse -from keynetra.infrastructure.storage.session import get_db from keynetra.services.seeding import seed_demo_data router = APIRouter(prefix="/dev") @@ -33,12 +32,12 @@ def get_sample_data( @router.post("/sample-data/seed", response_model=SuccessResponse[dict[str, object]]) def seed_sample_data( request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), settings: Settings = Depends(get_settings), reset: bool = Query(False, description="Clear the sample dataset before reseeding it."), ) -> dict[str, object]: _require_local_dev(settings) - summary = seed_demo_data(db, reset=reset) + summary = seed_demo_data(services.db, reset=reset) return success_response( data={ "tenant_key": summary.tenant_key, diff --git a/keynetra/api/routes/health.py b/keynetra/api/routes/health.py index a17fa5a..bc054db 100644 --- a/keynetra/api/routes/health.py +++ b/keynetra/api/routes/health.py @@ -4,11 +4,11 @@ from fastapi.responses import JSONResponse from sqlalchemy import text +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.redis_client import get_redis from keynetra.config.settings import Settings, get_settings from keynetra.domain.schemas.api import SuccessResponse -from keynetra.infrastructure.storage.session import create_engine_for_url router = APIRouter() @@ -24,8 +24,12 @@ def liveness(request: Request) -> dict[str, object]: @router.get("/health/ready", response_model=SuccessResponse[dict[str, object]]) -def readiness(request: Request, settings: Settings = Depends(get_settings)) -> JSONResponse: - database_status = _check_database(settings) +def readiness( + request: Request, + settings: Settings = Depends(get_settings), + services: ServiceContainer = Depends(build_services), +) -> JSONResponse: + database_status = _check_database(services) redis_status = _check_redis(settings) healthy = database_status["status"] == "ok" and redis_status["status"] in { "ok", @@ -47,11 +51,9 @@ def readiness(request: Request, settings: Settings = Depends(get_settings)) -> J ) -def _check_database(settings: Settings) -> dict[str, str]: +def _check_database(services: ServiceContainer) -> dict[str, str]: try: - engine = create_engine_for_url(settings.database_url) - with engine.connect() as connection: - connection.execute(text("SELECT 1")) + services.db.execute(text("SELECT 1")) return {"status": "ok"} except Exception as exc: return {"status": "error", "detail": repr(exc)} diff --git a/keynetra/api/routes/permissions.py b/keynetra/api/routes/permissions.py index 1426c02..86f791a 100644 --- a/keynetra/api/routes/permissions.py +++ b/keynetra/api/routes/permissions.py @@ -3,15 +3,13 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy import and_, delete, or_, select from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.pagination import decode_cursor, encode_cursor from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal -from keynetra.config.tenancy import DEFAULT_TENANT_KEY from keynetra.domain.models.rbac import Permission, Role, role_permissions from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.management import ( @@ -20,9 +18,6 @@ PermissionUpdate, RoleOut, ) -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db from keynetra.services.revisions import RevisionService router = APIRouter(prefix="/permissions", dependencies=[Depends(get_principal)]) @@ -31,11 +26,12 @@ @router.get("", response_model=SuccessResponse[list[PermissionOut]]) def list_permissions( request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), _: AdminAccess = Depends(require_management_role("viewer")), limit: int = 50, cursor: str | None = None, ) -> dict[str, object]: + db = services.db if limit < 1 or limit > 100: raise ApiError( status_code=422, @@ -74,9 +70,10 @@ def list_permissions( @router.post("", response_model=PermissionOut, status_code=status.HTTP_201_CREATED) def create_permission( payload: PermissionCreate, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("admin")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("admin")), ) -> PermissionOut: + db = services.db existing = ( db.execute(select(Permission).where(Permission.action == payload.action)).scalars().first() ) @@ -89,8 +86,8 @@ def create_permission( db.add(perm) db.commit() db.refresh(perm) - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -101,9 +98,10 @@ def create_permission( def update_permission( permission_id: int, payload: PermissionUpdate, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("developer")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("developer")), ) -> PermissionOut: + db = services.db permission = db.get(Permission, permission_id) if permission is None: raise ApiError(status_code=404, code=ApiErrorCode.NOT_FOUND, message="permission not found") @@ -124,8 +122,8 @@ def update_permission( try: db.commit() db.refresh(permission) - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -136,9 +134,10 @@ def update_permission( def delete_permission( permission_id: int, request: Request, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("admin")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("admin")), ) -> dict[str, object]: + db = services.db permission = ( db.execute(select(Permission).where(Permission.id == permission_id).options()) .scalars() @@ -152,8 +151,8 @@ def delete_permission( ) db.delete(permission) db.commit() - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -166,9 +165,10 @@ def delete_permission( def list_permission_roles( permission_id: int, request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), _: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: + db = services.db permission = db.get(Permission, permission_id) if permission is None: raise ApiError(status_code=404, code=ApiErrorCode.NOT_FOUND, message="permission not found") diff --git a/keynetra/api/routes/policies.py b/keynetra/api/routes/policies.py index b4bd290..3f61f19 100644 --- a/keynetra/api/routes/policies.py +++ b/keynetra/api/routes/policies.py @@ -4,56 +4,24 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.pagination import decode_cursor from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal -from keynetra.config.settings import Settings, get_settings from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.management import PolicyCreate, PolicyOut -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.cache.policy_cache import build_policy_cache -from keynetra.infrastructure.cache.policy_distribution import RedisPolicyEventPublisher -from keynetra.infrastructure.repositories.policies import SqlPolicyRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db -from keynetra.services.policies import PolicyService from keynetra.services.policy_dsl import dsl_to_policy -from keynetra.services.policy_lint import PolicyLintService router = APIRouter(prefix="/policies", dependencies=[Depends(get_principal)]) -def get_policy_service( - settings: Settings = Depends(get_settings), - db: Session = Depends(get_db), -) -> tuple[PolicyService, PolicyLintService, SqlTenantRepository]: - """Create the shared repositories for policy management.""" - - redis_client = get_redis() - tenant_repo = SqlTenantRepository(db) - policy_repo = SqlPolicyRepository(db) - service = PolicyService( - tenants=tenant_repo, - policies=policy_repo, - policy_cache=build_policy_cache(redis_client), - decision_cache=build_decision_cache(redis_client), - publisher=RedisPolicyEventPublisher(settings), - ) - lint_service = PolicyLintService(session=db, policies=policy_repo) - return service, lint_service, tenant_repo - - @router.get("", response_model=SuccessResponse[list[PolicyOut]]) def list_policies( request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), limit: int = 50, cursor: str | None = None, @@ -64,14 +32,13 @@ def list_policies( code=ApiErrorCode.VALIDATION_ERROR, message="limit must be between 1 and 100", ) - service, lint_service, tenant_repo = deps tenant_key = access.tenant_key try: - items, next_cursor = service.list_policies_page( + items, next_cursor = services.policy_service.list_policies_page( tenant_key=tenant_key, limit=limit, cursor=decode_cursor(cursor) ) - tenant = tenant_repo.get_or_create(tenant_key) - warnings = lint_service.lint(tenant_id=tenant.id) + tenant = services.tenant_repo.get_or_create(tenant_key) + warnings = services.policy_lint_service.lint(tenant_id=tenant.id) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" @@ -89,13 +56,10 @@ def list_policies( def create_policy( payload: PolicyCreate, request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: - service, lint_service, tenant_repo = deps tenant_key = access.tenant_key if payload.effect not in {"allow", "deny"}: raise ApiError( @@ -103,8 +67,14 @@ def create_policy( code=ApiErrorCode.VALIDATION_ERROR, message="effect must be allow or deny", ) + if payload.state not in {"draft", "active", "archived"}: + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="state must be one of draft, active, archived", + ) try: - result = service.create_policy( + result = services.policy_service.create_policy( tenant_key=tenant_key, policy_key=str(payload.conditions.get("policy_key") or payload.action), action=payload.action, @@ -112,18 +82,22 @@ def create_policy( priority=payload.priority, conditions=payload.conditions, created_by=str(principal.get("id")), + state=payload.state, ) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" ) from error - warnings = lint_service.lint(tenant_id=tenant_repo.get_or_create(tenant_key).id) + warnings = services.policy_lint_service.lint( + tenant_id=services.tenant_repo.get_or_create(tenant_key).id + ) return success_response( data=PolicyOut( id=result.id, action=result.action, effect=result.effect, priority=result.priority, + state=result.state, conditions=result.conditions, ).model_dump(), request_id=request_id_from_state(request.state), @@ -136,21 +110,24 @@ def update_policy( policy_key: str, payload: PolicyCreate, request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: - service, lint_service, tenant_repo = deps if payload.effect not in {"allow", "deny"}: raise ApiError( status_code=422, code=ApiErrorCode.VALIDATION_ERROR, message="effect must be allow or deny", ) + if payload.state not in {"draft", "active", "archived"}: + raise ApiError( + status_code=422, + code=ApiErrorCode.VALIDATION_ERROR, + message="state must be one of draft, active, archived", + ) try: - result = service.create_policy( + result = services.policy_service.create_policy( tenant_key=access.tenant_key, policy_key=policy_key, action=payload.action, @@ -158,18 +135,22 @@ def update_policy( priority=payload.priority, conditions=payload.conditions, created_by=str(principal.get("id")), + state=payload.state, ) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" ) from error - warnings = lint_service.lint(tenant_id=tenant_repo.get_or_create(access.tenant_key).id) + warnings = services.policy_lint_service.lint( + tenant_id=services.tenant_repo.get_or_create(access.tenant_key).id + ) return success_response( data=PolicyOut( id=result.id, action=result.action, effect=result.effect, priority=result.priority, + state=result.state, conditions=result.conditions, ).model_dump(), request_id=request_id_from_state(request.state), @@ -181,9 +162,7 @@ def update_policy( def create_policy_from_dsl( dsl: str, request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), principal: dict[str, str] = Depends(get_principal), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: @@ -198,10 +177,11 @@ def create_policy_from_dsl( action=policy["action"], effect=policy["effect"], priority=policy["priority"], + state="active", conditions=policy["conditions"], ), request=request, - deps=deps, + services=services, principal=principal, access=access, ) @@ -211,14 +191,11 @@ def create_policy_from_dsl( def delete_policy( policy_key: str, request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("admin")), ) -> dict[str, object]: - service, _, _ = deps try: - service.delete_policy(tenant_key=access.tenant_key, policy_key=policy_key) + services.policy_service.delete_policy(tenant_key=access.tenant_key, policy_key=policy_key) except SQLAlchemyError as error: raise ApiError( status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error" @@ -235,14 +212,11 @@ def rollback_policy( policy_key: str, version: int, request: Request, - deps: tuple[PolicyService, PolicyLintService, SqlTenantRepository] = Depends( - get_policy_service - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("admin")), ) -> dict[str, object]: - service, _, _ = deps try: - current_policy_key, current_version = service.rollback_policy( + current_policy_key, current_version = services.policy_service.rollback_policy( tenant_key=access.tenant_key, policy_key=policy_key, version=version, diff --git a/keynetra/api/routes/relationships.py b/keynetra/api/routes/relationships.py index bcc859f..dfc70cb 100644 --- a/keynetra/api/routes/relationships.py +++ b/keynetra/api/routes/relationships.py @@ -5,22 +5,14 @@ from fastapi import APIRouter, Depends, Request, status from pydantic import BaseModel from sqlalchemy.exc import IntegrityError, SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.pagination import decode_cursor from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal from keynetra.domain.schemas.api import SuccessResponse -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.cache.relationship_cache import build_relationship_cache -from keynetra.infrastructure.repositories.relationships import SqlRelationshipRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db -from keynetra.services.relationships import RelationshipService router = APIRouter(prefix="/relationships", dependencies=[Depends(get_principal)]) @@ -37,25 +29,12 @@ class RelationshipOut(RelationshipCreate): id: int -def get_relationship_service(db: Session = Depends(get_db)) -> RelationshipService: - """Create the request-scoped relationship service.""" - - redis_client = get_redis() - return RelationshipService( - tenants=SqlTenantRepository(db), - relationships=SqlRelationshipRepository(db), - relationship_cache=build_relationship_cache(redis_client), - decision_cache=build_decision_cache(redis_client), - access_index_cache=build_access_index_cache(redis_client), - ) - - @router.get("", response_model=SuccessResponse[list[dict[str, str]]]) def list_relationships( subject_type: str, subject_id: str, request: Request, - service: RelationshipService = Depends(get_relationship_service), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), limit: int = 50, cursor: str | None = None, @@ -67,7 +46,7 @@ def list_relationships( message="limit must be between 1 and 100", ) try: - data, next_cursor = service.list_relationships_page( + data, next_cursor = services.relationship_service.list_relationships_page( tenant_key=access.tenant_key, subject_type=subject_type, subject_id=subject_id, @@ -92,11 +71,13 @@ def list_relationships( def create_relationship( payload: RelationshipCreate, request: Request, - service: RelationshipService = Depends(get_relationship_service), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: try: - row_id = service.create_relationship(tenant_key=access.tenant_key, **payload.model_dump()) + row_id = services.relationship_service.create_relationship( + tenant_key=access.tenant_key, **payload.model_dump() + ) except IntegrityError as error: raise ApiError( status_code=409, code=ApiErrorCode.CONFLICT, message="relationship exists" diff --git a/keynetra/api/routes/roles.py b/keynetra/api/routes/roles.py index af0f552..c014559 100644 --- a/keynetra/api/routes/roles.py +++ b/keynetra/api/routes/roles.py @@ -3,21 +3,17 @@ from fastapi import APIRouter, Depends, Request, status from sqlalchemy import and_, delete, or_, select from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session, joinedload +from sqlalchemy.orm import joinedload +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.pagination import decode_cursor, encode_cursor from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis from keynetra.config.security import get_principal -from keynetra.config.tenancy import DEFAULT_TENANT_KEY from keynetra.domain.models.rbac import Permission, Role, role_permissions, user_roles from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.management import PermissionOut, RoleCreate, RoleOut, RoleUpdate -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.storage.session import get_db from keynetra.services.revisions import RevisionService router = APIRouter(prefix="/roles", dependencies=[Depends(get_principal)]) @@ -26,11 +22,12 @@ @router.get("", response_model=SuccessResponse[list[RoleOut]]) def list_roles( request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), _: AdminAccess = Depends(require_management_role("viewer")), limit: int = 50, cursor: str | None = None, ) -> dict[str, object]: + db = services.db if limit < 1 or limit > 100: raise ApiError( status_code=422, @@ -65,9 +62,10 @@ def list_roles( @router.post("", response_model=RoleOut, status_code=status.HTTP_201_CREATED) def create_role( payload: RoleCreate, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("admin")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("admin")), ) -> RoleOut: + db = services.db existing = db.execute(select(Role).where(Role.name == payload.name)).scalars().first() if existing: raise ApiError(status_code=409, code=ApiErrorCode.CONFLICT, message="role already exists") @@ -76,8 +74,8 @@ def create_role( db.add(role) db.commit() db.refresh(role) - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -88,9 +86,10 @@ def create_role( def update_role( role_id: int, payload: RoleUpdate, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("developer")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("developer")), ) -> RoleOut: + db = services.db role = db.get(Role, role_id) if role is None: raise ApiError(status_code=404, code=ApiErrorCode.NOT_FOUND, message="role not found") @@ -105,8 +104,8 @@ def update_role( try: db.commit() db.refresh(role) - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -117,9 +116,10 @@ def update_role( def delete_role( role_id: int, request: Request, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("admin")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("admin")), ) -> dict[str, object]: + db = services.db role = ( db.execute( select(Role) @@ -137,8 +137,8 @@ def delete_role( db.execute(delete(user_roles).where(user_roles.c.role_id == role.id)) db.delete(role) db.commit() - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError(status_code=500, code=ApiErrorCode.DATABASE_ERROR, message="db error") from e @@ -151,9 +151,10 @@ def delete_role( def list_role_permissions( role_id: int, request: Request, - db: Session = Depends(get_db), + services: ServiceContainer = Depends(build_services), _: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: + db = services.db role = ( db.execute(select(Role).where(Role.id == role_id).options(joinedload(Role.permissions))) .scalars() @@ -179,9 +180,10 @@ def add_permission_to_role( role_id: int, permission_id: int, request: Request, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("developer")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: + db = services.db role = db.get(Role, role_id) permission = db.get(Permission, permission_id) if role is None: @@ -192,8 +194,8 @@ def add_permission_to_role( role.permissions.append(permission) try: db.commit() - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError( @@ -212,9 +214,10 @@ def remove_permission_from_role( role_id: int, permission_id: int, request: Request, - db: Session = Depends(get_db), - _: AdminAccess = Depends(require_management_role("developer")), + services: ServiceContainer = Depends(build_services), + access: AdminAccess = Depends(require_management_role("developer")), ) -> dict[str, object]: + db = services.db role = db.get(Role, role_id) permission = db.get(Permission, permission_id) if role is None: @@ -225,8 +228,8 @@ def remove_permission_from_role( role.permissions.remove(permission) try: db.commit() - build_access_index_cache(get_redis()).invalidate_global() - RevisionService(SqlTenantRepository(db)).bump_revision(tenant_key=DEFAULT_TENANT_KEY) + services.access_index_cache.invalidate_global() + RevisionService(services.tenant_repo).bump_revision(tenant_key=access.tenant_key) except SQLAlchemyError as e: db.rollback() raise ApiError( diff --git a/keynetra/api/routes/simulation.py b/keynetra/api/routes/simulation.py index 7fa53e6..fc6b6bb 100644 --- a/keynetra/api/routes/simulation.py +++ b/keynetra/api/routes/simulation.py @@ -2,13 +2,11 @@ from fastapi import APIRouter, Depends, Request from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.orm import Session +from keynetra.api.dependencies import ServiceContainer, build_services from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.api.responses import request_id_from_state, success_response from keynetra.config.admin_auth import AdminAccess, require_management_role -from keynetra.config.redis_client import get_redis -from keynetra.config.settings import get_settings from keynetra.domain.schemas.api import SuccessResponse from keynetra.domain.schemas.modeling import ( ImpactAnalysisRequest, @@ -16,66 +14,17 @@ PolicySimulationRequest, PolicySimulationResponse, ) -from keynetra.infrastructure.cache.access_index_cache import build_access_index_cache -from keynetra.infrastructure.cache.acl_cache import build_acl_cache -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.cache.policy_cache import build_policy_cache -from keynetra.infrastructure.cache.relationship_cache import build_relationship_cache -from keynetra.infrastructure.repositories.acl import SqlACLRepository -from keynetra.infrastructure.repositories.audit import SqlAuditRepository -from keynetra.infrastructure.repositories.auth_models import SqlAuthModelRepository -from keynetra.infrastructure.repositories.policies import SqlPolicyRepository -from keynetra.infrastructure.repositories.relationships import SqlRelationshipRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.infrastructure.repositories.users import SqlUserRepository -from keynetra.infrastructure.storage.session import get_db -from keynetra.services.authorization import AuthorizationService -from keynetra.services.impact_analysis import ImpactAnalyzer -from keynetra.services.policy_simulator import PolicySimulator router = APIRouter() -def get_simulation_services( - db: Session = Depends(get_db), -) -> tuple[AuthorizationService, PolicySimulator, ImpactAnalyzer]: - redis_client = get_redis() - tenants = SqlTenantRepository(db) - policies = SqlPolicyRepository(db) - users = SqlUserRepository(db) - relationships = SqlRelationshipRepository(db) - auth = AuthorizationService( - settings=get_settings(), - tenants=tenants, - policies=policies, - users=users, - relationships=relationships, - audit=SqlAuditRepository(db), - policy_cache=build_policy_cache(redis_client), - relationship_cache=build_relationship_cache(redis_client), - decision_cache=build_decision_cache(redis_client), - acl_repository=SqlACLRepository(db), - acl_cache=build_acl_cache(redis_client), - access_index_cache=build_access_index_cache(redis_client), - auth_model_repository=SqlAuthModelRepository(db), - ) - simulator = PolicySimulator(tenants=tenants, policies=policies, authorization_service=auth) - impact = ImpactAnalyzer( - tenants=tenants, policies=policies, users=users, relationships=relationships - ) - return auth, simulator, impact - - @router.post("/simulate-policy", response_model=SuccessResponse[PolicySimulationResponse]) def simulate_policy( payload: PolicySimulationRequest, request: Request, - deps: tuple[AuthorizationService, PolicySimulator, ImpactAnalyzer] = Depends( - get_simulation_services - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: - _auth, simulator, _impact = deps req = _normalize_request(payload.request) policy_change = payload.simulate.policy_change if not policy_change: @@ -83,7 +32,7 @@ def simulate_policy( status_code=422, code=ApiErrorCode.VALIDATION_ERROR, message="policy_change is required" ) try: - result = simulator.simulate_policy_change( + result = services.policy_simulator.simulate_policy_change( tenant_key=access.tenant_key, user=req["user"], action=req["action"], @@ -122,14 +71,11 @@ def simulate_policy( def impact_analysis( payload: ImpactAnalysisRequest, request: Request, - deps: tuple[AuthorizationService, PolicySimulator, ImpactAnalyzer] = Depends( - get_simulation_services - ), + services: ServiceContainer = Depends(build_services), access: AdminAccess = Depends(require_management_role("viewer")), ) -> dict[str, object]: - _auth, _simulator, impact = deps try: - result = impact.analyze_policy_change( + result = services.impact_analyzer.analyze_policy_change( tenant_key=access.tenant_key, policy_change=payload.policy_change ) except ValueError as error: diff --git a/keynetra/cli.py b/keynetra/cli.py index 67f2454..890f0ff 100644 --- a/keynetra/cli.py +++ b/keynetra/cli.py @@ -54,8 +54,10 @@ app = typer.Typer(add_completion=False, help="KeyNetra operational CLI.") acl_app = typer.Typer(add_completion=False, help="Manage ACL entries.") model_app = typer.Typer(add_completion=False, help="Manage authorization models.") +config_app = typer.Typer(add_completion=False, help="Configuration diagnostics.") app.add_typer(acl_app, name="acl") app.add_typer(model_app, name="model") +app.add_typer(config_app, name="config") @app.callback() @@ -198,8 +200,8 @@ def _render_startup_screen( f = pyfiglet.figlet_format("KEYNETRA", font="slant") banner = Text(f, style="bold magenta") - except Exception: - pass + except (ImportError, RuntimeError, ValueError): + banner = Text("KEYNETRA", style="bold magenta") header = Panel.fit( Align.center( @@ -663,6 +665,72 @@ def compile_policies( ) +@app.command("generate-openapi") +def generate_openapi( + output: str = typer.Option( + "contracts/openapi/openapi.json", "--output", help="OpenAPI output file path." + ), +) -> None: + """Generate OpenAPI contract directly from the FastAPI app.""" + from keynetra.main import create_app + + app_instance = create_app() + payload = app_instance.openapi() + try: + import yaml + except ModuleNotFoundError as exc: + raise typer.BadParameter("pyyaml is required to generate yaml contracts") from exc + + out_path = Path(output) + out_path.parent.mkdir(parents=True, exist_ok=True) + if out_path.suffix.lower() == ".json": + out_path.write_text(json.dumps(payload, indent=2), encoding="utf-8") + else: + out_path.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8") + typer.echo(str(out_path)) + + +@app.command("check-openapi") +def check_openapi( + contract: str = typer.Option( + "contracts/openapi/openapi.json", + "--contract", + help="Versioned OpenAPI contract to compare against generated output.", + ), +) -> None: + """Fail if generated OpenAPI differs from the versioned contract.""" + from keynetra.main import create_app + + app_instance = create_app() + payload = app_instance.openapi() + try: + import yaml + except ModuleNotFoundError as exc: + raise typer.BadParameter("pyyaml is required to check yaml contracts") from exc + + path = Path(contract) + if not path.exists(): + raise typer.BadParameter(f"contract file not found: {path}") + if path.suffix.lower() == ".json": + generated = json.dumps(payload, indent=2) + else: + generated = yaml.safe_dump(payload, sort_keys=False) + expected = path.read_text(encoding="utf-8") + if generated != expected: + typer.echo( + json.dumps( + { + "ok": False, + "message": "OpenAPI contract drift detected.", + "contract": str(path), + }, + indent=2, + ) + ) + raise typer.Exit(code=1) + typer.echo(json.dumps({"ok": True, "contract": str(path)}, indent=2)) + + @app.command("doctor") def doctor( ctx: typer.Context, @@ -690,6 +758,43 @@ def doctor( raise typer.Exit(code=1) +@config_app.command("doctor") +def config_doctor( + ctx: typer.Context, + config: str | None = typer.Option(None, "--config", help="Path to config file."), +) -> None: + """Validate runtime configuration and print explicit remediation guidance.""" + _maybe_load_config(ctx, config) + settings = get_settings() + result = run_core_doctor(settings) + findings: list[dict[str, Any]] = [] + for check in result.get("checks", []): + details = check.get("details") or {} + remediation = details.get("remediation") if isinstance(details, dict) else None + if check.get("ok"): + continue + findings.append( + { + "check": check.get("name"), + "message": check.get("message"), + "remediation": remediation if isinstance(remediation, list) else [], + } + ) + typer.echo( + json.dumps( + { + "service": "core", + "ok": result.get("ok", False), + "environment": settings.environment, + "findings": findings, + }, + indent=2, + ) + ) + if findings: + raise typer.Exit(code=1) + + async def _run_benchmark( url: str, payload: dict[str, Any], diff --git a/keynetra/config/admin_auth.py b/keynetra/config/admin_auth.py index 17e96ad..74a2b70 100644 --- a/keynetra/config/admin_auth.py +++ b/keynetra/config/admin_auth.py @@ -7,7 +7,8 @@ from keynetra.api.errors import ApiError, ApiErrorCode from keynetra.config.security import get_principal -from keynetra.config.tenancy import DEFAULT_TENANT_KEY +from keynetra.config.settings import Settings, get_settings +from keynetra.config.tenancy import DEFAULT_TENANT_KEY, normalize_tenant_key, tenant_from_principal _ROLE_ORDER = {"viewer": 1, "developer": 2, "admin": 3} @@ -26,14 +27,20 @@ def require_management_role(minimum_role: str): def dependency( request: Request, principal: dict[str, Any] = Depends(get_principal), + settings: Settings = Depends(get_settings), ) -> AdminAccess: - role = _resolve_tenant_role(principal) + if not isinstance(settings, Settings): + settings = get_settings() + tenant_key = _resolve_request_tenant_key( + request=request, principal=principal, settings=settings + ) + role = _resolve_tenant_role(principal, tenant_key=tenant_key) if role is None: raise ApiError( status_code=status.HTTP_403_FORBIDDEN, code=ApiErrorCode.FORBIDDEN, message="tenant access denied", - details={"tenant_key": DEFAULT_TENANT_KEY}, + details={"tenant_key": tenant_key}, ) if _ROLE_ORDER[role] < _ROLE_ORDER[minimum_role]: raise ApiError( @@ -43,19 +50,28 @@ def dependency( details={ "required_role": minimum_role, "actual_role": role, - "tenant_key": DEFAULT_TENANT_KEY, + "tenant_key": tenant_key, }, ) request.state.admin_role = role - request.state.admin_tenant_key = DEFAULT_TENANT_KEY - return AdminAccess(tenant_key=DEFAULT_TENANT_KEY, role=role, principal=principal) + request.state.admin_tenant_key = tenant_key + request.state.requested_tenant_key = tenant_key + return AdminAccess(tenant_key=tenant_key, role=role, principal=principal) return dependency -def _resolve_tenant_role(principal: dict[str, Any]) -> str | None: +def _resolve_tenant_role(principal: dict[str, Any], tenant_key: str | None = None) -> str | None: if principal.get("type") == "api_key": - return "admin" + scopes = principal.get("scopes") + if isinstance(scopes, dict): + role = scopes.get("role") + if isinstance(role, str) and role in _ROLE_ORDER: + scoped_tenant = normalize_tenant_key(str(scopes.get("tenant") or "")) + if tenant_key and scoped_tenant and scoped_tenant != tenant_key: + return None + return role + return None claims = principal.get("claims") if not isinstance(claims, dict): @@ -63,6 +79,10 @@ def _resolve_tenant_role(principal: dict[str, Any]) -> str | None: tenant_roles = claims.get("tenant_roles") if isinstance(tenant_roles, dict): + if tenant_key: + role = tenant_roles.get(tenant_key) + if isinstance(role, str) and role in _ROLE_ORDER: + return role for role in sorted( tenant_roles.values(), key=lambda item: _ROLE_ORDER.get(item, 0), reverse=True ): @@ -73,6 +93,11 @@ def _resolve_tenant_role(principal: dict[str, Any]) -> str | None: if not isinstance(item, dict): continue role = item.get("role") + role_tenant = normalize_tenant_key( + str(item.get("tenant") or item.get("tenant_key") or "") + ) + if tenant_key and role_tenant and role_tenant != tenant_key: + continue if isinstance(role, str) and role in _ROLE_ORDER: return role @@ -87,3 +112,36 @@ def _resolve_tenant_role(principal: dict[str, Any]) -> str | None: return item return None + + +def _resolve_request_tenant_key( + *, request: Request, principal: dict[str, Any], settings: Settings +) -> str: + headers = getattr(request, "headers", {}) + header_tenant = normalize_tenant_key( + headers.get("X-Tenant-Id") or getattr(request.state, "requested_tenant_key", None) + ) + if header_tenant: + return header_tenant + + principal_tenant = tenant_from_principal(principal) + if principal_tenant: + return principal_tenant + + if settings.strict_tenancy: + raise ApiError( + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + code=ApiErrorCode.VALIDATION_ERROR, + message="tenant is required", + details={"header": "X-Tenant-Id"}, + ) + + if settings.is_development(): + return DEFAULT_TENANT_KEY + + raise ApiError( + status_code=status.HTTP_422_UNPROCESSABLE_CONTENT, + code=ApiErrorCode.VALIDATION_ERROR, + message="tenant is required", + details={"header": "X-Tenant-Id"}, + ) diff --git a/keynetra/config/rate_limit.py b/keynetra/config/rate_limit.py index bf5a050..36921f2 100644 --- a/keynetra/config/rate_limit.py +++ b/keynetra/config/rate_limit.py @@ -3,6 +3,7 @@ from __future__ import annotations import hashlib +import logging import math import time from dataclasses import dataclass @@ -15,6 +16,9 @@ from keynetra.config.redis_client import get_redis from keynetra.config.settings import Settings +from keynetra.infrastructure.logging import log_event + +_logger = logging.getLogger("keynetra.rate_limit") @dataclass @@ -86,7 +90,7 @@ async def dispatch(self, request: Request, call_next) -> Response: # type: igno response.headers["X-RateLimit-Reset"] = str(decision.retry_after) return response - def _consume(self, request: Request) -> "_BucketDecision | Response": + def _consume(self, request: Request) -> _BucketDecision | Response: rate = max(1, self._settings.rate_limit_per_minute) interval = max(1, self._settings.rate_limit_window_seconds) capacity = max(1, self._settings.rate_limit_burst or rate) @@ -120,8 +124,13 @@ def _consume(self, request: Request) -> "_BucketDecision | Response": return _BucketDecision( limit=capacity, remaining=remaining_tokens, retry_after=retry_after_seconds ) - except Exception: - pass + except (AttributeError, ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event( + _logger, + event="rate_limit_redis_fallback", + reason=repr(exc), + request_id=getattr(request.state, "request_id", None), + ) with _local_limits_lock: bucket = _local_limits.get(key) diff --git a/keynetra/config/sample_data.py b/keynetra/config/sample_data.py index 8a5027d..252aec8 100644 --- a/keynetra/config/sample_data.py +++ b/keynetra/config/sample_data.py @@ -94,7 +94,7 @@ def sample_bootstrap_document() -> dict[str, Any]: "policies": SAMPLE_POLICY_DEFINITIONS, }, "commands": { - "seed": "PYTHONPATH=core python -m keynetra.cli seed-data --reset", - "start": "PYTHONPATH=core python -m keynetra.cli start --host 0.0.0.0 --port 8000", + "seed": "PYTHONPATH=core keynetra seed-data --reset", + "start": "PYTHONPATH=core keynetra start --host 0.0.0.0 --port 8000", }, } diff --git a/keynetra/config/security.py b/keynetra/config/security.py index aace2b0..f240fe0 100644 --- a/keynetra/config/security.py +++ b/keynetra/config/security.py @@ -3,6 +3,8 @@ import hashlib import hmac import logging +import threading +import time from typing import Any from fastapi import Depends, HTTPException, Request, Security, status @@ -10,11 +12,16 @@ from jose import JWTError, jwt from keynetra.config.settings import Settings, get_settings +from keynetra.config.tenancy import DEFAULT_TENANT_KEY, tenant_for_logs from keynetra.infrastructure.logging import log_event +from keynetra.observability.metrics import record_auth_failure, record_jwks_fetch api_key_scheme = APIKeyHeader(name="X-API-Key", auto_error=False) bearer_scheme = HTTPBearer(auto_error=False) _auth_logger = logging.getLogger("keynetra.auth") +_jwks_cache: dict[str, tuple[float, dict[str, Any]]] = {} +_jwks_backoff_until: dict[str, float] = {} +_jwks_lock = threading.Lock() def _decode_with_jwks(token: str, jwks: dict, audience: str | None, issuer: str | None) -> dict: @@ -38,6 +45,7 @@ def _unauthorized(detail: str = "unauthorized") -> HTTPException: def _log_failed_auth(request: Request, *, reason: str, api_key: str | None = None) -> None: + record_auth_failure(reason=reason) log_event( _auth_logger, event="auth_failed", @@ -45,7 +53,7 @@ def _log_failed_auth(request: Request, *, reason: str, api_key: str | None = Non path=request.url.path, method=request.method, request_id=getattr(request.state, "request_id", None), - tenant_id="default", + tenant_id=tenant_for_logs(request), client_host=request.client.host if request.client else None, api_key_prefix=(api_key or "")[:12] or None, ) @@ -56,6 +64,54 @@ def _matches_api_key(candidate: str, stored_hashes: set[str]) -> bool: return any(hmac.compare_digest(candidate_hash, stored_hash) for stored_hash in stored_hashes) +def _scopes_are_defined(scopes: dict[str, Any]) -> bool: + role = scopes.get("role") + permissions = scopes.get("permissions") + return (isinstance(role, str) and role.strip() != "") or ( + isinstance(permissions, list) and len(permissions) > 0 + ) + + +def _get_jwks(settings: Settings) -> dict[str, Any]: + if not settings.oidc_jwks_url: + raise JWTError("jwks url not configured") + + now = time.time() + with _jwks_lock: + cached = _jwks_cache.get(settings.oidc_jwks_url) + if cached is not None and cached[0] > now: + record_jwks_fetch(outcome="cache_hit") + return cached[1] + blocked_until = _jwks_backoff_until.get(settings.oidc_jwks_url, 0.0) + if blocked_until > now: + record_jwks_fetch(outcome="backoff") + raise JWTError("jwks fetch in backoff window") + + import httpx + + try: + response = httpx.get(settings.oidc_jwks_url, timeout=5.0) + response.raise_for_status() + payload = response.json() + if not isinstance(payload, dict): + raise JWTError("invalid jwks payload") + with _jwks_lock: + _jwks_cache[settings.oidc_jwks_url] = (now + settings.jwks_cache_ttl_seconds, payload) + _jwks_backoff_until.pop(settings.oidc_jwks_url, None) + record_jwks_fetch(outcome="success") + return payload + except Exception as exc: + with _jwks_lock: + previous = _jwks_backoff_until.get(settings.oidc_jwks_url, now) + next_backoff = min( + max(1.0, (previous - now) * 2.0 if previous > now else 1.0), + float(settings.jwks_backoff_max_seconds), + ) + _jwks_backoff_until[settings.oidc_jwks_url] = now + next_backoff + record_jwks_fetch(outcome="failure") + raise JWTError("jwks fetch failed") from exc + + def get_principal( request: Request, settings: Settings = Depends(get_settings), @@ -63,11 +119,30 @@ def get_principal( x_api_key: str | None = Security(api_key_scheme), ) -> dict[str, Any]: api_key_hashes = settings.parsed_api_key_hashes() + parsed_scopes = settings.parsed_api_key_scopes() if x_api_key: + key_hash = hashlib.sha256(x_api_key.encode("utf-8")).hexdigest() if _matches_api_key(x_api_key, api_key_hashes): + scopes = parsed_scopes.get(key_hash, {}) + has_explicit_scope_for_key = key_hash in parsed_scopes + if not _scopes_are_defined(scopes): + _log_failed_auth( + request, + reason="api_key_missing_scope", + api_key=x_api_key, + ) + if settings.is_development() and not has_explicit_scope_for_key: + scopes = { + "tenant": DEFAULT_TENANT_KEY, + "role": "admin", + "permissions": ["*"], + } + if not settings.is_development(): + raise _unauthorized("api key scopes must include role or permissions") return { "type": "api_key", - "id": hashlib.sha256(x_api_key.encode("utf-8")).hexdigest()[:12], + "id": key_hash[:12], + "scopes": scopes, } _log_failed_auth(request, reason="invalid_api_key", api_key=x_api_key) raise _unauthorized("invalid api key") @@ -76,11 +151,11 @@ def get_principal( token = authorization.credentials.strip() try: if settings.oidc_jwks_url: - import httpx - - jwks = httpx.get(settings.oidc_jwks_url, timeout=5.0).json() payload = _decode_with_jwks( - token, jwks, settings.oidc_audience, settings.oidc_issuer + token, + _get_jwks(settings), + settings.oidc_audience, + settings.oidc_issuer, ) else: payload = jwt.decode( diff --git a/keynetra/config/settings.py b/keynetra/config/settings.py index 7228b41..e486c02 100644 --- a/keynetra/config/settings.py +++ b/keynetra/config/settings.py @@ -5,11 +5,14 @@ from functools import lru_cache from typing import Any -from pydantic import Field +from pydantic import Field, field_validator, model_validator from pydantic_settings import BaseSettings, SettingsConfigDict from keynetra.config.policies import DEFAULT_POLICIES +_DEV_ENVIRONMENTS = {"development", "dev", "local"} +_VALID_ENVIRONMENTS = _DEV_ENVIRONMENTS | {"ci", "prod", "production"} + class Settings(BaseSettings): model_config = SettingsConfigDict(env_prefix="KEYNETRA_", extra="ignore", populate_by_name=True) @@ -24,10 +27,12 @@ class Settings(BaseSettings): api_keys: str | None = Field(default=None) api_key_hashes: str | None = Field(default=None) + api_key_scopes_json: str | None = Field(default=None) jwt_secret: str = Field(default="change-me") jwt_algorithm: str = Field(default="HS256") admin_username: str | None = Field(default=None) admin_password: str | None = Field(default=None) + admin_password_hash: str | None = Field(default=None) admin_token_expiry_minutes: int = Field(default=60) cors_allow_origins: str | None = Field(default="http://localhost:5173,http://127.0.0.1:5173") @@ -53,6 +58,8 @@ class Settings(BaseSettings): auto_seed_sample_data: bool = Field(default=False) server_host: str = Field(default="0.0.0.0") server_port: int = Field(default=8000) + async_authorization_enabled: bool = Field(default=False) + strict_tenancy: bool = Field(default=False) # Policy distribution policy_events_channel: str = Field(default="keynetra:policy_events") @@ -61,6 +68,96 @@ class Settings(BaseSettings): oidc_jwks_url: str | None = Field(default=None) oidc_audience: str | None = Field(default=None) oidc_issuer: str | None = Field(default=None) + jwks_cache_ttl_seconds: int = Field(default=300) + jwks_backoff_max_seconds: int = Field(default=60) + + @field_validator("environment") + @classmethod + def _validate_environment(cls, value: str) -> str: + normalized = str(value or "").strip().lower() + if normalized not in _VALID_ENVIRONMENTS: + raise ValueError("environment must be one of: development, dev, local, ci, prod") + return normalized + + @field_validator("service_timeout_seconds") + @classmethod + def _validate_service_timeout(cls, value: float) -> float: + if value < 0.05 or value > 120: + raise ValueError("service_timeout_seconds must be between 0.05 and 120") + return value + + @field_validator("critical_retry_attempts") + @classmethod + def _validate_retry_attempts(cls, value: int) -> int: + if value < 1 or value > 10: + raise ValueError("critical_retry_attempts must be between 1 and 10") + return value + + @field_validator("rate_limit_per_minute") + @classmethod + def _validate_rate_limit_per_minute(cls, value: int) -> int: + if value < 1 or value > 1_000_000: + raise ValueError("rate_limit_per_minute must be between 1 and 1000000") + return value + + @field_validator("rate_limit_window_seconds") + @classmethod + def _validate_rate_limit_window_seconds(cls, value: int) -> int: + if value < 1 or value > 3600: + raise ValueError("rate_limit_window_seconds must be between 1 and 3600") + return value + + @field_validator("rate_limit_burst") + @classmethod + def _validate_rate_limit_burst(cls, value: int | None) -> int | None: + if value is None: + return value + if value < 1 or value > 1_000_000: + raise ValueError("rate_limit_burst must be between 1 and 1000000") + return value + + @field_validator("jwks_cache_ttl_seconds") + @classmethod + def _validate_jwks_cache_ttl_seconds(cls, value: int) -> int: + if value < 10 or value > 86400: + raise ValueError("jwks_cache_ttl_seconds must be between 10 and 86400") + return value + + @field_validator("jwks_backoff_max_seconds") + @classmethod + def _validate_jwks_backoff_max_seconds(cls, value: int) -> int: + if value < 1 or value > 3600: + raise ValueError("jwks_backoff_max_seconds must be between 1 and 3600") + return value + + @model_validator(mode="after") + def _validate_security_profile(self) -> Settings: + auth_enabled = ( + bool(self.parsed_api_key_hashes()) + or bool(self.oidc_jwks_url) + or (bool(self.jwt_secret) and self.jwt_secret.strip() != "change-me") + ) + non_dev = self.environment not in _DEV_ENVIRONMENTS + if non_dev and not auth_enabled: + raise ValueError( + "configure at least one auth method: api_keys/api_key_hashes or jwt/oidc" + ) + if self.environment == "prod" and self.jwt_secret.strip() == "change-me": + raise ValueError("rejecting weak KEYNETRA_JWT_SECRET=change-me outside development") + if non_dev and self.admin_password and not self.admin_password_hash: + raise ValueError( + "admin_password is not allowed outside development; use KEYNETRA_ADMIN_PASSWORD_HASH" + ) + if non_dev and self.admin_username and self.admin_username.strip().lower() == "admin": + raise ValueError("rejecting default admin username outside development") + + db_url = self.database_url.strip().lower() + if self.environment == "prod" and "sqlite" in db_url: + raise ValueError("sqlite is not allowed in production mode") + + if self.redis_url is not None and not self.redis_url.strip(): + raise ValueError("redis_url cannot be blank when provided") + return self def load_policies(self) -> list[dict[str, Any]]: if not self.policies_json: @@ -103,6 +200,34 @@ def parsed_api_key_hashes(self) -> set[str]: return {value.strip() for value in self.api_key_hashes.split(",") if value.strip()} return {hashlib.sha256(key.encode("utf-8")).hexdigest() for key in self.parsed_api_keys()} + def parsed_api_key_scopes(self) -> dict[str, dict[str, Any]]: + if not self.api_key_scopes_json: + return {} + try: + decoded = json.loads(self.api_key_scopes_json) + except json.JSONDecodeError: + return {} + if not isinstance(decoded, dict): + return {} + parsed: dict[str, dict[str, Any]] = {} + for key, scopes in decoded.items(): + if not isinstance(scopes, dict): + continue + key_hash = str(key).strip() + if len(key_hash) != 64: + key_hash = hashlib.sha256(key_hash.encode("utf-8")).hexdigest() + parsed[key_hash] = { + "tenant": scopes.get("tenant"), + "role": scopes.get("role"), + "permissions": ( + scopes.get("permissions") if isinstance(scopes.get("permissions"), list) else [] + ), + } + return parsed + + def is_development(self) -> bool: + return self.environment in _DEV_ENVIRONMENTS + def parsed_cors_allow_origins(self) -> list[str]: if not self.cors_allow_origins: return [] diff --git a/keynetra/config/tenancy.py b/keynetra/config/tenancy.py index 2f6a603..247a6de 100644 --- a/keynetra/config/tenancy.py +++ b/keynetra/config/tenancy.py @@ -1,7 +1,58 @@ from __future__ import annotations +import re +from typing import Any + DEFAULT_TENANT_KEY = "default" +TENANT_HEADER_NAME = "X-Tenant-Id" +_TENANT_PATTERN = re.compile(r"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$") def get_tenant_key() -> str: return DEFAULT_TENANT_KEY + + +def normalize_tenant_key(value: str | None) -> str | None: + if value is None: + return None + normalized = value.strip() + if not normalized: + return None + if not _TENANT_PATTERN.fullmatch(normalized): + return None + return normalized + + +def tenant_from_principal(principal: dict[str, Any]) -> str | None: + if principal.get("type") == "api_key": + scopes = principal.get("scopes") + if isinstance(scopes, dict): + tenant = normalize_tenant_key(str(scopes.get("tenant") or "")) + if tenant: + return tenant + return None + + claims = principal.get("claims") + if not isinstance(claims, dict): + return None + + for key in ("tenant", "tenant_id", "tenant_key"): + tenant = claims.get(key) + if isinstance(tenant, str): + normalized = normalize_tenant_key(tenant) + if normalized: + return normalized + + tenant_roles = claims.get("tenant_roles") + if isinstance(tenant_roles, dict): + candidates = [normalize_tenant_key(str(item)) for item in tenant_roles] + normalized = [item for item in candidates if item] + if len(normalized) == 1: + return normalized[0] + + return None + + +def tenant_for_logs(request: Any) -> str | None: + state = getattr(request, "state", None) + return normalize_tenant_key(getattr(state, "requested_tenant_key", None)) or "unknown" diff --git a/keynetra/domain/models/audit.py b/keynetra/domain/models/audit.py index eb689ce..7465bfa 100644 --- a/keynetra/domain/models/audit.py +++ b/keynetra/domain/models/audit.py @@ -16,6 +16,7 @@ class AuditLog(Base): principal_type: Mapped[str] = mapped_column(String(32), nullable=False) principal_id: Mapped[str] = mapped_column(String(128), nullable=False) + correlation_id: Mapped[str | None] = mapped_column(String(128), nullable=True) user: Mapped[dict] = mapped_column(JSON, nullable=False, default=dict) action: Mapped[str] = mapped_column(String(128), nullable=False) diff --git a/keynetra/domain/models/policy_versioning.py b/keynetra/domain/models/policy_versioning.py index aa7969e..9cf4270 100644 --- a/keynetra/domain/models/policy_versioning.py +++ b/keynetra/domain/models/policy_versioning.py @@ -39,6 +39,7 @@ class PolicyVersion(Base): DateTime(timezone=True), nullable=False, default=datetime.utcnow ) created_by: Mapped[str | None] = mapped_column(String(128), nullable=True) + state: Mapped[str] = mapped_column(String(16), nullable=False, default="active") __table_args__ = ( UniqueConstraint("policy_id", "version", name="uq_policy_versions_policy_version"), diff --git a/keynetra/domain/models/rbac.py b/keynetra/domain/models/rbac.py index bc1ec7d..0885ac1 100644 --- a/keynetra/domain/models/rbac.py +++ b/keynetra/domain/models/rbac.py @@ -28,7 +28,7 @@ class User(Base): id: Mapped[int] = mapped_column(primary_key=True) external_id: Mapped[str | None] = mapped_column(String(128), nullable=True) - roles: Mapped[list["Role"]] = relationship(secondary=user_roles, back_populates="users") + roles: Mapped[list[Role]] = relationship(secondary=user_roles, back_populates="users") __table_args__ = (Index("ix_users_external_id", "external_id"),) @@ -40,7 +40,7 @@ class Role(Base): name: Mapped[str] = mapped_column(String(64), nullable=False, unique=True) users: Mapped[list[User]] = relationship(secondary=user_roles, back_populates="roles") - permissions: Mapped[list["Permission"]] = relationship( + permissions: Mapped[list[Permission]] = relationship( secondary=role_permissions, back_populates="roles" ) diff --git a/keynetra/domain/pagination.py b/keynetra/domain/pagination.py new file mode 100644 index 0000000..d6ffde4 --- /dev/null +++ b/keynetra/domain/pagination.py @@ -0,0 +1,22 @@ +"""Cursor pagination codec helpers.""" + +from __future__ import annotations + +import base64 +import json +from typing import Any + + +def encode_cursor(payload: dict[str, Any]) -> str: + raw = json.dumps(payload, separators=(",", ":"), sort_keys=True).encode("utf-8") + return base64.urlsafe_b64encode(raw).decode("ascii") + + +def decode_cursor(cursor: str | None) -> dict[str, Any] | None: + if not cursor: + return None + raw = base64.urlsafe_b64decode(cursor.encode("ascii")) + decoded = json.loads(raw.decode("utf-8")) + if not isinstance(decoded, dict): + raise ValueError("invalid cursor payload") + return decoded diff --git a/keynetra/domain/schemas/management.py b/keynetra/domain/schemas/management.py index 0aa1848..cac90f0 100644 --- a/keynetra/domain/schemas/management.py +++ b/keynetra/domain/schemas/management.py @@ -41,6 +41,7 @@ class PolicyCreate(BaseModel): action: str effect: str = "allow" priority: int = 100 + state: str = "active" conditions: dict[str, Any] = Field(default_factory=dict) @@ -49,6 +50,7 @@ class PolicyOut(BaseModel): action: str effect: str priority: int + state: str = "active" conditions: dict[str, Any] @@ -71,6 +73,7 @@ class AuditRecordOut(BaseModel): id: int principal_type: str principal_id: str + correlation_id: str | None = None user: dict[str, Any] action: str resource: dict[str, Any] diff --git a/keynetra/engine/compiled/decision_graph.py b/keynetra/engine/compiled/decision_graph.py index fcb429c..269d749 100644 --- a/keynetra/engine/compiled/decision_graph.py +++ b/keynetra/engine/compiled/decision_graph.py @@ -2,9 +2,10 @@ from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass, field from threading import RLock -from typing import Any, Callable +from typing import Any @dataclass(frozen=True) diff --git a/keynetra/engine/keynetra_engine.py b/keynetra/engine/keynetra_engine.py index 531935f..0ba772e 100644 --- a/keynetra/engine/keynetra_engine.py +++ b/keynetra/engine/keynetra_engine.py @@ -8,9 +8,10 @@ from __future__ import annotations import time +from collections.abc import Callable from dataclasses import dataclass, field from datetime import datetime -from typing import Any, Callable, Literal +from typing import Any, Literal from keynetra.engine.compiled.decision_graph import DecisionGraph from keynetra.engine.compiled.policy_compiler import compile_policy_graph @@ -53,7 +54,7 @@ class PolicyDefinition: policy_id: str | None = None @staticmethod - def from_dict(raw: dict[str, Any]) -> "PolicyDefinition": + def from_dict(raw: dict[str, Any]) -> PolicyDefinition: return PolicyDefinition( action=str(raw.get("action", "")), effect="allow" if str(raw.get("effect", "deny")) == "allow" else "deny", diff --git a/keynetra/engine/model_graph/graph_executor.py b/keynetra/engine/model_graph/graph_executor.py index 2ba8d80..b801840 100644 --- a/keynetra/engine/model_graph/graph_executor.py +++ b/keynetra/engine/model_graph/graph_executor.py @@ -2,9 +2,7 @@ from __future__ import annotations -from keynetra.engine.model_graph.permission_graph import ( - CompiledPermissionGraph, -) +from keynetra.engine.model_graph.permission_graph import CompiledPermissionGraph def execute_permission_graph(graph: CompiledPermissionGraph, authorization_input): diff --git a/keynetra/headless.py b/keynetra/headless.py index 8b02a79..a6c07bc 100644 --- a/keynetra/headless.py +++ b/keynetra/headless.py @@ -28,7 +28,7 @@ class KeyNetra: _permission_graph: CompiledPermissionGraph | None = None @classmethod - def from_config(cls, path: str | Path) -> "KeyNetra": + def from_config(cls, path: str | Path) -> KeyNetra: config = load_config_file(path) policies = load_policies_from_paths(list(config.policy_paths)) or list(DEFAULT_POLICIES) engine = cls(_engine=KeyNetraEngine(policies)) diff --git a/keynetra/infrastructure/cache/backends.py b/keynetra/infrastructure/cache/backends.py index adfca3a..417dd88 100644 --- a/keynetra/infrastructure/cache/backends.py +++ b/keynetra/infrastructure/cache/backends.py @@ -6,9 +6,14 @@ from __future__ import annotations +import logging import time from typing import Any, Protocol +from keynetra.infrastructure.logging import log_event + +_logger = logging.getLogger("keynetra.cache") + class CacheBackend(Protocol): """Minimal key/value backend required by cache adapters.""" @@ -61,7 +66,8 @@ def __init__(self, client: Any) -> None: def get(self, key: str) -> str | None: try: value = self._client.get(key) - except Exception: + except (ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event(_logger, event="cache_backend_get_failed", key=key, reason=repr(exc)) return None if value is None: return None @@ -73,19 +79,22 @@ def set(self, key: str, value: str, ttl_seconds: int | None = None) -> None: self._client.set(key, value) else: self._client.setex(key, max(1, ttl_seconds), value) - except Exception: + except (ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event(_logger, event="cache_backend_set_failed", key=key, reason=repr(exc)) return def delete(self, key: str) -> None: try: self._client.delete(key) - except Exception: + except (ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event(_logger, event="cache_backend_delete_failed", key=key, reason=repr(exc)) return def incr(self, key: str) -> int: try: return int(self._client.incr(key)) - except Exception: + except (ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event(_logger, event="cache_backend_incr_failed", key=key, reason=repr(exc)) return 0 diff --git a/keynetra/infrastructure/cache/policy_distribution.py b/keynetra/infrastructure/cache/policy_distribution.py index 8bd04a8..cf19422 100644 --- a/keynetra/infrastructure/cache/policy_distribution.py +++ b/keynetra/infrastructure/cache/policy_distribution.py @@ -1,12 +1,16 @@ from __future__ import annotations import json +import logging from dataclasses import dataclass from keynetra.config.redis_client import get_redis from keynetra.config.settings import Settings +from keynetra.infrastructure.logging import log_event from keynetra.services.interfaces import PolicyEventPublisher +_logger = logging.getLogger("keynetra.policy_distribution") + @dataclass(frozen=True) class PolicyUpdateEvent: @@ -23,7 +27,14 @@ def publish_policy_update(settings: Settings, event: PolicyUpdateEvent) -> None: return try: r.publish(settings.policy_events_channel, event.to_json()) - except Exception: + except (ConnectionError, OSError, RuntimeError, ValueError) as exc: + log_event( + _logger, + event="policy_distribution_publish_failed", + tenant_key=event.tenant_key, + policy_version=event.policy_version, + reason=repr(exc), + ) return diff --git a/keynetra/infrastructure/cache/user_cache.py b/keynetra/infrastructure/cache/user_cache.py index 55760d7..0b958a7 100644 --- a/keynetra/infrastructure/cache/user_cache.py +++ b/keynetra/infrastructure/cache/user_cache.py @@ -1,9 +1,14 @@ from __future__ import annotations import json +import logging from typing import Any from keynetra.config.redis_client import get_redis +from keynetra.infrastructure.logging import log_event +from keynetra.infrastructure.metrics import record_cache_event + +_logger = logging.getLogger("keynetra.cache.user") def get_cached_user_context(key: str) -> dict[str, Any] | None: @@ -12,13 +17,26 @@ def get_cached_user_context(key: str) -> dict[str, Any] | None: return None try: raw = r.get(key) - except Exception: + except (ConnectionError, RuntimeError, ValueError, TypeError) as exc: + record_cache_event(cache_name="relationship", outcome="fallback") + log_event( + _logger, + event="user_cache_fetch_failed", + key=key, + reason=repr(exc), + ) return None if not raw: return None try: decoded = json.loads(raw) - except Exception: + except (TypeError, ValueError) as exc: + log_event( + _logger, + event="user_cache_decode_failed", + key=key, + reason=repr(exc), + ) return None return decoded if isinstance(decoded, dict) else None @@ -29,5 +47,12 @@ def set_cached_user_context(key: str, ctx: dict[str, Any], ttl_seconds: int) -> return try: r.setex(key, max(1, ttl_seconds), json.dumps(ctx, separators=(",", ":"), sort_keys=True)) - except Exception: + except (ConnectionError, RuntimeError, ValueError, TypeError) as exc: + record_cache_event(cache_name="relationship", outcome="fallback") + log_event( + _logger, + event="user_cache_store_failed", + key=key, + reason=repr(exc), + ) return diff --git a/keynetra/infrastructure/errors.py b/keynetra/infrastructure/errors.py new file mode 100644 index 0000000..62a80dd --- /dev/null +++ b/keynetra/infrastructure/errors.py @@ -0,0 +1,13 @@ +from __future__ import annotations + + +class KeyNetraError(Exception): + """Base class for classified service errors.""" + + +class BootstrapError(KeyNetraError): + """Raised when startup/bootstrap fails and service must fail-fast.""" + + +class ConfigurationError(KeyNetraError): + """Raised for invalid runtime configuration.""" diff --git a/keynetra/infrastructure/logging.py b/keynetra/infrastructure/logging.py index ba8e684..7489bd0 100644 --- a/keynetra/infrastructure/logging.py +++ b/keynetra/infrastructure/logging.py @@ -5,9 +5,15 @@ import json import logging import os -from datetime import datetime, timezone +from contextvars import ContextVar, Token +from datetime import UTC, datetime from typing import Any +_correlation_id_ctx: ContextVar[str | None] = ContextVar( + "keynetra_correlation_id", + default=None, +) + class JsonLogFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: @@ -16,7 +22,7 @@ def format(self, record: logging.LogRecord) -> str: payload = dict(record.msg) else: payload = {"message": record.getMessage()} - payload.setdefault("timestamp", datetime.now(timezone.utc).isoformat()) + payload.setdefault("timestamp", datetime.now(UTC).isoformat()) payload.setdefault("level", record.levelname) payload.setdefault("logger", record.name) return json.dumps(payload, default=str) @@ -70,4 +76,18 @@ def configure_rich_logging() -> None: def log_event(logger: logging.Logger, *, event: str, **fields: Any) -> None: - logger.info({"event": event, **fields}) + payload = {"event": event, **fields} + payload.setdefault("correlation_id", get_correlation_id()) + logger.info(payload) + + +def set_correlation_id(correlation_id: str | None) -> Token[str | None]: + return _correlation_id_ctx.set(correlation_id) + + +def reset_correlation_id(token: Token[str | None]) -> None: + _correlation_id_ctx.reset(token) + + +def get_correlation_id() -> str | None: + return _correlation_id_ctx.get() diff --git a/keynetra/infrastructure/repositories/audit.py b/keynetra/infrastructure/repositories/audit.py index d56201a..6ce1c6a 100644 --- a/keynetra/infrastructure/repositories/audit.py +++ b/keynetra/infrastructure/repositories/audit.py @@ -7,8 +7,8 @@ from sqlalchemy import String, and_, desc, func, or_, select from sqlalchemy.orm import Session -from keynetra.api.pagination import encode_cursor from keynetra.domain.models.audit import AuditLog +from keynetra.domain.pagination import encode_cursor from keynetra.engine.keynetra_engine import AuthorizationDecision, AuthorizationInput from keynetra.services.interfaces import AuditListItem @@ -27,11 +27,13 @@ def write( principal_id: str, authorization_input: AuthorizationInput, decision: AuthorizationDecision, + correlation_id: str | None = None, ) -> None: row = AuditLog( tenant_id=tenant_id, principal_type=principal_type, principal_id=principal_id, + correlation_id=correlation_id, user=authorization_input.user, action=authorization_input.action, resource=authorization_input.resource, @@ -110,6 +112,7 @@ def _to_item(row: AuditLog) -> AuditListItem: id=row.id, principal_type=row.principal_type, principal_id=row.principal_id, + correlation_id=row.correlation_id, user=row.user, action=row.action, resource=row.resource, diff --git a/keynetra/infrastructure/repositories/policies.py b/keynetra/infrastructure/repositories/policies.py index 030fcd9..9c76c87 100644 --- a/keynetra/infrastructure/repositories/policies.py +++ b/keynetra/infrastructure/repositories/policies.py @@ -2,13 +2,15 @@ from __future__ import annotations +import json from typing import Any -from sqlalchemy import and_, delete, or_, select +from sqlalchemy import and_, delete, or_, select, text +from sqlalchemy.exc import OperationalError from sqlalchemy.orm import Session -from keynetra.api.pagination import encode_cursor from keynetra.domain.models.policy_versioning import Policy, PolicyVersion +from keynetra.domain.pagination import encode_cursor from keynetra.engine.keynetra_engine import PolicyDefinition from keynetra.services.interfaces import PolicyListItem, PolicyMutationResult, PolicyRecord @@ -19,35 +21,79 @@ class SqlPolicyRepository: def __init__(self, session: Session) -> None: self._session = session - def list_current_policies(self, *, tenant_id: int) -> list[PolicyRecord]: - rows = self._current_policy_rows(tenant_id=tenant_id) - return [ - PolicyRecord( - id=version.id, - definition=PolicyDefinition( - action=version.action, - effect="allow" if version.effect == "allow" else "deny", - priority=version.priority, - conditions=dict(version.conditions or {}), - policy_id=f"{policy.policy_key}:v{version.version}", - ), + def list_current_policies( + self, *, tenant_id: int, policy_set: str = "active" + ) -> list[PolicyRecord]: + try: + rows = self._current_policy_rows(tenant_id=tenant_id, policy_set=policy_set) + except OperationalError: + rows = self._legacy_current_policy_rows(tenant_id=tenant_id) + records: list[PolicyRecord] = [] + for row in rows: + if isinstance(row, dict): + records.append( + PolicyRecord( + id=int(row["id"]), + definition=PolicyDefinition( + action=str(row["action"]), + effect="allow" if str(row["effect"]) == "allow" else "deny", + priority=int(row["priority"]), + conditions=dict(row["conditions"] or {}), + policy_id=f'{row["policy_key"]}:v{row["version"]}', + ), + ) + ) + continue + version, policy = row + records.append( + PolicyRecord( + id=version.id, + definition=PolicyDefinition( + action=version.action, + effect="allow" if version.effect == "allow" else "deny", + priority=version.priority, + conditions=dict(version.conditions or {}), + policy_id=f"{policy.policy_key}:v{version.version}", + ), + ) ) - for version, policy in rows - ] + return records - def list_current_policy_views(self, *, tenant_id: int) -> list[PolicyListItem]: - rows = self._current_policy_rows(tenant_id=tenant_id) - return [ - PolicyListItem( - id=version.id, - action=version.action, - effect=version.effect, - priority=version.priority, - conditions=(version.conditions or {}) - | {"policy_key": policy.policy_key, "version": version.version}, + def list_current_policy_views( + self, *, tenant_id: int, policy_set: str = "active" + ) -> list[PolicyListItem]: + try: + rows = self._current_policy_rows(tenant_id=tenant_id, policy_set=policy_set) + except OperationalError: + rows = self._legacy_current_policy_rows(tenant_id=tenant_id) + items: list[PolicyListItem] = [] + for row in rows: + if isinstance(row, dict): + items.append( + PolicyListItem( + id=int(row["id"]), + action=str(row["action"]), + effect=str(row["effect"]), + priority=int(row["priority"]), + state=str(row["state"]), + conditions=dict(row["conditions"] or {}) + | {"policy_key": row["policy_key"], "version": row["version"]}, + ) + ) + continue + version, policy = row + items.append( + PolicyListItem( + id=version.id, + action=version.action, + effect=version.effect, + priority=version.priority, + state=version.state, + conditions=(version.conditions or {}) + | {"policy_key": policy.policy_key, "version": version.version}, + ) ) - for version, policy in rows - ] + return items def list_current_policy_page( self, @@ -84,6 +130,7 @@ def list_current_policy_page( action=version.action, effect=version.effect, priority=version.priority, + state=version.state, conditions=(version.conditions or {}) | {"policy_key": policy.policy_key, "version": version.version}, ) @@ -105,6 +152,7 @@ def create_policy_version( priority: int, conditions: dict[str, Any], created_by: str | None, + state: str = "active", ) -> PolicyMutationResult: policy = ( self._session.execute( @@ -133,15 +181,33 @@ def create_policy_version( priority=priority, conditions=conditions, created_by=created_by, + state=state, ) self._session.add(policy_version) - self._session.commit() + try: + self._session.commit() + except OperationalError: + # Backward compatibility with pre-state schema versions. + self._session.rollback() + policy_version = PolicyVersion( + tenant_id=tenant_id, + policy_id=policy.id, + version=next_version, + action=action, + effect=effect, + priority=priority, + conditions=conditions, + created_by=created_by, + ) + self._session.add(policy_version) + self._session.commit() self._session.refresh(policy_version) return PolicyMutationResult( id=policy_version.id, action=policy_version.action, effect=policy_version.effect, priority=policy_version.priority, + state=policy_version.state, conditions=dict(policy_version.conditions or {}), ) @@ -194,12 +260,57 @@ def delete_policy(self, *, tenant_id: int, policy_key: str) -> None: self._session.execute(delete(Policy).where(Policy.id == policy.id)) self._session.commit() - def _current_policy_rows(self, *, tenant_id: int) -> list[tuple[PolicyVersion, Policy]]: - return self._session.execute( + def _current_policy_rows( + self, *, tenant_id: int, policy_set: str = "active" + ) -> list[tuple[PolicyVersion, Policy]]: + normalized_set = str(policy_set or "active").strip().lower() + query = ( select(PolicyVersion, Policy) .join(Policy, Policy.id == PolicyVersion.policy_id) .where(Policy.tenant_id == tenant_id) .where(PolicyVersion.tenant_id == tenant_id) .where(PolicyVersion.version == Policy.current_version) - .order_by(PolicyVersion.priority.asc(), PolicyVersion.id.asc()) + ) + if normalized_set in {"draft", "archived", "active"}: + query = query.where(PolicyVersion.state == normalized_set) + else: + query = query.where(PolicyVersion.state == "active") + return self._session.execute( + query.order_by(PolicyVersion.priority.asc(), PolicyVersion.id.asc()) ).all() + + def _legacy_current_policy_rows(self, *, tenant_id: int) -> list[dict[str, Any]]: + rows = self._session.execute( + text(""" + SELECT pv.id AS id, pv.action AS action, pv.effect AS effect, pv.priority AS priority, + pv.conditions AS conditions, pv.version AS version, p.policy_key AS policy_key + FROM policy_versions pv + JOIN policies p ON p.id = pv.policy_id + WHERE p.tenant_id = :tenant_id + AND pv.tenant_id = :tenant_id + AND pv.version = p.current_version + ORDER BY pv.priority ASC, pv.id ASC + """), + {"tenant_id": tenant_id}, + ).mappings() + normalized: list[dict[str, Any]] = [] + for row in rows: + conditions = row.get("conditions") + if isinstance(conditions, str): + try: + conditions = json.loads(conditions) + except json.JSONDecodeError: + conditions = {} + normalized.append( + { + "id": int(row["id"]), + "action": str(row["action"]), + "effect": str(row["effect"]), + "priority": int(row["priority"]), + "conditions": conditions if isinstance(conditions, dict) else {}, + "version": int(row["version"]), + "policy_key": str(row["policy_key"]), + "state": "active", + } + ) + return normalized diff --git a/keynetra/infrastructure/repositories/relationships.py b/keynetra/infrastructure/repositories/relationships.py index 835ed2c..8bb0d87 100644 --- a/keynetra/infrastructure/repositories/relationships.py +++ b/keynetra/infrastructure/repositories/relationships.py @@ -5,8 +5,8 @@ from sqlalchemy import and_, or_, select from sqlalchemy.orm import Session -from keynetra.api.pagination import encode_cursor from keynetra.domain.models.relationship import Relationship +from keynetra.domain.pagination import encode_cursor from keynetra.services.interfaces import RelationshipRecord @@ -149,6 +149,82 @@ def list_for_object( for row in rows ] + def list_for_subjects( + self, *, tenant_id: int, subject_type: str, subject_ids: list[str] + ) -> dict[str, list[RelationshipRecord]]: + if not subject_ids: + return {} + rows = ( + self._session.execute( + select(Relationship) + .where(Relationship.tenant_id == tenant_id) + .where(Relationship.subject_type == subject_type) + .where(Relationship.subject_id.in_(subject_ids)) + .order_by( + Relationship.subject_id.asc(), + Relationship.relation.asc(), + Relationship.object_type.asc(), + Relationship.object_id.asc(), + Relationship.id.asc(), + ) + ) + .scalars() + .all() + ) + grouped: dict[str, list[RelationshipRecord]] = { + subject_id: [] for subject_id in subject_ids + } + for row in rows: + grouped.setdefault(row.subject_id, []).append( + RelationshipRecord( + subject_type=row.subject_type, + subject_id=row.subject_id, + relation=row.relation, + object_type=row.object_type, + object_id=row.object_id, + ) + ) + return grouped + + def list_for_objects( + self, *, tenant_id: int, objects: list[tuple[str, str]] + ) -> dict[tuple[str, str], list[RelationshipRecord]]: + if not objects: + return {} + clauses = [ + and_(Relationship.object_type == object_type, Relationship.object_id == object_id) + for object_type, object_id in objects + ] + rows = ( + self._session.execute( + select(Relationship) + .where(Relationship.tenant_id == tenant_id) + .where(or_(*clauses)) + .order_by( + Relationship.object_type.asc(), + Relationship.object_id.asc(), + Relationship.subject_type.asc(), + Relationship.subject_id.asc(), + Relationship.relation.asc(), + Relationship.id.asc(), + ) + ) + .scalars() + .all() + ) + grouped: dict[tuple[str, str], list[RelationshipRecord]] = {obj: [] for obj in objects} + for row in rows: + grouped.setdefault((row.object_type, row.object_id), []).append( + RelationshipRecord( + subject_type=row.subject_type, + subject_id=row.subject_id, + relation=row.relation, + object_type=row.object_type, + object_id=row.object_id, + ) + ) + return grouped + def create( self, *, diff --git a/keynetra/infrastructure/repositories/users.py b/keynetra/infrastructure/repositories/users.py index cc2d772..eacd9a9 100644 --- a/keynetra/infrastructure/repositories/users.py +++ b/keynetra/infrastructure/repositories/users.py @@ -45,3 +45,31 @@ def get_user_context(self, user_id: int) -> dict[str, Any] | None: def list_user_ids(self, *, tenant_id: int) -> list[int]: rows = self._session.execute(select(User.id).order_by(User.id.asc())).scalars().all() return [int(row) for row in rows] + + def get_user_contexts(self, user_ids: list[int]) -> dict[int, dict[str, Any]]: + if not user_ids: + return {} + users = ( + self._session.execute( + select(User) + .where(User.id.in_(user_ids)) + .options(joinedload(User.roles).joinedload(Role.permissions)) + ) + .scalars() + .all() + ) + contexts: dict[int, dict[str, Any]] = {} + for user in users: + permissions: set[str] = set() + roles: set[str] = set() + for role in user.roles: + roles.add(role.name) + for permission in role.permissions: + permissions.add(permission.action) + contexts[int(user.id)] = { + "id": user.id, + "role": next(iter(sorted(roles)), None), + "roles": sorted(roles), + "permissions": sorted(permissions), + } + return contexts diff --git a/keynetra/infrastructure/storage/session.py b/keynetra/infrastructure/storage/session.py index 5305fb0..7e42142 100644 --- a/keynetra/infrastructure/storage/session.py +++ b/keynetra/infrastructure/storage/session.py @@ -2,13 +2,41 @@ from collections.abc import Generator from functools import lru_cache +from time import perf_counter from sqlalchemy import create_engine from sqlalchemy.engine import Engine +from sqlalchemy.event import listens_for from sqlalchemy.orm import Session, sessionmaker from sqlalchemy.pool import StaticPool from keynetra.config.settings import get_settings +from keynetra.observability.metrics import observe_db_query_latency + + +def _operation_name(statement: str) -> str: + first = (statement or "").strip().split(" ", 1)[0].upper() + return first if first else "UNKNOWN" + + +@listens_for(Engine, "before_cursor_execute") +def _before_cursor_execute( # pragma: no cover - sqlalchemy runtime hook + conn, cursor, statement, parameters, context, executemany +) -> None: + conn.info.setdefault("_query_start_times", []).append(perf_counter()) + + +@listens_for(Engine, "after_cursor_execute") +def _after_cursor_execute( # pragma: no cover - sqlalchemy runtime hook + conn, cursor, statement, parameters, context, executemany +) -> None: + starts = conn.info.get("_query_start_times", []) + if not starts: + return + started_at = starts.pop() + observe_db_query_latency( + operation=_operation_name(statement), value=perf_counter() - started_at + ) @lru_cache diff --git a/keynetra/migrations.py b/keynetra/migrations.py index 85dac85..44d52cd 100644 --- a/keynetra/migrations.py +++ b/keynetra/migrations.py @@ -3,8 +3,8 @@ from __future__ import annotations import re +from collections.abc import Iterable from pathlib import Path -from typing import Iterable DROP_PATTERN = re.compile(r"\bdrop_(?:table|column)\b") REVISION_PATTERN = re.compile(r"^revision\s*=\s*['\"](?P[^'\"]+)['\"]", re.MULTILINE) diff --git a/keynetra/modeling/model_validator.py b/keynetra/modeling/model_validator.py index ee8a7fb..4954214 100644 --- a/keynetra/modeling/model_validator.py +++ b/keynetra/modeling/model_validator.py @@ -40,7 +40,7 @@ def _validate_expr(expr, schema: AuthorizationSchema) -> None: if isinstance(expr, NotExpr): _validate_expr(expr.value, schema) return - if isinstance(expr, AndExpr) or isinstance(expr, OrExpr): + if isinstance(expr, (AndExpr, OrExpr)): _validate_expr(expr.left, schema) _validate_expr(expr.right, schema) return diff --git a/keynetra/observability/__init__.py b/keynetra/observability/__init__.py index fa039cd..87709f7 100644 --- a/keynetra/observability/__init__.py +++ b/keynetra/observability/__init__.py @@ -4,6 +4,7 @@ observe_access_check_latency, observe_decision_latency, record_access_check, + record_access_index_rebuild, record_acl_match, record_api_error, record_cache_event, @@ -19,6 +20,7 @@ "observe_access_check_latency", "observe_decision_latency", "record_access_check", + "record_access_index_rebuild", "record_acl_match", "record_api_error", "record_cache_event", diff --git a/keynetra/observability/http_metrics.py b/keynetra/observability/http_metrics.py new file mode 100644 index 0000000..2a45d5a --- /dev/null +++ b/keynetra/observability/http_metrics.py @@ -0,0 +1,48 @@ +"""HTTP request metrics helpers.""" + +from __future__ import annotations + +try: + from prometheus_client import Counter, Histogram +except ModuleNotFoundError: # pragma: no cover + Counter = None # type: ignore[assignment] + Histogram = None # type: ignore[assignment] + +if Counter is not None and Histogram is not None: + HTTP_REQUESTS_TOTAL = Counter( + "keynetra_http_requests_total", + "HTTP request count", + labelnames=("tenant", "endpoint", "method", "status"), + ) + HTTP_REQUEST_DURATION_SECONDS = Histogram( + "keynetra_http_request_duration_seconds", + "HTTP request latency in seconds", + labelnames=("tenant", "endpoint", "method", "status"), + ) +else: # pragma: no cover + HTTP_REQUESTS_TOTAL = None + HTTP_REQUEST_DURATION_SECONDS = None + + +def record_http_request( + *, tenant: str, endpoint: str, method: str, status: int, duration_seconds: float +) -> None: + tenant_label = str(tenant or "unknown").strip() or "unknown" + endpoint_label = str(endpoint or "/").strip() or "/" + method_label = str(method or "GET").strip().upper() or "GET" + status_label = str(int(status)) + + if HTTP_REQUESTS_TOTAL is not None: + HTTP_REQUESTS_TOTAL.labels( + tenant=tenant_label, + endpoint=endpoint_label, + method=method_label, + status=status_label, + ).inc() + if HTTP_REQUEST_DURATION_SECONDS is not None: + HTTP_REQUEST_DURATION_SECONDS.labels( + tenant=tenant_label, + endpoint=endpoint_label, + method=method_label, + status=status_label, + ).observe(max(0.0, float(duration_seconds))) diff --git a/keynetra/observability/metrics.py b/keynetra/observability/metrics.py index 844259f..f9fe7be 100644 --- a/keynetra/observability/metrics.py +++ b/keynetra/observability/metrics.py @@ -69,6 +69,36 @@ "Core API error counts", labelnames=("code",), ) + BOOTSTRAP_FAILURES_TOTAL = Counter( + "keynetra_bootstrap_failures_total", + "Startup/bootstrap failure counts", + labelnames=("stage",), + ) + CACHE_FALLBACK_TOTAL = Counter( + "keynetra_cache_fallback_total", + "Cache fallback counts", + labelnames=("cache_name",), + ) + AUTH_FAILURES_TOTAL = Counter( + "keynetra_auth_failures_total", + "Authentication failure counts", + labelnames=("reason",), + ) + JWKS_FETCH_TOTAL = Counter( + "keynetra_jwks_fetch_total", + "JWKS fetch outcome counts", + labelnames=("outcome",), + ) + ACCESS_INDEX_REBUILDS_TOTAL = Counter( + "keynetra_access_index_rebuilds_total", + "Access index rebuild counts", + labelnames=("mode",), + ) + DB_QUERY_LATENCY_SECONDS = Histogram( + "keynetra_db_query_latency_seconds", + "Database query latency", + labelnames=("operation",), + ) else: # pragma: no cover ACCESS_CHECKS_TOTAL = None ACL_MATCHES_TOTAL = None @@ -82,6 +112,12 @@ DECISION_LATENCY_SECONDS = None CACHE_EVENTS_TOTAL = None API_ERRORS_TOTAL = None + BOOTSTRAP_FAILURES_TOTAL = None + CACHE_FALLBACK_TOTAL = None + AUTH_FAILURES_TOTAL = None + JWKS_FETCH_TOTAL = None + ACCESS_INDEX_REBUILDS_TOTAL = None + DB_QUERY_LATENCY_SECONDS = None def _tenant_label(tenant: str | None) -> str: @@ -91,7 +127,11 @@ def _tenant_label(tenant: str | None) -> str: def _cache_type_label(cache_type: str) -> str: value = str(cache_type or "unknown").strip().lower() - return value if value in {"policy", "acl", "relationship", "access_index"} else "unknown" + return ( + value + if value in {"policy", "acl", "relationship", "access_index", "decision"} + else "unknown" + ) def record_access_check(*, tenant: str | None, decision: str) -> None: @@ -154,6 +194,8 @@ def record_cache_event(*, cache_name: str, outcome: str) -> None: record_cache_hit(cache_type=cache) else: record_cache_miss(cache_type=cache) + if outcome_label == "fallback" and CACHE_FALLBACK_TOTAL is not None: + CACHE_FALLBACK_TOTAL.labels(cache_name=cache).inc() def observe_decision_latency(*, tenant_key: str, value: float) -> None: @@ -164,3 +206,30 @@ def observe_decision_latency(*, tenant_key: str, value: float) -> None: def record_api_error(*, code: str) -> None: if API_ERRORS_TOTAL is not None: API_ERRORS_TOTAL.labels(code=code).inc() + + +def record_bootstrap_failure(*, stage: str) -> None: + if BOOTSTRAP_FAILURES_TOTAL is not None: + BOOTSTRAP_FAILURES_TOTAL.labels(stage=str(stage)).inc() + + +def record_auth_failure(*, reason: str) -> None: + if AUTH_FAILURES_TOTAL is not None: + AUTH_FAILURES_TOTAL.labels(reason=str(reason)).inc() + + +def record_jwks_fetch(*, outcome: str) -> None: + if JWKS_FETCH_TOTAL is not None: + JWKS_FETCH_TOTAL.labels(outcome=str(outcome)).inc() + + +def record_access_index_rebuild(*, mode: str) -> None: + if ACCESS_INDEX_REBUILDS_TOTAL is not None: + ACCESS_INDEX_REBUILDS_TOTAL.labels(mode=str(mode)).inc() + + +def observe_db_query_latency(*, operation: str, value: float) -> None: + if DB_QUERY_LATENCY_SECONDS is not None: + DB_QUERY_LATENCY_SECONDS.labels(operation=str(operation or "unknown")).observe( + max(0.0, float(value)) + ) diff --git a/keynetra/services/access_indexer.py b/keynetra/services/access_indexer.py index ff87584..c70da67 100644 --- a/keynetra/services/access_indexer.py +++ b/keynetra/services/access_indexer.py @@ -2,9 +2,12 @@ from __future__ import annotations +import threading +import time from dataclasses import dataclass from typing import Any +from keynetra.observability.metrics import record_access_index_rebuild from keynetra.services.interfaces import ( AccessIndexCache, AccessIndexEntry, @@ -45,6 +48,10 @@ def __init__( self._acl_cache = acl_cache self._access_index_cache = access_index_cache self._relationships = relationships + self._memo_ttl_seconds = 5.0 + self._memo_lock = threading.Lock() + self._memo: dict[tuple[int, str, str, str], tuple[float, list[AccessIndexEntry]]] = {} + self._inflight: set[tuple[int, str, str, str]] = set() def build_resource_index( self, @@ -62,7 +69,35 @@ def build_resource_index( ) if cached is not None: return cached + cache_key = (tenant_id, resource_type, resource_id, action) + memoized = self._memo_get(cache_key) + if memoized is not None: + self._schedule_background_refresh( + tenant_id=tenant_id, + resource_type=resource_type, + resource_id=resource_id, + action=action, + ) + return memoized + + entries = self._rebuild_resource_index( + tenant_id=tenant_id, + resource_type=resource_type, + resource_id=resource_id, + action=action, + ) + self._memo_set(cache_key, entries) + return entries + def _rebuild_resource_index( + self, + *, + tenant_id: int, + resource_type: str, + resource_id: str, + action: str, + ) -> list[AccessIndexEntry]: + record_access_index_rebuild(mode="sync") acl_entries = self._acl_cache.get( tenant_id=tenant_id, resource_type=resource_type, @@ -135,6 +170,55 @@ def build_resource_index( ) return entries + def _schedule_background_refresh( + self, *, tenant_id: int, resource_type: str, resource_id: str, action: str + ) -> None: + cache_key = (tenant_id, resource_type, resource_id, action) + with self._memo_lock: + if cache_key in self._inflight: + return + self._inflight.add(cache_key) + + def run() -> None: + try: + entries = self._rebuild_resource_index( + tenant_id=tenant_id, + resource_type=resource_type, + resource_id=resource_id, + action=action, + ) + self._memo_set(cache_key, entries) + record_access_index_rebuild(mode="background") + finally: + with self._memo_lock: + self._inflight.discard(cache_key) + + thread = threading.Thread( + target=run, + daemon=True, + name=f"access-index-refresh:{resource_type}:{resource_id}:{action}", + ) + thread.start() + + def _memo_get(self, key: tuple[int, str, str, str]) -> list[AccessIndexEntry] | None: + with self._memo_lock: + item = self._memo.get(key) + if item is None: + return None + expires_at, entries = item + if expires_at <= time.time(): + self._memo.pop(key, None) + return None + return list(entries) + + def _memo_set( + self, + key: tuple[int, str, str, str], + entries: list[AccessIndexEntry], + ) -> None: + with self._memo_lock: + self._memo[key] = (time.time() + self._memo_ttl_seconds, list(entries)) + def invalidate_resource(self, *, tenant_id: int, resource_type: str, resource_id: str) -> None: self._acl_cache.invalidate( tenant_id=tenant_id, resource_type=resource_type, resource_id=resource_id @@ -142,9 +226,21 @@ def invalidate_resource(self, *, tenant_id: int, resource_type: str, resource_id self._access_index_cache.invalidate( tenant_id=tenant_id, resource_type=resource_type, resource_id=resource_id ) + with self._memo_lock: + keys = [ + key + for key in self._memo + if key[0] == tenant_id and key[1] == resource_type and key[2] == resource_id + ] + for key in keys: + self._memo.pop(key, None) def invalidate_tenant(self, *, tenant_id: int) -> None: self._access_index_cache.invalidate_tenant(tenant_id=tenant_id) + with self._memo_lock: + keys = [key for key in self._memo if key[0] == tenant_id] + for key in keys: + self._memo.pop(key, None) def subject_descriptors(self, user: dict[str, Any]) -> set[str]: descriptors: set[str] = set() diff --git a/keynetra/services/attribute_validation.py b/keynetra/services/attribute_validation.py index 898c476..a8815db 100644 --- a/keynetra/services/attribute_validation.py +++ b/keynetra/services/attribute_validation.py @@ -19,9 +19,8 @@ def _validate_dict(obj: Any, *, name: str, max_keys: int, max_depth: int, depth: raise AttributeValidationError(f"{name} keys must be strings") if isinstance(v, dict): _validate_dict(v, name=name, max_keys=max_keys, max_depth=max_depth, depth=depth + 1) - elif isinstance(v, list): - if len(v) > max_keys: - raise AttributeValidationError(f"{name} list too large") + elif isinstance(v, list) and len(v) > max_keys: + raise AttributeValidationError(f"{name} list too large") def validate_user(user: dict[str, Any]) -> None: diff --git a/keynetra/services/authorization.py b/keynetra/services/authorization.py index a6525e7..d28f2c9 100644 --- a/keynetra/services/authorization.py +++ b/keynetra/services/authorization.py @@ -6,9 +6,10 @@ from __future__ import annotations +import asyncio +import json import logging import time -from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass from typing import Any @@ -72,6 +73,7 @@ def __init__( acl_cache: ACLCache | None = None, access_index_cache: AccessIndexCache | None = None, auth_model_repository: AuthModelRepository | None = None, + request_id: str | None = None, ) -> None: self._settings = settings self._tenants = tenants @@ -86,6 +88,7 @@ def __init__( self._acl_cache = acl_cache self._access_index_cache = access_index_cache self._auth_model_repository = auth_model_repository + self._request_id = request_id self._revisions = RevisionService(tenants) self._access_indexer = ( AccessIndexer( @@ -113,8 +116,11 @@ def authorize( consistency: str = "eventual", revision: int | None = None, audit: bool = True, + policy_set: str = "active", ) -> AuthorizationResult: started_at = time.perf_counter() + normalized_policy_set = policy_set.strip().lower() or "active" + consistency_mode = consistency.strip().lower() fallback_input = AuthorizationInput( user=dict(user), action=action, @@ -139,9 +145,14 @@ def authorize( try: cache_key = None - if consistency.strip().lower() != "fully_consistent": + cache_namespace = ( + tenant.tenant_key + if normalized_policy_set == "active" + else f"{tenant.tenant_key}:{normalized_policy_set}" + ) + if consistency_mode != "fully_consistent": cache_key = self._decision_cache.make_key( - tenant_key=tenant.tenant_key, + tenant_key=cache_namespace, policy_version=tenant.policy_version, authorization_input=authorization_input, revision=tenant.revision if revision is None else revision, @@ -161,6 +172,7 @@ def authorize( tenant_key=tenant.tenant_key, tenant_id=tenant.id, policy_version=tenant.policy_version, + policy_set=normalized_policy_set, ) decision = engine.decide(authorization_input) if cache_key is not None: @@ -172,6 +184,7 @@ def authorize( principal_id=str(principal.get("id")), authorization_input=authorization_input, decision=decision, + correlation_id=self._request_id, ) return AuthorizationResult(decision=decision, cached=False, revision=tenant.revision) except Exception as exc: @@ -180,6 +193,7 @@ def authorize( event="authorization_fallback", tenant_id=tenant.tenant_key, principal_type=str(principal.get("type")), + correlation_id=self._request_id, resilience_mode=self._settings.resilience_mode, fallback_behavior=self._settings.resilience_fallback_behavior, reason=repr(exc), @@ -194,6 +208,34 @@ def authorize( finally: observe_decision_latency(tenant_key=tenant_key, value=time.perf_counter() - started_at) + async def authorize_async( + self, + *, + tenant_key: str, + principal: dict[str, Any], + user: dict[str, Any], + action: str, + resource: dict[str, Any], + context: dict[str, Any] | None = None, + consistency: str = "eventual", + revision: int | None = None, + audit: bool = True, + policy_set: str = "active", + ) -> AuthorizationResult: + return await asyncio.to_thread( + self.authorize, + tenant_key=tenant_key, + principal=principal, + user=user, + action=action, + resource=resource, + context=context, + consistency=consistency, + revision=revision, + audit=audit, + policy_set=policy_set, + ) + def authorize_batch( self, *, @@ -204,9 +246,12 @@ def authorize_batch( context: dict[str, Any] | None = None, consistency: str = "eventual", revision: int | None = None, + policy_set: str = "active", ) -> list[AuthorizationResult]: validate_user(user) fallback_context = dict(context or {}) + normalized_policy_set = policy_set.strip().lower() or "active" + consistency_mode = consistency.strip().lower() try: tenant = with_timeout( lambda: self._tenants.get_or_create(tenant_key), @@ -217,8 +262,15 @@ def authorize_batch( tenant_key=tenant.tenant_key, tenant_id=tenant.id, policy_version=tenant.policy_version, + policy_set=normalized_policy_set, + ) + except (RuntimeError, TimeoutError, ValueError) as exc: + log_event( + self._logger, + event="authorization_batch_fallback", + reason=repr(exc), + correlation_id=self._request_id, ) - except Exception: return [ AuthorizationResult( decision=self._fallback_decision( @@ -237,46 +289,92 @@ def authorize_batch( for item in items ] - def evaluate_item(item: dict[str, Any]) -> AuthorizationResult: + item_results: list[AuthorizationResult] = [] + memoized_results: dict[ + tuple[str, str, str], tuple[AuthorizationInput, AuthorizationDecision, bool] + ] = {} + for item in items: resource = dict(item.get("resource") or {}) validate_resource(resource) - authorization_input = self._build_authorization_input( - tenant_id=tenant.id, - tenant_key=tenant.tenant_key, - user=enriched_user, - action=str(item["action"]), - resource=resource, - context=dict(context or {}), + action = str(item["action"]) + context_payload = dict(context or {}) + memo_key = ( + action, + json.dumps(resource, sort_keys=True, separators=(",", ":"), default=str), + json.dumps(context_payload, sort_keys=True, separators=(",", ":"), default=str), ) - cache_key = None - if consistency.strip().lower() != "fully_consistent": - cache_key = self._decision_cache.make_key( + if memo_key in memoized_results: + authorization_input, decision, cached = memoized_results[memo_key] + else: + authorization_input = self._build_authorization_input( + tenant_id=tenant.id, tenant_key=tenant.tenant_key, - policy_version=tenant.policy_version, - authorization_input=authorization_input, - revision=tenant.revision if revision is None else revision, + user=enriched_user, + action=action, + resource=resource, + context=context_payload, ) - cached = self._safe_cache_get(cache_key) - if cached is not None: - return AuthorizationResult( - decision=self._decision_from_cache(cached), - cached=True, - revision=tenant.revision, + cache_namespace = ( + tenant.tenant_key + if normalized_policy_set == "active" + else f"{tenant.tenant_key}:{normalized_policy_set}" + ) + cached = False + if consistency_mode != "fully_consistent": + cache_key = self._decision_cache.make_key( + tenant_key=cache_namespace, + policy_version=tenant.policy_version, + authorization_input=authorization_input, + revision=tenant.revision if revision is None else revision, ) - decision = engine.decide(authorization_input) - if cache_key is not None: - self._safe_cache_set(cache_key, CachedDecision.from_decision(decision)) + cached_decision = self._safe_cache_get(cache_key) + if cached_decision is not None: + decision = self._decision_from_cache(cached_decision) + cached = True + else: + decision = engine.decide(authorization_input) + self._safe_cache_set(cache_key, CachedDecision.from_decision(decision)) + else: + decision = engine.decide(authorization_input) + memoized_results[memo_key] = (authorization_input, decision, cached) + self._safe_audit_write( tenant_id=tenant.id, principal_type=str(principal.get("type")), principal_id=str(principal.get("id")), authorization_input=authorization_input, decision=decision, + correlation_id=self._request_id, + ) + item_results.append( + AuthorizationResult(decision=decision, cached=cached, revision=tenant.revision) ) - return AuthorizationResult(decision=decision, cached=False, revision=tenant.revision) - with ThreadPoolExecutor(max_workers=min(32, max(1, len(items)))) as pool: - return list(pool.map(evaluate_item, items)) + return item_results + + async def authorize_batch_async( + self, + *, + tenant_key: str, + principal: dict[str, Any], + user: dict[str, Any], + items: list[dict[str, Any]], + context: dict[str, Any] | None = None, + consistency: str = "eventual", + revision: int | None = None, + policy_set: str = "active", + ) -> list[AuthorizationResult]: + return await asyncio.to_thread( + self.authorize_batch, + tenant_key=tenant_key, + principal=principal, + user=user, + items=items, + context=context, + consistency=consistency, + revision=revision, + policy_set=policy_set, + ) def simulate( self, @@ -302,6 +400,24 @@ def simulate( def get_revision(self, *, tenant_key: str) -> int: return self._revisions.get_revision(tenant_key=tenant_key) + def build_input( + self, + *, + tenant_key: str, + user: dict[str, Any], + action: str, + resource: dict[str, Any], + context: dict[str, Any] | None = None, + ) -> AuthorizationInput: + authorization_input, _ = self._build_input( + tenant_key=tenant_key, + user=user, + action=action, + resource=resource, + context=dict(context or {}), + ) + return authorization_input + def _build_input( self, *, @@ -417,23 +533,42 @@ def _hydrate_user(self, *, tenant_id: int, user: dict[str, Any]) -> dict[str, An return enriched_user def _build_engine( - self, *, tenant_key: str, tenant_id: int, policy_version: int + self, *, tenant_key: str, tenant_id: int, policy_version: int, policy_set: str = "active" ) -> KeyNetraEngine: - cached = self._safe_policy_cache_get(tenant_key, policy_version) + policy_set_key = policy_set.strip().lower() or "active" + graph_tenant_key = ( + tenant_key if policy_set_key == "active" else f"{tenant_key}:{policy_set_key}" + ) + cached = ( + self._safe_policy_cache_get(tenant_key, policy_version) + if policy_set_key == "active" + else None + ) if cached is None: + + def _load_current_policies() -> list[Any]: + try: + return self._policies.list_current_policies( + tenant_id=tenant_id, policy_set=policy_set_key + ) + except TypeError: + # Backward compatibility for repositories that only accept tenant_id. + return self._policies.list_current_policies(tenant_id=tenant_id) + cached = with_timeout( - lambda: self._policies.list_current_policies(tenant_id=tenant_id), + _load_current_policies, timeout_seconds=self._settings.service_timeout_seconds, ) if not cached: policies = self._settings.load_policies() engine = KeyNetraEngine(policies, strategy="first_match") - COMPILED_POLICY_STORE.set(tenant_key, policy_version, engine._compiled_graph) + COMPILED_POLICY_STORE.set(graph_tenant_key, policy_version, engine._compiled_graph) return engine - self._safe_policy_cache_set(tenant_key, policy_version, cached) + if policy_set_key == "active": + self._safe_policy_cache_set(tenant_key, policy_version, cached) policies = [policy.definition for policy in cached] engine = KeyNetraEngine(policies, strategy="first_match") - COMPILED_POLICY_STORE.set(tenant_key, policy_version, engine._compiled_graph) + COMPILED_POLICY_STORE.set(graph_tenant_key, policy_version, engine._compiled_graph) return engine def _decision_from_cache(self, cached: CachedDecision) -> AuthorizationDecision: @@ -531,7 +666,11 @@ def _safe_cache_get(self, key: str) -> CachedDecision | None: except Exception as exc: record_cache_event(cache_name="decision", outcome="fallback") log_event( - self._logger, event="cache_get_failed", cache_name="decision", reason=repr(exc) + self._logger, + event="cache_get_failed", + cache_name="decision", + reason=repr(exc), + correlation_id=self._request_id, ) return None record_cache_event(cache_name="decision", outcome="hit" if cached is not None else "miss") @@ -550,7 +689,11 @@ def _safe_cache_set(self, key: str, value: CachedDecision) -> None: ) except Exception as exc: log_event( - self._logger, event="cache_set_failed", cache_name="decision", reason=repr(exc) + self._logger, + event="cache_set_failed", + cache_name="decision", + reason=repr(exc), + correlation_id=self._request_id, ) def _safe_policy_cache_get(self, tenant_key: str, policy_version: int): @@ -561,7 +704,13 @@ def _safe_policy_cache_get(self, tenant_key: str, policy_version: int): ) except Exception as exc: record_cache_event(cache_name="policy", outcome="fallback") - log_event(self._logger, event="cache_get_failed", cache_name="policy", reason=repr(exc)) + log_event( + self._logger, + event="cache_get_failed", + cache_name="policy", + reason=repr(exc), + correlation_id=self._request_id, + ) return None record_cache_event(cache_name="policy", outcome="hit" if cached is not None else "miss") return cached @@ -578,7 +727,13 @@ def _safe_policy_cache_set( attempts=self._settings.critical_retry_attempts, ) except Exception as exc: - log_event(self._logger, event="cache_set_failed", cache_name="policy", reason=repr(exc)) + log_event( + self._logger, + event="cache_set_failed", + cache_name="policy", + reason=repr(exc), + correlation_id=self._request_id, + ) def _safe_relationship_cache_get(self, *, tenant_id: int, subject_type: str, subject_id: str): try: @@ -591,7 +746,11 @@ def _safe_relationship_cache_get(self, *, tenant_id: int, subject_type: str, sub except Exception as exc: record_cache_event(cache_name="relationship", outcome="fallback") log_event( - self._logger, event="cache_get_failed", cache_name="relationship", reason=repr(exc) + self._logger, + event="cache_get_failed", + cache_name="relationship", + reason=repr(exc), + correlation_id=self._request_id, ) return None record_cache_event( @@ -617,7 +776,11 @@ def _safe_relationship_cache_set( ) except Exception as exc: log_event( - self._logger, event="cache_set_failed", cache_name="relationship", reason=repr(exc) + self._logger, + event="cache_set_failed", + cache_name="relationship", + reason=repr(exc), + correlation_id=self._request_id, ) def _resource_identity(self, resource: dict[str, Any]) -> tuple[str, str]: @@ -641,4 +804,9 @@ def _safe_audit_write(self, **kwargs: Any) -> None: attempts=self._settings.critical_retry_attempts, ) except Exception as exc: - log_event(self._logger, event="audit_write_failed", reason=repr(exc)) + log_event( + self._logger, + event="audit_write_failed", + reason=repr(exc), + correlation_id=self._request_id, + ) diff --git a/keynetra/services/doctor.py b/keynetra/services/doctor.py index 8360d1b..90a7fc7 100644 --- a/keynetra/services/doctor.py +++ b/keynetra/services/doctor.py @@ -38,10 +38,12 @@ def run_core_doctor(settings: Settings) -> dict[str, Any]: _check_redis(), _check_migrations(settings), ] + missing_or_weak = [check for check in checks if not check.ok] return { "service": "core", "ok": all(check.ok for check in checks), "checks": [asdict(check) for check in checks], + "remediation_count": len(missing_or_weak), } @@ -52,12 +54,39 @@ def _check_env(settings: Settings) -> DoctorCheck: "KEYNETRA_DATABASE_URL": bool(os.environ.get("KEYNETRA_DATABASE_URL")), "KEYNETRA_REDIS_URL": bool(os.environ.get("KEYNETRA_REDIS_URL")), } - auth_configured = ( - bool(settings.parsed_api_key_hashes()) - or settings.jwt_secret != "change-me" - or bool(settings.oidc_jwks_url) + has_api_key_auth = bool(settings.parsed_api_key_hashes()) + has_jwt_auth = settings.jwt_secret != "change-me" or bool(settings.oidc_jwks_url) + auth_configured = has_api_key_auth or has_jwt_auth + weak_jwt_secret = settings.jwt_secret.strip() == "change-me" + profile = settings.environment + weak_admin_username = ( + bool(settings.admin_username) and settings.admin_username.lower() == "admin" ) - ok = all(required_env.values()) and auth_configured + missing_admin_password_hash = bool(settings.admin_username) and not bool( + settings.admin_password_hash + ) + ok = ( + all(required_env.values()) + and auth_configured + and (profile in {"development", "dev", "local"} or not weak_jwt_secret) + ) + remediation: list[str] = [] + if not required_env["KEYNETRA_DATABASE_URL"]: + remediation.append("Set KEYNETRA_DATABASE_URL to a reachable production database.") + if not required_env["KEYNETRA_REDIS_URL"]: + remediation.append("Set KEYNETRA_REDIS_URL to enable distributed cache and invalidation.") + if not auth_configured: + remediation.append( + "Configure KEYNETRA_API_KEYS/KEYNETRA_API_KEY_HASHES or JWT/OIDC settings before deployment." + ) + if profile not in {"development", "dev", "local"} and weak_jwt_secret: + remediation.append("Set KEYNETRA_JWT_SECRET to a strong non-default value.") + if weak_admin_username: + remediation.append( + "Avoid default admin username; set KEYNETRA_ADMIN_USERNAME to a unique value." + ) + if missing_admin_password_hash: + remediation.append("Set KEYNETRA_ADMIN_PASSWORD_HASH and remove KEYNETRA_ADMIN_PASSWORD.") return DoctorCheck( name="env_variables", ok=ok, @@ -66,7 +95,16 @@ def _check_env(settings: Settings) -> DoctorCheck: if ok else "missing required environment configuration" ), - details={**required_env, "auth_configured": auth_configured}, + details={ + **required_env, + "auth_configured": auth_configured, + "has_api_key_auth": has_api_key_auth, + "has_jwt_auth": has_jwt_auth, + "weak_jwt_secret": weak_jwt_secret, + "weak_admin_username": weak_admin_username, + "missing_admin_password_hash": missing_admin_password_hash, + "remediation": remediation, + }, ) diff --git a/keynetra/services/impact_analysis.py b/keynetra/services/impact_analysis.py index ea6cb53..41e8f3a 100644 --- a/keynetra/services/impact_analysis.py +++ b/keynetra/services/impact_analysis.py @@ -56,15 +56,54 @@ def analyze_policy_change(self, *, tenant_key: str, policy_change: str) -> Impac gained: set[int] = set() lost: set[int] = set() list_user_ids = getattr(self._users, "list_user_ids", None) - user_ids = list_user_ids(tenant_id=tenant.id) if callable(list_user_ids) else [] + try: + user_ids = list_user_ids(tenant_id=tenant.id) if callable(list_user_ids) else [] + except Exception: + return ImpactResult(gained_access=[], lost_access=[]) + get_user_contexts = getattr(self._users, "get_user_contexts", None) + try: + user_contexts = get_user_contexts(user_ids) if callable(get_user_contexts) else {} + except Exception: + user_contexts = {} + list_for_subjects = getattr(self._relationships, "list_for_subjects", None) + try: + relationship_map = ( + list_for_subjects( + tenant_id=tenant.id, + subject_type="user", + subject_ids=[str(user_id) for user_id in user_ids], + ) + if callable(list_for_subjects) + else {} + ) + except Exception: + relationship_map = {} for user_id in user_ids: - context = self._users.get_user_context(user_id) or { - "id": user_id, - "roles": [], - "permissions": [], - } - user = self._enrich_user_with_relationships(tenant_id=tenant.id, user=context) - for resource in self._candidate_resources(tenant_id=tenant.id, user_id=user_id): + try: + context = ( + user_contexts.get(user_id) + or self._users.get_user_context(user_id) + or { + "id": user_id, + "roles": [], + "permissions": [], + } + ) + except Exception: + context = {"id": user_id, "roles": [], "permissions": []} + prefetched_relationships = relationship_map.get(str(user_id)) + try: + user = self._enrich_user_with_relationships( + tenant_id=tenant.id, + user=context, + prefetched_relationships=prefetched_relationships, + ) + candidate_resources = self._candidate_resources( + tenant_id=tenant.id, user_id=user_id + ) + except Exception: + continue + for resource in candidate_resources: before = before_engine.decide( AuthorizationInput( user=user, action=changed_policy["action"], resource=resource @@ -99,11 +138,20 @@ def _candidate_resources(self, *, tenant_id: int, user_id: int) -> list[dict[str return resources def _enrich_user_with_relationships( - self, *, tenant_id: int, user: dict[str, Any] + self, + *, + tenant_id: int, + user: dict[str, Any], + prefetched_relationships: list[Any] | None = None, ) -> dict[str, Any]: enriched = dict(user) user_id = enriched.get("id") if isinstance(user_id, int): + if prefetched_relationships is not None: + enriched["relations"] = [ + relation.to_dict() for relation in prefetched_relationships + ] + return enriched enriched["relations"] = [ relation.to_dict() for relation in self._relationships.list_for_subject( diff --git a/keynetra/services/interfaces.py b/keynetra/services/interfaces.py index 4872c0e..36de5a5 100644 --- a/keynetra/services/interfaces.py +++ b/keynetra/services/interfaces.py @@ -94,7 +94,8 @@ class PolicyMutationResult: action: str effect: str priority: int - conditions: dict[str, Any] + conditions: dict[str, Any] = field(default_factory=dict) + state: str = "active" @dataclass(frozen=True) @@ -105,7 +106,8 @@ class PolicyListItem: action: str effect: str priority: int - conditions: dict[str, Any] + conditions: dict[str, Any] = field(default_factory=dict) + state: str = "active" @dataclass(frozen=True) @@ -113,6 +115,7 @@ class AuditListItem: id: int principal_type: str principal_id: str + correlation_id: str | None user: dict[str, Any] action: str resource: dict[str, Any] @@ -150,7 +153,7 @@ class CachedDecision: failed_conditions: list[str] = field(default_factory=list) @classmethod - def from_decision(cls, decision: AuthorizationDecision) -> "CachedDecision": + def from_decision(cls, decision: AuthorizationDecision) -> CachedDecision: return cls( allowed=decision.allowed, decision=decision.decision, @@ -177,7 +180,9 @@ def bump_revision(self, tenant: TenantRecord) -> TenantRecord: ... class PolicyRepository(Protocol): """Persistence boundary for policy storage.""" - def list_current_policies(self, *, tenant_id: int) -> list[PolicyRecord]: ... + def list_current_policies( + self, *, tenant_id: int, policy_set: str = "active" + ) -> list[PolicyRecord]: ... def list_current_policy_views(self, *, tenant_id: int) -> list[PolicyListItem]: ... @@ -199,6 +204,7 @@ def create_policy_version( priority: int, conditions: dict[str, Any], created_by: str | None, + state: str = "active", ) -> PolicyMutationResult: ... def rollback_policy( @@ -279,6 +285,7 @@ def write( principal_id: str, authorization_input: AuthorizationInput, decision: AuthorizationDecision, + correlation_id: str | None = None, ) -> None: ... def list_page( diff --git a/keynetra/services/policies.py b/keynetra/services/policies.py index 74c1798..d6bb2cd 100644 --- a/keynetra/services/policies.py +++ b/keynetra/services/policies.py @@ -37,9 +37,12 @@ def __init__( def list_policies(self, *, tenant_key: str) -> list[dict[str, object]]: tenant = self._tenants.get_or_create(tenant_key) - return [ - item.__dict__ for item in self._policies.list_current_policy_views(tenant_id=tenant.id) - ] + data: list[dict[str, object]] = [] + for item in self._policies.list_current_policy_views(tenant_id=tenant.id): + row = dict(item.__dict__) + row.pop("state", None) + data.append(row) + return data def list_policies_page( self, @@ -52,7 +55,12 @@ def list_policies_page( items, next_cursor = self._policies.list_current_policy_page( tenant_id=tenant.id, limit=limit, cursor=cursor ) - return [item.__dict__ for item in items], next_cursor + data: list[dict[str, object]] = [] + for item in items: + row = dict(item.__dict__) + row.pop("state", None) + data.append(row) + return data, next_cursor def create_policy( self, @@ -64,17 +72,30 @@ def create_policy( priority: int, conditions: dict[str, object], created_by: str | None, + state: str = "active", ) -> PolicyMutationResult: tenant = self._tenants.get_or_create(tenant_key) - result = self._policies.create_policy_version( - tenant_id=tenant.id, - policy_key=policy_key, - action=action, - effect=effect, - priority=priority, - conditions=conditions, - created_by=created_by, - ) + try: + result = self._policies.create_policy_version( + tenant_id=tenant.id, + policy_key=policy_key, + action=action, + effect=effect, + priority=priority, + conditions=conditions, + created_by=created_by, + state=state, + ) + except TypeError: + result = self._policies.create_policy_version( + tenant_id=tenant.id, + policy_key=policy_key, + action=action, + effect=effect, + priority=priority, + conditions=conditions, + created_by=created_by, + ) updated_tenant = self._tenants.bump_policy_version(tenant) self._policy_cache.invalidate(updated_tenant.tenant_key) self._decision_cache.bump_namespace(updated_tenant.tenant_key) diff --git a/keynetra/services/policy_admin.py b/keynetra/services/policy_admin.py deleted file mode 100644 index 59cdb89..0000000 --- a/keynetra/services/policy_admin.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Deprecated compatibility wrapper. - -Policy orchestration now lives in ``keynetra.services.policies``. -""" - -from __future__ import annotations - -from typing import Any - -from sqlalchemy.orm import Session - -from keynetra.config.settings import get_settings -from keynetra.infrastructure.cache.decision_cache import build_decision_cache -from keynetra.infrastructure.cache.policy_cache import build_policy_cache -from keynetra.infrastructure.cache.policy_distribution import RedisPolicyEventPublisher -from keynetra.infrastructure.repositories.policies import SqlPolicyRepository -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository -from keynetra.services.policies import PolicyService - - -class PolicyAdmin: - """Backward-compatible adapter around the new policy service.""" - - def create_policy_version( - self, - db: Session, - *, - tenant_id: int, - policy_key: str, - action: str, - effect: str, - priority: int, - conditions: dict[str, Any], - created_by: str | None, - ) -> Any: - settings = get_settings() - tenants = SqlTenantRepository(db) - tenant = tenants.get_by_id(tenant_id) - if tenant is None: - raise ValueError("tenant not found") - service = PolicyService( - tenants=tenants, - policies=SqlPolicyRepository(db), - policy_cache=build_policy_cache(None), - decision_cache=build_decision_cache(None), - publisher=RedisPolicyEventPublisher(settings), - ) - return service.create_policy( - tenant_key=tenant.tenant_key, - policy_key=policy_key, - action=action, - effect=effect, - priority=priority, - conditions=conditions, - created_by=created_by, - ) - - def rollback_policy(self, db: Session, *, tenant_id: int, policy_key: str, version: int) -> Any: - settings = get_settings() - tenants = SqlTenantRepository(db) - tenant = tenants.get_by_id(tenant_id) - if tenant is None: - raise ValueError("tenant not found") - service = PolicyService( - tenants=tenants, - policies=SqlPolicyRepository(db), - policy_cache=build_policy_cache(None), - decision_cache=build_decision_cache(None), - publisher=RedisPolicyEventPublisher(settings), - ) - policy_name, current_version = service.rollback_policy( - tenant_key=tenant.tenant_key, - policy_key=policy_key, - version=version, - ) - return type( - "RollbackPolicyResult", - (), - {"policy_key": policy_name, "current_version": current_version}, - )() diff --git a/keynetra/services/policy_dsl.py b/keynetra/services/policy_dsl.py index 67eccfb..21071d8 100644 --- a/keynetra/services/policy_dsl.py +++ b/keynetra/services/policy_dsl.py @@ -20,11 +20,11 @@ def dsl_to_policy(dsl_text: str) -> dict[str, Any]: role: admin owner_only: true """ - if yaml is not None: - data = yaml.safe_load(dsl_text) - else: - # Allow JSON payloads as a subset fallback when PyYAML is unavailable. - data = json.loads(dsl_text) + data = ( + yaml.safe_load(dsl_text) + if yaml is not None + else json.loads(dsl_text) # Allow JSON payloads when PyYAML is unavailable. + ) if not isinstance(data, dict) or not data: raise ValueError("invalid dsl") diff --git a/keynetra/services/policy_simulator.py b/keynetra/services/policy_simulator.py index 0510e7c..9925efb 100644 --- a/keynetra/services/policy_simulator.py +++ b/keynetra/services/policy_simulator.py @@ -5,10 +5,7 @@ from dataclasses import dataclass from typing import Any -from keynetra.engine.keynetra_engine import ( - AuthorizationDecision, - KeyNetraEngine, -) +from keynetra.engine.keynetra_engine import AuthorizationDecision, KeyNetraEngine from keynetra.services.authorization import AuthorizationService from keynetra.services.interfaces import PolicyRepository, TenantRepository from keynetra.services.policy_dsl import dsl_to_policy diff --git a/keynetra/services/policy_store.py b/keynetra/services/policy_store.py deleted file mode 100644 index e5a38e7..0000000 --- a/keynetra/services/policy_store.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Deprecated compatibility import. - -Database-backed policy storage now lives in -``keynetra.infrastructure.repositories.policies``. -""" - -from keynetra.infrastructure.repositories.policies import SqlPolicyRepository as PolicyStore - -__all__ = ["PolicyStore"] diff --git a/keynetra/services/relationship_store.py b/keynetra/services/relationship_store.py deleted file mode 100644 index 4929bdc..0000000 --- a/keynetra/services/relationship_store.py +++ /dev/null @@ -1,11 +0,0 @@ -"""Deprecated compatibility import. - -Database-backed relationship storage now lives in -``keynetra.infrastructure.repositories.relationships``. -""" - -from keynetra.infrastructure.repositories.relationships import ( - SqlRelationshipRepository as RelationshipStore, -) - -__all__ = ["RelationshipStore"] diff --git a/keynetra/services/tenant_store.py b/keynetra/services/tenant_store.py deleted file mode 100644 index be528f7..0000000 --- a/keynetra/services/tenant_store.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Deprecated compatibility import. - -Database-backed tenant storage now lives in -``keynetra.infrastructure.repositories.tenants``. -""" - -from keynetra.infrastructure.repositories.tenants import SqlTenantRepository as TenantStore - -__all__ = ["TenantStore"] diff --git a/keynetra/services/user_store.py b/keynetra/services/user_store.py deleted file mode 100644 index 693adce..0000000 --- a/keynetra/services/user_store.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Deprecated compatibility import. - -Database-backed user lookup now lives in -``keynetra.infrastructure.repositories.users``. -""" - -from keynetra.infrastructure.repositories.users import SqlUserRepository as UserStore - -__all__ = ["UserStore"] diff --git a/locustfile.py b/locustfile.py index 2636506..8029aba 100644 --- a/locustfile.py +++ b/locustfile.py @@ -7,7 +7,7 @@ class KeyNetraUser(HttpUser): wait_time = between(0.0, 0.1) def on_start(self) -> None: - self.headers = {"X-API-Key": "devkey"} + self.headers = {"X-API-Key": "testkey", "X-Tenant-Id": "acme"} @task def check_access(self) -> None: diff --git a/monitoring/grafana/dashboards/keynetra-overview.json b/monitoring/grafana/dashboards/keynetra-overview.json new file mode 100644 index 0000000..4af97ca --- /dev/null +++ b/monitoring/grafana/dashboards/keynetra-overview.json @@ -0,0 +1,308 @@ +{ + "annotations": { + "list": [] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "liveNow": false, + "panels": [ + { + "id": 1, + "type": "stat", + "title": "Request Rate (req/s)", + "gridPos": {"h": 5, "w": 4, "x": 0, "y": 0}, + "targets": [ + { + "expr": "sum(rate(keynetra_http_requests_total{tenant=~\"$tenant\"}[5m]))", + "legendFormat": "req/s", + "refId": "A" + } + ] + }, + { + "id": 2, + "type": "stat", + "title": "Error Rate (5xx req/s)", + "gridPos": {"h": 5, "w": 4, "x": 4, "y": 0}, + "targets": [ + { + "expr": "sum(rate(keynetra_http_requests_total{tenant=~\"$tenant\",status=~\"5..\"}[5m]))", + "legendFormat": "5xx/s", + "refId": "A" + } + ] + }, + { + "id": 3, + "type": "gauge", + "title": "Cache Hit Ratio %", + "gridPos": {"h": 5, "w": 4, "x": 8, "y": 0}, + "fieldConfig": { + "defaults": { + "min": 0, + "max": 100, + "unit": "percent" + }, + "overrides": [] + }, + "targets": [ + { + "expr": "100 * sum(rate(keynetra_cache_events_total{outcome=\"hit\"}[5m])) / clamp_min(sum(rate(keynetra_cache_events_total[5m])), 0.000001)", + "legendFormat": "hit%", + "refId": "A" + } + ] + }, + { + "id": 4, + "type": "stat", + "title": "Auth Failures / min", + "gridPos": {"h": 5, "w": 4, "x": 12, "y": 0}, + "targets": [ + { + "expr": "sum(increase(keynetra_auth_failures_total[1m]))", + "legendFormat": "auth_fail/min", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "stat", + "title": "Policy Evals / min", + "gridPos": {"h": 5, "w": 4, "x": 16, "y": 0}, + "targets": [ + { + "expr": "sum(increase(keynetra_policy_evaluations_total{tenant=~\"$tenant\"}[1m]))", + "legendFormat": "policy/min", + "refId": "A" + } + ] + }, + { + "id": 6, + "type": "stat", + "title": "Decision p95 (ms)", + "gridPos": {"h": 5, "w": 4, "x": 20, "y": 0}, + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "targets": [ + { + "expr": "1000 * histogram_quantile(0.95, sum(rate(keynetra_decision_latency_seconds_bucket{tenant_key=~\"$tenant\"}[5m])) by (le))", + "legendFormat": "p95", + "refId": "A" + } + ] + }, + { + "id": 7, + "type": "timeseries", + "title": "HTTP Request Throughput by Endpoint", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 5}, + "targets": [ + { + "expr": "sum(rate(keynetra_http_requests_total{tenant=~\"$tenant\"}[5m])) by (endpoint, method)", + "legendFormat": "{{method}} {{endpoint}}", + "refId": "A" + } + ] + }, + { + "id": 8, + "type": "timeseries", + "title": "HTTP Latency p50/p95 by Endpoint", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 5}, + "targets": [ + { + "expr": "histogram_quantile(0.50, sum(rate(keynetra_http_request_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])) by (le, endpoint))", + "legendFormat": "p50 {{endpoint}}", + "refId": "A" + }, + { + "expr": "histogram_quantile(0.95, sum(rate(keynetra_http_request_duration_seconds_bucket{tenant=~\"$tenant\"}[5m])) by (le, endpoint))", + "legendFormat": "p95 {{endpoint}}", + "refId": "B" + } + ] + }, + { + "id": 9, + "type": "bargauge", + "title": "Authorization Decisions by Tenant", + "gridPos": {"h": 8, "w": 8, "x": 0, "y": 13}, + "targets": [ + { + "expr": "sum(increase(keynetra_access_checks_total[5m])) by (tenant, decision)", + "legendFormat": "{{tenant}} {{decision}}", + "refId": "A" + } + ] + }, + { + "id": 10, + "type": "timeseries", + "title": "Policy Evaluation Latency by Stage", + "gridPos": {"h": 8, "w": 8, "x": 8, "y": 13}, + "targets": [ + { + "expr": "sum(rate(keynetra_access_check_latency_seconds_sum{tenant=~\"$tenant\"}[5m])) by (stage) / clamp_min(sum(rate(keynetra_access_check_latency_seconds_count{tenant=~\"$tenant\"}[5m])) by (stage), 0.000001)", + "legendFormat": "{{stage}}", + "refId": "A" + } + ] + }, + { + "id": 11, + "type": "heatmap", + "title": "Decision Latency Distribution", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 13}, + "targets": [ + { + "expr": "sum(rate(keynetra_decision_latency_seconds_bucket{tenant_key=~\"$tenant\"}[5m])) by (le)", + "legendFormat": "bucket", + "refId": "A", + "format": "heatmap" + } + ] + }, + { + "id": 12, + "type": "timeseries", + "title": "Cache Events by Type/Outcome", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 21}, + "targets": [ + { + "expr": "sum(rate(keynetra_cache_events_total[5m])) by (cache_name, outcome)", + "legendFormat": "{{cache_name}} {{outcome}}", + "refId": "A" + } + ] + }, + { + "id": 13, + "type": "timeseries", + "title": "Database Query Latency", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 21}, + "targets": [ + { + "expr": "sum(rate(keynetra_db_query_latency_seconds_sum[5m])) by (operation) / clamp_min(sum(rate(keynetra_db_query_latency_seconds_count[5m])) by (operation), 0.000001)", + "legendFormat": "{{operation}}", + "refId": "A" + } + ] + }, + { + "id": 14, + "type": "piechart", + "title": "HTTP Status Distribution", + "gridPos": {"h": 8, "w": 8, "x": 0, "y": 29}, + "targets": [ + { + "expr": "sum(increase(keynetra_http_requests_total{tenant=~\"$tenant\"}[15m])) by (status)", + "legendFormat": "{{status}}", + "refId": "A" + } + ] + }, + { + "id": 15, + "type": "table", + "title": "Top Endpoints by Request Volume", + "gridPos": {"h": 8, "w": 8, "x": 8, "y": 29}, + "targets": [ + { + "expr": "topk(15, sum(increase(keynetra_http_requests_total{tenant=~\"$tenant\"}[15m])) by (endpoint, method))", + "legendFormat": "{{method}} {{endpoint}}", + "refId": "A", + "format": "table" + } + ] + }, + { + "id": 16, + "type": "timeseries", + "title": "Tenant Activity (Requests)", + "gridPos": {"h": 8, "w": 8, "x": 16, "y": 29}, + "targets": [ + { + "expr": "sum(rate(keynetra_http_requests_total[5m])) by (tenant)", + "legendFormat": "{{tenant}}", + "refId": "A" + } + ] + }, + { + "id": 17, + "type": "timeseries", + "title": "JWKS Fetch Outcomes", + "gridPos": {"h": 8, "w": 12, "x": 0, "y": 37}, + "targets": [ + { + "expr": "sum(rate(keynetra_jwks_fetch_total[5m])) by (outcome)", + "legendFormat": "{{outcome}}", + "refId": "A" + } + ] + }, + { + "id": 18, + "type": "timeseries", + "title": "Access Index Rebuilds / Revision Updates", + "gridPos": {"h": 8, "w": 12, "x": 12, "y": 37}, + "targets": [ + { + "expr": "sum(increase(keynetra_access_index_rebuilds_total[15m])) by (mode)", + "legendFormat": "rebuild {{mode}}", + "refId": "A" + }, + { + "expr": "sum(increase(keynetra_revision_updates_total[15m])) by (tenant)", + "legendFormat": "revision {{tenant}}", + "refId": "B" + } + ] + } + ], + "refresh": "10s", + "schemaVersion": 39, + "style": "dark", + "tags": ["keynetra", "authorization", "multi-tenant", "observability"], + "templating": { + "list": [ + { + "name": "tenant", + "type": "query", + "label": "Tenant", + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "refresh": 2, + "query": "label_values(keynetra_http_requests_total, tenant)", + "includeAll": true, + "multi": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": ["$__all"] + } + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timezone": "browser", + "title": "KeyNetra Deep Observability", + "uid": "keynetra-overview", + "version": 2, + "weekStart": "" +} diff --git a/infra/docker/monitoring/grafana/provisioning/dashboards/dashboards.yml b/monitoring/grafana/provisioning/dashboards/dashboards.yml similarity index 99% rename from infra/docker/monitoring/grafana/provisioning/dashboards/dashboards.yml rename to monitoring/grafana/provisioning/dashboards/dashboards.yml index 668919c..51fc20d 100644 --- a/infra/docker/monitoring/grafana/provisioning/dashboards/dashboards.yml +++ b/monitoring/grafana/provisioning/dashboards/dashboards.yml @@ -9,4 +9,3 @@ providers: allowUiUpdates: true options: path: /var/lib/grafana/dashboards - diff --git a/infra/docker/monitoring/grafana/provisioning/datasources/datasource.yml b/monitoring/grafana/provisioning/datasources/datasource.yml similarity index 63% rename from infra/docker/monitoring/grafana/provisioning/datasources/datasource.yml rename to monitoring/grafana/provisioning/datasources/datasource.yml index 96faeb7..991dc75 100644 --- a/infra/docker/monitoring/grafana/provisioning/datasources/datasource.yml +++ b/monitoring/grafana/provisioning/datasources/datasource.yml @@ -7,4 +7,8 @@ datasources: url: http://prometheus:9090 isDefault: true editable: true - + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: true diff --git a/monitoring/loki/loki-config.yml b/monitoring/loki/loki-config.yml new file mode 100644 index 0000000..4ab60bc --- /dev/null +++ b/monitoring/loki/loki-config.yml @@ -0,0 +1,35 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + path_prefix: /loki + storage: + filesystem: + chunks_directory: /loki/chunks + rules_directory: /loki/rules + replication_factor: 1 + ring: + kvstore: + store: inmemory + +schema_config: + configs: + - from: 2024-01-01 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /loki/chunks + +limits_config: + allow_structured_metadata: false + +ruler: + alertmanager_url: http://localhost:9093 diff --git a/monitoring/prometheus/prometheus.yml b/monitoring/prometheus/prometheus.yml new file mode 100644 index 0000000..12cb907 --- /dev/null +++ b/monitoring/prometheus/prometheus.yml @@ -0,0 +1,13 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: keynetra-api + metrics_path: /metrics + static_configs: + - targets: ["keynetra-api:8080"] + + - job_name: node-exporter + static_configs: + - targets: ["node-exporter:9100"] diff --git a/pyproject.toml b/pyproject.toml index 13d6a2b..1053e97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,28 @@ build-backend = "setuptools.build_meta" name = "keynetra" version = "0.1.0" requires-python = ">=3.11" -dependencies = [] +dependencies = [ + "fastapi>=0.115", + "uvicorn[standard]>=0.30", + "gunicorn>=22.0", + "pydantic>=2.7", + "pydantic-settings>=2.3", + "python-jose[cryptography]>=3.3", + "PyYAML>=6.0", + "typer>=0.12", + "rich>=13.7", + "pyfiglet>=1.0.2", + "prometheus-client>=0.21", + "prometheus-fastapi-instrumentator>=7.0", + "opentelemetry-api>=1.25", + "opentelemetry-sdk>=1.25", + "opentelemetry-instrumentation-fastapi>=0.46b0", + "sqlalchemy>=2.0", + "alembic>=1.13", + "psycopg[binary]>=3.1", + "redis>=5.0", + "httpx>=0.27", +] [project.scripts] keynetra = "keynetra.cli:main" @@ -15,7 +36,7 @@ keynetra = "keynetra.cli:main" include-package-data = true [tool.setuptools.packages.find] -include = ["keynetra*"] +include = ["keynetra*", "integrations*"] [tool.black] line-length = 100 @@ -25,6 +46,8 @@ target-version = ["py311"] profile = "black" line_length = 100 py_version = 311 +src_paths = ["keynetra", "tests", "alembic"] +known_first_party = ["keynetra"] [tool.ruff] line-length = 100 @@ -32,7 +55,8 @@ target-version = "py311" src = ["keynetra", "tests"] [tool.ruff.lint] -select = ["E4", "E7", "E9", "F"] +select = ["E4", "E7", "E9", "F", "B", "UP", "SIM"] +ignore = ["B008"] [tool.pytest.ini_options] testpaths = ["tests"] @@ -44,18 +68,6 @@ source = ["keynetra"] omit = [ "keynetra/api/router.py", "keynetra/engine/model_graph/graph_executor.py", - "keynetra/infrastructure/cache/user_cache.py", - "keynetra/infrastructure/repositories/users.py", - "keynetra/config/security.py", - "keynetra/services/access_indexer.py", - "keynetra/services/audit.py", - "keynetra/services/policy_admin.py", - "keynetra/services/policy_store.py", - "keynetra/services/relationship_store.py", - "keynetra/services/seeding.py", - "keynetra/services/tenant_store.py", - "keynetra/services/user_store.py", - "keynetra/modeling/model_validator.py", ] [tool.coverage.report] @@ -64,16 +76,14 @@ skip_empty = true omit = [ "keynetra/api/router.py", "keynetra/engine/model_graph/graph_executor.py", - "keynetra/infrastructure/cache/user_cache.py", - "keynetra/infrastructure/repositories/users.py", - "keynetra/config/security.py", - "keynetra/services/access_indexer.py", - "keynetra/services/audit.py", - "keynetra/services/policy_admin.py", - "keynetra/services/policy_store.py", - "keynetra/services/relationship_store.py", - "keynetra/services/seeding.py", - "keynetra/services/tenant_store.py", - "keynetra/services/user_store.py", - "keynetra/modeling/model_validator.py", ] + +[tool.mypy] +python_version = "3.11" +ignore_missing_imports = true +warn_unused_ignores = true +warn_redundant_casts = true +warn_unreachable = true +disallow_untyped_defs = false +check_untyped_defs = true +exclude = ["^tests/"] diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..1579309 --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,17 @@ +-r requirements.lock + +black==26.3.1 +build==1.4.2 +coverage[toml]==7.13.5 +isort==8.0.1 +pytest-cov==7.1.0 +pytest==9.0.2 +locust==2.43.4 +mypy==1.20.0 +pip-audit==2.9.0 +pre-commit==4.3.0 +ruff==0.15.9 +safety==3.7.0 +streamlit==1.56.0 +import-linter==2.7 +pip-tools==7.5.1 diff --git a/requirements-dev.txt b/requirements-dev.txt index f60adbd..e267abf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -7,5 +7,11 @@ isort>=5.13 pytest-cov>=5.0 pytest>=8.2 locust>=2.31 +mypy>=1.11 +pip-audit>=2.7 +pre-commit>=3.8 ruff>=0.6 +safety>=3.2 streamlit>=1.36 +import-linter>=2.0 +pip-tools>=7.4 diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..e2fd358 --- /dev/null +++ b/requirements.lock @@ -0,0 +1,20 @@ +fastapi==0.135.3 +uvicorn[standard]==0.43.0 +gunicorn==25.3.0 +pydantic==2.12.5 +pydantic-settings==2.13.1 +python-jose[cryptography]==3.5.0 +PyYAML==6.0.1 +typer==0.24.1 +rich==14.3.3 +pyfiglet==1.0.4 +prometheus-client==0.24.1 +prometheus-fastapi-instrumentator==7.1.0 +opentelemetry-api==1.40.0 +opentelemetry-sdk==1.40.0 +opentelemetry-instrumentation-fastapi==0.61b0 +sqlalchemy==2.0.49 +alembic==1.18.4 +psycopg[binary]==3.3.3 +redis==7.4.0 +httpx==0.28.1 diff --git a/scripts/check_coverage.py b/scripts/check_coverage.py new file mode 100644 index 0000000..963c386 --- /dev/null +++ b/scripts/check_coverage.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import os +from pathlib import Path + +STRICT_ENV = "STRICT_MODULE_COVERAGE" + +MODULE_MINIMUMS = { + "keynetra/services/authorization.py": 85.0, + "keynetra/config/security.py": 80.0, + "keynetra/api/routes/access.py": 85.0, +} + + +def main() -> int: + path = Path("coverage.json") + if not path.exists(): + print("coverage.json not found; run pytest with --cov-report=json") + return 2 + payload = json.loads(path.read_text(encoding="utf-8")) + files = payload.get("files", {}) + failures: list[str] = [] + for file_path, minimum in MODULE_MINIMUMS.items(): + metrics = files.get(file_path) + if not isinstance(metrics, dict): + failures.append(f"{file_path}: missing from coverage report") + continue + summary = metrics.get("summary", {}) + pct = float(summary.get("percent_covered", 0.0)) + if pct < minimum: + failures.append(f"{file_path}: {pct:.2f}% < minimum {minimum:.2f}%") + if failures: + print("module coverage thresholds failed:") + for failure in failures: + print(f" - {failure}") + if os.environ.get(STRICT_ENV, "0") == "1": + return 1 + print(f"notice: set {STRICT_ENV}=1 to enforce module percentage gates") + return 0 + print("module coverage thresholds passed") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check_load_budget.py b/scripts/check_load_budget.py new file mode 100644 index 0000000..009477c --- /dev/null +++ b/scripts/check_load_budget.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import csv +import re +from pathlib import Path + +import httpx + +P95_BUDGET_MS = 500.0 +CACHE_HIT_RATIO_MIN = 0.10 + + +def _parse_p95(path: Path) -> float: + if not path.exists(): + raise FileNotFoundError(path) + with path.open("r", encoding="utf-8") as fh: + reader = csv.DictReader(fh) + for row in reader: + if row.get("Name") == "Aggregated": + return float(row.get("95%", "0") or "0") + return 0.0 + + +def _cache_hit_ratio(metrics_text: str) -> float: + hit = 0.0 + miss = 0.0 + hit_pattern = re.compile(r'^keynetra_cache_hits_total\{cache_type="decision"\}\s+([0-9.]+)$') + miss_pattern = re.compile(r'^keynetra_cache_misses_total\{cache_type="decision"\}\s+([0-9.]+)$') + for line in metrics_text.splitlines(): + mh = hit_pattern.match(line) + if mh: + hit = float(mh.group(1)) + continue + mm = miss_pattern.match(line) + if mm: + miss = float(mm.group(1)) + denom = hit + miss + return (hit / denom) if denom > 0 else 0.0 + + +def main() -> int: + p95 = _parse_p95(Path("/tmp/locust_stats.csv")) + if p95 > P95_BUDGET_MS: + print(f"p95 latency budget failed: {p95:.2f}ms > {P95_BUDGET_MS:.2f}ms") + return 1 + metrics = httpx.get("http://127.0.0.1:8000/metrics", timeout=5.0).text + ratio = _cache_hit_ratio(metrics) + if ratio < CACHE_HIT_RATIO_MIN: + print(f"decision cache hit ratio budget failed: {ratio:.3f} < {CACHE_HIT_RATIO_MIN:.3f}") + return 1 + print(f"load budgets passed: p95={p95:.2f}ms cache_hit_ratio={ratio:.3f}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/load_tests/README.md b/scripts/load_tests/README.md new file mode 100644 index 0000000..44ea6d2 --- /dev/null +++ b/scripts/load_tests/README.md @@ -0,0 +1,19 @@ +# Scenario Load Tests + +Scenario-driven Locust profiles for KeyNetra performance smoke checks. + +## Scenarios + +- `rbac_heavy_locust.py`: role/permission dominated checks +- `rebac_graph_locust.py`: relationship-heavy checks +- `multi_tenant_locust.py`: tenant/policy version variation +- `cache_warm_cold_locust.py`: warm and cold cache behavior + +## Run + +```bash +KEYNETRA_API_KEYS=devkey keynetra serve +locust -f scripts/load_tests/rbac_heavy_locust.py --host http://127.0.0.1:8000 --headless -u 25 -r 5 -t 30s +``` + +Use `--csv /tmp/locust` to export p95/throughput snapshots for CI gates. diff --git a/scripts/load_tests/cache_warm_cold_locust.py b/scripts/load_tests/cache_warm_cold_locust.py new file mode 100644 index 0000000..7d2ade8 --- /dev/null +++ b/scripts/load_tests/cache_warm_cold_locust.py @@ -0,0 +1,35 @@ +from __future__ import annotations + +from locust import HttpUser, between, task + + +class CacheWarmColdUser(HttpUser): + wait_time = between(0.01, 0.05) + + @task(5) + def warm_path(self) -> None: + self.client.post( + "/check-access", + headers={"X-API-Key": "devkey"}, + json={ + "user": {"id": 1, "role": "member"}, + "action": "read", + "resource": {"id": "doc-warm"}, + "context": {}, + }, + name="cache/warm", + ) + + @task(1) + def cold_path(self) -> None: + self.client.post( + "/check-access", + headers={"X-API-Key": "devkey"}, + json={ + "user": {"id": 1, "role": "member"}, + "action": "read", + "resource": {"id": "doc-cold"}, + "context": {"nonce": "cold"}, + }, + name="cache/cold", + ) diff --git a/scripts/load_tests/multi_tenant_locust.py b/scripts/load_tests/multi_tenant_locust.py new file mode 100644 index 0000000..62e07d6 --- /dev/null +++ b/scripts/load_tests/multi_tenant_locust.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from itertools import cycle + +from locust import HttpUser, between, task + +_TENANTS = cycle(["tenant-a", "tenant-b", "tenant-c"]) + + +class MultiTenantUser(HttpUser): + wait_time = between(0.01, 0.05) + + @task + def check_access_multi_tenant(self) -> None: + tenant = next(_TENANTS) + self.client.post( + "/check-access", + headers={"X-API-Key": "devkey"}, + params={"policy_set": "active"}, + json={ + "user": {"id": f"{tenant}-u1", "role": "member"}, + "action": "view_dashboard", + "resource": {"id": f"{tenant}-dashboard"}, + "context": {"tenant": tenant}, + }, + name="multi-tenant/check-access", + ) diff --git a/scripts/load_tests/rbac_heavy_locust.py b/scripts/load_tests/rbac_heavy_locust.py new file mode 100644 index 0000000..e2f577e --- /dev/null +++ b/scripts/load_tests/rbac_heavy_locust.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from locust import HttpUser, between, task + + +class RBACHeavyUser(HttpUser): + wait_time = between(0.01, 0.05) + + @task + def check_access_rbac(self) -> None: + self.client.post( + "/check-access", + headers={"X-API-Key": "devkey"}, + json={ + "user": {"id": "u-rbac-1", "role": "manager", "permissions": ["approve_payment"]}, + "action": "approve_payment", + "resource": {"id": "invoice-1", "amount": 500}, + "context": {}, + }, + name="rbac/check-access", + ) diff --git a/scripts/load_tests/rebac_graph_locust.py b/scripts/load_tests/rebac_graph_locust.py new file mode 100644 index 0000000..00dcf87 --- /dev/null +++ b/scripts/load_tests/rebac_graph_locust.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from locust import HttpUser, between, task + + +class ReBACGraphUser(HttpUser): + wait_time = between(0.01, 0.05) + + @task + def check_access_rebac(self) -> None: + self.client.post( + "/check-access", + headers={"X-API-Key": "devkey"}, + json={ + "user": {"id": "u-rebac-1"}, + "action": "read_document", + "resource": {"resource_type": "document", "resource_id": "doc-42"}, + "context": {}, + }, + name="rebac/check-access", + ) diff --git a/tests/test_access_routes_tenant.py b/tests/test_access_routes_tenant.py new file mode 100644 index 0000000..96b028a --- /dev/null +++ b/tests/test_access_routes_tenant.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +from types import SimpleNamespace + +import pytest +from fastapi import Request + +from keynetra.api.errors import ApiError, ApiErrorCode +from keynetra.api.routes.access import _resolve_tenant_key +from keynetra.config.tenancy import DEFAULT_TENANT_KEY, TENANT_HEADER_NAME + + +class DummyServices(SimpleNamespace): + pass + + +def request_with_header(value: str | None) -> Request: + return SimpleNamespace( + headers={TENANT_HEADER_NAME: value} if value else {}, + state=SimpleNamespace(), + ) + + +def principal_with_tenant(tenant: str | None): + if not tenant: + return {} + return {"type": "jwt", "claims": {"tenant": tenant}} + + +def test_resolve_tenant_from_header(): + request = request_with_header("acme") + principal = principal_with_tenant(None) + services = DummyServices( + settings=SimpleNamespace(strict_tenancy=False, is_development=lambda: False) + ) + assert _resolve_tenant_key(request=request, principal=principal, services=services) == "acme" + + +def test_resolve_tenant_falls_back_to_principal(): + request = request_with_header(None) + principal = principal_with_tenant("tenant-x") + services = DummyServices( + settings=SimpleNamespace(strict_tenancy=False, is_development=lambda: False) + ) + assert ( + _resolve_tenant_key(request=request, principal=principal, services=services) == "tenant-x" + ) + + +def test_resolve_tenant_development_default(): + request = request_with_header(None) + principal = {} + services = DummyServices( + settings=SimpleNamespace(strict_tenancy=False, is_development=lambda: True) + ) + assert ( + _resolve_tenant_key(request=request, principal=principal, services=services) + == DEFAULT_TENANT_KEY + ) + + +def test_resolve_tenant_strict_without_tenant_raises(): + request = request_with_header(None) + principal = {} + settings = SimpleNamespace(strict_tenancy=True, is_development=lambda: False) + services = DummyServices(settings=settings) + with pytest.raises(ApiError) as exc: + _resolve_tenant_key(request=request, principal=principal, services=services) + assert exc.value.code == ApiErrorCode.VALIDATION_ERROR diff --git a/tests/test_admin_audit.py b/tests/test_admin_audit.py index e664eb0..ee6e40b 100644 --- a/tests/test_admin_audit.py +++ b/tests/test_admin_audit.py @@ -1,7 +1,7 @@ from __future__ import annotations import os -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from fastapi.testclient import TestClient from jose import jwt @@ -14,6 +14,9 @@ def _client(database_url: str) -> TestClient: os.environ["KEYNETRA_DATABASE_URL"] = database_url os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"admin","permissions":["*"]}}' + ) os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" reset_settings_cache() initialize_database(database_url) @@ -44,7 +47,7 @@ def test_viewer_can_list_but_cannot_mutate_management_api(tmp_path) -> None: == 201 ) - viewer_headers = _jwt_headers(tenant_key="tenant-a", role="viewer") + viewer_headers = _jwt_headers(tenant_key="default", role="viewer") listed = client.get("/policies", headers=viewer_headers) denied = client.post( "/policies", @@ -92,7 +95,7 @@ def test_admin_required_for_global_management_writes(tmp_path) -> None: def test_audit_endpoints_support_filters_time_range_and_pagination(tmp_path) -> None: client = _client(f"sqlite+pysqlite:///{tmp_path / 'audit.db'}") admin_headers = {"X-API-Key": "testkey"} - viewer_headers = _jwt_headers(tenant_key="tenant-a", role="viewer") + viewer_headers = _jwt_headers(tenant_key="default", role="viewer") assert ( client.post( @@ -129,8 +132,8 @@ def test_audit_endpoints_support_filters_time_range_and_pagination(tmp_path) -> == 200 ) - start_time = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat() - end_time = (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat() + start_time = (datetime.now(UTC) - timedelta(minutes=5)).isoformat() + end_time = (datetime.now(UTC) + timedelta(minutes=5)).isoformat() page_one = client.get( "/audit", @@ -157,3 +160,19 @@ def test_audit_endpoints_support_filters_time_range_and_pagination(tmp_path) -> assert len(deny_only.json()["data"]) == 1 assert deny_only.json()["data"][0]["decision"] == "DENY" assert deny_only.json()["data"][0]["user"]["id"] == "u2" + + +def test_api_key_without_role_scope_is_rejected_for_management_routes(tmp_path) -> None: + database_url = f"sqlite+pysqlite:///{tmp_path / 'no-role.db'}" + os.environ["KEYNETRA_DATABASE_URL"] = database_url + os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = '{"testkey":{"tenant":"default","permissions":[]}}' + os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" + reset_settings_cache() + initialize_database(database_url) + client = TestClient(create_app()) + + response = client.post("/roles", json={"name": "viewer"}, headers={"X-API-Key": "testkey"}) + + assert response.status_code == 403 + assert response.json()["error"]["message"] == "tenant access denied" diff --git a/tests/test_api.py b/tests/test_api.py index 7556ccf..ba19d55 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -301,6 +301,7 @@ def test_simulate_policy_and_impact_analysis_endpoints_work_for_admin_api_key() import os os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ.pop("KEYNETRA_API_KEY_SCOPES_JSON", None) os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" os.environ["KEYNETRA_RATE_LIMIT_BURST"] = "1000" os.environ["KEYNETRA_RATE_LIMIT_WINDOW_SECONDS"] = "60" diff --git a/tests/test_auth_model.py b/tests/test_auth_model.py index d861786..a77575f 100644 --- a/tests/test_auth_model.py +++ b/tests/test_auth_model.py @@ -60,6 +60,9 @@ def test_auth_model_route_round_trip(tmp_path) -> None: database_url = f"sqlite+pysqlite:///{tmp_path / 'auth-model.db'}" os.environ["KEYNETRA_DATABASE_URL"] = database_url os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"developer","permissions":["auth_model:write"]}}' + ) os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" os.environ["KEYNETRA_RATE_LIMIT_BURST"] = "1000" reset_settings_cache() diff --git a/tests/test_consistency_revisions.py b/tests/test_consistency_revisions.py index 73f337a..b17ad51 100644 --- a/tests/test_consistency_revisions.py +++ b/tests/test_consistency_revisions.py @@ -23,6 +23,9 @@ def test_revision_token_increments_across_model_and_acl_changes(tmp_path) -> Non database_url = f"sqlite+pysqlite:///{tmp_path / 'revisions.db'}" os.environ["KEYNETRA_DATABASE_URL"] = database_url os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"developer","permissions":["auth_model:write","acl:write"]}}' + ) os.environ["KEYNETRA_POLICIES_JSON"] = "[]" os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" os.environ["KEYNETRA_RATE_LIMIT_BURST"] = "1000" diff --git a/tests/test_doctor.py b/tests/test_doctor.py index 60227b6..e88bda5 100644 --- a/tests/test_doctor.py +++ b/tests/test_doctor.py @@ -43,7 +43,7 @@ def test_run_core_doctor_reports_all_checks_healthy( ) -> None: database_url = f"sqlite+pysqlite:///{tmp_path}/core-doctor.db" _set_core_env(database_url) - _prepare_alembic_version(database_url, "20260405_000008") + _prepare_alembic_version(database_url, "20260407_000001") monkeypatch.setattr("keynetra.services.doctor.get_redis", lambda: _FakeRedis()) result = run_core_doctor(Settings()) diff --git a/tests/test_idempotency.py b/tests/test_idempotency.py index 04fd854..358850f 100644 --- a/tests/test_idempotency.py +++ b/tests/test_idempotency.py @@ -17,6 +17,9 @@ def _build_client(database_url: str) -> TestClient: os.environ["KEYNETRA_DATABASE_URL"] = database_url os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"developer","permissions":["policies:write","relationships:write"]}}' + ) reset_settings_cache() initialize_database(database_url) return TestClient(create_app()) diff --git a/tests/test_integrations_scaffolding.py b/tests/test_integrations_scaffolding.py new file mode 100644 index 0000000..f65869f --- /dev/null +++ b/tests/test_integrations_scaffolding.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from integrations.interfaces import TupleRecord +from integrations.opa_rego_adapter import OPARegoPolicyAdapter +from integrations.openfga_adapter import InMemoryOpenFGATupleAdapter +from integrations.terraform_provider import TerraformPolicyResourceAdapter + + +def test_openfga_adapter_round_trip() -> None: + adapter = InMemoryOpenFGATupleAdapter() + inserted = adapter.import_tuples( + [TupleRecord(subject="user:1", relation="viewer", object="doc:1")] + ) + assert inserted == 1 + exported = adapter.export_tuples() + assert len(exported) == 1 + assert exported[0].relation == "viewer" + + +def test_opa_adapter_round_trip() -> None: + adapter = OPARegoPolicyAdapter() + count = adapter.import_policies("package keynetra\nallow { true }\n") + assert count >= 1 + assert "allow" in adapter.export_policies() + + +def test_terraform_adapter_plan_apply() -> None: + adapter = TerraformPolicyResourceAdapter(policy_count=3) + assert adapter.plan()["changes"] == 3 + assert adapter.apply()["applied"] is True diff --git a/tests/test_management_routes_coverage.py b/tests/test_management_routes_coverage.py index 023472d..da11dd4 100644 --- a/tests/test_management_routes_coverage.py +++ b/tests/test_management_routes_coverage.py @@ -12,6 +12,9 @@ def _client(database_url: str) -> TestClient: os.environ["KEYNETRA_DATABASE_URL"] = database_url os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"admin","permissions":["*"]}}' + ) os.environ.pop("KEYNETRA_REDIS_URL", None) reset_settings_cache() initialize_database(database_url) diff --git a/tests/test_pagination_versioning_security.py b/tests/test_pagination_versioning_security.py index 015c4bc..7ffcddf 100644 --- a/tests/test_pagination_versioning_security.py +++ b/tests/test_pagination_versioning_security.py @@ -23,6 +23,9 @@ def _client(database_url: str) -> TestClient: def test_roles_cursor_pagination_and_version_header(tmp_path) -> None: database_url = f"sqlite+pysqlite:///{tmp_path / 'roles.db'}" os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"admin","permissions":["*"]}}' + ) client = _client(database_url) first = client.post("/roles", json={"name": "admin"}, headers={"X-API-Key": "testkey"}) @@ -48,6 +51,9 @@ def test_roles_cursor_pagination_and_version_header(tmp_path) -> None: def test_policies_cursor_pagination(tmp_path) -> None: database_url = f"sqlite+pysqlite:///{tmp_path / 'policies.db'}" os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"admin","permissions":["*"]}}' + ) client = _client(database_url) headers = {"X-API-Key": "testkey"} diff --git a/tests/test_policy_lint.py b/tests/test_policy_lint.py index 57a2e18..5826e13 100644 --- a/tests/test_policy_lint.py +++ b/tests/test_policy_lint.py @@ -27,6 +27,9 @@ def test_policy_creation_emits_role_warning(tmp_path) -> None: os.environ["KEYNETRA_DATABASE_URL"] = database_url _setup_database(database_url) os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"developer","permissions":["policies:write"]}}' + ) reset_settings_cache() client = TestClient(create_app()) headers = {"X-API-Key": "testkey"} diff --git a/tests/test_policy_state_canary.py b/tests/test_policy_state_canary.py new file mode 100644 index 0000000..05dbe41 --- /dev/null +++ b/tests/test_policy_state_canary.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import os + +from fastapi.testclient import TestClient + +from keynetra.config.settings import reset_settings_cache +from keynetra.infrastructure.storage.session import initialize_database +from keynetra.main import create_app + + +def test_draft_policy_set_isolated_from_active(tmp_path) -> None: + database_url = f"sqlite+pysqlite:///{tmp_path}/policy-state.db" + os.environ["KEYNETRA_DATABASE_URL"] = database_url + os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = ( + '{"testkey":{"tenant":"default","role":"developer","permissions":["policies:write"]}}' + ) + os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" + os.environ["KEYNETRA_RATE_LIMIT_BURST"] = "1000" + reset_settings_cache() + initialize_database(database_url) + client = TestClient(create_app()) + headers = {"X-API-Key": "testkey"} + + active = client.post( + "/policies", + headers=headers, + json={ + "action": "download_report", + "effect": "deny", + "priority": 1, + "state": "active", + "conditions": {"policy_key": "download_report"}, + }, + ) + assert active.status_code == 201 + + draft = client.put( + "/policies/download_report", + headers=headers, + json={ + "action": "download_report", + "effect": "allow", + "priority": 1, + "state": "draft", + "conditions": {"policy_key": "download_report"}, + }, + ) + assert draft.status_code == 200 + + payload = {"user": {"id": 1}, "action": "download_report", "resource": {"id": "r1"}} + active_eval = client.post("/check-access", headers=headers, json=payload) + draft_eval = client.post("/check-access?policy_set=draft", headers=headers, json=payload) + assert active_eval.status_code == 200 + assert draft_eval.status_code == 200 + assert active_eval.json()["data"]["allowed"] is False + assert draft_eval.json()["data"]["allowed"] is True diff --git a/tests/test_release_hardening.py b/tests/test_release_hardening.py index 54f9f40..0ae6979 100644 --- a/tests/test_release_hardening.py +++ b/tests/test_release_hardening.py @@ -1,5 +1,6 @@ from __future__ import annotations +import asyncio import hashlib from types import SimpleNamespace @@ -20,11 +21,7 @@ check_access_batch, ) from keynetra.api.routes.access import simulate as access_simulate -from keynetra.api.routes.dev import ( - _require_local_dev, - get_sample_data, - seed_sample_data, -) +from keynetra.api.routes.dev import _require_local_dev, get_sample_data, seed_sample_data from keynetra.api.routes.simulation import ( ImpactAnalysisRequest, PolicySimulationRequest, @@ -34,10 +31,7 @@ ) from keynetra.cli import app from keynetra.config.admin_auth import AdminAccess, _resolve_tenant_role, require_management_role -from keynetra.config.security import ( - _matches_api_key, - get_principal, -) +from keynetra.config.security import _matches_api_key, get_principal from keynetra.config.settings import Settings, reset_settings_cache from keynetra.domain.models.base import Base from keynetra.domain.models.rbac import Permission, Role @@ -151,7 +145,9 @@ def test_get_principal_supports_api_key_and_bearer_jwt( ) -> None: request = DummyRequest() monkeypatch.setattr("keynetra.config.security._matches_api_key", lambda *_: True) - api_key_settings = Settings() + api_key_settings = Settings( + api_key_scopes_json='{"test-key":{"tenant":"default","role":"developer","permissions":["*"]}}' + ) api_key_principal = get_principal( request, @@ -210,7 +206,8 @@ def test_get_principal_rejects_invalid_jwt() -> None: def test_resolve_tenant_role_covers_list_and_dict_claims() -> None: - assert _resolve_tenant_role({"type": "api_key"}) == "admin" + assert _resolve_tenant_role({"type": "api_key"}) is None + assert _resolve_tenant_role({"type": "api_key", "scopes": {"role": "admin"}}) == "admin" assert _resolve_tenant_role({"claims": {"tenant_roles": {"acme": "developer"}}}) == "developer" assert _resolve_tenant_role({"claims": {"tenant_roles": [{"role": "viewer"}]}}) == "viewer" assert _resolve_tenant_role({"claims": {"roles": ["developer", "viewer"]}}) == "developer" @@ -220,7 +217,10 @@ def test_require_management_role_resolves_and_enforces_roles() -> None: request = DummyRequest() dependency = require_management_role("developer") - access = dependency(request, principal={"type": "api_key", "id": "test"}) + access = dependency( + request, + principal={"type": "api_key", "id": "test", "scopes": {"role": "admin"}}, + ) assert access.role == "admin" assert request.state.admin_role == "admin" @@ -434,37 +434,47 @@ def get_revision(self, *, tenant_key: str) -> int: # noqa: ARG002 request = DummyRequest() service = FakeAccessService() - - check = check_access( - payload=AccessRequest( - user={"id": 1}, action="read", resource={}, context={}, consistency="eventual" - ), - request=request, - service=service, - principal={"type": "api_key"}, + services = SimpleNamespace(settings=SimpleNamespace(async_authorization_enabled=False)) + + check = asyncio.run( + check_access( + payload=AccessRequest( + user={"id": 1}, action="read", resource={}, context={}, consistency="eventual" + ), + request=request, + service=service, + services=services, + principal={"type": "api_key"}, + ) ) assert check["data"]["decision"] == "allow" assert check["data"]["revision"] == 9 - simulated = access_simulate( - payload=AccessRequest( - user={"id": 1}, action="read", resource={}, context={}, consistency="eventual" - ), - request=request, - service=service, - principal={"type": "api_key"}, + simulated = asyncio.run( + access_simulate( + payload=AccessRequest( + user={"id": 1}, action="read", resource={}, context={}, consistency="eventual" + ), + request=request, + service=service, + services=services, + principal={"type": "api_key"}, + ) ) assert simulated["data"]["decision"] == "deny" - batch = check_access_batch( - payload=BatchAccessRequest( - user={"id": 1}, - items=[{"action": "read"}, {"action": "write"}], - consistency="eventual", - ), - request=request, - service=service, - principal={"type": "api_key"}, + batch = asyncio.run( + check_access_batch( + payload=BatchAccessRequest( + user={"id": 1}, + items=[{"action": "read"}, {"action": "write"}], + consistency="eventual", + ), + request=request, + service=service, + services=services, + principal={"type": "api_key"}, + ) ) assert batch["data"]["results"] == [ {"action": "read", "allowed": True, "revision": 1}, @@ -509,7 +519,14 @@ def analyze_policy_change(self, **_: object) -> SimpleNamespace: _require_local_dev(Settings(environment="development")) with pytest.raises(ApiError): - _require_local_dev(Settings(environment="production")) + _require_local_dev( + Settings( + environment="production", + api_keys="prod-key", + api_key_scopes_json='{"prod-key":{"tenant":"default","role":"viewer","permissions":["*"]}}', + jwt_secret="strong-prod-secret", + ) + ) request = DummyRequest() sample = get_sample_data(request=request, settings=Settings(environment="development")) @@ -517,7 +534,7 @@ def analyze_policy_change(self, **_: object) -> SimpleNamespace: seeded = seed_sample_data( request=request, - db=object(), + services=SimpleNamespace(db=object()), settings=Settings(environment="development"), reset=True, ) @@ -539,7 +556,7 @@ def analyze_policy_change(self, **_: object) -> SimpleNamespace: request=normalized, ), request=request, - deps=(SimpleNamespace(), FakeSimulator(), FakeImpact()), + services=SimpleNamespace(policy_simulator=FakeSimulator(), impact_analyzer=FakeImpact()), access=AdminAccess(tenant_key="default", role="viewer", principal={"type": "api_key"}), ) assert simulation["data"]["decision_before"]["decision"] == "deny" @@ -548,7 +565,7 @@ def analyze_policy_change(self, **_: object) -> SimpleNamespace: impact = impact_analysis( payload=ImpactAnalysisRequest(policy_change="allow read"), request=request, - deps=(SimpleNamespace(), FakeSimulator(), FakeImpact()), + services=SimpleNamespace(policy_simulator=FakeSimulator(), impact_analyzer=FakeImpact()), access=AdminAccess(tenant_key="default", role="viewer", principal={"type": "api_key"}), ) assert impact["data"]["gained_access"] == [1, 2] @@ -871,6 +888,10 @@ def test_management_routes_cover_permissions_roles_and_acl( database_url = f"sqlite+pysqlite:///{tmp_path / 'management.db'}" monkeypatch.setenv("KEYNETRA_DATABASE_URL", database_url) monkeypatch.setenv("KEYNETRA_API_KEYS", "testkey") + monkeypatch.setenv( + "KEYNETRA_API_KEY_SCOPES_JSON", + '{"testkey":{"tenant":"default","role":"admin","permissions":["*"]}}', + ) monkeypatch.setenv("KEYNETRA_RATE_LIMIT_PER_MINUTE", "1000") monkeypatch.setenv("KEYNETRA_RATE_LIMIT_BURST", "1000") reset_settings_cache() diff --git a/tests/test_security_config_module.py b/tests/test_security_config_module.py new file mode 100644 index 0000000..a6fd555 --- /dev/null +++ b/tests/test_security_config_module.py @@ -0,0 +1,89 @@ +from __future__ import annotations + +import hashlib +from types import SimpleNamespace + +import pytest +from fastapi import HTTPException + +from keynetra.config.security import _matches_api_key, _scopes_are_defined, get_principal + + +class DummyRequest(SimpleNamespace): + def __init__(self, *, headers=None, client=None, state=None, method="GET", url=None): + super().__init__() + self.headers = headers or {} + self.client = client + self.method = method + self.state = state or SimpleNamespace() + self.url = SimpleNamespace(path=url or "/") + + +class DummySettings: + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + self._api_key_hashes = {"key-hash"} + self.jwks_cache_ttl_seconds = 60 + self.jwks_backoff_max_seconds = 60 + self.jwt_secret = kwargs.get("jwt_secret", "secret") + self.jwt_algorithm = kwargs.get("jwt_algorithm", "HS256") + self.oidc_jwks_url = kwargs.get("oidc_jwks_url") + self.oidc_audience = kwargs.get("oidc_audience") + self.oidc_issuer = kwargs.get("oidc_issuer") + self._development = kwargs.get("development", False) + + def parsed_api_key_hashes(self) -> set[str]: + return self._api_key_hashes + + def parsed_api_key_scopes(self) -> dict[str, dict[str, object]]: + return {list(self._api_key_hashes)[0]: {"role": "admin"}} + + def is_development(self) -> bool: + return self._development + + +def test_matches_api_key_returns_true_for_valid_candidate(): + stored = hashlib.sha256(b"secret").hexdigest() + assert _matches_api_key("secret", {stored}) + + +def test_matches_api_key_returns_false_for_invalid_candidate(): + stored = hashlib.sha256(b"secret").hexdigest() + assert not _matches_api_key("bad", {stored}) + + +def test_scopes_defined_with_role_and_permission(): + assert _scopes_are_defined({"role": "admin"}) + assert _scopes_are_defined({"permissions": ["read"]}) + + +def test_scopes_undefined_without_role_or_permissions(): + assert not _scopes_are_defined({"role": ""}) + assert not _scopes_are_defined({"permissions": []}) + + +def test_get_principal_raises_without_credentials(monkeypatch): + request = DummyRequest() + settings = DummySettings() + monkeypatch.setattr("keynetra.config.security.get_settings", lambda: settings) + with pytest.raises(HTTPException): + get_principal(request, settings=settings, authorization=None, x_api_key=None) + + +def test_get_principal_returns_jwt_structure(monkeypatch): + request = DummyRequest() + token = SimpleNamespace(scheme="bearer", credentials="token") + settings = DummySettings(jwt_secret="secret", jwt_algorithm="HS256") + monkeypatch.setattr("keynetra.config.security.get_settings", lambda: settings) + monkeypatch.setattr( + "keynetra.config.security.jwt.decode", + lambda token, key, algorithms: {"sub": "user:1"}, + ) + principal = get_principal( + request, + settings=settings, + authorization=token, + x_api_key=None, + ) + assert principal["type"] == "jwt" + assert principal["id"] == "user:1" diff --git a/tests/test_strict_tenancy.py b/tests/test_strict_tenancy.py new file mode 100644 index 0000000..2d6fd96 --- /dev/null +++ b/tests/test_strict_tenancy.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import os + +from fastapi.testclient import TestClient +from jose import jwt + +from keynetra.config.settings import reset_settings_cache +from keynetra.infrastructure.storage.session import initialize_database +from keynetra.main import create_app + + +def _strict_client(database_url: str, *, scopes_json: str) -> TestClient: + os.environ["KEYNETRA_DATABASE_URL"] = database_url + os.environ["KEYNETRA_API_KEYS"] = "testkey" + os.environ["KEYNETRA_API_KEY_SCOPES_JSON"] = scopes_json + os.environ["KEYNETRA_RATE_LIMIT_PER_MINUTE"] = "1000" + os.environ["KEYNETRA_STRICT_TENANCY"] = "true" + reset_settings_cache() + initialize_database(database_url) + return TestClient(create_app()) + + +def test_check_access_requires_explicit_tenant_when_strict(tmp_path) -> None: + client = _strict_client( + f"sqlite+pysqlite:///{tmp_path / 'strict-access.db'}", + scopes_json='{"testkey":{"role":"admin","permissions":["*"]}}', + ) + headers = {"X-API-Key": "testkey"} + + missing_tenant = client.post( + "/check-access", + json={ + "user": {"id": "u1"}, + "action": "read", + "resource": {"id": "doc-1"}, + "context": {}, + }, + headers=headers, + ) + assert missing_tenant.status_code == 422 + assert missing_tenant.json()["error"]["message"] == "tenant is required" + + explicit_tenant = client.post( + "/check-access", + json={ + "user": {"id": "u1"}, + "action": "read", + "resource": {"id": "doc-1"}, + "context": {}, + }, + headers={**headers, "X-Tenant-Id": "acme"}, + ) + assert explicit_tenant.status_code == 200 + + +def test_management_routes_require_tenant_when_strict(tmp_path) -> None: + client = _strict_client( + f"sqlite+pysqlite:///{tmp_path / 'strict-management.db'}", + scopes_json='{"testkey":{"role":"admin","permissions":["*"]}}', + ) + token = jwt.encode({"sub": "viewer", "role": "viewer"}, "change-me", algorithm="HS256") + jwt_headers = {"Authorization": f"Bearer {token}"} + + missing_tenant = client.get("/roles", headers=jwt_headers) + assert missing_tenant.status_code == 422 + assert missing_tenant.json()["error"]["message"] == "tenant is required" + + explicit_tenant = client.get("/roles", headers={**jwt_headers, "X-Tenant-Id": "acme"}) + assert explicit_tenant.status_code == 200