Skip to content

Commit

Permalink
feat: remove group.permissions code
Browse files Browse the repository at this point in the history
Also cleanup of createAccountGroup and updateAccountGroup mutations

Signed-off-by: Eric Dobbertin <[email protected]>
  • Loading branch information
aldeed committed Feb 26, 2020
1 parent b0d1913 commit 91f5b14
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 270 deletions.
59 changes: 34 additions & 25 deletions src/core-services/account/mutations/createAccountGroup.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Logger from "@reactioncommerce/logger";
import Random from "@reactioncommerce/random";
import ReactionError from "@reactioncommerce/reaction-error";
import getSlug from "@reactioncommerce/api-utils/getSlug.js";

Expand All @@ -10,57 +11,65 @@ import getSlug from "@reactioncommerce/api-utils/getSlug.js";
* It creates permission group for a given shop with passed in roles
* This method also effectively creates a role in reaction authorization service with the same
* name as the supplied group if kafka connect mongo has been configured on the mongo db
* @param {object} context - The GraphQL context
* @param {String} context.shopId - id of the shop the group belongs to
* @param {object} input - The input supplied from GraphQL
* @param {Object} input.groupData - info about group to create
* @param {String} input.groupData.name - name of the group to be created
* @param {String} input.groupData.description - Optional description of the group to be created
* @param {Array} input.groupData.permissions - permissions to assign to the group being created
* @param {Array} input.groupData.members - members of the
* @returns {Object} - `object.status` of 200 on success or Error object on failure
* @param {Object} context - The GraphQL context
* @param {Object} input - The input supplied from GraphQL
* @param {String} input.shopId - ID of the shop the group belongs to
* @param {Object} input.group - info about group to create
* @param {String} input.group.name - name of the group to be created
* @param {String} input.group.description - Optional description of the group to be created
* @returns {Object} An object where `group` is the newly created group.
*/
export default async function createAccountGroup(context, input) {
const { group, shopId } = input;
let newlyCreatedGroup;
const { Groups } = context.collections;
const { group, shopId = null } = input;
const {
accountId,
appEvents,
collections: { Groups },
simpleSchemas: { Group },
userId
} = context;

// we are limiting group method actions to only users within the account managers role
await context.validatePermissions("reaction:legacy:groups", "create", { shopId });

const nowDate = new Date();
const newGroupData = Object.assign({}, group, {
slug: getSlug(group.name),
shopId,
const newGroup = Object.assign({}, group, {
_id: Random.id(),
createdAt: nowDate,
createdBy: accountId,
shopId,
slug: group.slug || getSlug(group.name),
updatedAt: nowDate
});

// TODO: Remove when we move away from legacy permission verification
if (!newGroupData.permissions) {
newGroupData.permissions = [];
}

// ensure one group type per shop
const groupExists = await Groups.findOne({ slug: newGroupData.slug, shopId });
const groupExists = await Groups.findOne({ slug: newGroup.slug, shopId });

if (groupExists) {
throw new ReactionError("conflict", "Group already exist for this shop");
}

Group.validate(newGroup);

Logger.debug(`creating group ${newGroup.slug} for shop ${shopId}`);

try {
/** Kafka connect mongo should be listening for insert events
and should place the newly created group on the kakfa groups topic.
and should place the newly created group on the Kafka groups topic.
reaction authorization listens on the topic and creates role (group) in
reaction authorization
*/

const result = await Groups.insertOne(newGroupData);
newlyCreatedGroup = result.ops ? result.ops[0] : {};
await Groups.insertOne(newGroup);
} catch (error) {
Logger.error(error);
throw new ReactionError("invalid-parameter", "Bad request");
}

return { group: newlyCreatedGroup };
await appEvents.emit("afterAccountGroupCreate", {
createdBy: userId,
group: newGroup
});

return { group: newGroup };
}
54 changes: 18 additions & 36 deletions src/core-services/account/mutations/createAccountGroup.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import mockContext from "@reactioncommerce/api-utils/tests/mockContext.js";
import getSlug from "@reactioncommerce/api-utils/getSlug.js";
import ReactionError from "@reactioncommerce/reaction-error";
import { Group } from "../simpleSchemas.js";
import createAccountGroup from "./createAccountGroup.js";

mockContext.validatePermissions = jest.fn("validatePermissions");
mockContext.collections.Groups.insert = jest.fn("collections.Groups.insertOne");
mockContext.collections.Groups.findOne = jest.fn("collections.Groups.findOne");
mockContext.simpleSchemas = { Group };

test("should create a group for the shop", async () => {
const groupName = "test group";
Expand All @@ -17,37 +18,27 @@ test("should create a group for the shop", async () => {

const input = { group, shopId };

const fakeResult = {
_id: "test-group-id",
name: "test group",
slug: "test group",
permissions: [],
shopId: "test-shop-id"
};


const insertOneRes = {
ops: [fakeResult]
};
mockContext.collections.Groups.insertOne.mockReturnValueOnce(Promise.resolve(insertOneRes));
mockContext.collections.Groups.insertOne.mockReturnValueOnce(Promise.resolve(undefined));
mockContext.collections.Groups.findOne.mockReturnValueOnce(Promise.resolve(undefined));

mockContext.validatePermissions.mockReturnValueOnce(Promise.resolve(undefined));

const result = await createAccountGroup(mockContext, input);
const expected = Object.assign({}, { group: fakeResult });
await expect(result).toEqual(expected);

expect(mockContext.validatePermissions).toHaveBeenCalledWith("reaction:legacy:groups", "create", { shopId });
expect(mockContext.collections.Groups.findOne).toHaveBeenNthCalledWith(1, { slug: "test-group", shopId });
expect(mockContext.collections.Groups.insertOne).toHaveBeenCalledWith({
const expectedGroup = {
_id: jasmine.any(String),
createdAt: jasmine.any(Date),
createdBy: "FAKE_ACCOUNT_ID",
name: "test group",
slug: getSlug("test group"),
permissions: [],
shopId: "test-shop-id",
updatedAt: expect.any(Date),
createdAt: expect.any(Date)
});
slug: "test group",
updatedAt: jasmine.any(Date)
};

await expect(result).toEqual({ group: expectedGroup });

expect(mockContext.validatePermissions).toHaveBeenCalledWith("reaction:legacy:groups", "create", { shopId });
expect(mockContext.collections.Groups.findOne).toHaveBeenNthCalledWith(1, { slug: "test group", shopId });
expect(mockContext.collections.Groups.insertOne).toHaveBeenCalledWith(expectedGroup);
});

test("should throw if group already exists", async () => {
Expand All @@ -64,20 +55,11 @@ test("should throw if group already exists", async () => {
_id: "test-group-id",
name: "test group",
slug: "test group",
permissions: [
"dashboard"
],
shopId: "test-shop-id"
};

const insertOneRes = {
ops: [fakeResult]
};
mockContext.collections.Groups.insertOne.mockReturnValueOnce(Promise.resolve(insertOneRes));
mockContext.collections.Groups.findOne
.mockReturnValueOnce(Promise.resolve(fakeResult))
.mockReturnValueOnce(Promise.resolve(undefined));

mockContext.collections.Groups.insertOne.mockReturnValueOnce(Promise.resolve(undefined));
mockContext.collections.Groups.findOne.mockReturnValueOnce(Promise.resolve(fakeResult));
mockContext.validatePermissions.mockReturnValueOnce(Promise.resolve(undefined));

await expect(createAccountGroup(mockContext, input)).rejects.toThrow(new ReactionError("conflict", "Group already exist for this shop"));
Expand Down
38 changes: 8 additions & 30 deletions src/core-services/account/mutations/createAuthGroupsForShop.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
import Logger from "@reactioncommerce/logger";
import Random from "@reactioncommerce/random";
import {
defaultShopOwnerRoles,
defaultShopManagerRoles
} from "../util/defaultRoles.js";

/**
* @name createAuthGroupsForShop
* @method
Expand All @@ -15,31 +8,16 @@ import {
* @returns {undefined}
*/
export default async function createAuthGroupsForShop(context, shopId) {
const {
collections: {
Groups,
Shops
}
} = context;

const roles = {
"shop manager": defaultShopManagerRoles,
"owner": defaultShopOwnerRoles
};

const primaryShop = await Shops.findOne({ shopType: "primary" });
const { collections: { Groups } } = context;

const promises = Object.keys(roles).map(async (slug) => {
const promises = ["shop manager", "owner"].map(async (slug) => {
const existingGroup = await Groups.findOne({ shopId, slug });
if (!existingGroup) { // create group only if it doesn't exist before
Logger.debug(`creating group ${slug} for shop ${shopId}`);
// get roles from the default groups of the primary shop; we try to use this first before using default roles
const primaryShopGroup = primaryShop ? await Groups.findOne({ shopId: primaryShop._id, slug }) : null;
await Groups.insertOne({
_id: Random.id(),
name: slug,
slug,
permissions: (primaryShopGroup && primaryShopGroup.permissions) || roles[slug],
if (!existingGroup) {
await context.mutations.createAccountGroup(context.getInternalContext(), {
group: {
name: slug,
slug
},
shopId
});
}
Expand Down
77 changes: 22 additions & 55 deletions src/core-services/account/mutations/updateAccountGroup.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
import ReactionError from "@reactioncommerce/reaction-error";
import SimpleSchema from "simpl-schema";
import getSlug from "@reactioncommerce/api-utils/getSlug.js";
import defaultAccountGroups from "../util/defaultAccountGroups.js";

const inputSchema = new SimpleSchema({
"slug": { type: String, optional: true },
"name": { type: String, optional: true },
"description": { type: String, optional: true },
"permissions": { type: Array, optional: true },
"permissions.$": String,
"updatedAt": Date
});

/**
* @name group/updateAccountGroup
Expand All @@ -21,70 +9,49 @@ const inputSchema = new SimpleSchema({
* This method also effectively updates a role in reaction authorization service with the same
* name as the supplied group if kafka connect mongo has been configured on the mongo db
* @param {object} context - The GraphQL context
* @param {String} context.shopId - id of the shop the group belongs to
* @param {object} input - The input supplied from GraphQL
* @param {Object} input.groupData - info about group to updated
* @param {String} input.groupData.name - name of the group to be updated
* @param {String} input.groupData.description - Optional description of the group to be updated
* @param {Array} input.groupData.permissions - permissions to assign to the group being updated
* @param {Array} input.groupData.members - members of the
* @returns {Object} - `object.status` of 200 on success or Error object on failure
* @param {String} input.shopId - id of the shop the group belongs to
* @param {Object} input.group - info about group to updated
* @param {String} input.group.name - name of the group to be updated
* @param {String} input.group.description - Optional description of the group to be updated
* @returns {Object} Updated group
*/
export default async function updateAccountGroup(context, input) {
const { group, groupId, shopId } = input;
const { appEvents, user } = context;
const { Groups } = context.collections;
const {
appEvents,
collections: { Groups },
simpleSchemas: { Group },
user
} = context;

// we are limiting group method actions to only users within the account managers role
await context.validatePermissions(`reaction:legacy:groups:${groupId}`, "update", { shopId });

// Ensure group exists before proceeding
const existingGroup = await Groups.findOne({ _id: groupId });

const existingGroup = await Groups.findOne({ _id: groupId, shopId });
if (!existingGroup) {
throw new ReactionError("not-found", `Group with ID (${groupId}) doesn't exist`);
}

const updateGroupData = {
updatedAt: new Date()
const modifier = {
$set: {
...group,
updatedAt: new Date()
}
};

// Update the name if provided
if (group.name) {
updateGroupData.name = group.name;
}

// Prevent updating the slug of the default groups.
// For example, changing the slug of the shop manager group could cause various features of the application to stop working as intended.
if (defaultAccountGroups.includes(existingGroup.slug) && group.slug && group.slug !== existingGroup.slug) {
throw new ReactionError("access-denied", `Field 'slug' cannot be updated for default group with ID (${groupId}) and name (${existingGroup.name}).`);
} else if (group.slug) { // Update the slug if available for other groups
updateGroupData.slug = getSlug(group.slug);
}

// Update description
if (group.description) {
updateGroupData.description = group.description;
}

// Update the permissions on the group and any user in those groups
if (Array.isArray(group.permissions)) {
updateGroupData.permissions = group.permissions;
}

// Validate final group object
inputSchema.validate(updateGroupData);
Group.validate(modifier, { modifier: true });

/** Kafka connect mongo should be listening for update events
and should place the updated group on the kakfa groups topic.
and should place the updated group on the Kafka groups topic.
reaction authorization listens on the topic and updates role (group) in
reaction authorization
*/
const { value: updatedGroup } = await Groups.findOneAndUpdate(
{ _id: groupId },
{
$set: updateGroupData
},
modifier,
{
returnOriginal: false
}
Expand All @@ -93,9 +60,9 @@ export default async function updateAccountGroup(context, input) {
if (!updatedGroup) throw new ReactionError("server-error", `Unable to update Group ${group._id}`);

await appEvents.emit("afterAccountGroupUpdate", {
account: updatedGroup,
group: updatedGroup,
updatedBy: user._id,
updatedFields: Object.keys(updateGroupData)
updatedFields: Object.keys(modifier.$set)
});

return updatedGroup;
Expand Down
Loading

0 comments on commit 91f5b14

Please sign in to comment.