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

Disable UI starting dataset creating jobs for unprivileged users #7753

Merged
Merged
Show file tree
Hide file tree
Changes from 8 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
1 change: 1 addition & 0 deletions CHANGELOG.unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ For upgrade instructions, please check the [migration guide](MIGRATIONS.released
- Changed the time-tracking overview to show times spent in annotations and tasks and filter them by teams and projects. In the linked detail view, the tracked times can also be filtered by type (annotations or tasks) and project. [#7524](https://github.com/scalableminds/webknossos/pull/7524)
- The time tracking api route `/api/users/:id/loggedTime`, which is used by the webknossos-libs client, and groups the times by month, now uses UTC when determining month limits, rather than the server’s local timezone. [#7524](https://github.com/scalableminds/webknossos/pull/7524)
- Duplicated annotations are opened in a new browser tab. [#7724](https://github.com/scalableminds/webknossos/pull/7724)
- Non admin or manager user can no longer start long runnning jobs creating datasets. This includes annotation materialization and AI inferrals. [#7753](https://github.com/scalableminds/webknossos/pull/7753)
- When proofreading segments and merging two segments, the segment item that doesn't exist anymore after the merge is automatically removed. [#7729](https://github.com/scalableminds/webknossos/pull/7729)
- Changed some internal APIs to use spelling dataset instead of dataSet. This requires all connected datastores to be the latest version. [#7690](https://github.com/scalableminds/webknossos/pull/7690)
- Toasts are shown until WEBKNOSSOS is running in the active browser tab again. Also, the content of most toasts that show errors or warnings is printed to the browser's console. [#7741](https://github.com/scalableminds/webknossos/pull/7741)
Expand Down
30 changes: 30 additions & 0 deletions frontend/javascripts/components/permission_enforcer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import { Button, Result, Col, Row } from "antd";
import { Link } from "react-router-dom";

export function PageNotAvailableToNormalUser() {
return (
<Row justify="center" align="middle" className="full-viewport-height">
<Col>
<Result
status="error"
title="Forbidden"
icon={<i className="drawing drawing-forbidden-view" />}
subTitle={
<>
Apologies, but you don't have permission to view this page.
<br />
Please reach out to a team manager, dataset manager or administrator to assist you
with the actions you'd like to take.
</>
}
extra={
<Link to="/">
<Button type="primary">Return to Dashboard</Button>
</Link>
}
/>
</Col>
</Row>
);
}
11 changes: 10 additions & 1 deletion frontend/javascripts/components/secured_route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import {
isFeatureAllowedByPricingPlan,
PricingPlanEnum,
} from "admin/organization/pricing_plan_utils";
import { APIOrganization } from "types/api_flow_types";
import { APIOrganization, APIUser } from "types/api_flow_types";
import { PageUnavailableForYourPlanView } from "components/pricing_enforcers";
import type { ComponentType } from "react";
import { isUserAdminOrManager } from "libs/utils";
import type { RouteComponentProps } from "react-router-dom";
import type { OxalisState } from "oxalis/store";
import { PageNotAvailableToNormalUser } from "./permission_enforcer";

type StateProps = {
activeOrganization: APIOrganization | null;
activeUser: APIUser | null | undefined;
};
export type SecuredRouteProps = RouteComponentProps &
StateProps & {
Expand All @@ -22,6 +25,7 @@ export type SecuredRouteProps = RouteComponentProps &
render?: (arg0: RouteComponentProps) => React.ReactNode;
isAuthenticated: boolean;
requiredPricingPlan?: PricingPlanEnum;
requiresAdminOrManagerRole?: boolean;
serverAuthenticationCallback?: (...args: Array<any>) => any;
exact?: boolean;
};
Expand Down Expand Up @@ -62,6 +66,7 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {
const isCompletelyAuthenticated = serverAuthenticationCallback
? isAuthenticated || this.state.isAdditionallyAuthenticated
: isAuthenticated;
const isAdminOrManager = this.props.activeUser && isUserAdminOrManager(this.props.activeUser);
return (
<Route
{...rest}
Expand All @@ -83,6 +88,9 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {
/>
);
}
if (this.props.requiresAdminOrManagerRole && !isAdminOrManager) {
return <PageNotAvailableToNormalUser />;
}

if (Component != null) {
return <Component />;
Expand All @@ -98,6 +106,7 @@ class SecuredRoute extends React.PureComponent<SecuredRouteProps, State> {
}
const mapStateToProps = (state: OxalisState): StateProps => ({
activeOrganization: state.activeOrganization,
activeUser: state.activeUser,
});

const connector = connect(mapStateToProps);
Expand Down
8 changes: 7 additions & 1 deletion frontend/javascripts/libs/react_helpers.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { useState, useEffect, useRef } from "react";
import { useStore } from "react-redux";
import { useSelector, useStore } from "react-redux";
import type { OxalisState } from "oxalis/store";
import { ArbitraryFunction } from "types/globals";
import { isUserAdminOrManager } from "libs/utils";

// From https://overreacted.io/making-setinterval-declarative-with-react-hooks/
export function useInterval(
Expand Down Expand Up @@ -88,4 +89,9 @@ export function makeComponentLazy<T extends { isOpen: boolean }>(
};
}

export function useIsActiveUserAdminOrManager() {
const user = useSelector((state: OxalisState) => state.activeUser);
return user != null && isUserAdminOrManager(user);
}

export default {};
2 changes: 1 addition & 1 deletion frontend/javascripts/oxalis/model/actions/ui_actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { AnnotationTool, Vector3 } from "oxalis/constants";
import type { OxalisState, BorderOpenStatus, Theme } from "oxalis/store";
import { StartAIJobModalState } from "oxalis/view/action-bar/starting_job_modals";
import type { StartAIJobModalState } from "oxalis/view/action-bar/starting_job_modals";

type SetDropzoneModalVisibilityAction = ReturnType<typeof setDropzoneModalVisibilityAction>;
type SetVersionRestoreVisibilityAction = ReturnType<typeof setVersionRestoreVisibilityAction>;
Expand Down
4 changes: 3 additions & 1 deletion frontend/javascripts/oxalis/view/action-bar/toolbar_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ import { MenuInfo } from "rc-menu/lib/interface";
import { getViewportExtents } from "oxalis/model/accessors/view_mode_accessor";
import { ensureLayerMappingsAreLoadedAction } from "oxalis/model/actions/dataset_actions";
import { APIJobType } from "types/api_flow_types";
import { useIsActiveUserAdminOrManager } from "libs/react_helpers";

const NARROW_BUTTON_STYLE = {
paddingLeft: 10,
Expand Down Expand Up @@ -406,6 +407,7 @@ function AdditionalSkeletonModesButtons() {
(state: OxalisState) => state.userConfiguration.newNodeNewTree,
);
const dataset = useSelector((state: OxalisState) => state.dataset);
const isUserAdminOrManager = useIsActiveUserAdminOrManager();

const segmentationTracingLayer = useSelector((state: OxalisState) =>
getActiveSegmentationTracing(state),
Expand Down Expand Up @@ -464,7 +466,7 @@ function AdditionalSkeletonModesButtons() {
alt="Merger Mode"
/>
</ButtonComponent>
{isMergerModeEnabled && isMaterializeVolumeAnnotationEnabled && (
{isMergerModeEnabled && isMaterializeVolumeAnnotationEnabled && isUserAdminOrManager && (
<ButtonComponent
style={NARROW_BUTTON_STYLE}
onClick={() => setShowMaterializeVolumeAnnotationModal(true)}
Expand Down
9 changes: 7 additions & 2 deletions frontend/javascripts/oxalis/view/action_bar_view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ import { ArbitraryVectorInput } from "libs/vector_input";
import { APIJobType, type AdditionalCoordinate } from "types/api_flow_types";
import ButtonComponent from "./components/button_component";
import { setAIJobModalStateAction } from "oxalis/model/actions/ui_actions";
import { StartAIJobModalState, StartAIJobModal } from "./action-bar/starting_job_modals";
import { type StartAIJobModalState, StartAIJobModal } from "./action-bar/starting_job_modals";
import { isUserAdminOrTeamManager } from "libs/utils";

const VersionRestoreWarning = (
<Alert
Expand Down Expand Up @@ -245,7 +246,9 @@ class ActionBarView extends React.PureComponent<Props, State> {
hasSkeleton,
layoutProps,
viewMode,
activeUser,
} = this.props;
const isAdminOrDatasetManager = activeUser && isUserAdminOrTeamManager(activeUser);
const isViewMode = controlMode === ControlModeEnum.VIEW;
const isArbitrarySupported = hasSkeleton || isViewMode;
const isAIAnalysisEnabled = () => {
Expand Down Expand Up @@ -281,7 +284,9 @@ class ActionBarView extends React.PureComponent<Props, State> {
<DatasetPositionView />
<AdditionalCoordinatesInputView />
{isArbitrarySupported && !is2d ? <ViewModesView /> : null}
{isAIAnalysisEnabled() ? this.renderStartAIJobButton(!datasetHasColorLayer) : null}
{isAIAnalysisEnabled() && isAdminOrDatasetManager
? this.renderStartAIJobButton(!datasetHasColorLayer)
: null}
{!isReadOnly && constants.MODES_PLANE.indexOf(viewMode) > -1 ? <ToolbarView /> : null}
{isViewMode ? this.renderStartTracingButton() : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ type DatasetSettingsProps = {
controlMode: ControlMode;
isArbitraryMode: boolean;
isAdminOrDatasetManager: boolean;
isAdminOrManager: boolean;
isSuperUser: boolean;
};

Expand Down Expand Up @@ -558,7 +559,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
layerSettings: DatasetLayerConfiguration,
hasLessThanTwoColorLayers: boolean = true,
) => {
const { tracing, dataset } = this.props;
const { tracing, dataset, isAdminOrManager } = this.props;
const { intensityRange } = layerSettings;
const layer = getLayerByName(dataset, layerName);
const isSegmentation = layer.category === "segmentation";
Expand Down Expand Up @@ -611,7 +612,7 @@ class DatasetSettings extends React.PureComponent<DatasetSettingsProps, State> {
readableName,
);
const possibleItems: MenuProps["items"] = [
isVolumeTracing && !isDisabled && maybeFallbackLayer != null
isVolumeTracing && !isDisabled && maybeFallbackLayer != null && isAdminOrManager
? {
label: this.getMergeWithFallbackLayerButton(layer),
key: "mergeWithFallbackLayerButton",
Expand Down Expand Up @@ -1502,6 +1503,7 @@ const mapStateToProps = (state: OxalisState) => ({
isArbitraryMode: Constants.MODES_ARBITRARY.includes(state.temporaryConfiguration.viewMode),
isAdminOrDatasetManager:
state.activeUser != null ? Utils.isUserAdminOrDatasetManager(state.activeUser) : false,
isAdminOrManager: state.activeUser != null ? Utils.isUserAdminOrManager(state.activeUser) : false,
isSuperUser: state.activeUser?.isSuperUser || false,
});

Expand Down
1 change: 1 addition & 0 deletions frontend/javascripts/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ class ReactRouter extends React.Component<Props> {
<SecuredRouteWithErrorBoundary
isAuthenticated={isAuthenticated}
path="/datasets/upload"
requiresAdminOrManagerRole
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, this prop can be added to a few other components:

path="/users/:userId/details"
path="/users"
path="/teams"
path="/timetracking"
path="/reports/projectProgress"
path="/reports/availableTasks"
path="/tasks"
path="/tasks/create"
path="/tasks/:taskId/edit"
path="/tasks/:taskId"
path="/projects"
path="/projects/create"
path="/projects/:projectId/tasks"
path="/projects/:projectId/edit"
path="/datasets/:organizationName/:datasetName/import"
path="/datasets/:organizationName/:datasetName/edit"
path="/taskTypes"
path="/taskTypes/create"
path="/taskTypes/:taskTypeId/edit"
path="/taskTypes/:taskTypeId/tasks"
path="/taskTypes/:taskTypeId/projects"
path="/scripts/create"
path="/scripts/:scriptId/edit"
path="/scripts"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for listing all other components 👍

Initially, I though that this should be added in a separate PR but doing it already here is also fine to me and might also save some time.

I'll test each route as admin and non-admin later :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll test each route as admin and non-admin later :)

Done, worked out as expected 👍. All pages/view listed above are available to sample@scm.io but not to sample2@scm.io

render={() => <DatasetAddView />}
/>
<SecuredRouteWithErrorBoundary
Expand Down
6 changes: 6 additions & 0 deletions frontend/stylesheets/_drawings.less
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
background-image: url("/assets/images/drawings/empty-list-dataset-upload.svg");
}

.drawing-forbidden-view {
height: 30vh;
width: 30vh;
background-image: url("/assets/images/drawings/forbidden-view.svg");
}

.drawing-upgrade-users {
background: right -40px / 35% no-repeat url("/assets/images/pricing/add_users_light_mode.svg");
}
Expand Down
11 changes: 6 additions & 5 deletions frontend/stylesheets/dark.less
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I formatted this page with prettier, as biome currently does not support formatting css.

See: biomejs/biome#1285

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
.dark-theme {

.brain-loading-content img {
filter: invert(1) contrast(0.7);
}
Expand All @@ -21,7 +20,6 @@
}
}


// Change the bright versions to dark, too
.icon-sidebar-hide-left-bright {
background-image: url(/assets/images/icon-sidebar-hide-left-dark.svg);
Expand Down Expand Up @@ -51,9 +49,8 @@
}
}


.floating-buttons-bar {
// Navigation buttons for mobile
// Navigation buttons for mobile
.ant-btn-default {
color: rgba(0, 0, 0, 0.85);
background: white;
Expand Down Expand Up @@ -91,6 +88,10 @@
background-image: url("/assets/images/drawings/empty-list-dataset-upload-dark.svg");
}

.drawing-forbidden-view {
background-image: url("/assets/images/drawings/forbidden-view-dark.svg");
}

.icon-mouse-left {
background-image: url("/assets/images/icon-mouse-left-dark.svg");
}
Expand All @@ -106,4 +107,4 @@
.dataset-table-thumbnail.icon-thumbnail {
filter: invert() contrast(0.8);
}
}
}
Loading