Metadata-Version: 2.4
Name: motp-admin
Version: 0.1.1
Summary: multiOTP Credential Provider administration tooling (P0 to P2 MVP).
Author: SysCo systemes de communication sa
License: Apache-2.0
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: qrcode[pil]>=7.3
Requires-Dist: pyzipper>=0.3.6
Requires-Dist: fpdf2>=2.7
Requires-Dist: pyotp>=2.9
Requires-Dist: ldap3>=2.9
Requires-Dist: flask>=3.0
Dynamic: license-file

# motp-admin (P0 / MVP)

Pre-stage one of the multiOTP Credential Provider admin tooling. The
Python package under `motp_admin/` ships the whole P0 capability:

- CSV import with deterministic account keys (UPN, `DOMAIN\user`,
  local account).
- Pluggable user provisioner (`inmemory` driver today; multiOTP CLI
  driver stubbed and ready to wire).
- Batch execution with `batch.json` checkpoints, per-user outcomes,
  and a resumable state file.
- Encrypted AES-256 ZIP enrollment pack using `pyzipper`.
- Append-only JSONL audit log keyed by `(actor, action, target, result)`.
- Rollback command that plays the captured `would-create` snapshot
  back through the provider.

A thin PowerShell module lives under `powershell/` and forwards every
verb to the Python CLI:

```powershell
Import-Module ./powershell/MOTPAdmin.psm1
Invoke-MotpCapability
Start-MotpEnrollment -Csv users.csv -Out out -Issuer "Example"
Undo-MotpBulkOperation -BatchFile out/<batch-id>.batch.json -DryRun
```

## Smoke test

```bash
cd tools/admin
PYTHONPATH=. python3 -m motp_admin enroll --csv tests/fixtures/users.csv \
    --out ./out --issuer "Example" --provider inmemory
PYTHONPATH=. python3 -m motp_admin rollback ./out/<batch-id>.batch.json --dry-run
PYTHONPATH=. python3 tests/test_account_map.py
PYTHONPATH=. python3 tests/test_rollback.py
```

## Notes for production wiring

- Replace the in-memory provisioner with `MultiOtpCliProvisioner`
  from `motp_admin/providers/multiotp_cli.py`.
- The CLI driver shells out without ever logging the seed; the QR
  URI returned by `assign_token` is the only place a secret appears
  in memory, and it goes straight into `pyzipper` for the pack.
- Capability detection (`invoke-motp-capability`) is the first
  command an operator should run on a new host.


## Policy store (P2)

The `policy.py` module now ships a JSON-backed access-policy store.
Each user can carry:

- `valid_from` / `valid_until` (ISO-8601 in UTC or site timezone)
- `allowed_login_times` parsed from strings like `Mon-Fri 08:00-18:00`
- `time_zone` (defaults to UTC)
- `holidays` (path to a `date,label` CSV)
- `review_at` for the next manual review

Use the CLI:

```bash
PYTHONPATH=. python3 -m motp_admin policy-set   --user alice@example.com   --valid-from 2026-01-01T00:00:00   --valid-until 2026-12-31T23:59:59   --allowed "Mon-Fri 08:00-18:00"   --timezone Asia/Shanghai   --holidays tests/fixtures/holidays.csv   --policy-file smoke_policy/policies.json

PYTHONPATH=. python3 -m motp_admin policy-check   --user alice@example.com   --at 2026-10-02T10:00:00+08:00   --policy-file smoke_policy/policies.json
```

PowerShell equivalents: `Set-MotpPolicy`, `Get-MotpPolicy`, `Test-MotpPolicy`.


## Self-binding portal (P2 beta)

A minimal Flask app is part of the package and reachable via:

```bash
PYTHONPATH=. python3 -m motp_admin serve-portal --port 8088   --policy-file smoke_policy/policies.json --issuer "Example Corp"
```

Endpoints:

- `POST /bind` with `{"username":"...", "display_name":"...", "email":"..."}`
  returns a `session_id`, the otpauth URI and a base64 PNG QR; expires in 5 minutes.
- `POST /bind/verify` with `{"session_id":"...", "otp":"123456"}`
- `GET /status` returns active binding sessions and known users

PowerShell wrappers: `Start-MotpPortal`, `Test-MotpPortal.Bind`,
`Test-MotpPortal.Verify`. A Pester scaffold for the module is at
`tools/admin/tests/MOTPAdmin.Tests.ps1`.

## Packaging

```bash
pip install -e .
motp-admin capability
```

The package metadata lives in `pyproject.toml`; it bundles
`qrcode[pil]`, `pyzipper`, `fpdf2`, `pyotp`, `ldap3`, and `flask`.


## Release flow (wheel + Windows .exe)

```bash
# 1. Publish the wheel and sdist to the Python index
TWINE_USERNAME=… TWINE_PASSWORD=… ./scripts/release.sh --upload

# 2. Push the git tag and attach the Windows .exe via GitHub Actions
git tag v0.1.0 && git push --tags
./scripts/release_windows.sh v0.1.0
```

`scripts/release_windows.sh` requires:

- the GitHub CLI (`gh`) authenticated with write/release scopes
- `jq`
- a tag that has been pushed to the remote (`git push --tags`)

It will create the release if missing, trigger
`.github/workflows/windows-build.yml`, wait for the run, download the
`motp-admin.exe` artifact and attach it to the release (overwriting any
previous copy).


## PyPI OIDC trusted publishing

Push a version tag (`vX.Y.Z`) and CI's `release` workflow handles
publishing via an OIDC trusted publisher — no `TWINE_USERNAME`,
no `TWINE_PASSWORD`. Configure PyPI once:

1. PyPI project page → Publishing → Add a new pending publisher
   - Owner: `<org>/multiOTPCredentialProvider`
   - Repository name: `multiOTPCredentialProvider`
   - Workflow filename: `release.yml`
   - Environment name: `pypi`
2. GitHub repository → Settings → Environments → New environment `pypi`
   (no secrets required; the environment just records who may publish).


## Signing the Windows .exe

`scripts/sign_windows.ps1` wraps `signtool`, computes a SHA256
sidecar and (when called from CI) leaves a verified signature.

In CI the signing step is optional and only runs when both
secrets `WINDOWS_SIGN_CERT` (base64-encoded .pfx) and
`WINDOWS_SIGN_PASSWORD` are present. The `.sha256` file is always
written next to `motp-admin.exe`.


## PowerShell Gallery publishing

```powershell
# Local pack only
pwsh ./scripts/publish_psgallery.ps1 -LocalOnly

# Publish to PSGallery (needs PSGALLERY_API_KEY secret)
$env:PSGALLERY_API_KEY = "…"
pwsh ./scripts/publish_psgallery.ps1 -ApiKey $env:PSGALLERY_API_KEY
```

A CI workflow at `.github/workflows/psgallery.yml` validates the
manifest on every push and publishes to PSGallery on `vX.Y.Z`
tags. The workflow also uploads a `MOTPAdmin.zip` artifact for
manual distribution.


## Real multiOTP integration test

`tests/integration/test_real_multiotp.py` exercises the
`MultiOtpCliProvisioner` against the actual binary on a Windows
runner. Set `MOTP_REAL_TEST=1` and `MOTP_BINARY=…` in your
environment to enable. The test creates a user, assigns a token,
verifies the OTP, then locks and unlocks the user; it cleans up
after itself.

`.github/workflows/integration-windows.yml` runs weekly
(Monday 03:00 UTC) and on `workflow_dispatch`. It downloads
multiOTP from `download.multiotp.net`, installs silently, runs
the test and reports the outcome.


## Docker image

```bash
docker build -f docker/Dockerfile -t motp-admin:dev .
docker run --rm -p 8088:8088 -e ISSUER=Acme motp-admin:dev
```

`docker/docker-compose.yml` provides a ready-to-run stack; mount your
own `policies.json` for the portal to enforce login windows.

The `.github/workflows/docker-image.yml` workflow builds and pushes
multi-arch (amd64 / arm64) images to GHCR on every push to `main`
and on `vX.Y.Z` tags. SemVer tags are produced automatically.


## Metrics and reporting

The portal exposes Prometheus text format at `GET /metrics`
(`Content-Type: text/plain; version=0.0.4`). Counters cover bind
attempts, policy decisions and OTP validation outcomes;
gauges report active users and active binding sessions.

The `report` subcommand reads `audit.jsonl` files and emits a
summary in JSON, CSV or HTML. Point it at an output directory or
a single file:

```bash
PYTHONPATH=tools/admin python3 -m motp_admin report     --audit-dir out --format html --out report.html
```

CSV output flattens sections (`action`, `actor`, `day`, `target`,
`failures`) to play nicely with SIEM tooling.


## Scheduled AD sync

```bash
PYTHONPATH=tools/admin python3 -m motp_admin sync-scheduler     --source ldap --server ldap://corp --base-dn dc=example,dc=com     --filter "(memberOf=cn=vpn-users,ou=groups,dc=example,dc=com)"     --out-csv /var/lib/motp/users.csv --interval 900 --max-runs 0
```

- `--interval` seconds between runs; `--max-runs 0` runs forever
- each run writes `<out-csv>.cache.json` with sha256 + timestamp
- a follow-up `sync-cache --out-csv …` prints the recorded state
- the cache uses sha256 of the produced CSV to skip downstream work
  on unchanged inputs
