movidas openspec a /docs
This commit is contained in:
parent
0ef0c9951c
commit
aae6b6096e
9 changed files with 486 additions and 0 deletions
|
|
@ -0,0 +1,87 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Least-privilege ACLs for group order models
|
||||
|
||||
`group.order` and `group.order.slot` SHALL NOT grant write, create or unlink to portal, public
|
||||
or unassigned internal users. Write/create/unlink SHALL be limited to the group-order manager
|
||||
group (`website_sale_aplicoop.group_group_order_manager`) and ERP admins. Read MAY be granted to
|
||||
internal users and portal users through explicit, group-scoped ACLs (no empty `group_id` rows).
|
||||
Controller mutations continue to run via `sudo()` and MUST NOT depend on broad ACLs.
|
||||
|
||||
#### Scenario: Portal user cannot mutate group orders
|
||||
- **WHEN** a portal user calls the external API to `create`, `write` or `unlink` on `group.order`
|
||||
or `group.order.slot`
|
||||
- **THEN** the operation is denied with an `AccessError`
|
||||
|
||||
#### Scenario: Manager retains full access
|
||||
- **WHEN** a user in `group_group_order_manager` creates or edits a `group.order` of their company
|
||||
- **THEN** the operation succeeds
|
||||
|
||||
#### Scenario: Controllers still operate
|
||||
- **WHEN** an authenticated portal member confirms a cart through the eskaera controller (which
|
||||
uses `sudo()`)
|
||||
- **THEN** the underlying `sale.order`/`group.order` writes succeed despite the tighter ACLs
|
||||
|
||||
### Requirement: Supplier cost not broadly exposed to portal
|
||||
|
||||
Portal users SHALL NOT read `product.supplierinfo` cost/price rows for products or vendors
|
||||
unrelated to them. The unrestricted record rule (`domain=[(1,'=',1)]`) granting portal read on
|
||||
`product.supplierinfo` SHALL NOT exist. The product **origin** and **main seller** shown in the
|
||||
eskaera shop — currently painted from `supplierinfo` — MUST keep working, either by preparing those
|
||||
values server-side (via `sudo()` in the controller, per the repo's "no logic in QWeb" rule) or via
|
||||
a record rule scoped to the products the user can access, exposing no supplier cost/price fields.
|
||||
|
||||
#### Scenario: Portal user cannot list unrelated supplier cost
|
||||
- **WHEN** a portal user calls `search_read` on `product.supplierinfo` via the external API
|
||||
- **THEN** the result does not include supplier cost/price rows of products/vendors unrelated to
|
||||
the user
|
||||
|
||||
#### Scenario: Origin and main seller still render
|
||||
- **WHEN** a portal member opens the eskaera shop
|
||||
- **THEN** each product's origin and main seller display correctly, without exposing supplier
|
||||
cost/price across all vendors
|
||||
|
||||
### Requirement: CSRF protection on state-changing eskaera endpoints
|
||||
|
||||
State-changing eskaera endpoints SHALL be protected against cross-site request forgery. The
|
||||
endpoints `/eskaera/save-order`, `/eskaera/confirm`, `/eskaera/clear-cart` and `/eskaera/save-cart`
|
||||
(which create, confirm, merge or cancel `sale.order` records) SHALL NOT be `type="http"` with
|
||||
`csrf=False`; they SHALL be `type="json"` (or otherwise validate a CSRF token) so that a cross-site
|
||||
HTML form POST cannot trigger them.
|
||||
|
||||
#### Scenario: Cross-site form POST is rejected
|
||||
- **WHEN** a logged-in user's browser is induced to submit a cross-site form POST to
|
||||
`/eskaera/clear-cart` or `/eskaera/confirm`
|
||||
- **THEN** the request is rejected and no order is cancelled/confirmed
|
||||
|
||||
#### Scenario: Legitimate same-origin call works
|
||||
- **WHEN** the eskaera frontend submits the JSON-RPC request from the same origin
|
||||
- **THEN** the order is created/confirmed as before
|
||||
|
||||
### Requirement: Consumer-group membership enforced on group-order endpoints
|
||||
|
||||
Every eskaera endpoint that resolves a `group.order` from a request id SHALL verify that the
|
||||
requesting user belongs to a consumer group of that order before returning data. This applies to
|
||||
`load_eskaera_page`, `load_products_ajax`, `add_to_eskaera_cart`, `eskaera_checkout` and
|
||||
`check_group_order_status`, reusing `_validate_user_group_access` / `_get_consumer_group_for_user`.
|
||||
Internal users MAY be exempt (matching `eskaera_shop`).
|
||||
|
||||
#### Scenario: Non-member gets no cross-group data
|
||||
- **WHEN** a portal user who is not a member of a group order requests
|
||||
`/eskaera/<order_id>/load-products-ajax` for that order
|
||||
- **THEN** the endpoint returns empty/redirect and discloses no catalog or pricing of that order
|
||||
|
||||
#### Scenario: Member accesses own group order
|
||||
- **WHEN** a portal member of the order's consumer group requests the same endpoint
|
||||
- **THEN** the products and pricing are returned normally
|
||||
|
||||
### Requirement: Safe template output in inline scripts
|
||||
|
||||
QWeb templates SHALL NOT inject data into inline `<script>` blocks via `t-raw`. JSON payloads for
|
||||
the client SHALL be emitted through a safe pattern (e.g. `<script type="application/json">` read by
|
||||
the JS layer).
|
||||
|
||||
#### Scenario: Rendered history payload is not raw-injected
|
||||
- **WHEN** the load-from-history template renders its JSON payload
|
||||
- **THEN** the payload is emitted through a safe (escaped/typed) mechanism, not `t-raw` inside a
|
||||
`<script>` tag
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Hardened Odoo configuration
|
||||
|
||||
Production `odoo.conf` SHALL set a strong `admin_passwd` (not the default `admin`), `list_db =
|
||||
False`, a `dbfilter` scoping databases per host, `proxy_mode = True`, `workers > 0`, request/time
|
||||
limits, `without_demo = True`, and a strong database password (not the literal `odoo`). The
|
||||
database manager SHALL NOT be reachable publicly.
|
||||
|
||||
#### Scenario: Database manager not exposed
|
||||
- **WHEN** an anonymous client requests `/web/database/manager` on production
|
||||
- **THEN** it is not served (blocked at the proxy and/or `list_db=False`)
|
||||
|
||||
#### Scenario: Real client IP reaches Odoo logs
|
||||
- **WHEN** a login fails behind the reverse proxy
|
||||
- **THEN** with `proxy_mode=True` the Odoo log records the real client IP (from `X-Forwarded-For`),
|
||||
not the proxy IP
|
||||
|
||||
### Requirement: Reverse proxy enforces TLS, rate limiting and security headers
|
||||
|
||||
The nginx/traefik layer SHALL terminate TLS with strong protocols/ciphers, apply `limit_req` to
|
||||
`/web/login`, `/web/session/authenticate`, `/jsonrpc` and `/xmlrpc`, and set security headers
|
||||
(HSTS, X-Frame-Options/CSP, X-Content-Type-Options, Referrer-Policy). The `session_id` cookie
|
||||
SHALL be `Secure`, `HttpOnly` and `SameSite`.
|
||||
|
||||
#### Scenario: Login rate limited
|
||||
- **WHEN** a client exceeds the configured request rate on `/web/login`
|
||||
- **THEN** the proxy returns 429 and further attempts are throttled
|
||||
|
||||
#### Scenario: Security headers present
|
||||
- **WHEN** a response is returned from production
|
||||
- **THEN** it includes HSTS, anti-clickjacking and content-type-options headers, and the session
|
||||
cookie carries Secure/HttpOnly/SameSite
|
||||
|
||||
### Requirement: Brute-force sources banned on web and API
|
||||
|
||||
fail2ban SHALL ban source IPs that exceed a failed-authentication threshold, covering both the web
|
||||
login route and the API authentication routes (`/web/session/authenticate`, `/xmlrpc/2/common`),
|
||||
using the real client IP.
|
||||
|
||||
#### Scenario: Repeated web login failures banned
|
||||
- **WHEN** a source exceeds the failed-login threshold on `/web/login`
|
||||
- **THEN** fail2ban bans that IP for the configured duration
|
||||
|
||||
#### Scenario: API auth brute force banned
|
||||
- **WHEN** a source brute-forces `/web/session/authenticate` or `/xmlrpc/2/common`
|
||||
- **THEN** the same fail2ban jail bans it (attacks via API do not bypass protection)
|
||||
|
||||
### Requirement: Secrets and database access hardened
|
||||
|
||||
Database and master secrets SHALL be strong and SHALL NOT be committed in plaintext in versioned
|
||||
files. The PostgreSQL port SHALL NOT be reachable from outside the host/network (firewalled), and
|
||||
the database user SHALL have least privilege.
|
||||
|
||||
#### Scenario: DB port not externally reachable
|
||||
- **WHEN** an external client attempts to connect to PostgreSQL (5432) on production
|
||||
- **THEN** the connection is refused/filtered by the firewall
|
||||
|
||||
#### Scenario: No plaintext secrets in repo
|
||||
- **WHEN** the repository is inspected
|
||||
- **THEN** production database/master passwords are not present in plaintext in versioned files
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Authorization precedes active testing
|
||||
|
||||
Active security testing (brute force, fuzzing) SHALL NOT begin until a written authorization
|
||||
recording scope, source IPs, time window and emergency contact exists, a tested backup/snapshot is
|
||||
taken, and dedicated test accounts are provisioned. Active tests SHALL run against a staging clone
|
||||
or within a maintenance window, and SHALL NOT target real user accounts.
|
||||
|
||||
#### Scenario: Test blocked without authorization
|
||||
- **WHEN** an operator attempts an active test without the recorded authorization and snapshot
|
||||
- **THEN** the runbook procedure halts at the pre-flight gate and the test does not run
|
||||
|
||||
#### Scenario: Test uses dedicated accounts on staging
|
||||
- **WHEN** a brute-force test runs
|
||||
- **THEN** it targets a dedicated test account on staging (or in a maintenance window), never a
|
||||
real user
|
||||
|
||||
### Requirement: Pre-flight security checklist
|
||||
|
||||
The repo SHALL ship a pre-flight checklist (`docs/SECURITY_AUDIT_CHECKLIST.md`) covering the three
|
||||
fronts — brute force, API/data access, inadvertent vectors — plus infrastructure. Each item SHALL
|
||||
have an explicit pass/fail criterion.
|
||||
|
||||
#### Scenario: Checklist covers all fronts with criteria
|
||||
- **WHEN** an operator opens the checklist
|
||||
- **THEN** it contains markable items for each of the three fronts and infra, each with a defined
|
||||
pass/fail criterion
|
||||
|
||||
### Requirement: Non-destructive audit scripts
|
||||
|
||||
The repo SHALL provide audit scripts under `scripts/security/` (ACL probe, IDOR probe, brute-force
|
||||
harness, CSRF PoC, web recon) that are non-destructive and parameterized with test-account
|
||||
credentials rather than hard-coded production secrets.
|
||||
|
||||
#### Scenario: ACL probe reports access matrix
|
||||
- **WHEN** the ACL probe runs with a portal test account
|
||||
- **THEN** it reports, per model, whether read/write/create/unlink is allowed or denied, without
|
||||
modifying production data
|
||||
|
||||
#### Scenario: Scripts take credentials as parameters
|
||||
- **WHEN** a script is invoked
|
||||
- **THEN** target URL and credentials are supplied as parameters/env, with no secrets committed to
|
||||
the repo
|
||||
|
||||
### Requirement: Findings register tracks resolution
|
||||
|
||||
Findings SHALL be recorded in `docs/SECURITY_FINDINGS.md` with severity, evidence and status, and
|
||||
each SHALL be tracked until resolved and verified.
|
||||
|
||||
#### Scenario: Confirmed findings tracked to closure
|
||||
- **WHEN** the audit completes
|
||||
- **THEN** the register lists H1–H5 (and any new findings) with severity and a status of
|
||||
resolved/verified or an owner and plan
|
||||
Loading…
Add table
Add a link
Reference in a new issue