Initialize identity module
This commit is contained in:
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
35
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
name: "Bug"
|
||||
about: "Report a reproducible defect, regression, or incorrect behavior"
|
||||
title: "[Bug] "
|
||||
labels:
|
||||
- type/bug
|
||||
- status/triage
|
||||
- module/identity
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- Repository:
|
||||
- Area/module:
|
||||
- Affected version or commit:
|
||||
|
||||
## Behavior
|
||||
|
||||
Expected:
|
||||
|
||||
Actual:
|
||||
|
||||
## Reproduction
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Evidence
|
||||
|
||||
Logs, screenshots, traces, or failing test output:
|
||||
|
||||
## Verification Target
|
||||
|
||||
Command or workflow that should pass when fixed:
|
||||
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
1
.gitea/ISSUE_TEMPLATE/config.yaml
Normal file
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
27
.gitea/ISSUE_TEMPLATE/docs_workflow.md
Normal file
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: "Docs / workflow"
|
||||
about: "Request documentation, process, or developer workflow changes"
|
||||
title: "[Docs] "
|
||||
labels:
|
||||
- type/docs
|
||||
- status/triage
|
||||
- module/identity
|
||||
- area/docs
|
||||
---
|
||||
|
||||
## Scope
|
||||
|
||||
- Repository:
|
||||
- Document or workflow:
|
||||
|
||||
## Current State
|
||||
|
||||
What is missing, unclear, duplicated, or stale?
|
||||
|
||||
## Desired State
|
||||
|
||||
What should the docs or workflow make clear?
|
||||
|
||||
## Verification Target
|
||||
|
||||
How should this be checked?
|
||||
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
32
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
name: "Feature"
|
||||
about: "Propose new user-visible behavior or platform capability"
|
||||
title: "[Feature] "
|
||||
labels:
|
||||
- type/feature
|
||||
- status/triage
|
||||
- module/identity
|
||||
---
|
||||
|
||||
## Problem
|
||||
|
||||
What user, operator, or developer problem should this solve?
|
||||
|
||||
## Proposed Capability
|
||||
|
||||
What should exist when this is done?
|
||||
|
||||
## Ownership
|
||||
|
||||
- Owning repository:
|
||||
- Related module repositories:
|
||||
- Extension point or integration boundary:
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## Verification Target
|
||||
|
||||
Command, scenario, or UI flow that should prove completion:
|
||||
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
28
.gitea/ISSUE_TEMPLATE/task.md
Normal file
@@ -0,0 +1,28 @@
|
||||
---
|
||||
name: "Task"
|
||||
about: "Track implementation, maintenance, or migration work"
|
||||
title: "[Task] "
|
||||
labels:
|
||||
- type/task
|
||||
- status/triage
|
||||
- module/identity
|
||||
---
|
||||
|
||||
## Objective
|
||||
|
||||
What needs to be completed?
|
||||
|
||||
## Scope
|
||||
|
||||
- Owning repository:
|
||||
- In-scope:
|
||||
- Out-of-scope:
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ]
|
||||
- [ ]
|
||||
|
||||
## Verification Target
|
||||
|
||||
Command or manual check:
|
||||
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
25
.gitea/ISSUE_TEMPLATE/tech_debt.md
Normal file
@@ -0,0 +1,25 @@
|
||||
---
|
||||
name: "Tech debt"
|
||||
about: "Track cleanup, refactoring, risk reduction, or deferred engineering work"
|
||||
title: "[Debt] "
|
||||
labels:
|
||||
- type/debt
|
||||
- status/triage
|
||||
- module/identity
|
||||
---
|
||||
|
||||
## Current Cost
|
||||
|
||||
What does this make harder, riskier, slower, or more fragile?
|
||||
|
||||
## Desired Shape
|
||||
|
||||
What should the code, tests, or architecture look like afterwards?
|
||||
|
||||
## Constraints
|
||||
|
||||
What behavior, compatibility, or module boundary must be preserved?
|
||||
|
||||
## Verification Target
|
||||
|
||||
Focused checks that should pass:
|
||||
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
15
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
@@ -0,0 +1,15 @@
|
||||
## Issue
|
||||
|
||||
Closes #
|
||||
|
||||
## Summary
|
||||
|
||||
-
|
||||
|
||||
## Verification
|
||||
|
||||
-
|
||||
|
||||
## Notes
|
||||
|
||||
Follow-up issues:
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
build/
|
||||
dist/
|
||||
27
AGENTS.md
Normal file
27
AGENTS.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# GovOPlaN Identity Codex Guide
|
||||
|
||||
## Scope
|
||||
|
||||
This repository owns the canonical GovOPlaN identity directory: internal
|
||||
identity records and links between identities and platform accounts.
|
||||
|
||||
The module does not own authentication sessions, passwords, API keys, roles,
|
||||
permissions, or external IDM connector behavior.
|
||||
|
||||
## Boundaries
|
||||
|
||||
- Depend on kernel contracts from `govoplan-core`.
|
||||
- Do not import access, IDM, organizations, postbox, workflow, or portal
|
||||
internals.
|
||||
- Expose integration through manifests, capabilities, API routes, events, and
|
||||
typed DTOs.
|
||||
- Keep external-provider synchronization in `govoplan-idm`; this module stores
|
||||
accepted normalized identity facts.
|
||||
|
||||
## Local Workflow
|
||||
|
||||
Use Gitea issues as the canonical backlog and state log. The shared workflow is
|
||||
installed under `.gitea/`, with labels in `docs/gitea-labels.json`.
|
||||
|
||||
Focused verification is still driven from `/mnt/DATA/git/govoplan-core` for
|
||||
cross-repository behavior.
|
||||
39
README.md
Normal file
39
README.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# GovOPlaN Identity
|
||||
|
||||
`govoplan-identity` is the canonical identity directory module for GovOPlaN.
|
||||
|
||||
It owns identities and links between identities and platform accounts. This is
|
||||
separate from IDM connectors and from access control.
|
||||
|
||||
## Boundary
|
||||
|
||||
Identity owns:
|
||||
|
||||
- identity records for people, services, and other internal subjects
|
||||
- account links for identities that can use multiple accounts
|
||||
- primary account selection for display and explainability
|
||||
- accepted normalized identity facts after import or manual administration
|
||||
|
||||
Identity does not own:
|
||||
|
||||
- password authentication, sessions, API keys, or CSRF
|
||||
- roles, rights, permission evaluation, or access decisions
|
||||
- organization units, functions, or function assignments
|
||||
- external directory connector internals
|
||||
|
||||
## Module Contract
|
||||
|
||||
The module registers the `identity.directory` capability from
|
||||
`govoplan_core.core.identity`.
|
||||
|
||||
Feature modules should consume the capability instead of importing identity ORM
|
||||
models.
|
||||
|
||||
## Development Install
|
||||
|
||||
From the core checkout:
|
||||
|
||||
```bash
|
||||
cd /mnt/DATA/git/govoplan-core
|
||||
./.venv/bin/python -m pip install -e ../govoplan-identity
|
||||
```
|
||||
27
docs/IDENTITY_MODEL.md
Normal file
27
docs/IDENTITY_MODEL.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Identity Model
|
||||
|
||||
The identity model answers who the platform subject is, independent of how they
|
||||
authenticate and independent of what they may do.
|
||||
|
||||
## Layers
|
||||
|
||||
- Identity: a person, service, or other subject known to GovOPlaN.
|
||||
- Account link: a reference from that identity to one platform account.
|
||||
- Primary account: the account used as the default display/explainability
|
||||
anchor when multiple accounts exist.
|
||||
|
||||
## Boundary With Access
|
||||
|
||||
Access owns authorization. Identity only tells access which identity is behind
|
||||
an account. Access can then evaluate roles, rights, delegation, and policy.
|
||||
|
||||
## Boundary With Organizations
|
||||
|
||||
Organization functions and function assignments live in
|
||||
`govoplan-organizations`. Those assignments may reference identity and account
|
||||
IDs, but identity does not own organizational structure.
|
||||
|
||||
## Boundary With IDM
|
||||
|
||||
`govoplan-idm` imports, previews, reconciles, and applies external identity
|
||||
facts. Once accepted, normalized identity records belong here.
|
||||
416
docs/gitea-labels.json
Normal file
416
docs/gitea-labels.json
Normal file
@@ -0,0 +1,416 @@
|
||||
[
|
||||
{
|
||||
"name": "type/bug",
|
||||
"color": "d73a4a",
|
||||
"description": "A reproducible defect, regression, or incorrect behavior.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "type/feature",
|
||||
"color": "0e8a16",
|
||||
"description": "New user-visible behavior or platform capability.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "type/task",
|
||||
"color": "1d76db",
|
||||
"description": "Implementation, maintenance, migration, or operational work.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "type/debt",
|
||||
"color": "fbca04",
|
||||
"description": "Cleanup, refactoring, risk reduction, or deferred engineering work.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "type/docs",
|
||||
"color": "5319e7",
|
||||
"description": "Documentation, process, or developer workflow work.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "priority/p0",
|
||||
"color": "b60205",
|
||||
"description": "Immediate stop-the-line priority.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "priority/p1",
|
||||
"color": "d93f0b",
|
||||
"description": "High priority for the next focused work window.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "priority/p2",
|
||||
"color": "fbca04",
|
||||
"description": "Normal planned priority.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "priority/p3",
|
||||
"color": "c2e0c6",
|
||||
"description": "Low priority or opportunistic cleanup.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "status/triage",
|
||||
"color": "d4c5f9",
|
||||
"description": "Needs review, ownership, priority, or acceptance criteria.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "status/ready",
|
||||
"color": "0e8a16",
|
||||
"description": "Ready for implementation.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "status/in-progress",
|
||||
"color": "0052cc",
|
||||
"description": "Currently being worked.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "status/blocked",
|
||||
"color": "b60205",
|
||||
"description": "Cannot progress without a decision, dependency, credential, or external change.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "status/needs-info",
|
||||
"color": "f9d0c4",
|
||||
"description": "Needs clarifying input before implementation can proceed safely.",
|
||||
"exclusive": true
|
||||
},
|
||||
{
|
||||
"name": "module/access",
|
||||
"color": "0e8a16",
|
||||
"description": "GovOPlaN access, identity, authentication, RBAC, and administration behavior.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/addresses",
|
||||
"color": "5319e7",
|
||||
"description": "GovOPlaN Addresses module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/admin",
|
||||
"color": "006b75",
|
||||
"description": "GovOPlaN Admin module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/appointments",
|
||||
"color": "d876e3",
|
||||
"description": "GovOPlaN Appointments module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/audit",
|
||||
"color": "0e8a16",
|
||||
"description": "GovOPlaN Audit module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/calendar",
|
||||
"color": "bfd4f2",
|
||||
"description": "GovOPlaN Calendar module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/campaign",
|
||||
"color": "d876e3",
|
||||
"description": "GovOPlaN campaign module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/cases",
|
||||
"color": "1d76db",
|
||||
"description": "GovOPlaN Cases module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/connectors",
|
||||
"color": "c2e0c6",
|
||||
"description": "GovOPlaN Connectors module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/core",
|
||||
"color": "0052cc",
|
||||
"description": "GovOPlaN core runner, shared primitives, shell, or extension points.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/dms",
|
||||
"color": "c5def5",
|
||||
"description": "GovOPlaN Dms module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/erp",
|
||||
"color": "fef2c0",
|
||||
"description": "GovOPlaN Erp module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/files",
|
||||
"color": "006b75",
|
||||
"description": "GovOPlaN files module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/fit-connect",
|
||||
"color": "fbca04",
|
||||
"description": "GovOPlaN Fit Connect module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/forms",
|
||||
"color": "f9d0c4",
|
||||
"description": "GovOPlaN Forms module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/identity-trust",
|
||||
"color": "d4c5f9",
|
||||
"description": "GovOPlaN Identity Trust module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/identity",
|
||||
"color": "bfd4f2",
|
||||
"description": "GovOPlaN Identity module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/idm",
|
||||
"color": "0052cc",
|
||||
"description": "GovOPlaN Idm module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/ledger",
|
||||
"color": "5319e7",
|
||||
"description": "GovOPlaN Ledger module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/mail",
|
||||
"color": "5319e7",
|
||||
"description": "GovOPlaN mail module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/notifications",
|
||||
"color": "d876e3",
|
||||
"description": "GovOPlaN Notifications module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/ops",
|
||||
"color": "0e8a16",
|
||||
"description": "GovOPlaN Ops module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/organizations",
|
||||
"color": "bfdadc",
|
||||
"description": "GovOPlaN Organizations module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/payments",
|
||||
"color": "bfd4f2",
|
||||
"description": "GovOPlaN Payments module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/policy",
|
||||
"color": "d93f0b",
|
||||
"description": "GovOPlaN Policy module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/portal",
|
||||
"color": "1d76db",
|
||||
"description": "GovOPlaN Portal module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/postbox",
|
||||
"color": "d93f0b",
|
||||
"description": "GovOPlaN Postbox module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/reporting",
|
||||
"color": "c2e0c6",
|
||||
"description": "GovOPlaN Reporting module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/search",
|
||||
"color": "bfdadc",
|
||||
"description": "GovOPlaN Search module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/scheduling",
|
||||
"color": "d93f0b",
|
||||
"description": "GovOPlaN Scheduling module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/tasks",
|
||||
"color": "c5def5",
|
||||
"description": "GovOPlaN Tasks module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/templates",
|
||||
"color": "fef2c0",
|
||||
"description": "GovOPlaN Templates module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/tenancy",
|
||||
"color": "e4e669",
|
||||
"description": "GovOPlaN Tenancy module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/web",
|
||||
"color": "bfd4f2",
|
||||
"description": "GovOPlaN public website, product page, or publication content.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/workflow",
|
||||
"color": "f9d0c4",
|
||||
"description": "GovOPlaN Workflow module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/xoev",
|
||||
"color": "d4c5f9",
|
||||
"description": "GovOPlaN Xoev module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/xrechnung",
|
||||
"color": "0052cc",
|
||||
"description": "GovOPlaN Xrechnung module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "module/xta-osci",
|
||||
"color": "5319e7",
|
||||
"description": "GovOPlaN Xta Osci module behavior or integration.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/auth",
|
||||
"color": "bfd4f2",
|
||||
"description": "Authentication, sessions, access bootstrap, or login behavior.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/tenancy",
|
||||
"color": "bfdadc",
|
||||
"description": "Tenant boundaries, provisioning, or tenant-scoped data behavior.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/rbac",
|
||||
"color": "c5def5",
|
||||
"description": "Permissions, roles, delegation, or authorization policy.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/governance",
|
||||
"color": "fef2c0",
|
||||
"description": "Governance policy, audit, privacy, retention, or compliance behavior.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/module-system",
|
||||
"color": "bfe5bf",
|
||||
"description": "Module discovery, manifests, capabilities, routing, or optional integrations.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/migrations",
|
||||
"color": "e4e669",
|
||||
"description": "Alembic migrations, schema bootstrap, or persistence evolution.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/webui",
|
||||
"color": "1d76db",
|
||||
"description": "Shared WebUI shell, frontend components, routing, or frontend tests.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/api",
|
||||
"color": "5319e7",
|
||||
"description": "HTTP API contracts, routers, schemas, or API smoke behavior.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/db",
|
||||
"color": "0e8a16",
|
||||
"description": "Database sessions, models, transactions, or persistence primitives.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/devex",
|
||||
"color": "bfd4f2",
|
||||
"description": "Local developer workflow, scripts, tests, tooling, or release helpers.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/release",
|
||||
"color": "c2e0c6",
|
||||
"description": "Versioning, release locks, tags, packaging, or dependency pins.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/docs",
|
||||
"color": "5319e7",
|
||||
"description": "Durable documentation and project guidance.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "area/marketing",
|
||||
"color": "f9d0c4",
|
||||
"description": "Public website, product messaging, publication copy, or legal page content.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "source/todo-scan",
|
||||
"color": "cccccc",
|
||||
"description": "Imported from inline TODO/FIXME/HACK markers by the Gitea TODO importer.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "source/backlog-import",
|
||||
"color": "bfdadc",
|
||||
"description": "Imported from markdown backlog, roadmap, plan, or TODO files.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "codex/ready",
|
||||
"color": "0e8a16",
|
||||
"description": "Suitable for Codex to pick up with the existing issue context.",
|
||||
"exclusive": false
|
||||
},
|
||||
{
|
||||
"name": "codex/needs-human",
|
||||
"color": "f9d0c4",
|
||||
"description": "Needs an explicit human decision before Codex should implement.",
|
||||
"exclusive": false
|
||||
}
|
||||
]
|
||||
23
pyproject.toml
Normal file
23
pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=69", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "govoplan-identity"
|
||||
version = "0.1.6"
|
||||
description = "GovOPlaN identity directory module."
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
authors = [{ name = "GovOPlaN" }]
|
||||
dependencies = [
|
||||
"govoplan-core>=0.1.6",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
govoplan_identity = ["py.typed"]
|
||||
|
||||
[project.entry-points."govoplan.modules"]
|
||||
identity = "govoplan_identity.backend.manifest:get_manifest"
|
||||
3
src/govoplan_identity/__init__.py
Normal file
3
src/govoplan_identity/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""GovOPlaN identity module."""
|
||||
|
||||
__version__ = "0.1.6"
|
||||
1
src/govoplan_identity/backend/__init__.py
Normal file
1
src/govoplan_identity/backend/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Backend integration for the GovOPlaN identity module."""
|
||||
1
src/govoplan_identity/backend/db/__init__.py
Normal file
1
src/govoplan_identity/backend/db/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Identity database models."""
|
||||
58
src/govoplan_identity/backend/db/models.py
Normal file
58
src/govoplan_identity/backend/db/models.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy import Boolean, ForeignKey, Index, JSON, String, UniqueConstraint, text
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
|
||||
from govoplan_core.db.base import Base, TimestampMixin
|
||||
|
||||
|
||||
def new_uuid() -> str:
|
||||
return str(uuid.uuid4())
|
||||
|
||||
|
||||
class CanonicalIdentity(Base, TimestampMixin):
|
||||
__tablename__ = "identity_identities"
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
display_name: Mapped[str | None] = mapped_column(String(255))
|
||||
external_subject: Mapped[str | None] = mapped_column(String(255), index=True)
|
||||
source: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
|
||||
settings: Mapped[dict[str, Any]] = mapped_column(JSON, default=dict, nullable=False)
|
||||
|
||||
|
||||
class CanonicalIdentityAccountLink(Base, TimestampMixin):
|
||||
__tablename__ = "identity_account_links"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("identity_id", "account_id", name="uq_identity_module_account_links_identity_account"),
|
||||
Index(
|
||||
"uq_identity_module_account_links_primary_account",
|
||||
"account_id",
|
||||
unique=True,
|
||||
sqlite_where=text("is_primary = 1"),
|
||||
postgresql_where=text("is_primary IS TRUE"),
|
||||
),
|
||||
Index(
|
||||
"uq_identity_module_account_links_primary_identity",
|
||||
"identity_id",
|
||||
unique=True,
|
||||
sqlite_where=text("is_primary = 1"),
|
||||
postgresql_where=text("is_primary IS TRUE"),
|
||||
),
|
||||
)
|
||||
|
||||
id: Mapped[str] = mapped_column(String(36), primary_key=True, default=new_uuid)
|
||||
identity_id: Mapped[str] = mapped_column(ForeignKey("identity_identities.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
account_id: Mapped[str] = mapped_column(String(36), nullable=False, index=True)
|
||||
is_primary: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
|
||||
source: Mapped[str] = mapped_column(String(50), default="local", nullable=False)
|
||||
|
||||
|
||||
Identity = CanonicalIdentity
|
||||
IdentityAccountLink = CanonicalIdentityAccountLink
|
||||
|
||||
|
||||
__all__ = ["CanonicalIdentity", "CanonicalIdentityAccountLink", "Identity", "IdentityAccountLink", "new_uuid"]
|
||||
105
src/govoplan_identity/backend/directory.py
Normal file
105
src/govoplan_identity/backend/directory.py
Normal file
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
from govoplan_core.core.identity import IdentityAccountLinkRef, IdentityDirectory, IdentityRef
|
||||
from govoplan_core.db.session import get_database
|
||||
from govoplan_identity.backend.db.models import Identity, IdentityAccountLink
|
||||
|
||||
|
||||
def _status(active: bool) -> str:
|
||||
return "active" if active else "inactive"
|
||||
|
||||
|
||||
def _identity_ref(identity: Identity, links: Sequence[IdentityAccountLink]) -> IdentityRef:
|
||||
primary_account_id = next((link.account_id for link in links if link.is_primary), None)
|
||||
return IdentityRef(
|
||||
id=identity.id,
|
||||
display_name=identity.display_name,
|
||||
external_subject=identity.external_subject,
|
||||
source=identity.source,
|
||||
primary_account_id=primary_account_id,
|
||||
account_ids=tuple(link.account_id for link in links),
|
||||
status=_status(identity.is_active), # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
|
||||
def _link_ref(link: IdentityAccountLink) -> IdentityAccountLinkRef:
|
||||
return IdentityAccountLinkRef(
|
||||
id=link.id,
|
||||
identity_id=link.identity_id,
|
||||
account_id=link.account_id,
|
||||
is_primary=link.is_primary,
|
||||
source=link.source,
|
||||
)
|
||||
|
||||
|
||||
class SqlIdentityDirectory(IdentityDirectory):
|
||||
def get_identity(self, identity_id: str) -> IdentityRef | None:
|
||||
with get_database().session() as session:
|
||||
identity = session.get(Identity, identity_id)
|
||||
if identity is None:
|
||||
return None
|
||||
links = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id == identity.id)
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return _identity_ref(identity, links)
|
||||
|
||||
def identity_for_account(self, account_id: str) -> IdentityRef | None:
|
||||
with get_database().session() as session:
|
||||
link = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.account_id == account_id)
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
|
||||
.first()
|
||||
)
|
||||
if link is None:
|
||||
return None
|
||||
identity = session.get(Identity, link.identity_id)
|
||||
if identity is None:
|
||||
return None
|
||||
links = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id == identity.id)
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return _identity_ref(identity, links)
|
||||
|
||||
def identities_for_accounts(self, account_ids: Sequence[str]) -> tuple[IdentityRef, ...]:
|
||||
ids = sorted({str(account_id) for account_id in account_ids if account_id})
|
||||
if not ids:
|
||||
return ()
|
||||
with get_database().session() as session:
|
||||
identity_ids = [
|
||||
row[0]
|
||||
for row in session.query(IdentityAccountLink.identity_id)
|
||||
.filter(IdentityAccountLink.account_id.in_(ids))
|
||||
.distinct()
|
||||
.all()
|
||||
]
|
||||
identities = session.query(Identity).filter(Identity.id.in_(identity_ids)).all() if identity_ids else []
|
||||
links = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id.in_(identity_ids))
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
|
||||
.all()
|
||||
if identity_ids else []
|
||||
)
|
||||
links_by_identity: dict[str, list[IdentityAccountLink]] = {}
|
||||
for link in links:
|
||||
links_by_identity.setdefault(link.identity_id, []).append(link)
|
||||
return tuple(_identity_ref(identity, links_by_identity.get(identity.id, [])) for identity in identities)
|
||||
|
||||
def accounts_for_identity(self, identity_id: str) -> tuple[IdentityAccountLinkRef, ...]:
|
||||
with get_database().session() as session:
|
||||
links = (
|
||||
session.query(IdentityAccountLink)
|
||||
.filter(IdentityAccountLink.identity_id == identity_id)
|
||||
.order_by(IdentityAccountLink.is_primary.desc(), IdentityAccountLink.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
return tuple(_link_ref(link) for link in links)
|
||||
51
src/govoplan_identity/backend/manifest.py
Normal file
51
src/govoplan_identity/backend/manifest.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from govoplan_core.core.identity import CAPABILITY_IDENTITY_DIRECTORY
|
||||
from govoplan_core.core.module_guards import persistent_table_uninstall_guard
|
||||
from govoplan_core.core.modules import DocumentationTopic, MigrationSpec, ModuleContext, ModuleManifest
|
||||
from govoplan_core.db.base import Base
|
||||
from govoplan_identity.backend.db import models as identity_models # noqa: F401 - populate metadata
|
||||
|
||||
|
||||
def _identity_directory(context: ModuleContext) -> object:
|
||||
del context
|
||||
from govoplan_identity.backend.directory import SqlIdentityDirectory
|
||||
|
||||
return SqlIdentityDirectory()
|
||||
|
||||
|
||||
manifest = ModuleManifest(
|
||||
id="identity",
|
||||
name="Identity",
|
||||
version="0.1.6",
|
||||
migration_spec=MigrationSpec(module_id="identity", metadata=Base.metadata),
|
||||
uninstall_guard_providers=(
|
||||
persistent_table_uninstall_guard(
|
||||
identity_models.Identity,
|
||||
identity_models.IdentityAccountLink,
|
||||
label="Identity",
|
||||
),
|
||||
),
|
||||
capability_factories={
|
||||
CAPABILITY_IDENTITY_DIRECTORY: _identity_directory,
|
||||
},
|
||||
documentation=(
|
||||
DocumentationTopic(
|
||||
id="identity.model",
|
||||
title="Identity directory",
|
||||
summary="Identity owns normalized subjects and links them to platform accounts. Access owns authorization.",
|
||||
body=(
|
||||
"An identity can have multiple accounts. Identity facts remain separate from authentication sessions, "
|
||||
"organization functions, and permission decisions."
|
||||
),
|
||||
layer="configured",
|
||||
documentation_types=("admin", "user"),
|
||||
audience=("tenant_admin", "access_admin", "operator"),
|
||||
order=24,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def get_manifest() -> ModuleManifest:
|
||||
return manifest
|
||||
1
src/govoplan_identity/py.typed
Normal file
1
src/govoplan_identity/py.typed
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
Reference in New Issue
Block a user