mirror of
git://f0xx.org/android_cast
synced 2026-07-29 05:58:14 +03:00
rbac
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user