diff --git a/Gemfile b/Gemfile index 5c3793a04..0bc92366e 100644 --- a/Gemfile +++ b/Gemfile @@ -66,6 +66,9 @@ require 'yaml' gem 'net-ldap' +# password security +gem 'argon2' + gem 'writeexcel' gem 'icalendar' gem 'browser' @@ -115,6 +118,9 @@ group :development, :test do # Setting ENV for testing purposes gem 'figaro' + + # Use Factory Girl for generating random test data + gem 'factory_girl_rails' end gem 'puma', group: :puma diff --git a/Gemfile.lock b/Gemfile.lock index 2d4f2c7b0..8de6cbc06 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -46,6 +46,9 @@ GEM tzinfo (~> 1.1) addressable (2.4.0) arel (6.0.3) + argon2 (1.1.1) + ffi (~> 1.9) + ffi-compiler (~> 0.1) ast (2.3.0) autoprefixer-rails (6.4.1.1) execjs @@ -107,11 +110,19 @@ GEM erubis (2.7.0) eventmachine (1.2.0.1) execjs (2.7.0) + factory_girl (4.8.0) + activesupport (>= 3.0.0) + factory_girl_rails (4.8.0) + factory_girl (~> 4.8.0) + railties (>= 3.0.0) faraday (0.9.2) multipart-post (>= 1.2, < 3) faraday-http-cache (1.3.1) faraday (~> 0.8) ffi (1.9.14) + ffi-compiler (0.1.3) + ffi (>= 1.0.0) + rake figaro (1.1.1) thor (~> 0.14) formatador (0.2.5) @@ -396,6 +407,7 @@ PLATFORMS DEPENDENCIES activerecord-nulldb-adapter activerecord-session_store + argon2 autoprefixer-rails biz browser @@ -413,6 +425,7 @@ DEPENDENCIES email_verifier eventmachine execjs + factory_girl_rails figaro github_changelog_generator guard diff --git a/VERSION b/VERSION new file mode 100644 index 000000000..9370d0503 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.3.x diff --git a/app/assets/javascripts/app/controllers/ticket_overview.coffee b/app/assets/javascripts/app/controllers/ticket_overview.coffee index 7fc700e74..e7e6e970f 100644 --- a/app/assets/javascripts/app/controllers/ticket_overview.coffee +++ b/app/assets/javascripts/app/controllers/ticket_overview.coffee @@ -44,7 +44,6 @@ class App.TicketOverview extends App.Controller # TODO: fire @cancelDrag on ESC dragItem: (event) => - event.preventDefault() pos = @batchDragger.data() threshold = 3 x = event.pageX - pos.dx @@ -52,8 +51,12 @@ class App.TicketOverview extends App.Controller dir = if event.pageY > pos.startY then 1 else -1 if !pos.moved - if Math.abs(event.pageX - pos.startX) > threshold or Math.abs(event.pageY - pos.startY) > threshold + # trigger when moved a little up or down + # but don't trigge when moved left or right + # because the user might just want to select text + if Math.abs(event.pageY - pos.startY) - Math.abs(event.pageX - pos.startX) > threshold @batchDragger.data 'moved', true + @el.addClass('u-no-userselect') # check grabbed items batch checkbox to make sure its checked # (could be grabbed without checking the checkbox it) @grabbedItemWasntChecked = !@grabbedItem.find('[name="bulk"]').prop('checked') @@ -84,6 +87,8 @@ class App.TicketOverview extends App.Controller else return + event.preventDefault() + $.Velocity.hook @batchDragger, 'translateX', "#{x}px" $.Velocity.hook @batchDragger, 'translateY', "#{y}px" @@ -103,15 +108,17 @@ class App.TicketOverview extends App.Controller scale: 1.1 options: duration: 200 - complete: -> + complete: => @hoveredBatchEntry.velocity 'reverse', duration: 200 complete: => # clean scale + action = @hoveredBatchEntry.attr('data-action') + items = @el.find('[name="bulk"]:checked') @hoveredBatchEntry.removeAttr('style') @cleanUpDrag(true) - @performBatchAction @hoveredBatchEntry.attr('data-action') - @hoveredBatchEntry = null + + @performBatchAction items, action @batchDragger.velocity properties: scale: 0 @@ -125,7 +132,9 @@ class App.TicketOverview extends App.Controller cleanUpDrag: (success) -> @hideBatchOverlay() + @el.removeClass('u-no-userselect') $('.batch-dragger').remove() + @hoveredBatchEntry = null if @grabbedItemWasntChecked @grabbedItem.find('[name="bulk"]').prop('checked', false) @@ -151,8 +160,8 @@ class App.TicketOverview extends App.Controller easing: 'ease-in-out' duration: 300 - performBatchAction: (action) -> - console.log "perform action #{action} on checked items" + performBatchAction: (items, action) -> + console.log "perform action #{action} on #{items.length} checked items" showBatchOverlay: -> @batchOverlay.show() @@ -755,7 +764,7 @@ class Table extends App.Controller checkbox: checkbox ) table = $(table) - table.delegate('[name="bulk_all"]', 'click', (e) -> + table.delegate('[name="bulk_all"]', 'change', (e) -> if $(e.currentTarget).prop('checked') $(e.currentTarget).closest('table').find('[name="bulk"]').prop('checked', true) else @@ -891,7 +900,7 @@ class Table extends App.Controller @bulkForm.show() # show/hide bulk action - @$('.table-overview').delegate('input[name="bulk"], input[name="bulk_all"]', 'click', (e) => + @$('.table-overview').delegate('input[name="bulk"], input[name="bulk_all"]', 'change', (e) => if @$('.table-overview').find('input[name="bulk"]:checked').length == 0 @bulkForm.hide() @bulkForm.reset() @@ -900,9 +909,21 @@ class Table extends App.Controller ) # deselect bulk_all if one item is uncheck observ - @$('.table-overview').delegate('[name="bulk"]', 'click', (e) -> - if !$(e.target).prop('checked') - $(e.target).parents().find('[name="bulk_all"]').prop('checked', false) + @$('.table-overview').delegate('[name="bulk"]', 'change', (e) => + bulkAll = @$('.table-overview').find('[name="bulk_all"]') + checkedCount = @$('.table-overview').find('input[name="bulk"]:checked').length + checkboxCount = @$('.table-overview').find('input[name="bulk"]').length + + if checkedCount is 0 + bulkAll.prop('indeterminate', false) + bulkAll.prop('checked', false) + else + if checkedCount is checkboxCount + bulkAll.prop('indeterminate', false) + bulkAll.prop('checked', true) + else + bulkAll.prop('checked', false) + bulkAll.prop('indeterminate', true) ) getSelected: -> diff --git a/app/assets/javascripts/app/controllers/version.coffee b/app/assets/javascripts/app/controllers/version.coffee new file mode 100644 index 000000000..44bad0de8 --- /dev/null +++ b/app/assets/javascripts/app/controllers/version.coffee @@ -0,0 +1,28 @@ +class Index extends App.ControllerSubContent + requiredPermission: 'admin.version' + header: 'Version' + + constructor: -> + super + @load() + + # fetch data, render view + load: -> + @startLoading() + @ajax( + id: 'version' + type: 'GET' + url: "#{@apiPath}/version" + success: (data) => + @stopLoading() + @version = data.version + @render() + ) + + render: -> + + @html App.view('version')( + version: @version + ) + +App.Config.set('Version', { prio: 3800, name: 'Version', parent: '#system', target: '#system/version', controller: Index, permission: ['admin.version'] }, 'NavBarAdmin' ) diff --git a/app/assets/javascripts/app/views/generic/table.jst.eco b/app/assets/javascripts/app/views/generic/table.jst.eco index 8374ac870..d535c3e45 100644 --- a/app/assets/javascripts/app/views/generic/table.jst.eco +++ b/app/assets/javascripts/app/views/generic/table.jst.eco @@ -10,6 +10,7 @@ <%- @Icon('checkbox', 'icon-unchecked') %> <%- @Icon('checkbox-checked', 'icon-checked') %> + <%- @Icon('checkbox-indeterminate', 'icon-indeterminate') %> <% end %> diff --git a/app/assets/javascripts/app/views/version.jst.eco b/app/assets/javascripts/app/views/version.jst.eco new file mode 100644 index 000000000..93883b72c --- /dev/null +++ b/app/assets/javascripts/app/views/version.jst.eco @@ -0,0 +1,8 @@ +
+

<%- @T('Version') %>

+
+
+

+ <%- @T('This is Zammad version %s', @version) %> +

+
diff --git a/app/assets/stylesheets/svg-dimensions.css b/app/assets/stylesheets/svg-dimensions.css index 2ab6116f7..ee2e6940a 100644 --- a/app/assets/stylesheets/svg-dimensions.css +++ b/app/assets/stylesheets/svg-dimensions.css @@ -4,6 +4,7 @@ .icon-arrow-up { width: 13px; height: 7px; } .icon-chat { width: 24px; height: 24px; } .icon-checkbox-checked { width: 11px; height: 11px; } +.icon-checkbox-indeterminate { width: 11px; height: 11px; } .icon-checkbox { width: 11px; height: 11px; } .icon-checkmark { width: 16px; height: 14px; } .icon-clipboard { width: 16px; height: 16px; } diff --git a/app/assets/stylesheets/zammad.scss b/app/assets/stylesheets/zammad.scss index 96d76fddd..3a8f61042 100644 --- a/app/assets/stylesheets/zammad.scss +++ b/app/assets/stylesheets/zammad.scss @@ -132,6 +132,10 @@ ul { user-select: none; } +.u-no-userselect { + user-select: none; +} + .u-textTruncate { white-space: nowrap; overflow: hidden; @@ -1018,76 +1022,82 @@ th.align-right { align-items: center; justify-content: center; position: relative; + + .icon-checked { + color: black; + } + + .icon-unchecked { + color: hsl(60,1%,61%); + } + + .icon-indeterminate { + display: none; + color: hsl(60,1%,61%); + } + + &.is-disabled { + cursor: default; + } + + &.checkbox-replacement--fullscreen, + &.radio-replacement--fullscreen { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + + &.checkbox-replacement--inline, + &.radio-replacement--inline { + display: inline-flex; + margin-right: 3px; + } + + input { + pointer-events: none; + position: absolute; + opacity: 0; + + &:disabled ~ .icon { + opacity: 0.33; + fill: none; + cursor: default; + } + + &:not(:checked) ~ .icon-checked, + &:checked ~ .icon-unchecked { + display: none; + } + + &:focus:not(.is-active) ~ .icon-checked, + &:focus:not(.is-active) ~ .icon-unchecked { + box-shadow: 0 0 0 2px hsl(201,62%,90%); + color: hsl(200,71%,59%); + } + + &:indeterminate { + ~ .icon-checked, + ~ .icon-unchecked { + display: none; + } + ~ .icon-indeterminate { + display: block; + } + } + } + + + .label-text { + margin-left: 0; + } } -.checkbox-replacement.checkbox-replacement--fullscreen, -.radio-replacement.radio-replacement--fullscreen { - position: absolute; - left: 0; - top: 0; - width: 100%; - height: 100%; -} - - -.checkbox-replacement.checkbox-replacement--inline, -.radio-replacement.radio-replacement--inline { - display: inline-flex; - margin-right: 3px; -} - -.checkbox-replacement input, -.radio-replacement input { - pointer-events: none; - position: absolute; - opacity: 0; -} - -.checkbox-replacement .icon-checked, -.radio-replacement .icon-checked { - color: black; -} - -.checkbox-replacement .icon-unchecked, -.radio-replacement .icon-unchecked { - color: hsl(60,1%,61%); -} - -.checkbox-replacement.is-disabled, -.radio-replacement.is-disabled { - cursor: default; -} - -.checkbox-replacement input:disabled ~.icon, -.radio-replacement input:disabled ~.icon { - opacity: 0.33; - fill: none; - cursor: default; -} - -.checkbox-replacement input:not(:checked) ~ .icon-checked, -.checkbox-replacement input:checked ~ .icon-unchecked, -.radio-replacement input:not(:checked) ~ .icon-checked, -.radio-replacement input:checked ~ .icon-unchecked { - display: none; -} - -.checkbox-replacement input:focus:not(.is-active) ~ .icon-checked, -.checkbox-replacement input:focus:not(.is-active) ~ .icon-unchecked, -.radio-replacement input:focus:not(.is-active) ~ .icon-checked, -.radio-replacement input:focus:not(.is-active) ~ .icon-unchecked, { - box-shadow: 0 0 0 2px hsl(201,62%,90%); - color: hsl(200,71%,59%); -} - -.radio-replacement input:focus ~ .icon-checked, -.radio-replacement input:focus ~ .icon-unchecked { - border-radius: 100%; -} - -.checkbox-replacement + .label-text, -.radio-replacement + .label-text { - margin-left: 0; +.radio-replacement { + input:focus ~ .icon-checked, + input:focus ~ .icon-unchecked { + border-radius: 100%; + } } .table .checkbox-replacement, @@ -2639,7 +2649,8 @@ ol.tabs li { } .icon-checkbox, -.icon-checkbox-checked { +.icon-checkbox-checked, +.icon-checkbox-indeterminate { fill: white; } @@ -3836,7 +3847,7 @@ footer { .popover .two-columns, .popover .three-columns { - margin-bottom: -8px; + margin-top: -8px; } .popover .column label { @@ -3844,7 +3855,7 @@ footer { } .popover .column { - margin-bottom: 8px; + margin-top: 8px; } .popover--notifications { @@ -8360,6 +8371,7 @@ output { will-change: display; cursor: grabbing; overflow: hidden; + user-select: none; &-backdrop { @extend .fit; diff --git a/app/controllers/version_controller.rb b/app/controllers/version_controller.rb new file mode 100644 index 000000000..221715f1e --- /dev/null +++ b/app/controllers/version_controller.rb @@ -0,0 +1,13 @@ +# Copyright (C) 2012-2017 Zammad Foundation, http://zammad-foundation.org/ + +class VersionController < ApplicationController + before_action { authentication_check(permission: 'admin.version') } + + # GET /api/v1/version + def index + render json: { + version: Version.get + } + end + +end diff --git a/app/models/user.rb b/app/models/user.rb index aa0570a0f..d35ba3ede 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -32,7 +32,7 @@ class User < ApplicationModel load 'user/search_index.rb' include User::SearchIndex - before_validation :check_name, :check_email, :check_login, :check_password + before_validation :check_name, :check_email, :check_login, :ensure_password before_create :check_preferences_default, :validate_roles, :domain_based_assignment before_update :check_preferences_default, :validate_roles after_create :avatar_for_email_check @@ -906,26 +906,23 @@ returns Avatar.remove('User', id) end - def check_password - - # set old password again if not given - if password.blank? - - # get current record - if id - #current = User.find(self.id) - #self.password = current.password - self.password = password_was - end - - end - - # crypt password if not already crypted - return if !password - return if password =~ /^\{sha2\}/ - - crypted = Digest::SHA2.hexdigest(password) - self.password = "{sha2}#{crypted}" + def ensure_password + return if password_empty? + return if PasswordHash.crypted?(password) + self.password = PasswordHash.crypt(password) end + def password_empty? + # set old password again if not given + return if password.present? + + # skip if it's not desired to set a password (yet) + return true if !password + + # get current record + return if !id + + self.password = password_was + true + end end diff --git a/config/routes/version.rb b/config/routes/version.rb new file mode 100644 index 000000000..ae54d1ae1 --- /dev/null +++ b/config/routes/version.rb @@ -0,0 +1,6 @@ +Zammad::Application.routes.draw do + api_path = Rails.configuration.api_path + + match api_path + '/version', to: 'version#index', via: :get + +end diff --git a/contrib/icon-sprite.sketch b/contrib/icon-sprite.sketch index c621909d8..4d9d54570 100644 Binary files a/contrib/icon-sprite.sketch and b/contrib/icon-sprite.sketch differ diff --git a/contrib/nginx/zammad_ssl.conf b/contrib/nginx/zammad_ssl.conf new file mode 100644 index 000000000..248790af3 --- /dev/null +++ b/contrib/nginx/zammad_ssl.conf @@ -0,0 +1,143 @@ +# +# this is an example nginx config for zammad with free letsencrypt.org ssl certificates +# replace all occurrences of example.com with your domain +# when creating letsencrypt certificates the first time comment out the https parts in the config or nginx will not start +# create letsencrypt certificate by: /usr/bin/letsencrypt certonly --rsa-key-size 4096 --duplicate --text --webroot-path /var/www/html/ --webroot -d example.com -d www.example.com +# create dhparam.pem by: openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096 +# download x3 certificate by: wget -q https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem -P /etc/nginx/ssl +# you can test your ssl configuration @ https://www.ssllabs.com/ssltest/analyze.html +# + +upstream zammad { + server localhost:3000; +} + +upstream zammad-websocket { + server localhost:6042; +} + +server { + listen 80; + listen [::]:80; + + server_name example.com www.example.com; + + access_log /var/log/nginx/example.com.access.log; + error_log /var/log/nginx/example.com.error.log; + + location /.well-known/ { + root /var/www/html; + } + + location / { + rewrite ^/(.*)$ https://www.example.com/$1 permanent; + } +} + + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name example.com; + + ssl_certificate /etc/nginx/ssl/example.com-fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/example.com-privkey.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + + ssl_ciphers HIGH:!aNULL:!MD5; + + ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + ssl_prefer_server_ciphers on; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 180m; + + ssl_stapling on; + ssl_stapling_verify on; + + ssl_trusted_certificate /etc/nginx/ssl/lets-encrypt-x3-cross-signed.pem; + + resolver 8.8.8.8 8.8.4.4; + + add_header Strict-Transport-Security "max-age=31536000" always; + + access_log /var/log/nginx/example.com.access.log; + error_log /var/log/nginx/example.com.error.log; + + rewrite ^/(.*)$ https://www.example.com/$1 permanent; +} + +server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name www.example.com; + + ssl_certificate /etc/nginx/ssl/example.com-fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/example.com-privkey.pem; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2; + + ssl_ciphers HIGH:!aNULL:!MD5; + + ssl_dhparam /etc/nginx/ssl/dhparam.pem; + + ssl_prefer_server_ciphers on; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 180m; + + ssl_stapling on; + ssl_stapling_verify on; + + ssl_trusted_certificate /etc/nginx/ssl/lets-encrypt-x3-cross-signed.pem; + + resolver 8.8.8.8 8.8.4.4; + + add_header Strict-Transport-Security "max-age=31536000" always; + + location = /robots.txt { + access_log off; log_not_found off; + } + + location = /favicon.ico { + access_log off; log_not_found off; + } + + root /opt/zammad/public; + + access_log /var/log/nginx/example.com.access.log; + error_log /var/log/nginx/example.com.error.log; + + client_max_body_size 50M; + + location ~ ^/(assets/|robots.txt|humans.txt|favicon.ico) { + expires max; + } + + location /ws { + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header CLIENT_IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 86400; + proxy_pass http://zammad-websocket; + } + + location / { + proxy_set_header Host $http_host; + proxy_set_header CLIENT_IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_read_timeout 180; + proxy_pass http://zammad; + + gzip on; + gzip_types text/plain text/xml text/css image/svg+xml application/javascript application/x-javascript application/json application/xml; + gzip_proxied any; + } + +} diff --git a/db/migrate/20170126091128_application_secret_setting.rb b/db/migrate/20170126091128_application_secret_setting.rb new file mode 100644 index 000000000..6a81251eb --- /dev/null +++ b/db/migrate/20170126091128_application_secret_setting.rb @@ -0,0 +1,20 @@ +class ApplicationSecretSetting < ActiveRecord::Migration + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + Setting.create_if_not_exists( + title: 'Application secret', + name: 'application_secret', + area: 'Core', + description: 'Defines the random application secret.', + options: {}, + state: SecureRandom.hex(128), + preferences: { + permission: ['admin'], + }, + frontend: false + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index f43b3eefb..bf1b538e4 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,6 +10,18 @@ # clear old caches to start from scratch Cache.clear +Setting.create_if_not_exists( + title: 'Application secret', + name: 'application_secret', + area: 'Core', + description: 'Defines the random application secret.', + options: {}, + state: SecureRandom.hex(128), + preferences: { + permission: ['admin'], + }, + frontend: false +) Setting.create_if_not_exists( title: 'System Init Done', name: 'system_init_done', diff --git a/lib/auth/internal.rb b/lib/auth/internal.rb index 4eaf8b500..b8a26a751 100644 --- a/lib/auth/internal.rb +++ b/lib/auth/internal.rb @@ -1,21 +1,30 @@ # Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ module Auth::Internal - def self.check(username, password, _config, user) + + # rubocop:disable Style/ModuleFunction + extend self + + def check(username, password, _config, user) # return if no user exists return false if !username return false if !user - # sha auth check - if user.password =~ /^\{sha2\}/ - crypted = Digest::SHA2.hexdigest(password) - return user if user.password == "{sha2}#{crypted}" + if PasswordHash.legacy?(user.password, password) + update_password(user, password) + return user end - # plain auth check - return user if user.password == password + return false if !PasswordHash.verified?(user.password, password) - false + user + end + + private + + def update_password(user, password) + user.password = PasswordHash.crypt(password) + user.save end end diff --git a/lib/password_hash.rb b/lib/password_hash.rb new file mode 100644 index 000000000..4adae53c4 --- /dev/null +++ b/lib/password_hash.rb @@ -0,0 +1,48 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +module PasswordHash + include ApplicationLib + + # rubocop:disable Style/ModuleFunction + extend self + + def crypt(password) + argon2.create(password) + end + + def verified?(pw_hash, password) + Argon2::Password.verify_password(password, pw_hash, secret) + rescue + false + end + + def crypted?(pw_hash) + return if !pw_hash + # taken from: https://github.com/technion/ruby-argon2/blob/7e1f4a2634316e370ab84150e4f5fd91d9263713/lib/argon2.rb#L33 + return if pw_hash !~ /^\$argon2i\$.{,112}/ + true + end + + def legacy?(pw_hash, password) + return if pw_hash.blank? + return if !password + legacy_sha2?(pw_hash, password) + end + + private + + def legacy_sha2?(pw_hash, password) + return if !pw_hash.start_with?('{sha2}') + crypted = Digest::SHA2.hexdigest(password) + pw_hash == "{sha2}#{crypted}" + end + + def argon2 + return @argon2 if @argon2 + @argon2 = Argon2::Password.new(secret: secret) + end + + def secret + Setting.get('application_secret') + end +end diff --git a/lib/version.rb b/lib/version.rb new file mode 100644 index 000000000..011ba1a3d --- /dev/null +++ b/lib/version.rb @@ -0,0 +1,32 @@ +# Copyright (C) 2012-2017 Zammad Foundation, http://zammad-foundation.org/ + +class Version + +=begin + +Returns version number of application + + version = Version.get + +returns + + '1.3.0' # example + +=end + + def self.get + + begin + version = File.read("#{Rails.root}/VERSION") + version.strip! + rescue => e + message = e.to_s + Rails.logger.error "VERSION file could not be read: #{message}" + + version = '' + end + + version + end + +end diff --git a/public/assets/images/icons.svg b/public/assets/images/icons.svg index 69291a6bc..624589138 100644 --- a/public/assets/images/icons.svg +++ b/public/assets/images/icons.svg @@ -1 +1 @@ -arrow-downarrow-leftarrow-rightarrow-upchatcheckbox-checkedcheckboxcheckmarkclipboardclockcloudcogcrowndashboarddiagonal-crossdownloaddraggabledropdown-listemail-buttonemaileyedropperfacebook-buttonfacebookformgithub-buttongitlab-buttongoogle-buttongrouphelpimportantin-processinfoline-left-arrowline-right-arrowlinkedin-buttonlistloadinglock-openlocklogotypelong-arrow-rightmagnifiermarkermessageminus-smallminusmood-badmood-goodmood-okmood-superbadmood-supergoodmutenoteoauth2-buttonone-ticketorganizationoutbound-callsoverviewspackagepaperclippenpersonphoneplus-smallplusradio-checkedradioreceived-callsreloadreopeningreply-allreplyreportsearchdetailsignoutsmall-dotsplitstatus-modified-outer-circlestatusstopwatchswitchViewtask-stateteamtemplatestoolstotal-ticketstrashtwitter-buttontwitterunmuteuserwebzoom-inzoom-out \ No newline at end of file +arrow-downarrow-leftarrow-rightarrow-upchatcheckbox-checkedcheckbox-indeterminatecheckboxcheckmarkclipboardclockcloudcogcrowndashboarddiagonal-crossdownloaddraggabledropdown-listemail-buttonemaileyedropperfacebook-buttonfacebookformgithub-buttongitlab-buttongoogle-buttongrouphelpimportantin-processinfoline-left-arrowline-right-arrowlinkedin-buttonlistloadinglock-openlocklogotypelong-arrow-rightmagnifiermarkermessageminus-smallminusmood-badmood-goodmood-okmood-superbadmood-supergoodmutenoteoauth2-buttonone-ticketorganizationoutbound-callsoverviewspackagepaperclippenpersonphoneplus-smallplusradio-checkedradioreceived-callsreloadreopeningreply-allreplyreportsearchdetailsignoutsmall-dotsplitstatus-modified-outer-circlestatusstopwatchswitchViewtask-stateteamtemplatestoolstotal-ticketstrashtwitter-buttontwitterunmuteuserwebzoom-inzoom-out \ No newline at end of file diff --git a/public/assets/images/icons/checkbox-indeterminate.svg b/public/assets/images/icons/checkbox-indeterminate.svg new file mode 100644 index 000000000..a91d66d94 --- /dev/null +++ b/public/assets/images/icons/checkbox-indeterminate.svg @@ -0,0 +1,14 @@ + + + + checkbox-indeterminate + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/spec/auth/internal_spec.rb b/spec/auth/internal_spec.rb new file mode 100644 index 000000000..df50bd5cf --- /dev/null +++ b/spec/auth/internal_spec.rb @@ -0,0 +1,31 @@ +require 'rails_helper' + +RSpec.describe Auth::Internal do + + it 'authenticates via password' do + user = create(:user) + password = 'zammad' + result = described_class.check(user.login, password, {}, user) + + expect(result).to be_an_instance_of(User) + end + + it "doesn't authenticate via plain password" do + user = create(:user) + result = described_class.check(user.login, user.password, {}, user) + + expect(result).to be_falsy + end + + it 'converts legacy sha2 passwords' do + user = create(:user_legacy_password_sha2) + password = 'zammad' + + expect(PasswordHash.crypted?(user.password)).to be_falsy + + result = described_class.check(user.login, password, {}, user) + + expect(result).to be_an_instance_of(User) + expect(PasswordHash.crypted?(user.password)).to be true + end +end diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 000000000..bae6b1953 --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,24 @@ +FactoryGirl.define do + sequence :email do |n| + "nicole.braun#{n}@zammad.org" + end +end + +FactoryGirl.define do + + factory :user do + login 'nicole.braun' + firstname 'Nicole' + lastname 'Braun' + email { generate(:email) } + password 'zammad' + active true + updated_by_id 1 + created_by_id 1 + end + + factory :user_legacy_password_sha2, parent: :user do + after(:build) { |user| user.class.skip_callback(:validation, :before, :ensure_password) } + password '{sha2}dd9c764fa7ea18cd992c8600006d3dc3ac983d1ba22e9ba2d71f6207456be0ba' # zammad + end +end diff --git a/spec/password_hash_spec.rb b/spec/password_hash_spec.rb new file mode 100644 index 000000000..9c62daf68 --- /dev/null +++ b/spec/password_hash_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe PasswordHash do + + let(:pw_plain) { 'zammad' } + + context 'stable API' do + it 'responds to crypt' do + expect(described_class).to respond_to(:crypt) + end + + it 'responds to verified?' do + expect(described_class).to respond_to(:verified?) + end + + it 'responds to crypted?' do + expect(described_class).to respond_to(:crypted?) + end + + it 'responds to legacy?' do + expect(described_class).to respond_to(:legacy?) + end + end + + context 'encryption' do + + it 'crypts passwords' do + pw_crypted = described_class.crypt(pw_plain) + expect(pw_crypted).not_to eq(pw_plain) + end + + it 'verifies crypted passwords' do + pw_crypted = described_class.crypt(pw_plain) + expect(described_class.verified?(pw_crypted, pw_plain)).to be true + end + + it 'detects crypted passwords' do + pw_crypted = described_class.crypt(pw_plain) + expect(described_class.crypted?(pw_crypted)).to be true + end + end + + context 'legacy' do + + let(:zammad_sha2) { '{sha2}dd9c764fa7ea18cd992c8600006d3dc3ac983d1ba22e9ba2d71f6207456be0ba' } + + it 'requires hash to be not blank' do + expect(described_class.legacy?(nil, pw_plain)).to be_falsy + expect(described_class.legacy?('', pw_plain)).to be_falsy + end + + it 'requires password to be not nil' do + expect(described_class.legacy?(zammad_sha2, nil)).to be_falsy + end + + it 'detects sha2 hashes' do + expect(described_class.legacy?(zammad_sha2, pw_plain)).to be true + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index fc688e11d..9d8b4dd45 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -6,6 +6,7 @@ require File.expand_path('../../config/environment', __FILE__) abort('The Rails environment is running in production mode!') if Rails.env.production? require 'spec_helper' require 'rspec/rails' +require 'support/factory_girl' # Add additional requires below this line. Rails is not loaded until this point! # Requires supporting ruby files with custom matchers and macros, etc, in diff --git a/spec/support/factory_girl.rb b/spec/support/factory_girl.rb new file mode 100644 index 000000000..eec437fb3 --- /dev/null +++ b/spec/support/factory_girl.rb @@ -0,0 +1,3 @@ +RSpec.configure do |config| + config.include FactoryGirl::Syntax::Methods +end