1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-15 09:34:13 +00:00

Resolve "Show disabled RBAC roles when not available"

This commit is contained in:
Eimantas Stonys 2024-04-05 15:45:23 +00:00
parent 6cd0d63bb4
commit a602bb1ea7
19 changed files with 560 additions and 57 deletions

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Display unavailable advanced/enterprise roles even if the user isn't on one of these plans",
"issue_number": 1313,
"bullet_points": [],
"created_at": "2024-03-27"
}

View file

@ -12,6 +12,7 @@
ref="editRoleContext"
:subject="rowSanitised"
:roles="roles"
:workspace="workspace"
role-value-column="permissions"
@update-role="roleUpdate($event)"
></EditRoleContext>
@ -51,7 +52,9 @@ export default {
rowSanitised() {
return {
...this.row,
permissions: this.roles.some(({ uid }) => uid === this.row.permissions)
permissions: this.roles.some(
(role) => role.uid === this.row.permissions
)
? this.row.permissions
: 'BUILDER',
}

View file

@ -36,9 +36,6 @@ export default {
role() {
return this.roles.find((r) => r.uid === this.roleUID)
},
roleIsBillable() {
return this?.role.isBillable
},
workspace() {
return this.$store.getters['workspace/get'](
this.column.additionalProps.workspaceId

View file

@ -1,24 +1,53 @@
<template>
<div>
<a class="context__menu-item-link" @click="() => $refs.modal.show()">
<i class="context__menu-item-icon iconoir-community"></i
>{{ $t('memberRolesDatabaseContexItem.label') }}</a
<a
class="context__menu-item-link"
@click="
() => {
if (deactivated) {
$refs.premiumModal.show()
} else {
$refs.memberRolesModal.show()
}
}
"
>
<MemberRolesModal ref="modal" :database="application" />
<i class="context__menu-item-icon iconoir-community"></i>
{{ $t('memberRolesDatabaseContexItem.label') }}
<div v-if="deactivated" class="deactivated-label">
<i class="iconoir-lock"></i>
</div>
</a>
<MemberRolesModal ref="memberRolesModal" :database="application" />
<PremiumModal
ref="premiumModal"
:name="$t('memberRolesDatabaseContexItem.additionalRoles')"
:workspace="application.workspace"
></PremiumModal>
</div>
</template>
<script>
import MemberRolesModal from '@baserow_enterprise/components/member-roles/MemberRolesModal'
import EnterpriseFeatures from '@baserow_enterprise/features'
import PremiumModal from '@baserow_premium/components/PremiumModal'
export default {
name: 'MemberRolesDatabaseContextItem',
components: { MemberRolesModal },
components: { MemberRolesModal, PremiumModal },
props: {
application: {
type: Object,
required: true,
},
},
computed: {
deactivated() {
return !this.$hasFeature(
EnterpriseFeatures.RBAC,
this.application.workspace.id
)
},
},
}
</script>

View file

@ -1,21 +1,46 @@
<template>
<div>
<div class="context__menu-item" @click="() => $refs.modal.show()">
<a class="context__menu-item-link">
<i class="context__menu-item-icon iconoir-community"></i
>{{ $t('memberRolesTableContexItem.label') }}</a
<div class="context__menu-item">
<a
class="context__menu-item-link"
@click="
() => {
if (deactivated) {
$refs.premiumModal.show()
} else {
$refs.memberRolesModal.show()
}
}
"
>
<i class="context__menu-item-icon iconoir-community"></i>
{{ $t('memberRolesDatabaseContexItem.label') }}
<div v-if="deactivated" class="deactivated-label">
<i class="iconoir-lock"></i>
</div>
</a>
</div>
<MemberRolesModal ref="modal" :database="database" :table="table" />
<MemberRolesModal
ref="memberRolesModal"
:database="database"
:table="table"
/>
<PremiumModal
ref="premiumModal"
:name="$t('memberRolesTableContexItem.additionalRoles')"
:workspace="database.workspace"
></PremiumModal>
</div>
</template>
<script>
import MemberRolesModal from '@baserow_enterprise/components/member-roles/MemberRolesModal'
import EnterpriseFeatures from '@baserow_enterprise/features'
import PremiumModal from '@baserow_premium/components/PremiumModal'
export default {
name: 'MemberRolesTableContextItem',
components: { MemberRolesModal },
components: { MemberRolesModal, PremiumModal },
props: {
table: {
type: Object,
@ -26,5 +51,13 @@ export default {
required: true,
},
},
computed: {
deactivated() {
return !this.$hasFeature(
EnterpriseFeatures.RBAC,
this.database.workspace.id
)
},
},
}
</script>

View file

@ -49,7 +49,7 @@ export default {
},
methods: {
roleUpdated({ uid }) {
const role = this.roles.find((role) => role.uid === uid)
const role = this.roles.find((role) => role.getUid() === uid)
this.$emit('input', role)
},
},

View file

@ -209,10 +209,12 @@
"subLabel": "{totalUserAmount} workspace members"
},
"memberRolesDatabaseContexItem": {
"label": "Manage members"
"label": "Manage members",
"additionalRoles": "Additional roles"
},
"memberRolesTableContexItem": {
"label": "Manage members"
"label": "Manage members",
"additionalRoles": "Additional roles"
},
"memberRolesMembersList": {
"remove": "Remove",

View file

@ -28,6 +28,16 @@ import { EnterprisePlugin } from '@baserow_enterprise/plugins'
import { LocalBaserowUserSourceType } from '@baserow_enterprise/integrations/userSourceTypes'
import { LocalBaserowPasswordAppAuthProviderType } from '@baserow_enterprise/integrations/appAuthProviderTypes'
import { AuthFormElementType } from '@baserow_enterprise/builder/elementTypes'
import {
EnterpriseAdminRoleType,
EnterpriseMemberRoleType,
EnterpriseBuilderRoleType,
EnterpriseEditorRoleType,
EnterpriseCommenterRoleType,
EnterpriseViewerRoleType,
NoAccessRoleType,
NoRoleLowPriorityRoleType,
} from '@baserow_enterprise/roleTypes'
export default (context) => {
const { app, isDev, store } = context
@ -97,5 +107,14 @@ export default (context) => {
new LocalBaserowPasswordAppAuthProviderType(context)
)
app.$registry.register('roles', new EnterpriseAdminRoleType(context))
app.$registry.register('roles', new EnterpriseMemberRoleType(context))
app.$registry.register('roles', new EnterpriseBuilderRoleType(context))
app.$registry.register('roles', new EnterpriseEditorRoleType(context))
app.$registry.register('roles', new EnterpriseCommenterRoleType(context))
app.$registry.register('roles', new EnterpriseViewerRoleType(context))
app.$registry.register('roles', new NoAccessRoleType(context))
app.$registry.register('roles', new NoRoleLowPriorityRoleType(context))
app.$registry.register('element', new AuthFormElementType(context))
}

View file

@ -29,7 +29,6 @@ export class EnterprisePlugin extends BaserowPlugin {
getAdditionalDatabaseContextComponents(workspace, database) {
if (
this.app.$hasFeature(EnterpriseFeatures.RBAC, workspace.id) &&
this.app.$hasPermission('application.read_role', database, workspace.id)
) {
return [MemberRolesDatabaseContextItem]
@ -40,7 +39,6 @@ export class EnterprisePlugin extends BaserowPlugin {
getAdditionalTableContextComponents(workspace, table) {
if (
this.app.$hasFeature(EnterpriseFeatures.RBAC, workspace.id) &&
this.app.$hasPermission('database.table.read_role', table, workspace.id)
) {
return [MemberRolesTableContextItem]

View file

@ -0,0 +1,243 @@
import {
AdminRoleType,
MemberRoleType,
} from '@baserow/modules/database/roleTypes'
import PremiumModal from '@baserow_premium/components/PremiumModal'
import EnterpriseFeatures from '@baserow_enterprise/features'
export class EnterpriseAdminRoleType extends AdminRoleType {
showIsBillable(workspaceId) {
return this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getIsBillable(workspaceId) {
return this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
}
export class EnterpriseMemberRoleType extends MemberRoleType {
// This role doesn't exist in enterprise, so we hide it completely.
isVisible(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
}
export class EnterpriseBuilderRoleType extends MemberRoleType {
getType() {
return 'builder'
}
getUid() {
return 'BUILDER'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.builder.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.builder.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return true
}
isVisible(workspaceId) {
return this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal() {
return PremiumModal
}
}
export class EnterpriseEditorRoleType extends MemberRoleType {
getType() {
return 'editor'
}
getUid() {
return 'EDITOR'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.editor.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.editor.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return true
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal(workspaceId) {
return PremiumModal
}
}
export class EnterpriseCommenterRoleType extends MemberRoleType {
getType() {
return 'commenter'
}
getUid() {
return 'COMMENTER'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.commenter.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.commenter.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return false
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal(workspaceId) {
return PremiumModal
}
}
export class EnterpriseViewerRoleType extends MemberRoleType {
getType() {
return 'viewer'
}
getUid() {
return 'VIEWER'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.viewer.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.viewer.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return false
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal(workspaceId) {
return PremiumModal
}
}
export class NoAccessRoleType extends MemberRoleType {
getType() {
return 'noAccess'
}
getUid() {
return 'NO_ACCESS'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.noAccess.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.noAccess.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return false
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal(workspaceId) {
return PremiumModal
}
}
export class NoRoleLowPriorityRoleType extends MemberRoleType {
getType() {
return 'noRoleLowPriority'
}
getUid() {
return 'NO_ROLE_LOW_PRIORITY'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.noRoleLowPriority.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.noRoleLowPriority.description')
}
showIsBillable(workspaceId) {
return true
}
getIsBillable(workspaceId) {
return false
}
isDeactivated(workspaceId) {
return !this.app.$hasFeature(EnterpriseFeatures.RBAC, workspaceId)
}
getDeactivatedClickModal(workspaceId) {
return PremiumModal
}
}

View file

@ -21,24 +21,44 @@
</div>
</div>
<ul class="context__menu context__menu--can-be-active">
<li v-for="role in roles" :key="role.uid" class="context__menu-item">
<li
v-for="(role, index) in visibleRoles"
:key="index"
class="context__menu-item"
>
<a
class="context__menu-item-link context__menu-item-link--with-desc"
:class="{ active: subject[roleValueColumn] === role.uid }"
@click="roleUpdate(role.uid, subject)"
:class="{
active: subject[roleValueColumn] === role.uid,
disabled: role.isDeactivated,
}"
@click="
!role.isDeactivated
? roleUpdate(role.uid, subject)
: clickOnDeactivatedItem(role.uid)
"
>
<span class="context__menu-item-title">
{{ role.name }}
<Badge v-if="role.isBillable" color="cyan" size="small" bold
<Badge
v-if="role.showIsBillable && role.isBillable"
color="cyan"
size="small"
bold
>{{ $t('common.billable') }}
</Badge>
<Badge
v-else-if="!role.isBillable && atLeastOneBillableRole"
v-else-if="
role.showIsBillable &&
!role.isBillable &&
atLeastOneBillableRole
"
color="yellow"
size="small"
bold
>{{ $t('common.free') }}
</Badge>
<i v-if="role.isDeactivated" class="iconoir-lock"></i>
</span>
<div v-if="role.description" class="context__menu-item-description">
{{ role.description }}
@ -48,6 +68,13 @@
class="context__menu-active-icon iconoir-check"
></i>
</a>
<component
:is="deactivatedClickModal(role)"
:ref="'deactivatedClickModal-' + role.uid"
:v-if="deactivatedClickModal(role)"
:name="$t('editRoleContext.additionalRoles')"
:workspace="workspace"
></component>
</li>
<li
v-if="allowRemovingRole"
@ -71,6 +98,11 @@ export default {
name: 'EditRoleContext',
mixins: [context],
props: {
workspace: {
type: Object,
required: false,
default: null,
},
subject: {
required: true,
type: Object,
@ -90,6 +122,9 @@ export default {
},
},
computed: {
visibleRoles() {
return this.roles.filter((role) => role.isVisible)
},
atLeastOneBillableRole() {
return this.roles.some((role) => role.isBillable)
},
@ -103,6 +138,15 @@ export default {
this.$emit('update-role', { uid: roleNew, subject })
this.hide()
},
deactivatedClickModal(role) {
const allRoles = Object.values(this.$registry.getAll('roles'))
return allRoles
.find((r) => r.getUid() === role.uid)
.getDeactivatedClickModal()
},
clickOnDeactivatedItem(value) {
this.$refs[`deactivatedClickModal-${value}`][0].show()
},
},
}
</script>

View file

@ -42,6 +42,7 @@
ref="editRoleContext"
:subject="editRoleMember"
:roles="roles"
:workspace="workspace"
@update-role="roleUpdate($event)"
></EditRoleContext>
</template>

View file

@ -28,27 +28,47 @@
v-model="values.permissions"
class="group-invite-form__role-selector-dropdown"
:show-search="false"
:fixed-items="true"
small
>
<DropdownItem
v-for="role in roles"
:key="role.uid"
v-for="(role, index) in roles"
:key="index"
:ref="'role' + role.uid"
:name="role.name"
:value="role.uid"
:disabled="role.isDeactivated"
:description="role.description"
@click="clickOnDeactivatedItem($event)"
>
{{ role.name }}
<Badge v-if="role.isBillable" color="cyan" size="small" bold
<Badge
v-if="role.showIsBillable && role.isBillable"
color="cyan"
size="small"
bold
>{{ $t('common.billable') }}
</Badge>
<Badge
v-else-if="!role.isBillable && atLeastOneBillableRole"
v-else-if="
role.showIsBillable &&
!role.isBillable &&
atLeastOneBillableRole
"
color="yellow"
size="small"
bold
class="margin-left-1"
>{{ $t('common.free') }}
</Badge>
<i v-if="role.isDeactivated" class="iconoir-lock"></i>
<component
:is="deactivatedClickModal(role)"
:ref="'deactivatedClickModal-' + role.uid"
:v-if="deactivatedClickModal(role)"
:name="$t('workspaceInviteForm.additionalRoles')"
:workspace="workspace"
></component>
</DropdownItem>
</Dropdown>
</div>
@ -113,10 +133,11 @@ export default {
return MESSAGE_MAX_LENGTH
},
roles() {
return this.workspace._.roles
return this.workspace._.roles.filter((role) => role.isVisible)
},
defaultRole() {
return this.roles.length > 0 ? this.roles[this.roles.length - 1] : null
const activeRoles = this.roles.filter((role) => !role.isDeactivated)
return activeRoles.length > 0 ? activeRoles[activeRoles.length - 1] : null
},
atLeastOneBillableRole() {
return this.roles.some((role) => role.isBillable)
@ -130,6 +151,20 @@ export default {
immediate: true,
},
},
methods: {
deactivatedClickModal(role) {
const allRoles = Object.values(this.$registry.getAll('roles'))
return allRoles
.find((r) => r.getUid() === role.uid)
.getDeactivatedClickModal()
},
clickOnDeactivatedItem(value) {
const role = this.roles.find((role) => role.uid === value)
if (role && role.isDeactivated) {
this.$refs[`deactivatedClickModal-${value}`][0].show()
}
},
},
validations: {
values: {
email: { required, email },

View file

@ -134,7 +134,8 @@
"invitationFormTitle": "Invite by email",
"optionalMessagePlaceholder": "Optional message",
"errorInvalidEmail": "Please enter a valid e-mail address.",
"errorTooLongMessage": "Messages are limited to {amount} characters."
"errorTooLongMessage": "Messages are limited to {amount} characters.",
"additionalRoles": "Additional roles"
},
"workspacesContext": {
"search": "Search workspaces",
@ -528,7 +529,8 @@
"remove": "Remove"
},
"editRoleContext": {
"billableRolesLink": "Billable roles documentation"
"billableRolesLink": "Billable roles documentation",
"additionalRoles": "Additional roles"
},
"highestPaidRoleField": {
"billable": "Billable"

View file

@ -49,6 +49,7 @@ export default {
if (!disabled) {
this.$parent.select(value)
}
this.$emit('click', value)
},
hover(value, disabled) {
if (!disabled && this.$parent.hover !== value) {

View file

@ -71,6 +71,11 @@ import {
import priorityBus from '@baserow/modules/core/plugins/priorityBus'
import {
AdminRoleType,
MemberRoleType,
} from '@baserow/modules/database/roleTypes'
export default (context, inject) => {
const { store, isDev, app } = context
inject('bus', new Vue())
@ -106,6 +111,7 @@ export default (context, inject) => {
registry.registerNamespace('service')
registry.registerNamespace('userSource')
registry.registerNamespace('appAuthProvider')
registry.registerNamespace('roles')
registry.register('settings', new AccountSettingsType(context))
registry.register('settings', new PasswordSettingsType(context))
@ -170,6 +176,9 @@ export default (context, inject) => {
registry.register('runtimeFormulaFunction', new RuntimeGet(context))
registry.register('runtimeFormulaFunction', new RuntimeAdd(context))
registry.register('roles', new AdminRoleType(context))
registry.register('roles', new MemberRoleType(context))
// Notification types
registry.register(
'notification',

View file

@ -1,30 +1,29 @@
export default (client, $hasFeature) => {
export default (client, $hasFeature, $registry) => {
return {
// TODO implement once endpoint exists
get(workspace) {
if ($hasFeature('RBAC', workspace.id)) {
return {
data: [
{ uid: 'ADMIN', isBillable: true },
{ uid: 'BUILDER', isBillable: true },
{ uid: 'EDITOR', isBillable: true },
{ uid: 'COMMENTER', isBillable: false },
{ uid: 'VIEWER', isBillable: false },
{ uid: 'NO_ACCESS', isBillable: false },
{
uid: 'NO_ROLE_LOW_PRIORITY',
allowed_scope_types: ['workspace'],
allowed_subject_types: ['auth.User'],
isBillable: false,
},
],
}
}
return {
data: [
{ uid: 'ADMIN', isBillable: false },
{ uid: 'MEMBER', isBillable: false },
],
data: Object.values($registry.getAll('roles')).map((role) =>
role.getUid() === 'NO_ROLE_LOW_PRIORITY'
? {
uid: role.getUid(),
description: role.getDescription(),
showIsBillable: role.showIsBillable(workspace.id),
isBillable: role.getIsBillable(workspace.id),
isVisible: role.isVisible(workspace.id),
isDeactivated: role.isDeactivated(),
allowed_scope_types: ['workspace'],
allowed_subject_types: ['auth.User'],
}
: {
uid: role.getUid(),
description: role.getDescription(),
showIsBillable: role.showIsBillable(workspace.id),
isBillable: role.getIsBillable(workspace.id),
isVisible: role.isVisible(workspace.id),
isDeactivated: role.isDeactivated(),
}
),
}
},
}

View file

@ -355,7 +355,8 @@ export const actions = {
try {
const { data } = await RolesService(
this.$client,
this.app.$hasFeature
this.app.$hasFeature,
this.$registry
).get(workspace)
const translatedRoles = appendRoleTranslations(data, this.app.$registry)
commit('SET_ROLES', { workspaceId: workspace.id, roles: translatedRoles })

View file

@ -0,0 +1,80 @@
import { Registerable } from '@baserow/modules/core/registry'
class RoleType extends Registerable {
getUid() {
return null
}
getName() {
return null
}
getDescription() {
return null
}
// Indicates weather to show the role as billable/non-billable or show nothing.
showIsBillable(workspaceId) {
return false
}
// Indicates whether the role is billable.
getIsBillable(workspaceId) {
return false
}
// Indicates whether the role should be visible in the list.
isVisible(workspaceId) {
return true
}
// Indicates whether the role is visible, but in a deactivated state.
isDeactivated(workspaceId) {
return false
}
// The modal component that must be shown when a deactivated role is clicked.
getDeactivatedClickModal(workspaceId) {
return null
}
}
export class AdminRoleType extends RoleType {
getType() {
return 'admin'
}
getUid() {
return 'ADMIN'
}
getName() {
const { i18n } = this.app
return i18n.t('roles.admin.name')
}
getDescription() {
const { i18n } = this.app
return i18n.t('roles.admin.description')
}
}
export class MemberRoleType extends RoleType {
getType() {
return 'member'
}
getUid() {
return 'MEMBER'
}
getName() {
const { i18n } = this.app
return i18n.t('permission.member')
}
getDescription() {
const { i18n } = this.app
return i18n.t('permission.memberDescription')
}
}