ldap.coffee 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584
  1. class Index extends App.ControllerIntegrationBase
  2. featureIntegration: 'ldap_integration'
  3. featureName: 'LDAP'
  4. featureConfig: 'ldap_config'
  5. description: [
  6. ['This service enables Zammad to connect with your LDAP server.']
  7. ]
  8. events:
  9. 'change .js-switch input': 'switch'
  10. render: =>
  11. super
  12. new Form(
  13. el: @$('.js-form')
  14. )
  15. #new App.ImportJob(
  16. # el: @$('.js-importJob')
  17. # facility: 'ldap'
  18. #)
  19. new App.HttpLog(
  20. el: @$('.js-log')
  21. facility: 'ldap'
  22. )
  23. switch: =>
  24. super
  25. active = @$('.js-switch input').prop('checked')
  26. if active
  27. @ajax(
  28. id: 'jobs_config'
  29. type: 'POST'
  30. url: "#{@apiPath}/integration/ldap/job_start"
  31. processData: true
  32. success: (data, status, xhr) =>
  33. @render(true)
  34. )
  35. class Form extends App.Controller
  36. elements:
  37. '.js-lastImport': 'lastImport'
  38. '.js-wizard': 'wizardButton'
  39. events:
  40. 'click .js-wizard': 'startWizard'
  41. 'click .js-start-sync': 'startSync'
  42. constructor: ->
  43. super
  44. @render()
  45. @lastResult()
  46. @activeDryRun()
  47. currentConfig: ->
  48. App.Setting.get('ldap_config') || {}
  49. setConfig: (value) =>
  50. App.Setting.set('ldap_config', value, {notify: true})
  51. @startSync()
  52. render: (top = false) =>
  53. @config = @currentConfig()
  54. group_role_map = {}
  55. for source, dests of @config.group_role_map
  56. group_role_map[source] = dests.map((dest) ->
  57. App.Role.find(dest).displayName()
  58. ).join ', '
  59. @html App.view('integration/ldap')(
  60. config: @config,
  61. group_role_map: group_role_map
  62. )
  63. if _.isEmpty(@config)
  64. @$('.js-notConfigured').removeClass('hide')
  65. @$('.js-summary').addClass('hide')
  66. else
  67. @$('.js-notConfigured').addClass('hide')
  68. @$('.js-summary').removeClass('hide')
  69. if top
  70. a = =>
  71. @scrollToIfNeeded($('.content.active .page-header'))
  72. @delay(a, 500)
  73. startSync: =>
  74. @ajax(
  75. id: 'jobs_config'
  76. type: 'POST'
  77. url: "#{@apiPath}/integration/ldap/job_start"
  78. processData: true
  79. success: (data, status, xhr) =>
  80. @render(true)
  81. )
  82. startWizard: (e) =>
  83. e.preventDefault()
  84. new ConnectionWizard(
  85. container: @el.closest('.content')
  86. config: @config
  87. callback: (config) =>
  88. @setConfig(config)
  89. )
  90. lastResult: =>
  91. @ajax(
  92. id: 'jobs_start_index'
  93. type: 'GET'
  94. url: "#{@apiPath}/integration/ldap/job_start"
  95. processData: true
  96. success: (job, status, xhr) =>
  97. if !_.isEmpty(job)
  98. if !@lastResultShowJob || @lastResultShowJob.updated_at != job.updated_at
  99. @lastResultShowJob = job
  100. @lastResultShow(job)
  101. if job.finished_at
  102. @wizardButton.attr('disabled', false)
  103. else
  104. @wizardButton.attr('disabled', true)
  105. @delay(@lastResult, 5000)
  106. )
  107. lastResultShow: (job) =>
  108. if _.isEmpty(job)
  109. @lastImport.html('')
  110. return
  111. countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped + job.result.failed
  112. if !job.result.roles
  113. job.result.roles = {}
  114. for role_id, statistic of job.result.role_ids
  115. role = App.Role.find(role_id)
  116. job.result.roles[role.displayName()] = statistic
  117. el = $(App.view('integration/ldap_last_import')(job: job, countDone: countDone))
  118. @lastImport.html(el)
  119. activeDryRun: =>
  120. @ajax(
  121. id: 'jobs_try_index'
  122. type: 'GET'
  123. url: "#{@apiPath}/integration/ldap/job_try"
  124. data:
  125. finished: false
  126. processData: true
  127. success: (job, status, xhr) =>
  128. return if _.isEmpty(job)
  129. # show analyzing
  130. new ConnectionWizard(
  131. container: @el.closest('.content')
  132. config: job.payload
  133. start: 'tryLoop'
  134. callback: (config) =>
  135. @wizardButton.attr('disabled', false)
  136. @setConfig(config)
  137. )
  138. @wizardButton.attr('disabled', true)
  139. )
  140. class State
  141. @current: ->
  142. App.Setting.get('ldap_integration')
  143. class ConnectionWizard extends App.WizardModal
  144. wizardConfig: {}
  145. slideMethod:
  146. 'js-bind': 'bindShow'
  147. 'js-mapping': 'mappingShow'
  148. events:
  149. 'submit form.js-discover': 'discover'
  150. 'submit form.js-bind': 'bindChange'
  151. 'click .js-mapping .js-submitTry': 'mappingChange'
  152. 'click .js-try .js-submitSave': 'save'
  153. 'click .js-close': 'hide'
  154. 'click .js-remove': 'removeRow'
  155. 'click .js-userMappingForm .js-add': 'addUserMapping'
  156. 'click .js-groupRoleForm .js-add': 'addGroupRoleMapping'
  157. 'click .js-goToSlide': 'goToSlide'
  158. 'input .js-hostUrl': 'sslVerifyChange'
  159. elements:
  160. '.modal-body': 'body'
  161. '.js-userMappingForm': 'userMappingForm'
  162. '.js-groupRoleForm': 'groupRoleForm'
  163. '.js-expertForm': 'expertForm'
  164. constructor: ->
  165. super
  166. if !_.isEmpty(@config)
  167. @wizardConfig = @config
  168. if @container
  169. @el.addClass('modal--local')
  170. @render()
  171. @el.modal
  172. keyboard: true
  173. show: true
  174. backdrop: true
  175. container: @container
  176. .on
  177. 'show.bs.modal': @onShow
  178. 'shown.bs.modal': @onShown
  179. 'hidden.bs.modal': =>
  180. @el.remove()
  181. if @slide
  182. @showSlide(@slide)
  183. else
  184. @showHost()
  185. if @start
  186. @[@start]()
  187. render: =>
  188. @html App.view('integration/ldap_wizard')()
  189. save: (e) =>
  190. e.preventDefault()
  191. @callback(@wizardConfig)
  192. @hide(e)
  193. showSlide: (slide) =>
  194. method = @slideMethod[slide]
  195. if method && @[method]
  196. @[method](true)
  197. super
  198. showHost: =>
  199. @$('.js-discover input[name="host_url"]').val(@wizardConfig.host_url)
  200. @checkSslVerifyVisibility(@wizardConfig.host_url)
  201. sslVerifyChange: (e) =>
  202. @checkSslVerifyVisibility($(e.currentTarget).val())
  203. checkSslVerifyVisibility: (host_url) =>
  204. el = @$('.js-discover .js-sslVerify')
  205. exists = el.length
  206. disabled = true
  207. if host_url && host_url.startsWith('ldaps')
  208. disabled = false
  209. if exists && disabled
  210. el.parent().remove()
  211. else if !exists && !disabled
  212. @$('.js-discover tbody tr').last().after(@buildRowSslVerify())
  213. buildRowSslVerify: =>
  214. el = $(App.view('integration/ldap_ssl_verify_row')())
  215. ssl_verify = true
  216. if typeof @wizardConfig.ssl_verify != 'undefined'
  217. ssl_verify = @wizardConfig.ssl_verify
  218. sslVerifyElement = App.UiElement.boolean.render(
  219. name: 'ssl_verify'
  220. null: false
  221. options: { true: 'yes', false: 'no' }
  222. default: ssl_verify
  223. translate: true
  224. class: 'form-control form-control--small'
  225. )
  226. el.find('.js-sslVerify').html sslVerifyElement
  227. el
  228. discover: (e) =>
  229. e.preventDefault()
  230. @showSlide('js-connect')
  231. params = @formParam(e.target)
  232. @ajax(
  233. id: 'ldap_discover'
  234. type: 'POST'
  235. url: "#{@apiPath}/integration/ldap/discover"
  236. data: JSON.stringify(params)
  237. processData: true
  238. success: (data, status, xhr) =>
  239. if data.result isnt 'ok'
  240. @showSlide('js-discover')
  241. @showAlert('js-discover', data.message)
  242. return
  243. @wizardConfig.host_url = params.host_url
  244. @wizardConfig.ssl_verify = params.ssl_verify
  245. option = ''
  246. options = {}
  247. if !_.isEmpty data.attributes
  248. for dn in data.attributes.namingcontexts
  249. options[dn] = dn
  250. if option is ''
  251. option = dn
  252. if option.length > dn.length
  253. option = dn
  254. @wizardConfig.options = options
  255. @wizardConfig.option = option
  256. @bindShow()
  257. error: (xhr, statusText, error) =>
  258. detailsRaw = xhr.responseText
  259. details = {}
  260. if !_.isEmpty(detailsRaw)
  261. details = JSON.parse(detailsRaw)
  262. @showSlide('js-discover')
  263. @showAlert('js-discover', details.error || 'Unable to perform backend.')
  264. )
  265. bindShow: (alreadyShown) =>
  266. @showSlide('js-bind') if !alreadyShown
  267. @$('.js-bind .js-baseDn').html(@createSelection('base_dn', @wizardConfig.options, @wizardConfig.base_dn || @wizardConfig.option, true))
  268. @$('.js-bind input[name="bind_user"]').val(@wizardConfig.bind_user)
  269. @$('.js-bind input[name="bind_pw"]').val(@wizardConfig.bind_pw)
  270. bindChange: (e) =>
  271. e.preventDefault()
  272. @showSlide('js-analyze')
  273. params = @formParam(e.target)
  274. params.host_url = @wizardConfig.host_url
  275. params.ssl_verify = @wizardConfig.ssl_verify
  276. @ajax(
  277. id: 'ldap_bind'
  278. type: 'POST'
  279. url: "#{@apiPath}/integration/ldap/bind"
  280. data: JSON.stringify(params)
  281. processData: true
  282. success: (data, status, xhr) =>
  283. if data.result isnt 'ok'
  284. @showSlide('js-bind')
  285. @showAlert('js-bind', data.message)
  286. return
  287. if _.isEmpty(data.user_attributes)
  288. @showSlide('js-bind')
  289. @showAlert('js-bind', 'Unable to retrive user information, please check your bind user permissions.')
  290. return
  291. if _.isEmpty(data.groups)
  292. @showSlide('js-bind')
  293. @showAlert('js-bind', 'Unable to retrive group information, please check your bind user permissions.')
  294. return
  295. # update config if successfull
  296. for key, value of params
  297. @wizardConfig[key] = value
  298. # remember payload
  299. user_attributes = {}
  300. for key, value of App.User.attributesGet()
  301. continue if key == 'login'
  302. if (value.tag is 'input' || value.tag is 'richtext' || value.tag is 'textarea') && value.type isnt 'password'
  303. user_attributes[key] = value.display || key
  304. roles = {}
  305. for role in App.Role.findAllByAttribute('active', true)
  306. roles[role.id] = role.displayName()
  307. # update wizard data
  308. @wizardConfig.wizardData= {}
  309. @wizardConfig.wizardData.backend_user_attributes = data.user_attributes
  310. @wizardConfig.wizardData.backend_groups = data.groups
  311. @wizardConfig.wizardData.user_attributes = user_attributes
  312. @wizardConfig.wizardData.roles = roles
  313. for key in ['user_uid', 'user_filter', 'group_uid', 'group_filter']
  314. @wizardConfig[key] = data[key]
  315. @mappingShow()
  316. error: (xhr, statusText, error) =>
  317. detailsRaw = xhr.responseText
  318. details = {}
  319. if !_.isEmpty(detailsRaw)
  320. details = JSON.parse(detailsRaw)
  321. @showSlide('js-bind')
  322. @showAlert('js-bind', details.error || 'Unable to perform backend.')
  323. )
  324. mappingShow: (alreadyShown) =>
  325. @showSlide('js-mapping') if !alreadyShown
  326. user_attribute_map = @wizardConfig.user_attributes
  327. if _.isEmpty(user_attribute_map)
  328. user_attribute_map =
  329. givenname: 'firstname'
  330. sn: 'lastname'
  331. mail: 'email'
  332. telephonenumber: 'phone'
  333. @userMappingForm.find('tbody tr.js-entry').remove()
  334. @userMappingForm.find('tbody tr').before(@buildRowsUserMap(user_attribute_map))
  335. @groupRoleForm.find('tbody tr.js-entry').remove()
  336. @groupRoleForm.find('tbody tr').before(@buildRowsGroupRole(@wizardConfig.group_role_map))
  337. @$('.js-mapping input[name="user_filter"]').val(@wizardConfig.user_filter)
  338. unassigned_users_choices =
  339. sigup_roles: App.i18n.translatePlain('Assign signup roles')
  340. skip_sync: App.i18n.translatePlain('Don\'t synchronize')
  341. @$('.js-unassignedUsers').html(@createSelection('unassigned_users', unassigned_users_choices, @wizardConfig.unassigned_users || 'sigup_roles'))
  342. mappingChange: (e) =>
  343. e.preventDefault()
  344. # user map
  345. user_attributes = @formParam(@userMappingForm)
  346. for key in ['source', 'dest']
  347. if !_.isArray(user_attributes[key])
  348. user_attributes[key] = [user_attributes[key]]
  349. user_attributes_local =
  350. "#{@wizardConfig['user_uid']}": 'login'
  351. length = user_attributes.source.length-1
  352. for count in [0..length]
  353. if user_attributes.source[count] && user_attributes.dest[count]
  354. user_attributes_local[user_attributes.source[count]] = user_attributes.dest[count]
  355. @wizardConfig.user_attributes = user_attributes_local
  356. # group role map
  357. group_role_map = @formParam(@groupRoleForm)
  358. for key in ['source', 'dest']
  359. if !_.isArray(group_role_map[key])
  360. group_role_map[key] = [group_role_map[key]]
  361. group_role_map_local = {}
  362. length = group_role_map.source.length-1
  363. for count in [0..length]
  364. if group_role_map.source[count] && group_role_map.dest[count]
  365. if !_.isArray(group_role_map_local[group_role_map.source[count]])
  366. group_role_map_local[group_role_map.source[count]] = []
  367. group_role_map_local[group_role_map.source[count]].push group_role_map.dest[count]
  368. @wizardConfig.group_role_map = group_role_map_local
  369. expertSettings = @formParam(@expertForm)
  370. @wizardConfig.user_filter = expertSettings.user_filter
  371. @wizardConfig.unassigned_users = expertSettings.unassigned_users
  372. @tryShow()
  373. buildRowsUserMap: (user_attribute_map) =>
  374. # show static login row
  375. userUidDisplayValue = @wizardConfig.wizardData.backend_user_attributes[ @wizardConfig['user_uid'] ]
  376. el = [
  377. $(App.view('integration/ldap_user_attribute_row_read_only')(
  378. key: userUidDisplayValue,
  379. value: 'Login'
  380. ))
  381. ]
  382. for source, dest of user_attribute_map
  383. continue if source == @wizardConfig['user_uid']
  384. continue if !(source of @wizardConfig.wizardData.backend_user_attributes)
  385. el.push @buildRowUserAttribute(source, dest)
  386. el
  387. buildRowUserAttribute: (source, dest) =>
  388. el = $(App.view('integration/ldap_user_attribute_row')())
  389. el.find('.js-ldapAttribute').html(@createSelection('source', @wizardConfig.wizardData.backend_user_attributes, source))
  390. el.find('.js-userAttribute').html(@createSelection('dest', @wizardConfig.wizardData.user_attributes, dest))
  391. el
  392. buildRowsGroupRole: (group_role_map) =>
  393. el = []
  394. for source, dests of group_role_map
  395. for dest in dests
  396. el.push @buildRowGroupRole(source, dest)
  397. el
  398. buildRowGroupRole: (source, dest) =>
  399. el = $(App.view('integration/ldap_group_role_row')())
  400. el.find('.js-ldapList').html(@createSelection('source', @wizardConfig.wizardData.backend_groups, source))
  401. el.find('.js-roleList').html(@createSelection('dest', @wizardConfig.wizardData.roles, dest))
  402. el
  403. createSelection: (name, options, selected, unknown) ->
  404. return App.UiElement.searchable_select.render(
  405. name: name
  406. multiple: false
  407. limit: 100
  408. null: false
  409. nulloption: false
  410. options: options
  411. value: selected
  412. unknown: unknown
  413. class: 'form-control--small'
  414. )
  415. removeRow: (e) ->
  416. e.preventDefault()
  417. $(e.target).closest('tr').remove()
  418. addUserMapping: (e) =>
  419. e.preventDefault()
  420. @userMappingForm.find('tbody tr').last().before(@buildRowUserAttribute())
  421. addGroupRoleMapping: (e) =>
  422. e.preventDefault()
  423. @groupRoleForm.find('tbody tr').last().before(@buildRowGroupRole())
  424. tryShow: (e) =>
  425. if e
  426. e.preventDefault()
  427. @showSlide('js-analyze')
  428. # create import job
  429. @ajax(
  430. id: 'ldap_try'
  431. type: 'POST'
  432. url: "#{@apiPath}/integration/ldap/job_try"
  433. data: JSON.stringify(@wizardConfig)
  434. processData: true
  435. success: (data, status, xhr) =>
  436. @tryLoop()
  437. )
  438. tryLoop: =>
  439. @showSlide('js-dry')
  440. @ajax(
  441. id: 'jobs_try_index'
  442. type: 'GET'
  443. url: "#{@apiPath}/integration/ldap/job_try"
  444. data:
  445. finished: true
  446. processData: true
  447. success: (job, status, xhr) =>
  448. if job.result && (job.result.error || job.result.info)
  449. @showSlide('js-error')
  450. @showAlert('js-error', (job.result.error || job.result.info))
  451. return
  452. if job.result && job.result.sum
  453. @$('.js-preprogress').addClass('hide')
  454. @$('.js-analyzing').removeClass('hide')
  455. total = 0
  456. if job.result.created
  457. total += job.result.created
  458. if job.result.failed
  459. total += job.result.failed
  460. if job.result.skipped
  461. total += job.result.skipped
  462. if job.result.unchanged
  463. total += job.result.unchanged
  464. if job.result.updated
  465. total += job.result.updated
  466. @$('.js-progress progress').attr('value', total)
  467. @$('.js-progress progress').attr('max', job.result.sum)
  468. if job.finished_at
  469. # reset initial state in case the back button is used
  470. @$('.js-preprogress').removeClass('hide')
  471. @$('.js-analyzing').addClass('hide')
  472. @tryResult(job)
  473. return
  474. else
  475. @delay(@tryLoop, 4000)
  476. return
  477. @hide()
  478. )
  479. tryResult: (job) =>
  480. if !job.result.roles
  481. job.result.roles = {}
  482. for role_id, statistic of job.result.role_ids
  483. role = App.Role.find(role_id)
  484. job.result.roles[role.displayName()] = statistic
  485. countDone = job.result.created + job.result.updated + job.result.unchanged + job.result.skipped
  486. @showSlide('js-try')
  487. el = $(App.view('integration/ldap_summary')(job: job, countDone: countDone))
  488. @el.find('.js-summary').html(el)
  489. App.Config.set(
  490. 'IntegrationLDAP'
  491. {
  492. name: 'LDAP'
  493. target: '#system/integration/ldap'
  494. description: 'LDAP integration for user management.'
  495. controller: Index
  496. state: State
  497. }
  498. 'NavBarIntegrations'
  499. )