Fixes #967 - Access to my own Tickets (where I'm customer of) in a Group im not Agent.

This commit is contained in:
Rolf Schmidt 2020-08-20 09:10:08 +02:00 committed by Thorsten Eckel
parent 59c787bacb
commit 4a07c783a3
56 changed files with 1063 additions and 378 deletions

View file

@ -212,4 +212,15 @@ class Index extends App.ControllerContent
@formEnable(@$('.js-submit'), 'button') @formEnable(@$('.js-submit'), 'button')
App.Config.set('customer_ticket_new', Index, 'Routes') App.Config.set('customer_ticket_new', Index, 'Routes')
App.Config.set('CustomerTicketNew', { prio: 8003, parent: '#new', name: 'New Ticket', translate: true, target: '#customer_ticket_new', permission: ['ticket.customer'], setting: ['customer_ticket_create'], divider: true }, 'NavBarRight') App.Config.set('CustomerTicketNew', {
prio: 8003,
parent: '#new',
name: 'New Ticket',
translate: true,
target: '#customer_ticket_new',
permission: (navigation) ->
return false if navigation.permissionCheck('ticket.agent')
return navigation.permissionCheck('ticket.customer')
setting: ['customer_ticket_create'],
divider: true
}, 'NavBarRight')

View file

@ -361,6 +361,7 @@ class App.Navigation extends App.ControllerWidgetPermanent
filterNavbarPermissionOk: (item) -> filterNavbarPermissionOk: (item) ->
return true unless item.permission return true unless item.permission
return item.permission(@) if typeof item.permission is 'function'
return _.any item.permission, (permissionName) => return _.any item.permission, (permissionName) =>
return @permissionCheck(permissionName) return @permissionCheck(permissionName)

View file

@ -1049,7 +1049,7 @@ class Table extends App.Controller
ticketListShow.push App.Ticket.find(ticket.id) ticketListShow.push App.Ticket.find(ticket.id)
# if customer and no ticket exists, show the following message only # if customer and no ticket exists, show the following message only
if !ticketListShow[0] && @permissionCheck('ticket.customer') if !ticketListShow[0] && !@permissionCheck('ticket.agent')
@html App.view('customer_not_ticket_exists')() @html App.view('customer_not_ticket_exists')()
return return
@ -1057,13 +1057,14 @@ class Table extends App.Controller
@overview = App.Overview.find(overview.id) @overview = App.Overview.find(overview.id)
# render init page # render init page
checkbox = true checkbox = false
edit = false edit = false
if @permissionCheck('admin.overview') if @permissionCheck('admin.overview')
edit = true edit = true
if @permissionCheck('ticket.customer') if @permissionCheck('ticket.agent')
checkbox = false checkbox = true
edit = false view_modes = []
if @permissionCheck('ticket.agent')
view_modes = [ view_modes = [
{ {
name: 'S' name: 'S'
@ -1076,8 +1077,6 @@ class Table extends App.Controller
class: 'active' if @view_mode is 'm' class: 'active' if @view_mode is 'm'
} }
] ]
if @permissionCheck('ticket.customer')
view_modes = []
html = App.view('agent_ticket_view/content')( html = App.view('agent_ticket_view/content')(
overview: @overview overview: @overview
view_modes: view_modes view_modes: view_modes

View file

@ -137,23 +137,20 @@ class App.TicketZoom extends App.Controller
) )
load: (data, ignoreSame = false, local = false) => load: (data, ignoreSame = false, local = false) =>
# check if ticket has changed
newTicketRaw = data.assets.Ticket[@ticket_id] newTicketRaw = data.assets.Ticket[@ticket_id]
#console.log(newTicketRaw.updated_at)
#console.log(@ticketUpdatedAtLastCall)
loadAssets = true
if @ticketUpdatedAtLastCall if @ticketUpdatedAtLastCall
# ignore if record is already shown # ignore if record is already shown
if ignoreSame && new Date(newTicketRaw.updated_at).getTime() is new Date(@ticketUpdatedAtLastCall).getTime() if ignoreSame && new Date(newTicketRaw.updated_at).getTime() is new Date(@ticketUpdatedAtLastCall).getTime()
#console.log('debug no fetched, current ticket already there or requested') #console.log('debug no fetched, current ticket already there or requested')
return loadAssets = false
# do not render if newer ticket is already requested # do not render if newer ticket is already requested
if new Date(newTicketRaw.updated_at).getTime() < new Date(@ticketUpdatedAtLastCall).getTime() if new Date(newTicketRaw.updated_at).getTime() < new Date(@ticketUpdatedAtLastCall).getTime()
#console.log('fetched no fetch, current ticket already newer') #console.log('fetched no fetch, current ticket already newer')
return loadAssets = false
# remember current record if newer as requested record # remember current record if newer as requested record
if new Date(newTicketRaw.updated_at).getTime() > new Date(@ticketUpdatedAtLastCall).getTime() if new Date(newTicketRaw.updated_at).getTime() > new Date(@ticketUpdatedAtLastCall).getTime()
@ -161,6 +158,9 @@ class App.TicketZoom extends App.Controller
else else
@ticketUpdatedAtLastCall = newTicketRaw.updated_at @ticketUpdatedAtLastCall = newTicketRaw.updated_at
# load assets
if loadAssets
# notify if ticket changed not by my self # notify if ticket changed not by my self
if @initFetched if @initFetched
if newTicketRaw.updated_by_id isnt @Session.get('id') if newTicketRaw.updated_by_id isnt @Session.get('id')
@ -180,16 +180,31 @@ class App.TicketZoom extends App.Controller
# remember tags # remember tags
@tags = data.tags @tags = data.tags
# get edit form attributes
@formMeta = data.form_meta
# load assets
App.Collection.loadAssets(data.assets, targetModel: 'Ticket') App.Collection.loadAssets(data.assets, targetModel: 'Ticket')
# get data # get ticket
@ticket = App.Ticket.fullLocal(@ticket_id) @ticket = App.Ticket.fullLocal(@ticket_id)
@ticket.article = undefined @ticket.article = undefined
view = @ticket.currentView()
readable = @ticket.userGroupAccess('read')
changeable = @ticket.userGroupAccess('change')
fullable = @ticket.userGroupAccess('full')
formMeta = data.form_meta
# on the following states we want to rerender the ticket:
# - if the object attribute configuration has changed (attribute values, restrictions, filters)
# - if the user view has changed (agent/customer)
# - if the ticket permission has changed (read/write/full)
if @view && ( !_.isEqual(@formMeta, formMeta) || @view isnt view || @readable isnt readable || @changeable isnt changeable || @fullable isnt fullable )
@renderDone = false
@view = view
@readable = readable
@changeable = changeable
@fullable = fullable
@formMeta = formMeta
# render page # render page
@render(local) @render(local)
@ -410,7 +425,6 @@ class App.TicketZoom extends App.Controller
elLocal = $(App.view('ticket_zoom') elLocal = $(App.view('ticket_zoom')
ticket: @ticket ticket: @ticket
nav: @nav nav: @nav
isCustomer: @permissionCheck('ticket.customer')
scrollbarWidth: App.Utils.getScrollBarWidth() scrollbarWidth: App.Utils.getScrollBarWidth()
dir: App.i18n.dir() dir: App.i18n.dir()
) )
@ -460,6 +474,7 @@ class App.TicketZoom extends App.Controller
@highligher = new App.TicketZoomHighlighter( @highligher = new App.TicketZoomHighlighter(
el: elLocal.find('.js-highlighterContainer') el: elLocal.find('.js-highlighterContainer')
ticket: @ticket
ticket_id: @ticket_id ticket_id: @ticket_id
) )
@ -611,12 +626,12 @@ class App.TicketZoom extends App.Controller
subject: '' subject: ''
type: 'note' type: 'note'
body: '' body: ''
internal: internal internal: ''
in_reply_to: '' in_reply_to: ''
subtype: '' subtype: ''
if @permissionCheck('ticket.customer') if @ticket.currentView() is 'agent'
currentStore.article.internal = '' currentStore.article.internal = internal
currentStore currentStore
@ -637,7 +652,7 @@ class App.TicketZoom extends App.Controller
return if modelDiff.ticket.state_id return if modelDiff.ticket.state_id
# and we are in the customer interface # and we are in the customer interface
return if !@permissionCheck('ticket.customer') return if @ticket.currentView() isnt 'customer'
# and the default is was not set before # and the default is was not set before
return if @isDefaultFollowUpStateSet return if @isDefaultFollowUpStateSet
@ -676,7 +691,7 @@ class App.TicketZoom extends App.Controller
delete currentParams.article.form_id delete currentParams.article.form_id
if @permissionCheck('ticket.customer') if @ticket.currentView() is 'customer'
currentParams.article.internal = '' currentParams.article.internal = ''
currentParams currentParams
@ -802,7 +817,7 @@ class App.TicketZoom extends App.Controller
) )
# set defaults # set defaults
if !@permissionCheck('ticket.customer') if ticket.currentView() is 'agent'
if !ticket['owner_id'] if !ticket['owner_id']
ticket['owner_id'] = 1 ticket['owner_id'] = 1
@ -875,7 +890,7 @@ class App.TicketZoom extends App.Controller
return return
# time tracking # time tracking
if @permissionCheck('ticket.customer') if ticket.currentView() is 'customer'
@submitPost(e, ticket, macro) @submitPost(e, ticket, macro)
return return

View file

@ -41,7 +41,7 @@ class Delete
timeframe_miliseconds - (now - created_at) timeframe_miliseconds - (now - created_at)
@deletableForAgent: (actions, ticket, article, ui) -> @deletableForAgent: (actions, ticket, article, ui) ->
return false if !ui.permissionCheck('ticket.agent') return false if ticket.currentView() is 'customer'
return false if article.created_by_id != App.User.current()?.id return false if article.created_by_id != App.User.current()?.id
return false if article.type.communication return false if article.type.communication

View file

@ -1,6 +1,6 @@
class EmailReply extends App.Controller class EmailReply extends App.Controller
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if !ui.permissionCheck('ticket.agent') return actions if ticket.currentView() is 'customer'
group = ticket.group group = ticket.group
return actions if !group.email_address_id return actions if !group.email_address_id
@ -241,7 +241,7 @@ class EmailReply extends App.Controller
true true
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
group = ticket.group group = ticket.group
return articleTypes if !group.email_address_id return articleTypes if !group.email_address_id

View file

@ -1,6 +1,6 @@
class FacebookReply class FacebookReply
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment' if article.type.name is 'facebook feed post' || article.type.name is 'facebook feed comment'
actions.push { actions.push {
@ -35,7 +35,7 @@ class FacebookReply
true true
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
return articleTypes if !ticket || !ticket.create_article_type_id return articleTypes if !ticket || !ticket.create_article_type_id

View file

@ -1,6 +1,6 @@
class Internal class Internal
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
if article.internal is true if article.internal is true
actions.push { actions.push {

View file

@ -7,7 +7,7 @@ class Note
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
internal = false internal = false
if ui.permissionCheck('ticket.agent') if ticket.currentView() is 'agent'
internal = ui.Config.get('ui_ticket_zoom_article_note_new_internal') internal = ui.Config.get('ui_ticket_zoom_article_note_new_internal')
articleTypes.push { articleTypes.push {

View file

@ -6,7 +6,7 @@ class PhoneReply
true true
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
articleTypes.push { articleTypes.push {
name: 'phone' name: 'phone'
icon: 'phone' icon: 'phone'

View file

@ -1,6 +1,6 @@
class SmsReply class SmsReply
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
if article.sender.name is 'Customer' && article.type.name is 'sms' if article.sender.name is 'Customer' && article.type.name is 'sms'
actions.push { actions.push {
@ -43,7 +43,7 @@ class SmsReply
true true
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
return articleTypes if !ticket || !ticket.create_article_type_id return articleTypes if !ticket || !ticket.create_article_type_id

View file

@ -1,6 +1,6 @@
class Split class Split
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
actions.push { actions.push {
name: 'split' name: 'split'

View file

@ -1,6 +1,6 @@
class TelegramReply class TelegramReply
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message' if article.sender.name is 'Customer' && article.type.name is 'telegram personal-message'
actions.push { actions.push {
@ -43,7 +43,7 @@ class TelegramReply
true true
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
return articleTypes if !ticket || !ticket.create_article_type_id return articleTypes if !ticket || !ticket.create_article_type_id

View file

@ -1,6 +1,6 @@
class TwitterReply class TwitterReply
@action: (actions, ticket, article, ui) -> @action: (actions, ticket, article, ui) ->
return actions if ui.permissionCheck('ticket.customer') return actions if ticket.currentView() is 'customer'
if article.type.name is 'twitter status' if article.type.name is 'twitter status'
actions.push { actions.push {
@ -126,7 +126,7 @@ class TwitterReply
}) })
@articleTypes: (articleTypes, ticket, ui) -> @articleTypes: (articleTypes, ticket, ui) ->
return articleTypes if !ui.permissionCheck('ticket.agent') return articleTypes if ticket.currentView() is 'customer'
return articleTypes if !ticket || !ticket.create_article_type_id return articleTypes if !ticket || !ticket.create_article_type_id

View file

@ -31,12 +31,12 @@ class App.TicketZoomArticleNew extends App.Controller
constructor: -> constructor: ->
super super
@internalSelector = true @internalSelector = false
@type = @defaults['type'] || 'note' @type = @defaults['type'] || 'note'
@setPossibleArticleTypes() @setPossibleArticleTypes()
if @permissionCheck('ticket.customer') if @ticket.currentView() is 'agent'
@internalSelector = false @internalSelector = true
@textareaHeight = @textareaHeight =
open: 148 open: 148
@ -165,7 +165,7 @@ class App.TicketZoomArticleNew extends App.Controller
articleTypes: @articleTypes articleTypes: @articleTypes
article: @defaults article: @defaults
form_id: @form_id form_id: @form_id
isCustomer: @permissionCheck('ticket.customer') isCustomer: ticket.currentView() is 'customer'
internalSelector: @internalSelector internalSelector: @internalSelector
) )
@setArticleTypePre(@type) @setArticleTypePre(@type)
@ -246,7 +246,7 @@ class App.TicketZoomArticleNew extends App.Controller
@bindAttachmentDelete() @bindAttachmentDelete()
# show text module UI # show text module UI
if !@permissionCheck('ticket.customer') if ticket.currentView() is 'agent'
textModule = new App.WidgetTextModule( textModule = new App.WidgetTextModule(
el: @$('.js-textarea').parent() el: @$('.js-textarea').parent()
data: data:
@ -272,16 +272,18 @@ class App.TicketZoomArticleNew extends App.Controller
params.form_id = @form_id params.form_id = @form_id
params.content_type = 'text/html' params.content_type = 'text/html'
if @permissionCheck('ticket.customer') ticket = App.Ticket.find(@ticket_id)
sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web') if ticket.currentView() is 'agent'
params.type_id = type.id
params.sender_id = sender.id
else
sender = App.TicketArticleSender.findByAttribute('name', 'Agent') sender = App.TicketArticleSender.findByAttribute('name', 'Agent')
type = App.TicketArticleType.findByAttribute('name', params['type']) type = App.TicketArticleType.findByAttribute('name', params['type'])
params.sender_id = sender.id params.sender_id = sender.id
params.type_id = type.id params.type_id = type.id
else
sender = App.TicketArticleSender.findByAttribute('name', 'Customer')
type = App.TicketArticleType.findByAttribute('name', 'web')
params.type_id = type.id
params.sender_id = sender.id
if params.internal if params.internal
params.internal = true params.internal = true

View file

@ -46,7 +46,7 @@ class App.TicketZoomAttributeBar extends App.Controller
@macroLastUpdated = App.Macro.lastUpdatedAt() @macroLastUpdated = App.Macro.lastUpdatedAt()
@possibleMacros = [] @possibleMacros = []
if _.isEmpty(macros) || !@permissionCheck('ticket.agent') if _.isEmpty(macros) || @ticket.currentView() is 'customer'
macroDisabled = true macroDisabled = true
else else
for macro in macros for macro in macros
@ -63,7 +63,7 @@ class App.TicketZoomAttributeBar extends App.Controller
)) ))
@setSecondaryAction(@secondaryAction, localeEl) @setSecondaryAction(@secondaryAction, localeEl)
if @permissionCheck('ticket.agent') if @ticket.currentView() is 'agent'
@taskbarWatcher = new App.TaskbarWatcher( @taskbarWatcher = new App.TaskbarWatcher(
taskKey: @taskKey taskKey: @taskKey
el: localeEl.filter('.js-avatars') el: localeEl.filter('.js-avatars')

View file

@ -36,7 +36,7 @@ class App.TicketZoomHighlighter extends App.Controller
constructor: -> constructor: ->
super super
return if !@permissionCheck('ticket.agent') return if @ticket.currentView() isnt 'agent'
@currentHighlights = {} @currentHighlights = {}
@ -93,7 +93,7 @@ class App.TicketZoomHighlighter extends App.Controller
# for testing purposes the highlights get stored in article preferences # for testing purposes the highlights get stored in article preferences
loadHighlights: (ticket_article_id) -> loadHighlights: (ticket_article_id) ->
return if !@permissionCheck('ticket.agent') return if @ticket.currentView() isnt 'agent'
article = App.TicketArticle.find(ticket_article_id) article = App.TicketArticle.find(ticket_article_id)
return if !article.preferences return if !article.preferences
return if !article.preferences.highlight return if !article.preferences.highlight

View file

@ -8,5 +8,5 @@ class App.TicketZoomMeta extends App.ObserverController
render: (ticket) => render: (ticket) =>
@html App.view('ticket_zoom/meta')( @html App.view('ticket_zoom/meta')(
ticket: ticket ticket: ticket
isCustomer: @permissionCheck('ticket.customer') isCustomer: ticket.currentView() is 'customer'
) )

View file

@ -1,6 +1,6 @@
class SidebarCustomer extends App.Controller class SidebarCustomer extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent') return if @ticket.currentView() isnt 'agent'
@item = { @item = {
name: 'customer' name: 'customer'
badgeCallback: @badgeRender badgeCallback: @badgeRender

View file

@ -1,6 +1,6 @@
class SidebarOrganization extends App.Controller class SidebarOrganization extends App.Controller
sidebarItem: => sidebarItem: =>
return if !@permissionCheck('ticket.agent') return if @ticket.currentView() isnt 'agent'
return if !@ticket.organization_id return if !@ticket.organization_id
@item = { @item = {
name: 'organization' name: 'organization'

View file

@ -19,10 +19,10 @@ class Edit extends App.ObserverController
if followUpPossible == 'new_ticket' && ticketState != 'closed' || if followUpPossible == 'new_ticket' && ticketState != 'closed' ||
followUpPossible != 'new_ticket' || followUpPossible != 'new_ticket' ||
@permissionCheck('admin') || @permissionCheck('ticket.agent') @permissionCheck('admin') || ticket.currentView() is 'agent'
new App.ControllerForm( new App.ControllerForm(
elReplace: @el elReplace: @el
model: App.Ticket model: { configure_attributes: @formMeta.configure_attributes }
screen: 'edit' screen: 'edit'
handlersConfig: handlers handlersConfig: handlers
filter: @formMeta.filter filter: @formMeta.filter
@ -35,7 +35,7 @@ class Edit extends App.ObserverController
else else
new App.ControllerForm( new App.ControllerForm(
elReplace: @el elReplace: @el
model: App.Ticket model: { configure_attributes: @formMeta.configure_attributes }
screen: 'edit' screen: 'edit'
handlersConfig: handlers handlersConfig: handlers
filter: @formMeta.filter filter: @formMeta.filter
@ -76,7 +76,7 @@ class SidebarTicket extends App.Controller
sidebarHead: 'Ticket' sidebarHead: 'Ticket'
sidebarCallback: @editTicket sidebarCallback: @editTicket
} }
if @permissionCheck('ticket.agent') if @ticket.currentView() is 'agent'
@item.sidebarActions = [ @item.sidebarActions = [
{ {
title: 'History' title: 'History'
@ -127,7 +127,7 @@ class SidebarTicket extends App.Controller
taskKey: @taskKey taskKey: @taskKey
) )
if @permissionCheck('ticket.agent') if @ticket.currentView() is 'agent'
@tagWidget = new App.WidgetTag( @tagWidget = new App.WidgetTag(
el: localEl.filter('.tags') el: localEl.filter('.tags')
object_type: 'Ticket' object_type: 'Ticket'

View file

@ -4,7 +4,7 @@ class App.TicketZoomTimeUnit extends App.ObserverController
time_unit: true time_unit: true
render: (ticket) => render: (ticket) =>
return if !@permissionCheck('ticket.agent') return if ticket.currentView() isnt 'agent'
return if !ticket.time_unit return if !ticket.time_unit
@html App.view('ticket_zoom/time_unit')( @html App.view('ticket_zoom/time_unit')(
ticket: ticket ticket: ticket

View file

@ -305,12 +305,32 @@ class App.Ticket extends App.Model
editable: (permission = 'change') -> editable: (permission = 'change') ->
user = App.User.current() user = App.User.current()
return false if !user? return false if !user?
return true if user.id is @customer_id return true if @currentView() is 'customer' && @userIsCustomer()
return true if user.organization_id && @organization_id && user.organization_id is @organization_id return true if @currentView() is 'customer' && user.organization_id && @organization_id && user.organization_id is @organization_id
return false if !@group_id return @userGroupAccess(permission)
userGroupAccess: (permission) ->
user = App.User.current()
group_ids = user.allGroupIds(permission) group_ids = user.allGroupIds(permission)
return false if !@group_id
for local_group_id in group_ids for local_group_id in group_ids
if local_group_id.toString() is @group_id.toString() if local_group_id.toString() is @group_id.toString()
return true return true
return false
userIsCustomer: ->
user = App.User.current()
return true if user.id is @customer_id
false false
userIsOwner: ->
user = App.User.current()
return true if user.id is @owner_id
false
currentView: ->
return 'agent' if App.User.current()?.permission('ticket.agent') && @userGroupAccess('read')
return 'customer' if App.User.current()?.permission('ticket.customer')
return

View file

@ -39,6 +39,18 @@ module Channel::Filter::SenderIsSystemAddress
mail['x-zammad-ticket-create-article-sender'.to_sym] = 'Agent' mail['x-zammad-ticket-create-article-sender'.to_sym] = 'Agent'
mail['x-zammad-article-sender'.to_sym] = 'Agent' mail['x-zammad-article-sender'.to_sym] = 'Agent'
# if the agent is also customer of the ticket then
# we need to set the sender as customer.
ticket_id = mail['x-zammad-ticket-id'.to_sym]
if ticket_id.present?
ticket = Ticket.lookup(id: ticket_id)
if ticket.present? && ticket.customer_id == user.id
mail['x-zammad-ticket-create-article-sender'.to_sym] = 'Customer'
mail['x-zammad-article-sender'.to_sym] = 'Customer'
end
end
return true return true
rescue => e rescue => e
Rails.logger.error 'SenderIsSystemAddress: ' + e.inspect Rails.logger.error 'SenderIsSystemAddress: ' + e.inspect

View file

@ -455,116 +455,6 @@ get the attribute model based on object and name
=begin =begin
get user based list of used object attributes
attribute_list = ObjectManager::Attribute.by_object('Ticket', user)
returns:
[
{ name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
{ name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
{ name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
]
=end
def self.by_object(object, user)
# lookups
if object
object_lookup_id = ObjectLookup.by_name(object)
end
# get attributes in right order
result = ObjectManager::Attribute.where(
object_lookup_id: object_lookup_id,
active: true,
to_create: false,
to_delete: false,
).order('position ASC, name ASC')
attributes = []
result.each do |item|
data = {
name: item.name,
display: item.display,
tag: item.data_type,
#:null => item.null,
}
if item.data_option[:permission]&.any?
next if !user
hint = false
item.data_option[:permission].each do |permission|
next if !user.permissions?(permission)
hint = true
break
end
next if !hint
end
if item.screens
data[:screen] = {}
item.screens.each do |screen, permission_options|
data[:screen][screen] = {}
if permission_options['-all-']
data[:screen][screen] = permission_options['-all-']
next
end
permission_options.each do |permission, options|
next if !user&.permissions?(permission)
options.each do |key, value|
if [true, false].include?(data[:screen][screen][key])
data[:screen][screen][key] = data[:screen][screen][key].nil? ? false : data[:screen][screen][key]
if options[key]
data[:screen][screen][key] = true
end
else
data[:screen][screen][key] = value
end
end
end
end
end
if item.data_option
data = data.merge(item.data_option.symbolize_keys)
end
attributes.push data
end
attributes
end
=begin
get user based list of object attributes as hash
attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user)
returns:
{
'api_key' => { name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
'api_ip_regexp' => { name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
'api_ip_max' => { name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
}
=end
def self.by_object_as_hash(object, user)
list = by_object(object, user)
hash = {}
list.each do |item|
hash[ item[:name] ] = item
end
hash
end
=begin
discard migration changes discard migration changes
ObjectManager::Attribute.discard_changes ObjectManager::Attribute.discard_changes

View file

@ -0,0 +1,7 @@
class ObjectManager::Element
include ::Mixin::HasBackends
def self.for_object(object)
"#{name}::#{object}".safe_constantize || ObjectManager::Element::Backend
end
end

View file

@ -0,0 +1,68 @@
class ObjectManager::Element::Backend
attr_reader :user, :attribute, :record
def initialize(user:, attribute:, record:)
@user = user
@attribute = attribute
@record = record
end
def visible?
return true if attribute.data_option[:permission].blank?
return false if user.blank?
attribute.data_option[:permission].any? do |permission|
authorized?(permission)
end
end
def authorized?(permission)
user.permissions?(permission)
end
def data
data = default_data
data[:screen] = screens if attribute.screens.present?
return data if attribute.data_option.blank?
data.merge(attribute.data_option.symbolize_keys)
end
def default_data
{
name: attribute.name,
display: attribute.display,
tag: attribute.data_type,
#:null => attribute.null,
}
end
def screens
attribute.screens.transform_values do |permission_options|
screen_value(permission_options)
end
end
def screen_value(permission_options)
return permission_options['-all-'] if permission_options['-all-']
return {} if user.blank?
screen_permission_options(permission_options)
end
def screen_permission_options(permission_options)
permission_options.each_with_object({}) do |(permission, options), result|
next if !authorized?(permission)
options.each do |key, value|
next if [true, false].include?(result[key]) && !value
result[key] = value
end
end
end
end

View file

@ -0,0 +1,37 @@
class ObjectManager::Element::Ticket < ObjectManager::Element::Backend
private
def authorized?(permission)
return false if skip?(permission)
super
end
def skip?(permission)
return true if agent_in_general_view?(permission)
return true if agent_access_missing?(permission)
authorized_customer_and_agent?(permission)
end
def agent_in_general_view?(permission)
record.blank? && permission == 'ticket.customer' && agent?
end
def agent_access_missing?(permission)
record.present? && permission == 'ticket.agent' && agent? && !read_access?
end
def authorized_customer_and_agent?(permission)
record.present? && permission == 'ticket.customer' && agent? && read_access?
end
def agent?
user.permissions?('ticket.agent')
end
def read_access?
user.group_access?(record.group_id, 'read')
end
end

View file

@ -0,0 +1,62 @@
class ObjectManager::Object
attr_reader :object_name
def initialize(object_name)
@object_name = object_name
end
=begin
get user based list of used object attributes
object = ObjectManager::Object.new('Ticket')
attribute_list = object.attributes(user)
returns:
[
{ name: 'api_key', display: 'API KEY', tag: 'input', null: true, edit: true, maxlength: 32 },
{ name: 'api_ip_regexp', display: 'API IP RegExp', tag: 'input', null: true, edit: true },
{ name: 'api_ip_max', display: 'API IP Max', tag: 'input', null: true, edit: true },
]
=end
def attributes(user, record = nil)
@attributes ||= begin
attribute_records.each_with_object([]) do |attribute_record, result|
element = element_class.new(
user: user,
attribute: attribute_record,
record: record,
)
next if !element.visible?
result.push element.data
end
end
end
private
def attribute_records
@attribute_records ||= begin
ObjectManager::Attribute.where(
object_lookup_id: object,
active: true,
to_create: false,
to_delete: false,
).order('position ASC, name ASC')
end
end
def object
@object ||= ObjectLookup.by_name(object_name)
end
def element_class
@element_class ||= ObjectManager::Element.for_object(object_name)
end
end

View file

@ -90,13 +90,28 @@ returns
=end =end
def self.access_condition(user, access) def self.access_condition(user, access)
sql = []
bind = []
if user.permissions?('ticket.agent') if user.permissions?('ticket.agent')
['group_id IN (?)', user.group_ids_access(access)] sql.push('group_id IN (?)')
elsif !user.organization || ( !user.organization.shared || user.organization.shared == false ) bind.push(user.group_ids_access(access))
['tickets.customer_id = ?', user.id]
else
['(tickets.customer_id = ? OR tickets.organization_id = ?)', user.id, user.organization.id]
end end
if user.permissions?('ticket.customer')
if !user.organization || ( !user.organization.shared || user.organization.shared == false )
sql.push('tickets.customer_id = ?')
bind.push(user.id)
else
sql.push('(tickets.customer_id = ? OR tickets.organization_id = ?)')
bind.push(user.id)
bind.push(user.organization.id)
end
end
return if sql.blank?
[ sql.join(' OR ') ].concat(bind)
end end
=begin =begin

View file

@ -19,33 +19,25 @@ returns
def self.all(data) def self.all(data)
current_user = data[:current_user] current_user = data[:current_user]
links = data[:links] overview_filter = {}
# get customer overviews
role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id')
if current_user.permissions?('ticket.customer')
overview_filter = { active: true, organization_shared: false }
if current_user.organization_id && current_user.organization.shared
overview_filter.delete(:organization_shared)
end
if links.present?
overview_filter[:link] = links
end
overviews = Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: nil }, overviews: overview_filter).or(Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: current_user.id }, overviews: overview_filter)).distinct('overview.id').order(:prio, :name)
return overviews
end
# get agent overviews
return [] if !current_user.permissions?('ticket.agent')
overview_filter = { active: true }
overview_filter_not = { out_of_office: true } overview_filter_not = { out_of_office: true }
return [] if !current_user.permissions?('ticket.customer') && !current_user.permissions?('ticket.agent')
role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id')
if data[:links].present?
overview_filter[:link] = data[:links]
end
overview_filter[:active] = true
if User.where('out_of_office = ? AND out_of_office_start_at <= ? AND out_of_office_end_at >= ? AND out_of_office_replacement_id = ? AND active = ?', true, Time.zone.today, Time.zone.today, current_user.id, true).count.positive? if User.where('out_of_office = ? AND out_of_office_start_at <= ? AND out_of_office_end_at >= ? AND out_of_office_replacement_id = ? AND active = ?', true, Time.zone.today, Time.zone.today, current_user.id, true).count.positive?
overview_filter_not = {} overview_filter_not = {}
end end
if links.present? if !current_user.organization_id || !current_user.organization.shared
overview_filter[:link] = links overview_filter[:organization_shared] = false
end end
Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: nil }, overviews: overview_filter).or(Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: current_user.id }, overviews: overview_filter)).where.not(overview_filter_not).distinct('overview.id').order(:prio, :name) Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: nil }, overviews: overview_filter).or(Overview.joins(:roles).left_joins(:users).where(overviews_roles: { role_id: role_ids }, overviews_users: { user_id: current_user.id }, overviews: overview_filter)).where.not(overview_filter_not).distinct('overview.id').order(:prio, :name)
end end

View file

@ -131,26 +131,18 @@ returns
end end
end end
=begin
# for performance reasons we moved from api calls to optimized sql queries
groups.each do |group|
filter[:group_id].push group.id
assets = group.assets(assets)
dependencies[:group_id][group.id] = { owner_id: [] }
User.group_access(group.id, 'full').each do |user| configure_attributes = nil
dependencies[:group_id][ group.id ][:owner_id].push user.id if params[:ticket].present?
next if agents[user.id] configure_attributes = ObjectManager::Object.new('Ticket').attributes(params[:current_user], params[:ticket])
agents[user.id] = true
assets = user.assets(assets)
end end
end
=end
{ {
assets: assets, assets: assets,
form_meta: { form_meta: {
filter: filter, filter: filter,
dependencies: dependencies, dependencies: dependencies,
configure_attributes: configure_attributes,
} }
} }
end end

View file

@ -127,16 +127,18 @@ returns
# try search index backend # try search index backend
if condition.blank? && SearchIndexBackend.enabled? if condition.blank? && SearchIndexBackend.enabled?
query_extension = {}
query_extension['bool'] = {}
query_extension['bool']['must'] = []
query_or = []
if current_user.permissions?('ticket.agent') if current_user.permissions?('ticket.agent')
group_ids = current_user.group_ids_access('read') group_ids = current_user.group_ids_access('read')
if group_ids.present?
access_condition = { access_condition = {
'query_string' => { 'default_field' => 'group_id', 'query' => "\"#{group_ids.join('" OR "')}\"" } 'query_string' => { 'default_field' => 'group_id', 'query' => "\"#{group_ids.join('" OR "')}\"" }
} }
else query_or.push(access_condition)
end
end
if current_user.permissions?('ticket.customer')
access_condition = if !current_user.organization || ( !current_user.organization.shared || current_user.organization.shared == false ) access_condition = if !current_user.organization || ( !current_user.organization.shared || current_user.organization.shared == false )
{ {
'query_string' => { 'default_field' => 'customer_id', 'query' => current_user.id } 'query_string' => { 'default_field' => 'customer_id', 'query' => current_user.id }
@ -150,9 +152,22 @@ returns
# customer_id: XXX OR organization_id: XXX # customer_id: XXX OR organization_id: XXX
# conditions = [ '( customer_id = ? OR organization_id = ? )', current_user.id, current_user.organization.id ] # conditions = [ '( customer_id = ? OR organization_id = ? )', current_user.id, current_user.organization.id ]
end end
query_or.push(access_condition)
end end
query_extension['bool']['must'].push access_condition return [] if query_or.blank?
query_extension = {
'bool': {
'must': [
{
'bool': {
'should': query_or,
},
},
],
}
}
items = SearchIndexBackend.search(query, 'Ticket', limit: limit, items = SearchIndexBackend.search(query, 'Ticket', limit: limit,
query_extension: query_extension, query_extension: query_extension,

View file

@ -239,7 +239,7 @@ class Transaction::Notification
locale = user.locale locale = user.locale
# only show allowed attributes # only show allowed attributes
attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) attribute_list = ObjectManager::Object.new('Ticket').attributes(user).index_by { |item| item[:name] }
user_related_changes = {} user_related_changes = {}
@item[:changes].each do |key, value| @item[:changes].each do |key, value|

View file

@ -208,7 +208,7 @@ class Transaction::Slack
locale = user.preferences[:locale] || Setting.get('locale_default') || 'en-us' locale = user.preferences[:locale] || Setting.get('locale_default') || 'en-us'
# only show allowed attributes # only show allowed attributes
attribute_list = ObjectManager::Attribute.by_object_as_hash('Ticket', user) attribute_list = ObjectManager::Object.new('Ticket').attributes(user).index_by { |item| item[:name] }
#puts "AL #{attribute_list.inspect}" #puts "AL #{attribute_list.inspect}"
user_related_changes = {} user_related_changes = {}
@item[:changes].each do |key, value| @item[:changes].each do |key, value|

View file

@ -2,15 +2,15 @@ class OrganizationPolicy < ApplicationPolicy
def show? def show?
return true if user.permissions?(['admin', 'ticket.agent']) return true if user.permissions?(['admin', 'ticket.agent'])
return false if !user.permissions?('ticket.customer') return true if record.id == user.organization_id
record.id == user.organization_id false
end end
def update? def update?
return false if user.permissions?('ticket.customer') return true if user.permissions?(['admin', 'ticket.agent'])
user.permissions?(['admin', 'ticket.agent']) false
end end
class Scope < ApplicationPolicy::Scope class Scope < ApplicationPolicy::Scope

View file

@ -55,9 +55,7 @@ class Ticket::ArticlePolicy < ApplicationPolicy
end end
def access?(query) def access?(query)
if record.internal == true && user.permissions?('ticket.customer') return false if record.internal == true && !user.permissions?('ticket.agent')
return false
end
ticket = Ticket.lookup(id: record.ticket_id) ticket = Ticket.lookup(id: record.ticket_id)
Pundit.authorize(user, ticket, query) Pundit.authorize(user, ticket, query)

View file

@ -38,26 +38,26 @@ class TicketPolicy < ApplicationPolicy
def access?(access) def access?(access)
# agent - access if requester is owner
return true if record.owner_id == user.id
# agent - access if requester is in group
return true if user.group_access?(record.group.id, access)
# check customer # check customer
if user.permissions?('ticket.customer') return false if !user.permissions?('ticket.customer')
# access ok if its own ticket # access ok if its own ticket
return true if record.customer_id == user.id return true if record.customer_id == user.id
# check organization ticket access organization_access?
end
def organization_access?
return false if record.organization_id.blank? return false if record.organization_id.blank?
return false if user.organization_id.blank? return false if user.organization_id.blank?
return false if record.organization_id != user.organization_id return false if record.organization_id != user.organization_id
return record.organization.shared? record.organization.shared?
end
# check agent
# access if requester is owner
return true if record.owner_id == user.id
# access if requester is in group
user.group_access?(record.group.id, access)
end end
end end

View file

@ -0,0 +1,20 @@
class AgentCustomer < ActiveRecord::Migration[5.2]
def change
# return if it's a new setup
return if !Setting.exists?(name: 'system_init_done')
Role.where(name: %w[Admin Agent Customer]).each do |role|
role.preferences.delete(:not)
role.save!
end
move_filter
end
def move_filter
Setting.find_by(name: '0010_postmaster_filter_trusted').update(name: '0005_postmaster_filter_trusted')
Setting.find_by(name: '0020_postmaster_filter_auto_response_check').update(name: '0006_postmaster_filter_auto_response_check')
Setting.find_by(name: '0100_postmaster_filter_follow_up_check').update(name: '0007_postmaster_filter_follow_up_check')
Setting.find_by(name: '0110_postmaster_filter_follow_up_merged').update(name: '0008_postmaster_filter_follow_up_merged')
end
end

View file

@ -2,9 +2,7 @@ Role.create_if_not_exists(
id: 1, id: 1,
name: 'Admin', name: 'Admin',
note: 'To configure your system.', note: 'To configure your system.',
preferences: { preferences: {},
not: ['Customer'],
},
default_at_signup: false, default_at_signup: false,
updated_by_id: 1, updated_by_id: 1,
created_by_id: 1 created_by_id: 1
@ -14,9 +12,7 @@ Role.create_if_not_exists(
name: 'Agent', name: 'Agent',
note: 'To work on Tickets.', note: 'To work on Tickets.',
default_at_signup: false, default_at_signup: false,
preferences: { preferences: {},
not: ['Customer'],
},
updated_by_id: 1, updated_by_id: 1,
created_by_id: 1 created_by_id: 1
) )
@ -24,9 +20,7 @@ Role.create_if_not_exists(
id: 3, id: 3,
name: 'Customer', name: 'Customer',
note: 'People who create Tickets ask for help.', note: 'People who create Tickets ask for help.',
preferences: { preferences: {},
not: %w[Agent Admin],
},
default_at_signup: true, default_at_signup: true,
updated_by_id: 1, updated_by_id: 1,
created_by_id: 1 created_by_id: 1

View file

@ -3306,13 +3306,40 @@ Setting.create_if_not_exists(
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Defines postmaster filter.', title: 'Defines postmaster filter.',
name: '0010_postmaster_filter_trusted', name: '0005_postmaster_filter_trusted',
area: 'Postmaster::PreFilter', area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to remove X-Zammad headers from not trusted sources.', description: 'Defines postmaster filter to remove X-Zammad headers from not trusted sources.',
options: {}, options: {},
state: 'Channel::Filter::Trusted', state: 'Channel::Filter::Trusted',
frontend: false frontend: false
) )
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0006_postmaster_filter_auto_response_check',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify auto responses to prevent auto replies from Zammad.',
options: {},
state: 'Channel::Filter::AutoResponseCheck',
frontend: false
)
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0007_postmaster_filter_follow_up_check',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify follow-ups (based on admin settings).',
options: {},
state: 'Channel::Filter::FollowUpCheck',
frontend: false
)
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0008_postmaster_filter_follow_up_merged',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify follow-up ticket for merged tickets.',
options: {},
state: 'Channel::Filter::FollowUpMerged',
frontend: false
)
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Defines postmaster filter.', title: 'Defines postmaster filter.',
name: '0011_postmaster_sender_based_on_reply_to', name: '0011_postmaster_sender_based_on_reply_to',
@ -3358,15 +3385,6 @@ Setting.create_if_not_exists(
state: 'Channel::Filter::SecureMailing', state: 'Channel::Filter::SecureMailing',
frontend: false frontend: false
) )
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0020_postmaster_filter_auto_response_check',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify auto responses to prevent auto replies from Zammad.',
options: {},
state: 'Channel::Filter::AutoResponseCheck',
frontend: false
)
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Defines postmaster filter.', title: 'Defines postmaster filter.',
name: '0030_postmaster_filter_out_of_office_check', name: '0030_postmaster_filter_out_of_office_check',
@ -3376,24 +3394,6 @@ Setting.create_if_not_exists(
state: 'Channel::Filter::OutOfOfficeCheck', state: 'Channel::Filter::OutOfOfficeCheck',
frontend: false frontend: false
) )
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0100_postmaster_filter_follow_up_check',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify follow-ups (based on admin settings).',
options: {},
state: 'Channel::Filter::FollowUpCheck',
frontend: false
)
Setting.create_if_not_exists(
title: 'Defines postmaster filter.',
name: '0110_postmaster_filter_follow_up_merged',
area: 'Postmaster::PreFilter',
description: 'Defines postmaster filter to identify follow-up ticket for merged tickets.',
options: {},
state: 'Channel::Filter::FollowUpMerged',
frontend: false
)
Setting.create_if_not_exists( Setting.create_if_not_exists(
title: 'Defines postmaster filter.', title: 'Defines postmaster filter.',
name: '0200_postmaster_filter_follow_up_possible_check', name: '0200_postmaster_filter_follow_up_possible_check',

View file

@ -31,7 +31,7 @@ module SessionHelper
models = {} models = {}
objects = ObjectManager.list_objects objects = ObjectManager.list_objects
objects.each do |object| objects.each do |object|
attributes = ObjectManager::Attribute.by_object(object, user) attributes = ObjectManager::Object.new(object).attributes(user)
models[object] = attributes models[object] = attributes
end end
models models

View file

@ -65,6 +65,48 @@ window.onload = function() {
active: true, active: true,
}]) }])
App.Role.refresh([
{
"name":"Agent",
"permission_ids":[
48,
],
"group_ids":{},
"default_at_signup":false,
"note":"To work on Tickets.",
"active":true,
"updated_at":"2020-07-29T14:57:27.304Z",
"id":2
},
{
"name":"Customer",
"permission_ids":[
49
],
"group_ids":{},
"default_at_signup":true,
"note":"People who create Tickets ask for help.",
"active":true,
"updated_at":"2020-07-29T14:57:27.314Z",
"id":3
}
])
App.Permission.refresh([
{
"name":"ticket.agent",
"note":"Access to Agent Tickets based on Group Access",
"active":true,
"id":48
},
{
"name":"ticket.customer",
"note":"Access to Customer Tickets based on current_user and organization",
"active":true,
"id":49
},
])
test('ticket.editabe customer user #1', function() { test('ticket.editabe customer user #1', function() {
App.Session.set(33) App.Session.set(33)
ticket1 = App.Ticket.find(1); ticket1 = App.Ticket.find(1);

View file

@ -3,7 +3,7 @@ FactoryBot.define do
sequence(:name) { |n| "Test Overview #{n}" } sequence(:name) { |n| "Test Overview #{n}" }
prio { 1 } prio { 1 }
role_ids { Role.where(name: %w[Customer Agent Admin]).pluck(:id) } role_ids { Role.where(name: %w[Customer Agent Admin]).pluck(:id) }
out_of_office { true } out_of_office { false }
updated_by_id { 1 } updated_by_id { 1 }
created_by_id { 1 } created_by_id { 1 }

View file

@ -29,6 +29,14 @@ FactoryBot.define do
end end
end end
factory :agent_and_customer do
role_ids { Role.signup_role_ids.push( Role.find_by(name: 'Agent').id ).sort }
trait :with_org do
organization
end
end
factory :agent do factory :agent do
roles { Role.where(name: 'Agent') } roles { Role.where(name: 'Agent') }
end end

View file

@ -140,6 +140,24 @@ RSpec.describe Channel::EmailParser, type: :model do
end end
end end
context 'when from address matches an existing agent customer' do
let!(:agent_customer) { create(:agent_and_customer, email: 'foo@bar.com') }
let!(:ticket) { create(:ticket, customer: agent_customer) }
let!(:raw_email) { <<~RAW.chomp }
From: foo@bar.com
To: myzammad@example.com
Subject: [#{Setting.get('ticket_hook') + Setting.get('ticket_hook_divider') + ticket.number}] test
Lorem ipsum dolor
RAW
it 'sets article.sender to "Customer"' do
described_class.new.process({}, raw_email)
expect(Ticket::Article.last.sender.name).to eq('Customer')
end
end
context 'when from address matches an existing customer' do context 'when from address matches an existing customer' do
let!(:customer) { create(:customer, email: 'foo@bar.com') } let!(:customer) { create(:customer, email: 'foo@bar.com') }

View file

@ -2,17 +2,6 @@ require 'rails_helper'
RSpec.describe ObjectManager::Attribute, type: :model do RSpec.describe ObjectManager::Attribute, type: :model do
let(:user_attribute_permissions) do
create(:user, roles: [role_attribute_permissions])
end
let(:role_attribute_permissions) do
create(:role).tap do |role|
role.permission_grant('admin.organization')
role.permission_grant('ticket.agent')
end
end
describe 'callbacks' do describe 'callbacks' do
context 'for setting default values on local data options' do context 'for setting default values on local data options' do
let(:subject) { described_class.new } let(:subject) { described_class.new }
@ -118,36 +107,4 @@ RSpec.describe ObjectManager::Attribute, type: :model do
end.not_to raise_error end.not_to raise_error
end end
end end
describe 'attribute permissions', db_strategy: :reset do
it 'merges attribute permissions' do
create(:object_manager_attribute_text, screens: { create: { 'admin.organization': { shown: true }, 'ticket.agent': { shown: false } } }, name: 'test_permissions')
migration = described_class.migration_execute
expect(migration).to be true
attribute = described_class.by_object('Ticket', user_attribute_permissions).detect { |attr| attr[:name] == 'test_permissions' }
expect(attribute[:screen]['create']['shown']).to be true
end
it 'overwrites permissions if all get set' do
create(:object_manager_attribute_text, screens: { create: { '-all-': { shown: true }, 'admin.organization': { shown: false }, 'ticket.agent': { shown: false } } }, name: 'test_permissions_all')
migration = described_class.migration_execute
expect(migration).to be true
attribute = described_class.by_object('Ticket', user_attribute_permissions).detect { |attr| attr[:name] == 'test_permissions_all' }
expect(attribute[:screen]['create']['shown']).to be true
end
it 'is able to handle other values than true or false' do
create(:object_manager_attribute_text, screens: { create: { '-all-': { shown: true, item_class: 'column' }, 'admin.organization': { shown: false }, 'ticket.agent': { shown: false } } }, name: 'test_permissions_item')
migration = described_class.migration_execute
expect(migration).to be true
attribute = described_class.by_object('Ticket', user_attribute_permissions).detect { |attr| attr[:name] == 'test_permissions_item' }
expect(attribute[:screen]['create']['item_class']).to eq('column')
end
end
end end

View file

@ -0,0 +1,110 @@
require 'rails_helper'
RSpec.describe ObjectManager::Object do
describe 'attribute permissions', db_strategy: :reset do
let(:user) do
create(:user, roles: [role_attribute_permissions])
end
let(:attribute) { described_class.new('Ticket').attributes(user).detect { |attribute| attribute[:name] == attribute_name } }
let(:role_attribute_permissions) do
create(:role).tap do |role|
role.permission_grant('admin.organization')
role.permission_grant('ticket.agent')
end
end
let(:attribute_name) { 'example_attribute' }
before do
create(:object_manager_attribute_text, name: attribute_name, screens: screens)
ObjectManager::Attribute.migration_execute
end
context 'when true and false values for show exist' do
let(:screens) do
{
create: {
'admin.organization': {
shown: true
},
'ticket.agent': {
shown: false
}
}
}
end
it 'uses true' do
expect(attribute[:screen]['create']['shown']).to be true
end
end
context 'when -all- is present' do
let(:screens) do
{
create: {
'-all-': {
shown: true
},
'admin.organization': {
shown: false
},
'ticket.agent': {
shown: false
}
}
}
end
it 'takes its values into account' do
expect(attribute[:screen]['create']['shown']).to be true
end
end
context 'when non boolean values are present' do
let(:screens) do
{
create: {
'-all-': {
shown: true,
item_class: 'column'
},
'admin.organization': {
shown: false
},
'ticket.agent': {
shown: false
}
}
}
end
it 'takes these values into account' do
expect(attribute[:screen]['create']['item_class']).to eq('column')
end
end
context 'when agent is also customer' do
let(:user) { create(:agent_and_customer) }
let(:screens) do
{
create: {
'ticket.customer': {
filter: [2, 4]
},
'ticket.agent': {
filter: [3, 5]
}
}
}
end
it 'prefers agent over customer permissions' do
expect(attribute[:screen]['create']['filter']).to eq([3, 5])
end
end
end
end

View file

@ -2,7 +2,48 @@ require 'rails_helper'
RSpec.describe Ticket::Overviews do RSpec.describe Ticket::Overviews do
describe '#index' do describe '.all' do
let(:views) { described_class.all(current_user: current_user).map(&:name) }
shared_examples 'containing' do |overview|
it "returns #{overview}" do
expect(views).to include(overview)
end
end
shared_examples 'not containing' do |overview|
it "doesn't return #{overview}" do
expect(views).not_to include(overview)
end
end
context 'when Agent' do
let(:current_user) { create(:agent) }
it_behaves_like 'containing', 'Open'
it_behaves_like 'not containing', 'My Tickets'
it_behaves_like 'not containing', 'My Organization Tickets'
end
context 'when Agent is also Customer' do
let(:current_user) { create(:agent_and_customer, :with_org) }
it_behaves_like 'containing', 'Open'
it_behaves_like 'containing', 'My Tickets'
it_behaves_like 'containing', 'My Organization Tickets'
end
context 'when Customer' do
let(:current_user) { create(:customer, :with_org) }
it_behaves_like 'not containing', 'Open'
it_behaves_like 'containing', 'My Tickets'
it_behaves_like 'containing', 'My Organization Tickets'
end
end
describe '.index' do
# https://github.com/zammad/zammad/issues/1769 # https://github.com/zammad/zammad/issues/1769
it 'does not return multiple results for a single ticket' do it 'does not return multiple results for a single ticket' do

View file

@ -861,6 +861,123 @@ RSpec.describe Ticket, type: :model do
end end
end end
describe '.search' do
shared_examples 'search permissions' do
let(:group) { create(:group) }
before do
ticket
end
shared_examples 'permitted' do
it 'finds Ticket' do
expect( described_class.search(query: ticket.number, current_user: current_user).count ).to eq(1)
end
end
shared_examples 'no permission' do
it "doesn't find Ticket" do
expect( described_class.search(query: ticket.number, current_user: current_user) ).to be_blank
end
end
context 'Agent with Group access' do
let(:ticket) do
ticket = create(:ticket, group: group)
create(:ticket_article, ticket: ticket)
ticket
end
let(:current_user) { create(:agent, groups: [group]) }
it_behaves_like 'permitted'
end
context 'when Agent is Customer of Ticket' do
let(:ticket) do
ticket = create(:ticket, customer: current_user)
create(:ticket_article, ticket: ticket)
ticket
end
let(:current_user) { create(:agent_and_customer) }
it_behaves_like 'permitted'
end
context 'for Organization access' do
let(:ticket) do
ticket = create(:ticket, customer: customer)
create(:ticket_article, ticket: ticket)
ticket
end
let(:customer) { create(:customer, organization: organization) }
context 'when Organization is shared' do
let(:organization) { create(:organization, shared: true) }
context 'for unrelated Agent' do
let(:current_user) { create(:agent) }
it_behaves_like 'no permission'
end
context 'for Agent in same Organization' do
let(:current_user) { create(:agent_and_customer, organization: organization) }
it_behaves_like 'permitted'
end
context 'for Customer of Ticket' do
let(:current_user) { customer }
it_behaves_like 'permitted'
end
end
context 'when Organization is not shared' do
let(:organization) { create(:organization, shared: false) }
context 'for unrelated Agent' do
let(:current_user) { create(:agent) }
it_behaves_like 'no permission'
end
context 'for Agent in same Organization' do
let(:current_user) { create(:agent_and_customer, organization: organization) }
it_behaves_like 'no permission'
end
context 'for Customer of Ticket' do
let(:current_user) { customer }
it_behaves_like 'permitted'
end
end
end
end
context 'with searchindex', searchindex: true do
include_examples 'search permissions' do
before do
configure_elasticsearch(required: true, rebuild: true)
end
end
end
context 'without searchindex' do
include_examples 'search permissions'
end
end
describe 'Callbacks & Observers -' do describe 'Callbacks & Observers -' do
describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do describe 'NULL byte handling (via ChecksAttributeValuesAndLength concern):' do
it 'removes them from title on creation, if necessary (postgres doesnt like them)' do it 'removes them from title on creation, if necessary (postgres doesnt like them)' do

View file

@ -0,0 +1,38 @@
require 'rails_helper'
describe OrganizationPolicy do
subject { described_class.new(user, record) }
let(:record) { create(:organization) }
context 'when customer' do
let(:user) { create(:customer, organization: record) }
it { is_expected.to permit_actions(%i[show]) }
it { is_expected.not_to permit_actions(%i[update]) }
end
context 'when customer without organization' do
let(:user) { create(:customer) }
it { is_expected.not_to permit_actions(%i[show update]) }
end
context 'when agent and customer' do
let(:user) { create(:agent_and_customer, organization: record) }
it { is_expected.to permit_actions(%i[show update]) }
end
context 'when agent' do
let(:user) { create(:agent) }
it { is_expected.to permit_actions(%i[show update]) }
end
context 'when admin' do
let(:user) { create(:admin) }
it { is_expected.to permit_actions(%i[show update]) }
end
end

View file

@ -0,0 +1,57 @@
require 'rails_helper'
describe Ticket::ArticlePolicy do
subject { described_class.new(user, record) }
let!(:group) { create(:group) }
let!(:ticket_customer) { create(:customer) }
let(:record) do
ticket = create(:ticket, group: group, customer: ticket_customer)
create(:ticket_article, ticket: ticket)
end
context 'when article internal' do
let(:record) do
ticket = create(:ticket, group: group, customer: ticket_customer)
create(:ticket_article, ticket: ticket, internal: true)
end
context 'when agent' do
let(:user) { create(:agent, groups: [group]) }
it { is_expected.to permit_actions(%i[show]) }
end
context 'when agent and customer' do
let(:user) { create(:agent_and_customer, groups: [group]) }
it { is_expected.to permit_actions(%i[show]) }
end
context 'when customer' do
let(:user) { ticket_customer }
it { is_expected.not_to permit_actions(%i[show]) }
end
end
context 'when agent' do
let(:user) { create(:agent, groups: [group]) }
it { is_expected.to permit_actions(%i[show]) }
end
context 'when agent and customer' do
let(:user) { create(:agent_and_customer, groups: [group]) }
it { is_expected.to permit_actions(%i[show]) }
end
context 'when customer' do
let(:user) { ticket_customer }
it { is_expected.to permit_actions(%i[show]) }
end
end

View file

@ -11,6 +11,12 @@ describe TicketPolicy do
it { is_expected.to permit_actions(%i[show full]) } it { is_expected.to permit_actions(%i[show full]) }
end end
context 'when given user that is agent and customer' do
let(:user) { create(:agent_and_customer, groups: [record.group]) }
it { is_expected.to permit_actions(%i[show full]) }
end
context 'when given a user that is neither owner nor customer' do context 'when given a user that is neither owner nor customer' do
let(:user) { create(:agent) } let(:user) { create(:agent) }

View file

@ -877,4 +877,151 @@ RSpec.describe 'Ticket zoom', type: :system do
expect(images_identical?(images.first, images.second)).to be(true) expect(images_identical?(images.first, images.second)).to be(true)
end end
end end
context 'object manager attribute permission view' do
let!(:group_users) { Group.find_by(name: 'Users') }
shared_examples 'shows attributes and values for agent view and editable' do
it 'shows attributes and values for agent view and editable', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_select('state_id', options: ['new', 'open', 'pending reminder', 'pending close', 'closed'])
expect(page).to have_select('priority_id')
expect(page).to have_select('owner_id')
expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
shared_examples 'shows attributes and values for agent view but disabled' do
it 'shows attributes and values for agent view but disabled', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_css('select[name=state_id][disabled]')
expect(page).to have_css('select[name=priority_id][disabled]')
expect(page).to have_css('select[name=owner_id][disabled]')
expect(page).to have_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
shared_examples 'shows attributes and values for customer view' do
it 'shows attributes and values for customer view', authenticated_as: :current_user do
visit "ticket/zoom/#{ticket.id}"
refresh # refresh to have assets generated for ticket
expect(page).to have_select('state_id', options: %w[new open closed])
expect(page).not_to have_select('priority_id')
expect(page).not_to have_select('owner_id')
expect(page).not_to have_css('div.tabsSidebar-tab[data-tab=customer]')
end
end
context 'as customer' do
let!(:current_user) { create(:customer) }
let(:ticket) { create(:ticket, customer: current_user) }
include_examples 'shows attributes and values for customer view'
end
context 'as agent with full permissions' do
let(:current_user) { create(:agent, groups: [ group_users ] ) }
let(:ticket) { create(:ticket, group: group_users ) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'as agent with change permissions' do
let!(:current_user) { create(:agent) }
let(:ticket) { create(:ticket, group: group_users) }
before do
current_user.group_names_access_map = {
group_users.name => %w[read change],
}
end
include_examples 'shows attributes and values for agent view and editable'
end
context 'as agent with read permissions' do
let!(:current_user) { create(:agent) }
let(:ticket) { create(:ticket, group: group_users) }
before do
current_user.group_names_access_map = {
group_users.name => 'read',
}
end
include_examples 'shows attributes and values for agent view but disabled'
end
context 'as agent+customer with full permissions' do
let!(:current_user) { create(:agent_and_customer, groups: [ group_users ] ) }
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users ) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users ) }
include_examples 'shows attributes and values for agent view and editable'
end
end
context 'as agent+customer with change permissions' do
let!(:current_user) { create(:agent_and_customer) }
before do
current_user.group_names_access_map = {
group_users.name => %w[read change],
}
end
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
include_examples 'shows attributes and values for agent view and editable'
end
end
context 'as agent+customer with read permissions' do
let!(:current_user) { create(:agent_and_customer) }
before do
current_user.group_names_access_map = {
group_users.name => 'read',
}
end
context 'normal ticket' do
let(:ticket) { create(:ticket, group: group_users) }
include_examples 'shows attributes and values for agent view but disabled'
end
context 'ticket where current_user is also customer' do
let(:ticket) { create(:ticket, customer: current_user, group: group_users) }
include_examples 'shows attributes and values for agent view but disabled'
end
end
context 'as agent+customer but only customer for the ticket (no agent access)' do
let!(:current_user) { create(:agent_and_customer) }
let(:ticket) { create(:ticket, customer: current_user) }
include_examples 'shows attributes and values for customer view'
end
end
end end

View file

@ -123,7 +123,7 @@ class TicketOverviewOutOfOfficeTest < ActiveSupport::TestCase
link: 'my_tickets', link: 'my_tickets',
prio: 1100, prio: 1100,
role_ids: [overview_role.id], role_ids: [overview_role.id],
out_of_office: true, out_of_office: false,
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',

View file

@ -651,12 +651,6 @@ class UserTest < ActiveSupport::TestCase
created_by_id: 1, created_by_id: 1,
) )
assert_raises(RuntimeError) do
customer3.roles = Role.where(name: %w[Customer Admin])
end
assert_raises(RuntimeError) do
customer3.roles = Role.where(name: %w[Customer Agent])
end
customer3.roles = Role.where(name: %w[Admin Agent]) customer3.roles = Role.where(name: %w[Admin Agent])
customer3.roles.each do |role| customer3.roles.each do |role|
assert_not_equal(role.name, 'Customer') assert_not_equal(role.name, 'Customer')