"""Split operator report viewing out of endpoints.view. Operator reports carry a screenshot of whatever was on the operator's screen when they filed the report. That is an uncontrolled human capture - it can include part numbers, customer names, email, or anything else that happened to be visible - whereas the rest of endpoints.view is uniform machine-collected telemetry. Reading a PC's CPU load does not imply a right to read someone's screen, so the reports and their images move to their own grant. Backfill preserves access rather than revoking it: every user who can see endpoints today gets the new grant, so nobody loses a panel they were already using. Tightening from there is an administrative decision made per account in the user editor, not something this migration should force. The grant is a per-user direct permission (endpoints.operatorreports is in the scope denylist), so the backfill writes userpermissions rows, not role rows. Revision ID: 7d47_endpoints_operatorreports Revises: 7d46_nest_app_packages """ from alembic import op import sqlalchemy as sa revision = '7d47_endpoints_operatorreports' down_revision = '7d46_nest_app_packages' branch_labels = None depends_on = None NEW_PERMISSION = ( 'endpoints.operatorreports', 'View operator-submitted reports and their screenshots', 'endpoints', ) SOURCE_PERMISSION = 'endpoints.view' def _tables_present(bind): inspector = sa.inspect(bind) tables = set(inspector.get_table_names()) return {'users', 'roles', 'permissions', 'rolepermissions', 'userpermissions', 'userroles'} <= tables def upgrade(): bind = op.get_bind() if not _tables_present(bind): return name, description, category = NEW_PERMISSION # Guarded in Python so the statement stays portable: MySQL's FROM DUAL # idiom does not run on SQLite, which the test suite uses. exists = bind.execute(sa.text( 'SELECT 1 FROM permissions WHERE name = :name'), {'name': name}).first() if not exists: bind.execute(sa.text( 'INSERT INTO permissions (name, description, category) ' 'VALUES (:name, :description, :category)'), {'name': name, 'description': description, 'category': category}) # Preserve access for anyone who reaches operator reports today, which is # anyone holding endpoints.view through a role. NOT EXISTS keeps the # migration re-runnable and avoids duplicate rows for users in two # qualifying roles. bind.execute(sa.text( 'INSERT INTO userpermissions (userid, permissionid) ' 'SELECT DISTINCT ur.userid, new.permissionid ' 'FROM userroles ur ' 'JOIN rolepermissions rp ON rp.roleid = ur.roleid ' 'JOIN permissions old ON old.permissionid = rp.permissionid ' 'JOIN permissions new ON new.name = :newname ' 'WHERE old.name = :oldname ' 'AND NOT EXISTS (SELECT 1 FROM userpermissions existing ' ' WHERE existing.userid = ur.userid ' ' AND existing.permissionid = new.permissionid)'), {'newname': name, 'oldname': SOURCE_PERMISSION}) def downgrade(): bind = op.get_bind() if not _tables_present(bind): return name = NEW_PERMISSION[0] bind.execute(sa.text( 'DELETE FROM userpermissions WHERE permissionid IN ' '(SELECT permissionid FROM permissions WHERE name = :name)'), {'name': name}) bind.execute(sa.text( 'DELETE FROM rolepermissions WHERE permissionid IN ' '(SELECT permissionid FROM permissions WHERE name = :name)'), {'name': name}) bind.execute(sa.text( 'DELETE FROM permissions WHERE name = :name'), {'name': name})