mirror of
https://github.com/wassname/talk.git
synced 2026-07-02 09:55:59 +08:00
[CORL-318] Overhaul Permissions Checks on the Client (#2246)
* feat: overhaul admin permission checks * feat: overhaul stream permission checks
This commit is contained in:
@@ -4,15 +4,16 @@ import { PropTypesOf } from "talk-framework/types";
|
||||
import { Logo } from "talk-ui/components";
|
||||
import { AppBar, Begin, Divider, End } from "talk-ui/components/AppBar";
|
||||
|
||||
import NavigationContainer from "../containers/NavigationContainer";
|
||||
import UserMenuContainer from "../containers/UserMenuContainer";
|
||||
import DecisionHistoryButton from "./DecisionHistoryButton";
|
||||
import Navigation from "./Navigation";
|
||||
import Version from "./Version";
|
||||
|
||||
import styles from "./App.css";
|
||||
|
||||
interface Props {
|
||||
viewer: PropTypesOf<typeof UserMenuContainer>["viewer"];
|
||||
viewer: PropTypesOf<typeof UserMenuContainer>["viewer"] &
|
||||
PropTypesOf<typeof NavigationContainer>["viewer"];
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -24,7 +25,7 @@ const App: StatelessComponent<Props> = ({ children, viewer }) => (
|
||||
<Logo />
|
||||
<Version />
|
||||
</div>
|
||||
<Navigation />
|
||||
<NavigationContainer viewer={viewer} />
|
||||
</Begin>
|
||||
<End>
|
||||
<DecisionHistoryButton />
|
||||
|
||||
@@ -5,6 +5,6 @@ import Navigation from "./Navigation";
|
||||
|
||||
it("renders correctly", () => {
|
||||
const renderer = createRenderer();
|
||||
renderer.render(<Navigation />);
|
||||
renderer.render(<Navigation showConfigure />);
|
||||
expect(renderer.getRenderOutput()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
@@ -5,7 +5,11 @@ import { AppBarNavigation } from "talk-ui/components";
|
||||
|
||||
import NavigationLink from "./NavigationLink";
|
||||
|
||||
const Navigation: StatelessComponent = () => (
|
||||
interface Props {
|
||||
showConfigure: boolean;
|
||||
}
|
||||
|
||||
const Navigation: StatelessComponent<Props> = props => (
|
||||
<AppBarNavigation>
|
||||
<Localized id="navigation-moderate">
|
||||
<NavigationLink to="/admin/moderate">Moderate</NavigationLink>
|
||||
@@ -16,9 +20,11 @@ const Navigation: StatelessComponent = () => (
|
||||
<Localized id="navigation-community">
|
||||
<NavigationLink to="/admin/community">Community</NavigationLink>
|
||||
</Localized>
|
||||
<Localized id="navigation-configure">
|
||||
<NavigationLink to="/admin/configure">Configure</NavigationLink>
|
||||
</Localized>
|
||||
{props.showConfigure && (
|
||||
<Localized id="navigation-configure">
|
||||
<NavigationLink to="/admin/configure">Configure</NavigationLink>
|
||||
</Localized>
|
||||
)}
|
||||
</AppBarNavigation>
|
||||
);
|
||||
|
||||
|
||||
@@ -17,7 +17,9 @@ exports[`renders correctly 1`] = `
|
||||
<withPropsOnChange(Logo) />
|
||||
<Version />
|
||||
</div>
|
||||
<Navigation />
|
||||
<withContext(createMutationContainer(Relay(NavigationContainer)))
|
||||
viewer={Object {}}
|
||||
/>
|
||||
</withPropsOnChange(Begin)>
|
||||
<withPropsOnChange(End)>
|
||||
<DecisionHistoryButton />
|
||||
|
||||
@@ -26,6 +26,7 @@ const enhanced = withRouteConfig({
|
||||
query AppContainerQuery {
|
||||
viewer {
|
||||
...UserMenuContainer_viewer
|
||||
...NavigationContainer_viewer
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -6,15 +6,25 @@ import {
|
||||
SetRedirectPathMutation,
|
||||
withSetRedirectPathMutation,
|
||||
} from "talk-admin/mutations";
|
||||
import { AbilityType, can } from "talk-admin/permissions";
|
||||
import RestrictedContainer from "talk-admin/views/restricted/containers/RestrictedContainer";
|
||||
import { graphql } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
interface Props {
|
||||
match: Match;
|
||||
router: Router;
|
||||
setRedirectPath: SetRedirectPathMutation;
|
||||
data: AuthCheckContainerQueryResponse | null;
|
||||
data:
|
||||
| AuthCheckContainerQueryResponse & {
|
||||
route: {
|
||||
// An AbilityType can be passed in as the Route data
|
||||
// to perform a permission check.
|
||||
data?: AbilityType;
|
||||
};
|
||||
}
|
||||
| null;
|
||||
}
|
||||
|
||||
class AuthCheckContainer extends React.Component<Props> {
|
||||
@@ -47,7 +57,14 @@ class AuthCheckContainer extends React.Component<Props> {
|
||||
settings: { auth },
|
||||
} = props.data!;
|
||||
if (viewer) {
|
||||
if (viewer.role === "COMMENTER") {
|
||||
if (
|
||||
viewer.role === GQLUSER_ROLE.COMMENTER ||
|
||||
viewer.role === GQLUSER_ROLE.STAFF ||
|
||||
(props.data &&
|
||||
props.data.route.data &&
|
||||
// Perform permission check on the ability passed in by the route data
|
||||
!can(viewer, props.data.route.data))
|
||||
) {
|
||||
return false;
|
||||
} else if (
|
||||
!viewer.email ||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
|
||||
import { NavigationContainer_viewer as ViewerData } from "talk-admin/__generated__/NavigationContainer_viewer.graphql";
|
||||
import { Ability, can } from "talk-admin/permissions";
|
||||
import { graphql, withFragmentContainer } from "talk-framework/lib/relay";
|
||||
import { SignOutMutation, withSignOutMutation } from "talk-framework/mutations";
|
||||
|
||||
import Navigation from "../components/Navigation";
|
||||
|
||||
interface Props {
|
||||
signOut: SignOutMutation;
|
||||
viewer: ViewerData | null;
|
||||
}
|
||||
|
||||
class NavigationContainer extends React.Component<Props> {
|
||||
public render() {
|
||||
return (
|
||||
<Navigation
|
||||
showConfigure={
|
||||
!!this.props.viewer &&
|
||||
can(this.props.viewer, Ability.CHANGE_CONFIGURATION)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const enhanced = withSignOutMutation(
|
||||
withFragmentContainer<Props>({
|
||||
viewer: graphql`
|
||||
fragment NavigationContainer_viewer on User {
|
||||
role
|
||||
}
|
||||
`,
|
||||
})(NavigationContainer)
|
||||
);
|
||||
|
||||
export default enhanced;
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from "talk-admin/mutations";
|
||||
import { graphql } from "talk-framework/lib/relay";
|
||||
import { withRouteConfig } from "talk-framework/lib/router";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
interface Props {
|
||||
match: Match;
|
||||
@@ -35,7 +36,7 @@ class RedirectLoginContainer extends React.Component<Props> {
|
||||
settings: { auth },
|
||||
} = props.data!;
|
||||
if (viewer) {
|
||||
if (viewer.role === "COMMENTER") {
|
||||
if (viewer.role === GQLUSER_ROLE.COMMENTER) {
|
||||
return "/admin/login";
|
||||
} else if (
|
||||
!viewer.email ||
|
||||
|
||||
@@ -13,6 +13,8 @@ import { GQLUSER_ROLE, GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
* the single point of truth.
|
||||
*/
|
||||
const permissionMap = {
|
||||
// Mutation.updateSettings
|
||||
CHANGE_CONFIGURATION: [GQLUSER_ROLE.ADMIN],
|
||||
// Mutation.updateUserRole
|
||||
CHANGE_ROLE: [GQLUSER_ROLE.ADMIN],
|
||||
// Mutation.openStory
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
|
||||
import AppContainer from "./containers/AppContainer";
|
||||
import AuthCheckContainer from "./containers/AuthCheckContainer";
|
||||
import { Ability } from "./permissions";
|
||||
import CommunityContainer from "./routes/community/containers/CommunityContainer";
|
||||
import ConfigureContainer from "./routes/configure/containers/ConfigureContainer";
|
||||
import ConfigureAdvancedRouteContainer from "./routes/configure/sections/advanced/containers/AdvancedConfigRouteContainer";
|
||||
@@ -45,29 +46,34 @@ export default makeRouteConfig(
|
||||
<Route path="stories" {...StoriesContainer.routeConfig} />
|
||||
<Route path="community" {...CommunityContainer.routeConfig} />
|
||||
<Route path="stories" Component={Stories} />
|
||||
<Route path="configure" Component={ConfigureContainer}>
|
||||
<Redirect from="/" to="/admin/configure/general" />
|
||||
<Route
|
||||
path="general"
|
||||
{...ConfigureGeneralRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="organization"
|
||||
{...ConfigureOrganizationRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="moderation"
|
||||
{...ConfigureModerationRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="wordList"
|
||||
{...ConfigureWordListRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route path="auth" {...ConfigureAuthRouteContainer.routeConfig} />
|
||||
<Route
|
||||
path="advanced"
|
||||
{...ConfigureAdvancedRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
{...AuthCheckContainer.routeConfig}
|
||||
data={Ability.CHANGE_CONFIGURATION}
|
||||
>
|
||||
<Route path="configure" Component={ConfigureContainer}>
|
||||
<Redirect from="/" to="/admin/configure/general" />
|
||||
<Route
|
||||
path="general"
|
||||
{...ConfigureGeneralRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="organization"
|
||||
{...ConfigureOrganizationRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="moderation"
|
||||
{...ConfigureModerationRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route
|
||||
path="wordList"
|
||||
{...ConfigureWordListRouteContainer.routeConfig}
|
||||
/>
|
||||
<Route path="auth" {...ConfigureAuthRouteContainer.routeConfig} />
|
||||
<Route
|
||||
path="advanced"
|
||||
{...ConfigureAdvancedRouteContainer.routeConfig}
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -1,6 +1,139 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`show restricted screen for commenters 1`] = `
|
||||
exports[`show restricted screen for commenters and staff 1`] = `
|
||||
<div
|
||||
data-testid="authBox"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="HorizontalGutter-root AuthBox-container HorizontalGutter-double"
|
||||
>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<div
|
||||
className="AuthBox-brandIcon"
|
||||
>
|
||||
<svg
|
||||
className="BrandIcon-base BrandIcon-lg"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1"
|
||||
viewBox="0 0 541.77 557.72"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<defs>
|
||||
<clipPath
|
||||
id="clip-path"
|
||||
>
|
||||
<rect
|
||||
fill="none"
|
||||
height="570"
|
||||
width="554"
|
||||
/>
|
||||
</clipPath>
|
||||
<clipPath
|
||||
id="clip-path-2"
|
||||
transform="translate(-8.12 -8.14)"
|
||||
>
|
||||
<path
|
||||
clipPath="url(#clip-path)"
|
||||
clipRule="evenodd"
|
||||
d="M61.63,350.45c-.67,10.22,1.34,21.13,6,32.21a95.36,95.36,0,0,0,27.22,36.41c26,21.16,52.14,17.65,77.43,14.24,21.22-2.85,43.17-5.8,69,5.43,33.54,14.57,59.71,45.39,66.65,78.46a99.16,99.16,0,0,1,.11,38.66H219.65a16.89,16.89,0,0,0-.71-2.44c-16.54-44.09-38.46-69.52-67.09-77.78-9.37-2.66-18-3-25.68-3.27-9.91-.37-17.75-.66-26-5.6s-14.93-13.28-19.59-24.42a17,17,0,0,0-22.12-9.07,16.69,16.69,0,0,0-9.17,21.88c7.45,17.8,18.6,31.31,33.25,40.21,15.85,9.48,30.54,10,42.35,10.48,6.55.24,12.2.45,17.55,2,15.64,4.51,29.43,20.65,41.07,48H63.07a45,45,0,0,1-44.95-45V322.78a144.51,144.51,0,0,0,43.51,27.67Zm13-31.55a109.22,109.22,0,0,1-56.48-52.22v-66a88.41,88.41,0,0,0,35.59,10.37c18.52,1.13,34.77-4.64,47-16.71,11.63-11.51,14.6-24.24,18.05-39,2.88-12.32,6.46-27.66,16.66-48.7,10.44-21.72,17.13-30,24.69-30.51,11-.79,29.3,13.46,32.09,34.75,2,14.93-3.58,25.14-10.07,37C175,161,166,177.28,172.4,198.15c6.81,22.16,25.44,31.45,40.4,38.91,15.61,7.78,25.42,13.21,30.37,26.2,4.26,11.27,4.87,27.9-.71,33.32-3.74,3.66-16.43.64-29.86-2.55-23-5.47-54.5-12.95-91.49-1.57-9.77,3-32.07,9.72-46.51,26.44ZM357.73,18.14c-15,17.37-28.76,40.23-34.85,69-4.25,19.33-13.09,59.57,13.51,88.26A71.68,71.68,0,0,0,388.11,198a61.12,61.12,0,0,0,22.65-4.23c31.63-12.61,46.84-43.4,49.25-56.36,3.24-17.51,5-35.76,6.33-51.32a11.43,11.43,0,0,1,5.38-8.61l.5-.31a12.23,12.23,0,0,1,13.33,1.63c19.82,16.56,36.73,31.82,51.69,46.63a17.07,17.07,0,0,0,2.64,2.14v93.21a16.92,16.92,0,0,0-3.87,4.69c-9.23,16.43-23.71,36.42-40.11,38.31-6.55.7-10.3-1.49-20.21-7.9S452.37,240.8,431,236c-23.2-5.22-38.56-2.06-50.9.48-10.61,2.18-17.62,3.62-28.9,0-27.43-9-42.31-36.22-51.21-52.47l-.24-.45c-19.81-36.54-18.23-71.85-16.84-103l0-.58a287.16,287.16,0,0,1,9.81-61.77Zm49.14,0h88.06a45,45,0,0,1,44.95,45V80.79c-10.08-9.22-21-18.69-32.79-28.58a45.72,45.72,0,0,0-50.93-5.38,19.1,19.1,0,0,0-2.55,1.52,46.1,46.1,0,0,0-21,34.46,1.49,1.49,0,0,0,0,.21c-1.24,14.7-2.91,31.88-5.86,47.9-.88,3.66-9.12,23.11-28.33,30.76-12.81,5.1-29.11-1.09-37.41-10-14.22-15.33-8.26-42.48-5-57.19C363.43,59.29,386.3,35,404.17,20.82a18,18,0,0,0,2.7-2.68Zm-149.42,0a319.88,319.88,0,0,0-8.67,60.21l0,.62c-1.43,32-3.38,75.91,21,120.82l.28.52c9.81,17.93,30.25,55.25,70.55,68.52,19.95,6.48,34,3.59,46.44,1,11-2.26,20.48-4.21,36.49-.61,15.43,3.48,24.69,9.46,33.63,15.25C467,290.87,478,298,493.8,298a54,54,0,0,0,5.86-.32c14.5-1.67,27.83-8.41,40.22-20.42v45.53a179.81,179.81,0,0,1-47.68.48c-17-2.18-28.9-6.21-40.39-10.1-16.13-5.46-31.37-10.63-54.46-8.79-12.34.86-49.5,3.52-68.67,32.46-21.27,32-4.79,71.47-1.27,79.05,0,.06,0,.11.07.17,11.15,23.37,28,33.16,44.25,42.63,9.57,5.57,19.46,11.33,29.45,20.4C421,497.23,435.05,523,443,555.86H342.61a132,132,0,0,0-1.29-45.63c-9.16-43.62-43.1-84-86.46-102.82-34.51-15-63.68-11.11-87.12-8-24.18,3.25-37.44,4.42-51.35-6.91-15-12.14-24-32.83-19.67-45.1,4.74-13.34,25.44-19.62,34.35-22.32,28.19-8.67,52.33-2.93,73.64,2.14,21.83,5.18,44.4,10.55,61.57-6.21,17.7-17.22,17-48.29,8.81-69.92-10-26.15-30.53-36.41-47.06-44.65-13.09-6.53-20.6-10.6-23-18.37-2-6.43.36-11.62,7-23.75,7.41-13.48,17.55-31.93,14-58-4.77-36.43-36.65-66.5-68.28-64.3-30.19,2.1-44.53,32-53.08,49.73-11.83,24.42-16.07,42.54-19.16,55.78-2.9,12.39-4.37,18.06-8.79,22.43-5.21,5.13-12.26,7.47-21,6.93-13-.8-27.57-8-37.68-18.45V63.15a45,45,0,0,1,45-45Zm282.43,449v43.76a45,45,0,0,1-44.95,45H477.46a16.09,16.09,0,0,0-.36-2.56c-4.89-22.41-12.24-42.37-22-59.73a120.79,120.79,0,0,1,39-21,119.51,119.51,0,0,1,45.77-5.45Zm0-34.82a152.6,152.6,0,0,0-56,7.12,154.85,154.85,0,0,0-48.66,25.94,153.26,153.26,0,0,0-11.29-11.5c-12.68-11.52-24.67-18.5-35.24-24.65-14.45-8.41-24-14-30.62-27.78-.87-1.9-12.66-28.42-1.23-45.62,10-15.1,33.94-16.77,42.95-17.4l.17,0c16.24-1.3,26.66,2.23,41.07,7.11,12.45,4.22,26.57,9,46.95,11.61a212.66,212.66,0,0,0,51.92.08v75.11Z"
|
||||
fill="none"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
<title>
|
||||
logo mark2018
|
||||
</title>
|
||||
<g
|
||||
clipPath="url(#clip-path-2)"
|
||||
>
|
||||
<rect
|
||||
fill="#f7705f"
|
||||
height="557.72"
|
||||
width="541.77"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
<span>
|
||||
Currently signed in to
|
||||
</span>
|
||||
</h1>
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter BrandName-root BrandName-lg"
|
||||
>
|
||||
Talk
|
||||
</h1>
|
||||
</div>
|
||||
<div
|
||||
className="HorizontalGutter-root HorizontalGutter-double"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="Icon-root Icon-lg Restricted-lockIcon"
|
||||
>
|
||||
lock
|
||||
</span>
|
||||
</div>
|
||||
<h1
|
||||
className="Typography-root Typography-heading3 Typography-colorTextPrimary Typography-alignCenter Restricted-noPermission"
|
||||
>
|
||||
You do not have permission to access this page.
|
||||
</h1>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
You are signed in as:
|
||||
<h1
|
||||
className="Typography-root Typography-heading1 Typography-colorTextPrimary Typography-alignCenter"
|
||||
>
|
||||
Markus
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="Flex-root Flex-flex Flex-justifyCenter"
|
||||
>
|
||||
<button
|
||||
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled"
|
||||
onBlur={[Function]}
|
||||
onClick={[Function]}
|
||||
onFocus={[Function]}
|
||||
onMouseOut={[Function]}
|
||||
onMouseOver={[Function]}
|
||||
onTouchEnd={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Sign in with a different account
|
||||
</button>
|
||||
</div>
|
||||
<p
|
||||
className="Typography-root Typography-bodyCopy Typography-colorTextPrimary Typography-alignCenter Restricted-contactAdmin"
|
||||
>
|
||||
If you think this is an error, please contact your administrator for assistance.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`show restricted screen for commenters and staff 2`] = `
|
||||
<div
|
||||
data-testid="authBox"
|
||||
>
|
||||
|
||||
@@ -3,6 +3,7 @@ import sinon from "sinon";
|
||||
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import {
|
||||
createAccessToken,
|
||||
replaceHistoryLocation,
|
||||
@@ -47,12 +48,15 @@ function createTestRenderer(
|
||||
return { testRenderer, context };
|
||||
}
|
||||
|
||||
it("show restricted screen for commenters", async () => {
|
||||
const { testRenderer } = createTestRenderer({ role: "COMMENTER" });
|
||||
const authBox = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("authBox")
|
||||
);
|
||||
expect(within(authBox).toJSON()).toMatchSnapshot();
|
||||
it("show restricted screen for commenters and staff", async () => {
|
||||
const restrictedRoles = [GQLUSER_ROLE.COMMENTER, GQLUSER_ROLE.STAFF];
|
||||
for (const role of restrictedRoles) {
|
||||
const { testRenderer } = createTestRenderer({ role });
|
||||
const authBox = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("authBox")
|
||||
);
|
||||
expect(within(authBox).toJSON()).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("show restricted screen when email is not set", async () => {
|
||||
@@ -71,7 +75,9 @@ it("show restricted screen local was not set (password)", async () => {
|
||||
});
|
||||
|
||||
it("sign out when clicking on sign in as", async () => {
|
||||
const { context, testRenderer } = createTestRenderer({ role: "COMMENTER" });
|
||||
const { context, testRenderer } = createTestRenderer({
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
});
|
||||
const authBox = await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("authBox")
|
||||
);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { get, merge } from "lodash";
|
||||
import sinon from "sinon";
|
||||
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import {
|
||||
replaceHistoryLocation,
|
||||
waitForElement,
|
||||
within,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import create from "../create";
|
||||
import { settings, users } from "../fixtures";
|
||||
|
||||
beforeEach(() => {
|
||||
replaceHistoryLocation("http://localhost/admin/configure/general");
|
||||
});
|
||||
|
||||
const createTestRenderer = async (
|
||||
resolver: any = {},
|
||||
options: { muteNetworkErrors?: boolean } = {}
|
||||
) => {
|
||||
const resolvers = {
|
||||
...resolver,
|
||||
Query: {
|
||||
...resolver.Query,
|
||||
settings: sinon
|
||||
.stub()
|
||||
.returns(merge({}, settings, get(resolver, "Query.settings"))),
|
||||
},
|
||||
};
|
||||
const { testRenderer } = create({
|
||||
// Set this to true, to see graphql responses.
|
||||
logNetwork: false,
|
||||
muteNetworkErrors: options.muteNetworkErrors,
|
||||
resolvers,
|
||||
initLocalState: localRecord => {
|
||||
localRecord.setValue(true, "loggedIn");
|
||||
},
|
||||
});
|
||||
return {
|
||||
testRenderer,
|
||||
};
|
||||
};
|
||||
|
||||
it("denies access to moderators", async () => {
|
||||
const deniedRoles = [GQLUSER_ROLE.MODERATOR];
|
||||
for (const r of deniedRoles) {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns({ ...users[0], role: r }),
|
||||
},
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByText("Sign in with a different account")
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("allows access to admins", async () => {
|
||||
const deniedRoles = [GQLUSER_ROLE.ADMIN];
|
||||
for (const r of deniedRoles) {
|
||||
const { testRenderer } = await createTestRenderer({
|
||||
Query: {
|
||||
viewer: sinon.stub().returns({ ...users[0], role: r }),
|
||||
},
|
||||
});
|
||||
await waitForElement(() =>
|
||||
within(testRenderer.root).getByTestID("configure-container")
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { merge } from "lodash";
|
||||
|
||||
import { GQLStory, GQLSTORY_STATUS } from "talk-framework/schema";
|
||||
import { GQLStory, GQLSTORY_STATUS, GQLUSER_ROLE } from "talk-framework/schema";
|
||||
|
||||
export const settings = {
|
||||
id: "settings",
|
||||
@@ -259,21 +259,21 @@ export const users = [
|
||||
id: "user-0",
|
||||
username: "Markus",
|
||||
email: "markus@test.com",
|
||||
role: "ADMIN",
|
||||
role: GQLUSER_ROLE.ADMIN,
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-1",
|
||||
username: "Lukas",
|
||||
email: "lukas@test.com",
|
||||
role: "MODERATOR",
|
||||
role: GQLUSER_ROLE.MODERATOR,
|
||||
},
|
||||
{
|
||||
...baseUser,
|
||||
id: "user-2",
|
||||
username: "Isabelle",
|
||||
email: "isabelle@test.com",
|
||||
role: "COMMENTER",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
import { TabBarContainer_story as StoryData } from "talk-stream/__generated__/TabBarContainer_story.graphql";
|
||||
import { TabBarContainer_viewer as ViewerData } from "talk-stream/__generated__/TabBarContainer_viewer.graphql";
|
||||
import { TabBarContainerLocal as Local } from "talk-stream/__generated__/TabBarContainerLocal.graphql";
|
||||
import { roleIsAtLeast } from "talk-stream/helpers";
|
||||
import {
|
||||
SetActiveTabInput,
|
||||
SetActiveTabMutation,
|
||||
withSetActiveTabMutation,
|
||||
} from "talk-stream/mutations";
|
||||
|
||||
import { Ability, can } from "talk-stream/permissions";
|
||||
import TabBar from "../components/TabBar";
|
||||
|
||||
interface Props {
|
||||
@@ -45,7 +45,7 @@ export class TabBarContainer extends Component<Props> {
|
||||
showProfileTab={loggedIn}
|
||||
showConfigureTab={
|
||||
!!this.props.viewer &&
|
||||
roleIsAtLeast(this.props.viewer.role, "MODERATOR")
|
||||
can(this.props.viewer, Ability.CHANGE_STORY_CONFIGURATION)
|
||||
}
|
||||
onTabClick={this.handleSetActiveTab}
|
||||
/>
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
// TODO: use generated schema types.
|
||||
type Role =
|
||||
| "ADMIN"
|
||||
| "MODERATOR"
|
||||
| "STAFF"
|
||||
| "COMMENTER"
|
||||
| "%future added value";
|
||||
import { GQLUSER_ROLE, GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
|
||||
const hierarchy: Role[] = ["COMMENTER", "STAFF", "MODERATOR", "ADMIN"];
|
||||
export default function roleIsAtLeast(role: Role, atLeast: Role) {
|
||||
const hierarchy: GQLUSER_ROLE_RL[] = [
|
||||
GQLUSER_ROLE.COMMENTER,
|
||||
GQLUSER_ROLE.STAFF,
|
||||
GQLUSER_ROLE.MODERATOR,
|
||||
GQLUSER_ROLE.ADMIN,
|
||||
];
|
||||
export default function roleIsAtLeast(
|
||||
role: GQLUSER_ROLE_RL,
|
||||
atLeast: GQLUSER_ROLE_RL
|
||||
) {
|
||||
[role, atLeast].forEach(r => {
|
||||
if (!hierarchy.includes(r)) {
|
||||
throw new Error(`Unknown role ${r}`);
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import { CreateCommentMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentMutation.graphql";
|
||||
|
||||
import {
|
||||
@@ -117,7 +118,8 @@ function commit(
|
||||
|
||||
// TODO: Generate and use schema types.
|
||||
const expectPremoderation =
|
||||
!roleIsAtLeast(me.role, "STAFF") && storySettings.moderation === "PRE";
|
||||
!roleIsAtLeast(me.role, GQLUSER_ROLE.STAFF) &&
|
||||
storySettings.moderation === "PRE";
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MutationInput,
|
||||
MutationResponsePromise,
|
||||
} from "talk-framework/lib/relay";
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import { CreateCommentReplyMutation as MutationTypes } from "talk-stream/__generated__/CreateCommentReplyMutation.graphql";
|
||||
|
||||
import {
|
||||
@@ -144,7 +145,8 @@ function commit(
|
||||
|
||||
// TODO: Generate and use schema types.
|
||||
const expectPremoderation =
|
||||
!roleIsAtLeast(viewer.role, "STAFF") && storySettings.moderation === "PRE";
|
||||
!roleIsAtLeast(viewer.role, GQLUSER_ROLE.STAFF) &&
|
||||
storySettings.moderation === "PRE";
|
||||
|
||||
return commitMutationPromiseNormalized<MutationTypes>(environment, {
|
||||
mutation,
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mapValues } from "lodash";
|
||||
import { GQLUSER_ROLE, GQLUSER_ROLE_RL } from "talk-framework/schema";
|
||||
|
||||
/**
|
||||
* permissionMap describes what abilities certain roles have.
|
||||
*
|
||||
* This list is currently manually managed. We want to
|
||||
* get to a point where this is generated from the schema.
|
||||
*
|
||||
* We currently specify in the comments which endpoints of
|
||||
* the graph is important for the ability, which we can later
|
||||
* used to auto generate the map making the schema become
|
||||
* the single point of truth.
|
||||
*/
|
||||
const permissionMap = {
|
||||
// Mutation.updateStorySettings
|
||||
CHANGE_STORY_CONFIGURATION: [GQLUSER_ROLE.ADMIN, GQLUSER_ROLE.MODERATOR],
|
||||
};
|
||||
|
||||
export type AbilityType = keyof typeof permissionMap;
|
||||
export const Ability = mapValues(permissionMap, (_, key) => key) as {
|
||||
[P in AbilityType]: P
|
||||
};
|
||||
|
||||
/**
|
||||
* can is used to check if the `viewer` has permission for `ability`.
|
||||
*
|
||||
* Example: `can(props.me, Ability.CHANGE_ROLE)`.
|
||||
*/
|
||||
export function can(viewer: { role: GQLUSER_ROLE_RL }, ability: AbilityType) {
|
||||
return permissionMap[ability].includes(viewer.role as GQLUSER_ROLE);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GQLUSER_ROLE } from "talk-framework/schema";
|
||||
import {
|
||||
denormalizeComment,
|
||||
denormalizeComments,
|
||||
@@ -76,17 +77,17 @@ export const users = [
|
||||
{
|
||||
id: "user-0",
|
||||
username: "Markus",
|
||||
role: "COMMENTER",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
{
|
||||
id: "user-1",
|
||||
username: "Lukas",
|
||||
role: "COMMENTER",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
{
|
||||
id: "user-2",
|
||||
username: "Isabelle",
|
||||
role: "COMMENTER",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -399,13 +400,13 @@ export const storyWithDeepestReplies = denormalizeStory({
|
||||
export const viewerAsModerator = {
|
||||
id: "me-as-moderator",
|
||||
username: "Moderator",
|
||||
role: "MODERATOR",
|
||||
role: GQLUSER_ROLE.MODERATOR,
|
||||
};
|
||||
|
||||
export const viewerWithComments = {
|
||||
id: "me-with-comments",
|
||||
username: "Markus",
|
||||
role: "COMMENTER",
|
||||
role: GQLUSER_ROLE.COMMENTER,
|
||||
comments: {
|
||||
edges: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user