Skip to content

Commit 464ccb7

Browse files
authored
feat: add more check when delete project or user from project (#35)
- added self remove from project - added more information in project now show members and project owner - added more check to avoid removing user with shared quota used - added more check to avoid deleting project with instance or volume in it
2 parents 7d4e10a + 00db56d commit 464ccb7

3 files changed

Lines changed: 122 additions & 32 deletions

File tree

fob_api/models/api/openstack.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ class OpenStackProjectCreate(BaseModel):
66

77
class OpenStackProject(OpenStackProjectCreate):
88
id: int
9-
type: str
9+
name: str
10+
owner: str
11+
members: List[str]
1012

1113
class OpenStackUserPassword(BaseModel):
1214
username: str

fob_api/routes/openstack.py

Lines changed: 107 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
from fastapi import APIRouter, Depends, HTTPException
44
from sqlmodel import Session, select
5+
from novaclient import exceptions as nova_exceptions
6+
from cinderclient import exceptions as cinder_exceptions
57

68
from fob_api import auth, openstack, random_end_uid, OPENSTACK_DOMAIN_ID, OPENSTACK_ROLE_MEMBER_ID, random_password, get_session
7-
from fob_api.models.database import User, Project, ProjectUserMembership
8-
from fob_api.models.api import OpenStackProject as OpenStackProjectAPI
9-
from fob_api.models.api import OpenStackUserPassword as OpenStackUserPasswordAPI
9+
from fob_api.models.database import User, Project, ProjectUserMembership # deprecated import for models
10+
from fob_api.models import database as db_models
11+
from fob_api.models import api as api_models
12+
from fob_api.models.api import OpenStackProject as OpenStackProjectAPI # deprecated import for models
13+
from fob_api.models.api import OpenStackUserPassword as OpenStackUserPasswordAPI # deprecated import for models
1014
from fob_api.tasks.openstack import get_or_create_user as openstack_get_or_create_user
1115
from fob_api.tasks.openstack import set_user_password as openstack_set_user_password
12-
16+
from fob_api.tasks import openstack as openstack_tasks
1317

1418
router = APIRouter(prefix="/openstack")
1519

20+
MAX_PROJECTS_USER_OWN = 3
21+
1622
@router.get("/projects/{username}", tags=["openstack"])
1723
def list_openstack_project_for_user(
1824
username: str,
@@ -25,18 +31,33 @@ def list_openstack_project_for_user(
2531
auth.is_admin_or_self(user, username)
2632
user_find = session.exec(select(User).where(User.username == username)).first()
2733
projects_owner = session.exec(select(Project).where(Project.owner_id == user_find.id)).all()
34+
# get all owner projects
35+
data = []
36+
for project in projects_owner:
37+
db_members = session.exec(select(ProjectUserMembership).where(ProjectUserMembership.project_id == project.id)).all()
38+
data.append(OpenStackProjectAPI(
39+
id=project.id,
40+
name=project.name,
41+
owner=username,
42+
members=[
43+
session.exec(select(User).where(User.id == member.user_id)).first().username
44+
for member in db_members
45+
]
46+
))
47+
# get all member projects and add to data
2848
project_memberships = session.exec(select(ProjectUserMembership).where(ProjectUserMembership.user_id == user_find.id)).all()
29-
data = [OpenStackProjectAPI(
30-
id=project.id,
31-
name=project.name,
32-
type="owner"
33-
) for project in projects_owner]
3449
for project_membership in project_memberships:
3550
local_project = session.exec(select(Project).where(Project.id == project_membership.project_id)).first()
51+
db_members = session.exec(select(ProjectUserMembership).where(ProjectUserMembership.project_id == local_project.id)).all()
52+
owner = session.exec(select(User).where(User.id == local_project.owner_id)).first()
3653
data.append(OpenStackProjectAPI(
3754
id=local_project.id,
3855
name=local_project.name,
39-
type="member"
56+
owner=owner.username,
57+
members=[
58+
session.exec(select(User).where(User.id == member.user_id)).first().username
59+
for member in db_members
60+
]
4061
))
4162
return data
4263

@@ -45,15 +66,15 @@ def create_openstack_project(
4566
project_name: str,
4667
user: Annotated[User, Depends(auth.get_current_user)],
4768
session: Session = Depends(get_session)
48-
) -> OpenStackProjectAPI | None:
69+
) -> api_models.OpenStackProject | None:
4970
"""
5071
Create a new OpenStack project
5172
"""
5273
openstack_client = openstack.get_keystone_client()
5374

5475
projects = session.exec(select(Project).where(Project.owner_id == user.id)).all()
55-
if len(projects) >= 3:
56-
raise HTTPException(status_code=400, detail="You cannot create more than 3 projects")
76+
if len(projects) >= MAX_PROJECTS_USER_OWN:
77+
raise HTTPException(status_code=400, detail=f"You cannot create more than {str(MAX_PROJECTS_USER_OWN)} projects")
5778
project_name = project_name + "-" + random_end_uid()
5879
new_project = Project(name=project_name, owner_id=user.id)
5980
session.add(new_project)
@@ -85,6 +106,20 @@ def delete_openstack_project(
85106
raise HTTPException(status_code=404, detail="Project not found")
86107
if project.owner_id != user.id and not user.is_admin:
87108
raise HTTPException(status_code=403, detail="Not allowed to delete this project")
109+
110+
# check if quota is given to project
111+
# here we are checking if project has any quotas assigned to it so nothing is left behind not assigned to any project
112+
project_quotas = session.exec(select(db_models.UserQuotaShare).where(db_models.UserQuotaShare.project_id == project.id)).all()
113+
for project_quota in project_quotas:
114+
if project_quota.quantity > 0:
115+
print(project_quota.quantity)
116+
raise HTTPException(status_code=400, detail="Cannot delete project with quotas assigned to it please remove all quotas assigned to project")
117+
118+
# check if project has members
119+
project_memberships = session.exec(select(db_models.ProjectUserMembership).where(db_models.ProjectUserMembership.project_id == project.id)).all()
120+
if project_memberships:
121+
raise HTTPException(status_code=400, detail="Cannot delete project with members please remove all members from project")
122+
88123
openstack_project = openstack_client.projects.find(name=project_name)
89124
openstack_client.projects.delete(openstack_project.id)
90125
session.delete(project)
@@ -115,19 +150,21 @@ def add_user_to_project(
115150
session: Session = Depends(get_session)
116151
) -> None:
117152
"""
118-
Add user to project if user is owner of the project
153+
Add user to project
119154
"""
120155
db_project = session.exec(select(Project).where(Project.name == project_name)).first()
156+
# check if owner of the project or is admin
121157
if db_project.owner_id != user.id and not user.is_admin:
122158
raise HTTPException(status_code=403, detail="Not allowed to add user to this project")
123-
159+
160+
# reject if user is owner of the project
161+
if user.username == username and db_project.owner_id == user.id:
162+
raise HTTPException(status_code=400, detail="Cannot add owner to project (owner is already in project)")
163+
164+
# get user to add
124165
user_to_add = session.exec(select(User).where(User.username == username)).first()
125166
if not user_to_add or not db_project:
126-
raise HTTPException(status_code=404, detail="User or project not found")
127-
128-
# if user is owner of the project
129-
if user_to_add.id == db_project.owner_id:
130-
raise HTTPException(status_code=400, detail="User is owner of the project do not need to be added")
167+
raise HTTPException(status_code=404, detail="User to add not found")
131168

132169
# check if user is already in project
133170
assignment = session.exec(select(ProjectUserMembership).where(ProjectUserMembership.project_id == db_project.id, ProjectUserMembership.user_id == user_to_add.id)).first()
@@ -153,31 +190,73 @@ def remove_user_from_project(
153190
session: Session = Depends(get_session)
154191
) -> None:
155192
"""
156-
Remove user from project if user is owner of the project
193+
Remove user from project
157194
"""
195+
# check if owner of the project or is admin
158196
db_project = session.exec(select(Project).where(Project.name == project_name)).first()
159-
if db_project.owner_id != user.id and not user.is_admin:
197+
db_project_members_name = [
198+
session.exec(select(User).where(User.id == member.user_id)).first().username
199+
for member in session.exec(select(ProjectUserMembership).where(ProjectUserMembership.project_id == db_project.id)).all()
200+
]
201+
202+
# action allowed if anyone of the following is true
203+
# 1. user is admin
204+
# 2. user is owner of the project and wants to remove other user
205+
# 3. user is not owner of the project and wants to remove himself from project
206+
207+
if db_project.owner_id != user.id and not user.is_admin and user.username not in db_project_members_name:
160208
raise HTTPException(status_code=403, detail="Not allowed to remove user from this project")
161209

210+
if user.username in db_project_members_name:
211+
# this avoids the case where user is not owner of the project and wants to remove other user
212+
username = user.username
213+
214+
# reject if user is owner of the project
215+
if user.username == username and db_project.owner_id == user.id:
216+
raise HTTPException(status_code=400, detail="Cannot remove yourself from project (delete project instead)")
217+
218+
# get user to remove
162219
user_to_remove = session.exec(select(User).where(User.username == username)).first()
163220
if not user_to_remove or not db_project:
164-
raise HTTPException(status_code=404, detail="User or project not found")
165-
166-
# check if user is owner of the project
167-
if db_project.owner_id == user_to_remove.id:
168-
raise HTTPException(status_code=400, detail="Cannot remove owner from project")
221+
raise HTTPException(status_code=404, detail="User not found")
169222

170223
# check if user is already in project
171224
assignment = session.exec(select(ProjectUserMembership).where(ProjectUserMembership.project_id == db_project.id, ProjectUserMembership.user_id == user_to_remove.id)).first()
172225
if not assignment:
173226
raise HTTPException(status_code=400, detail="User not in project")
174227

175-
session.delete(assignment)
228+
# get project from openstack
176229
openstack_client = openstack.get_keystone_client()
177230
try:
178231
os_project = openstack_client.projects.find(name=project_name)
179232
except Exception:
180233
raise HTTPException(status_code=500, detail="OpenStack error cant get project")
234+
235+
os_project = openstack.get_keystone_client().projects.find(name=project_name)
236+
# check if user has any quotas assigned to project
237+
project_quotas = session.exec(select(db_models.UserQuotaShare).where(db_models.UserQuotaShare.project_id == db_project.id, db_models.UserQuotaShare.user_id == user_to_remove.id)).all()
238+
# try to set all quotas to 0
239+
old_quotas_map = {k: 0 for k in db_models.QuotaType}
240+
for project_quota in project_quotas:
241+
old_quotas_map[project_quota.type] = project_quota.quantity
242+
project_quota.quantity = 0
243+
session.add(project_quota)
244+
session.commit()
245+
246+
# try to sync quotas with openstack without user quotas if it fails rollback quotas
247+
try:
248+
openstack_tasks.sync_project_quota(os_project)
249+
except (nova_exceptions.ClientException, cinder_exceptions.ClientException) as e:
250+
# rollback quotas when quota sync fails (when quota is lower than current usage)
251+
for project_quota in project_quotas:
252+
session.refresh(project_quota)
253+
project_quota.quantity = old_quotas_map[project_quota.type]
254+
session.add(project_quota)
255+
session.commit()
256+
raise HTTPException(status_code=400, detail="Cannot remove user from project, user share used quotas with project")
257+
258+
# remove user from project
181259
os_user = openstack_get_or_create_user(username)
182260
openstack_client.roles.revoke(role=OPENSTACK_ROLE_MEMBER_ID, user=os_user.id, project=os_project.id)
261+
session.delete(assignment)
183262
session.commit()

fob_api/tasks/openstack.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
from sqlmodel import Session, select
22
from sqlalchemy.exc import IntegrityError
33
from fob_api import engine, openstack, random_end_uid, random_password, OPENSTACK_DOMAIN_ID
4-
from fob_api.models.database import User
4+
from fob_api.models.database import User # deprecated call use db_models as prefix
5+
from fob_api.models import database as db_models
56

6-
def get_or_create_user(username: str) -> None: # todo return openstack user object
7+
def get_or_create_user(username: str): # todo return openstack user object
78
"""
89
Create OpenStack User if not exists
910
1011
Args:
1112
username (str): User name
1213
1314
Returns:
14-
None
15+
openstack user object
1516
"""
1617
with Session(engine) as session:
1718
user = session.exec(select(User).where(User.username == username)).first()
@@ -29,3 +30,11 @@ def set_user_password(username: str, password: str) -> None:
2930
openstack_client = openstack.get_keystone_client()
3031
user = get_or_create_user(username)
3132
openstack_client.users.update(user=user, password=password)
33+
34+
def sync_project_quota(openstack_project: db_models.Project) -> None:
35+
"""
36+
Temp warp function to the correct function
37+
"""
38+
# temporary for how long? who knows write 01-03-2025 19:19
39+
from fob_api.routes.quota import sync_project_quota as sync_project_quota_real
40+
sync_project_quota_real(openstack_project)

0 commit comments

Comments
 (0)