Skip to content
Closed
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
31 changes: 22 additions & 9 deletions lib/codebox-database.sh
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,17 @@
# CREATE DATABASE. Pointing a test harness at that account is the wrong
# direction to fail in — a broken test run should not be able to touch
# production data, and that account cannot create the scratch databases a
# test suite needs anyway. This provisions a separate, narrowly-scoped account
# instead: CREATE/DROP/DML limited to a `codebox_%` database-name pattern,
# nothing on the site database, no global privileges.
# test suite needs anyway. This provisions a separate, narrowly-scoped
# *provisioner* account instead. It is never projected into a test sandbox:
# wp-codebox's external MySQL provider uses it only on the host to create a
# per-run `wp_codebox_<random>` database plus a per-run user, hands the sandbox
# that throwaway user, and drops both afterwards. The grants are exactly what
# that contract needs and nothing on the site database:
# - ALL on `wp\_codebox\_%` WITH GRANT OPTION, so it can grant the per-run
# user its one database (grant option is limited to that pattern);
# - CREATE USER (a global privilege by MySQL design) for the per-run user;
# - SELECT (User, Host) on mysql.user, for wp-codebox's "is this random name
# unused" preflight. Column-scoped: password hashes stay unreadable.
#
# Credentials are generated once, stored root-owned (0600) outside the web
# root, and reach the agent service exactly the way the WP AI Gateway token
Expand All @@ -33,7 +41,7 @@ CODEBOX_DATABASE_PORT="${CODEBOX_DATABASE_PORT:-3306}"
# The db-name pattern a test harness may CREATE/DROP/use. Escaped so the
# underscore matches only a literal underscore rather than MySQL's "any single
# character" wildcard — tighter than what an unescaped pattern would grant.
CODEBOX_DATABASE_PATTERN="${CODEBOX_DATABASE_PATTERN:-codebox\\_%}"
CODEBOX_DATABASE_PATTERN="${CODEBOX_DATABASE_PATTERN:-wp\\_codebox\\_%}"
CODEBOX_DATABASE_MYSQL_BIN="${CODEBOX_DATABASE_MYSQL_BIN:-mysql}"

codebox_database_enabled() { systems_capabilities_enabled; }
Expand Down Expand Up @@ -82,15 +90,20 @@ codebox_database_env_content() {
# new value is simply discarded, and codebox_database_password() above already
# prefers the existing one for exactly that reason.
#
# The GRANT is scoped to the `codebox_%` pattern only: no ON *.*, no access to
# the site database, no WITH GRANT OPTION. That is the whole security
# invariant this module exists to hold, so it lives in one place rather than
# being reconstructed at each call site.
# The security invariant this module holds, in one place: no data privilege
# outside the `wp_codebox_%` pattern, no access to the site database, grant
# option only over that pattern, and the only global privilege is CREATE USER,
# which wp-codebox's per-run user isolation requires. mysql.user is readable
# only for its (User, Host) columns. The pattern matches the database names
# wp-codebox generates (`wp_codebox_<hex>`); the earlier `codebox_%` pattern
# never matched them, so every provision failed (homeboy#15333).
codebox_database_provision_sql() {
local password="$1"
cat <<SQL
CREATE USER IF NOT EXISTS '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST' IDENTIFIED BY '$password';
GRANT ALL PRIVILEGES ON \`$CODEBOX_DATABASE_PATTERN\`.* TO '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST';
GRANT ALL PRIVILEGES ON \`$CODEBOX_DATABASE_PATTERN\`.* TO '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST' WITH GRANT OPTION;
GRANT CREATE USER ON *.* TO '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST';
GRANT SELECT (User, Host) ON mysql.user TO '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST';
FLUSH PRIVILEGES;
SQL
}
Expand Down
15 changes: 10 additions & 5 deletions tests/codebox-database.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,20 @@ assert_contains "renders user" "$CONTENT" "WP_CODEBOX_DB_USER=$CODEBOX_DATABASE_
assert_contains "renders the password" "$CONTENT" "WP_CODEBOX_DB_PASSWORD=secretvalue"

echo ""
echo "==> the GRANT is scoped to the codebox_% pattern only — the security invariant"
echo "==> grants match wp-codebox's external MySQL contract, and nothing more"
SQL="$(codebox_database_provision_sql secretvalue)"
assert_contains "creates the user without erroring on re-run" "$SQL" "CREATE USER IF NOT EXISTS"
assert_contains "grants on the codebox pattern" "$SQL" "GRANT ALL PRIVILEGES ON \`codebox\\_%\`.* TO"
assert_contains "data grant targets the databases wp-codebox generates" "$SQL" "GRANT ALL PRIVILEGES ON \`wp\\_codebox\\_%\`.* TO"
assert_contains "grant option is limited to that pattern" "$SQL" "\`wp\\_codebox\\_%\`.* TO '$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST' WITH GRANT OPTION"
assert_contains "may create the per-run user" "$SQL" "GRANT CREATE USER ON *.* TO"
assert_contains "mysql.user is readable only for User and Host" "$SQL" "GRANT SELECT (User, Host) ON mysql.user TO"
assert_contains "scoped to this host only" "$SQL" "'$CODEBOX_DATABASE_USER'@'$CODEBOX_DATABASE_HOST'"
refute_contains "never grants on every database" "$SQL" "ON *.*"
refute_contains "never grants globally" "$SQL" "ON \`*\`.*"
refute_contains "never grants the ability to grant" "$SQL" "WITH GRANT OPTION"
refute_contains "never grants all privileges globally" "$SQL" "GRANT ALL PRIVILEGES ON *.*"
refute_contains "never grants on every table globally" "$SQL" "ON \`*\`.*"
refute_contains "never reads password hashes" "$SQL" "GRANT SELECT ON mysql.user"
refute_contains "never names a specific non-pattern database" "$SQL" "\`wordpress\`"
ON_STAR="$(printf '%s\n' "$SQL" | grep -c 'ON \*\.\*' || true)"
assert_eq "CREATE USER is the only global grant" "1" "$ON_STAR"

echo ""
echo "==> password is generated once and reused, not rotated on every run"
Expand Down
Loading