diff --git a/app/init_wizard.py b/app/init_wizard.py index f227036..5f9a66d 100644 --- a/app/init_wizard.py +++ b/app/init_wizard.py @@ -151,61 +151,93 @@ def init_default_permissions(db: Session) -> list[Permission]: return db.query(Permission).all() -def init_admin_role(db: Session, admin_user: models.User) -> None: - """Create admin role with all permissions and guest role with minimal permissions.""" - # Check if admin role already exists - admin_role = db.query(Role).filter(Role.name == "admin").first() - if not admin_role: - admin_role = Role( - name="admin", - description="Administrator - full access to all features", - is_global=True - ) - db.add(admin_role) +# --------------------------------------------------------------------------- +# Default role → permission mapping +# --------------------------------------------------------------------------- + +# mgr: project management + all milestone/task/propose actions +_MGR_PERMISSIONS = { + "project.read", "project.write", "project.manage_members", + "task.create", "task.read", "task.write", "task.delete", + "milestone.create", "milestone.read", "milestone.write", "milestone.delete", + "milestone.freeze", "milestone.start", "milestone.close", + "task.close", "task.reopen_closed", "task.reopen_completed", + "propose.accept", "propose.reject", "propose.reopen", + "monitor.read", +} + +# dev: day-to-day development work — no freeze/start/close milestone, no accept/reject propose +_DEV_PERMISSIONS = { + "project.read", + "task.create", "task.read", "task.write", + "milestone.read", + "task.close", "task.reopen_closed", "task.reopen_completed", + "monitor.read", +} + +# Role definitions: (name, description, permission_set) +_DEFAULT_ROLES = [ + ("admin", "Administrator - full access to all features", None), # None ⇒ all perms + ("mgr", "Manager - project & milestone management", _MGR_PERMISSIONS), + ("dev", "Developer - task execution & daily work", _DEV_PERMISSIONS), + ("guest", "Guest - read-only access", None), # special: *.read only +] + + +def _ensure_role(db: Session, name: str, description: str, is_global: bool = True) -> Role: + """Get or create a role by name.""" + role = db.query(Role).filter(Role.name == name).first() + if not role: + role = Role(name=name, description=description, is_global=is_global) + db.add(role) db.commit() - db.refresh(admin_role) - logger.info("Created admin role (id=%d)", admin_role.id) - - # Check if guest role already exists - guest_role = db.query(Role).filter(Role.name == "guest").first() - if not guest_role: - guest_role = Role( - name="guest", - description="Guest - read-only access", - is_global=True - ) - db.add(guest_role) - db.commit() - db.refresh(guest_role) - logger.info("Created guest role (id=%d)", guest_role.id) - - # Get all permissions + db.refresh(role) + logger.info("Created role '%s' (id=%d)", name, role.id) + return role + + +def _sync_role_permissions(db: Session, role: Role, target_perm_names: set[str] | None) -> None: + """Ensure *role* has exactly the permissions in *target_perm_names*. + + * ``None`` means **all** permissions (admin). + * The special sentinel ``"__read_only__"`` is handled by the caller passing + just the ``*.read`` names. + Only adds missing permissions; never removes manually-granted ones (additive). + """ all_perms = db.query(Permission).all() - - # Assign all permissions to admin role - existing_admin_perm_ids = {rp.permission_id for rp in admin_role.permissions} - for perm in all_perms: - if perm.id not in existing_admin_perm_ids: - rp = RolePermission(role_id=admin_role.id, permission_id=perm.id) - db.add(rp) - - if all_perms: + perm_by_name = {p.name: p for p in all_perms} + + if target_perm_names is None: + wanted_ids = {p.id for p in all_perms} + else: + wanted_ids = {perm_by_name[n].id for n in target_perm_names if n in perm_by_name} + + existing_ids = {rp.permission_id for rp in role.permissions} + added = 0 + for pid in wanted_ids - existing_ids: + db.add(RolePermission(role_id=role.id, permission_id=pid)) + added += 1 + + if added: db.commit() - logger.info("Assigned %d permissions to admin role", len(all_perms)) - - # Assign only read permissions to guest role - read_perms = db.query(Permission).filter(Permission.name.like("%.read")).all() - existing_guest_perm_ids = {rp.permission_id for rp in guest_role.permissions} - for perm in read_perms: - if perm.id not in existing_guest_perm_ids: - rp = RolePermission(role_id=guest_role.id, permission_id=perm.id) - db.add(rp) - - if read_perms: - db.commit() - logger.info("Assigned %d read permissions to guest role", len(read_perms)) - - logger.info("Admin and guest roles setup complete") + logger.info("Assigned %d new permissions to role '%s'", added, role.name) + + +def init_admin_role(db: Session, admin_user: models.User) -> None: + """Create default roles (admin / mgr / dev / guest) with preset permissions.""" + + all_perms = db.query(Permission).all() + read_perm_names = {p.name for p in all_perms if p.name.endswith(".read")} + + for name, description, perm_set in _DEFAULT_ROLES: + role = _ensure_role(db, name, description) + + if name == "guest": + _sync_role_permissions(db, role, read_perm_names) + else: + _sync_role_permissions(db, role, perm_set) + + logger.info("Default roles setup complete (admin, mgr, dev, guest)") def run_init(db: Session) -> None: