From 6bd855b3c52952fe5b69c14df34b12f5bee5f397 Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 21 Aug 2023 21:32:23 +1000 Subject: [PATCH] New database schema for better RBAC --- gns3server/db/models/__init__.py | 2 ++ gns3server/db/models/acl.py | 41 ++++++++++++++++++++++++++ gns3server/db/models/images.py | 6 ++-- gns3server/db/models/permissions.py | 9 +++--- gns3server/db/models/resources.py | 45 +++++++++++++++++++++++++++++ gns3server/db/models/roles.py | 13 +++++---- gns3server/db/models/templates.py | 4 +-- gns3server/db/models/users.py | 15 ++++++---- 8 files changed, 114 insertions(+), 21 deletions(-) create mode 100644 gns3server/db/models/acl.py create mode 100644 gns3server/db/models/resources.py diff --git a/gns3server/db/models/__init__.py b/gns3server/db/models/__init__.py index d10d0668..a643d38c 100644 --- a/gns3server/db/models/__init__.py +++ b/gns3server/db/models/__init__.py @@ -16,6 +16,8 @@ # along with this program. If not, see . from .base import Base +from .acl import ACL +from .resources import Resource from .users import User, UserGroup from .roles import Role from .permissions import Permission diff --git a/gns3server/db/models/acl.py b/gns3server/db/models/acl.py new file mode 100644 index 00000000..23ed6f8f --- /dev/null +++ b/gns3server/db/models/acl.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# +# Copyright (C) 2023 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from sqlalchemy import Column, Boolean, ForeignKey +from sqlalchemy.orm import relationship + +from .base import BaseTable, generate_uuid, GUID + +import logging + +log = logging.getLogger(__name__) + + +class ACL(BaseTable): + + __tablename__ = "acl" + + acl_id = Column(GUID, primary_key=True, default=generate_uuid) + allowed = Column(Boolean, default=True) + user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) + user = relationship("User", back_populates="acl_entries") + group_id = Column(GUID, ForeignKey('user_groups.user_group_id', ondelete="CASCADE")) + group = relationship("UserGroup", back_populates="acl_entries") + resource_id = Column(GUID, ForeignKey('resources.resource_id', ondelete="CASCADE")) + resource = relationship("Resource", back_populates="acl_entries") + role_id = Column(GUID, ForeignKey('roles.role_id', ondelete="CASCADE")) + role = relationship("Role", back_populates="acl_entries") diff --git a/gns3server/db/models/images.py b/gns3server/db/models/images.py index 175ab278..529afe4b 100644 --- a/gns3server/db/models/images.py +++ b/gns3server/db/models/images.py @@ -21,8 +21,8 @@ from sqlalchemy.orm import relationship from .base import Base, BaseTable, GUID -image_template_link = Table( - "images_templates_link", +image_template_map = Table( + "image_template_map", Base.metadata, Column("image_id", Integer, ForeignKey("images.image_id", ondelete="CASCADE")), Column("template_id", GUID, ForeignKey("templates.template_id", ondelete="CASCADE")) @@ -40,4 +40,4 @@ class Image(BaseTable): image_size = Column(BigInteger) checksum = Column(String, index=True) checksum_algorithm = Column(String) - templates = relationship("Template", secondary=image_template_link, back_populates="images") + templates = relationship("Template", secondary=image_template_map, back_populates="images") diff --git a/gns3server/db/models/permissions.py b/gns3server/db/models/permissions.py index f7344e31..04029c7e 100644 --- a/gns3server/db/models/permissions.py +++ b/gns3server/db/models/permissions.py @@ -25,8 +25,8 @@ import logging log = logging.getLogger(__name__) -permission_role_link = Table( - "permissions_roles_link", +permission_role_map = Table( + "permission_role_map", Base.metadata, Column("permission_id", GUID, ForeignKey("permissions.permission_id", ondelete="CASCADE")), Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")) @@ -44,7 +44,8 @@ class Permission(BaseTable): path = Column(String) action = Column(String) user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) - roles = relationship("Role", secondary=permission_role_link, back_populates="permissions") + user = relationship("User", back_populates="permissions") + roles = relationship("Role", secondary=permission_role_map, back_populates="permissions") @event.listens_for(Permission.__table__, 'after_create') @@ -95,7 +96,7 @@ def create_default_roles(target, connection, **kw): log.debug("The default permissions have been created in the database") -@event.listens_for(permission_role_link, 'after_create') +@event.listens_for(permission_role_map, 'after_create') def add_permissions_to_role(target, connection, **kw): from .roles import Role diff --git a/gns3server/db/models/resources.py b/gns3server/db/models/resources.py new file mode 100644 index 00000000..6cc11ff3 --- /dev/null +++ b/gns3server/db/models/resources.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python +# +# Copyright (C) 2023 GNS3 Technologies Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from sqlalchemy import Column, String, Boolean, ForeignKey +from sqlalchemy.orm import relationship + +from .base import BaseTable, generate_uuid, GUID + +import logging + +log = logging.getLogger(__name__) + + +class Resource(BaseTable): + + __tablename__ = "resources" + + resource_id = Column(GUID, primary_key=True, default=generate_uuid) + name = Column(String, unique=True, index=True) + description = Column(String) + propagate = Column(Boolean, default=True) + user_id = Column(GUID, ForeignKey('users.user_id', ondelete="CASCADE")) + user = relationship("User", back_populates="resources") + acl_entries = relationship("ACL") + parent_id = Column(GUID, ForeignKey("resources.resource_id", ondelete="CASCADE")) + children = relationship( + "Resource", + remote_side=[resource_id], + cascade="all, delete-orphan", + single_parent=True + ) diff --git a/gns3server/db/models/roles.py b/gns3server/db/models/roles.py index b531a50f..fb4ef4f8 100644 --- a/gns3server/db/models/roles.py +++ b/gns3server/db/models/roles.py @@ -19,14 +19,14 @@ from sqlalchemy import Table, Column, String, Boolean, ForeignKey, event from sqlalchemy.orm import relationship from .base import Base, BaseTable, generate_uuid, GUID -from .permissions import permission_role_link +from .permissions import permission_role_map import logging log = logging.getLogger(__name__) -role_group_link = Table( - "roles_groups_link", +role_group_map = Table( + "role_group_map", Base.metadata, Column("role_id", GUID, ForeignKey("roles.role_id", ondelete="CASCADE")), Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")) @@ -41,8 +41,9 @@ class Role(BaseTable): name = Column(String, unique=True, index=True) description = Column(String) is_builtin = Column(Boolean, default=False) - permissions = relationship("Permission", secondary=permission_role_link, back_populates="roles") - groups = relationship("UserGroup", secondary=role_group_link, back_populates="roles") + permissions = relationship("Permission", secondary=permission_role_map, back_populates="roles") + groups = relationship("UserGroup", secondary=role_group_map, back_populates="roles") + acl_entries = relationship("ACL") @event.listens_for(Role.__table__, 'after_create') @@ -59,7 +60,7 @@ def create_default_roles(target, connection, **kw): log.debug("The default roles have been created in the database") -@event.listens_for(role_group_link, 'after_create') +@event.listens_for(role_group_map, 'after_create') def add_admin_to_group(target, connection, **kw): from .users import UserGroup diff --git a/gns3server/db/models/templates.py b/gns3server/db/models/templates.py index 210ce1c5..a35ed076 100644 --- a/gns3server/db/models/templates.py +++ b/gns3server/db/models/templates.py @@ -20,7 +20,7 @@ from sqlalchemy import Boolean, Column, String, Integer, ForeignKey, PickleType from sqlalchemy.orm import relationship from .base import BaseTable, generate_uuid, GUID -from .images import image_template_link +from .images import image_template_map class Template(BaseTable): @@ -37,7 +37,7 @@ class Template(BaseTable): usage = Column(String) template_type = Column(String) compute_id = Column(String) - images = relationship("Image", secondary=image_template_link, back_populates="templates") + images = relationship("Image", secondary=image_template_map, back_populates="templates") __mapper_args__ = { "polymorphic_identity": "templates", diff --git a/gns3server/db/models/users.py b/gns3server/db/models/users.py index 0c85ccbe..cc923aec 100644 --- a/gns3server/db/models/users.py +++ b/gns3server/db/models/users.py @@ -19,7 +19,7 @@ from sqlalchemy import Table, Boolean, Column, String, DateTime, ForeignKey, eve from sqlalchemy.orm import relationship from .base import Base, BaseTable, generate_uuid, GUID -from .roles import role_group_link +from .roles import role_group_map from gns3server.config import Config from gns3server.services import auth_service @@ -28,8 +28,8 @@ import logging log = logging.getLogger(__name__) -user_group_link = Table( - "users_groups_link", +user_group_map = Table( + "user_group_map", Base.metadata, Column("user_id", GUID, ForeignKey("users.user_id", ondelete="CASCADE")), Column("user_group_id", GUID, ForeignKey("user_groups.user_group_id", ondelete="CASCADE")) @@ -48,8 +48,10 @@ class User(BaseTable): last_login = Column(DateTime) is_active = Column(Boolean, default=True) is_superadmin = Column(Boolean, default=False) - groups = relationship("UserGroup", secondary=user_group_link, back_populates="users") + groups = relationship("UserGroup", secondary=user_group_map, back_populates="users") + resources = relationship("Resource") permissions = relationship("Permission") + acl_entries = relationship("ACL") @event.listens_for(User.__table__, 'after_create') @@ -77,8 +79,9 @@ class UserGroup(BaseTable): user_group_id = Column(GUID, primary_key=True, default=generate_uuid) name = Column(String, unique=True, index=True) is_builtin = Column(Boolean, default=False) - users = relationship("User", secondary=user_group_link, back_populates="groups") - roles = relationship("Role", secondary=role_group_link, back_populates="groups") + users = relationship("User", secondary=user_group_map, back_populates="groups") + roles = relationship("Role", secondary=role_group_map, back_populates="groups") + acl_entries = relationship("ACL") @event.listens_for(UserGroup.__table__, 'after_create')