1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:58:14 +03:00
This commit is contained in:
Anton Afanasyeu
2026-05-23 00:55:51 +02:00
parent 0d9cf0cf7c
commit 8a86abb833
5 changed files with 155 additions and 27 deletions

View File

@@ -60,7 +60,14 @@ Config (`config.php`):
``` ```
Permission checks: `Rbac::can('edit_tags', $companyId)` (used by `Auth::canEditTags()`). Permission checks: `Rbac::can('edit_tags', $companyId)` (used by `Auth::canEditTags()`).
Manual SQL: `sql/migrations/001_rbac_companies.sql` (MariaDB). Re-login after deploy so session includes company memberships. Manual SQL (MariaDB, **as root**`androidcast` has no `ALTER`):
```sh
mysql -u root -p androidcast_crashes < sql/migrations/001_rbac_companies.sql
mysql -u root -p androidcast_crashes < sql/migrations/002_reports_company_columns.sql
```
Re-login after deploy so session includes company memberships.
## UI languages ## UI languages

View File

@@ -7,6 +7,7 @@ set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)" ROOT="$(cd "$(dirname "$0")/.." && pwd)"
SCHEMA="$ROOT/sql/schema.mariadb.sql" SCHEMA="$ROOT/sql/schema.mariadb.sql"
MIG002="$ROOT/sql/migrations/002_reports_company_columns.sql"
DB_NAME="${CRASH_DB_NAME:-androidcast_crashes}" DB_NAME="${CRASH_DB_NAME:-androidcast_crashes}"
DB_USER="${CRASH_DB_USER:-androidcast}" DB_USER="${CRASH_DB_USER:-androidcast}"
DB_PASS="${CRASH_DB_PASS:-}" DB_PASS="${CRASH_DB_PASS:-}"
@@ -19,6 +20,11 @@ fi
echo "Applying schema from $SCHEMA ..." echo "Applying schema from $SCHEMA ..."
mysql -u root -p < "$SCHEMA" mysql -u root -p < "$SCHEMA"
if [ -f "$MIG002" ]; then
echo "Applying RBAC report columns (002) ..."
mysql -u root -p "$DB_NAME" < "$MIG002"
fi
if [ -n "$DB_PASS" ]; then if [ -n "$DB_PASS" ]; then
echo "Creating app user ${DB_USER}@localhost and @127.0.0.1 ..." echo "Creating app user ${DB_USER}@localhost and @127.0.0.1 ..."
mysql -u root -p <<EOF mysql -u root -p <<EOF

View File

@@ -41,6 +41,6 @@ CREATE TABLE IF NOT EXISTS devices (
CONSTRAINT fk_devices_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE CONSTRAINT fk_devices_company FOREIGN KEY (company_id) REFERENCES companies (id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- reports.company_id / reports.device_id added by application migration when missing -- reports.company_id / reports.device_id: run 002_reports_company_columns.sql as root (app user cannot ALTER)
INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default'); INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default');

View File

@@ -0,0 +1,24 @@
-- Add RBAC columns to existing reports table (requires ALTER — run as MySQL/MariaDB root).
-- App user androidcast typically has SELECT,INSERT,UPDATE,DELETE only.
--
-- mysql -u root -p androidcast_crashes < sql/migrations/002_reports_company_columns.sql
--
-- If 001_rbac_companies.sql was not applied yet, run that first.
USE androidcast_crashes;
INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default');
ALTER TABLE reports
ADD COLUMN IF NOT EXISTS company_id INT UNSIGNED NOT NULL DEFAULT 1 AFTER id,
ADD COLUMN IF NOT EXISTS device_id BIGINT UNSIGNED NULL AFTER company_id;
UPDATE reports SET company_id = 1 WHERE company_id IS NULL OR company_id = 0;
ALTER TABLE reports ADD INDEX IF NOT EXISTS idx_reports_company (company_id);
ALTER TABLE reports ADD INDEX IF NOT EXISTS idx_reports_device (device_id);
INSERT IGNORE INTO company_memberships (user_id, company_id, role)
SELECT u.id, 1,
CASE WHEN LOWER(u.role) IN ('root', 'admin', 'platform_admin') THEN 'owner' ELSE 'member' END
FROM users u;

View File

@@ -158,6 +158,32 @@ final class Database {
self::ensureRbacSchema($pdo); self::ensureRbacSchema($pdo);
} }
/**
* Run DDL; app DB user often has no ALTER — log and continue (run sql/migrations as root).
*/
private static function execSchemaSql(PDO $pdo, string $sql, string $label): bool {
try {
$pdo->exec($sql);
return true;
} catch (PDOException $e) {
$msg = $label . ': ' . $e->getMessage();
error_log('crash_reporter schema ' . $msg);
if (function_exists('log_crash_event')) {
log_crash_event('schema', $msg);
}
return false;
}
}
public static function reportsHaveRbacColumns(?PDO $pdo = null): bool {
$pdo ??= self::$pdo;
if ($pdo === null || !self::tableExists($pdo, 'reports')) {
return false;
}
$names = self::columnNames($pdo, 'reports');
return in_array('company_id', $names, true);
}
/** Phase 1 RBAC tables + report company/device columns (idempotent). */ /** Phase 1 RBAC tables + report company/device columns (idempotent). */
public static function ensureRbacSchema(?PDO $pdo = null): void { public static function ensureRbacSchema(?PDO $pdo = null): void {
$pdo ??= self::$pdo; $pdo ??= self::$pdo;
@@ -170,6 +196,12 @@ final class Database {
self::ensureDevicesTable($pdo); self::ensureDevicesTable($pdo);
self::ensureReportCompanyColumns($pdo); self::ensureReportCompanyColumns($pdo);
self::backfillCompanyMemberships($pdo); self::backfillCompanyMemberships($pdo);
if (!self::reportsHaveRbacColumns($pdo)) {
error_log(
'crash_reporter: reports.company_id missing — run as MySQL root: '
. 'mysql -u root -p androidcast_crashes < sql/migrations/002_reports_company_columns.sql'
);
}
} }
private static function ensureCompaniesTable(PDO $pdo): void { private static function ensureCompaniesTable(PDO $pdo): void {
@@ -177,33 +209,45 @@ final class Database {
return; return;
} }
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE companies ( "CREATE TABLE companies (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(64) NOT NULL, slug VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_companies_slug (slug) UNIQUE KEY uq_companies_slug (slug)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'create companies'
); );
return; return;
} }
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE companies ( "CREATE TABLE companies (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE, slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL, name TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
)" )",
'create companies'
); );
} }
private static function seedDefaultCompany(PDO $pdo): void { private static function seedDefaultCompany(PDO $pdo): void {
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec("INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')"); self::execSchemaSql(
$pdo,
"INSERT IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')",
'seed company'
);
return; return;
} }
$pdo->exec("INSERT OR IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')"); self::execSchemaSql(
$pdo,
"INSERT OR IGNORE INTO companies (id, slug, name) VALUES (1, 'default', 'Default')",
'seed company'
);
} }
private static function ensureCompanyMembershipsTable(PDO $pdo): void { private static function ensureCompanyMembershipsTable(PDO $pdo): void {
@@ -211,7 +255,8 @@ final class Database {
return; return;
} }
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE company_memberships ( "CREATE TABLE company_memberships (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id INT UNSIGNED NOT NULL, user_id INT UNSIGNED NOT NULL,
@@ -221,11 +266,13 @@ final class Database {
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_membership_user_company (user_id, company_id), UNIQUE KEY uq_membership_user_company (user_id, company_id),
KEY idx_membership_company (company_id) KEY idx_membership_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'create company_memberships'
); );
return; return;
} }
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE company_memberships ( "CREATE TABLE company_memberships (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL, user_id INTEGER NOT NULL,
@@ -234,7 +281,8 @@ final class Database {
permissions_json TEXT, permissions_json TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')), created_at TEXT NOT NULL DEFAULT (datetime('now')),
UNIQUE (user_id, company_id) UNIQUE (user_id, company_id)
)" )",
'create company_memberships'
); );
} }
@@ -243,7 +291,8 @@ final class Database {
return; return;
} }
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE devices ( "CREATE TABLE devices (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
company_id INT UNSIGNED NOT NULL, company_id INT UNSIGNED NOT NULL,
@@ -257,11 +306,13 @@ final class Database {
meta_json LONGTEXT NULL, meta_json LONGTEXT NULL,
UNIQUE KEY uq_devices_company_external (company_id, external_id), UNIQUE KEY uq_devices_company_external (company_id, external_id),
KEY idx_devices_company (company_id) KEY idx_devices_company (company_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci" ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci",
'create devices'
); );
return; return;
} }
$pdo->exec( self::execSchemaSql(
$pdo,
"CREATE TABLE devices ( "CREATE TABLE devices (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
company_id INTEGER NOT NULL, company_id INTEGER NOT NULL,
@@ -274,7 +325,8 @@ final class Database {
last_seen_at_ms INTEGER NOT NULL, last_seen_at_ms INTEGER NOT NULL,
meta_json TEXT, meta_json TEXT,
UNIQUE (company_id, external_id) UNIQUE (company_id, external_id)
)" )",
'create devices'
); );
} }
@@ -285,22 +337,48 @@ final class Database {
$names = self::columnNames($pdo, 'reports'); $names = self::columnNames($pdo, 'reports');
if (!in_array('company_id', $names, true)) { if (!in_array('company_id', $names, true)) {
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec( self::execSchemaSql(
'ALTER TABLE reports ADD COLUMN company_id INT UNSIGNED NOT NULL DEFAULT 1' $pdo,
'ALTER TABLE reports ADD COLUMN company_id INT UNSIGNED NOT NULL DEFAULT 1',
'alter reports.company_id'
);
self::execSchemaSql(
$pdo,
'ALTER TABLE reports ADD KEY idx_reports_company (company_id)',
'index reports.company_id'
); );
$pdo->exec('ALTER TABLE reports ADD KEY idx_reports_company (company_id)');
} else { } else {
$pdo->exec('ALTER TABLE reports ADD COLUMN company_id INTEGER NOT NULL DEFAULT 1'); self::execSchemaSql(
$pdo,
'ALTER TABLE reports ADD COLUMN company_id INTEGER NOT NULL DEFAULT 1',
'alter reports.company_id'
);
} }
$pdo->exec('UPDATE reports SET company_id = 1 WHERE company_id IS NULL OR company_id = 0'); self::execSchemaSql(
$pdo,
'UPDATE reports SET company_id = 1 WHERE company_id IS NULL OR company_id = 0',
'backfill reports.company_id'
);
} }
$names = self::columnNames($pdo, 'reports'); $names = self::columnNames($pdo, 'reports');
if (!in_array('device_id', $names, true)) { if (!in_array('device_id', $names, true)) {
if (self::isMysql()) { if (self::isMysql()) {
$pdo->exec('ALTER TABLE reports ADD COLUMN device_id BIGINT UNSIGNED NULL'); self::execSchemaSql(
$pdo->exec('ALTER TABLE reports ADD KEY idx_reports_device (device_id)'); $pdo,
'ALTER TABLE reports ADD COLUMN device_id BIGINT UNSIGNED NULL',
'alter reports.device_id'
);
self::execSchemaSql(
$pdo,
'ALTER TABLE reports ADD KEY idx_reports_device (device_id)',
'index reports.device_id'
);
} else { } else {
$pdo->exec('ALTER TABLE reports ADD COLUMN device_id INTEGER NULL'); self::execSchemaSql(
$pdo,
'ALTER TABLE reports ADD COLUMN device_id INTEGER NULL',
'alter reports.device_id'
);
} }
} }
} }
@@ -340,8 +418,17 @@ final class Database {
private static function ensureMysqlReportColumns(PDO $pdo): void { private static function ensureMysqlReportColumns(PDO $pdo): void {
$names = self::columnNames($pdo, 'reports'); $names = self::columnNames($pdo, 'reports');
if (!in_array('tags_json', $names, true)) { if (!in_array('tags_json', $names, true)) {
$pdo->exec("ALTER TABLE reports ADD COLUMN tags_json LONGTEXT NOT NULL"); if (self::execSchemaSql(
$pdo->exec("UPDATE reports SET tags_json = '[]' WHERE tags_json IS NULL OR tags_json = ''"); $pdo,
'ALTER TABLE reports ADD COLUMN tags_json LONGTEXT NOT NULL',
'alter reports.tags_json'
)) {
self::execSchemaSql(
$pdo,
"UPDATE reports SET tags_json = '[]' WHERE tags_json IS NULL OR tags_json = ''",
'backfill reports.tags_json'
);
}
} }
} }
@@ -364,7 +451,11 @@ final class Database {
private static function ensureReportColumns(PDO $pdo): void { private static function ensureReportColumns(PDO $pdo): void {
$names = self::columnNames($pdo, 'reports'); $names = self::columnNames($pdo, 'reports');
if (!in_array('tags_json', $names, true)) { if (!in_array('tags_json', $names, true)) {
$pdo->exec("ALTER TABLE reports ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'"); self::execSchemaSql(
$pdo,
"ALTER TABLE reports ADD COLUMN tags_json TEXT NOT NULL DEFAULT '[]'",
'alter reports.tags_json'
);
} }
} }