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('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 @@
-
\ No newline at end of file
+
\ 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 @@
+
+
\ 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