File size: 8,662 Bytes
d70361b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
"""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))