refactor: Policies Architecture (#1016)
* add policy serialize method * Add policies to collection responses * wip * test: remove .only * refactor: Return policies with team and document requests * store policies on the client * refactor: drive admin UI from policies
This commit is contained in:
parent
cf18b952a4
commit
e2b28dfeb7
|
@ -25,12 +25,14 @@ import Bubble from './components/Bubble';
|
|||
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
import DocumentsStore from 'stores/DocumentsStore';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import UiStore from 'stores/UiStore';
|
||||
import { observable } from 'mobx';
|
||||
|
||||
type Props = {
|
||||
auth: AuthStore,
|
||||
documents: DocumentsStore,
|
||||
policies: PoliciesStore,
|
||||
ui: UiStore,
|
||||
};
|
||||
|
||||
|
@ -55,11 +57,12 @@ class MainSidebar extends React.Component<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { auth, documents } = this.props;
|
||||
const { auth, documents, policies } = this.props;
|
||||
const { user, team } = auth;
|
||||
if (!user || !team) return null;
|
||||
|
||||
const draftDocumentsCount = documents.drafts.length;
|
||||
const can = policies.abilties(team.id);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
|
@ -125,7 +128,7 @@ class MainSidebar extends React.Component<Props> {
|
|||
documents.active ? documents.active.isArchived : undefined
|
||||
}
|
||||
/>
|
||||
{user.isAdmin && (
|
||||
{can.invite && (
|
||||
<SidebarLink
|
||||
onClick={this.handleInviteModalOpen}
|
||||
icon={<PlusIcon />}
|
||||
|
@ -151,4 +154,4 @@ const Drafts = styled(Flex)`
|
|||
height: 24px;
|
||||
`;
|
||||
|
||||
export default inject('documents', 'auth', 'ui')(MainSidebar);
|
||||
export default inject('documents', 'policies', 'auth', 'ui')(MainSidebar);
|
||||
|
|
|
@ -23,10 +23,12 @@ import Section from './components/Section';
|
|||
import Header from './components/Header';
|
||||
import SidebarLink from './components/SidebarLink';
|
||||
import HeaderBlock from './components/HeaderBlock';
|
||||
import PoliciesStore from 'stores/PoliciesStore';
|
||||
import AuthStore from 'stores/AuthStore';
|
||||
|
||||
type Props = {
|
||||
history: RouterHistory,
|
||||
policies: PoliciesStore,
|
||||
auth: AuthStore,
|
||||
};
|
||||
|
||||
|
@ -37,8 +39,11 @@ class SettingsSidebar extends React.Component<Props> {
|
|||
};
|
||||
|
||||
render() {
|
||||
const { team, user } = this.props.auth;
|
||||
if (!team || !user) return null;
|
||||
const { policies, auth } = this.props;
|
||||
const { team } = auth;
|
||||
if (!team) return null;
|
||||
|
||||
const can = policies.abilties(team.id);
|
||||
|
||||
return (
|
||||
<Sidebar>
|
||||
|
@ -71,14 +76,14 @@ class SettingsSidebar extends React.Component<Props> {
|
|||
</Section>
|
||||
<Section>
|
||||
<Header>Team</Header>
|
||||
{user.isAdmin && (
|
||||
{can.update && (
|
||||
<SidebarLink
|
||||
to="/settings/details"
|
||||
icon={<TeamIcon />}
|
||||
label="Details"
|
||||
/>
|
||||
)}
|
||||
{user.isAdmin && (
|
||||
{can.update && (
|
||||
<SidebarLink
|
||||
to="/settings/security"
|
||||
icon={<PadlockIcon />}
|
||||
|
@ -96,14 +101,14 @@ class SettingsSidebar extends React.Component<Props> {
|
|||
icon={<LinkIcon />}
|
||||
label="Share Links"
|
||||
/>
|
||||
{user.isAdmin && (
|
||||
{can.auditLog && (
|
||||
<SidebarLink
|
||||
to="/settings/events"
|
||||
icon={<BulletedListIcon />}
|
||||
label="Audit Log"
|
||||
/>
|
||||
)}
|
||||
{user.isAdmin && (
|
||||
{can.export && (
|
||||
<SidebarLink
|
||||
to="/settings/export"
|
||||
icon={<DocumentIcon />}
|
||||
|
@ -111,7 +116,7 @@ class SettingsSidebar extends React.Component<Props> {
|
|||
/>
|
||||
)}
|
||||
</Section>
|
||||
{user.isAdmin && (
|
||||
{can.update && (
|
||||
<Section>
|
||||
<Header>Integrations</Header>
|
||||
<SidebarLink
|
||||
|
@ -133,4 +138,4 @@ class SettingsSidebar extends React.Component<Props> {
|
|||
}
|
||||
}
|
||||
|
||||
export default inject('auth')(SettingsSidebar);
|
||||
export default inject('auth', 'policies')(SettingsSidebar);
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
// @flow
|
||||
import BaseModel from './BaseModel';
|
||||
|
||||
class Policy extends BaseModel {
|
||||
id: string;
|
||||
abilities: {
|
||||
[key: string]: boolean,
|
||||
};
|
||||
}
|
||||
|
||||
export default Policy;
|
|
@ -44,6 +44,12 @@ export default class AuthStore {
|
|||
});
|
||||
}
|
||||
|
||||
addPolicies = policies => {
|
||||
if (policies) {
|
||||
policies.forEach(policy => this.rootStore.policies.add(policy));
|
||||
}
|
||||
};
|
||||
|
||||
@computed
|
||||
get authenticated(): boolean {
|
||||
return !!this.token;
|
||||
|
@ -64,6 +70,7 @@ export default class AuthStore {
|
|||
invariant(res && res.data, 'Auth not available');
|
||||
|
||||
runInAction('AuthStore#fetch', () => {
|
||||
this.addPolicies(res.policies);
|
||||
const { user, team } = res.data;
|
||||
this.user = user;
|
||||
this.team = team;
|
||||
|
@ -112,6 +119,7 @@ export default class AuthStore {
|
|||
invariant(res && res.data, 'User response not available');
|
||||
|
||||
runInAction('AuthStore#updateUser', () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.user = res.data;
|
||||
});
|
||||
} finally {
|
||||
|
@ -132,6 +140,7 @@ export default class AuthStore {
|
|||
invariant(res && res.data, 'Team response not available');
|
||||
|
||||
runInAction('AuthStore#updateTeam', () => {
|
||||
this.addPolicies(res.policies);
|
||||
this.team = res.data;
|
||||
});
|
||||
} finally {
|
||||
|
|
|
@ -37,6 +37,12 @@ export default class BaseStore<T: BaseModel> {
|
|||
this.data.clear();
|
||||
}
|
||||
|
||||
addPolicies = policies => {
|
||||
if (policies) {
|
||||
policies.forEach(policy => this.rootStore.policies.add(policy));
|
||||
}
|
||||
};
|
||||
|
||||
@action
|
||||
add = (item: Object): T => {
|
||||
const Model = this.model;
|
||||
|
@ -80,6 +86,8 @@ export default class BaseStore<T: BaseModel> {
|
|||
const res = await client.post(`/${this.modelName}s.create`, params);
|
||||
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
|
@ -97,6 +105,8 @@ export default class BaseStore<T: BaseModel> {
|
|||
const res = await client.post(`/${this.modelName}s.update`, params);
|
||||
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
} finally {
|
||||
this.isSaving = false;
|
||||
|
@ -132,6 +142,8 @@ export default class BaseStore<T: BaseModel> {
|
|||
try {
|
||||
const res = await client.post(`/${this.modelName}s.info`, { id });
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
} finally {
|
||||
this.isFetching = false;
|
||||
|
@ -149,7 +161,9 @@ export default class BaseStore<T: BaseModel> {
|
|||
const res = await client.post(`/${this.modelName}s.list`, params);
|
||||
|
||||
invariant(res && res.data, 'Data not available');
|
||||
|
||||
runInAction(`list#${this.modelName}`, () => {
|
||||
this.addPolicies(res.policies);
|
||||
res.data.forEach(this.add);
|
||||
this.isLoaded = true;
|
||||
});
|
||||
|
|
|
@ -320,6 +320,8 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||
shareId: options.shareId,
|
||||
});
|
||||
invariant(res && res.data, 'Document not available');
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
this.add(res.data);
|
||||
|
||||
runInAction('DocumentsStore#fetch', () => {
|
||||
|
@ -363,6 +365,7 @@ export default class DocumentsStore extends BaseStore<Document> {
|
|||
const collection = this.getCollectionForDocument(document);
|
||||
if (collection) collection.refresh();
|
||||
|
||||
this.addPolicies(res.policies);
|
||||
return this.add(res.data);
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
// @flow
|
||||
import BaseStore from './BaseStore';
|
||||
import RootStore from './RootStore';
|
||||
import Policy from 'models/Policy';
|
||||
|
||||
export default class PoliciesStore extends BaseStore<Policy> {
|
||||
actions = [];
|
||||
|
||||
constructor(rootStore: RootStore) {
|
||||
super(rootStore, Policy);
|
||||
}
|
||||
|
||||
abilties(id: string) {
|
||||
const policy = this.get(id);
|
||||
return policy ? policy.abilities : {};
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import DocumentsStore from './DocumentsStore';
|
|||
import EventsStore from './EventsStore';
|
||||
import IntegrationsStore from './IntegrationsStore';
|
||||
import NotificationSettingsStore from './NotificationSettingsStore';
|
||||
import PoliciesStore from './PoliciesStore';
|
||||
import RevisionsStore from './RevisionsStore';
|
||||
import SharesStore from './SharesStore';
|
||||
import UiStore from './UiStore';
|
||||
|
@ -20,6 +21,7 @@ export default class RootStore {
|
|||
events: EventsStore;
|
||||
integrations: IntegrationsStore;
|
||||
notificationSettings: NotificationSettingsStore;
|
||||
policies: PoliciesStore;
|
||||
revisions: RevisionsStore;
|
||||
shares: SharesStore;
|
||||
ui: UiStore;
|
||||
|
@ -34,6 +36,7 @@ export default class RootStore {
|
|||
this.events = new EventsStore(this);
|
||||
this.integrations = new IntegrationsStore(this);
|
||||
this.notificationSettings = new NotificationSettingsStore(this);
|
||||
this.policies = new PoliciesStore(this);
|
||||
this.revisions = new RevisionsStore(this);
|
||||
this.shares = new SharesStore(this);
|
||||
this.ui = new UiStore();
|
||||
|
@ -48,6 +51,7 @@ export default class RootStore {
|
|||
this.events.clear();
|
||||
this.integrations.clear();
|
||||
this.notificationSettings.clear();
|
||||
this.policies.clear();
|
||||
this.revisions.clear();
|
||||
this.shares.clear();
|
||||
this.users.clear();
|
||||
|
|
|
@ -64,10 +64,10 @@ export default class UsersStore extends BaseStore<User> {
|
|||
id: user.id,
|
||||
});
|
||||
invariant(res && res.data, 'Data should be available');
|
||||
const { data } = res;
|
||||
|
||||
runInAction(`UsersStore#${action}`, () => {
|
||||
this.add(data);
|
||||
this.addPolicies(res.policies);
|
||||
this.add(res.data);
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// @flow
|
||||
import Router from 'koa-router';
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentUser, presentTeam } from '../presenters';
|
||||
import { presentUser, presentTeam, presentPolicies } from '../presenters';
|
||||
import { Team } from '../models';
|
||||
|
||||
const router = new Router();
|
||||
|
@ -15,6 +15,7 @@ router.post('auth.info', auth(), async ctx => {
|
|||
user: presentUser(user, { includeDetails: true }),
|
||||
team: presentTeam(team),
|
||||
},
|
||||
policies: presentPolicies(user, [team]),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import fs from 'fs';
|
|||
import Router from 'koa-router';
|
||||
import auth from '../middlewares/authentication';
|
||||
import pagination from './middlewares/pagination';
|
||||
import { presentCollection, presentUser } from '../presenters';
|
||||
import { presentCollection, presentUser, presentPolicies } from '../presenters';
|
||||
import { Collection, CollectionUser, Team, Event, User } from '../models';
|
||||
import { ValidationError, InvalidRequestError } from '../errors';
|
||||
import { exportCollections } from '../logistics';
|
||||
|
@ -45,6 +45,7 @@ router.post('collections.create', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentCollection(collection),
|
||||
policies: presentPolicies(user, [collection]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -52,11 +53,13 @@ router.post('collections.info', auth(), async ctx => {
|
|||
const { id } = ctx.body;
|
||||
ctx.assertUuid(id, 'id is required');
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findByPk(id);
|
||||
authorize(ctx.state.user, 'read', collection);
|
||||
authorize(user, 'read', collection);
|
||||
|
||||
ctx.body = {
|
||||
data: await presentCollection(collection),
|
||||
policies: presentPolicies(user, [collection]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -243,6 +246,7 @@ router.post('collections.update', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: presentCollection(collection),
|
||||
policies: presentPolicies(user, [collection]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -263,10 +267,12 @@ router.post('collections.list', auth(), pagination(), async ctx => {
|
|||
const data = await Promise.all(
|
||||
collections.map(async collection => await presentCollection(collection))
|
||||
);
|
||||
const policies = presentPolicies(user, collections);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -358,6 +358,7 @@ describe('#collections.create', async () => {
|
|||
expect(res.status).toEqual(200);
|
||||
expect(body.data.id).toBeTruthy();
|
||||
expect(body.data.name).toBe('Test');
|
||||
expect(body.policies.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import {
|
|||
presentDocument,
|
||||
presentCollection,
|
||||
presentRevision,
|
||||
presentPolicies,
|
||||
} from '../presenters';
|
||||
import {
|
||||
Collection,
|
||||
|
@ -88,9 +89,12 @@ router.post('documents.list', auth(), pagination(), async ctx => {
|
|||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -123,9 +127,12 @@ router.post('documents.pinned', auth(), pagination(), async ctx => {
|
|||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -154,9 +161,12 @@ router.post('documents.archived', auth(), pagination(), async ctx => {
|
|||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -191,13 +201,17 @@ router.post('documents.viewed', auth(), pagination(), async ctx => {
|
|||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const documents = views.map(view => view.document);
|
||||
const data = await Promise.all(
|
||||
views.map(view => presentDocument(view.document))
|
||||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -234,13 +248,17 @@ router.post('documents.starred', auth(), pagination(), async ctx => {
|
|||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
|
||||
const documents = stars.map(star => star.document);
|
||||
const data = await Promise.all(
|
||||
stars.map(star => presentDocument(star.document))
|
||||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -266,9 +284,12 @@ router.post('documents.drafts', auth(), pagination(), async ctx => {
|
|||
documents.map(document => presentDocument(document))
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -311,6 +332,7 @@ router.post('documents.info', auth({ required: false }), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document, { isPublic }),
|
||||
policies: isPublic ? undefined : presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -404,6 +426,7 @@ router.post('documents.restore', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -443,6 +466,7 @@ router.post('documents.search', auth(), pagination(), async ctx => {
|
|||
limit,
|
||||
});
|
||||
|
||||
const documents = results.map(result => result.document);
|
||||
const data = await Promise.all(
|
||||
results.map(async result => {
|
||||
const document = await presentDocument(result.document);
|
||||
|
@ -450,9 +474,12 @@ router.post('documents.search', auth(), pagination(), async ctx => {
|
|||
})
|
||||
);
|
||||
|
||||
const policies = presentPolicies(user, documents);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -479,6 +506,7 @@ router.post('documents.pin', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -505,6 +533,7 @@ router.post('documents.unpin', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -637,6 +666,7 @@ router.post('documents.create', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -719,6 +749,7 @@ router.post('documents.update', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -774,6 +805,7 @@ router.post('documents.move', auth(), async ctx => {
|
|||
collections: await Promise.all(
|
||||
collections.map(collection => presentCollection(collection))
|
||||
),
|
||||
policies: presentPolicies(user, documents),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -800,6 +832,7 @@ router.post('documents.archive', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Team } from '../models';
|
|||
import { publicS3Endpoint } from '../utils/s3';
|
||||
|
||||
import auth from '../middlewares/authentication';
|
||||
import { presentTeam } from '../presenters';
|
||||
import { presentTeam, presentPolicies } from '../presenters';
|
||||
import policy from '../policies';
|
||||
|
||||
const { authorize } = policy;
|
||||
|
@ -32,6 +32,7 @@ router.post('team.update', auth(), async ctx => {
|
|||
|
||||
ctx.body = {
|
||||
data: presentTeam(team),
|
||||
policies: presentPolicies(user, [team]),
|
||||
};
|
||||
});
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ allow(User, 'archive', Document, (user, document) => {
|
|||
if (cannot(user, 'read', document.collection)) return false;
|
||||
}
|
||||
if (!document.publishedAt) return false;
|
||||
if (document.archivedAt) return false;
|
||||
|
||||
return user.teamId === document.teamId;
|
||||
});
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
// @flow
|
||||
import { Team, User, Collection, Document } from '../models';
|
||||
import policy from './policy';
|
||||
import './apiKey';
|
||||
import './collection';
|
||||
|
@ -9,4 +10,36 @@ import './share';
|
|||
import './user';
|
||||
import './team';
|
||||
|
||||
const { can, abilities } = policy;
|
||||
|
||||
type Policy = {
|
||||
[key: string]: boolean,
|
||||
};
|
||||
|
||||
/*
|
||||
* Given a user and a model – output an object which describes the actions the
|
||||
* user may take against the model. This serialized policy is used for testing
|
||||
* and sent in API responses to allow clients to adjust which UI is displayed.
|
||||
*/
|
||||
export function serialize(
|
||||
model: User,
|
||||
target: Team | Collection | Document
|
||||
): Policy {
|
||||
let output = {};
|
||||
|
||||
abilities.forEach(ability => {
|
||||
if (model instanceof ability.model && target instanceof ability.target) {
|
||||
let response = true;
|
||||
try {
|
||||
response = can(model, ability.action, target);
|
||||
} catch (err) {
|
||||
response = false;
|
||||
}
|
||||
output[ability.action] = response;
|
||||
}
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
export default policy;
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
/* eslint-disable flowtype/require-valid-file-annotation */
|
||||
import { flushdb } from '../test/support';
|
||||
import { buildUser } from '../test/factories';
|
||||
import { serialize } from './index';
|
||||
|
||||
beforeEach(flushdb);
|
||||
|
||||
it('should serialize policy', async () => {
|
||||
const user = await buildUser();
|
||||
const response = serialize(user, user);
|
||||
expect(response.update).toEqual(true);
|
||||
expect(response.delete).toEqual(true);
|
||||
});
|
|
@ -17,6 +17,11 @@ allow(User, 'auditLog', Team, user => {
|
|||
return false;
|
||||
});
|
||||
|
||||
allow(User, 'invite', Team, user => {
|
||||
if (user.isAdmin) return true;
|
||||
return false;
|
||||
});
|
||||
|
||||
allow(User, ['update', 'export'], Team, (user, team) => {
|
||||
if (!team || user.teamId !== team.id) return false;
|
||||
if (user.isAdmin) return true;
|
||||
|
|
|
@ -11,6 +11,7 @@ import presentTeam from './team';
|
|||
import presentIntegration from './integration';
|
||||
import presentNotificationSetting from './notificationSetting';
|
||||
import presentSlackAttachment from './slackAttachment';
|
||||
import presentPolicies from './policy';
|
||||
|
||||
export {
|
||||
presentUser,
|
||||
|
@ -25,4 +26,5 @@ export {
|
|||
presentIntegration,
|
||||
presentNotificationSetting,
|
||||
presentSlackAttachment,
|
||||
presentPolicies,
|
||||
};
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
// @flow
|
||||
import { User } from '../models';
|
||||
|
||||
type Policy = { id: string, abilities: { [key: string]: boolean } };
|
||||
|
||||
export default function present(user: User, objects: Object[]): Policy[] {
|
||||
const { serialize } = require('../policies');
|
||||
|
||||
return objects.map(object => ({
|
||||
id: object.id,
|
||||
abilities: serialize(user, object),
|
||||
}));
|
||||
}
|
Reference in New Issue