This reverts commit 9c76754e79.
This commit is contained in:
1 parent
9c76754e79
commit
2273cc519a
15 files changed
+654
-112
No files matched your search
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"plugins": ["prettier"],
|
|
||||||
"extends": ["prettier"],
|
|
||||||
"rules": {
|
|
||||||
"prettier/prettier": "error",
|
|
||||||
"semi": "error",
|
|
||||||
"no-unused-expressions": "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,8 +11,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "babel src --out-dir dist/ --ignore **/__tests__",
|
"build": "babel src --out-dir dist/ --ignore **/__tests__",
|
||||||
"prepare": "cross-env NODE_ENV=production npm run build",
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
"watch": "babel -w src --out-dir dist/ --ignore **/__tests__",
|
"watch": "babel -w src --out-dir dist/ --ignore **/__tests__"
|
||||||
"test": "jest"
|
|
||||||
},
|
},
|
||||||
"author": "Sebastian Rindom",
|
"author": "Sebastian Rindom",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
@@ -22,11 +21,8 @@
|
|||||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
"@babel/preset-env": "^7.7.5",
|
"@babel/preset-env": "^7.7.5",
|
||||||
"@babel/runtime": "^7.9.6",
|
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"eslint": "^6.8.0",
|
"eslint": "^6.8.0"
|
||||||
"jest": "^25.5.0",
|
|
||||||
"prettier": "^1.19.1"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mongoose": "^5.8.0"
|
"mongoose": "^5.8.0"
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
import BaseService from "../base-service"
|
|
||||||
|
|
||||||
describe("BaseService", () => {
|
|
||||||
describe("addDecorator", () => {
|
|
||||||
const baseService = new BaseService()
|
|
||||||
|
|
||||||
it("successfully adds decorator", () => {
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
return (obj.decorator1 = true)
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(baseService.decorators_.length).toEqual(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("throws if decorator is not a function", () => {
|
|
||||||
expect(() => baseService.addDecorator("not a function")).toThrow(
|
|
||||||
"Decorators must be of type function"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe("runDecorators_", () => {
|
|
||||||
it("returns success when passwords match", async () => {
|
|
||||||
const baseService = new BaseService()
|
|
||||||
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
obj.decorator1 = true
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
obj.decorator2 = true
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await baseService.runDecorators_({ data: "initial" })
|
|
||||||
expect(result).toEqual({
|
|
||||||
data: "initial",
|
|
||||||
decorator1: true,
|
|
||||||
decorator2: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("skips failing decorator", async () => {
|
|
||||||
const baseService = new BaseService()
|
|
||||||
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
obj.decorator1 = true
|
|
||||||
return obj
|
|
||||||
})
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
return Promise.reject("fail")
|
|
||||||
})
|
|
||||||
baseService.addDecorator(obj => {
|
|
||||||
obj.decorator3 = true
|
|
||||||
return Promise.resolve(obj)
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await baseService.runDecorators_({ data: "initial" })
|
|
||||||
expect(result).toEqual({
|
|
||||||
data: "initial",
|
|
||||||
decorator1: true,
|
|
||||||
decorator3: true,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -2,35 +2,5 @@
|
|||||||
* Common functionality for Services
|
* Common functionality for Services
|
||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
class BaseService {
|
class BaseService {}
|
||||||
constructor() {
|
|
||||||
this.decorators_ = []
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a decorator to a service. The decorator must be a function and should
|
|
||||||
* return a decorated object.
|
|
||||||
* @param {function} fn - the decorator to add to the service
|
|
||||||
*/
|
|
||||||
addDecorator(fn) {
|
|
||||||
if (typeof fn !== "function") {
|
|
||||||
throw Error("Decorators must be of type function")
|
|
||||||
}
|
|
||||||
|
|
||||||
this.decorators_.push(fn)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs the decorators registered on the service. The decorators are run in
|
|
||||||
* the order they have been registered in. Failing decorators will be skipped
|
|
||||||
* in order to ensure deliverability in spite of breaking code.
|
|
||||||
* @param {object} obj - the object to decorate.
|
|
||||||
* @return {object} the decorated object.
|
|
||||||
*/
|
|
||||||
runDecorators_(obj) {
|
|
||||||
return this.decorators_.reduce(async (acc, next) => {
|
|
||||||
return acc.then(res => next(res)).catch(() => acc)
|
|
||||||
}, Promise.resolve(obj))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export default BaseService
|
export default BaseService
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"plugins": ["@babel/plugin-proposal-class-properties"],
|
||||||
|
"presets": ["@babel/preset-env"],
|
||||||
|
"env": {
|
||||||
|
"test": {
|
||||||
|
"plugins": ["@babel/plugin-transform-runtime"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
/*.js
|
||||||
|
!index.js
|
||||||
|
yarn.lock
|
||||||
|
yarn-error.log
|
||||||
File renamed without changes.
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-plugin-permissions",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Role permission for Medusa core",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/medusa-plugin-permissions"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "babel src --out-dir dist/ --ignore **/__tests__",
|
||||||
|
"prepare": "cross-env NODE_ENV=production npm run build",
|
||||||
|
"watch": "babel -w src --out-dir dist/ --ignore **/__tests__",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"author": "Oliver Juhl",
|
||||||
|
"license": "AGPL-3.0-or-later",
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/cli": "^7.7.5",
|
||||||
|
"@babel/core": "^7.7.5",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||||
|
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||||
|
"@babel/preset-env": "^7.7.5",
|
||||||
|
"@babel/runtime": "^7.7.6",
|
||||||
|
"cross-env": "^5.2.1",
|
||||||
|
"jest": "^24.9.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mongoose": "^5.8.0",
|
||||||
|
"medusa-test-utils": "^1.0.0",
|
||||||
|
"medusa-core-utils": "^1.0.0",
|
||||||
|
"medusa-interfaces": "^1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// This middleware is injected to ensure authorization of requests
|
||||||
|
// Since this middleware uses the user object on the request, this should be
|
||||||
|
// injected after authentication in the core middleware, hence we name
|
||||||
|
// the middleware postAuth.
|
||||||
|
export default postAuth = () => {
|
||||||
|
return (err, req, res, next) => {
|
||||||
|
const permissionService = req.scope.resolve("permissionService")
|
||||||
|
if (permissionService.hasPermission(req.user, req.method, req.path)) {
|
||||||
|
next()
|
||||||
|
} else {
|
||||||
|
res.status(422)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const permissions = {
|
||||||
|
productEditorPermission: {
|
||||||
|
_id: IdMap.getId("product_editor"),
|
||||||
|
name: "product_editor",
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "/products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
endpoint: "/products",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RoleModelMock = {
|
||||||
|
create: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
deleteOne: jest.fn().mockReturnValue(Promise.resolve()),
|
||||||
|
findOne: jest.fn().mockImplementation(query => {
|
||||||
|
if (query.name === "product_editor") {
|
||||||
|
return Promise.resolve(permissions.productEditorPermission)
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
}),
|
||||||
|
updateOne: jest.fn().mockImplementation((query, update) => {
|
||||||
|
return Promise.resolve()
|
||||||
|
}),
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { BaseModel } from "medusa-interfaces"
|
||||||
|
|
||||||
|
import PermissionSchema from "./schemas/permission"
|
||||||
|
|
||||||
|
class RoleModel extends BaseModel {
|
||||||
|
static modelName = "Role"
|
||||||
|
static schema = {
|
||||||
|
name: { type: String, required: true, unique: true },
|
||||||
|
permissions: { type: [PermissionSchema], required: true, default: [] },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RoleModel
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import mongoose from "mongoose"
|
||||||
|
|
||||||
|
export default new mongoose.Schema({
|
||||||
|
method: { type: String },
|
||||||
|
endpoint: { type: String },
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
|
||||||
|
export const PermissionServiceMock = {
|
||||||
|
hasPermission: jest.fn().mockImplementation((user, method, endpoint) => {
|
||||||
|
if (user._id === IdMap.getId("test-user")) {
|
||||||
|
return Promise.resolve(true)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
const mock = jest.fn().mockImplementation(() => {
|
||||||
|
return PermissionServiceMock
|
||||||
|
})
|
||||||
|
|
||||||
|
export default mock
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
import mongoose from "mongoose"
|
||||||
|
import { IdMap } from "medusa-test-utils"
|
||||||
|
import PermissionService from "../permission"
|
||||||
|
import { permissions, RoleModelMock } from "../../models/__mocks__/role"
|
||||||
|
|
||||||
|
describe("PermissionService", () => {
|
||||||
|
describe("hasPermission", () => {
|
||||||
|
let result
|
||||||
|
let user = {
|
||||||
|
_id: IdMap.getId("test-user"),
|
||||||
|
email: "oliver@medusa.test",
|
||||||
|
passwordHash: "123456789",
|
||||||
|
metadata: {
|
||||||
|
roles: ["product_editor"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
result = await permissionService.hasPermission(user, "POST", "/products")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls permission model functions", () => {
|
||||||
|
expect(RoleModelMock.findOne).toHaveBeenCalledTimes(
|
||||||
|
user.metadata.roles.length
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully grants access to user", () => {
|
||||||
|
expect(result).toEqual(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("succesfully denies access to user", async () => {
|
||||||
|
const accessDenied = await permissionService.hasPermission(
|
||||||
|
user,
|
||||||
|
"CREATE",
|
||||||
|
"/orders"
|
||||||
|
)
|
||||||
|
expect(accessDenied).toEqual(false)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("retrieveRole", () => {
|
||||||
|
let result
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
})
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
result = await permissionService.retrieveRole("product_editor")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls permission model functions", () => {
|
||||||
|
expect(RoleModelMock.findOne).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully fetches product editor permissions", () => {
|
||||||
|
expect(result).toEqual(permissions.productEditorPermission)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if role with name does not exist", async () => {
|
||||||
|
try {
|
||||||
|
await permissionService.retrieveRole("product_editor")
|
||||||
|
} catch (error) {
|
||||||
|
expect(error.message).toEqual(
|
||||||
|
"test_editor does not exist. Use method createRole to create it."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("createRole", () => {
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
|
||||||
|
const contentEditorPermissions = [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
await permissionService.createRole(
|
||||||
|
"content_editor",
|
||||||
|
contentEditorPermissions
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("calls permission model functions", () => {
|
||||||
|
expect(RoleModelMock.create).toHaveBeenCalledTimes(1)
|
||||||
|
expect(RoleModelMock.create).toHaveBeenCalledWith({
|
||||||
|
name: "content_editor",
|
||||||
|
permissions: [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: "PUT",
|
||||||
|
endpoint: "/contents",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if any permission is invalid", async () => {
|
||||||
|
try {
|
||||||
|
await permissionService.createRole("content_editor", [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/products",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Should fail since this is not a valid http request
|
||||||
|
method: "FETCH",
|
||||||
|
endpoint: "/products",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual("Permission is not valid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if role with name already exists", async () => {
|
||||||
|
try {
|
||||||
|
await permissionService.createRole("product_editor", [
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/order",
|
||||||
|
},
|
||||||
|
])
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual("product_editor already exists")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("grantRole", () => {
|
||||||
|
const setMetadataMock = jest.fn().mockReturnValue(Promise.resolve())
|
||||||
|
const userRetrieveMock = jest.fn().mockImplementation((data) => {
|
||||||
|
if (data === IdMap.getId("permission-user")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
_id: IdMap.getId("permission-user"),
|
||||||
|
email: "oliver@test.dk",
|
||||||
|
metadata: {
|
||||||
|
roles: ["content_editor"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (data === IdMap.getId("user-without-roles")) {
|
||||||
|
return Promise.resolve({
|
||||||
|
_id: IdMap.getId("user-without-roles"),
|
||||||
|
email: "oliver@test.dk",
|
||||||
|
metadata: {},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return Promise.resolve(undefined)
|
||||||
|
})
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
userService: {
|
||||||
|
setMetadata: setMetadataMock,
|
||||||
|
retrieve: userRetrieveMock,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully grants role to user", async () => {
|
||||||
|
await permissionService.grantRole(
|
||||||
|
IdMap.getId("permission-user"),
|
||||||
|
"product_editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("permission-user"),
|
||||||
|
"roles",
|
||||||
|
["content_editor", "product_editor"]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("sets user metadata.roles to user that does not have metadata.roles", async () => {
|
||||||
|
await permissionService.grantRole(
|
||||||
|
IdMap.getId("user-without-roles"),
|
||||||
|
"product_editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("user-without-roles"),
|
||||||
|
"roles",
|
||||||
|
["product_editor"]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if user already has role", async () => {
|
||||||
|
try {
|
||||||
|
await permissionService.grantRole(
|
||||||
|
IdMap.getId("permission-user"),
|
||||||
|
"product_editor"
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual("User already has role: product_editor")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("addPermission", () => {
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully adds permission", async () => {
|
||||||
|
const toAdd = {
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/products",
|
||||||
|
}
|
||||||
|
await permissionService.addPermission("product_editor", toAdd)
|
||||||
|
|
||||||
|
expect(RoleModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||||
|
expect(RoleModelMock.updateOne).toHaveBeenCalledWith(
|
||||||
|
{ _id: IdMap.getId("product_editor") },
|
||||||
|
{
|
||||||
|
$push: { permissions: { method: "POST", endpoint: "/products" } },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if permission is not valid", async () => {
|
||||||
|
const toAdd = {
|
||||||
|
method: "POST",
|
||||||
|
endpoint: 1234,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await permissionService.addPermission("product_editor", toAdd)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual("Permission is not valid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("removePermission", () => {
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
})
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully removes permission", async () => {
|
||||||
|
const toRemove = {
|
||||||
|
method: "POST",
|
||||||
|
endpoint: "/products",
|
||||||
|
}
|
||||||
|
await permissionService.removePermission("product_editor", toRemove)
|
||||||
|
|
||||||
|
expect(RoleModelMock.updateOne).toHaveBeenCalledTimes(1)
|
||||||
|
expect(RoleModelMock.updateOne).toHaveBeenCalledWith(
|
||||||
|
{ _id: IdMap.getId("product_editor") },
|
||||||
|
{
|
||||||
|
$pull: { permissions: { method: "POST", endpoint: "/products" } },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("throws if permission is not valid", async () => {
|
||||||
|
const update = {
|
||||||
|
method: "FETCH",
|
||||||
|
endpoint: "/cart",
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await permissionService.addPermission("product_editor", update)
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toEqual("Permission is not valid")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("revokeRole", () => {
|
||||||
|
const setMetadataMock = jest.fn().mockReturnValue(Promise.resolve())
|
||||||
|
const userRetrieveMock = jest.fn().mockReturnValue(
|
||||||
|
Promise.resolve({
|
||||||
|
_id: IdMap.getId("product_editor"),
|
||||||
|
email: "oliver@test.dk",
|
||||||
|
metadata: {
|
||||||
|
roles: ["product_editor"],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
)
|
||||||
|
const permissionService = new PermissionService({
|
||||||
|
roleModel: RoleModelMock,
|
||||||
|
userService: {
|
||||||
|
setMetadata: setMetadataMock,
|
||||||
|
retrieve: userRetrieveMock,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
beforeEach(async () => {
|
||||||
|
jest.clearAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("successfully revokes a role from user", async () => {
|
||||||
|
await permissionService.revokeRole(
|
||||||
|
IdMap.getId("product_editor"),
|
||||||
|
"product_editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledTimes(1)
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledWith(
|
||||||
|
IdMap.getId("product_editor"),
|
||||||
|
"roles",
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("succeeds idempotently if user does not have the role to delete", async () => {
|
||||||
|
await permissionService.revokeRole(
|
||||||
|
IdMap.getId("product_editor"),
|
||||||
|
"content_editor"
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(setMetadataMock).toHaveBeenCalledTimes(0)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
import { BaseService } from "medusa-interfaces"
|
||||||
|
import { Validator, MedusaError } from "medusa-core-utils"
|
||||||
|
|
||||||
|
class PermissionService extends BaseService {
|
||||||
|
constructor({ userService, roleModel }) {
|
||||||
|
super()
|
||||||
|
|
||||||
|
/** @private @const {UserService} */
|
||||||
|
this.userService_ = userService
|
||||||
|
|
||||||
|
/** @private @const {RoleModel} */
|
||||||
|
this.roleModel_ = roleModel
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePermission_(permission) {
|
||||||
|
const schema = Validator.object({
|
||||||
|
method: Validator.string().valid(
|
||||||
|
"POST",
|
||||||
|
"GET",
|
||||||
|
"PUT",
|
||||||
|
"PATCH",
|
||||||
|
"DELETE",
|
||||||
|
"CONNECT",
|
||||||
|
"OPTIONS",
|
||||||
|
"HEAD",
|
||||||
|
"TRACE"
|
||||||
|
),
|
||||||
|
endpoint: Validator.string(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { value, error } = schema.validate(permission)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
"Permission is not valid"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
async retrieveRole(name) {
|
||||||
|
const role = await this.roleModel_.findOne({ name }).catch((err) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!role) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`${name} does not exist. Use method createRole to create it.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return role
|
||||||
|
}
|
||||||
|
|
||||||
|
async hasPermission(user, method, endpoint) {
|
||||||
|
for (let i = 0; i < user.metadata.roles.length; i++) {
|
||||||
|
const role = user.metadata.roles[i]
|
||||||
|
const permissions = await this.retrieveRole(role)
|
||||||
|
return permissions.permissions.some((action) => {
|
||||||
|
return action.method === method && action.endpoint === endpoint
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
async createRole(roleName, permissions) {
|
||||||
|
const validatedPermissions = permissions.map((permission) =>
|
||||||
|
this.validatePermission_(permission)
|
||||||
|
)
|
||||||
|
|
||||||
|
return this.retrieveRole(roleName)
|
||||||
|
.then((role) => {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
`${role.name} already exists`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
if (error.name === MedusaError.Types.NOT_FOUND) {
|
||||||
|
return this.roleModel_.create({
|
||||||
|
name: roleName,
|
||||||
|
permissions: validatedPermissions,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteRole(roleName) {
|
||||||
|
const role = await this.retrieve(roleName)
|
||||||
|
// Delete is idempotent, but we return a promise to allow then-chaining
|
||||||
|
if (!role) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.roleModel_
|
||||||
|
.deleteOne({
|
||||||
|
_id: role._id,
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
throw new MedusaError(MedusaError.Types.DB_ERROR, err.message)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async addPermission(roleName, permission) {
|
||||||
|
const role = await this.retrieveRole(roleName)
|
||||||
|
const validatedPermission = this.validatePermission_(permission)
|
||||||
|
|
||||||
|
return this.roleModel_.updateOne(
|
||||||
|
{ _id: role._id },
|
||||||
|
{ $push: { permissions: validatedPermission } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async removePermission(roleName, permission) {
|
||||||
|
const role = await this.retrieveRole(roleName)
|
||||||
|
const validatedPermission = this.validatePermission_(permission)
|
||||||
|
|
||||||
|
return this.roleModel_.updateOne(
|
||||||
|
{ _id: role._id },
|
||||||
|
{ $pull: { permissions: validatedPermission } }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async grantRole(userId, roleName) {
|
||||||
|
const role = await this.retrieveRole(roleName)
|
||||||
|
const user = await this.userService_.retrieve(userId)
|
||||||
|
|
||||||
|
if (!user.metadata.roles) {
|
||||||
|
return this.userService_.setMetadata(userId, "roles", [roleName])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.metadata.roles.includes(role.name)) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.DB_ERROR,
|
||||||
|
`User already has role: ${role.name}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.metadata.roles.push(roleName)
|
||||||
|
return this.userService_.setMetadata(userId, "roles", user.metadata.roles)
|
||||||
|
}
|
||||||
|
|
||||||
|
async revokeRole(userId, roleName) {
|
||||||
|
const user = await this.userService_.retrieve(userId)
|
||||||
|
|
||||||
|
if (!user.metadata.roles || !user.metadata.roles.includes(roleName)) {
|
||||||
|
// revokeRole is idempotent, we return a promise to allow then-chaining
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
// remove role from metadata.roles
|
||||||
|
const newRoles = user.metadata.roles.filter((r) => r !== roleName)
|
||||||
|
|
||||||
|
return this.userService_.setMetadata(userId, "roles", newRoles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PermissionService
|
||||||
Reference in new issue
Block a user