Register qonto-assistant as a protected system (finance.qonto.read)
QONTO-WP-0004-T04. Modeled directly on examples/tenant-engine/: one resource type (finance-snapshot), one action (finance.qonto.read), two registered subjects (an agent-harness session identity and the founder's human identity), and a Rego policy gating on resource.system + action + subject.type + tenant match. Tenant capability-role/plan liveness (VEN/CUS) is deliberately NOT encoded here -- that's qonto-assistant's separate tenant-engine live-lookup check, per this package's own scope note. Verified: flex-auth test-policy (6 rego tests + 6 fixtures, all pass), load-registry, and CLI check for both an allow and a deny case. Also verified end-to-end over real HTTP: a live flex-auth serve loaded with this exact registry+policy, hit by qonto-assistant's actual FlexAuthCheckClient (not a mock) -- allow for tenant:friendly:binky, deny (wrong_tenant) for a mismatched tenant. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
0061835d3a
commit
ae295824bd
8 changed files with 510 additions and 0 deletions
65
examples/qonto-assistant/README.md
Normal file
65
examples/qonto-assistant/README.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# qonto-assistant Consumer Integration Fixtures
|
||||
|
||||
`QONTO-WP-0004-T04`. Registers `qonto-assistant` as a flex-auth
|
||||
protected-system consumer, gating its `finance.qonto.read` capability
|
||||
(the live-authorization half of `docs/SecurityPractice.md` — the other half
|
||||
is a `tenant-engine` capability-role/plan liveness check, deliberately kept
|
||||
separate; see `policy_package.md`'s scope note).
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| `protected_system_manifest.yaml` | Resource type (`finance-snapshot`) and action (`finance.qonto.read`) |
|
||||
| `subject_manifest.yaml` | Registered callers: an agent-harness session identity and the founder's human identity, both in `group:qonto-assistant-readers` |
|
||||
| `policy_package.md` | Rego rules + embedded tests gating the read action |
|
||||
| `policy_fixtures.yaml` | Allow/deny request/decision pairs, referenced by `policy_package.md`'s frontmatter |
|
||||
| `registry_snapshot.json` | Merged `systems`/`subjects`/`groups` snapshot assembled from the two manifests above, loadable by `flex-auth serve`/`check`/`load-registry` |
|
||||
| `check_request_allow_read.json`, `check_request_deny_wrong_tenant.json` | Standalone example requests for `flex-auth check` |
|
||||
|
||||
**No `resource_manifest.yaml`** — like tenant-engine's tenants, `qonto-assistant`
|
||||
doesn't have a fixed enumerable resource inventory to register; the single
|
||||
`finance-snapshot` resource type is declared on the protected-system
|
||||
manifest and that is sufficient for this policy's coarse per-actor/per-tenant
|
||||
gate.
|
||||
|
||||
## Verified
|
||||
|
||||
```bash
|
||||
go build -o bin/flex-auth ./cmd/flex-auth
|
||||
|
||||
# Rego rules + embedded tests + fixtures, all pass:
|
||||
bin/flex-auth test-policy -file examples/qonto-assistant/policy_package.md
|
||||
|
||||
# Registry loads cleanly:
|
||||
bin/flex-auth load-registry -file examples/qonto-assistant/registry_snapshot.json
|
||||
|
||||
# Individual requests via the CLI:
|
||||
bin/flex-auth check \
|
||||
-registry examples/qonto-assistant/registry_snapshot.json \
|
||||
-policy examples/qonto-assistant/policy_package.md \
|
||||
-request examples/qonto-assistant/check_request_allow_read.json
|
||||
|
||||
# End-to-end over real HTTP: a live `flex-auth serve` loaded with this exact
|
||||
# registry+policy, hit by qonto-assistant's actual FlexAuthCheckClient (not
|
||||
# a mock) -- see qonto-assistant/tests/test_flex_auth_client.py and
|
||||
# tests/test_live_authorization_gate.py, and the manual curl walkthrough
|
||||
# below:
|
||||
bin/flex-auth serve -addr 127.0.0.1:9099 \
|
||||
-registry examples/qonto-assistant/registry_snapshot.json \
|
||||
-policy examples/qonto-assistant/policy_package.md
|
||||
# from another shell:
|
||||
curl -s -X POST http://127.0.0.1:9099/v1/check -H "Content-Type: application/json" \
|
||||
-d @examples/qonto-assistant/check_request_allow_read.json
|
||||
# -> effect: allow, reason: finance_read_policy_matched
|
||||
curl -s -X POST http://127.0.0.1:9099/v1/check -H "Content-Type: application/json" \
|
||||
-d @examples/qonto-assistant/check_request_deny_wrong_tenant.json
|
||||
# -> effect: deny, reason: wrong_tenant
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- `qonto-assistant/docs/SecurityPractice.md`
|
||||
- `qonto-assistant/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md`
|
||||
- `examples/tenant-engine/` — the pattern this was modeled on
|
||||
- `net-kingdom/docs/adr/ADR-0014-tenant-capability-roles-and-tenant-engine-ownership.md`
|
||||
15
examples/qonto-assistant/check_request_allow_read.json
Normal file
15
examples/qonto-assistant/check_request_allow_read.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "check:qonto-assistant-read-t1",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {
|
||||
"id": "agent-harness-binky",
|
||||
"type": "agent"
|
||||
},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {
|
||||
"id": "finance-snapshot",
|
||||
"type": "finance-snapshot",
|
||||
"system": "qonto-assistant"
|
||||
},
|
||||
"context": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"id": "check:qonto-assistant-read-t6",
|
||||
"tenant": "tenant:friendly:some-other-company",
|
||||
"subject": {
|
||||
"id": "agent-harness-binky",
|
||||
"type": "agent"
|
||||
},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {
|
||||
"id": "finance-snapshot",
|
||||
"type": "finance-snapshot",
|
||||
"system": "qonto-assistant"
|
||||
},
|
||||
"context": {}
|
||||
}
|
||||
74
examples/qonto-assistant/policy_fixtures.yaml
Normal file
74
examples/qonto-assistant/policy_fixtures.yaml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
[
|
||||
{
|
||||
"id": "fixture:qonto-assistant-agent-read-allow",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t1",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "allow", "reason": "finance_read_policy_matched"}
|
||||
},
|
||||
{
|
||||
"id": "fixture:qonto-assistant-human-read-allow",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t2",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "bernd.worsch", "type": "human"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "allow", "reason": "finance_read_policy_matched"}
|
||||
},
|
||||
{
|
||||
"id": "fixture:qonto-assistant-wrong-system-deny",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t3",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "x", "type": "finance-snapshot", "system": "some-other-system"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "deny", "reason": "wrong_system"}
|
||||
},
|
||||
{
|
||||
"id": "fixture:qonto-assistant-unknown-action-deny",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t4",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.transfer",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "deny", "reason": "unknown_action"}
|
||||
},
|
||||
{
|
||||
"id": "fixture:qonto-assistant-wrong-subject-type-deny",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t5",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "unknown-device", "type": "device"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "deny", "reason": "wrong_subject_type"}
|
||||
},
|
||||
{
|
||||
"id": "fixture:qonto-assistant-wrong-tenant-deny",
|
||||
"request": {
|
||||
"id": "check:qonto-assistant-read-t6",
|
||||
"tenant": "tenant:friendly:some-other-company",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"},
|
||||
"context": {}
|
||||
},
|
||||
"expect": {"effect": "deny", "reason": "wrong_tenant"}
|
||||
}
|
||||
]
|
||||
167
examples/qonto-assistant/policy_package.md
Normal file
167
examples/qonto-assistant/policy_package.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
---
|
||||
id: qonto-assistant.finance-read
|
||||
name: qonto-assistant finance.qonto.read authorization
|
||||
namespace: qonto-assistant:finance
|
||||
version: v1
|
||||
status: ready
|
||||
package: flexauth.qonto_assistant.finance_read
|
||||
actions:
|
||||
- finance.qonto.read
|
||||
owner: team:platform-security
|
||||
fixtures:
|
||||
- policy_fixtures.yaml
|
||||
caring:
|
||||
profile: caring-0.4.0-rc2
|
||||
enforce: false
|
||||
canonical_roles:
|
||||
- Operator
|
||||
organization_relations:
|
||||
- ServiceProvider
|
||||
- Customer
|
||||
scopes:
|
||||
- level: Tenant
|
||||
id: tenant:friendly:binky
|
||||
tenant: tenant:friendly:binky
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
capabilities:
|
||||
- View
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Masked
|
||||
conditions:
|
||||
- Logged
|
||||
restrictions:
|
||||
- PrivilegeEscalationBlocked
|
||||
activation:
|
||||
mode: local
|
||||
metadata:
|
||||
source: examples/qonto-assistant/policy_package.md
|
||||
flex_auth_contract: protected-system-v0
|
||||
---
|
||||
|
||||
# qonto-assistant finance.qonto.read authorization
|
||||
|
||||
This package authorizes `qonto-assistant`'s read surface
|
||||
(`QONTO-WP-0004-T04`'s live authorization gate). `qonto-assistant` keeps
|
||||
custody of the bank credential and its own default-deny policy kernel
|
||||
(spend/transfer/card/write tools are hard-denied there and never reach this
|
||||
policy); flex-auth decides whether a specific *actor* may use the
|
||||
`finance.qonto.read` capability at all.
|
||||
|
||||
**Scope note:** this policy governs *who may call `finance.qonto.read`* (an
|
||||
actor/tenant question) — it does not evaluate a *tenant's* capability roles
|
||||
or plan status (`PLTF`/`IAM`/`VEN`/`CUS`, ADR-0014). Those are tenant state
|
||||
`qonto-assistant` checks separately via `tenant-engine`'s live-lookup
|
||||
endpoint (`GET /tenants/{id}/roles/live`); conflating the two here would
|
||||
authorize the wrong thing, exactly as tenant-engine's own
|
||||
`policy_package.md` notes for its analogous case.
|
||||
|
||||
Single-tenant dogfood today (`tenant:friendly:binky` only); generalizing to
|
||||
other tenants is a policy update here, not a `qonto-assistant` code change.
|
||||
|
||||
## Rules
|
||||
|
||||
```rego
|
||||
import future.keywords.contains
|
||||
import future.keywords.if
|
||||
import future.keywords.in
|
||||
|
||||
valid_actions := {"finance.qonto.read"}
|
||||
|
||||
valid_subject_types := {"agent", "human", "service"}
|
||||
|
||||
known_tenant := "tenant:friendly:binky"
|
||||
|
||||
decision := {"effect": "allow", "reason": "finance_read_policy_matched"} if {
|
||||
allowed
|
||||
} else := {"effect": "deny", "reason": first_denial} if {
|
||||
true
|
||||
}
|
||||
|
||||
allowed if {
|
||||
input.resource.system == "qonto-assistant"
|
||||
input.action in valid_actions
|
||||
input.subject.type in valid_subject_types
|
||||
input.tenant == known_tenant
|
||||
}
|
||||
|
||||
default first_denial := "no_matching_rule"
|
||||
|
||||
first_denial := "wrong_system" if {
|
||||
input.resource.system != "qonto-assistant"
|
||||
} else := "unknown_action" if {
|
||||
not input.action in valid_actions
|
||||
} else := "wrong_subject_type" if {
|
||||
not input.subject.type in valid_subject_types
|
||||
} else := "wrong_tenant" if {
|
||||
input.tenant != known_tenant
|
||||
}
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```rego test
|
||||
package flexauth.qonto_assistant.finance_read_test
|
||||
|
||||
import future.keywords.if
|
||||
import data.flexauth.qonto_assistant.finance_read
|
||||
|
||||
base_request := {
|
||||
"id": "check:qonto-assistant-read",
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}
|
||||
}
|
||||
|
||||
test_agent_read_allowed if {
|
||||
finance_read.decision.effect == "allow" with input as base_request
|
||||
}
|
||||
|
||||
test_human_read_allowed if {
|
||||
finance_read.decision.effect == "allow" with input as {
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "bernd.worsch", "type": "human"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}
|
||||
}
|
||||
}
|
||||
|
||||
test_wrong_system_denied if {
|
||||
finance_read.decision.reason == "wrong_system" with input as {
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "x", "type": "finance-snapshot", "system": "some-other-system"}
|
||||
}
|
||||
}
|
||||
|
||||
test_unknown_action_denied if {
|
||||
finance_read.decision.reason == "unknown_action" with input as {
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.transfer",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}
|
||||
}
|
||||
}
|
||||
|
||||
test_wrong_subject_type_denied if {
|
||||
finance_read.decision.reason == "wrong_subject_type" with input as {
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"subject": {"id": "unknown-device", "type": "device"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}
|
||||
}
|
||||
}
|
||||
|
||||
test_wrong_tenant_denied if {
|
||||
finance_read.decision.reason == "wrong_tenant" with input as {
|
||||
"tenant": "tenant:friendly:some-other-company",
|
||||
"subject": {"id": "agent-harness-binky", "type": "agent"},
|
||||
"action": "finance.qonto.read",
|
||||
"resource": {"id": "finance-snapshot", "type": "finance-snapshot", "system": "qonto-assistant"}
|
||||
}
|
||||
}
|
||||
```
|
||||
37
examples/qonto-assistant/protected_system_manifest.yaml
Normal file
37
examples/qonto-assistant/protected_system_manifest.yaml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
id: qonto-assistant
|
||||
name: Qonto Governed Assistant
|
||||
resource_types:
|
||||
- name: finance-snapshot
|
||||
scope_level: Resource
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
metadata:
|
||||
description: >-
|
||||
Read-only Qonto finance capability surface (org summary,
|
||||
transactions, CostRunRate hints). No spend/transfer/card/write
|
||||
capability is ever registered here -- those are hard-denied inside
|
||||
qonto-assistant's own policy kernel and never reach flex-auth.
|
||||
actions:
|
||||
- name: finance.qonto.read
|
||||
capabilities:
|
||||
- View
|
||||
- Audit
|
||||
planes:
|
||||
- Data
|
||||
- Audit
|
||||
exposure_modes:
|
||||
- Masked
|
||||
metadata:
|
||||
required_context: []
|
||||
description: >-
|
||||
Coarse "may this actor use qonto-assistant's read surface at all"
|
||||
gate. Tenant capability-role/plan liveness (VEN/CUS, ADR-0014) is a
|
||||
separate check against tenant-engine's live-lookup endpoint, not
|
||||
encoded in this policy -- conflating the two would authorize the
|
||||
wrong thing (see tenant-engine's own policy_package.md note).
|
||||
caring_profiles:
|
||||
- caring-0.4.0-rc2
|
||||
metadata:
|
||||
flex_auth_contract: protected-system-v0
|
||||
boundary_contract: qonto-assistant/docs/SecurityPractice.md
|
||||
101
examples/qonto-assistant/registry_snapshot.json
Normal file
101
examples/qonto-assistant/registry_snapshot.json
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
"systems": [
|
||||
{
|
||||
"id": "qonto-assistant",
|
||||
"name": "Qonto Governed Assistant",
|
||||
"resource_types": [
|
||||
{
|
||||
"name": "finance-snapshot",
|
||||
"scope_level": "Resource",
|
||||
"planes": [
|
||||
"Data",
|
||||
"Audit"
|
||||
],
|
||||
"metadata": {
|
||||
"description": "Read-only Qonto finance capability surface (org summary, transactions, CostRunRate hints). No spend/transfer/card/write capability is ever registered here -- those are hard-denied inside qonto-assistant's own policy kernel and never reach flex-auth."
|
||||
}
|
||||
}
|
||||
],
|
||||
"actions": [
|
||||
{
|
||||
"name": "finance.qonto.read",
|
||||
"capabilities": [
|
||||
"View",
|
||||
"Audit"
|
||||
],
|
||||
"planes": [
|
||||
"Data",
|
||||
"Audit"
|
||||
],
|
||||
"exposure_modes": [
|
||||
"Masked"
|
||||
],
|
||||
"metadata": {
|
||||
"required_context": [],
|
||||
"description": "Coarse \"may this actor use qonto-assistant's read surface at all\" gate. Tenant capability-role/plan liveness (VEN/CUS, ADR-0014) is a separate check against tenant-engine's live-lookup endpoint, not encoded in this policy."
|
||||
}
|
||||
}
|
||||
],
|
||||
"caring_profiles": [
|
||||
"caring-0.4.0-rc2"
|
||||
],
|
||||
"metadata": {
|
||||
"flex_auth_contract": "protected-system-v0",
|
||||
"boundary_contract": "qonto-assistant/docs/SecurityPractice.md"
|
||||
}
|
||||
}
|
||||
],
|
||||
"resource_manifests": [],
|
||||
"tenants": [
|
||||
{
|
||||
"id": "tenant:friendly:binky",
|
||||
"name": "Binky Hedgehog GmbH"
|
||||
}
|
||||
],
|
||||
"subjects": [
|
||||
{
|
||||
"id": "agent-harness-binky",
|
||||
"type": "Agent",
|
||||
"display_name": "agent-harness session (binky tenant)",
|
||||
"organization_relation": "ServiceProvider",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:qonto-assistant-readers"
|
||||
],
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"metadata": {
|
||||
"description": "Harness-run agent sessions calling qonto-assistant's finance.qonto.read capability over REST or MCP."
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bernd.worsch",
|
||||
"type": "Human",
|
||||
"display_name": "Bernd Worsch (founder)",
|
||||
"organization_relation": "Customer",
|
||||
"roles": [
|
||||
"Operator"
|
||||
],
|
||||
"groups": [
|
||||
"group:qonto-assistant-readers"
|
||||
],
|
||||
"tenant": "tenant:friendly:binky",
|
||||
"metadata": {
|
||||
"description": "Founder operator, human REST/MCP caller."
|
||||
}
|
||||
}
|
||||
],
|
||||
"groups": [
|
||||
{
|
||||
"id": "group:qonto-assistant-readers",
|
||||
"display_name": "qonto-assistant finance.qonto.read callers",
|
||||
"members": [
|
||||
"agent-harness-binky",
|
||||
"bernd.worsch"
|
||||
],
|
||||
"tenant": "tenant:friendly:binky"
|
||||
}
|
||||
],
|
||||
"relationships": []
|
||||
}
|
||||
36
examples/qonto-assistant/subject_manifest.yaml
Normal file
36
examples/qonto-assistant/subject_manifest.yaml
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
id: subjects:qonto-assistant-readers
|
||||
tenants:
|
||||
- id: tenant:friendly:binky
|
||||
name: Binky Hedgehog GmbH
|
||||
subjects:
|
||||
- id: agent-harness-binky
|
||||
type: Agent
|
||||
display_name: agent-harness session (binky tenant)
|
||||
organization_relation: ServiceProvider
|
||||
roles:
|
||||
- Operator
|
||||
groups:
|
||||
- group:qonto-assistant-readers
|
||||
tenant: tenant:friendly:binky
|
||||
metadata:
|
||||
description: >-
|
||||
Harness-run agent sessions calling qonto-assistant's
|
||||
finance.qonto.read capability over REST or MCP.
|
||||
- id: bernd.worsch
|
||||
type: Human
|
||||
display_name: Bernd Worsch (founder)
|
||||
organization_relation: Customer
|
||||
roles:
|
||||
- Operator
|
||||
groups:
|
||||
- group:qonto-assistant-readers
|
||||
tenant: tenant:friendly:binky
|
||||
metadata:
|
||||
description: Founder operator, human REST/MCP caller.
|
||||
groups:
|
||||
- id: group:qonto-assistant-readers
|
||||
display_name: qonto-assistant finance.qonto.read callers
|
||||
members:
|
||||
- agent-harness-binky
|
||||
- bernd.worsch
|
||||
tenant: tenant:friendly:binky
|
||||
Loading…
Add table
Add a link
Reference in a new issue