Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sample-apps/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,6 @@ Overview :
- it runs 4 processes, called workers, (**multi-process**) which handles requests using ASGI protocol
- This application uses **ASGI** (Asynchronous Server Gateway Interface)
- Runs on 8114. Without Aikido runs on 8115
- `odoo-postgres/` is a real Odoo app using PostgreSQL and an installed Zen wheel.
- It supports Odoo's threaded mode and configurable prefork workers.
- Runs on 8116. Without Aikido runs on 8117.
19 changes: 19 additions & 0 deletions sample-apps/odoo-postgres/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
ARG ODOO_IMAGE=odoo:16.0@sha256:0f36a5002a200bb1649771c2cb9403ca5392d7ac4bb23f9ad500b339df3f5a3a
FROM ${ODOO_IMAGE}

USER root

RUN apt-get update \
&& apt-get install --yes --no-install-recommends build-essential python3-dev \
&& rm -rf /var/lib/apt/lists/*

COPY dist/*.whl /tmp/aikido-zen/
RUN python3 -m pip install --no-cache-dir /tmp/aikido-zen/*.whl \
&& rm -rf /tmp/aikido-zen

COPY sample-apps/odoo-postgres/config/odoo.conf /etc/odoo/odoo.conf
COPY --chown=odoo:odoo sample-apps/odoo-postgres/addons/ /mnt/extra-addons/
COPY sample-apps/odoo-postgres/scripts/initialize-database.sh /usr/local/bin/initialize-odoo-database
RUN chmod +x /usr/local/bin/initialize-odoo-database

USER odoo
30 changes: 30 additions & 0 deletions sample-apps/odoo-postgres/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
ROOT_DIR := ../..
COMPOSE := docker compose

.PHONY: build build-wheel up run runZenDisabled smoke logs down clean

build-wheel:
$(MAKE) -C $(ROOT_DIR) build

build: build-wheel
$(COMPOSE) build

up: build
$(COMPOSE) up --detach --wait odoo odoo-disabled

run: build
$(COMPOSE) up odoo

runZenDisabled: build
$(COMPOSE) up odoo-disabled

smoke: build
./scripts/smoke-test.sh

logs:
$(COMPOSE) logs --follow odoo odoo-disabled

down:
$(COMPOSE) down --volumes --remove-orphans

clean: down
83 changes: 83 additions & 0 deletions sample-apps/odoo-postgres/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Odoo/PostgreSQL sample

> **Warning:** This application contains intentional command- and SQL-injection vulnerabilities. Run it only in an isolated development environment. Never deploy it or expose it to an untrusted network.

This sample runs the official Odoo server with PostgreSQL. The image installs the locally built `aikido_zen` wheel and loads Zen through the `post_load` hook of the server-wide `aikido_zen_bootstrap` addon. It does not use a source bind mount, `PYTHONSTARTUP`, a standalone WSGI application, or middleware around Odoo.

The Compose project creates two isolated Odoo databases and data volumes:

- `odoo` uses the Zen bootstrap and listens on port `8116`.
- `odoo-disabled` loads the same bootstrap with `AIKIDO_DISABLE=true` and listens on port `8117`.

Database initialization installs the `zen_test` addon without loading the Zen bootstrap. The runtime processes then start `/usr/bin/odoo` with `--load=base,web,aikido_zen_bootstrap` before Odoo preloads registries or database connections.

## Requirements

- Docker with Compose
- GNU Make
- Poetry, as required by the repository build

## Run the sample

```bash
make up
curl http://localhost:8116/zen/status
curl http://localhost:8117/zen/status
make down
```

Use prefork mode by setting the worker count before starting the services:

```bash
ODOO_WORKERS=2 make up
```

Override the host ports when necessary:

```bash
ODOO_PORT=8216 ODOO_DISABLED_PORT=8217 make up
```

The default image is the official Odoo 16 image pinned to an immutable digest. A different official image can be supplied for compatibility work:

```bash
ODOO_IMAGE=odoo:17.0 make build
```

## Smoke test

```bash
make smoke
```

The smoke test runs fresh `workers=0` and `workers=2` environments. It verifies that PostgreSQL is healthy, both databases contain the installed test addon, the wheel is installed under `/usr/local`, `/usr/bin/odoo` is the real server process, the bootstrap runs only at runtime, form/JSON/JSON-RPC/route inputs reach controllers unchanged, the disabled service starts, and shutdown removes the project containers and volumes.

Set `ODOO_WORKER_COUNTS` to test a subset while developing:

```bash
ODOO_WORKER_COUNTS=0 make smoke
```

## Test endpoints

| Endpoint | Input | Behavior |
| --- | --- | --- |
| `GET /zen/status` | None | Returns `ok`. |
| `GET /zen/shell/query?command=...` | Query string | Passes `command` to a shell. |
| `POST /zen/shell/form` | Form field `command` | Passes `command` to a shell. |
| `POST /zen/shell/json` | JSON field `command` | Passes `command` to a shell through an HTTP route. |
| `POST /zen/shell/jsonrpc` | JSON-RPC `params.command` | Passes `command` to a shell. |
| `GET /zen/shell/header` | `X-Command` header | Passes the header to a shell. |
| `GET /zen/shell/cookie` | `command` cookie | Passes the cookie to a shell. |
| `GET /zen/shell/route/<command>` | Route parameter | Passes the route value to a shell. |
| `GET /zen/sql?query=...` | Query string | Executes raw SQL through `request.env.cr.execute`. |
| `GET /zen/error` | None | Raises an intentional application exception. |
| `GET /zen/stream` | None | Returns a two-chunk streaming response. |
| `GET /zen/user` | Authenticated session | Returns the stable Odoo user ID. |
| `POST /zen/request-block-side-effect` | None | Records a request-block test side effect. |
| `POST /zen/rate-limit-side-effect` | None | Records a rate-limit test side effect. |
| `GET /zen/side-effects/<name>` | Route parameter | Returns the persisted side-effect count. |

The installed test account is `zen-test-user` with password `zen-test-password`. It exists only for authentication lifecycle checks in this isolated sample.

Phase 1 validates the real runtime and bootstrap only. Odoo-specific Zen request blocking is added and asserted in later phases.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .hooks import post_load
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "Aikido Zen Bootstrap",
"version": "1.0.0",
"license": "AGPL-3",
"depends": ["base"],
"post_load": "post_load",
"installable": True,
"application": False,
}
10 changes: 10 additions & 0 deletions sample-apps/odoo-postgres/addons/aikido_zen_bootstrap/hooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import logging

import aikido_zen

logger = logging.getLogger(__name__)


def post_load():
aikido_zen.protect()
logger.info("Aikido Zen bootstrap post_load completed")
2 changes: 2 additions & 0 deletions sample-apps/odoo-postgres/addons/zen_test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import controllers
from . import models
12 changes: 12 additions & 0 deletions sample-apps/odoo-postgres/addons/zen_test/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "Zen Odoo Test Application",
"version": "1.0.0",
"license": "AGPL-3",
"depends": ["base", "web"],
"data": [
"security/ir.model.access.csv",
"data/test_users.xml",
],
"installable": True,
"application": False,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import main
141 changes: 141 additions & 0 deletions sample-apps/odoo-postgres/addons/zen_test/controllers/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import subprocess

from odoo import http, release
from odoo.http import Response, request

JSON_ROUTE_TYPE = "jsonrpc" if release.version_info[0] >= 19 else "json"


def _text_response(value, status=200):
return request.make_response(
value,
headers=[("Content-Type", "text/plain; charset=utf-8")],
status=status,
)


def _execute_command(command):
subprocess.run(command, capture_output=True, check=False, shell=True, text=True)
return command


def _record_side_effect(name):
request.env["zen.test.side.effect"].sudo().create({"name": name})


class ZenTestController(http.Controller):
@http.route("/zen/status", type="http", auth="public", methods=["GET"])
def status(self):
return _text_response("ok")

@http.route("/zen/shell/query", type="http", auth="public", methods=["GET"])
def shell_query(self, command=""):
return _text_response(_execute_command(command))

@http.route(
"/zen/shell/form",
type="http",
auth="public",
methods=["POST"],
csrf=False,
)
def shell_form(self, command=""):
return _text_response(_execute_command(command))

@http.route(
"/zen/shell/json",
type="http",
auth="public",
methods=["POST"],
csrf=False,
)
def shell_json(self):
command = request.get_json_data().get("command", "")
return _text_response(_execute_command(command))

@http.route(
"/zen/shell/jsonrpc",
type=JSON_ROUTE_TYPE,
auth="public",
methods=["POST"],
csrf=False,
)
def shell_jsonrpc(self, command=""):
return _execute_command(command)

@http.route("/zen/shell/header", type="http", auth="public", methods=["GET"])
def shell_header(self):
command = request.httprequest.headers.get("X-Command", "")
return _text_response(_execute_command(command))

@http.route("/zen/shell/cookie", type="http", auth="public", methods=["GET"])
def shell_cookie(self):
command = request.httprequest.cookies.get("command", "")
return _text_response(_execute_command(command))

@http.route(
"/zen/shell/route/<path:command>",
type="http",
auth="public",
methods=["GET"],
)
def shell_route(self, command):
return _text_response(_execute_command(command))

@http.route("/zen/sql", type="http", auth="public", methods=["GET"])
def unsafe_sql(self, query="SELECT 1"):
request.env.cr.execute(query)
result = request.env.cr.fetchone() if request.env.cr.description else None
return _text_response(repr(result))

@http.route("/zen/error", type="http", auth="public", methods=["GET"])
def error(self):
raise RuntimeError("Intentional exception from the Zen Odoo test addon")

@http.route("/zen/stream", type="http", auth="public", methods=["GET"])
def stream(self):
def chunks():
yield b"first\n"
yield b"second\n"

return Response(chunks(), content_type="text/plain")

@http.route("/zen/user", type="http", auth="user", methods=["GET"])
def user(self):
return _text_response(str(request.session.uid))

@http.route(
"/zen/request-block-side-effect",
type="http",
auth="public",
methods=["POST"],
csrf=False,
)
def request_block_side_effect(self):
_record_side_effect("request-block")
return _text_response("recorded")

@http.route(
"/zen/rate-limit-side-effect",
type="http",
auth="public",
methods=["POST"],
csrf=False,
)
def rate_limit_side_effect(self):
_record_side_effect("rate-limit")
return _text_response("recorded")

@http.route(
"/zen/side-effects/<string:name>",
type="http",
auth="public",
methods=["GET"],
)
def side_effect_count(self, name):
count = (
request.env["zen.test.side.effect"]
.sudo()
.search_count([("name", "=", name)])
)
return _text_response(str(count))
9 changes: 9 additions & 0 deletions sample-apps/odoo-postgres/addons/zen_test/data/test_users.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="zen_test_user" model="res.users">
<field name="name">Zen Test User</field>
<field name="login">zen-test-user</field>
<field name="password">zen-test-password</field>
<field name="groups_id" eval="[(6, 0, [ref('base.group_user')])]"/>
</record>
</odoo>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import side_effect
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from odoo import fields, models


class ZenTestSideEffect(models.Model):
_name = "zen.test.side.effect"
_description = "Zen Test Side Effect"

name = fields.Char(required=True, index=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_zen_test_side_effect_system,zen.test.side.effect.system,model_zen_test_side_effect,base.group_system,1,1,1,1
11 changes: 11 additions & 0 deletions sample-apps/odoo-postgres/config/odoo.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[options]
addons_path = /usr/lib/python3/dist-packages/odoo/addons,/mnt/extra-addons
data_dir = /var/lib/odoo
admin_passwd = admin
db_host = postgres
db_port = 5432
db_user = odoo
db_password = odoo
http_port = 8069
list_db = False
log_level = info
Loading
Loading