satdetect-dev / app /dda /rbac /seed.py
coderuday21's picture
Cursor
Deploy satdetect-dev with Priyanka audit/exception logs.
d70361b
Raw History Blame Contribute Delete
8.66 kB
"""Seed the default roles, modules, menu items, and role permissions, and
backfill role_id on any user still only carrying the legacy role string.
Idempotent — safe to call on every startup (mirrors app/dda/seed.py)."""
from __future__ import annotations
import logging
from sqlalchemy.orm import Session
from ...models import User
from .models import MenuItem, Module, Role, RolePermission
logger = logging.getLogger(__name__)
# (name, description, rank) — rank preserves the ordinal check dda_auth.py
# already relied on (ROLE_RANK) before this table existed.
DEFAULT_ROLES = [
("viewer", "Read-only access", 0),
("uploader", "Can view and upload images", 1),
("analyst", "Can upload, compare, and run detections", 2),
("admin", "Full system access", 3),
]
# (key, name, description)
DEFAULT_MODULES = [
("home", "Dashboard", "System dashboard — detection overview, regions, reports"),
("library", "Master Library", "Browse and upload satellite / drone imagery"),
("masters", "Masters", "Zone / district and village / location master data"),
("detect", "Change Detection", "Compare imagery and run detections"),
("reports", "Reports", "Detection history, exports, and PDF reports"),
("logs", "Logs", "Application log viewer"),
("admin", "Administration", "Users, roles, modules, and menu management"),
("audit_log", "Audit Log", "User activity audit trail"),
("exception_log", "Exception Log", "System error log"),
]
# (label, url, module_key, sort_order, parent_label)
# Items with a parent_label render as a collapsible group in the sidebar
# (see templates/partials/navbar_dda.html) — group headers themselves have no
# module (url "#…" never navigated to directly) so they stay structurally
# present; the navbar only shows the group once it has a visible child.
DEFAULT_MENU_ITEMS = [
("Dashboard", "/", "home", 0, None),
("Masters", "#masters", None, 1, None),
("Master Library", "/library", "library", 0, "Masters"),
("Zone / District", "/masters/zones", "masters", 1, "Masters"),
("Village / Location", "/masters/villages", "masters", 2, "Masters"),
("Change Detection", "/detect", "detect", 2, None),
("Reports", "/reports", "reports", 3, None),
# "Logs" (top-level) was retired below — Application Logs is now a tab on
# the Exception Log page instead of its own sidebar entry.
("Configuration", "#", None, 5, None),
("Roles & Users", "/admin/roles-users", "admin", 0, "Configuration"),
("App Modules", "/admin/modules", "admin", 1, "Configuration"),
("Menu Management", "/admin/menu", "admin", 2, "Configuration"),
("Audit Log", "/audit-log", "audit_log", 3, "Configuration"),
("Exception Log", "/exception-log", "exception_log", 4, "Configuration"),
]
# module_key -> {role_name: (can_view, can_create, can_edit, can_delete)}
# Approximates current viewer < uploader < analyst < admin behavior.
DEFAULT_PERMISSIONS = {
"home": {
"viewer": (1, 0, 0, 0), "uploader": (1, 0, 0, 0),
"analyst": (1, 0, 0, 0), "admin": (1, 1, 1, 1),
},
"library": {
"viewer": (1, 0, 0, 0), "uploader": (1, 1, 0, 0),
"analyst": (1, 1, 1, 0), "admin": (1, 1, 1, 1),
},
"masters": {
"viewer": (1, 0, 0, 0), "uploader": (1, 0, 0, 0),
"analyst": (1, 1, 1, 0), "admin": (1, 1, 1, 1),
},
"detect": {
"viewer": (1, 0, 0, 0), "uploader": (1, 1, 0, 0),
"analyst": (1, 1, 1, 0), "admin": (1, 1, 1, 1),
},
"reports": {
"viewer": (1, 0, 0, 0), "uploader": (1, 0, 0, 0),
"analyst": (1, 1, 1, 0), "admin": (1, 1, 1, 1),
},
"logs": {
"viewer": (0, 0, 0, 0), "uploader": (0, 0, 0, 0),
"analyst": (0, 0, 0, 0), "admin": (1, 1, 1, 1),
},
"admin": {
"viewer": (0, 0, 0, 0), "uploader": (0, 0, 0, 0),
"analyst": (0, 0, 0, 0), "admin": (1, 1, 1, 1),
},
"audit_log": {
"viewer": (0, 0, 0, 0), "uploader": (0, 0, 0, 0),
"analyst": (0, 0, 0, 0), "admin": (1, 1, 1, 1),
},
"exception_log": {
"viewer": (0, 0, 0, 0), "uploader": (0, 0, 0, 0),
"analyst": (0, 0, 0, 0), "admin": (1, 1, 1, 1),
},
}
def seed_rbac(db: Session) -> None:
roles_by_name = {}
for name, description, rank in DEFAULT_ROLES:
role = db.query(Role).filter(Role.name == name).first()
if not role:
role = Role(name=name, description=description, rank=rank, is_system=True)
db.add(role)
db.flush()
roles_by_name[name] = role
modules_by_key = {}
for key, name, description in DEFAULT_MODULES:
module = db.query(Module).filter(Module.key == key).first()
if not module:
module = Module(key=key, name=name, description=description, status="in_use")
db.add(module)
db.flush()
else:
if module.name != name:
module.name = name
if description and module.description != description:
module.description = description
modules_by_key[key] = module
# Two passes: top-level items (incl. "Configuration") first, so their ids
# exist when the second pass wires up children via parent_label.
items_by_label = {}
for label, url, module_key, sort_order, parent_label in DEFAULT_MENU_ITEMS:
if parent_label is not None:
continue
module_id = modules_by_key[module_key].id if module_key else None
existing_item = db.query(MenuItem).filter(MenuItem.url == url).first()
if not existing_item:
existing_item = MenuItem(
label=label, url=url, module_id=module_id,
sort_order=sort_order, is_active=True,
)
db.add(existing_item)
db.flush()
else:
existing_item.label = label
existing_item.module_id = module_id
existing_item.sort_order = sort_order
existing_item.parent_id = None
items_by_label[label] = existing_item
for label, url, module_key, sort_order, parent_label in DEFAULT_MENU_ITEMS:
if parent_label is None:
continue
parent = items_by_label[parent_label]
module_id = modules_by_key[module_key].id if module_key else None
existing_item = db.query(MenuItem).filter(MenuItem.url == url).first()
if not existing_item:
db.add(MenuItem(
label=label, url=url, module_id=module_id, parent_id=parent.id,
sort_order=sort_order, is_active=True,
))
else:
# Fixup for rows seeded before this item had a parent group (e.g.
# Image Library was top-level; admin pages were under a flat
# "Admin" label) — re-parent + re-label known system nav rows.
if existing_item.label in ("Admin", "Image Library") or existing_item.label != label:
existing_item.label = label
existing_item.module_id = module_id
existing_item.parent_id = parent.id
existing_item.sort_order = sort_order
for module_key, role_perms in DEFAULT_PERMISSIONS.items():
module = modules_by_key[module_key]
for role_name, (view, create, edit, delete) in role_perms.items():
role = roles_by_name[role_name]
existing = (
db.query(RolePermission)
.filter(RolePermission.role_id == role.id, RolePermission.module_id == module.id)
.first()
)
if not existing:
db.add(RolePermission(
role_id=role.id, module_id=module.id,
can_view=bool(view), can_create=bool(create),
can_edit=bool(edit), can_delete=bool(delete),
))
# Retire the old top-level "Logs" sidebar entry — its content (Application
# Logs) is now a tab on the Exception Log page (see exception_log_dda.html).
retired_logs_item = (
db.query(MenuItem)
.filter(MenuItem.url == "/logs", MenuItem.parent_id.is_(None), MenuItem.is_active.is_(True))
.first()
)
if retired_logs_item:
retired_logs_item.is_active = False
db.commit()
unmigrated = db.query(User).filter(User.role_id.is_(None)).all()
if unmigrated:
for u in unmigrated:
legacy = (u.role or "analyst").strip().lower()
role = roles_by_name.get(legacy, roles_by_name["analyst"])
u.role_id = role.id
db.commit()
logger.info("RBAC: backfilled role_id for %d user(s)", len(unmigrated))