Skip to content

Commit b0c4fc1

Browse files
authored
Merge pull request #2275 from GNS3/rbac-new-implementation
New RBAC implementation
2 parents 74cb3be + e72b07b commit b0c4fc1

40 files changed

+2210
-1535
lines changed

gns3server/api/routes/controller/__init__.py

+5-15
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from . import users
3333
from . import groups
3434
from . import roles
35-
from . import permissions
35+
from . import acl
3636

3737
from .dependencies.authentication import get_current_active_user
3838

@@ -43,35 +43,30 @@
4343

4444
router.include_router(
4545
groups.router,
46-
dependencies=[Depends(get_current_active_user)],
4746
prefix="/groups",
4847
tags=["Users groups"]
4948
)
5049

5150
router.include_router(
5251
roles.router,
53-
dependencies=[Depends(get_current_active_user)],
5452
prefix="/roles",
5553
tags=["Roles"]
5654
)
5755

5856
router.include_router(
59-
permissions.router,
60-
dependencies=[Depends(get_current_active_user)],
61-
prefix="/permissions",
62-
tags=["Permissions"]
57+
acl.router,
58+
prefix="/acl",
59+
tags=["ACL"]
6360
)
6461

6562
router.include_router(
6663
images.router,
67-
dependencies=[Depends(get_current_active_user)],
6864
prefix="/images",
6965
tags=["Images"]
7066
)
7167

7268
router.include_router(
7369
templates.router,
74-
dependencies=[Depends(get_current_active_user)],
7570
prefix="/templates",
7671
tags=["Templates"]
7772
)
@@ -83,21 +78,18 @@
8378

8479
router.include_router(
8580
nodes.router,
86-
dependencies=[Depends(get_current_active_user)],
8781
prefix="/projects/{project_id}/nodes",
8882
tags=["Nodes"]
8983
)
9084

9185
router.include_router(
9286
links.router,
93-
dependencies=[Depends(get_current_active_user)],
9487
prefix="/projects/{project_id}/links",
9588
tags=["Links"]
9689
)
9790

9891
router.include_router(
9992
drawings.router,
100-
dependencies=[Depends(get_current_active_user)],
10193
prefix="/projects/{project_id}/drawings",
10294
tags=["Drawings"])
10395

@@ -108,7 +100,6 @@
108100

109101
router.include_router(
110102
snapshots.router,
111-
dependencies=[Depends(get_current_active_user)],
112103
prefix="/projects/{project_id}/snapshots",
113104
tags=["Snapshots"])
114105

@@ -126,15 +117,14 @@
126117

127118
router.include_router(
128119
appliances.router,
129-
dependencies=[Depends(get_current_active_user)],
130120
prefix="/appliances",
131121
tags=["Appliances"]
132122
)
133123

134124
router.include_router(
135125
gns3vm.router,
136-
deprecated=True,
137126
dependencies=[Depends(get_current_active_user)],
127+
deprecated=True,
138128
prefix="/gns3vm",
139129
tags=["GNS3 VM"]
140130
)
+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
#!/usr/bin/env python
2+
#
3+
# Copyright (C) 2023 GNS3 Technologies Inc.
4+
#
5+
# This program is free software: you can redistribute it and/or modify
6+
# it under the terms of the GNU General Public License as published by
7+
# the Free Software Foundation, either version 3 of the License, or
8+
# (at your option) any later version.
9+
#
10+
# This program is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
# GNU General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU General Public License
16+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
18+
"""
19+
API routes for ACL.
20+
"""
21+
22+
import re
23+
24+
from fastapi import APIRouter, Depends, Request, status
25+
from fastapi.routing import APIRoute
26+
from uuid import UUID
27+
from typing import List
28+
29+
30+
from gns3server import schemas
31+
from gns3server.controller.controller_error import (
32+
ControllerBadRequestError,
33+
ControllerNotFoundError
34+
)
35+
36+
from gns3server.controller import Controller
37+
from gns3server.db.repositories.users import UsersRepository
38+
from gns3server.db.repositories.rbac import RbacRepository
39+
from gns3server.db.repositories.images import ImagesRepository
40+
from gns3server.db.repositories.templates import TemplatesRepository
41+
from .dependencies.database import get_repository
42+
from .dependencies.rbac import has_privilege
43+
44+
import logging
45+
46+
log = logging.getLogger(__name__)
47+
48+
router = APIRouter()
49+
50+
51+
@router.get(
52+
"/endpoints",
53+
status_code=status.HTTP_201_CREATED,
54+
dependencies=[Depends(has_privilege("ACE.Audit"))]
55+
)
56+
async def endpoints(
57+
users_repo: UsersRepository = Depends(get_repository(UsersRepository)),
58+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
59+
images_repo: ImagesRepository = Depends(get_repository(ImagesRepository)),
60+
templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository))
61+
) -> List[dict]:
62+
"""
63+
List all endpoints to be used in ACL entries.
64+
"""
65+
66+
controller = Controller.instance()
67+
endpoints = [{"endpoint": "/", "name": "All endpoints", "endpoint_type": "root"}]
68+
69+
def add_to_endpoints(endpoint: str, name: str, endpoint_type: str) -> None:
70+
if endpoint not in endpoints:
71+
endpoints.append({"endpoint": endpoint, "name": name, "endpoint_type": endpoint_type})
72+
73+
# projects
74+
add_to_endpoints("/projects", "All projects", "project")
75+
projects = [p for p in controller.projects.values()]
76+
for project in projects:
77+
add_to_endpoints(f"/projects/{project.id}", f'Project "{project.name}"', "project")
78+
79+
# nodes
80+
add_to_endpoints(f"/projects/{project.id}/nodes", f'All nodes in project "{project.name}"', "node")
81+
for node in project.nodes.values():
82+
add_to_endpoints(
83+
f"/projects/{project.id}/nodes/{node['node_id']}",
84+
f'Node "{node["name"]}" in project "{project.name}"',
85+
endpoint_type="node"
86+
)
87+
88+
# links
89+
add_to_endpoints(f"/projects/{project.id}/links", f'All links in project "{project.name}"', "link")
90+
for link in project.links.values():
91+
node_id_1 = link["nodes"][0]["node_id"]
92+
node_id_2 = link["nodes"][1]["node_id"]
93+
node_name_1 = project.nodes[node_id_1]["name"]
94+
node_name_2 = project.nodes[node_id_2]["name"]
95+
add_to_endpoints(
96+
f"/projects/{project.id}/links/{link['link_id']}",
97+
f'Link from "{node_name_1}" to "{node_name_2}" in project "{project.name}"',
98+
endpoint_type="link"
99+
)
100+
101+
# users
102+
add_to_endpoints("/users", "All users", "user")
103+
users = await users_repo.get_users()
104+
for user in users:
105+
add_to_endpoints(f"/users/{user.user_id}", f'User "{user.username}"', "user")
106+
107+
# groups
108+
add_to_endpoints("/groups", "All groups", "group")
109+
groups = await users_repo.get_user_groups()
110+
for group in groups:
111+
add_to_endpoints(f"/groups/{group.user_group_id}", f'Group "{group.name}"', "group")
112+
113+
# roles
114+
add_to_endpoints("/roles", "All roles", "role")
115+
roles = await rbac_repo.get_roles()
116+
for role in roles:
117+
add_to_endpoints(f"/roles/{role.role_id}", f'Role "{role.name}"', "role")
118+
119+
# images
120+
add_to_endpoints("/images", "All images", "image")
121+
images = await images_repo.get_images()
122+
for image in images:
123+
add_to_endpoints(f"/images/{image.filename}", f'Image "{image.filename}"', "image")
124+
125+
# templates
126+
add_to_endpoints("/templates", "All templates", "template")
127+
templates = await templates_repo.get_templates()
128+
for template in templates:
129+
add_to_endpoints(f"/templates/{template.template_id}", f'Template "{template.name}"', "template")
130+
131+
return endpoints
132+
133+
134+
@router.get(
135+
"",
136+
response_model=List[schemas.ACE],
137+
dependencies=[Depends(has_privilege("ACE.Audit"))]
138+
)
139+
async def get_aces(
140+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
141+
) -> List[schemas.ACE]:
142+
"""
143+
Get all ACL entries.
144+
145+
Required privilege: ACE.Audit
146+
"""
147+
148+
return await rbac_repo.get_aces()
149+
150+
151+
@router.post(
152+
"",
153+
response_model=schemas.ACE,
154+
status_code=status.HTTP_201_CREATED,
155+
dependencies=[Depends(has_privilege("ACE.Allocate"))]
156+
)
157+
async def create_ace(
158+
request: Request,
159+
ace_create: schemas.ACECreate,
160+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
161+
) -> schemas.ACE:
162+
"""
163+
Create a new ACL entry.
164+
165+
Required privilege: ACE.Allocate
166+
"""
167+
168+
for route in request.app.routes:
169+
if isinstance(route, APIRoute):
170+
171+
# remove the prefix (e.g. "/v3") from the route path
172+
route_path = re.sub(r"^/v[0-9]", "", route.path)
173+
# replace route path ID parameters by a UUID regex
174+
route_path = re.sub(r"{\w+_id}", "[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}", route_path)
175+
# replace remaining route path parameters by a word matching regex
176+
route_path = re.sub(r"/{[\w:]+}", r"/\\w+", route_path)
177+
178+
if re.fullmatch(route_path, ace_create.path):
179+
log.info("Creating ACE for route path", ace_create.path, route_path)
180+
return await rbac_repo.create_ace(ace_create)
181+
182+
raise ControllerBadRequestError(f"Path '{ace_create.path}' doesn't match any existing endpoint")
183+
184+
185+
@router.get(
186+
"/{ace_id}",
187+
response_model=schemas.ACE,
188+
dependencies=[Depends(has_privilege("ACE.Audit"))]
189+
)
190+
async def get_ace(
191+
ace_id: UUID,
192+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
193+
) -> schemas.ACE:
194+
"""
195+
Get an ACL entry.
196+
197+
Required privilege: ACE.Audit
198+
"""
199+
200+
ace = await rbac_repo.get_ace(ace_id)
201+
if not ace:
202+
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
203+
return ace
204+
205+
206+
@router.put(
207+
"/{ace_id}",
208+
response_model=schemas.ACE,
209+
dependencies=[Depends(has_privilege("ACE.Modify"))]
210+
)
211+
async def update_ace(
212+
ace_id: UUID,
213+
ace_update: schemas.ACEUpdate,
214+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository))
215+
) -> schemas.ACE:
216+
"""
217+
Update an ACL entry.
218+
219+
Required privilege: ACE.Modify
220+
"""
221+
222+
ace = await rbac_repo.get_ace(ace_id)
223+
if not ace:
224+
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
225+
226+
return await rbac_repo.update_ace(ace_id, ace_update)
227+
228+
229+
@router.delete(
230+
"/{ace_id}",
231+
status_code=status.HTTP_204_NO_CONTENT,
232+
dependencies=[Depends(has_privilege("ACE.Allocate"))]
233+
)
234+
async def delete_ace(
235+
ace_id: UUID,
236+
rbac_repo: RbacRepository = Depends(get_repository(RbacRepository)),
237+
) -> None:
238+
"""
239+
Delete an ACL entry.
240+
241+
Required privilege: ACE.Allocate
242+
"""
243+
244+
ace = await rbac_repo.get_ace(ace_id)
245+
if not ace:
246+
raise ControllerNotFoundError(f"ACL entry '{ace_id}' not found")
247+
248+
success = await rbac_repo.delete_ace(ace_id)
249+
if not success:
250+
raise ControllerNotFoundError(f"ACL entry '{ace_id}' could not be deleted")

0 commit comments

Comments
 (0)