Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow superadmins to transfer project ownership #229

Merged
merged 3 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading