Skip to content

Commit

Permalink
feat: Allow superadmins to transfer project ownership (#229)
Browse files Browse the repository at this point in the history
* Allow superadmins to change project ownership permission

---------

Co-authored-by: mgonnav <[email protected]>
  • Loading branch information
reqhiem and mgonnav authored Oct 23, 2024
1 parent 44ad862 commit 5da817d
Show file tree
Hide file tree
Showing 8 changed files with 66 additions and 24 deletions.
3 changes: 2 additions & 1 deletion estela-api/api/serializers/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ class UserProfileSerializer(serializers.HyperlinkedModelSerializer):

class Meta:
model = User
fields = ["username", "email", "password"]
fields = ["username", "email", "password", "is_superuser"]
read_only_fields = ["is_superuser"]

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
Expand Down
1 change: 1 addition & 0 deletions estela-api/api/serializers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class ProjectUpdateSerializer(serializers.ModelSerializer):
("ADMIN", "Admin"),
("DEVELOPER", "Developer"),
("VIEWER", "Viewer"),
("OWNER", "Owner")
]
pid = serializers.UUIDField(
read_only=True, help_text="A UUID identifying this project."
Expand Down
13 changes: 13 additions & 0 deletions estela-api/api/views/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,19 @@ def update(self, request, *args, **kwargs):
instance.users.remove(affected_user)
description = f"removed user {user_email}."
elif action == "update":
if permission == Permission.OWNER_PERMISSION:
if not is_superuser:
raise PermissionDenied(
{"permission": "You do not have permission to do this."}
)
old_owner = instance.users.filter(
permission__permission=Permission.OWNER_PERMISSION
).get()
instance.users.remove(old_owner)
instance.users.add(
old_owner,
through_defaults={"permission": Permission.ADMIN_PERMISSION},
)
instance.users.remove(affected_user)
instance.users.add(
affected_user, through_defaults={"permission": permission}
Expand Down
7 changes: 7 additions & 0 deletions estela-api/docs/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1709,6 +1709,12 @@ definitions:
title: Password
type: string
minLength: 1
is_superuser:
title: Superuser status
description: Designates that this user has all permissions without explicitly
assigning them.
type: boolean
readOnly: true
User:
required:
- username
Expand Down Expand Up @@ -1966,6 +1972,7 @@ definitions:
- ADMIN
- DEVELOPER
- VIEWER
- OWNER
data_status:
title: Data status
description: New data status.
Expand Down
54 changes: 33 additions & 21 deletions estela-web/src/pages/ProjectMemberPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface ProjectMemberPageState {
newUser: string;
members: MemberState[];
permission: ProjectUpdatePermissionEnum;
changeRolePermissions: ProjectUpdatePermissionEnum[];
}

interface RouteParams {
Expand All @@ -58,6 +59,11 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
newUser: "",
permission: ProjectUpdatePermissionEnum.Viewer,
members: [],
changeRolePermissions: [
ProjectUpdatePermissionEnum.Admin,
ProjectUpdatePermissionEnum.Developer,
ProjectUpdatePermissionEnum.Viewer,
],
};
apiService = ApiService();
projectId: string = this.props.match.params.projectId;
Expand Down Expand Up @@ -90,6 +96,23 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
},
];

verifyIsSuperuser = (): void => {
const requestParams = { username: AuthService.getUserUsername() || "" };
this.apiService.apiAuthProfileRead(requestParams).then(
(response) => {
this.setState({
changeRolePermissions:
response.isSuperuser === true
? [...this.state.changeRolePermissions, ProjectUpdatePermissionEnum.Owner]
: [...this.state.changeRolePermissions],
});
},
(error: unknown) => {
error;
},
);
};

updateInfo = (): void => {
const requestParams: ApiProjectsReadRequest = { pid: this.projectId };
this.apiService.apiProjectsRead(requestParams).then(
Expand Down Expand Up @@ -117,6 +140,7 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
};

async componentDidMount(): Promise<void> {
this.verifyIsSuperuser();
this.updateInfo();
}

Expand Down Expand Up @@ -324,27 +348,15 @@ export class ProjectMemberPage extends Component<RouteComponentProps<RouteParams
defaultValue={ProjectUpdatePermissionEnum.Viewer}
onChange={this.handleSelectChange}
>
<Option
key={1}
className="hover:bg-button-hover hover:text-estela"
value={ProjectUpdatePermissionEnum.Admin}
>
Admin
</Option>
<Option
key={2}
className="hover:bg-button-hover hover:text-estela"
value={ProjectUpdatePermissionEnum.Developer}
>
Developer
</Option>
<Option
key={3}
className="hover:bg-button-hover hover:text-estela"
value={ProjectUpdatePermissionEnum.Viewer}
>
Viewer
</Option>
{this.state.changeRolePermissions.map((permission, index) => (
<Option
key={index}
className="hover:bg-button-hover hover:text-estela"
value={permission}
>
{permission[0] + permission.slice(1).toLowerCase()}
</Option>
))}
</Select>
<Row className="mt-6 w-full grid grid-cols-2" justify="center">
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ export enum ProjectUpdateFrameworkEnum {
export enum ProjectUpdatePermissionEnum {
Admin = 'ADMIN',
Developer = 'DEVELOPER',
Viewer = 'VIEWER'
Viewer = 'VIEWER',
Owner = 'OWNER'
}/**
* @export
* @enum {string}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export interface UserProfile {
* @memberof UserProfile
*/
password: string;
/**
* Designates that this user has all permissions without explicitly assigning them.
* @type {boolean}
* @memberof UserProfile
*/
readonly isSuperuser?: boolean;
}

export function UserProfileFromJSON(json: any): UserProfile {
Expand All @@ -52,6 +58,7 @@ export function UserProfileFromJSONTyped(json: any, ignoreDiscriminator: boolean
'username': json['username'],
'email': json['email'],
'password': json['password'],
'isSuperuser': !exists(json, 'is_superuser') ? undefined : json['is_superuser'],
};
}

Expand Down
2 changes: 1 addition & 1 deletion queueing/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Build Stage
FROM python:3.9-slim as builder
FROM python:3.9-slim AS builder

ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
Expand Down

0 comments on commit 5da817d

Please sign in to comment.