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
|
super
|
||||||
@controllerBind('config_update_local', (data) => @configUpdated(data))
|
@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')
|
if @permissionCheck('knowledge_base.*') and App.Config.get('kb_active')
|
||||||
@updateNavMenu()
|
@updateNavMenu()
|
||||||
else if App.Config.get('kb_active_publicly')
|
else if App.Config.get('kb_active_publicly')
|
||||||
|
@ -51,15 +55,8 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
@loadChange(pushed_data)
|
@loadChange(pushed_data)
|
||||||
, 1000, key, 'kb_data_changed_loading')
|
, 1000, key, 'kb_data_changed_loading')
|
||||||
)
|
)
|
||||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @renderAfterChangeLoaded
|
||||||
return if !@displayingError
|
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @renderAfterChangeLoaded
|
||||||
|
|
||||||
object = @constructor.pickObjectUsing(@lastParams, @)
|
|
||||||
|
|
||||||
if !@objectVisibleInternally(object)
|
|
||||||
return
|
|
||||||
|
|
||||||
@renderControllers(@lastParams)
|
|
||||||
|
|
||||||
@checkForUpdates()
|
@checkForUpdates()
|
||||||
|
|
||||||
|
@ -112,6 +109,19 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
notifyChangeLoaded: ->
|
notifyChangeLoaded: ->
|
||||||
App.KnowledgeBase.trigger('kb_data_change_loaded')
|
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) ->
|
active: (state) ->
|
||||||
return @shown if state is undefined
|
return @shown if state is undefined
|
||||||
@shown = state
|
@shown = state
|
||||||
|
@ -138,6 +148,10 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
if !@permissionCheckRedirect("knowledge_base.#{@requiredPermissionSuffix(params)}")
|
if !@permissionCheckRedirect("knowledge_base.#{@requiredPermissionSuffix(params)}")
|
||||||
return
|
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()?
|
if @loaded && @rendered && @lastParams && !params.knowledge_base_id && @contentController && @kb_locale()?
|
||||||
@navigate @lastParams.match[0] , { hideCurrentLocationFromHistory: true }
|
@navigate @lastParams.match[0] , { hideCurrentLocationFromHistory: true }
|
||||||
return
|
return
|
||||||
|
@ -169,6 +183,7 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
@pendingParams = params
|
@pendingParams = params
|
||||||
|
|
||||||
renderScreenErrorInContent: (text) ->
|
renderScreenErrorInContent: (text) ->
|
||||||
|
@contentController?.releaseController()
|
||||||
@contentController = undefined
|
@contentController = undefined
|
||||||
@renderScreenError(detail: text, el: @$('.page-content'))
|
@renderScreenError(detail: text, el: @$('.page-content'))
|
||||||
@displayingError = true
|
@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]
|
kb.kb_locales().filter((elem) => elem.system_locale_id == @lastParams.selectedSystemLocale.id)[0]
|
||||||
|
|
||||||
getKnowledgeBase: ->
|
getKnowledgeBase: ->
|
||||||
App.KnowledgeBase.find(@lastParams.knowledge_base_id)
|
App.KnowledgeBase.find(@lastParams?.knowledge_base_id)
|
||||||
|
|
||||||
fetchAndRender: =>
|
fetchAndRender: =>
|
||||||
@fetch(true, true)
|
@fetch(true, true)
|
||||||
|
|
||||||
fetch: (showLoader, processLoaded) ->
|
fetch: (showLoader, processLoaded, notifyVisibilityChangeLoaded) ->
|
||||||
if showLoader
|
if showLoader
|
||||||
@startLoading()
|
@startLoading()
|
||||||
|
|
||||||
|
@ -278,6 +293,9 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
|
|
||||||
if processLoaded
|
if processLoaded
|
||||||
@processLoaded()
|
@processLoaded()
|
||||||
|
|
||||||
|
if notifyVisibilityChangeLoaded
|
||||||
|
@notifyVisibilityChangeLoaded()
|
||||||
,
|
,
|
||||||
error: (xhr) =>
|
error: (xhr) =>
|
||||||
if showLoader
|
if showLoader
|
||||||
|
@ -308,10 +326,11 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
App[elem.modelName].find(id)?.remove(clear: true)
|
App[elem.modelName].find(id)?.remove(clear: true)
|
||||||
|
|
||||||
calculateIdsToDelete: (data) ->
|
calculateIdsToDelete: (data) ->
|
||||||
Object
|
App.KnowledgeBase
|
||||||
.keys(data)
|
.allKbModelNames()
|
||||||
.filter (elem) -> elem.match(/^KnowledgeBase/)
|
|
||||||
.map (model) ->
|
.map (model) ->
|
||||||
|
return {ids : []} if !data[model]
|
||||||
|
|
||||||
newIds = Object.keys data[model]
|
newIds = Object.keys data[model]
|
||||||
oldIds = App[model].all().map (elem) -> elem.id
|
oldIds = App[model].all().map (elem) -> elem.id
|
||||||
diff = oldIds.filter (elem) -> !_.includes(newIds, String(elem))
|
diff = oldIds.filter (elem) -> !_.includes(newIds, String(elem))
|
||||||
|
@ -339,8 +358,13 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
parentController: @
|
parentController: @
|
||||||
)
|
)
|
||||||
|
|
||||||
|
access: (params) ->
|
||||||
|
@constructor
|
||||||
|
.pickObjectUsing(params, @)
|
||||||
|
?.access()
|
||||||
|
|
||||||
isEditor: ->
|
isEditor: ->
|
||||||
App.User.current().permission('knowledge_base.editor')
|
@access(@lastParams) == 'editor'
|
||||||
|
|
||||||
checkForUpdates: ->
|
checkForUpdates: ->
|
||||||
@interval(@checkUpdatesAction, 10 * 60 * 1000, 'kb_interval_check')
|
@interval(@checkUpdatesAction, 10 * 60 * 1000, 'kb_interval_check')
|
||||||
|
@ -371,6 +395,27 @@ class App.KnowledgeBaseAgentController extends App.Controller
|
||||||
clicked: ->
|
clicked: ->
|
||||||
window.open(App.KnowledgeBase.first().publicBaseUrl(), '_blank')
|
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) ->
|
@pickObjectUsing: (params, parentController) ->
|
||||||
kb = parentController.getKnowledgeBase()
|
kb = parentController.getKnowledgeBase()
|
||||||
return if !kb
|
return if !kb
|
||||||
|
|
|
@ -103,7 +103,7 @@ class App.KnowledgeBaseContentCanBePublishedForm extends App.ControllerForm
|
||||||
,
|
,
|
||||||
value: 'internal'
|
value: 'internal'
|
||||||
name: __('Internal')
|
name: __('Internal')
|
||||||
note: __('Visible to agents & editors')
|
note: __('Visible to readers & editors')
|
||||||
,
|
,
|
||||||
value: 'published'
|
value: 'published'
|
||||||
name: __('Public')
|
name: __('Public')
|
||||||
|
|
|
@ -15,6 +15,12 @@ class App.KnowledgeBaseEditorCoordinator
|
||||||
parentController: @parentController
|
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
|
# built-in Spine's function doesn't work when object has no ID set and includes "undefined" in URL
|
||||||
urlFor: (object) ->
|
urlFor: (object) ->
|
||||||
if object.id
|
if object.id
|
||||||
|
|
|
@ -13,8 +13,8 @@ class App.KnowledgeBaseNavigation extends App.Controller
|
||||||
|
|
||||||
@controllerBind('knowledge_base::navigation::rerender', => @needsUpdate())
|
@controllerBind('knowledge_base::navigation::rerender', => @needsUpdate())
|
||||||
|
|
||||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @needsUpdate
|
||||||
@needsUpdate()
|
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @needsUpdate
|
||||||
|
|
||||||
buildCrumbsForRendering: (array, kb_locale, action) ->
|
buildCrumbsForRendering: (array, kb_locale, action) ->
|
||||||
if action is 'search'
|
if action is 'search'
|
||||||
|
@ -73,7 +73,7 @@ class App.KnowledgeBaseNavigation extends App.Controller
|
||||||
else
|
else
|
||||||
false
|
false
|
||||||
|
|
||||||
needsUpdate: ->
|
needsUpdate: =>
|
||||||
@show(@savedParams, @savedAction)
|
@show(@savedParams, @savedAction)
|
||||||
|
|
||||||
selectedLocaleDisplay: ->
|
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', =>
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
||||||
@renderAnswer(@object, kb_locale)
|
@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
|
if !answer
|
||||||
@parentController.renderNotFound()
|
@parentController.renderNotFound()
|
||||||
return
|
return
|
||||||
|
@ -63,13 +66,16 @@ class App.KnowledgeBaseReaderController extends App.Controller
|
||||||
@parentController.renderNotAvailableAnymore()
|
@parentController.renderNotAvailableAnymore()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
paginator = new App.KnowledgeBaseReaderPagination(object: @object, kb_locale: kb_locale)
|
||||||
|
@answerPagination.html paginator.el
|
||||||
|
|
||||||
|
if onlyVisibility
|
||||||
|
return
|
||||||
|
|
||||||
@renderAttachments(answer.attachments)
|
@renderAttachments(answer.attachments)
|
||||||
@renderTags(answer.tags)
|
@renderTags(answer.tags)
|
||||||
@renderLinkedTickets(answer.translation(kb_locale.id)?.linked_tickets())
|
@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)
|
answer_translation = answer.translation(kb_locale.id)
|
||||||
|
|
||||||
if !answer_translation
|
if !answer_translation
|
||||||
|
|
|
@ -3,13 +3,13 @@ class App.KnowledgeBaseReaderListContainer extends App.Controller
|
||||||
super
|
super
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', =>
|
@listenTo App.KnowledgeBase, 'kb_data_change_loaded', @parentRefreshed
|
||||||
@parentRefreshed()
|
@listenTo App.KnowledgeBase, 'kb_visibility_change_loaded', @parentRefreshed
|
||||||
|
|
||||||
tag: 'ul'
|
tag: 'ul'
|
||||||
className: 'sections'
|
className: 'sections'
|
||||||
|
|
||||||
parentRefreshed: ->
|
parentRefreshed: =>
|
||||||
newIds = @children().map (elem) -> elem.id
|
newIds = @children().map (elem) -> elem.id
|
||||||
oldIds = @el.children().toArray().map (elem) -> parseInt(elem.dataset.id)
|
oldIds = @el.children().toArray().map (elem) -> parseInt(elem.dataset.id)
|
||||||
|
|
||||||
|
@ -59,7 +59,9 @@ class App.KnowledgeBaseReaderListContainer.Categories extends App.KnowledgeBaseR
|
||||||
@parent.children()
|
@parent.children()
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|
||||||
# coffeelint: enable=indentation
|
# coffeelint: enable=indentation
|
||||||
|
#
|
||||||
|
|
||||||
if !@isEditor
|
if !@isEditor
|
||||||
items = items.filter (elem) => elem.visibleInternally(@kb_locale)
|
items = items.filter (elem) => elem.visibleInternally(@kb_locale)
|
||||||
|
|
|
@ -3,9 +3,16 @@ class App.KnowledgeBaseReaderListController extends App.Controller
|
||||||
super
|
super
|
||||||
@render()
|
@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()
|
if !@objectVisibleInternally()
|
||||||
@parentController.renderNotAvailableAnymore()
|
@parentController.renderNotAvailableAnymore()
|
||||||
|
return
|
||||||
|
|
||||||
|
if @renderEmptinessState != @object.isEmpty()
|
||||||
|
@render()
|
||||||
|
|
||||||
elements:
|
elements:
|
||||||
'.js-readerListContainer': 'container'
|
'.js-readerListContainer': 'container'
|
||||||
|
@ -18,11 +25,13 @@ class App.KnowledgeBaseReaderListController extends App.Controller
|
||||||
@parentController.renderNotFound()
|
@parentController.renderNotFound()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@renderEmptinessState = @object.isEmpty()
|
||||||
|
|
||||||
if @object.isEmpty()
|
if @object.isEmpty()
|
||||||
@renderScreenPlaceholder(
|
@renderScreenPlaceholder(
|
||||||
icon: App.Utils.icon('mood-ok')
|
icon: App.Utils.icon('mood-ok')
|
||||||
detail: __('This category is empty')
|
detail: __('This category is empty')
|
||||||
action: __('Start Editing')
|
action: if @parentController.isEditor() then __('Start Editing')
|
||||||
actionCallback: =>
|
actionCallback: =>
|
||||||
url = @object.uiUrl(@parentController.kb_locale(), 'edit')
|
url = @object.uiUrl(@parentController.kb_locale(), 'edit')
|
||||||
@navigate url
|
@navigate url
|
||||||
|
|
|
@ -18,7 +18,10 @@ class App.KnowledgeBaseReaderListItem extends App.Controller
|
||||||
|
|
||||||
@sort_order = @item.position
|
@sort_order = @item.position
|
||||||
|
|
||||||
|
try
|
||||||
attrs = @item.attributesForRendering(@kb_locale, isEditor: @isEditor)
|
attrs = @item.attributesForRendering(@kb_locale, isEditor: @isEditor)
|
||||||
|
catch e
|
||||||
|
attrs = {}
|
||||||
|
|
||||||
@el
|
@el
|
||||||
.prop('className')
|
.prop('className')
|
||||||
|
|
|
@ -6,6 +6,7 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
||||||
|
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
|
@renderedWidgets = []
|
||||||
@show()
|
@show()
|
||||||
|
|
||||||
@controllerBind 'knowledge_base::sidebar::rerender', => @rerender()
|
@controllerBind 'knowledge_base::sidebar::rerender', => @rerender()
|
||||||
|
@ -14,9 +15,9 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
||||||
@rerender()
|
@rerender()
|
||||||
true
|
true
|
||||||
|
|
||||||
rerender: ->
|
rerender: =>
|
||||||
@delay( =>
|
@delay( =>
|
||||||
@show(@savedParams, @savedAction)
|
@update()
|
||||||
, 300, 'rerender')
|
, 300, 'rerender')
|
||||||
|
|
||||||
contentActionClicked: (e) ->
|
contentActionClicked: (e) ->
|
||||||
|
@ -24,10 +25,15 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
||||||
actionName = switch e.target.dataset.action
|
actionName = switch e.target.dataset.action
|
||||||
when 'delete' then 'clickedDelete'
|
when 'delete' then 'clickedDelete'
|
||||||
when 'visibility' then 'clickedCanBePublished'
|
when 'visibility' then 'clickedCanBePublished'
|
||||||
|
when 'permissions' then 'clickedPermissions'
|
||||||
# coffeelint: enable=indentation
|
# coffeelint: enable=indentation
|
||||||
|
|
||||||
@parentController.bodyModal = @parentController.coordinator[actionName]?(@savedParams)
|
@parentController.bodyModal = @parentController.coordinator[actionName]?(@savedParams)
|
||||||
|
|
||||||
|
update: =>
|
||||||
|
for elem in @renderedWidgets
|
||||||
|
elem.updateIfNeeded?()
|
||||||
|
|
||||||
show: (object, action) ->
|
show: (object, action) ->
|
||||||
isEdit = action is 'edit'
|
isEdit = action is 'edit'
|
||||||
|
|
||||||
|
@ -39,17 +45,22 @@ class App.KnowledgeBaseSidebar extends App.Controller
|
||||||
if !isEdit
|
if !isEdit
|
||||||
return
|
return
|
||||||
|
|
||||||
for widget in @widgets(object)
|
@renderedWidgets = []
|
||||||
@el.append new widget(
|
|
||||||
|
for elem in @getWidgets(object)
|
||||||
|
widget = new elem(
|
||||||
object: object
|
object: object
|
||||||
kb_locale: @parentController.kb_locale()
|
kb_locale: @parentController.kb_locale()
|
||||||
parentController: @parentController
|
parentController: @parentController
|
||||||
).el
|
)
|
||||||
|
|
||||||
|
@renderedWidgets.push widget
|
||||||
|
@el.append widget.el
|
||||||
|
|
||||||
hide: ->
|
hide: ->
|
||||||
@el.addClass('hidden')
|
@el.addClass('hidden')
|
||||||
|
|
||||||
widgets: (object) ->
|
getWidgets: (object) ->
|
||||||
output = [App.KnowledgeBaseSidebarActions]
|
output = [App.KnowledgeBaseSidebarActions]
|
||||||
|
|
||||||
if object instanceof App.KnowledgeBase || object instanceof App.KnowledgeBaseCategory
|
if object instanceof App.KnowledgeBase || object instanceof App.KnowledgeBaseCategory
|
||||||
|
|
|
@ -8,6 +8,9 @@ class App.KnowledgeBaseSidebarGenericList extends App.Controller
|
||||||
constructor: ->
|
constructor: ->
|
||||||
super
|
super
|
||||||
|
|
||||||
|
@render()
|
||||||
|
|
||||||
|
render: ->
|
||||||
@html App.view('knowledge_base/sidebar/generic_list')(@templateOptions())
|
@html App.view('knowledge_base/sidebar/generic_list')(@templateOptions())
|
||||||
|
|
||||||
templateOptions: ->
|
templateOptions: ->
|
||||||
|
@ -52,3 +55,6 @@ class App.KnowledgeBaseSidebarGenericList extends App.Controller
|
||||||
|
|
||||||
urlNew: ->
|
urlNew: ->
|
||||||
#has to be overridden
|
#has to be overridden
|
||||||
|
|
||||||
|
updateIfNeeded: ->
|
||||||
|
@render()
|
||||||
|
|
|
@ -19,9 +19,9 @@ class App.KnowledgeBaseSidebarAttachments extends App.Controller
|
||||||
super
|
super
|
||||||
|
|
||||||
@render()
|
@render()
|
||||||
@listenTo @object, 'refresh', @needsUpdate
|
@listenTo @object, 'refresh', @updateIfNeeded
|
||||||
|
|
||||||
needsUpdate: =>
|
updateIfNeeded: =>
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
|
|
|
@ -12,9 +12,9 @@ class App.KnowledgeBaseSidebarLinkedTickets extends App.Controller
|
||||||
super
|
super
|
||||||
|
|
||||||
@render()
|
@render()
|
||||||
@listenTo @object, 'refresh', @needsUpdate
|
@listenTo @object, 'refresh', @updateIfNeeded
|
||||||
|
|
||||||
needsUpdate: =>
|
updateIfNeeded: =>
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
|
|
|
@ -11,3 +11,6 @@ class App.KnowledgeBaseSidebarTags extends App.Controller
|
||||||
object: @object
|
object: @object
|
||||||
tags: @object.tags
|
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()
|
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)
|
if !(@ instanceof App.KnowledgeBase)
|
||||||
buttons.push {
|
buttons.push {
|
||||||
iconName: 'trash'
|
iconName: 'trash'
|
||||||
|
|
|
@ -34,12 +34,6 @@ InstanceMethods =
|
||||||
attrs.iconFont = true
|
attrs.iconFont = true
|
||||||
attrs.icon = @category_icon
|
attrs.icon = @category_icon
|
||||||
attrs.count = @countDeepAnswers()
|
attrs.count = @countDeepAnswers()
|
||||||
|
|
||||||
if options.isEditor
|
|
||||||
attrs.editorOnly = !@visibleInternally(kb_locale)
|
|
||||||
else
|
|
||||||
attrs.editorOnly = false
|
|
||||||
|
|
||||||
attrs.state = @visibilityState(kb_locale)
|
attrs.state = @visibilityState(kb_locale)
|
||||||
|
|
||||||
if @ instanceof App.KnowledgeBaseAnswer
|
if @ instanceof App.KnowledgeBaseAnswer
|
||||||
|
@ -47,16 +41,6 @@ InstanceMethods =
|
||||||
attrs.state = @can_be_published_state()
|
attrs.state = @can_be_published_state()
|
||||||
attrs.tags = @tags
|
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 = {}
|
attrs.icons = {}
|
||||||
|
|
||||||
if attrs.missingTranslation
|
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'
|
@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 Spine.Model.Ajax
|
||||||
@extend App.KnowledgeBaseActions
|
@extend App.KnowledgeBaseActions
|
||||||
|
@extend App.KnowledgeBaseAccess
|
||||||
@url: @apiPath + '/knowledge_bases'
|
@url: @apiPath + '/knowledge_bases'
|
||||||
|
|
||||||
@manageUrl: @apiPath + '/knowledge_bases/manage'
|
@manageUrl: @apiPath + '/knowledge_bases/manage'
|
||||||
|
@ -76,7 +77,7 @@ class App.KnowledgeBase extends App.Model
|
||||||
, initial
|
, initial
|
||||||
|
|
||||||
visibleInternally: (kb_locale) ->
|
visibleInternally: (kb_locale) ->
|
||||||
@active
|
@active && @access() != 'none'
|
||||||
|
|
||||||
visiblePublicly: (kb_locale) ->
|
visiblePublicly: (kb_locale) ->
|
||||||
@active
|
@active
|
||||||
|
@ -88,6 +89,41 @@ class App.KnowledgeBase extends App.Model
|
||||||
|
|
||||||
attrs
|
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: [
|
@configure_attributes: [
|
||||||
{
|
{
|
||||||
name: 'translation::title'
|
name: 'translation::title'
|
||||||
|
|
|
@ -3,6 +3,7 @@ class App.KnowledgeBaseAnswer extends App.Model
|
||||||
@extend Spine.Model.Ajax
|
@extend Spine.Model.Ajax
|
||||||
@extend App.KnowledgeBaseActions
|
@extend App.KnowledgeBaseActions
|
||||||
@extend App.KnowledgeBaseCanBePublished
|
@extend App.KnowledgeBaseCanBePublished
|
||||||
|
@extend App.KnowledgeBaseAccess
|
||||||
|
|
||||||
@serverClassName: 'KnowledgeBase::Answer'
|
@serverClassName: 'KnowledgeBase::Answer'
|
||||||
|
|
||||||
|
@ -95,4 +96,4 @@ class App.KnowledgeBaseAnswer extends App.Model
|
||||||
'Answer'
|
'Answer'
|
||||||
|
|
||||||
visibleInternally: (kb_locale) =>
|
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'
|
@configure 'KnowledgeBaseCategory', 'category_icon', 'parent_id', 'child_ids', 'translation_ids'
|
||||||
@extend Spine.Model.Ajax
|
@extend Spine.Model.Ajax
|
||||||
@extend App.KnowledgeBaseActions
|
@extend App.KnowledgeBaseActions
|
||||||
|
@extend App.KnowledgeBaseAccess
|
||||||
|
|
||||||
url: ->
|
url: ->
|
||||||
@knowledge_base().generateURL('categories')
|
@knowledge_base().generateURL('categories')
|
||||||
|
@ -166,6 +167,8 @@ class App.KnowledgeBaseCategory extends App.Model
|
||||||
'draft'
|
'draft'
|
||||||
|
|
||||||
visibleInternally: (kb_locale) =>
|
visibleInternally: (kb_locale) =>
|
||||||
|
#return false if @access() == 'none'
|
||||||
|
|
||||||
@findDeepAnswer( (record) ->
|
@findDeepAnswer( (record) ->
|
||||||
record.is_internally_published(kb_locale)
|
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) ->
|
submit: (e) ->
|
||||||
@preventDefaultAndStopPropagation(e)
|
@preventDefaultAndStopPropagation(e)
|
||||||
|
|
||||||
#debuggerj
|
|
||||||
formController = @formControllers.filter((elem) -> (elem.form[0] is e.currentTarget) or (e.currentTarget.contains(elem.form[0])))[0]
|
formController = @formControllers.filter((elem) -> (elem.form[0] is e.currentTarget) or (e.currentTarget.contains(elem.form[0])))[0]
|
||||||
params = @formParam(formController.form)
|
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;
|
vertical-align: middle;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-list-radio-cell {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-boxes {
|
.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
|
private
|
||||||
|
|
||||||
def render_alternative
|
def render_alternative
|
||||||
@alternative = policy_scope(@knowledge_base.answers)
|
@alternative = find_answer @knowledge_base.answers.eager_load(translations: :kb_locale), params[:answer], locale: false
|
||||||
.eager_load(translations: :kb_locale)
|
|
||||||
.find_by(id: params[:answer])
|
|
||||||
|
|
||||||
raise ActiveRecord::RecordNotFound if !@alternative&.translations&.any?
|
raise ActiveRecord::RecordNotFound if !@alternative&.translations&.any?
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,8 @@
|
||||||
|
|
||||||
class KnowledgeBase::Public::BaseController < ApplicationController
|
class KnowledgeBase::Public::BaseController < ApplicationController
|
||||||
before_action :load_kb
|
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'
|
layout 'knowledge_base'
|
||||||
|
|
||||||
|
@ -31,12 +32,9 @@ class KnowledgeBase::Public::BaseController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
def fallback_locale
|
def fallback_locale
|
||||||
if all_locales.find { |locale| locale.id == system_locale_via_uri&.id }
|
return system_locale_via_uri 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
|
|
||||||
|
|
||||||
|
filter_primary_kb_locale || all_locales.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def filter_primary_kb_locale
|
def filter_primary_kb_locale
|
||||||
|
@ -62,16 +60,40 @@ class KnowledgeBase::Public::BaseController < ApplicationController
|
||||||
list
|
list
|
||||||
.localed(system_locale_via_uri)
|
.localed(system_locale_via_uri)
|
||||||
.sorted
|
.sorted
|
||||||
.select { |category| policy(category).show? }
|
.select { |category| policy(category).show_public? }
|
||||||
end
|
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?
|
return if scope.nil?
|
||||||
|
|
||||||
policy_scope(scope)
|
scope = scope.include_contents
|
||||||
.localed(system_locale_via_uri)
|
scope = scope.localed(locale) if locale
|
||||||
.include_contents
|
|
||||||
.find_by(id: id)
|
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
|
end
|
||||||
|
|
||||||
def not_found(e)
|
def not_found(e)
|
||||||
|
|
|
@ -15,14 +15,11 @@ class KnowledgeBase::Public::CategoriesController < KnowledgeBase::Public::BaseC
|
||||||
def show
|
def show
|
||||||
@object = find_category(params[:category])
|
@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)
|
@categories = categories_filter(@object.children)
|
||||||
@object_locales = find_locales(@object)
|
@object_locales = find_locales(@object)
|
||||||
|
@answers = answers_filter(@object.answers)
|
||||||
@answers = policy_scope(@object.answers)
|
|
||||||
.localed(system_locale_via_uri)
|
|
||||||
.sorted
|
|
||||||
|
|
||||||
render :index
|
render :index
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,11 +3,6 @@
|
||||||
class KnowledgeBase::Public::TagsController < KnowledgeBase::Public::BaseController
|
class KnowledgeBase::Public::TagsController < KnowledgeBase::Public::BaseController
|
||||||
def show
|
def show
|
||||||
@object = [:tag, params[:tag]]
|
@object = [:tag, params[:tag]]
|
||||||
|
@answers = answers_filter KnowledgeBase::Answer.tag_objects(params[:tag])
|
||||||
all_tagged = KnowledgeBase::Answer.tag_objects(params[:tag])
|
|
||||||
|
|
||||||
@answers = policy_scope(all_tagged)
|
|
||||||
.localed(system_locale_via_uri)
|
|
||||||
.sorted
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -5,15 +5,41 @@ class KnowledgeBasesController < KnowledgeBase::BaseController
|
||||||
render json: assets(params[:answer_translation_content_ids])
|
render json: assets(params[:answer_translation_content_ids])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def visible_ids
|
||||||
|
render json: KnowledgeBase::InternalAssets.new(current_user).visible_ids
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assets(answer_translation_content_ids = nil)
|
def assets(answer_translation_content_ids = nil)
|
||||||
return editor_assets(answer_translation_content_ids) if current_user&.permissions?('knowledge_base.editor')
|
if KnowledgeBase.granular_permissions?
|
||||||
return reader_assets(answer_translation_content_ids) if current_user&.permissions?('knowledge_base.reader')
|
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
|
public_assets
|
||||||
end
|
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)
|
def editor_assets(answer_translation_content_ids)
|
||||||
assets = [
|
assets = [
|
||||||
KnowledgeBase,
|
KnowledgeBase,
|
||||||
|
|
|
@ -36,7 +36,7 @@ module KnowledgeBaseTopBarHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def render_top_bar_if_needed(object, knowledge_base)
|
def render_top_bar_if_needed(object, knowledge_base)
|
||||||
return if !policy(:knowledge_base).edit?
|
return if !can_preview?
|
||||||
|
|
||||||
editable = object || knowledge_base
|
editable = object || knowledge_base
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module KnowledgeBaseVisibilityClassHelper
|
module KnowledgeBaseVisibilityClassHelper
|
||||||
def visibility_class_name(object)
|
def visibility_class_name(object)
|
||||||
return if !policy(:knowledge_base).edit?
|
return if !can_preview?
|
||||||
|
|
||||||
suffix = case object
|
suffix = case object
|
||||||
when CanBePublished
|
when CanBePublished
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module KnowledgeBaseVisibilityNoteHelper
|
module KnowledgeBaseVisibilityNoteHelper
|
||||||
def visibility_note(object)
|
def visibility_note(object)
|
||||||
return if !policy(:knowledge_base).edit?
|
return if !can_preview?
|
||||||
|
|
||||||
text = visibility_text(object)
|
text = visibility_text(object)
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,12 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
||||||
include HasActiveJobLock
|
include HasActiveJobLock
|
||||||
|
|
||||||
def lock_key
|
def lock_key
|
||||||
# "ChecksKbClientNotificationJob/KnowledgeBase::Answer/42/destroy"
|
# "ChecksKbClientNotificationJob/KnowledgeBase::Answer/42"
|
||||||
"#{self.class.name}/#{arguments[0]}/#{arguments[1]}/#{arguments[2]}"
|
"#{self.class.name}/#{arguments[0]}/#{arguments[1]}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def perform(klass_name, id, event)
|
def perform(klass_name, object_id)
|
||||||
object = klass_name.constantize.find_by(id: id)
|
object = klass_name.constantize.find_by(id: object_id)
|
||||||
return if object.blank?
|
return if object.blank?
|
||||||
|
|
||||||
level = needs_editor?(object) ? 'editor' : '*'
|
level = needs_editor?(object) ? 'editor' : '*'
|
||||||
|
@ -24,7 +24,7 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
||||||
|
|
||||||
def build_data(object, event)
|
def build_data(object, event)
|
||||||
timestamp = event == :destroy ? Time.zone.now : object.updated_at
|
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,
|
class: object.class.to_s,
|
||||||
|
@ -57,8 +57,4 @@ class ChecksKbClientNotificationJob < ApplicationJob
|
||||||
.filter_map { |user_id| User.find_by(id: user_id) }
|
.filter_map { |user_id| User.find_by(id: user_id) }
|
||||||
.select { |user| user.permissions? "knowledge_base.#{permission_suffix}" }
|
.select { |user| user.permissions? "knowledge_base.#{permission_suffix}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.notify_later(object, event)
|
|
||||||
perform_later(object.class.to_s, object.id, event.to_s)
|
|
||||||
end
|
|
||||||
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
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
included do
|
included do
|
||||||
after_create :notify_kb_clients_after_create
|
after_commit :notify_kb_clients_after
|
||||||
after_update :notify_kb_clients_after_update
|
|
||||||
after_touch :notify_kb_clients_after_touch
|
|
||||||
after_destroy :notify_kb_clients_after_destroy
|
|
||||||
|
|
||||||
class_attribute :notify_kb_clients_suspend, default: false
|
class_attribute :notify_kb_clients_suspend, default: false
|
||||||
end
|
end
|
||||||
|
@ -30,25 +27,9 @@ module ChecksKbClientNotification
|
||||||
|
|
||||||
# generic call
|
# generic call
|
||||||
|
|
||||||
def notify_kb_clients(event)
|
def notify_kb_clients_after
|
||||||
return if self.class.notify_kb_clients_suspend?
|
return if self.class.notify_kb_clients_suspend?
|
||||||
|
|
||||||
ChecksKbClientNotificationJob.notify_later(self, event)
|
ChecksKbClientNotificationJob.perform_later(self.class.name, id)
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
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 :answers, through: :categories
|
||||||
|
|
||||||
|
has_many :permissions, class_name: 'KnowledgeBase::Permission',
|
||||||
|
as: :permissionable,
|
||||||
|
autosave: true,
|
||||||
|
dependent: :destroy
|
||||||
|
|
||||||
validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
validates :category_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||||
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
validates :homepage_layout, inclusion: { in: KnowledgeBase::LAYOUTS }
|
||||||
|
|
||||||
|
@ -156,6 +161,24 @@ class KnowledgeBase < ApplicationModel
|
||||||
.any? { |e| e > 1 }
|
.any? { |e| e > 1 }
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def set_defaults
|
def set_defaults
|
||||||
|
|
|
@ -6,6 +6,7 @@ class KnowledgeBase::Answer < ApplicationModel
|
||||||
include HasTags
|
include HasTags
|
||||||
include CanBePublished
|
include CanBePublished
|
||||||
include ChecksKbClientNotification
|
include ChecksKbClientNotification
|
||||||
|
include ChecksKbClientVisibility
|
||||||
include CanCloneAttachments
|
include CanCloneAttachments
|
||||||
|
|
||||||
AGENT_ALLOWED_ATTRIBUTES = %i[category_id promoted internal_note].freeze
|
AGENT_ALLOWED_ATTRIBUTES = %i[category_id promoted internal_note].freeze
|
||||||
|
@ -49,7 +50,13 @@ class KnowledgeBase::Answer < ApplicationModel
|
||||||
siblings = category.answers
|
siblings = category.answers
|
||||||
|
|
||||||
if !User.lookup(id: UserInfo.current_user_id)&.permissions?('knowledge_base.editor')
|
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
|
end
|
||||||
|
|
||||||
data = ApplicationModel::CanAssets.reduce(siblings, data)
|
data = ApplicationModel::CanAssets.reduce(siblings, data)
|
||||||
|
|
|
@ -4,6 +4,7 @@ class KnowledgeBase::Category < ApplicationModel
|
||||||
include HasTranslations
|
include HasTranslations
|
||||||
include HasAgentAllowedParams
|
include HasAgentAllowedParams
|
||||||
include ChecksKbClientNotification
|
include ChecksKbClientNotification
|
||||||
|
include ChecksKbClientVisibility
|
||||||
|
|
||||||
AGENT_ALLOWED_ATTRIBUTES = %i[knowledge_base_id parent_id category_icon].freeze
|
AGENT_ALLOWED_ATTRIBUTES = %i[knowledge_base_id parent_id category_icon].freeze
|
||||||
AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
|
AGENT_ALLOWED_NESTED_RELATIONS = %i[translations].freeze
|
||||||
|
@ -24,6 +25,11 @@ class KnowledgeBase::Category < ApplicationModel
|
||||||
touch: true,
|
touch: true,
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
has_many :permissions, class_name: 'KnowledgeBase::Permission',
|
||||||
|
as: :permissionable,
|
||||||
|
autosave: true,
|
||||||
|
dependent: :destroy
|
||||||
|
|
||||||
validates :category_icon, presence: true
|
validates :category_icon, presence: true
|
||||||
|
|
||||||
scope :root, -> { where(parent: nil) }
|
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)
|
Rails.application.routes.url_helpers.knowledge_base_category_path(knowledge_base, self)
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def cannot_be_child_of_parent
|
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 :users, after_add: :cache_update, after_remove: :cache_update
|
||||||
has_and_belongs_to_many :permissions,
|
has_and_belongs_to_many :permissions,
|
||||||
before_add: %i[validate_agent_limit_by_permission validate_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,
|
before_remove: :last_admin_check_by_permission,
|
||||||
after_remove: :cache_update
|
after_remove: %i[cache_update cache_remove_kb_permission]
|
||||||
validates :name, presence: true
|
validates :name, presence: true
|
||||||
store :preferences
|
store :preferences
|
||||||
|
has_many :knowledge_base_permissions, class_name: 'KnowledgeBase::Permission', dependent: :destroy
|
||||||
|
|
||||||
before_create :check_default_at_signup_permissions
|
before_create :check_default_at_signup_permissions
|
||||||
before_update :last_admin_check_by_attribute, :validate_agent_limit_by_attributes, :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."
|
raise Exceptions::UnprocessableEntity, "Cannot set default at signup when role has #{forbidden_permissions.join(', ')} permissions."
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -2,9 +2,34 @@
|
||||||
|
|
||||||
class Controllers::KnowledgeBase::AnswersControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
class Controllers::KnowledgeBase::AnswersControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
||||||
def show?
|
def show?
|
||||||
return true if user.permissions?('knowledge_base.editor')
|
access(__method__)
|
||||||
|
end
|
||||||
|
|
||||||
object = record.klass.find(record.params[:id])
|
def create?
|
||||||
object.can_be_published_aasm.internal? || object.can_be_published_aasm.published?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,38 @@
|
||||||
|
|
||||||
class Controllers::KnowledgeBase::CategoriesControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
class Controllers::KnowledgeBase::CategoriesControllerPolicy < Controllers::KnowledgeBase::BaseControllerPolicy
|
||||||
def show?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -12,4 +12,18 @@ class Controllers::KnowledgeBasesControllerPolicy < Controllers::KnowledgeBase::
|
||||||
def destroy?
|
def destroy?
|
||||||
false
|
false
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -2,13 +2,39 @@
|
||||||
|
|
||||||
class KnowledgeBase::AnswerPolicy < ApplicationPolicy
|
class KnowledgeBase::AnswerPolicy < ApplicationPolicy
|
||||||
def show?
|
def show?
|
||||||
return true if user&.permissions?(%w[knowledge_base.editor])
|
return true if access_editor?
|
||||||
|
|
||||||
record.visible? ||
|
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
|
end
|
||||||
|
|
||||||
def destroy?
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,13 +2,51 @@
|
||||||
|
|
||||||
class KnowledgeBase::CategoryPolicy < ApplicationPolicy
|
class KnowledgeBase::CategoryPolicy < ApplicationPolicy
|
||||||
def show?
|
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
|
end
|
||||||
|
|
||||||
private
|
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?
|
def user_required?
|
||||||
false
|
false
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,7 +1,25 @@
|
||||||
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
# Copyright (C) 2012-2022 Zammad Foundation, https://zammad-foundation.org/
|
||||||
|
|
||||||
class KnowledgeBasePolicy < ApplicationPolicy
|
class KnowledgeBasePolicy < ApplicationPolicy
|
||||||
def edit?
|
def show?
|
||||||
user&.permissions?('knowledge_base.editor')
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -20,6 +20,7 @@ Zammad::Application.routes.draw do
|
||||||
resources :knowledge_bases, only: %i[show update] do
|
resources :knowledge_bases, only: %i[show update] do
|
||||||
collection do
|
collection do
|
||||||
post :init
|
post :init
|
||||||
|
get :visible_ids
|
||||||
post :search, controller: 'knowledge_base/search'
|
post :search, controller: 'knowledge_base/search'
|
||||||
get :recent_answers, controller: 'knowledge_base/answers'
|
get :recent_answers, controller: 'knowledge_base/answers'
|
||||||
|
|
||||||
|
@ -35,11 +36,17 @@ Zammad::Application.routes.draw do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
member do
|
||||||
|
resource :permissions, controller: 'knowledge_base/permissions', only: %i[update show]
|
||||||
|
end
|
||||||
|
|
||||||
resources :categories, controller: 'knowledge_base/categories',
|
resources :categories, controller: 'knowledge_base/categories',
|
||||||
except: %i[new edit] do
|
except: %i[new edit] do
|
||||||
|
|
||||||
member do
|
member do
|
||||||
patch :reorder_categories, :reorder_answers
|
patch :reorder_categories, :reorder_answers
|
||||||
|
|
||||||
|
resource :permissions, controller: 'knowledge_base/permissions', only: %i[update show]
|
||||||
end
|
end
|
||||||
|
|
||||||
collection do
|
collection do
|
||||||
|
|
|
@ -108,6 +108,18 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
||||||
t.timestamps # rubocop:disable Zammad/ExistsDateTimePrecision
|
t.timestamps # rubocop:disable Zammad/ExistsDateTimePrecision
|
||||||
end
|
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(
|
Setting.create_if_not_exists(
|
||||||
title: 'Kb multi-lingual support',
|
title: 'Kb multi-lingual support',
|
||||||
name: 'kb_multi_lingual_support',
|
name: 'kb_multi_lingual_support',
|
||||||
|
@ -173,7 +185,8 @@ class InitializeKnowledgeBase < ActiveRecord::Migration[5.0]
|
||||||
note: 'Access %s',
|
note: 'Access %s',
|
||||||
preferences: {
|
preferences: {
|
||||||
translations: ['Knowledge Base']
|
translations: ['Knowledge Base']
|
||||||
}
|
},
|
||||||
|
allow_signup: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
Permission.create_if_not_exists(
|
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'),
|
note: __('Manage %s'),
|
||||||
preferences: {
|
preferences: {
|
||||||
translations: [__('Knowledge Base Reader')]
|
translations: [__('Knowledge Base Reader')]
|
||||||
}
|
},
|
||||||
|
allow_signup: true,
|
||||||
)
|
)
|
||||||
|
|
||||||
admin = Role.find_by(name: 'Admin')
|
admin = Role.find_by(name: 'Admin')
|
||||||
|
|
|
@ -5163,6 +5163,10 @@ msgstr ""
|
||||||
msgid "Invalid payload, need data:image in logo param"
|
msgid "Invalid payload, need data:image in logo param"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: lib/knowledge_base/permissions_update.rb
|
||||||
|
msgid "Invalid permissions, do not lock out yourself"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: app/models/concerns/checks_condition_validation.rb
|
#: app/models/concerns/checks_condition_validation.rb
|
||||||
msgid "Invalid ticket selector conditions"
|
msgid "Invalid ticket selector conditions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -7613,6 +7617,7 @@ msgstr ""
|
||||||
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
#: app/assets/javascripts/app/controllers/_ui_element/core_workflow_condition.coffee
|
||||||
#: app/assets/javascripts/app/controllers/role.coffee
|
#: app/assets/javascripts/app/controllers/role.coffee
|
||||||
#: app/assets/javascripts/app/views/integration/ldap.jst.eco
|
#: app/assets/javascripts/app/views/integration/ldap.jst.eco
|
||||||
|
#: app/assets/javascripts/app/views/knowledge_base/permissions_dialog.jst.eco
|
||||||
msgid "Role"
|
msgid "Role"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -9570,6 +9575,14 @@ msgstr ""
|
||||||
msgid "URL (AJAX endpoint)"
|
msgid "URL (AJAX endpoint)"
|
||||||
msgstr ""
|
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
|
#: app/controllers/first_steps_controller.rb
|
||||||
#: db/seeds/overviews.rb
|
#: db/seeds/overviews.rb
|
||||||
msgid "Unassigned & Open Tickets"
|
msgid "Unassigned & Open Tickets"
|
||||||
|
@ -10024,11 +10037,11 @@ msgid "Visibility"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
||||||
msgid "Visible to agents & editors"
|
msgid "Visible to everyone"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
#: app/assets/javascripts/app/controllers/knowledge_base/content_can_be_published_form.coffee
|
||||||
msgid "Visible to everyone"
|
msgid "Visible to readers & editors"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: app/assets/javascripts/app/controllers/_integration/placetel.coffee
|
#: app/assets/javascripts/app/controllers/_integration/placetel.coffee
|
||||||
|
|
|
@ -37,3 +37,24 @@ module ActiveRecord
|
||||||
end
|
end
|
||||||
end
|
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 }
|
created_by_id { 1 }
|
||||||
updated_by_id { 1 }
|
updated_by_id { 1 }
|
||||||
|
|
||||||
|
transient do
|
||||||
|
permission_names { nil }
|
||||||
|
end
|
||||||
|
|
||||||
|
permissions { Permission.where(name: permission_names) }
|
||||||
|
|
||||||
factory :agent_role do
|
factory :agent_role do
|
||||||
permissions { Permission.where(name: 'ticket.agent') }
|
permissions { Permission.where(name: 'ticket.agent') }
|
||||||
end
|
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(:answers) }
|
||||||
it { is_expected.to have_many(:children) }
|
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(:parent).optional }
|
||||||
it { is_expected.to belong_to(:knowledge_base) }
|
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]
|
# @return [Hash, nil]
|
||||||
def find_assets_of(object, actual)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -96,18 +96,6 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
||||||
let(:new_tag_name) { 'capybara_kb_tag' }
|
let(:new_tag_name) { 'capybara_kb_tag' }
|
||||||
|
|
||||||
it 'adds a new tag' do
|
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
|
within :active_content do
|
||||||
click '.js-newTagLabel'
|
click '.js-newTagLabel'
|
||||||
|
|
||||||
|
@ -116,6 +104,7 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
||||||
elem.send_keys :return
|
elem.send_keys :return
|
||||||
|
|
||||||
wait.until_exists { published_answer_with_tag.reload.tag_list.include? new_tag_name }
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -127,22 +116,10 @@ RSpec.describe 'Knowledge Base Locale Answer Edit', type: :system do
|
||||||
|
|
||||||
it 'deletes a tag' do
|
it 'deletes a tag' do
|
||||||
within :active_content do
|
within :active_content do
|
||||||
click '.js-newTagLabel'
|
|
||||||
|
|
||||||
find('.list-item', text: published_answer_tag_name)
|
find('.list-item', text: published_answer_tag_name)
|
||||||
.find('.js-delete').click
|
.find('.js-delete').click
|
||||||
|
|
||||||
expect(page).to have_no_css('a.js-tag', text: published_answer_tag_name)
|
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 }
|
wait.until_exists { published_answer_with_tag.reload.tag_list.exclude? published_answer_tag_name }
|
||||||
end
|
end
|
||||||
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
|
def forward
|
||||||
within :active_content do
|
within :active_content do
|
||||||
# binding.pry
|
|
||||||
wait.until_exists { find('.textBubble-content .richtext-content') }
|
wait.until_exists { find('.textBubble-content .richtext-content') }
|
||||||
click '.js-ArticleAction[data-type=emailForward]'
|
click '.js-ArticleAction[data-type=emailForward]'
|
||||||
fill_in 'To', with: 'customer@example.com'
|
fill_in 'To', with: 'customer@example.com'
|
||||||
|
|
Loading…
Reference in a new issue