Adds medusa-plugin-permissions (#12)
This commit is contained in:
1 parent
bc5dc5fab4
commit
f6d5180e5f
13 files changed
+666
-1
No files matched your search
@@ -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
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": false,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"trailingComma": "es5"
|
"trailingComma": "es5"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -6,6 +6,11 @@ export const users = {
|
|||||||
email: "oliver@medusa.test",
|
email: "oliver@medusa.test",
|
||||||
passwordHash: "123456789",
|
passwordHash: "123456789",
|
||||||
},
|
},
|
||||||
|
permissionUser: {
|
||||||
|
_id: IdMap.getId("permissions-user"),
|
||||||
|
email: "oliver@medusa.com",
|
||||||
|
passwordHash: "123456789",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserModelMock = {
|
export const UserModelMock = {
|
||||||
@@ -18,6 +23,9 @@ export const UserModelMock = {
|
|||||||
if (query._id === IdMap.getId("test-user")) {
|
if (query._id === IdMap.getId("test-user")) {
|
||||||
return Promise.resolve(users.testUser)
|
return Promise.resolve(users.testUser)
|
||||||
}
|
}
|
||||||
|
if (query._id === IdMap.getId("permission-user")) {
|
||||||
|
return Promise.resolve(users.permissionUser)
|
||||||
|
}
|
||||||
return Promise.resolve(undefined)
|
return Promise.resolve(undefined)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user