Fixes #2603 - Limit access to KB Categories based on roles.
This commit is contained in:
parent
3a4ada93b4
commit
154b3accf9
75 changed files with 2123 additions and 187 deletions
|
@ -11,6 +11,10 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
super
|
||||
@controllerBind('config_update_local', (data) => @configUpdated(data))
|
||||
|
||||
@listenTo App.User.current(), 'refresh', @visibilityMayHaveChanged
|
||||
|
||||
App.Event.bind 'kb_visibility_may_have_changed', @visibilityMayHaveChanged
|
||||
|
||||
if @permissionCheck('knowledge_base.*') and App.Config.get('kb_active')
|
||||
@updateNavMenu()
|
||||
else if App.Config.get('kb_active_publicly')
|
||||
|
@ -51,15 +55,8 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
@loadChange(pushed_data)
|
||||
, 1000, key, 'kb_data_changed_loading')
|
||||
)
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||
return if !@displayingError
|
||||
|
||||
object = @constructor.pickObjectUsing(@lastParams, @)
|
||||
|
||||
if !@objectVisibleInternally(object)
|
||||
return
|
||||
|
||||
@renderControllers(@lastParams)
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @renderAfterChangeLoaded
|
||||
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @renderAfterChangeLoaded
|
||||
|
||||
@checkForUpdates()
|
||||
|
||||
|
@ -112,6 +109,19 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
notifyChangeLoaded: ->
|
||||
App.KnowledgeBase.trigger('kb_data_change_loaded')
|
||||
|
||||
notifyVisibilityChangeLoaded: ->
|
||||
App.KnowledgeBase.trigger('kb_visibility_change_loaded')
|
||||
|
||||
renderAfterChangeLoaded: =>
|
||||
return if !@displayingError
|
||||
|
||||
object = @constructor.pickObjectUsing(@lastParams, @)
|
||||
|
||||
if !@objectVisibleInternally(object)
|
||||
return
|
||||
|
||||
@renderControllers(@lastParams)
|
||||
|
||||
active: (state) ->
|
||||
return @shown if state is undefined
|
||||
@shown = state
|
||||
|
@ -138,6 +148,10 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
if !@permissionCheckRedirect("knowledge_base.#{@requiredPermissionSuffix(params)}")
|
||||
return
|
||||
|
||||
if @loaded && @requiredPermissionSuffix(params) == 'editor' && @access(params) != 'editor'
|
||||
@navigate params.match[0].replace(/\/edit$/, ''), { hideCurrentLocationFromHistory: true }
|
||||
return
|
||||
|
||||
if @loaded && @rendered && @lastParams && !params.knowledge_base_id && @contentController && @kb_locale()?
|
||||
@navigate @lastParams.match[0] , { hideCurrentLocationFromHistory: true }
|
||||
return
|
||||
|
@ -169,6 +183,7 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
@pendingParams = params
|
||||
|
||||
renderScreenErrorInContent: (text) ->
|
||||
@contentController?.releaseController()
|
||||
@contentController = undefined
|
||||
@renderScreenError(detail: text, el: @$('.page-content'))
|
||||
@displayingError = true
|
||||
|
@ -255,12 +270,12 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
kb.kb_locales().filter((elem) => elem.system_locale_id == @lastParams.selectedSystemLocale.id)[0]
|
||||
|
||||
getKnowledgeBase: ->
|
||||
App.KnowledgeBase.find(@lastParams.knowledge_base_id)
|
||||
App.KnowledgeBase.find(@lastParams?.knowledge_base_id)
|
||||
|
||||
fetchAndRender: =>
|
||||
@fetch(true, true)
|
||||
|
||||
fetch: (showLoader, processLoaded) ->
|
||||
fetch: (showLoader, processLoaded, notifyVisibilityChangeLoaded) ->
|
||||
if showLoader
|
||||
@startLoading()
|
||||
|
||||
|
@ -278,6 +293,9 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
|
||||
if processLoaded
|
||||
@processLoaded()
|
||||
|
||||
if notifyVisibilityChangeLoaded
|
||||
@notifyVisibilityChangeLoaded()
|
||||
,
|
||||
error: (xhr) =>
|
||||
if showLoader
|
||||
|
@ -308,10 +326,11 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
App[elem.modelName].find(id)?.remove(clear: true)
|
||||
|
||||
calculateIdsToDelete: (data) ->
|
||||
Object
|
||||
.keys(data)
|
||||
.filter (elem) -> elem.match(/^KnowledgeBase/)
|
||||
App.KnowledgeBase
|
||||
.allKbModelNames()
|
||||
.map (model) ->
|
||||
return {ids : []} if !data[model]
|
||||
|
||||
newIds = Object.keys data[model]
|
||||
oldIds = App[model].all().map (elem) -> elem.id
|
||||
diff = oldIds.filter (elem) -> !_.includes(newIds, String(elem))
|
||||
|
@ -339,8 +358,13 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
parentController: @
|
||||
)
|
||||
|
||||
access: (params) ->
|
||||
@constructor
|
||||
.pickObjectUsing(params, @)
|
||||
?.access()
|
||||
|
||||
isEditor: ->
|
||||
App.User.current().permission('knowledge_base.editor')
|
||||
@access(@lastParams) == 'editor'
|
||||
|
||||
checkForUpdates: ->
|
||||
@interval(@checkUpdatesAction, 10 * 60 * 1000, 'kb_interval_check')
|
||||
|
@ -371,6 +395,27 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
|||
clicked: ->
|
||||
window.open(App.KnowledgeBase.first().publicBaseUrl(), '_blank')
|
||||
|
||||
visibilityMayHaveChanged: =>
|
||||
kb = @getKnowledgeBase()
|
||||
|
||||
return if !kb
|
||||
|
||||
@ajax
|
||||
id: 'kb_pull_visibility'
|
||||
type: 'GET'
|
||||
url: @apiPath + '/knowledge_bases/visible_ids'
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
didRemove = kb.removeAssetsIfNeeded(data)
|
||||
hasAssetsToLoad = kb.hasAssetsToLoad(data)
|
||||
|
||||
if hasAssetsToLoad
|
||||
@fetch(false, false, true)
|
||||
return
|
||||
|
||||
if didRemove
|
||||
@notifyVisibilityChangeLoaded()
|
||||
|
||||
@pickObjectUsing: (params, parentController) ->
|
||||
kb = parentController.getKnowledgeBase()
|
||||
return if !kb
|
||||
|
|
|
@ -103,7 +103,7 @@ class App.KnowledgeBaseContentCanBePublishedForm extends App.ControllerForm
|
|||
,
|
||||
value: 'internal'
|
||||
name: __('Internal')
|
||||
note: __('Visible to agents & editors')
|
||||
note: __('Visible to readers & editors')
|
||||
,
|
||||
value: 'published'
|
||||
name: __('Public')
|
||||
|
|
|
@ -15,6 +15,12 @@ class App.KnowledgeBaseEditorCoordinator
|
|||
parentController: @parentController
|
||||
)
|
||||
|
||||
clickedPermissions: (object) ->
|
||||
new App.KnowledgeBasePermissionsDialog(
|
||||
object: object
|
||||
container: @parentController.el
|
||||
)
|
||||
|
||||
# built-in Spine's function doesn't work when object has no ID set and includes "undefined" in URL
|
||||
urlFor: (object) ->
|
||||
if object.id
|
||||
|
|
|
@ -13,8 +13,8 @@ class App.KnowledgeBaseNavigation extends App.Controller
|
|||
|
||||
@controllerBind('knowledge_base::navigation::rerender', => @needsUpdate())
|
||||
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||
@needsUpdate()
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @needsUpdate
|
||||
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @needsUpdate
|
||||
|
||||
buildCrumbsForRendering: (array, kb_locale, action) ->
|
||||
if action is 'search'
|
||||
|
@ -73,7 +73,7 @@ class App.KnowledgeBaseNavigation extends App.Controller
|
|||
else
|
||||
false
|
||||
|
||||
needsUpdate: ->
|
||||
needsUpdate: =>
|
||||
@show(@savedParams, @savedAction)
|
||||
|
||||
selectedLocaleDisplay: ->
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
class App.KnowledgeBasePermissionsDialog extends App.ControllerModal
|
||||
events:
|
||||
#'submit form': 'submitPermissions'
|
||||
'click td.u-clickable': 'cellBackgroundClicked'
|
||||
|
||||
head: 'Permissions'
|
||||
includeForm: true
|
||||
buttonSubmit: true
|
||||
accessLevels: { editor: 'Editor', reader: 'Reader', none: 'None' }
|
||||
|
||||
cellBackgroundClicked: (e) ->
|
||||
return if e.target != e.currentTarget
|
||||
|
||||
e.preventDefault()
|
||||
e.currentTarget.querySelector('input')?.click()
|
||||
|
||||
data: null
|
||||
|
||||
constructor: (params) ->
|
||||
super
|
||||
|
||||
@load()
|
||||
|
||||
content: =>
|
||||
return if !@data
|
||||
|
||||
App.view('knowledge_base/permissions_dialog')(
|
||||
accessLevels: @accessLevels
|
||||
params: @loadedParams(@data)
|
||||
roles: @formRoles(@data)
|
||||
)
|
||||
|
||||
loadedParams: (data) ->
|
||||
params = []
|
||||
|
||||
for permission in data.permissions
|
||||
params[permission.role_id] = permission.access
|
||||
|
||||
for role in data.roles_editor
|
||||
params[role.id] ||= 'editor'
|
||||
|
||||
for role in data.roles_reader
|
||||
params[role.id] ||= 'reader'
|
||||
|
||||
params
|
||||
|
||||
formRoles: (data) ->
|
||||
data.roles_editor.forEach (elem) => @formRolesItem(elem, 'editor', data)
|
||||
data.roles_reader.forEach (elem) => @formRolesItem(elem, 'reader', data)
|
||||
|
||||
_.sortBy data.roles_editor.concat(data.roles_reader), (elem) -> elem.name
|
||||
|
||||
formRolesItem: (elem, role_name, data) ->
|
||||
elem.accessLevel = role_name
|
||||
elem.limit = _.findWhere(data.inherited, { role_id: elem.id })?.access
|
||||
|
||||
load: =>
|
||||
@ajax(
|
||||
id: 'knowledge_base_permissions_get'
|
||||
type: 'get'
|
||||
url: @object.generateURL('permissions')
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@data = data
|
||||
@update()
|
||||
error: (xhr) =>
|
||||
@showAlert(xhr.responseJSON?.error || __('Unable to load changes'))
|
||||
)
|
||||
|
||||
toggleDisabled: (state) =>
|
||||
@el.find('input, button').attr('disabled', state)
|
||||
|
||||
onSubmit: (e) =>
|
||||
@clearAlerts()
|
||||
@toggleDisabled(true)
|
||||
|
||||
data = @formParams()
|
||||
|
||||
params = { permissions_dialog: { permissions: data } }
|
||||
|
||||
@ajax(
|
||||
id: 'knowledge_base_permissions_patch'
|
||||
type: 'PATCH'
|
||||
data: JSON.stringify(params)
|
||||
url: @object.generateURL('permissions')
|
||||
processData: true
|
||||
success: (data, status, xhr) =>
|
||||
@close()
|
||||
error: (xhr) =>
|
||||
@toggleDisabled(false)
|
||||
@showAlert(xhr.responseJSON?.error || __('Unable to save changes'))
|
||||
)
|
|
@ -54,7 +54,10 @@ class App.KnowledgeBaseReaderController extends App.Controller
|
|||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||
@renderAnswer(@object, kb_locale)
|
||||
|
||||
renderAnswer: (answer, kb_locale) ->
|
||||
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', =>
|
||||
@renderAnswer(@object, kb_locale, true)
|
||||
|
||||
renderAnswer: (answer, kb_locale, onlyVisibility) ->
|
||||
if !answer
|
||||
@parentController.renderNotFound()
|
||||
return
|
||||
|
@ -63,13 +66,16 @@ class App.KnowledgeBaseReaderController extends App.Controller
|
|||
@parentController.renderNotAvailableAnymore()
|
||||
return
|
||||
|
||||
paginator = new App.KnowledgeBaseReaderPagination(object: @object, kb_locale: kb_locale)
|
||||
@answerPagination.html paginator.el
|
||||
|
||||
if onlyVisibility
|
||||
return
|
||||
|
||||
@renderAttachments(answer.attachments)
|
||||
@renderTags(answer.tags)
|
||||
@renderLinkedTickets(answer.translation(kb_locale.id)?.linked_tickets())
|
||||
|
||||
paginator = new App.KnowledgeBaseReaderPagination(object: @object, kb_locale: kb_locale)
|
||||
@answerPagination.html paginator.el
|
||||
|
||||
answer_translation = answer.translation(kb_locale.id)
|
||||
|
||||
if !answer_translation
|
||||
|
|
|
@ -3,13 +3,13 @@ class App.KnowledgeBaseReaderListContainer extends App.Controller
|
|||
super
|
||||
@render()
|
||||
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||
@parentRefreshed()
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @parentRefreshed
|
||||
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @parentRefreshed
|
||||
|
||||
tag: 'ul'
|
||||
className: 'sections'
|
||||
|
||||
parentRefreshed: ->
|
||||
parentRefreshed: =>
|
||||
newIds = @children().map (elem) -> elem.id
|
||||
oldIds = @el.children().toArray().map (elem) -> parseInt(elem.dataset.id)
|
||||
|
||||
|
@ -59,7 +59,9 @@ class App.KnowledgeBaseReaderListContainer.Categories extends App.KnowledgeBaseR
|
|||
@parent.children()
|
||||
else
|
||||
[]
|
||||
|
||||
# coffeelint: enable=indentation
|
||||
#
|
||||
|
||||
if !@isEditor
|
||||
items = items.filter (elem) => elem.visibleInternally(@kb_locale)
|
||||
|
|
|
@ -3,9 +3,16 @@ class App.KnowledgeBaseReaderListController extends App.Controller
|
|||
super
|
||||
@render()
|
||||
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @changeLoaded
|
||||
@listenTo App.KnowledgeBase, 'kb_visibility_may_have_changed', => @changeLoaded(true)
|
||||
|
||||
changeLoaded: =>
|
||||
if !@objectVisibleInternally()
|
||||
@parentController.renderNotAvailableAnymore()
|
||||
return
|
||||
|
||||
if @renderEmptinessState != @object.isEmpty()
|
||||
@render()
|
||||
|
||||
elements:
|
||||
'.js-readerListContainer': 'container'
|
||||
|
@ -18,11 +25,13 @@ class App.KnowledgeBaseReaderListController extends App.Controller
|
|||
@parentController.renderNotFound()
|
||||
return
|
||||
|
||||
@renderEmptinessState = @object.isEmpty()
|
||||
|
||||
if @object.isEmpty()
|
||||
@renderScreenPlaceholder(
|
||||
icon: App.Utils.icon('mood-ok')
|
||||
detail: __('This category is empty')
|
||||
action: __('Start Editing')
|
||||
action: if @parentController.isEditor() then __('Start Editing')
|
||||
actionCallback: =>
|
||||
url = @object.uiUrl(@parentController.kb_locale(), 'edit')
|
||||
@navigate url
|
||||
|
|
|
@ -18,7 +18,10 @@ class App.KnowledgeBaseReaderListItem extends App.Controller
|
|||
|
||||
@sort_order = @item.position
|
||||
|
||||
try
|
||||
attrs = @item.attributesForRendering(@kb_locale, isEditor: @isEditor)
|
||||
catch e
|
||||
attrs = {}
|
||||
|
||||
@el
|
||||
.prop('className')
|
||||
|
|
|
@ -6,6 +6,7 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
|||
|
||||
constructor: ->
|
||||
super
|
||||
@renderedWidgets = []
|
||||
@show()
|
||||
|
||||
@controllerBind 'knowledge_base::sidebar::rerender', => @rerender()
|
||||
|
@ -14,9 +15,9 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
|||
@rerender()
|
||||
true
|
||||
|
||||
rerender: ->
|
||||
rerender: =>
|
||||
@delay( =>
|
||||
@show(@savedParams, @savedAction)
|
||||
@update()
|
||||
, 300, 'rerender')
|
||||
|
||||
contentActionClicked: (e) ->
|
||||
|
@ -24,10 +25,15 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
|||
actionName = switch e.target.dataset.action
|
||||
when 'delete' then 'clickedDelete'
|
||||
when 'visibility' then 'clickedCanBePublished'
|
||||
when 'permissions' then 'clickedPermissions'
|
||||
# coffeelint: enable=indentation
|
||||
|
||||
@parentController.bodyModal = @parentController.coordinator[actionName]?(@savedParams)
|
||||
|
||||
update: =>
|
||||
for elem in @renderedWidgets
|
||||
elem.updateIfNeeded?()
|
||||
|
||||
show: (object, action) ->
|
||||
isEdit = action is 'edit'
|
||||
|
||||
|
@ -39,17 +45,22 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
|||
if !isEdit
|
||||
return
|
||||
|
||||
for widget in @widgets(object)
|
||||
@el.append new widget(
|
||||
@renderedWidgets = []
|
||||
|
||||
for elem in @getWidgets(object)
|
||||
widget = new elem(
|
||||
object: object
|
||||
kb_locale: @parentController.kb_locale()
|
||||
parentController: @parentController
|
||||
).el
|
||||
)
|
||||
|
||||
@renderedWidgets.push widget
|
||||
@el.append widget.el
|
||||
|
||||
hide: ->
|
||||
@el.addClass('hidden')
|
||||
|
||||
widgets: (object) ->
|
||||
getWidgets: (object) ->
|
||||
output = [App.KnowledgeBaseSidebarActions]
|
||||
|
||||
if object instanceof App.KnowledgeBase || object instanceof App.KnowledgeBaseCategory
|
||||
|
|
|
@ -8,6 +8,9 @@ class App.KnowledgeBaseSidebarGenericList extends App.Controller
|
|||
constructor: ->
|
||||
super
|
||||
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
@html App.view('knowledge_base/sidebar/generic_list')(@templateOptions())
|
||||
|
||||
templateOptions: ->
|
||||
|
@ -52,3 +55,6 @@ class App.KnowledgeBaseSidebarGenericList extends App.Controller
|
|||
|
||||
urlNew: ->
|
||||
#has to be overridden
|
||||
|
||||
updateIfNeeded: ->
|
||||
@render()
|
||||
|
|
|
@ -19,9 +19,9 @@ class App.KnowledgeBaseSidebarAttachments extends App.Controller
|
|||
super
|
||||
|
||||
@render()
|
||||
@listenTo @object, 'refresh', @needsUpdate
|
||||
@listenTo @object, 'refresh', @updateIfNeeded
|
||||
|
||||
needsUpdate: =>
|
||||
updateIfNeeded: =>
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
|
|
|
@ -12,9 +12,9 @@ class App.KnowledgeBaseSidebarLinkedTickets extends App.Controller
|
|||
super
|
||||
|
||||
@render()
|
||||
@listenTo @object, 'refresh', @needsUpdate
|
||||
@listenTo @object, 'refresh', @updateIfNeeded
|
||||
|
||||
needsUpdate: =>
|
||||
updateIfNeeded: =>
|
||||
@render()
|
||||
|
||||
render: ->
|
||||
|
|
|
@ -11,3 +11,6 @@ class App.KnowledgeBaseSidebarTags extends App.Controller
|
|||
object: @object
|
||||
tags: @object.tags
|
||||
)
|
||||
|
||||
updateIfNeeded: ->
|
||||
@widget.reload(@object.tags)
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
InstanceMethods =
|
||||
access: ->
|
||||
permission_reader = App.Permission.findByAttribute('name', 'knowledge_base.reader')
|
||||
permission_editor = App.Permission.findByAttribute('name', 'knowledge_base.editor')
|
||||
|
||||
permissions_effective = switch @constructor
|
||||
when App.KnowledgeBaseAnswer
|
||||
@category().permissions_effective
|
||||
when App.KnowledgeBaseCategory, App.KnowledgeBase
|
||||
@permissions_effective
|
||||
|
||||
access = 'none'
|
||||
|
||||
for role_id in App.User.current().role_ids
|
||||
kb_permission = _.findWhere(permissions_effective, { role_id: role_id })
|
||||
|
||||
if kb_permission
|
||||
switch kb_permission.access
|
||||
when 'editor'
|
||||
return 'editor'
|
||||
when 'reader'
|
||||
access = 'reader'
|
||||
else if role = App.Role.find(role_id)
|
||||
if role.permission_ids.indexOf(permission_editor.id) > -1
|
||||
return 'editor'
|
||||
if role.permission_ids.indexOf(permission_reader.id) > -1
|
||||
access = 'reader'
|
||||
|
||||
return access
|
||||
|
||||
App.KnowledgeBaseAccess =
|
||||
extended: ->
|
||||
@include InstanceMethods
|
|
@ -11,6 +11,15 @@ InstanceMethods =
|
|||
disabled: @isNew()
|
||||
}
|
||||
|
||||
if (@ instanceof App.KnowledgeBaseCategory) || (@ instanceof App.KnowledgeBase)
|
||||
buttons.push {
|
||||
iconName: 'lock'
|
||||
name: 'Permissions'
|
||||
action: 'permissions'
|
||||
cssClass: 'btn--success'
|
||||
disabled: @isNew()
|
||||
}
|
||||
|
||||
if !(@ instanceof App.KnowledgeBase)
|
||||
buttons.push {
|
||||
iconName: 'trash'
|
||||
|
|
|
@ -34,12 +34,6 @@ InstanceMethods =
|
|||
attrs.iconFont = true
|
||||
attrs.icon = @category_icon
|
||||
attrs.count = @countDeepAnswers()
|
||||
|
||||
if options.isEditor
|
||||
attrs.editorOnly = !@visibleInternally(kb_locale)
|
||||
else
|
||||
attrs.editorOnly = false
|
||||
|
||||
attrs.state = @visibilityState(kb_locale)
|
||||
|
||||
if @ instanceof App.KnowledgeBaseAnswer
|
||||
|
@ -47,16 +41,6 @@ InstanceMethods =
|
|||
attrs.state = @can_be_published_state()
|
||||
attrs.tags = @tags
|
||||
|
||||
if options.isEditor
|
||||
attrs.editorOnly = !@is_internally_published(kb_locale)
|
||||
else
|
||||
attrs.editorOnly = false
|
||||
|
||||
# attrs.className = if attrs.missingTranslation
|
||||
# 'kb-item--missing-translation'
|
||||
# else if attrs.editorOnly
|
||||
# 'kb-item--invisible'
|
||||
|
||||
attrs.icons = {}
|
||||
|
||||
if attrs.missingTranslation
|
||||
|
|
|
@ -2,6 +2,7 @@ class App.KnowledgeBase extends App.Model
|
|||
@configure 'KnowledgeBase', 'iconset', 'color_highlight', 'color_header', 'color_header_link', 'translation_ids', 'locale_ids', 'homepage_layout', 'category_layout', 'custom_address'
|
||||
@extend Spine.Model.Ajax
|
||||
@extend App.KnowledgeBaseActions
|
||||
@extend App.KnowledgeBaseAccess
|
||||
@url: @apiPath + '/knowledge_bases'
|
||||
|
||||
@manageUrl: @apiPath + '/knowledge_bases/manage'
|
||||
|
@ -76,7 +77,7 @@ class App.KnowledgeBase extends App.Model
|
|||
, initial
|
||||
|
||||
visibleInternally: (kb_locale) ->
|
||||
@active
|
||||
@active && @access() != 'none'
|
||||
|
||||
visiblePublicly: (kb_locale) ->
|
||||
@active
|
||||
|
@ -88,6 +89,41 @@ class App.KnowledgeBase extends App.Model
|
|||
|
||||
attrs
|
||||
|
||||
loadedAnswerIds: ->
|
||||
App.KnowledgeBaseAnswer
|
||||
.all()
|
||||
.filter (elem) => elem.knowledge_base().id == @id
|
||||
.map (elem) -> elem.id
|
||||
|
||||
loadedCategoryIds: ->
|
||||
App.KnowledgeBaseCategory
|
||||
.all()
|
||||
.map (elem) -> elem.id
|
||||
|
||||
removeAssetsIfNeeded: (data) =>
|
||||
removeAnswers = _.difference @loadedAnswerIds(), data.answer_ids
|
||||
removeCategories = _.difference @loadedAnswerIds(), data.category_ids
|
||||
|
||||
for answer_id in removeAnswers
|
||||
App.KnowledgeBaseAnswer.find(answer_id)?.remove(clear: true)
|
||||
|
||||
for category_id in removeCategories
|
||||
App.KnowledgeBaseCategory.find(category_id)?.remove(clear: true)
|
||||
|
||||
!_.isEmpty(removeAnswers) || !_.isEmpty(removeCategories)
|
||||
|
||||
hasAssetsToLoad: (data) =>
|
||||
needsLoadingAnswers = _.difference data.answer_ids, @loadedAnswerIds()
|
||||
needsLoadingCategories = _.difference data.category_ids, @loadedCategoryIds()
|
||||
|
||||
!_.isEmpty(needsLoadingAnswers) || !_.isEmpty(needsLoadingCategories)
|
||||
|
||||
@allKbModelNames: ->
|
||||
Object
|
||||
.keys(App)
|
||||
.filter (elem) ->
|
||||
elem.match(/^KnowledgeBase/) && App[elem]?.prototype instanceof App.Model
|
||||
|
||||
@configure_attributes: [
|
||||
{
|
||||
name: 'translation::title'
|
||||
|
|
|
@ -3,6 +3,7 @@ class App.KnowledgeBaseAnswer extends App.Model
|
|||
@extend Spine.Model.Ajax
|
||||
@extend App.KnowledgeBaseActions
|
||||
@extend App.KnowledgeBaseCanBePublished
|
||||
@extend App.KnowledgeBaseAccess
|
||||
|
||||
@serverClassName: 'KnowledgeBase::Answer'
|
||||
|
||||
|
@ -95,4 +96,4 @@ class App.KnowledgeBaseAnswer extends App.Model
|
|||
'Answer'
|
||||
|
||||
visibleInternally: (kb_locale) =>
|
||||
@is_internally_published(kb_locale)
|
||||
(@is_internally_published(kb_locale) && @access() != 'none') || @is_published(kb_locale)
|
||||
|
|
|
@ -2,6 +2,7 @@ class App.KnowledgeBaseCategory extends App.Model
|
|||
@configure 'KnowledgeBaseCategory', 'category_icon', 'parent_id', 'child_ids', 'translation_ids'
|
||||
@extend Spine.Model.Ajax
|
||||
@extend App.KnowledgeBaseActions
|
||||
@extend App.KnowledgeBaseAccess
|
||||
|
||||
url: ->
|
||||
@knowledge_base().generateURL('categories')
|
||||
|
@ -166,6 +167,8 @@ class App.KnowledgeBaseCategory extends App.Model
|
|||
'draft'
|
||||
|
||||
visibleInternally: (kb_locale) =>
|
||||
#return false if @access() == 'none'
|
||||
|
||||
@findDeepAnswer( (record) ->
|
||||
record.is_internally_published(kb_locale)
|
||||
)?
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
class App.KnowledgeBaseCategoryPermission extends App.Model
|
||||
@configure 'KnowledgeBaseCategoryPermission', 'access'
|
|
@ -65,7 +65,6 @@ class App.KnowledgeBaseForm extends App.Controller
|
|||
submit: (e) ->
|
||||
@preventDefaultAndStopPropagation(e)
|
||||
|
||||
#debuggerj
|
||||
formController = @formControllers.filter((elem) -> (elem.form[0] is e.currentTarget) or (e.currentTarget.contains(elem.form[0])))[0]
|
||||
params = @formParam(formController.form)
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<div style="padding-left: 18px; padding-top: 10px;">
|
||||
<table class="settings-list settings-list--roles-permissions">
|
||||
<thead>
|
||||
<th width=150><%- @T('Role') %>
|
||||
<% for key, text of @accessLevels: %>
|
||||
<th><%- @T(text) %>
|
||||
<% end %>
|
||||
<tbody>
|
||||
<% for role in @roles: %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= role.name %>
|
||||
<% for key, text of @accessLevels: %>
|
||||
<td class="settings-list-control-cell settings-list-radio-cell u-clickable">
|
||||
<label class="inline-label radio-replacement">
|
||||
<input
|
||||
type="radio"
|
||||
value="<%= key %>"
|
||||
name="<%= role.id %>"
|
||||
<% if @params[role.id] == key: %>checked<% end %>
|
||||
<% if role.limit?: %>
|
||||
<% if key == 'editor' && role.limit != 'editor': %>disabled<% end %>
|
||||
<% if key == 'reader' && role.limit == 'none': %>disabled<% end %>
|
||||
<% end %>
|
||||
/>
|
||||
<%- @Icon('radio', 'icon-unchecked') %>
|
||||
<%- @Icon('radio-checked', 'icon-checked') %>
|
||||
</label>
|
||||
<% end %>
|
||||
</tr>
|
||||
<% end %>
|
||||
</table>
|
||||
</div>
|
|
@ -10921,10 +10921,15 @@ output {
|
|||
}
|
||||
}
|
||||
|
||||
.settings-list-checkbox-cell {
|
||||
.settings-list-checkbox-cell,
|
||||
.settings-list-radio-cell {
|
||||
vertical-align: middle;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.settings-list-radio-cell {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.select-boxes {
|
||||
|
|
45
app/controllers/knowledge_base/permissions_controller.rb
Normal file
45
app/controllers/knowledge_base/permissions_controller.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase::PermissionsController < ApplicationController
|
||||
prepend_before_action :authentication_check
|
||||
before_action :fetch_object
|
||||
|
||||
def show
|
||||
render json: response_hash
|
||||
end
|
||||
|
||||
def update
|
||||
permissions_params = params.require(:permissions_dialog).permit(permissions: {})
|
||||
|
||||
KnowledgeBase::PermissionsUpdate.new(@object, current_user).update_using_params!(permissions_params)
|
||||
|
||||
render json: response_hash
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_object
|
||||
if params[:knowledge_base_id]
|
||||
@object = KnowledgeBase::Category.includes(:permissions).find params[:id]
|
||||
authorize @object, :permissions?
|
||||
else
|
||||
@object = KnowledgeBase.includes(:permissions).find params[:id]
|
||||
authorize @object, :update?
|
||||
end
|
||||
end
|
||||
|
||||
def parent_object
|
||||
return if !@object.is_a? KnowledgeBase::Category
|
||||
|
||||
@object.parent || @object.knowledge_base
|
||||
end
|
||||
|
||||
def response_hash
|
||||
{
|
||||
roles_reader: Role.with_permissions('knowledge_base.reader').pluck_as_hash(:id, :name),
|
||||
roles_editor: Role.with_permissions('knowledge_base.editor').pluck_as_hash(:id, :name),
|
||||
permissions: @object.permissions_effective.pluck_as_hash(:id, :access, :role_id),
|
||||
inherited: parent_object&.permissions_effective&.pluck_as_hash(:id, :access, :role_id)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -13,9 +13,7 @@ class KnowledgeBase::Public::AnswersController < KnowledgeBase::Public::BaseCont
|
|||
private
|
||||
|
||||
def render_alternative
|
||||
@alternative = policy_scope(@knowledge_base.answers)
|
||||
.eager_load(translations: :kb_locale)
|
||||
.find_by(id: params[:answer])
|
||||
@alternative = find_answer @knowledge_base.answers.eager_load(translations: :kb_locale), params[:answer], locale: false
|
||||
|
||||
raise ActiveRecord::RecordNotFound if !@alternative&.translations&.any?
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
class KnowledgeBase::Public::BaseController < ApplicationController
|
||||
before_action :load_kb
|
||||
helper_method :system_locale_via_uri, :fallback_locale, :current_user, :find_category, :filter_primary_kb_locale, :menu_items, :all_locales
|
||||
helper_method :system_locale_via_uri, :fallback_locale, :current_user, :find_category,
|
||||
:filter_primary_kb_locale, :menu_items, :all_locales, :can_preview?
|
||||
|
||||
layout 'knowledge_base'
|
||||
|
||||
|
@ -31,12 +32,9 @@ class KnowledgeBase::Public::BaseController < ApplicationController
|
|||
end
|
||||
|
||||
def fallback_locale
|
||||
if all_locales.find { |locale| locale.id == system_locale_via_uri&.id }
|
||||
system_locale_via_uri
|
||||
else
|
||||
filter_primary_kb_locale || all_locales.first
|
||||
end
|
||||
return system_locale_via_uri if all_locales.find { |locale| locale.id == system_locale_via_uri&.id }
|
||||
|
||||
filter_primary_kb_locale || all_locales.first
|
||||
end
|
||||
|
||||
def filter_primary_kb_locale
|
||||
|
@ -62,16 +60,40 @@ class KnowledgeBase::Public::BaseController < ApplicationController
|
|||
list
|
||||
.localed(system_locale_via_uri)
|
||||
.sorted
|
||||
.select { |category| policy(category).show? }
|
||||
.select { |category| policy(category).show_public? }
|
||||
end
|
||||
|
||||
def find_answer(scope, id)
|
||||
def answers_filter(list)
|
||||
answers = list
|
||||
.localed(system_locale_via_uri)
|
||||
.sorted
|
||||
|
||||
if current_user&.permissions?('knowledge_base.editor')
|
||||
answers.filter { |answer| policy(answer).show_public? }
|
||||
else
|
||||
answers.published
|
||||
end
|
||||
end
|
||||
|
||||
def find_answer(scope, id, locale: system_locale_via_uri)
|
||||
return if scope.nil?
|
||||
|
||||
policy_scope(scope)
|
||||
.localed(system_locale_via_uri)
|
||||
.include_contents
|
||||
.find_by(id: id)
|
||||
scope = scope.include_contents
|
||||
scope = scope.localed(locale) if locale
|
||||
|
||||
if !current_user&.permissions?('knowledge_base.editor')
|
||||
return scope.published.find_by(id: id)
|
||||
end
|
||||
|
||||
if (item = scope.find_by(id: id)) && policy(item).show_public?
|
||||
return item
|
||||
end
|
||||
|
||||
nil
|
||||
end
|
||||
|
||||
def can_preview?
|
||||
@can_preview ||= policy(@knowledge_base).update?
|
||||
end
|
||||
|
||||
def not_found(e)
|
||||
|
|
|
@ -15,14 +15,11 @@ class KnowledgeBase::Public::CategoriesController < KnowledgeBase::Public::BaseC
|
|||
def show
|
||||
@object = find_category(params[:category])
|
||||
|
||||
render_alternatives && return if @object.nil? || !policy(@object).show?
|
||||
render_alternatives && return if @object.nil? || !policy(@object).show_public?
|
||||
|
||||
@categories = categories_filter(@object.children)
|
||||
@object_locales = find_locales(@object)
|
||||
|
||||
@answers = policy_scope(@object.answers)
|
||||
.localed(system_locale_via_uri)
|
||||
.sorted
|
||||
@answers = answers_filter(@object.answers)
|
||||
|
||||
render :index
|
||||
end
|
||||
|
|
|
@ -3,11 +3,6 @@
|
|||
class KnowledgeBase::Public::TagsController < KnowledgeBase::Public::BaseController
|
||||
def show
|
||||
@object = [:tag, params[:tag]]
|
||||
|
||||
all_tagged = KnowledgeBase::Answer.tag_objects(params[:tag])
|
||||
|
||||
@answers = policy_scope(all_tagged)
|
||||
.localed(system_locale_via_uri)
|
||||
.sorted
|
||||
@answers = answers_filter KnowledgeBase::Answer.tag_objects(params[:tag])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,15 +5,41 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
|||
render json: assets(params[:answer_translation_content_ids])
|
||||
end
|
||||
|
||||
def visible_ids
|
||||
render json: KnowledgeBase::InternalAssets.new(current_user).visible_ids
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def assets(answer_translation_content_ids = nil)
|
||||
return editor_assets(answer_translation_content_ids) if current_user&.permissions?('knowledge_base.editor')
|
||||
return reader_assets(answer_translation_content_ids) if current_user&.permissions?('knowledge_base.reader')
|
||||
if KnowledgeBase.granular_permissions?
|
||||
return granular_assets(answer_translation_content_ids) if kb_permissions?
|
||||
else
|
||||
return editor_assets(answer_translation_content_ids) if kb_permission_editor?
|
||||
return reader_assets(answer_translation_content_ids) if kb_permission_reader?
|
||||
end
|
||||
|
||||
public_assets
|
||||
end
|
||||
|
||||
def kb_permissions?
|
||||
current_user&.permissions?(%w[knowledge_base.editor knowledge_base.reader])
|
||||
end
|
||||
|
||||
def kb_permission_editor?
|
||||
current_user&.permissions?('knowledge_base.editor')
|
||||
end
|
||||
|
||||
def kb_permission_reader?
|
||||
current_user&.permissions?('knowledge_base.reader')
|
||||
end
|
||||
|
||||
def granular_assets(answer_translation_content_ids)
|
||||
KnowledgeBase::InternalAssets
|
||||
.new(current_user, answer_translation_content_ids: answer_translation_content_ids)
|
||||
.collect_assets
|
||||
end
|
||||
|
||||
def editor_assets(answer_translation_content_ids)
|
||||
assets = [
|
||||
KnowledgeBase,
|
||||
|
|
|
@ -36,7 +36,7 @@ module KnowledgeBaseTopBarHelper
|
|||
end
|
||||
|
||||
def render_top_bar_if_needed(object, knowledge_base)
|
||||
return if !policy(:knowledge_base).edit?
|
||||
return if !can_preview?
|
||||
|
||||
editable = object || knowledge_base
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module KnowledgeBaseVisibilityClassHelper
|
||||
def visibility_class_name(object)
|
||||
return if !policy(:knowledge_base).edit?
|
||||
return if !can_preview?
|
||||
|
||||
suffix = case object
|
||||
when CanBePublished
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
module KnowledgeBaseVisibilityNoteHelper
|
||||
def visibility_note(object)
|
||||
return if !policy(:knowledge_base).edit?
|
||||
return if !can_preview?
|
||||
|
||||
text = visibility_text(object)
|
||||
|
||||
|
|
|
@ -4,12 +4,12 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
|||
include HasActiveJobLock
|
||||
|
||||
def lock_key
|
||||
# "ChecksKbClientNotificationJob/KnowledgeBase::Answer/42/destroy"
|
||||
"#{self.class.name}/#{arguments[0]}/#{arguments[1]}/#{arguments[2]}"
|
||||
# "ChecksKbClientNotificationJob/KnowledgeBase::Answer/42"
|
||||
"#{self.class.name}/#{arguments[0]}/#{arguments[1]}"
|
||||
end
|
||||
|
||||
def perform(klass_name, id, event)
|
||||
object = klass_name.constantize.find_by(id: id)
|
||||
def perform(klass_name, object_id)
|
||||
object = klass_name.constantize.find_by(id: object_id)
|
||||
return if object.blank?
|
||||
|
||||
level = needs_editor?(object) ? 'editor' : '*'
|
||||
|
@ -24,7 +24,7 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
|||
|
||||
def build_data(object, event)
|
||||
timestamp = event == :destroy ? Time.zone.now : object.updated_at
|
||||
url = event == :destroy ? nil : object.api_url
|
||||
url = event == :destroy ? nil : object.try(:api_url)
|
||||
|
||||
{
|
||||
class: object.class.to_s,
|
||||
|
@ -57,8 +57,4 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
|||
.filter_map { |user_id| User.find_by(id: user_id) }
|
||||
.select { |user| user.permissions? "knowledge_base.#{permission_suffix}" }
|
||||
end
|
||||
|
||||
def self.notify_later(object, event)
|
||||
perform_later(object.class.to_s, object.id, event.to_s)
|
||||
end
|
||||
end
|
||||
|
|
14
app/jobs/checks_kb_client_visibility_job.rb
Normal file
14
app/jobs/checks_kb_client_visibility_job.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class ChecksKbClientVisibilityJob < ApplicationJob
|
||||
include HasActiveJobLock
|
||||
|
||||
def lock_key
|
||||
# "ChecksKbClientVisibilityJob"
|
||||
self.class.name
|
||||
end
|
||||
|
||||
def perform
|
||||
Sessions.broadcast({ event: 'kb_visibility_may_have_changed' })
|
||||
end
|
||||
end
|
|
@ -4,10 +4,7 @@ module ChecksKbClientNotification
|
|||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_create :notify_kb_clients_after_create
|
||||
after_update :notify_kb_clients_after_update
|
||||
after_touch :notify_kb_clients_after_touch
|
||||
after_destroy :notify_kb_clients_after_destroy
|
||||
after_commit :notify_kb_clients_after
|
||||
|
||||
class_attribute :notify_kb_clients_suspend, default: false
|
||||
end
|
||||
|
@ -30,25 +27,9 @@ module ChecksKbClientNotification
|
|||
|
||||
# generic call
|
||||
|
||||
def notify_kb_clients(event)
|
||||
def notify_kb_clients_after
|
||||
return if self.class.notify_kb_clients_suspend?
|
||||
|
||||
ChecksKbClientNotificationJob.notify_later(self, event)
|
||||
end
|
||||
|
||||
def notify_kb_clients_after_create
|
||||
notify_kb_clients(:create)
|
||||
end
|
||||
|
||||
def notify_kb_clients_after_update
|
||||
notify_kb_clients(:update)
|
||||
end
|
||||
|
||||
def notify_kb_clients_after_touch
|
||||
notify_kb_clients(:touch)
|
||||
end
|
||||
|
||||
def notify_kb_clients_after_destroy
|
||||
notify_kb_clients(:destroy)
|
||||
ChecksKbClientNotificationJob.perform_later(self.class.name, id)
|
||||
end
|
||||
end
|
||||
|
|
17
app/models/concerns/checks_kb_client_visibility.rb
Normal file
17
app/models/concerns/checks_kb_client_visibility.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
module ChecksKbClientVisibility
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
after_commit :notify_kb_client_visibility
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def notify_kb_client_visibility
|
||||
return if self.class.notify_kb_clients_suspend?
|
||||
|
||||
ChecksKbClientVisibilityJob.perform_later
|
||||
end
|
||||
end
|
|
@ -24,6 +24,11 @@ class KnowledgeBase < ApplicationModel
|
|||
|
||||
has_many :answers, through: :categories
|
||||
|
||||
has_many :permissions, class_name: 'KnowledgeBase::Permission',
|
||||
as: :permissionable,
|
||||
autosave: true,
|
||||
dependent: :destroy
|
||||
|
||||
validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||
|
||||
|
@ -156,6 +161,24 @@ class KnowledgeBase < ApplicationModel
|
|||
.any? { |e| e > 1 }
|
||||
end
|
||||
|
||||
def permissions_effective
|
||||
cache_key = KnowledgeBase::Permission.cache_key self
|
||||
|
||||
Rails.cache.fetch cache_key do
|
||||
permissions
|
||||
end
|
||||
end
|
||||
|
||||
def attributes_with_association_ids
|
||||
attrs = super
|
||||
attrs[:permissions_effective] = permissions_effective
|
||||
attrs
|
||||
end
|
||||
|
||||
def self.granular_permissions?
|
||||
KnowledgeBase::Permission.any?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_defaults
|
||||
|
|
|
@ -6,6 +6,7 @@ class KnowledgeBase::Answer < ApplicationModel
|
|||
include HasTags
|
||||
include CanBePublished
|
||||
include ChecksKbClientNotification
|
||||
include ChecksKbClientVisibility
|
||||
include CanCloneAttachments
|
||||
|
||||
AGENT_ALLOWED_ATTRIBUTES = %i[category_id promoted internal_note].freeze
|
||||
|
@ -49,7 +50,13 @@ class KnowledgeBase::Answer < ApplicationModel
|
|||
siblings = category.answers
|
||||
|
||||
if !User.lookup(id: UserInfo.current_user_id)&.permissions?('knowledge_base.editor')
|
||||
siblings = siblings.internal
|
||||
ep = KnowledgeBase::EffectivePermission.new User.find(UserInfo.current_user_id), category
|
||||
|
||||
siblings = if ep.access_effective == 'public_reader'
|
||||
siblings.published
|
||||
else
|
||||
siblings.internal
|
||||
end
|
||||
end
|
||||
|
||||
data = ApplicationModel::CanAssets.reduce(siblings, data)
|
||||
|
|
|
@ -4,6 +4,7 @@ class KnowledgeBase::Category < ApplicationModel
|
|||
include HasTranslations
|
||||
include HasAgentAllowedParams
|
||||
include ChecksKbClientNotification
|
||||
include ChecksKbClientVisibility
|
||||
|
||||
AGENT_ALLOWED_ATTRIBUTES = %i[knowledge_base_id parent_id category_icon].freeze
|
||||
AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
|
||||
|
@ -24,6 +25,11 @@ class KnowledgeBase::Category < ApplicationModel
|
|||
touch: true,
|
||||
optional: true
|
||||
|
||||
has_many :permissions, class_name: 'KnowledgeBase::Permission',
|
||||
as: :permissionable,
|
||||
autosave: true,
|
||||
dependent: :destroy
|
||||
|
||||
validates :category_icon, presence: true
|
||||
|
||||
scope :root, -> { where(parent: nil) }
|
||||
|
@ -119,6 +125,20 @@ class KnowledgeBase::Category < ApplicationModel
|
|||
Rails.application.routes.url_helpers.knowledge_base_category_path(knowledge_base, self)
|
||||
end
|
||||
|
||||
def permissions_effective
|
||||
cache_key = KnowledgeBase::Permission.cache_key self
|
||||
|
||||
Rails.cache.fetch cache_key do
|
||||
KnowledgeBase::Category::Permission.new(self).permissions_effective
|
||||
end
|
||||
end
|
||||
|
||||
def attributes_with_association_ids
|
||||
attrs = super
|
||||
attrs[:permissions_effective] = permissions_effective
|
||||
attrs
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def cannot_be_child_of_parent
|
||||
|
|
16
app/models/knowledge_base/permission.rb
Normal file
16
app/models/knowledge_base/permission.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase::Permission < ApplicationModel
|
||||
belongs_to :permissionable, polymorphic: true, touch: true
|
||||
belongs_to :role
|
||||
|
||||
validates :access, inclusion: { in: %w[editor reader none] }
|
||||
validates :role, uniqueness: { scope: %i[permissionable_id permissionable_type] }
|
||||
|
||||
# cache key for calculated permissions
|
||||
# @param permissionable [KnowledgeBase::Category, KnowledgeBase]
|
||||
# @return [String]
|
||||
def self.cache_key(permissionable)
|
||||
"#{permissionable.class}::aws::#{permissionable.id}::permission::#{permissionable.updated_at}"
|
||||
end
|
||||
end
|
|
@ -14,11 +14,12 @@ class Role < ApplicationModel
|
|||
has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update
|
||||
has_and_belongs_to_many :permissions,
|
||||
before_add: %i[validate_agent_limit_by_permission validate_permissions],
|
||||
after_add: :cache_update,
|
||||
after_add: %i[cache_update cache_add_kb_permission],
|
||||
before_remove: :last_admin_check_by_permission,
|
||||
after_remove: :cache_update
|
||||
after_remove: %i[cache_update cache_remove_kb_permission]
|
||||
validates :name, presence: true
|
||||
store :preferences
|
||||
has_many :knowledge_base_permissions, class_name: 'KnowledgeBase::Permission', dependent: :destroy
|
||||
|
||||
before_create :check_default_at_signup_permissions
|
||||
before_update :last_admin_check_by_attribute, :validate_agent_limit_by_attributes, :check_default_at_signup_permissions
|
||||
|
@ -232,4 +233,31 @@ returns
|
|||
raise Exceptions::UnprocessableEntity, "Cannot set default at signup when role has #{forbidden_permissions.join(', ')} permissions."
|
||||
end
|
||||
|
||||
def cache_add_kb_permission(permission)
|
||||
return if !permission.name.starts_with? 'knowledge_base.'
|
||||
return if !KnowledgeBase.granular_permissions?
|
||||
|
||||
KnowledgeBase::Category.all.each(&:touch)
|
||||
end
|
||||
|
||||
def cache_remove_kb_permission(permission)
|
||||
return if !permission.name.starts_with? 'knowledge_base.'
|
||||
return if !KnowledgeBase.granular_permissions?
|
||||
|
||||
has_editor = permissions.where(name: 'knowledge_base.editor').any?
|
||||
has_reader = permissions.where(name: 'knowledge_base.reader').any?
|
||||
|
||||
KnowledgeBase::Permission
|
||||
.where(role: self)
|
||||
.each do |elem|
|
||||
if !has_editor && !has_reader
|
||||
elem.destroy!
|
||||
elsif !has_editor && has_reader
|
||||
elem.update!(access: 'reader') if permission.access == 'editor'
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
KnowledgeBase::Category.all.each(&:touch)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,9 +2,34 @@
|
|||
|
||||
class Controllers::KnowledgeBase::AnswersControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
||||
def show?
|
||||
return true if user.permissions?('knowledge_base.editor')
|
||||
access(__method__)
|
||||
end
|
||||
|
||||
object = record.klass.find(record.params[:id])
|
||||
object.can_be_published_aasm.internal? || object.can_be_published_aasm.published?
|
||||
def create?
|
||||
verify_category(__method__)
|
||||
end
|
||||
|
||||
def update?
|
||||
access(__method__) && verify_category(__method__)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
access(__method__)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object
|
||||
@object ||= record.klass.find(record.params[:id])
|
||||
end
|
||||
|
||||
def access(method)
|
||||
KnowledgeBase::AnswerPolicy.new(user, object).send(method)
|
||||
end
|
||||
|
||||
def verify_category(method)
|
||||
new_category = KnowledgeBase::Category.find(record.params[:category_id])
|
||||
|
||||
KnowledgeBase::CategoryPolicy.new(user, new_category).send(method)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,8 +2,38 @@
|
|||
|
||||
class Controllers::KnowledgeBase::CategoriesControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
||||
def show?
|
||||
return if user.permissions?('knowledge_base.editor')
|
||||
access(__method__)
|
||||
end
|
||||
|
||||
record.klass.find(record.params[:id]).internal_content?
|
||||
def create?
|
||||
verify_parent(__method__)
|
||||
end
|
||||
|
||||
def update?
|
||||
access(__method__) && verify_parent(__method__)
|
||||
end
|
||||
|
||||
def destroy?
|
||||
access(__method__)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object
|
||||
@object ||= record.klass.find(record.params[:id])
|
||||
end
|
||||
|
||||
def access(method)
|
||||
KnowledgeBase::CategoryPolicy.new(user, object).send(method)
|
||||
end
|
||||
|
||||
def verify_parent(method)
|
||||
if record.params[:parent_id].blank?
|
||||
return user.permissions?('knowledge_base.editor')
|
||||
end
|
||||
|
||||
parent = KnowledgeBase::Category.find(record.params[:parent_id])
|
||||
|
||||
KnowledgeBase::CategoryPolicy.new(user, parent).send(method)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,4 +12,18 @@ class Controllers::KnowledgeBasesControllerPolicy < Controllers::KnowledgeBase::
|
|||
def destroy?
|
||||
false
|
||||
end
|
||||
|
||||
def update?
|
||||
access(__method__)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def object
|
||||
@object ||= record.klass.find(record.params[:id])
|
||||
end
|
||||
|
||||
def access(method)
|
||||
KnowledgeBase::CategoryPolicy.new(user, object).send(method)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,13 +2,39 @@
|
|||
|
||||
class KnowledgeBase::AnswerPolicy < ApplicationPolicy
|
||||
def show?
|
||||
return true if user&.permissions?(%w[knowledge_base.editor])
|
||||
return true if access_editor?
|
||||
|
||||
record.visible? ||
|
||||
(user&.permissions?(%w[knowledge_base.reader]) && record.visible_internally?)
|
||||
(access_reader? && record.visible_internally?)
|
||||
end
|
||||
|
||||
def show_public?
|
||||
access_editor? || record.visible?
|
||||
end
|
||||
|
||||
def create?
|
||||
access_editor?
|
||||
end
|
||||
|
||||
def update?
|
||||
access_editor?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
user&.permissions?(%w[knowledge_base.editor])
|
||||
access_editor?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access
|
||||
@access ||= KnowledgeBase::EffectivePermission.new(user, record.category).access_effective
|
||||
end
|
||||
|
||||
def access_editor?
|
||||
access == 'editor'
|
||||
end
|
||||
|
||||
def access_reader?
|
||||
access == 'reader'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,13 +2,51 @@
|
|||
|
||||
class KnowledgeBase::CategoryPolicy < ApplicationPolicy
|
||||
def show?
|
||||
return true if user&.permissions?('knowledge_base.editor')
|
||||
access_editor? || access_reader?
|
||||
end
|
||||
|
||||
record.public_content?
|
||||
def show_public?
|
||||
access_editor? || record.public_content?
|
||||
end
|
||||
|
||||
def permissions?
|
||||
access_editor?
|
||||
end
|
||||
|
||||
def create?
|
||||
parent_editor?
|
||||
end
|
||||
|
||||
def update?
|
||||
access_editor?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
parent_editor?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access
|
||||
@access ||= KnowledgeBase::EffectivePermission.new(user, record).access_effective
|
||||
end
|
||||
|
||||
def access_editor?
|
||||
access == 'editor'
|
||||
end
|
||||
|
||||
def access_reader?
|
||||
access == 'reader'
|
||||
end
|
||||
|
||||
def parent_access
|
||||
@parent_access ||= KnowledgeBase::EffectivePermission.new(user, (record.parent || record.knowledge_base)).access_effective
|
||||
end
|
||||
|
||||
def parent_editor?
|
||||
parent_access == 'editor'
|
||||
end
|
||||
|
||||
def user_required?
|
||||
false
|
||||
end
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBasePolicy < ApplicationPolicy
|
||||
def edit?
|
||||
user&.permissions?('knowledge_base.editor')
|
||||
def show?
|
||||
access_editor? || access_reader?
|
||||
end
|
||||
|
||||
def update?
|
||||
access_editor?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def access
|
||||
@access ||= KnowledgeBase::EffectivePermission.new(user, record).access_effective
|
||||
end
|
||||
|
||||
def access_editor?
|
||||
access == 'editor'
|
||||
end
|
||||
|
||||
def access_reader?
|
||||
access == 'reader'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -20,6 +20,7 @@ Zammad::Application.routes.draw do
|
|||
resources :knowledge_bases, only: %i[show update] do
|
||||
collection do
|
||||
post :init
|
||||
get :visible_ids
|
||||
post :search, controller: 'knowledge_base/search'
|
||||
get :recent_answers, controller: 'knowledge_base/answers'
|
||||
|
||||
|
@ -35,11 +36,17 @@ Zammad::Application.routes.draw do
|
|||
end
|
||||
end
|
||||
|
||||
member do
|
||||
resource :permissions, controller: 'knowledge_base/permissions', only: %i[update show]
|
||||
end
|
||||
|
||||
resources :categories, controller: 'knowledge_base/categories',
|
||||
except: %i[new edit] do
|
||||
|
||||
member do
|
||||
patch :reorder_categories, :reorder_answers
|
||||
|
||||
resource :permissions, controller: 'knowledge_base/permissions', only: %i[update show]
|
||||
end
|
||||
|
||||
collection do
|
||||
|
|
|
@ -108,6 +108,18 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
|||
t.timestamps # rubocop:disable Zammad/ExistsDateTimePrecision
|
||||
end
|
||||
|
||||
create_table :knowledge_base_permissions do |t|
|
||||
t.references :permissionable, polymorphic: true, null: false, index: { name: 'index_knowledge_base_permissions_on_permissionable' }
|
||||
t.references :role, null: false, foreign_key: { to_table: :roles }
|
||||
|
||||
t.string 'access', limit: 50, default: 'full', null: false
|
||||
t.index 'access'
|
||||
|
||||
t.index %i[role_id permissionable_id permissionable_type], unique: true, name: 'knowledge_base_permissions_uniqueness'
|
||||
|
||||
t.timestamps limit: 3
|
||||
end
|
||||
|
||||
Setting.create_if_not_exists(
|
||||
title: 'Kb multi-lingual support',
|
||||
name: 'kb_multi_lingual_support',
|
||||
|
@ -173,7 +185,8 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
|||
note: 'Access %s',
|
||||
preferences: {
|
||||
translations: ['Knowledge Base']
|
||||
}
|
||||
},
|
||||
allow_signup: true,
|
||||
)
|
||||
|
||||
Permission.create_if_not_exists(
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
# Using older 5.0 migration to stick to Integer primary keys. Otherwise migration fails in MySQL.
|
||||
class CreateKnowledgeBasePermissions < ActiveRecord::Migration[5.0]
|
||||
def change
|
||||
return if !Setting.exists?(name: 'system_init_done')
|
||||
|
||||
create_table :knowledge_base_permissions do |t|
|
||||
t.references :permissionable, polymorphic: true, null: false, index: { name: 'index_knowledge_base_permissions_on_permissionable' }
|
||||
t.references :role, null: false, foreign_key: { to_table: :roles }
|
||||
|
||||
t.string 'access', limit: 50, default: 'full', null: false
|
||||
t.index 'access'
|
||||
|
||||
t.index %i[role_id permissionable_id permissionable_type], unique: true, name: 'knowledge_base_permissions_uniqueness'
|
||||
|
||||
t.timestamps limit: 3
|
||||
end
|
||||
|
||||
Permission.where(name: 'knowledge_base.reader').update_all(allow_signup: true) # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
end
|
|
@ -445,7 +445,8 @@ Permission.create_if_not_exists(
|
|||
note: __('Manage %s'),
|
||||
preferences: {
|
||||
translations: [__('Knowledge Base Reader')]
|
||||
}
|
||||
},
|
||||
allow_signup: true,
|
||||
)
|
||||
|
||||
admin = Role.find_by(name: 'Admin')
|
||||
|
|
|
@ -5163,6 +5163,10 @@ msgstr ""
|
|||
msgid "Invalid payload, need data:image in logo param"
|
||||
msgstr ""
|
||||
|
||||
#: lib/knowledge_base/permissions_update.rb
|
||||
msgid "Invalid permissions, do not lock out yourself"
|
||||
msgstr ""
|
||||
|
||||
#: app/models/concerns/checks_condition_validation.rb
|
||||
msgid "Invalid ticket selector conditions"
|
||||
msgstr ""
|
||||
|
@ -7613,6 +7617,7 @@ msgstr ""
|
|||
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||
#: app/assets/javascripts/app/controllers/role.coffee
|
||||
#: app/assets/javascripts/app/views/integration/ldap.jst.eco
|
||||
#: app/assets/javascripts/app/views/knowledge_base/permissions_dialog.jst.eco
|
||||
msgid "Role"
|
||||
msgstr ""
|
||||
|
||||
|
@ -9570,6 +9575,14 @@ msgstr ""
|
|||
msgid "URL (AJAX endpoint)"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/permissions_dialog.coffee
|
||||
msgid "Unable to load changes"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/permissions_dialog.coffee
|
||||
msgid "Unable to save changes"
|
||||
msgstr ""
|
||||
|
||||
#: app/controllers/first_steps_controller.rb
|
||||
#: db/seeds/overviews.rb
|
||||
msgid "Unassigned & Open Tickets"
|
||||
|
@ -10024,11 +10037,11 @@ msgid "Visibility"
|
|||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
||||
msgid "Visible to agents & editors"
|
||||
msgid "Visible to everyone"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
||||
msgid "Visible to everyone"
|
||||
msgid "Visible to readers & editors"
|
||||
msgstr ""
|
||||
|
||||
#: app/assets/javascripts/app/controllers/_integration/placetel.coffee
|
||||
|
|
|
@ -37,3 +37,24 @@ module ActiveRecord
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
module Enumerable
|
||||
def pluck_as_hash(*column_names)
|
||||
column_names.flatten! # flatten args in case array was given
|
||||
|
||||
pluck(*column_names)
|
||||
.map { |elem| pluck_as_hash_map(column_names, elem) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def pluck_as_hash_map(keys, values)
|
||||
if keys.one?
|
||||
{
|
||||
keys.first => values
|
||||
}
|
||||
else
|
||||
keys.zip(values).to_h
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
33
lib/knowledge_base/category/permission.rb
Normal file
33
lib/knowledge_base/category/permission.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase
|
||||
class Category
|
||||
class Permission
|
||||
def initialize(category)
|
||||
@category = category
|
||||
end
|
||||
|
||||
def permissions_effective
|
||||
parents_for_category
|
||||
.map(&:permissions)
|
||||
.flatten
|
||||
.each_with_object([]) do |elem, memo|
|
||||
memo << elem if !memo.find { |added| added.role == elem.role }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parents_for_category
|
||||
categories_tree = @category.self_with_parents
|
||||
|
||||
categories_with_permissions = KnowledgeBase::Category.where(id: categories_tree).includes(:permissions).to_a
|
||||
|
||||
sorted_with_permissions = categories_tree
|
||||
.map { |elem| categories_with_permissions.find { |elem_with_permissions| elem_with_permissions == elem } }
|
||||
|
||||
sorted_with_permissions + [@category.knowledge_base]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
58
lib/knowledge_base/effective_permission.rb
Normal file
58
lib/knowledge_base/effective_permission.rb
Normal file
|
@ -0,0 +1,58 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase
|
||||
class EffectivePermission
|
||||
def initialize(user, object)
|
||||
@user = user
|
||||
@object = object
|
||||
end
|
||||
|
||||
def access_effective
|
||||
return 'none' if !@user
|
||||
|
||||
@user.roles.reduce('none') do |memo, role|
|
||||
access = access_role_effective(role)
|
||||
|
||||
return access if access == 'editor'
|
||||
|
||||
memo == 'reader' ? memo : access
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permissions
|
||||
@permissions ||= @object.permissions_effective
|
||||
end
|
||||
|
||||
def access_role_effective(role)
|
||||
permission = permissions.find { |elem| elem.role == role }
|
||||
|
||||
return default_role_access(role) if !permission
|
||||
|
||||
calculate_role(role, permission)
|
||||
end
|
||||
|
||||
def calculate_role(role, permission)
|
||||
if permission.access == 'editor' && role.with_permission?('knowledge_base.editor')
|
||||
'editor'
|
||||
elsif %w[editor reader].include?(permission.access) && role.with_permission?(%w[knowledge_base.editor knowledge_base.reader])
|
||||
'reader'
|
||||
elsif @object.public_content?
|
||||
'public_reader'
|
||||
else
|
||||
'none'
|
||||
end
|
||||
end
|
||||
|
||||
def default_role_access(role)
|
||||
if role.with_permission?('knowledge_base.editor')
|
||||
'editor'
|
||||
elsif role.with_permission?('knowledge_base.reader')
|
||||
'reader'
|
||||
else
|
||||
'none'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
120
lib/knowledge_base/internal_assets.rb
Normal file
120
lib/knowledge_base/internal_assets.rb
Normal file
|
@ -0,0 +1,120 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase
|
||||
class InternalAssets
|
||||
CategoriesCache = Struct.new(:editor, :reader, :public_reader, keyword_init: true) do
|
||||
def all
|
||||
editor + reader + public_reader
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :assets
|
||||
|
||||
def initialize(user, answer_translation_content_ids: [])
|
||||
@user = user
|
||||
@assets = {}
|
||||
@answer_translation_content_ids = answer_translation_content_ids
|
||||
end
|
||||
|
||||
def collect_assets
|
||||
collect_base_assets
|
||||
|
||||
add_to_assets accessible_categories.all, type: :essential
|
||||
add_to_assets KnowledgeBase::Category::Translation.where(category: accessible_categories.all)
|
||||
|
||||
collect_all_answer_assets
|
||||
|
||||
@assets
|
||||
end
|
||||
|
||||
def accessible_categories
|
||||
@accessible_categories ||= accessible_categories_calculate
|
||||
end
|
||||
|
||||
def all_answer_ids
|
||||
all_answer_batches.each_with_object([]) do |elem, sum|
|
||||
sum.concat elem.pluck(:id)
|
||||
end
|
||||
end
|
||||
|
||||
def all_category_ids
|
||||
accessible_categories.all.pluck(:id)
|
||||
end
|
||||
|
||||
def visible_ids
|
||||
{
|
||||
answer_ids: all_answer_ids,
|
||||
category_ids: all_category_ids
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def accessible_categories_calculate
|
||||
struct = CategoriesCache.new editor: [], reader: [], public_reader: []
|
||||
|
||||
KnowledgeBase::Category.all.find_in_batches do |group|
|
||||
group.each do |cat|
|
||||
case KnowledgeBase::EffectivePermission.new(@user, cat).access_effective
|
||||
when 'editor'
|
||||
struct.editor << cat
|
||||
when 'reader'
|
||||
struct.reader << cat if cat.internal_content?
|
||||
when 'public_reader'
|
||||
struct.public_reader << cat if cat.public_content?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
struct
|
||||
end
|
||||
|
||||
def add_to_assets(objects, type: nil)
|
||||
@assets = ApplicationModel::CanAssets.reduce(objects, @assets, type)
|
||||
end
|
||||
|
||||
def collect_base_assets
|
||||
[KnowledgeBase, KnowledgeBase::Translation, KnowledgeBase::Locale]
|
||||
.each do |klass|
|
||||
klass.find_in_batches do |group|
|
||||
add_to_assets group, type: :essential
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def all_answer_batches
|
||||
[
|
||||
KnowledgeBase::Answer.where(category: accessible_categories.editor),
|
||||
KnowledgeBase::Answer.internal.where(category: accessible_categories.reader),
|
||||
KnowledgeBase::Answer.published.where(category: accessible_categories.public_reader)
|
||||
]
|
||||
end
|
||||
|
||||
def collect_all_answer_assets
|
||||
all_answer_batches.each do |batch|
|
||||
collect_answers_assets batch
|
||||
end
|
||||
end
|
||||
|
||||
def collect_answers_assets(scope)
|
||||
scope.find_in_batches do |group|
|
||||
add_to_assets group, type: :essential
|
||||
|
||||
translations = KnowledgeBase::Answer::Translation.where(answer: group)
|
||||
|
||||
add_to_assets translations, type: :essential
|
||||
|
||||
if @answer_translation_content_ids.present?
|
||||
contents = KnowledgeBase::Answer::Translation::Content
|
||||
.joins(:translation)
|
||||
.where(
|
||||
id: @answer_translation_content_ids,
|
||||
knowledge_base_answer_translations: { answer_id: group }
|
||||
)
|
||||
|
||||
add_to_assets contents
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
97
lib/knowledge_base/permissions_update.rb
Normal file
97
lib/knowledge_base/permissions_update.rb
Normal file
|
@ -0,0 +1,97 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
class KnowledgeBase
|
||||
class PermissionsUpdate
|
||||
def initialize(object, user = nil)
|
||||
@object = object
|
||||
@user = user
|
||||
end
|
||||
|
||||
def update!(**roles_to_permissions)
|
||||
ActiveRecord::Base.transaction do
|
||||
update_object(roles_to_permissions)
|
||||
|
||||
next if !@object.changed_for_autosave?
|
||||
|
||||
@object.save!
|
||||
|
||||
update_all_children
|
||||
ensure_editable!
|
||||
end
|
||||
end
|
||||
|
||||
def update_using_params!(params)
|
||||
roles_to_permissions = params[:permissions].transform_keys { |key| Role.find key }
|
||||
|
||||
update!(**roles_to_permissions)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_object(roles_to_permissions)
|
||||
@object.permissions.reject { |elem| roles_to_permissions.key? elem.role }.each(&:mark_for_destruction)
|
||||
|
||||
roles_to_permissions.each do |role, access|
|
||||
update_object_permission(role, access)
|
||||
end
|
||||
end
|
||||
|
||||
def update_object_permission(role, access)
|
||||
permission = @object.permissions.detect { |elem| elem.role == role } || @object.permissions.build(role: role)
|
||||
permission.access = access
|
||||
|
||||
mark_permission_for_cleanup_if_needed(permission, parent_object_permissions)
|
||||
end
|
||||
|
||||
def parent_object_permissions
|
||||
@parent_object_permissions ||= begin
|
||||
if @object.is_a? KnowledgeBase::Category
|
||||
(@object.parent || @object.knowledge_base).permissions_effective || []
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def all_children
|
||||
case @object
|
||||
when KnowledgeBase::Category
|
||||
@object.self_with_children - [@object]
|
||||
when KnowledgeBase
|
||||
@object.categories.root.map(&:self_with_children).flatten
|
||||
end
|
||||
end
|
||||
|
||||
def update_single_child(child)
|
||||
inherited_permissions = child.permissions_effective
|
||||
|
||||
child.permissions.each do |child_permission|
|
||||
mark_permission_for_cleanup_if_needed(child_permission, inherited_permissions)
|
||||
end
|
||||
|
||||
child.changed_for_autosave? ? child.save! : child.touch # rubocop:disable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
def update_all_children
|
||||
all_children.each do |child|
|
||||
update_single_child(child)
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_editable!
|
||||
return if !@user
|
||||
return if KnowledgeBase::EffectivePermission.new(@user, @object).access_effective == 'editor'
|
||||
|
||||
raise Exceptions::UnprocessableEntity, __('Invalid permissions, do not lock out yourself')
|
||||
end
|
||||
|
||||
def mark_permission_for_cleanup_if_needed(permission, parents)
|
||||
matching = parents.find { |elem| elem.role == permission.role }
|
||||
|
||||
return if !matching
|
||||
return if matching.access != permission.access && matching.access != 'none'
|
||||
|
||||
permission.mark_for_destruction
|
||||
end
|
||||
end
|
||||
end
|
9
spec/factories/knowledge_base_permissions.rb
Normal file
9
spec/factories/knowledge_base_permissions.rb
Normal file
|
@ -0,0 +1,9 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
FactoryBot.define do
|
||||
factory 'knowledge_base/permission', aliases: %i[knowledge_base_permission] do
|
||||
permissionable { create(:knowledge_base_category) }
|
||||
role { create(:role) }
|
||||
access { 'editor' }
|
||||
end
|
||||
end
|
|
@ -6,6 +6,12 @@ FactoryBot.define do
|
|||
created_by_id { 1 }
|
||||
updated_by_id { 1 }
|
||||
|
||||
transient do
|
||||
permission_names { nil }
|
||||
end
|
||||
|
||||
permissions { Permission.where(name: permission_names) }
|
||||
|
||||
factory :agent_role do
|
||||
permissions { Permission.where(name: 'ticket.agent') }
|
||||
end
|
||||
|
|
73
spec/lib/knowledge_base/category/permission_spec.rb
Normal file
73
spec/lib/knowledge_base/category/permission_spec.rb
Normal file
|
@ -0,0 +1,73 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe KnowledgeBase::Category::Permission do
|
||||
include_context 'basic Knowledge Base'
|
||||
|
||||
describe '#permissions_effective' do
|
||||
let(:child_category) { create(:knowledge_base_category, knowledge_base: knowledge_base, parent: category) }
|
||||
let(:child_permission) { create(:knowledge_base_permission, permissionable: child_category, access: 'reader') }
|
||||
let(:parent_permission) { create(:knowledge_base_permission, permissionable: category) }
|
||||
|
||||
context 'when no permissions exist' do
|
||||
it 'parent category returns nil' do
|
||||
expect(described_class.new(category).permissions_effective).to be_blank
|
||||
end
|
||||
|
||||
it 'child category returns nil' do
|
||||
expect(described_class.new(child_category).permissions_effective).to be_blank
|
||||
end
|
||||
end
|
||||
|
||||
context 'when parent category has permissions' do
|
||||
before { parent_permission && child_category }
|
||||
|
||||
it 'parent category returns parent permission' do
|
||||
expect(described_class.new(category).permissions_effective).to eq [parent_permission]
|
||||
end
|
||||
|
||||
it 'child category returns parent permission' do
|
||||
expect(described_class.new(child_category).permissions_effective).to eq [parent_permission]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when child category has permissions' do
|
||||
before { child_permission }
|
||||
|
||||
it 'parent category returns parent permission' do
|
||||
expect(described_class.new(category).permissions_effective).to be_blank
|
||||
end
|
||||
|
||||
it 'child category returns parent permission' do
|
||||
expect(described_class.new(child_category).permissions_effective).to eq [child_permission]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both parent and child categories have permissions' do
|
||||
before { child_permission && parent_permission }
|
||||
|
||||
it 'parent category returns parent permission' do
|
||||
expect(described_class.new(category).permissions_effective).to eq [parent_permission]
|
||||
end
|
||||
|
||||
it 'child category returns parent permission' do
|
||||
expect(described_class.new(child_category).permissions_effective).to eq [child_permission, parent_permission]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when both parent child categories have colliding permissions for the same role' do
|
||||
let(:child_permission_on_same_role) { create(:knowledge_base_permission, permissionable: child_category, access: 'reader', role: parent_permission.role) }
|
||||
|
||||
before { parent_permission && child_permission && child_permission_on_same_role }
|
||||
|
||||
it 'parent category returns parent permission' do
|
||||
expect(described_class.new(category).permissions_effective).to eq [parent_permission]
|
||||
end
|
||||
|
||||
it 'child category returns child permission override' do
|
||||
expect(described_class.new(child_category).permissions_effective).to eq [child_permission, child_permission_on_same_role]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
91
spec/lib/knowledge_base/effective_permission_spec.rb
Normal file
91
spec/lib/knowledge_base/effective_permission_spec.rb
Normal file
|
@ -0,0 +1,91 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe KnowledgeBase::EffectivePermission do
|
||||
include_context 'basic Knowledge Base'
|
||||
|
||||
describe '#access_effective' do
|
||||
let(:role_editor) { create(:role, permission_names: 'knowledge_base.editor') }
|
||||
let(:role_reader) { create(:role, permission_names: 'knowledge_base.reader') }
|
||||
let(:role_non_kb) { create(:role, :admin) }
|
||||
|
||||
let(:user) { create(:user, roles: [role_editor, role_reader, role_non_kb]) }
|
||||
let(:user_editor) { create(:user, roles: [role_editor]) }
|
||||
let(:user_admin) { create(:admin) }
|
||||
let(:user_reader) { create(:user, roles: [role_reader]) }
|
||||
let(:user_nonkb) { create(:user, roles: [role_non_kb]) }
|
||||
|
||||
let(:child_category) { create(:knowledge_base_category, parent: category) }
|
||||
|
||||
it 'editor with no permissions defined returns editor' do
|
||||
expect(described_class.new(user_editor, category).access_effective).to eq 'editor'
|
||||
end
|
||||
|
||||
it 'user with multiple permissions defined returns editor' do
|
||||
expect(described_class.new(user, category).access_effective).to eq 'editor'
|
||||
end
|
||||
|
||||
it 'reader with no permissions defined returns reader' do
|
||||
expect(described_class.new(user_reader, category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'non-kb with no permissions defined returns none' do
|
||||
expect(described_class.new(user_nonkb, category).access_effective).to eq 'none'
|
||||
end
|
||||
|
||||
it 'editor with both reader and editor permissions returns editor' do
|
||||
create_permission(role_reader, 'reader')
|
||||
create_permission(role_editor, 'editor')
|
||||
|
||||
expect(described_class.new(user_admin, category).access_effective).to eq 'editor'
|
||||
end
|
||||
|
||||
it 'editor with reader permission on parent category returns reader' do
|
||||
create_permission(role_editor, 'reader')
|
||||
|
||||
expect(described_class.new(user_editor, child_category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'editor with reader permission on KB returns reader' do
|
||||
create_permission(role_editor, 'reader', permissionable: knowledge_base)
|
||||
|
||||
expect(described_class.new(user_editor, category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'editor with reader permission on parent category but editor permission on category returns editor' do
|
||||
create_permission(role_editor, 'reader', permissionable: category)
|
||||
create_permission(role_editor, 'editor', permissionable: child_category)
|
||||
|
||||
expect(described_class.new(user_editor, child_category).access_effective).to eq 'editor'
|
||||
end
|
||||
|
||||
it 'editor with editor permission on parent category but reader permission on category returns reader' do
|
||||
create_permission(role_editor, 'editor', permissionable: category)
|
||||
create_permission(role_editor, 'reader', permissionable: child_category)
|
||||
|
||||
expect(described_class.new(user_editor, child_category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'reader with reader and non-effective permissions returns reader' do
|
||||
create_permission(role_reader, 'reader')
|
||||
create_permission(role_editor, 'editor')
|
||||
|
||||
expect(described_class.new(user_reader, category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'reader with no matching permissions returns reader' do
|
||||
create_permission(role_editor, 'editor')
|
||||
|
||||
expect(described_class.new(user_reader, category).access_effective).to eq 'reader'
|
||||
end
|
||||
|
||||
it 'retuns none when user not given' do
|
||||
expect(described_class.new(nil, category).access_effective).to eq 'none'
|
||||
end
|
||||
end
|
||||
|
||||
def create_permission(role, access, permissionable: category)
|
||||
create(:knowledge_base_permission, role: role, permissionable: permissionable, access: access)
|
||||
end
|
||||
end
|
87
spec/lib/knowledge_base/internal_assets_spec.rb
Normal file
87
spec/lib/knowledge_base/internal_assets_spec.rb
Normal file
|
@ -0,0 +1,87 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe KnowledgeBase::InternalAssets do
|
||||
include_context 'basic Knowledge Base' do
|
||||
before do
|
||||
draft_answer
|
||||
internal_answer
|
||||
published_answer
|
||||
end
|
||||
end
|
||||
|
||||
describe '#collect_assets' do
|
||||
subject(:assets) { described_class.new(user).collect_assets }
|
||||
|
||||
context 'when for KB editor' do
|
||||
let(:user) { create(:user, roles: Role.where(name: 'Admin')) }
|
||||
|
||||
it 'returns assets for all KB objects' do
|
||||
expect(assets).to include_assets_of(knowledge_base, category, draft_answer, internal_answer, published_answer)
|
||||
end
|
||||
|
||||
context 'when has editor permission' do
|
||||
before do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! user.roles.first => 'editor'
|
||||
end
|
||||
|
||||
it 'returns assets for all KB objects' do
|
||||
expect(assets).to include_assets_of(knowledge_base, category, draft_answer, internal_answer, published_answer)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has reader permission' do
|
||||
before do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! user.roles.first => 'reader'
|
||||
end
|
||||
|
||||
it 'returns assets for internally visible KB objects' do
|
||||
expect(assets)
|
||||
.to include_assets_of(knowledge_base, category, internal_answer, published_answer)
|
||||
.and not_include_assets_of(draft_answer)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when has none permission' do
|
||||
before do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! user.roles.first => 'none'
|
||||
end
|
||||
|
||||
it 'does not return assets for internally visible KB objects' do
|
||||
published_answer.destroy # make sure public item does not make category visible
|
||||
|
||||
expect(assets)
|
||||
.to include_assets_of(knowledge_base)
|
||||
.and not_include_assets_of(category, draft_answer, internal_answer, published_answer)
|
||||
end
|
||||
|
||||
it 'returns assets for published answer and it\'s category' do
|
||||
expect(assets)
|
||||
.to include_assets_of(knowledge_base, category, published_answer)
|
||||
.and not_include_assets_of(draft_answer, internal_answer)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when for agent' do
|
||||
let(:user) { create(:agent) }
|
||||
|
||||
it 'returns assets for all KB objects' do
|
||||
expect(assets)
|
||||
.to include_assets_of(knowledge_base, category, internal_answer, published_answer)
|
||||
.and not_include_assets_of(draft_answer)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when for customer' do
|
||||
let(:user) { create(:customer) }
|
||||
|
||||
it 'returns assets for all KB objects' do
|
||||
expect(assets)
|
||||
.to include_assets_of(knowledge_base)
|
||||
.and not_include_assets_of(category, draft_answer, internal_answer, published_answer)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
180
spec/lib/knowledge_base/permissions_update_spec.rb
Normal file
180
spec/lib/knowledge_base/permissions_update_spec.rb
Normal file
|
@ -0,0 +1,180 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe KnowledgeBase::PermissionsUpdate do
|
||||
describe '#update!' do
|
||||
include_context 'basic Knowledge Base'
|
||||
let(:role_editor) { create(:role, permission_names: %w[knowledge_base.editor]) }
|
||||
let(:role_another) { create(:role, permission_names: %w[knowledge_base.editor]) }
|
||||
let(:child_category) { create(:knowledge_base_category, parent: category) }
|
||||
|
||||
describe 'updating itself' do
|
||||
shared_examples 'updating itself' do |object_name:|
|
||||
let(:object) { send(object_name) }
|
||||
it 'adds role permission for self' do
|
||||
described_class.new(object).update! role_editor => 'editor'
|
||||
|
||||
expect(object.permissions)
|
||||
.to contain_exactly have_attributes(permissionable: object, role: role_editor, access: 'editor')
|
||||
end
|
||||
|
||||
it 'adds additional role permission for self' do
|
||||
described_class.new(object).update! role_editor => 'reader'
|
||||
described_class.new(object).update! role_editor => 'reader', role_another => 'reader'
|
||||
|
||||
expect(object.permissions)
|
||||
.to contain_exactly(have_attributes(role: role_editor), have_attributes(role: role_another))
|
||||
end
|
||||
|
||||
it 'does not update when re-adding an existing permission' do
|
||||
described_class.new(object).update! role_editor => 'reader'
|
||||
|
||||
travel 1.minute
|
||||
|
||||
expect { described_class.new(object).update! role_editor => 'reader' }
|
||||
.not_to change(object, :updated_at)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when saving role on KB itself' do
|
||||
include_context 'updating itself', object_name: :knowledge_base
|
||||
end
|
||||
|
||||
context 'when saving role on KB category' do
|
||||
include_context 'updating itself', object_name: :category
|
||||
end
|
||||
end
|
||||
|
||||
describe 'updating descendants' do
|
||||
context 'when saving role on KB itself' do
|
||||
it 'adds effective permissions to descendant categories' do
|
||||
described_class.new(knowledge_base).update! role_editor => 'reader'
|
||||
|
||||
expect(category.permissions_effective)
|
||||
.to contain_exactly have_attributes(role: role_editor, access: 'reader', permissionable: knowledge_base)
|
||||
end
|
||||
|
||||
it 'removing permission opens up access to descendants' do
|
||||
described_class.new(knowledge_base).update! role_editor => 'editor'
|
||||
described_class.new(knowledge_base).update!(**{})
|
||||
|
||||
expect(category.permissions_effective).to be_blank
|
||||
end
|
||||
|
||||
context 'when category has editor role has editor role with editor permission' do
|
||||
before do
|
||||
described_class.new(category).update! role_editor => 'editor'
|
||||
category.reload
|
||||
|
||||
travel 1.minute
|
||||
end
|
||||
|
||||
it 'removes identical permissions on descendant roles' do
|
||||
described_class.new(knowledge_base).update! role_editor => 'editor'
|
||||
category.reload
|
||||
|
||||
expect(category.permissions_effective)
|
||||
.to contain_exactly have_attributes(role: role_editor, access: 'editor', permissionable: knowledge_base)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when saving role on KB category' do
|
||||
it 'adds effective permissions to descendant roles' do
|
||||
described_class.new(category).update! role_editor => 'reader'
|
||||
|
||||
expect(child_category.permissions_effective)
|
||||
.to contain_exactly have_attributes(role: role_editor, access: 'reader', permissionable: category)
|
||||
end
|
||||
|
||||
context 'when child category has editor role with editor permission' do
|
||||
before do
|
||||
described_class.new(child_category).update! role_editor => 'editor'
|
||||
category.reload
|
||||
child_category.reload
|
||||
|
||||
travel 1.minute
|
||||
end
|
||||
|
||||
it 'removes conflicting permissions on descendant roles' do
|
||||
described_class.new(category).update! role_editor => 'none'
|
||||
category.reload
|
||||
child_category.reload
|
||||
|
||||
expect(child_category.permissions_effective)
|
||||
.to contain_exactly have_attributes(role: role_editor, access: 'none', permissionable: category)
|
||||
end
|
||||
|
||||
it 'removes identical permissions on descendant roles' do
|
||||
described_class.new(category).update! role_editor => 'editor'
|
||||
category.reload
|
||||
child_category.reload
|
||||
|
||||
expect(child_category.permissions_effective)
|
||||
.to contain_exactly have_attributes(role: role_editor, access: 'editor', permissionable: category)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when category has role editor with none permission' do
|
||||
before do
|
||||
described_class.new(category).update! role_editor => 'none'
|
||||
category.reload
|
||||
|
||||
travel 1.minute
|
||||
end
|
||||
|
||||
it 'removing permission opens up access to descendants' do
|
||||
described_class.new(category).update!(**{})
|
||||
category.reload
|
||||
|
||||
expect(child_category.permissions_effective).to be_blank
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'preventing user lockout' do
|
||||
let(:user) { create(:admin) }
|
||||
let(:role) { user.roles.first }
|
||||
|
||||
shared_examples 'preventing user lockout' do |object_name:|
|
||||
let(:object) { send(object_name) }
|
||||
|
||||
it 'raises an error when saving a lockout change for a given user' do
|
||||
expect { described_class.new(object, user).update! role => 'reader' }
|
||||
.to raise_error(Exceptions::UnprocessableEntity)
|
||||
end
|
||||
|
||||
it 'allows to save same change without a user' do
|
||||
expect { described_class.new(object).update! role => 'reader' }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when saving role on KB itself' do
|
||||
include_context 'preventing user lockout', object_name: 'knowledge_base'
|
||||
end
|
||||
|
||||
context 'when saving role on KB category' do
|
||||
include_context 'preventing user lockout', object_name: 'category'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update_using_params!' do
|
||||
subject(:updater) { described_class.new(category) }
|
||||
|
||||
let(:role) { create(:role, permission_names: %w[knowledge_base.editor]) }
|
||||
let(:category) { create(:knowledge_base_category) }
|
||||
|
||||
it 'calls update! with given roles' do
|
||||
updater.update_using_params!({ permissions: { role.id => 'editor' } })
|
||||
expect(category.permissions.first).to have_attributes(role: role, access: 'editor', permissionable: category)
|
||||
end
|
||||
|
||||
it 'raises an error when given a non existant role' do
|
||||
expect { updater.update_using_params!({ permissions: { (role.id + 1) => 'editor' } }) }
|
||||
.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,7 @@ RSpec.describe KnowledgeBase::Category, type: :model, current_user_id: 1 do
|
|||
|
||||
it { is_expected.to have_many(:answers) }
|
||||
it { is_expected.to have_many(:children) }
|
||||
it { is_expected.to have_many(:permissions) }
|
||||
it { is_expected.to belong_to(:parent).optional }
|
||||
it { is_expected.to belong_to(:knowledge_base) }
|
||||
|
||||
|
|
50
spec/models/knowledge_base/permission_spec.rb
Normal file
50
spec/models/knowledge_base/permission_spec.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
require 'models/contexts/factory_context'
|
||||
|
||||
RSpec.describe KnowledgeBase::Permission, type: :model do
|
||||
subject(:kb_category_permission) { create(:knowledge_base_permission) }
|
||||
|
||||
include_context 'basic Knowledge Base'
|
||||
include_context 'factory'
|
||||
|
||||
describe '#permissionable' do
|
||||
it { is_expected.to belong_to(:permissionable).touch(true) }
|
||||
|
||||
it 'allows multiple permissions for the same category' do
|
||||
permission = build(:knowledge_base_permission, permissionable: kb_category_permission.permissionable)
|
||||
permission.save
|
||||
|
||||
expect(permission).to be_persisted
|
||||
end
|
||||
|
||||
it 'does not allow same role/permission conbination' do
|
||||
permission = build(:knowledge_base_permission,
|
||||
permissionable: kb_category_permission.permissionable,
|
||||
role: kb_category_permission.role)
|
||||
permission.save
|
||||
|
||||
expect(permission).not_to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
describe '#role' do
|
||||
it { is_expected.to belong_to(:role) }
|
||||
|
||||
it 'allows multiple permissions for the same category' do
|
||||
permission = build(:knowledge_base_permission, role: kb_category_permission.role)
|
||||
permission.save
|
||||
|
||||
expect(permission).to be_persisted
|
||||
end
|
||||
end
|
||||
|
||||
describe '#access' do
|
||||
it { is_expected.to validate_presence_of(:access).with_message(%r{}) }
|
||||
it { is_expected.to allow_value('editor').for(:access) }
|
||||
it { is_expected.to allow_value('reader').for(:access) }
|
||||
it { is_expected.to allow_value('none').for(:access) }
|
||||
it { is_expected.not_to allow_value('foobar').for(:access) }
|
||||
end
|
||||
end
|
64
spec/policies/knowledge_base/answer_policy_spec.rb
Normal file
64
spec/policies/knowledge_base/answer_policy_spec.rb
Normal file
|
@ -0,0 +1,64 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
require 'policies/knowledge_base_policy_examples'
|
||||
|
||||
describe KnowledgeBase::AnswerPolicy do
|
||||
subject(:policy) { described_class.new(user, record) }
|
||||
|
||||
let(:record) { create(:knowledge_base_answer) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
shared_context 'with answer visibility' do |visible:, visible_internally:|
|
||||
before do
|
||||
allow(record).to receive(:visible?).and_return(visible)
|
||||
allow(record).to receive(:visible_internally?).and_return(visible_internally)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show?' do
|
||||
context 'when visible and visible internally' do
|
||||
include_examples 'with answer visibility', visible: true, visible_internally: true
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: true, method: :show?
|
||||
end
|
||||
|
||||
context 'when visible internally only' do
|
||||
include_examples 'with answer visibility', visible: false, visible_internally: true
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: false, method: :show?
|
||||
end
|
||||
|
||||
context 'when not visible' do
|
||||
include_examples 'with answer visibility', visible: false, visible_internally: false
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :show?
|
||||
end
|
||||
end
|
||||
|
||||
describe '#show_public?' do
|
||||
context 'when visible and visible internally' do
|
||||
include_examples 'with answer visibility', visible: true, visible_internally: true
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: true, method: :show_public?
|
||||
end
|
||||
|
||||
context 'when visible internally only' do
|
||||
include_examples 'with answer visibility', visible: false, visible_internally: true
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :show_public?
|
||||
end
|
||||
|
||||
context 'when not visible' do
|
||||
include_examples 'with answer visibility', visible: false, visible_internally: false
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :show_public?
|
||||
end
|
||||
end
|
||||
|
||||
describe '#update?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :update?
|
||||
end
|
||||
|
||||
describe '#create?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :create?
|
||||
end
|
||||
|
||||
describe '#destroy?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :destroy?
|
||||
end
|
||||
end
|
45
spec/policies/knowledge_base/category_policy_spec.rb
Normal file
45
spec/policies/knowledge_base/category_policy_spec.rb
Normal file
|
@ -0,0 +1,45 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
require 'policies/knowledge_base_policy_examples'
|
||||
|
||||
describe KnowledgeBase::CategoryPolicy do
|
||||
subject(:policy) { described_class.new(user, record) }
|
||||
|
||||
let(:record) { create(:knowledge_base_category) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '#show?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: false, method: :show?
|
||||
end
|
||||
|
||||
describe '#show_public?' do
|
||||
context 'when category has public content' do
|
||||
before { allow(record).to receive(:public_content?).and_return(true) }
|
||||
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: true, method: :show_public?
|
||||
end
|
||||
|
||||
context 'when category has no public content' do
|
||||
before { allow(record).to receive(:public_content?).and_return(false) }
|
||||
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :show_public?
|
||||
end
|
||||
end
|
||||
|
||||
describe '#permissions?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :permissions?
|
||||
end
|
||||
|
||||
describe '#update?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :update?
|
||||
end
|
||||
|
||||
describe '#create?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :create?, access_method: :parent_access
|
||||
end
|
||||
|
||||
describe '#destroy?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :destroy?, access_method: :parent_access
|
||||
end
|
||||
end
|
29
spec/policies/knowledge_base_policy_examples.rb
Normal file
29
spec/policies/knowledge_base_policy_examples.rb
Normal file
|
@ -0,0 +1,29 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
RSpec.shared_context 'with KB policy check' do |editor:, reader:, none:, method:, access_method: :access|
|
||||
let(:access_method) { access_method }
|
||||
|
||||
it 'returns true if editor' do
|
||||
mock_permission 'editor'
|
||||
|
||||
expect(policy.send(method)).to be editor
|
||||
end
|
||||
|
||||
it 'returns true if reader' do
|
||||
mock_permission 'reader'
|
||||
|
||||
expect(policy.send(method)).to be reader
|
||||
end
|
||||
|
||||
it 'returns false if none' do
|
||||
mock_permission 'none'
|
||||
|
||||
expect(policy.send(method)).to be none
|
||||
end
|
||||
|
||||
def mock_permission(access)
|
||||
allow(policy)
|
||||
.to receive(access_method)
|
||||
.and_return(access)
|
||||
end
|
||||
end
|
19
spec/policies/knowledge_base_policy_spec.rb
Normal file
19
spec/policies/knowledge_base_policy_spec.rb
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
require 'policies/knowledge_base_policy_examples'
|
||||
|
||||
describe KnowledgeBasePolicy do
|
||||
subject(:policy) { described_class.new(user, record) }
|
||||
|
||||
let(:record) { create(:knowledge_base) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '#show?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: true, none: false, method: :show?
|
||||
end
|
||||
|
||||
describe 'update?' do
|
||||
include_examples 'with KB policy check', editor: true, reader: false, none: false, method: :update?
|
||||
end
|
||||
end
|
|
@ -54,7 +54,9 @@ RSpec::Matchers.define :include_assets_of do
|
|||
#
|
||||
# @return [Hash, nil]
|
||||
def find_assets_of(object, actual)
|
||||
actual.dig(object.class.name.gsub(%r{::}, ''), object.id.to_s)
|
||||
actual
|
||||
.deep_stringify_keys
|
||||
.dig(object.class.name.gsub(%r{::}, ''), object.id.to_s)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -96,18 +96,6 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
|||
let(:new_tag_name) { 'capybara_kb_tag' }
|
||||
|
||||
it 'adds a new tag' do
|
||||
within :active_content do
|
||||
click '.js-newTagLabel'
|
||||
|
||||
elem = find('.js-newTagInput')
|
||||
elem.fill_in with: new_tag_name
|
||||
elem.send_keys :return
|
||||
|
||||
expect(page).to have_css('a.js-tag', text: new_tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
it 'saves new tag to the database' do
|
||||
within :active_content do
|
||||
click '.js-newTagLabel'
|
||||
|
||||
|
@ -116,6 +104,7 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
|||
elem.send_keys :return
|
||||
|
||||
wait.until_exists { published_answer_with_tag.reload.tag_list.include? new_tag_name }
|
||||
expect(page).to have_css('a.js-tag', text: new_tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -127,22 +116,10 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
|||
|
||||
it 'deletes a tag' do
|
||||
within :active_content do
|
||||
click '.js-newTagLabel'
|
||||
|
||||
find('.list-item', text: published_answer_tag_name)
|
||||
.find('.js-delete').click
|
||||
|
||||
expect(page).to have_no_css('a.js-tag', text: published_answer_tag_name)
|
||||
end
|
||||
end
|
||||
|
||||
it 'deletes the tag from the database' do
|
||||
within :active_content do
|
||||
click '.js-newTagLabel'
|
||||
|
||||
find('.list-item', text: published_answer_tag_name)
|
||||
.find('.js-delete').click
|
||||
|
||||
wait.until_exists { published_answer_with_tag.reload.tag_list.exclude? published_answer_tag_name }
|
||||
end
|
||||
end
|
||||
|
|
177
spec/system/knowledge_base/locale/category/permissions_spec.rb
Normal file
177
spec/system/knowledge_base/locale/category/permissions_spec.rb
Normal file
|
@ -0,0 +1,177 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Knowledge Base Locale Category Permissions', type: :system do
|
||||
include_context 'basic Knowledge Base'
|
||||
|
||||
let(:role_editor) { Role.find_by name: 'Admin' }
|
||||
let(:role_another_editor) { create(:role, permission_names: %w[knowledge_base.editor]) }
|
||||
let(:role_reader) { Role.find_by name: 'Agent' }
|
||||
|
||||
let(:child_category) { create(:knowledge_base_category, parent: category) }
|
||||
|
||||
it 'shows roles with has KB permissions only' do
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_text(%r{Admin}i)
|
||||
.and(have_text(%r{Agent}i))
|
||||
.and(have_no_text(%r{Customer}i))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permissions shown' do
|
||||
it 'shows existing permissions when category has no permissions' do
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_editor.id}'][value='editor'][checked]:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_editor.id}'][value='reader']:not([disabled])", visible: :all))
|
||||
.and(have_css("input[name='#{role_editor.id}'][value='none']:not([disabled])", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows existing permissions when category has inerited permissions only' do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! role_reader => 'none'
|
||||
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_reader.id}'][value='reader']:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_reader.id}'][value='none'][checked]:not([disabled])", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows existing permissions' do
|
||||
KnowledgeBase::PermissionsUpdate.new(child_category).update! role_reader => 'none'
|
||||
|
||||
open_page child_category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_reader.id}'][value='reader']:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_reader.id}'][value='none'][checked]:not([disabled])", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows editor permission not limited by parent category being read only' do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! role_another_editor => 'reader'
|
||||
|
||||
open_page child_category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_another_editor.id}'][value='none']:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_another_editor.id}'][value='reader'][checked]:not([disabled])", visible: :all))
|
||||
.and(have_css("input[name='#{role_another_editor.id}'][value='editor'][disabled]", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows editor permissions limited by parent category' do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! role_another_editor => 'none'
|
||||
|
||||
open_page child_category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_another_editor.id}'][value='none'][checked]:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_another_editor.id}'][value='reader'][disabled]", visible: :all))
|
||||
.and(have_css("input[name='#{role_another_editor.id}'][value='editor'][disabled]", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows reader permissions limited by parent category' do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! role_reader => 'none'
|
||||
|
||||
open_page child_category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_reader.id}'][value='none'][checked]:not([disabled])", visible: :all)
|
||||
.and(have_css("input[name='#{role_reader.id}'][value='reader'][disabled]", visible: :all))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'saving changes' do
|
||||
it 'saves permissions' do
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal do
|
||||
find("input[name='#{role_reader.id}'][value='none']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
end
|
||||
|
||||
expect(category.reload.permissions)
|
||||
.to contain_exactly(
|
||||
have_attributes(role: role_reader, access: 'none', permissionable: category),
|
||||
have_attributes(role: role_editor, access: 'editor', permissionable: category)
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows to modify existing permissions' do
|
||||
KnowledgeBase::PermissionsUpdate.new(category).update! role_reader => 'none'
|
||||
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal do
|
||||
find("input[name='#{role_reader.id}'][value='reader']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
end
|
||||
|
||||
expect(category.reload.permissions)
|
||||
.to contain_exactly(
|
||||
have_attributes(role: role_reader, access: 'reader', permissionable: category),
|
||||
have_attributes(role: role_editor, access: 'editor', permissionable: category)
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow to lock user himself' do
|
||||
open_page category
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
find("input[name='#{role_editor.id}'][value='reader']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
|
||||
expect(page).to have_css('.alert')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def open_page(category)
|
||||
visit "knowledge_base/#{knowledge_base.id}/locale/#{Locale.first.locale}/category/#{category.id}/edit"
|
||||
end
|
||||
end
|
|
@ -0,0 +1,118 @@
|
|||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Knowledge Base Locale Knowledge Base Permissions', type: :system do
|
||||
include_context 'basic Knowledge Base'
|
||||
|
||||
let(:role_editor) { Role.find_by name: 'Admin' }
|
||||
let(:role_another_editor) { create(:role, permission_names: %w[knowledge_base.editor]) }
|
||||
let(:role_reader) { Role.find_by name: 'Agent' }
|
||||
|
||||
it 'shows roles with has KB permissions only' do
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_text(%r{Admin}i)
|
||||
.and(have_text(%r{Agent}i))
|
||||
.and(have_no_text(%r{Customer}i))
|
||||
end
|
||||
end
|
||||
|
||||
describe 'permissions shown' do
|
||||
it 'shows existing permissions when KB has no permissions' do
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_editor.id}'][value='editor'][checked]", visible: :all)
|
||||
.and(have_css("input[name='#{role_editor.id}'][value='reader']", visible: :all))
|
||||
end
|
||||
end
|
||||
|
||||
it 'shows existing permissions' do
|
||||
KnowledgeBase::PermissionsUpdate.new(knowledge_base).update! role_another_editor => 'reader'
|
||||
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
expect(page)
|
||||
.to have_css("input[name='#{role_another_editor.id}'][value='reader'][checked]", visible: :all)
|
||||
.and(have_css("input[name='#{role_another_editor.id}'][value='editor']", visible: :all))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'saving changes' do
|
||||
it 'saves permissions' do
|
||||
role_another_editor
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal do
|
||||
find("input[name='#{role_another_editor.id}'][value='reader']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
end
|
||||
|
||||
expect(knowledge_base.reload.permissions)
|
||||
.to contain_exactly(
|
||||
have_attributes(role: role_reader, access: 'reader', permissionable: knowledge_base),
|
||||
have_attributes(role: role_another_editor, access: 'reader', permissionable: knowledge_base),
|
||||
have_attributes(role: role_editor, access: 'editor', permissionable: knowledge_base)
|
||||
)
|
||||
end
|
||||
|
||||
it 'allows to modify existing permissions' do
|
||||
KnowledgeBase::PermissionsUpdate.new(knowledge_base).update! role_another_editor => 'reader'
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal do
|
||||
find("input[name='#{role_another_editor.id}'][value='editor']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
end
|
||||
|
||||
expect(knowledge_base.reload.permissions)
|
||||
.to contain_exactly(
|
||||
have_attributes(role: role_reader, access: 'reader', permissionable: knowledge_base),
|
||||
have_attributes(role: role_another_editor, access: 'editor', permissionable: knowledge_base),
|
||||
have_attributes(role: role_editor, access: 'editor', permissionable: knowledge_base)
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not allow to lock user himself' do
|
||||
open_page
|
||||
|
||||
find('[data-action=permissions]').click
|
||||
|
||||
in_modal disappears: false do
|
||||
find("input[name='#{role_editor.id}'][value='reader']", visible: :all)
|
||||
.ancestor('label')
|
||||
.click
|
||||
|
||||
click_on 'Submit'
|
||||
|
||||
expect(page).to have_css('.alert')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def open_page
|
||||
visit "knowledge_base/#{knowledge_base.id}/locale/#{Locale.first.locale}/edit"
|
||||
end
|
||||
end
|
|
@ -939,7 +939,6 @@ RSpec.describe 'Ticket zoom', type: :system do
|
|||
|
||||
def forward
|
||||
within :active_content do
|
||||
# binding.pry
|
||||
wait.until_exists { find('.textBubble-content .richtext-content') }
|
||||
click '.js-ArticleAction[data-type=emailForward]'
|
||||
fill_in 'To', with: 'customer@example.com'
|
||||
|
|
Loading…
Reference in a new issue