[medusa-interfaces] : Adds decorator functionality to BaseService (#39)
Plugins and projects can add decorators in services. E.g. if a plugin needs to load some additional information on carts the plugin can register a decorator via: `cartService.addDecorator(someFunc)` which will be available later through `cartService.runDecorators()`.
This commit is contained in:
9
packages/medusa-interfaces/.eslintrc
Normal file
9
packages/medusa-interfaces/.eslintrc
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"plugins": ["prettier"],
|
||||
"extends": ["prettier"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"semi": "error",
|
||||
"no-unused-expressions": "true"
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,8 @@
|
||||
"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__"
|
||||
"watch": "babel -w src --out-dir dist/ --ignore **/__tests__",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "Sebastian Rindom",
|
||||
"license": "AGPL-3.0-or-later",
|
||||
@@ -21,8 +22,11 @@
|
||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
||||
"@babel/plugin-transform-runtime": "^7.7.6",
|
||||
"@babel/preset-env": "^7.7.5",
|
||||
"@babel/runtime": "^7.9.6",
|
||||
"cross-env": "^5.2.1",
|
||||
"eslint": "^6.8.0"
|
||||
"eslint": "^6.8.0",
|
||||
"jest": "^25.5.0",
|
||||
"prettier": "^1.19.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"mongoose": "^5.8.0"
|
||||
|
||||
66
packages/medusa-interfaces/src/__tests__/base-service.js
Normal file
66
packages/medusa-interfaces/src/__tests__/base-service.js
Normal file
@@ -0,0 +1,66 @@
|
||||
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,5 +2,35 @@
|
||||
* Common functionality for Services
|
||||
* @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
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"plugins": ["@babel/plugin-proposal-class-properties"],
|
||||
"presets": ["@babel/preset-env"],
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": ["@babel/plugin-transform-runtime"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
dist/
|
||||
node_modules/
|
||||
.DS_store
|
||||
.env*
|
||||
/*.js
|
||||
!index.js
|
||||
yarn.lock
|
||||
yarn-error.log
|
||||
@@ -1,35 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
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()
|
||||
}),
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
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
|
||||
@@ -1,6 +0,0 @@
|
||||
import mongoose from "mongoose"
|
||||
|
||||
export default new mongoose.Schema({
|
||||
method: { type: String },
|
||||
endpoint: { type: String },
|
||||
})
|
||||
@@ -1,15 +0,0 @@
|
||||
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
|
||||
@@ -1,354 +0,0 @@
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,161 +0,0 @@
|
||||
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