Merge branch 'develop' into design-section-feature-branch
This commit is contained in:
commit
a6e000ed34
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.9.30-alpha.11",
|
"version": "2.9.30-alpha.12",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -102,6 +102,10 @@ export const useAppBuilders = () => {
|
||||||
return useFeature(Feature.APP_BUILDERS)
|
return useFeature(Feature.APP_BUILDERS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useViewPermissions = () => {
|
||||||
|
return useFeature(Feature.VIEW_PERMISSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
// QUOTAS
|
// QUOTAS
|
||||||
|
|
||||||
export const setAutomationLogsQuota = (value: number) => {
|
export const setAutomationLogsQuota = (value: number) => {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit af8a40089809485712c7ef626d9e04090ef09975
|
Subproject commit b7815e099bbd5e1410185c464dbd54f7287e732f
|
|
@ -1,11 +1,12 @@
|
||||||
import { permissions, roles, context } from "@budibase/backend-core"
|
import { permissions, roles, context, HTTPError } from "@budibase/backend-core"
|
||||||
|
import { UserCtx, Database, Role, PermissionLevel } from "@budibase/types"
|
||||||
import { getRoleParams } from "../../db/utils"
|
import { getRoleParams } from "../../db/utils"
|
||||||
import {
|
import {
|
||||||
CURRENTLY_SUPPORTED_LEVELS,
|
CURRENTLY_SUPPORTED_LEVELS,
|
||||||
getBasePermissions,
|
getBasePermissions,
|
||||||
} from "../../utilities/security"
|
} from "../../utilities/security"
|
||||||
import { removeFromArray } from "../../utilities"
|
import { removeFromArray } from "../../utilities"
|
||||||
import { UserCtx, Database, Role } from "@budibase/types"
|
import sdk from "../../sdk"
|
||||||
|
|
||||||
const PermissionUpdateType = {
|
const PermissionUpdateType = {
|
||||||
REMOVE: "remove",
|
REMOVE: "remove",
|
||||||
|
@ -25,14 +26,25 @@ async function getAllDBRoles(db: Database) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updatePermissionOnRole(
|
async function updatePermissionOnRole(
|
||||||
appId: string,
|
|
||||||
{
|
{
|
||||||
roleId,
|
roleId,
|
||||||
resourceId,
|
resourceId,
|
||||||
level,
|
level,
|
||||||
}: { roleId: string; resourceId: string; level: string },
|
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||||
updateType: string
|
updateType: string
|
||||||
) {
|
) {
|
||||||
|
const allowedAction = await sdk.permissions.resourceActionAllowed({
|
||||||
|
resourceId,
|
||||||
|
level,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!allowedAction.allowed) {
|
||||||
|
throw new HTTPError(
|
||||||
|
`You are not allowed to '${allowedAction.level}' the resource type '${allowedAction.resourceType}'`,
|
||||||
|
403
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const db = context.getAppDB()
|
const db = context.getAppDB()
|
||||||
const remove = updateType === PermissionUpdateType.REMOVE
|
const remove = updateType === PermissionUpdateType.REMOVE
|
||||||
const isABuiltin = roles.isBuiltin(roleId)
|
const isABuiltin = roles.isBuiltin(roleId)
|
||||||
|
@ -163,16 +175,11 @@ export async function getResourcePerms(ctx: UserCtx) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addPermission(ctx: UserCtx) {
|
export async function addPermission(ctx: UserCtx) {
|
||||||
ctx.body = await updatePermissionOnRole(
|
ctx.body = await updatePermissionOnRole(ctx.params, PermissionUpdateType.ADD)
|
||||||
ctx.appId,
|
|
||||||
ctx.params,
|
|
||||||
PermissionUpdateType.ADD
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removePermission(ctx: UserCtx) {
|
export async function removePermission(ctx: UserCtx) {
|
||||||
ctx.body = await updatePermissionOnRole(
|
ctx.body = await updatePermissionOnRole(
|
||||||
ctx.appId,
|
|
||||||
ctx.params,
|
ctx.params,
|
||||||
PermissionUpdateType.REMOVE
|
PermissionUpdateType.REMOVE
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,5 +1,20 @@
|
||||||
const { roles } = require("@budibase/backend-core")
|
const mockedSdk = sdk.permissions as jest.Mocked<typeof sdk.permissions>
|
||||||
const setup = require("./utilities")
|
jest.mock("../../../sdk/app/permissions", () => ({
|
||||||
|
resourceActionAllowed: jest.fn(),
|
||||||
|
}))
|
||||||
|
|
||||||
|
import sdk from "../../../sdk"
|
||||||
|
|
||||||
|
import { roles } from "@budibase/backend-core"
|
||||||
|
import {
|
||||||
|
Document,
|
||||||
|
DocumentType,
|
||||||
|
PermissionLevel,
|
||||||
|
Row,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import * as setup from "./utilities"
|
||||||
|
|
||||||
const { basicRow } = setup.structures
|
const { basicRow } = setup.structures
|
||||||
const { BUILTIN_ROLE_IDS } = roles
|
const { BUILTIN_ROLE_IDS } = roles
|
||||||
|
|
||||||
|
@ -9,29 +24,27 @@ const STD_ROLE_ID = BUILTIN_ROLE_IDS.PUBLIC
|
||||||
describe("/permission", () => {
|
describe("/permission", () => {
|
||||||
let request = setup.getRequest()
|
let request = setup.getRequest()
|
||||||
let config = setup.getConfig()
|
let config = setup.getConfig()
|
||||||
let table
|
let table: Table & { _id: string }
|
||||||
let perms
|
let perms: Document[]
|
||||||
let row
|
let row: Row
|
||||||
|
|
||||||
afterAll(setup.afterAll)
|
afterAll(setup.afterAll)
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await config.init()
|
await config.init()
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
table = await config.createTable()
|
|
||||||
row = await config.createRow()
|
|
||||||
perms = await config.addPermission(STD_ROLE_ID, table._id)
|
|
||||||
})
|
|
||||||
|
|
||||||
async function getTablePermissions() {
|
beforeEach(async () => {
|
||||||
return request
|
mockedSdk.resourceActionAllowed.mockResolvedValue({ allowed: true })
|
||||||
.get(`/api/permission/${table._id}`)
|
|
||||||
.set(config.defaultHeaders())
|
table = (await config.createTable()) as typeof table
|
||||||
.expect("Content-Type", /json/)
|
row = await config.createRow()
|
||||||
.expect(200)
|
perms = await config.api.permission.set({
|
||||||
}
|
roleId: STD_ROLE_ID,
|
||||||
|
resourceId: table._id,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("levels", () => {
|
describe("levels", () => {
|
||||||
it("should be able to get levels", async () => {
|
it("should be able to get levels", async () => {
|
||||||
|
@ -65,8 +78,12 @@ describe("/permission", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should get resource permissions with multiple roles", async () => {
|
it("should get resource permissions with multiple roles", async () => {
|
||||||
perms = await config.addPermission(HIGHER_ROLE_ID, table._id, "write")
|
perms = await config.api.permission.set({
|
||||||
const res = await getTablePermissions()
|
roleId: HIGHER_ROLE_ID,
|
||||||
|
resourceId: table._id,
|
||||||
|
level: PermissionLevel.WRITE,
|
||||||
|
})
|
||||||
|
const res = await config.api.permission.get(table._id)
|
||||||
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
expect(res.body["read"]).toEqual(STD_ROLE_ID)
|
||||||
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
expect(res.body["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
const allRes = await request
|
const allRes = await request
|
||||||
|
@ -77,19 +94,59 @@ describe("/permission", () => {
|
||||||
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
expect(allRes.body[table._id]["write"]).toEqual(HIGHER_ROLE_ID)
|
||||||
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
expect(allRes.body[table._id]["read"]).toEqual(STD_ROLE_ID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("throw forbidden if the action is not allowed for the resource", async () => {
|
||||||
|
mockedSdk.resourceActionAllowed.mockResolvedValue({
|
||||||
|
allowed: false,
|
||||||
|
resourceType: DocumentType.DATASOURCE,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.permission.set(
|
||||||
|
{
|
||||||
|
roleId: STD_ROLE_ID,
|
||||||
|
resourceId: table._id,
|
||||||
|
level: PermissionLevel.EXECUTE,
|
||||||
|
},
|
||||||
|
{ expectStatus: 403 }
|
||||||
|
)
|
||||||
|
expect(response.message).toEqual(
|
||||||
|
"You are not allowed to 'read' the resource type 'datasource'"
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("remove", () => {
|
describe("remove", () => {
|
||||||
it("should be able to remove the permission", async () => {
|
it("should be able to remove the permission", async () => {
|
||||||
const res = await request
|
const res = await config.api.permission.revoke({
|
||||||
.delete(`/api/permission/${STD_ROLE_ID}/${table._id}/read`)
|
roleId: STD_ROLE_ID,
|
||||||
.set(config.defaultHeaders())
|
resourceId: table._id,
|
||||||
.expect("Content-Type", /json/)
|
level: PermissionLevel.READ,
|
||||||
.expect(200)
|
})
|
||||||
expect(res.body[0]._id).toEqual(STD_ROLE_ID)
|
expect(res.body[0]._id).toEqual(STD_ROLE_ID)
|
||||||
const permsRes = await getTablePermissions()
|
const permsRes = await config.api.permission.get(table._id)
|
||||||
expect(permsRes.body[STD_ROLE_ID]).toBeUndefined()
|
expect(permsRes.body[STD_ROLE_ID]).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("throw forbidden if the action is not allowed for the resource", async () => {
|
||||||
|
mockedSdk.resourceActionAllowed.mockResolvedValue({
|
||||||
|
allowed: false,
|
||||||
|
resourceType: DocumentType.DATASOURCE,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await config.api.permission.revoke(
|
||||||
|
{
|
||||||
|
roleId: STD_ROLE_ID,
|
||||||
|
resourceId: table._id,
|
||||||
|
level: PermissionLevel.EXECUTE,
|
||||||
|
},
|
||||||
|
{ expectStatus: 403 }
|
||||||
|
)
|
||||||
|
expect(response.body.message).toEqual(
|
||||||
|
"You are not allowed to 'read' the resource type 'datasource'"
|
||||||
|
)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("check public user allowed", () => {
|
describe("check public user allowed", () => {
|
||||||
|
@ -124,7 +181,9 @@ describe("/permission", () => {
|
||||||
.expect("Content-Type", /json/)
|
.expect("Content-Type", /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
expect(Array.isArray(res.body)).toEqual(true)
|
expect(Array.isArray(res.body)).toEqual(true)
|
||||||
const publicPerm = res.body.find(perm => perm._id === "public")
|
const publicPerm = res.body.find(
|
||||||
|
(perm: Document) => perm._id === "public"
|
||||||
|
)
|
||||||
expect(publicPerm).toBeDefined()
|
expect(publicPerm).toBeDefined()
|
||||||
expect(publicPerm.permissions).toBeDefined()
|
expect(publicPerm.permissions).toBeDefined()
|
||||||
expect(publicPerm.name).toBeDefined()
|
expect(publicPerm.name).toBeDefined()
|
|
@ -1,5 +1,6 @@
|
||||||
const { roles, events, permissions } = require("@budibase/backend-core")
|
const { roles, events, permissions } = require("@budibase/backend-core")
|
||||||
const setup = require("./utilities")
|
const setup = require("./utilities")
|
||||||
|
const { PermissionLevel } = require("@budibase/types")
|
||||||
const { basicRole } = setup.structures
|
const { basicRole } = setup.structures
|
||||||
const { BUILTIN_ROLE_IDS } = roles
|
const { BUILTIN_ROLE_IDS } = roles
|
||||||
const { BuiltinPermissionID } = permissions
|
const { BuiltinPermissionID } = permissions
|
||||||
|
@ -16,7 +17,7 @@ describe("/roles", () => {
|
||||||
|
|
||||||
const createRole = async (role) => {
|
const createRole = async (role) => {
|
||||||
if (!role) {
|
if (!role) {
|
||||||
role = basicRole()
|
role = basicRole()
|
||||||
}
|
}
|
||||||
|
|
||||||
return request
|
return request
|
||||||
|
@ -98,7 +99,7 @@ describe("/roles", () => {
|
||||||
|
|
||||||
it("should be able to get the role with a permission added", async () => {
|
it("should be able to get the role with a permission added", async () => {
|
||||||
const table = await config.createTable()
|
const table = await config.createTable()
|
||||||
await config.addPermission(BUILTIN_ROLE_IDS.POWER, table._id)
|
await config.api.permission.set({ roleId: BUILTIN_ROLE_IDS.POWER, resourceId: table._id, level: PermissionLevel.READ })
|
||||||
const res = await request
|
const res = await request
|
||||||
.get(`/api/roles`)
|
.get(`/api/roles`)
|
||||||
.set(config.defaultHeaders())
|
.set(config.defaultHeaders())
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
DocumentType,
|
||||||
|
PermissionLevel,
|
||||||
|
VirtualDocumentType,
|
||||||
|
} from "@budibase/types"
|
||||||
|
import { isViewID } from "../../../db/utils"
|
||||||
|
import { features } from "@budibase/pro"
|
||||||
|
|
||||||
|
type ResourceActionAllowedResult =
|
||||||
|
| { allowed: true }
|
||||||
|
| {
|
||||||
|
allowed: false
|
||||||
|
level: PermissionLevel
|
||||||
|
resourceType: DocumentType | VirtualDocumentType
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resourceActionAllowed({
|
||||||
|
resourceId,
|
||||||
|
level,
|
||||||
|
}: {
|
||||||
|
resourceId: string
|
||||||
|
level: PermissionLevel
|
||||||
|
}): Promise<ResourceActionAllowedResult> {
|
||||||
|
if (!isViewID(resourceId)) {
|
||||||
|
return { allowed: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await features.isViewPermissionEnabled()) {
|
||||||
|
return { allowed: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
allowed: false,
|
||||||
|
level,
|
||||||
|
resourceType: VirtualDocumentType.VIEW,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
import TestConfiguration from "../../../../tests/utilities/TestConfiguration"
|
||||||
|
import { PermissionLevel } from "@budibase/types"
|
||||||
|
import { mocks, structures } from "@budibase/backend-core/tests"
|
||||||
|
import { resourceActionAllowed } from ".."
|
||||||
|
import { generateViewID } from "../../../../db/utils"
|
||||||
|
|
||||||
|
describe("permissions sdk", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
new TestConfiguration()
|
||||||
|
mocks.licenses.useCloudFree()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("resourceActionAllowed", () => {
|
||||||
|
it("non view resources actions are always allowed", async () => {
|
||||||
|
const resourceId = structures.users.user()._id!
|
||||||
|
|
||||||
|
const result = await resourceActionAllowed({
|
||||||
|
resourceId,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({ allowed: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("view resources actions allowed if the feature flag is enabled", async () => {
|
||||||
|
mocks.licenses.useViewPermissions()
|
||||||
|
const resourceId = generateViewID(structures.generator.guid())
|
||||||
|
|
||||||
|
const result = await resourceActionAllowed({
|
||||||
|
resourceId,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({ allowed: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
it("view resources actions allowed if the feature flag is disabled", async () => {
|
||||||
|
const resourceId = generateViewID(structures.generator.guid())
|
||||||
|
|
||||||
|
const result = await resourceActionAllowed({
|
||||||
|
resourceId,
|
||||||
|
level: PermissionLevel.READ,
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
allowed: false,
|
||||||
|
level: "read",
|
||||||
|
resourceType: "view",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -8,6 +8,7 @@ import { default as rows } from "./app/rows"
|
||||||
import { default as users } from "./users"
|
import { default as users } from "./users"
|
||||||
import { default as plugins } from "./plugins"
|
import { default as plugins } from "./plugins"
|
||||||
import * as views from "./app/views"
|
import * as views from "./app/views"
|
||||||
|
import * as permissions from "./app/permissions"
|
||||||
|
|
||||||
const sdk = {
|
const sdk = {
|
||||||
backups,
|
backups,
|
||||||
|
@ -20,6 +21,7 @@ const sdk = {
|
||||||
queries,
|
queries,
|
||||||
plugins,
|
plugins,
|
||||||
views,
|
views,
|
||||||
|
permissions,
|
||||||
}
|
}
|
||||||
|
|
||||||
// default export for TS
|
// default export for TS
|
||||||
|
|
|
@ -620,18 +620,6 @@ class TestConfiguration {
|
||||||
return this._req(config, null, controllers.role.save)
|
return this._req(config, null, controllers.role.save)
|
||||||
}
|
}
|
||||||
|
|
||||||
async addPermission(roleId: string, resourceId: string, level = "read") {
|
|
||||||
return this._req(
|
|
||||||
null,
|
|
||||||
{
|
|
||||||
roleId,
|
|
||||||
resourceId,
|
|
||||||
level,
|
|
||||||
},
|
|
||||||
controllers.perms.addPermission
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// VIEW
|
// VIEW
|
||||||
|
|
||||||
async createView(config?: any) {
|
async createView(config?: any) {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import TestConfiguration from "../TestConfiguration"
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { PermissionAPI } from "./permission"
|
||||||
import { RowAPI } from "./row"
|
import { RowAPI } from "./row"
|
||||||
import { TableAPI } from "./table"
|
import { TableAPI } from "./table"
|
||||||
import { ViewV2API } from "./viewV2"
|
import { ViewV2API } from "./viewV2"
|
||||||
|
@ -7,10 +8,12 @@ export default class API {
|
||||||
table: TableAPI
|
table: TableAPI
|
||||||
viewV2: ViewV2API
|
viewV2: ViewV2API
|
||||||
row: RowAPI
|
row: RowAPI
|
||||||
|
permission: PermissionAPI
|
||||||
|
|
||||||
constructor(config: TestConfiguration) {
|
constructor(config: TestConfiguration) {
|
||||||
this.table = new TableAPI(config)
|
this.table = new TableAPI(config)
|
||||||
this.viewV2 = new ViewV2API(config)
|
this.viewV2 = new ViewV2API(config)
|
||||||
this.row = new RowAPI(config)
|
this.row = new RowAPI(config)
|
||||||
|
this.permission = new PermissionAPI(config)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { AnyDocument, PermissionLevel } from "@budibase/types"
|
||||||
|
import TestConfiguration from "../TestConfiguration"
|
||||||
|
import { TestAPI } from "./base"
|
||||||
|
|
||||||
|
export class PermissionAPI extends TestAPI {
|
||||||
|
constructor(config: TestConfiguration) {
|
||||||
|
super(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
get = async (
|
||||||
|
resourceId: string,
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
) => {
|
||||||
|
return this.request
|
||||||
|
.get(`/api/permission/${resourceId}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
set = async (
|
||||||
|
{
|
||||||
|
roleId,
|
||||||
|
resourceId,
|
||||||
|
level,
|
||||||
|
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
): Promise<any> => {
|
||||||
|
const res = await this.request
|
||||||
|
.post(`/api/permission/${roleId}/${resourceId}/${level}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
return res.body
|
||||||
|
}
|
||||||
|
|
||||||
|
revoke = async (
|
||||||
|
{
|
||||||
|
roleId,
|
||||||
|
resourceId,
|
||||||
|
level,
|
||||||
|
}: { roleId: string; resourceId: string; level: PermissionLevel },
|
||||||
|
{ expectStatus } = { expectStatus: 200 }
|
||||||
|
) => {
|
||||||
|
const res = await this.request
|
||||||
|
.delete(`/api/permission/${roleId}/${resourceId}/${level}`)
|
||||||
|
.set(this.config.defaultHeaders())
|
||||||
|
.expect("Content-Type", /json/)
|
||||||
|
.expect(expectStatus)
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ export enum Feature {
|
||||||
APP_BUILDERS = "appBuilders",
|
APP_BUILDERS = "appBuilders",
|
||||||
OFFLINE = "offline",
|
OFFLINE = "offline",
|
||||||
USER_ROLE_PUBLIC_API = "userRolePublicApi",
|
USER_ROLE_PUBLIC_API = "userRolePublicApi",
|
||||||
|
VIEW_PERMISSIONS = "viewPermission",
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
export type PlanFeatures = { [key in PlanType]: Feature[] | undefined }
|
||||||
|
|
Loading…
Reference in New Issue