Skip to content

Commit 5da817d

Browse files
reqhiemmgonnav
andauthored
feat: Allow superadmins to transfer project ownership (#229)
* Allow superadmins to change project ownership permission --------- Co-authored-by: mgonnav <mateo@emegona.com>
1 parent 44ad862 commit 5da817d

8 files changed

Lines changed: 66 additions & 24 deletions

File tree

estela-api/api/serializers/auth.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,8 @@ class UserProfileSerializer(serializers.HyperlinkedModelSerializer):
7373

7474
class Meta:
7575
model = User
76-
fields = ["username", "email", "password"]
76+
fields = ["username", "email", "password", "is_superuser"]
77+
read_only_fields = ["is_superuser"]
7778

7879
def __init__(self, *args, **kwargs):
7980
super().__init__(*args, **kwargs)

estela-api/api/serializers/project.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ class ProjectUpdateSerializer(serializers.ModelSerializer):
121121
("ADMIN", "Admin"),
122122
("DEVELOPER", "Developer"),
123123
("VIEWER", "Viewer"),
124+
("OWNER", "Owner")
124125
]
125126
pid = serializers.UUIDField(
126127
read_only=True, help_text="A UUID identifying this project."

estela-api/api/views/project.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,19 @@ def update(self, request, *args, **kwargs):
151151
instance.users.remove(affected_user)
152152
description = f"removed user {user_email}."
153153
elif action == "update":
154+
if permission == Permission.OWNER_PERMISSION:
155+
if not is_superuser:
156+
raise PermissionDenied(
157+
{"permission": "You do not have permission to do this."}
158+
)
159+
old_owner = instance.users.filter(
160+
permission__permission=Permission.OWNER_PERMISSION
161+
).get()
162+
instance.users.remove(old_owner)
163+
instance.users.add(
164+
old_owner,
165+
through_defaults={"permission": Permission.ADMIN_PERMISSION},
166+
)
154167
instance.users.remove(affected_user)
155168
instance.users.add(
156169
affected_user, through_defaults={"permission": permission}

estela-api/docs/api.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1709,6 +1709,12 @@ definitions:
17091709
title: Password
17101710
type: string
17111711
minLength: 1
1712+
is_superuser:
1713+
title: Superuser status
1714+
description: Designates that this user has all permissions without explicitly
1715+
assigning them.
1716+
type: boolean
1717+
readOnly: true
17121718
User:
17131719
required:
17141720
- username
@@ -1966,6 +1972,7 @@ definitions:
19661972
- ADMIN
19671973
- DEVELOPER
19681974
- VIEWER
1975+
- OWNER
19691976
data_status:
19701977
title: Data status
19711978
description: New data status.

estela-web/src/pages/ProjectMemberPage/index.tsx

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ interface ProjectMemberPageState {
4040
newUser: string;
4141
members: MemberState[];
4242
permission: ProjectUpdatePermissionEnum;
43+
changeRolePermissions: ProjectUpdatePermissionEnum[];
4344
}
4445

4546
interface RouteParams {
@@ -58,6 +59,11 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
5859
newUser: "",
5960
permission: ProjectUpdatePermissionEnum.Viewer,
6061
members: [],
62+
changeRolePermissions: [
63+
ProjectUpdatePermissionEnum.Admin,
64+
ProjectUpdatePermissionEnum.Developer,
65+
ProjectUpdatePermissionEnum.Viewer,
66+
],
6167
};
6268
apiService = ApiService();
6369
projectId: string = this.props.match.params.projectId;
@@ -90,6 +96,23 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
9096
},
9197
];
9298

99+
verifyIsSuperuser = (): void => {
100+
const requestParams = { username: AuthService.getUserUsername() || "" };
101+
this.apiService.apiAuthProfileRead(requestParams).then(
102+
(response) => {
103+
this.setState({
104+
changeRolePermissions:
105+
response.isSuperuser === true
106+
? [...this.state.changeRolePermissions, ProjectUpdatePermissionEnum.Owner]
107+
: [...this.state.changeRolePermissions],
108+
});
109+
},
110+
(error: unknown) => {
111+
error;
112+
},
113+
);
114+
};
115+
93116
updateInfo = (): void => {
94117
const requestParams: ApiProjectsReadRequest = { pid: this.projectId };
95118
this.apiService.apiProjectsRead(requestParams).then(
@@ -117,6 +140,7 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
117140
};
118141

119142
async componentDidMount(): Promise<void> {
143+
this.verifyIsSuperuser();
120144
this.updateInfo();
121145
}
122146

@@ -324,27 +348,15 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
324348
defaultValue={ProjectUpdatePermissionEnum.Viewer}
325349
onChange={this.handleSelectChange}
326350
>
327-
<Option
328-
key={1}
329-
className="hover:bg-button-hover hover:text-estela"
330-
value={ProjectUpdatePermissionEnum.Admin}
331-
>
332-
Admin
333-
</Option>
334-
<Option
335-
key={2}
336-
className="hover:bg-button-hover hover:text-estela"
337-
value={ProjectUpdatePermissionEnum.Developer}
338-
>
339-
Developer
340-
</Option>
341-
<Option
342-
key={3}
343-
className="hover:bg-button-hover hover:text-estela"
344-
value={ProjectUpdatePermissionEnum.Viewer}
345-
>
346-
Viewer
347-
</Option>
351+
{this.state.changeRolePermissions.map((permission, index) => (
352+
<Option
353+
key={index}
354+
className="hover:bg-button-hover hover:text-estela"
355+
value={permission}
356+
>
357+
{permission[0] + permission.slice(1).toLowerCase()}
358+
</Option>
359+
))}
348360
</Select>
349361
<Row className="mt-6 w-full grid grid-cols-2" justify="center">
350362
<Button

estela-web/src/services/api/generated-api/models/ProjectUpdate.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ export enum ProjectUpdateFrameworkEnum {
114114
export enum ProjectUpdatePermissionEnum {
115115
Admin = 'ADMIN',
116116
Developer = 'DEVELOPER',
117-
Viewer = 'VIEWER'
117+
Viewer = 'VIEWER',
118+
Owner = 'OWNER'
118119
}/**
119120
* @export
120121
* @enum {string}

estela-web/src/services/api/generated-api/models/UserProfile.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ export interface UserProfile {
3737
* @memberof UserProfile
3838
*/
3939
password: string;
40+
/**
41+
* Designates that this user has all permissions without explicitly assigning them.
42+
* @type {boolean}
43+
* @memberof UserProfile
44+
*/
45+
readonly isSuperuser?: boolean;
4046
}
4147

4248
export function UserProfileFromJSON(json: any): UserProfile {
@@ -52,6 +58,7 @@ export function UserProfileFromJSONTyped(json: any, ignoreDiscriminator: boolean
5258
'username': json['username'],
5359
'email': json['email'],
5460
'password': json['password'],
61+
'isSuperuser': !exists(json, 'is_superuser') ? undefined : json['is_superuser'],
5562
};
5663
}
5764

queueing/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build Stage
2-
FROM python:3.9-slim as builder
2+
FROM python:3.9-slim AS builder
33

44
ENV PYTHONDONTWRITEBYTECODE=1 \
55
PYTHONUNBUFFERED=1

0 commit comments

Comments
 (0)