##// END OF EJS Templates
Adds support for SCM/LDAP passwords encryption in the database (#7411)....
Jean-Philippe Lang -
r4830:a78d5659593d
parent child
Show More
@@ -0,0 +1,9
1 class ChangeRepositoriesPasswordLimit < ActiveRecord::Migration
2 def self.up
3 change_column :repositories, :password, :string, :limit => nil, :default => ''
4 end
5
6 def self.down
7 change_column :repositories, :password, :string, :limit => 60, :default => ''
8 end
9 end
@@ -0,0 +1,9
1 class ChangeAuthSourcesAccountPasswordLimit < ActiveRecord::Migration
2 def self.up
3 change_column :auth_sources, :account_password, :string, :limit => nil, :default => ''
4 end
5
6 def self.down
7 change_column :auth_sources, :account_password, :string, :limit => 60, :default => ''
8 end
9 end
@@ -0,0 +1,95
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 module Redmine
19 module Ciphering
20 def self.included(base)
21 base.extend ClassMethods
22 end
23
24 class << self
25 def encrypt_text(text)
26 if cipher_key.blank?
27 text
28 else
29 c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
30 iv = c.random_iv
31 c.encrypt
32 c.key = cipher_key
33 c.iv = iv
34 e = c.update(text.to_s)
35 e << c.final
36 "aes-256-cbc:" + [e, iv].map {|v| Base64.encode64(v).strip}.join('--')
37 end
38 end
39
40 def decrypt_text(text)
41 if text && match = text.match(/\Aaes-256-cbc:(.+)\Z/)
42 text = match[1]
43 c = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
44 e, iv = text.split("--").map {|s| Base64.decode64(s)}
45 c.decrypt
46 c.key = cipher_key
47 c.iv = iv
48 d = c.update(e)
49 d << c.final
50 else
51 text
52 end
53 end
54
55 def cipher_key
56 key = Redmine::Configuration['database_cipher_key'].to_s
57 key.blank? ? nil : Digest::SHA256.hexdigest(key)
58 end
59 end
60
61 module ClassMethods
62 def encrypt_all(attribute)
63 transaction do
64 all.each do |object|
65 clear = object.send(attribute)
66 object.send "#{attribute}=", clear
67 raise(ActiveRecord::Rollback) unless object.save(false)
68 end
69 end ? true : false
70 end
71
72 def decrypt_all(attribute)
73 transaction do
74 all.each do |object|
75 clear = object.send(attribute)
76 object.write_attribute attribute, clear
77 raise(ActiveRecord::Rollback) unless object.save(false)
78 end
79 end
80 end ? true : false
81 end
82
83 private
84
85 # Returns the value of the given ciphered attribute
86 def read_ciphered_attribute(attribute)
87 Redmine::Ciphering.decrypt_text(read_attribute(attribute))
88 end
89
90 # Sets the value of the given ciphered attribute
91 def write_ciphered_attribute(attribute, value)
92 write_attribute(attribute, Redmine::Ciphering.encrypt_text(value))
93 end
94 end
95 end
@@ -0,0 +1,35
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18
19 namespace :db do
20 desc 'Encrypts SCM and LDAP passwords in the database.'
21 task :encrypt => :environment do
22 unless (Repository.encrypt_all(:password) &&
23 AuthSource.encrypt_all(:account_password))
24 raise "Some objects could not be saved after encryption, update was rollback'ed."
25 end
26 end
27
28 desc 'Decrypts SCM and LDAP passwords in the database.'
29 task :decrypt => :environment do
30 unless (Repository.decrypt_all(:password) &&
31 AuthSource.decrypt_all(:account_password))
32 raise "Some objects could not be saved after decryption, update was rollback'ed."
33 end
34 end
35 end
@@ -0,0 +1,84
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../../../test_helper', __FILE__)
19
20 class Redmine::CipheringTest < ActiveSupport::TestCase
21
22 def test_password_should_be_encrypted
23 Redmine::Configuration.with 'database_cipher_key' => 'secret' do
24 r = Repository::Subversion.generate!(:password => 'foo')
25 assert_equal 'foo', r.password
26 assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/)
27 end
28 end
29
30 def test_password_should_be_clear_with_blank_key
31 Redmine::Configuration.with 'database_cipher_key' => '' do
32 r = Repository::Subversion.generate!(:password => 'foo')
33 assert_equal 'foo', r.password
34 assert_equal 'foo', r.read_attribute(:password)
35 end
36 end
37
38 def test_password_should_be_clear_with_nil_key
39 Redmine::Configuration.with 'database_cipher_key' => nil do
40 r = Repository::Subversion.generate!(:password => 'foo')
41 assert_equal 'foo', r.password
42 assert_equal 'foo', r.read_attribute(:password)
43 end
44 end
45
46 def test_unciphered_password_should_be_readable
47 Redmine::Configuration.with 'database_cipher_key' => nil do
48 r = Repository::Subversion.generate!(:password => 'clear')
49 end
50
51 Redmine::Configuration.with 'database_cipher_key' => 'secret' do
52 r = Repository.first(:order => 'id DESC')
53 assert_equal 'clear', r.password
54 end
55 end
56
57 def test_encrypt_all
58 Repository.delete_all
59 Redmine::Configuration.with 'database_cipher_key' => nil do
60 Repository::Subversion.generate!(:password => 'foo')
61 Repository::Subversion.generate!(:password => 'bar')
62 end
63
64 Redmine::Configuration.with 'database_cipher_key' => 'secret' do
65 assert Repository.encrypt_all(:password)
66 r = Repository.first(:order => 'id DESC')
67 assert_equal 'bar', r.password
68 assert r.read_attribute(:password).match(/\Aaes-256-cbc:.+\Z/)
69 end
70 end
71
72 def test_decrypt_all
73 Repository.delete_all
74 Redmine::Configuration.with 'database_cipher_key' => 'secret' do
75 Repository::Subversion.generate!(:password => 'foo')
76 Repository::Subversion.generate!(:password => 'bar')
77
78 assert Repository.decrypt_all(:password)
79 r = Repository.first(:order => 'id DESC')
80 assert_equal 'bar', r.password
81 assert_equal 'bar', r.read_attribute(:password)
82 end
83 end
84 end
@@ -1,58 +1,68
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class AuthSource < ActiveRecord::Base
18 class AuthSource < ActiveRecord::Base
19 include Redmine::Ciphering
20
19 has_many :users
21 has_many :users
20
22
21 validates_presence_of :name
23 validates_presence_of :name
22 validates_uniqueness_of :name
24 validates_uniqueness_of :name
23 validates_length_of :name, :maximum => 60
25 validates_length_of :name, :maximum => 60
24
26
25 def authenticate(login, password)
27 def authenticate(login, password)
26 end
28 end
27
29
28 def test_connection
30 def test_connection
29 end
31 end
30
32
31 def auth_method_name
33 def auth_method_name
32 "Abstract"
34 "Abstract"
33 end
35 end
34
36
37 def account_password
38 read_ciphered_attribute(:account_password)
39 end
40
41 def account_password=(arg)
42 write_ciphered_attribute(:account_password, arg)
43 end
44
35 def allow_password_changes?
45 def allow_password_changes?
36 self.class.allow_password_changes?
46 self.class.allow_password_changes?
37 end
47 end
38
48
39 # Does this auth source backend allow password changes?
49 # Does this auth source backend allow password changes?
40 def self.allow_password_changes?
50 def self.allow_password_changes?
41 false
51 false
42 end
52 end
43
53
44 # Try to authenticate a user not yet registered against available sources
54 # Try to authenticate a user not yet registered against available sources
45 def self.authenticate(login, password)
55 def self.authenticate(login, password)
46 AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
56 AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
47 begin
57 begin
48 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
58 logger.debug "Authenticating '#{login}' against '#{source.name}'" if logger && logger.debug?
49 attrs = source.authenticate(login, password)
59 attrs = source.authenticate(login, password)
50 rescue => e
60 rescue => e
51 logger.error "Error during authentication: #{e.message}"
61 logger.error "Error during authentication: #{e.message}"
52 attrs = nil
62 attrs = nil
53 end
63 end
54 return attrs if attrs
64 return attrs if attrs
55 end
65 end
56 return nil
66 return nil
57 end
67 end
58 end
68 end
@@ -1,130 +1,130
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require 'net/ldap'
18 require 'net/ldap'
19 require 'iconv'
19 require 'iconv'
20
20
21 class AuthSourceLdap < AuthSource
21 class AuthSourceLdap < AuthSource
22 validates_presence_of :host, :port, :attr_login
22 validates_presence_of :host, :port, :attr_login
23 validates_length_of :name, :host, :account_password, :maximum => 60, :allow_nil => true
23 validates_length_of :name, :host, :maximum => 60, :allow_nil => true
24 validates_length_of :account, :base_dn, :maximum => 255, :allow_nil => true
24 validates_length_of :account, :account_password, :base_dn, :maximum => 255, :allow_nil => true
25 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
25 validates_length_of :attr_login, :attr_firstname, :attr_lastname, :attr_mail, :maximum => 30, :allow_nil => true
26 validates_numericality_of :port, :only_integer => true
26 validates_numericality_of :port, :only_integer => true
27
27
28 before_validation :strip_ldap_attributes
28 before_validation :strip_ldap_attributes
29
29
30 def after_initialize
30 def after_initialize
31 self.port = 389 if self.port == 0
31 self.port = 389 if self.port == 0
32 end
32 end
33
33
34 def authenticate(login, password)
34 def authenticate(login, password)
35 return nil if login.blank? || password.blank?
35 return nil if login.blank? || password.blank?
36 attrs = get_user_dn(login)
36 attrs = get_user_dn(login)
37
37
38 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
38 if attrs && attrs[:dn] && authenticate_dn(attrs[:dn], password)
39 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
39 logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
40 return attrs.except(:dn)
40 return attrs.except(:dn)
41 end
41 end
42 rescue Net::LDAP::LdapError => text
42 rescue Net::LDAP::LdapError => text
43 raise "LdapError: " + text
43 raise "LdapError: " + text
44 end
44 end
45
45
46 # test the connection to the LDAP
46 # test the connection to the LDAP
47 def test_connection
47 def test_connection
48 ldap_con = initialize_ldap_con(self.account, self.account_password)
48 ldap_con = initialize_ldap_con(self.account, self.account_password)
49 ldap_con.open { }
49 ldap_con.open { }
50 rescue Net::LDAP::LdapError => text
50 rescue Net::LDAP::LdapError => text
51 raise "LdapError: " + text
51 raise "LdapError: " + text
52 end
52 end
53
53
54 def auth_method_name
54 def auth_method_name
55 "LDAP"
55 "LDAP"
56 end
56 end
57
57
58 private
58 private
59
59
60 def strip_ldap_attributes
60 def strip_ldap_attributes
61 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
61 [:attr_login, :attr_firstname, :attr_lastname, :attr_mail].each do |attr|
62 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
62 write_attribute(attr, read_attribute(attr).strip) unless read_attribute(attr).nil?
63 end
63 end
64 end
64 end
65
65
66 def initialize_ldap_con(ldap_user, ldap_password)
66 def initialize_ldap_con(ldap_user, ldap_password)
67 options = { :host => self.host,
67 options = { :host => self.host,
68 :port => self.port,
68 :port => self.port,
69 :encryption => (self.tls ? :simple_tls : nil)
69 :encryption => (self.tls ? :simple_tls : nil)
70 }
70 }
71 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
71 options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
72 Net::LDAP.new options
72 Net::LDAP.new options
73 end
73 end
74
74
75 def get_user_attributes_from_ldap_entry(entry)
75 def get_user_attributes_from_ldap_entry(entry)
76 {
76 {
77 :dn => entry.dn,
77 :dn => entry.dn,
78 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
78 :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
79 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
79 :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
80 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
80 :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
81 :auth_source_id => self.id
81 :auth_source_id => self.id
82 }
82 }
83 end
83 end
84
84
85 # Return the attributes needed for the LDAP search. It will only
85 # Return the attributes needed for the LDAP search. It will only
86 # include the user attributes if on-the-fly registration is enabled
86 # include the user attributes if on-the-fly registration is enabled
87 def search_attributes
87 def search_attributes
88 if onthefly_register?
88 if onthefly_register?
89 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
89 ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
90 else
90 else
91 ['dn']
91 ['dn']
92 end
92 end
93 end
93 end
94
94
95 # Check if a DN (user record) authenticates with the password
95 # Check if a DN (user record) authenticates with the password
96 def authenticate_dn(dn, password)
96 def authenticate_dn(dn, password)
97 if dn.present? && password.present?
97 if dn.present? && password.present?
98 initialize_ldap_con(dn, password).bind
98 initialize_ldap_con(dn, password).bind
99 end
99 end
100 end
100 end
101
101
102 # Get the user's dn and any attributes for them, given their login
102 # Get the user's dn and any attributes for them, given their login
103 def get_user_dn(login)
103 def get_user_dn(login)
104 ldap_con = initialize_ldap_con(self.account, self.account_password)
104 ldap_con = initialize_ldap_con(self.account, self.account_password)
105 login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
105 login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
106 object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
106 object_filter = Net::LDAP::Filter.eq( "objectClass", "*" )
107 attrs = {}
107 attrs = {}
108
108
109 ldap_con.search( :base => self.base_dn,
109 ldap_con.search( :base => self.base_dn,
110 :filter => object_filter & login_filter,
110 :filter => object_filter & login_filter,
111 :attributes=> search_attributes) do |entry|
111 :attributes=> search_attributes) do |entry|
112
112
113 if onthefly_register?
113 if onthefly_register?
114 attrs = get_user_attributes_from_ldap_entry(entry)
114 attrs = get_user_attributes_from_ldap_entry(entry)
115 else
115 else
116 attrs = {:dn => entry.dn}
116 attrs = {:dn => entry.dn}
117 end
117 end
118
118
119 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
119 logger.debug "DN found for #{login}: #{attrs[:dn]}" if logger && logger.debug?
120 end
120 end
121
121
122 attrs
122 attrs
123 end
123 end
124
124
125 def self.get_attr(entry, attr_name)
125 def self.get_attr(entry, attr_name)
126 if !attr_name.blank?
126 if !attr_name.blank?
127 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
127 entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
128 end
128 end
129 end
129 end
130 end
130 end
@@ -1,266 +1,277
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Repository < ActiveRecord::Base
18 class Repository < ActiveRecord::Base
19 include Redmine::Ciphering
20
19 belongs_to :project
21 belongs_to :project
20 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
22 has_many :changesets, :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC"
21 has_many :changes, :through => :changesets
23 has_many :changes, :through => :changesets
22
24
23 # Raw SQL to delete changesets and changes in the database
25 # Raw SQL to delete changesets and changes in the database
24 # has_many :changesets, :dependent => :destroy is too slow for big repositories
26 # has_many :changesets, :dependent => :destroy is too slow for big repositories
25 before_destroy :clear_changesets
27 before_destroy :clear_changesets
26
28
29 validates_length_of :password, :maximum => 255, :allow_nil => true
27 # Checks if the SCM is enabled when creating a repository
30 # Checks if the SCM is enabled when creating a repository
28 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
31 validate_on_create { |r| r.errors.add(:type, :invalid) unless Setting.enabled_scm.include?(r.class.name.demodulize) }
29
32
30 # Removes leading and trailing whitespace
33 # Removes leading and trailing whitespace
31 def url=(arg)
34 def url=(arg)
32 write_attribute(:url, arg ? arg.to_s.strip : nil)
35 write_attribute(:url, arg ? arg.to_s.strip : nil)
33 end
36 end
34
37
35 # Removes leading and trailing whitespace
38 # Removes leading and trailing whitespace
36 def root_url=(arg)
39 def root_url=(arg)
37 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
40 write_attribute(:root_url, arg ? arg.to_s.strip : nil)
38 end
41 end
39
42
43 def password
44 read_ciphered_attribute(:password)
45 end
46
47 def password=(arg)
48 write_ciphered_attribute(:password, arg)
49 end
50
40 def scm_adapter
51 def scm_adapter
41 self.class.scm_adapter_class
52 self.class.scm_adapter_class
42 end
53 end
43
54
44 def scm
55 def scm
45 @scm ||= self.scm_adapter.new(url, root_url,
56 @scm ||= self.scm_adapter.new(url, root_url,
46 login, password, path_encoding)
57 login, password, path_encoding)
47 update_attribute(:root_url, @scm.root_url) if root_url.blank?
58 update_attribute(:root_url, @scm.root_url) if root_url.blank?
48 @scm
59 @scm
49 end
60 end
50
61
51 def scm_name
62 def scm_name
52 self.class.scm_name
63 self.class.scm_name
53 end
64 end
54
65
55 def supports_cat?
66 def supports_cat?
56 scm.supports_cat?
67 scm.supports_cat?
57 end
68 end
58
69
59 def supports_annotate?
70 def supports_annotate?
60 scm.supports_annotate?
71 scm.supports_annotate?
61 end
72 end
62
73
63 def entry(path=nil, identifier=nil)
74 def entry(path=nil, identifier=nil)
64 scm.entry(path, identifier)
75 scm.entry(path, identifier)
65 end
76 end
66
77
67 def entries(path=nil, identifier=nil)
78 def entries(path=nil, identifier=nil)
68 scm.entries(path, identifier)
79 scm.entries(path, identifier)
69 end
80 end
70
81
71 def branches
82 def branches
72 scm.branches
83 scm.branches
73 end
84 end
74
85
75 def tags
86 def tags
76 scm.tags
87 scm.tags
77 end
88 end
78
89
79 def default_branch
90 def default_branch
80 scm.default_branch
91 scm.default_branch
81 end
92 end
82
93
83 def properties(path, identifier=nil)
94 def properties(path, identifier=nil)
84 scm.properties(path, identifier)
95 scm.properties(path, identifier)
85 end
96 end
86
97
87 def cat(path, identifier=nil)
98 def cat(path, identifier=nil)
88 scm.cat(path, identifier)
99 scm.cat(path, identifier)
89 end
100 end
90
101
91 def diff(path, rev, rev_to)
102 def diff(path, rev, rev_to)
92 scm.diff(path, rev, rev_to)
103 scm.diff(path, rev, rev_to)
93 end
104 end
94
105
95 def diff_format_revisions(cs, cs_to, sep=':')
106 def diff_format_revisions(cs, cs_to, sep=':')
96 text = ""
107 text = ""
97 text << cs_to.format_identifier + sep if cs_to
108 text << cs_to.format_identifier + sep if cs_to
98 text << cs.format_identifier if cs
109 text << cs.format_identifier if cs
99 text
110 text
100 end
111 end
101
112
102 # Returns a path relative to the url of the repository
113 # Returns a path relative to the url of the repository
103 def relative_path(path)
114 def relative_path(path)
104 path
115 path
105 end
116 end
106
117
107 # Finds and returns a revision with a number or the beginning of a hash
118 # Finds and returns a revision with a number or the beginning of a hash
108 def find_changeset_by_name(name)
119 def find_changeset_by_name(name)
109 return nil if name.blank?
120 return nil if name.blank?
110 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
121 changesets.find(:first, :conditions => (name.match(/^\d*$/) ? ["revision = ?", name.to_s] : ["revision LIKE ?", name + '%']))
111 end
122 end
112
123
113 def latest_changeset
124 def latest_changeset
114 @latest_changeset ||= changesets.find(:first)
125 @latest_changeset ||= changesets.find(:first)
115 end
126 end
116
127
117 # Returns the latest changesets for +path+
128 # Returns the latest changesets for +path+
118 # Default behaviour is to search in cached changesets
129 # Default behaviour is to search in cached changesets
119 def latest_changesets(path, rev, limit=10)
130 def latest_changesets(path, rev, limit=10)
120 if path.blank?
131 if path.blank?
121 changesets.find(:all, :include => :user,
132 changesets.find(:all, :include => :user,
122 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
133 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
123 :limit => limit)
134 :limit => limit)
124 else
135 else
125 changes.find(:all, :include => {:changeset => :user},
136 changes.find(:all, :include => {:changeset => :user},
126 :conditions => ["path = ?", path.with_leading_slash],
137 :conditions => ["path = ?", path.with_leading_slash],
127 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
138 :order => "#{Changeset.table_name}.committed_on DESC, #{Changeset.table_name}.id DESC",
128 :limit => limit).collect(&:changeset)
139 :limit => limit).collect(&:changeset)
129 end
140 end
130 end
141 end
131
142
132 def scan_changesets_for_issue_ids
143 def scan_changesets_for_issue_ids
133 self.changesets.each(&:scan_comment_for_issue_ids)
144 self.changesets.each(&:scan_comment_for_issue_ids)
134 end
145 end
135
146
136 # Returns an array of committers usernames and associated user_id
147 # Returns an array of committers usernames and associated user_id
137 def committers
148 def committers
138 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
149 @committers ||= Changeset.connection.select_rows("SELECT DISTINCT committer, user_id FROM #{Changeset.table_name} WHERE repository_id = #{id}")
139 end
150 end
140
151
141 # Maps committers username to a user ids
152 # Maps committers username to a user ids
142 def committer_ids=(h)
153 def committer_ids=(h)
143 if h.is_a?(Hash)
154 if h.is_a?(Hash)
144 committers.each do |committer, user_id|
155 committers.each do |committer, user_id|
145 new_user_id = h[committer]
156 new_user_id = h[committer]
146 if new_user_id && (new_user_id.to_i != user_id.to_i)
157 if new_user_id && (new_user_id.to_i != user_id.to_i)
147 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
158 new_user_id = (new_user_id.to_i > 0 ? new_user_id.to_i : nil)
148 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
159 Changeset.update_all("user_id = #{ new_user_id.nil? ? 'NULL' : new_user_id }", ["repository_id = ? AND committer = ?", id, committer])
149 end
160 end
150 end
161 end
151 @committers = nil
162 @committers = nil
152 @found_committer_users = nil
163 @found_committer_users = nil
153 true
164 true
154 else
165 else
155 false
166 false
156 end
167 end
157 end
168 end
158
169
159 # Returns the Redmine User corresponding to the given +committer+
170 # Returns the Redmine User corresponding to the given +committer+
160 # It will return nil if the committer is not yet mapped and if no User
171 # It will return nil if the committer is not yet mapped and if no User
161 # with the same username or email was found
172 # with the same username or email was found
162 def find_committer_user(committer)
173 def find_committer_user(committer)
163 unless committer.blank?
174 unless committer.blank?
164 @found_committer_users ||= {}
175 @found_committer_users ||= {}
165 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
176 return @found_committer_users[committer] if @found_committer_users.has_key?(committer)
166
177
167 user = nil
178 user = nil
168 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
179 c = changesets.find(:first, :conditions => {:committer => committer}, :include => :user)
169 if c && c.user
180 if c && c.user
170 user = c.user
181 user = c.user
171 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
182 elsif committer.strip =~ /^([^<]+)(<(.*)>)?$/
172 username, email = $1.strip, $3
183 username, email = $1.strip, $3
173 u = User.find_by_login(username)
184 u = User.find_by_login(username)
174 u ||= User.find_by_mail(email) unless email.blank?
185 u ||= User.find_by_mail(email) unless email.blank?
175 user = u
186 user = u
176 end
187 end
177 @found_committer_users[committer] = user
188 @found_committer_users[committer] = user
178 user
189 user
179 end
190 end
180 end
191 end
181
192
182 # Fetches new changesets for all repositories of active projects
193 # Fetches new changesets for all repositories of active projects
183 # Can be called periodically by an external script
194 # Can be called periodically by an external script
184 # eg. ruby script/runner "Repository.fetch_changesets"
195 # eg. ruby script/runner "Repository.fetch_changesets"
185 def self.fetch_changesets
196 def self.fetch_changesets
186 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
197 Project.active.has_module(:repository).find(:all, :include => :repository).each do |project|
187 if project.repository
198 if project.repository
188 begin
199 begin
189 project.repository.fetch_changesets
200 project.repository.fetch_changesets
190 rescue Redmine::Scm::Adapters::CommandFailed => e
201 rescue Redmine::Scm::Adapters::CommandFailed => e
191 logger.error "scm: error during fetching changesets: #{e.message}"
202 logger.error "scm: error during fetching changesets: #{e.message}"
192 end
203 end
193 end
204 end
194 end
205 end
195 end
206 end
196
207
197 # scan changeset comments to find related and fixed issues for all repositories
208 # scan changeset comments to find related and fixed issues for all repositories
198 def self.scan_changesets_for_issue_ids
209 def self.scan_changesets_for_issue_ids
199 find(:all).each(&:scan_changesets_for_issue_ids)
210 find(:all).each(&:scan_changesets_for_issue_ids)
200 end
211 end
201
212
202 def self.scm_name
213 def self.scm_name
203 'Abstract'
214 'Abstract'
204 end
215 end
205
216
206 def self.available_scm
217 def self.available_scm
207 subclasses.collect {|klass| [klass.scm_name, klass.name]}
218 subclasses.collect {|klass| [klass.scm_name, klass.name]}
208 end
219 end
209
220
210 def self.factory(klass_name, *args)
221 def self.factory(klass_name, *args)
211 klass = "Repository::#{klass_name}".constantize
222 klass = "Repository::#{klass_name}".constantize
212 klass.new(*args)
223 klass.new(*args)
213 rescue
224 rescue
214 nil
225 nil
215 end
226 end
216
227
217 def self.scm_adapter_class
228 def self.scm_adapter_class
218 nil
229 nil
219 end
230 end
220
231
221 def self.scm_command
232 def self.scm_command
222 ret = ""
233 ret = ""
223 begin
234 begin
224 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
235 ret = self.scm_adapter_class.client_command if self.scm_adapter_class
225 rescue Redmine::Scm::Adapters::CommandFailed => e
236 rescue Redmine::Scm::Adapters::CommandFailed => e
226 logger.error "scm: error during get command: #{e.message}"
237 logger.error "scm: error during get command: #{e.message}"
227 end
238 end
228 ret
239 ret
229 end
240 end
230
241
231 def self.scm_version_string
242 def self.scm_version_string
232 ret = ""
243 ret = ""
233 begin
244 begin
234 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
245 ret = self.scm_adapter_class.client_version_string if self.scm_adapter_class
235 rescue Redmine::Scm::Adapters::CommandFailed => e
246 rescue Redmine::Scm::Adapters::CommandFailed => e
236 logger.error "scm: error during get version string: #{e.message}"
247 logger.error "scm: error during get version string: #{e.message}"
237 end
248 end
238 ret
249 ret
239 end
250 end
240
251
241 def self.scm_available
252 def self.scm_available
242 ret = false
253 ret = false
243 begin
254 begin
244 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
255 ret = self.scm_adapter_class.client_available if self.scm_adapter_class
245 rescue Redmine::Scm::Adapters::CommandFailed => e
256 rescue Redmine::Scm::Adapters::CommandFailed => e
246 logger.error "scm: error during get scm available: #{e.message}"
257 logger.error "scm: error during get scm available: #{e.message}"
247 end
258 end
248 ret
259 ret
249 end
260 end
250
261
251 private
262 private
252
263
253 def before_save
264 def before_save
254 # Strips url and root_url
265 # Strips url and root_url
255 url.strip!
266 url.strip!
256 root_url.strip!
267 root_url.strip!
257 true
268 true
258 end
269 end
259
270
260 def clear_changesets
271 def clear_changesets
261 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
272 cs, ch, ci = Changeset.table_name, Change.table_name, "#{table_name_prefix}changesets_issues#{table_name_suffix}"
262 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
273 connection.delete("DELETE FROM #{ch} WHERE #{ch}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
263 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
274 connection.delete("DELETE FROM #{ci} WHERE #{ci}.changeset_id IN (SELECT #{cs}.id FROM #{cs} WHERE #{cs}.repository_id = #{id})")
264 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
275 connection.delete("DELETE FROM #{cs} WHERE #{cs}.repository_id = #{id}")
265 end
276 end
266 end
277 end
@@ -1,133 +1,147
1 # = Redmine configuration file
1 # = Redmine configuration file
2 #
2 #
3 # Each environment has it's own configuration options. If you are only
3 # Each environment has it's own configuration options. If you are only
4 # running in production, only the production block needs to be configured.
4 # running in production, only the production block needs to be configured.
5 # Environment specific configuration options override the default ones.
5 # Environment specific configuration options override the default ones.
6 #
6 #
7 # Note that this file needs to be a valid YAML file.
7 # Note that this file needs to be a valid YAML file.
8 #
8 #
9 # == Outgoing email settings (email_delivery setting)
9 # == Outgoing email settings (email_delivery setting)
10 #
10 #
11 # === Common configurations
11 # === Common configurations
12 #
12 #
13 # ==== Sendmail command
13 # ==== Sendmail command
14 #
14 #
15 # production:
15 # production:
16 # email_delivery:
16 # email_delivery:
17 # delivery_method: :sendmail
17 # delivery_method: :sendmail
18 #
18 #
19 # ==== Simple SMTP server at localhost
19 # ==== Simple SMTP server at localhost
20 #
20 #
21 # production:
21 # production:
22 # email_delivery:
22 # email_delivery:
23 # delivery_method: :smtp
23 # delivery_method: :smtp
24 # smtp_settings:
24 # smtp_settings:
25 # address: "localhost"
25 # address: "localhost"
26 # port: 25
26 # port: 25
27 #
27 #
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
28 # ==== SMTP server at example.com using LOGIN authentication and checking HELO for foo.com
29 #
29 #
30 # production:
30 # production:
31 # email_delivery:
31 # email_delivery:
32 # delivery_method: :smtp
32 # delivery_method: :smtp
33 # smtp_settings:
33 # smtp_settings:
34 # address: "example.com"
34 # address: "example.com"
35 # port: 25
35 # port: 25
36 # authentication: :login
36 # authentication: :login
37 # domain: 'foo.com'
37 # domain: 'foo.com'
38 # user_name: 'myaccount'
38 # user_name: 'myaccount'
39 # password: 'password'
39 # password: 'password'
40 #
40 #
41 # ==== SMTP server at example.com using PLAIN authentication
41 # ==== SMTP server at example.com using PLAIN authentication
42 #
42 #
43 # production:
43 # production:
44 # email_delivery:
44 # email_delivery:
45 # delivery_method: :smtp
45 # delivery_method: :smtp
46 # smtp_settings:
46 # smtp_settings:
47 # address: "example.com"
47 # address: "example.com"
48 # port: 25
48 # port: 25
49 # authentication: :plain
49 # authentication: :plain
50 # domain: 'example.com'
50 # domain: 'example.com'
51 # user_name: 'myaccount'
51 # user_name: 'myaccount'
52 # password: 'password'
52 # password: 'password'
53 #
53 #
54 # ==== SMTP server at using TLS (GMail)
54 # ==== SMTP server at using TLS (GMail)
55 #
55 #
56 # This requires some additional configuration. See the article at:
56 # This requires some additional configuration. See the article at:
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
57 # http://redmineblog.com/articles/setup-redmine-to-send-email-using-gmail/
58 #
58 #
59 # production:
59 # production:
60 # email_delivery:
60 # email_delivery:
61 # delivery_method: :smtp
61 # delivery_method: :smtp
62 # smtp_settings:
62 # smtp_settings:
63 # tls: true
63 # tls: true
64 # address: "smtp.gmail.com"
64 # address: "smtp.gmail.com"
65 # port: 587
65 # port: 587
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
66 # domain: "smtp.gmail.com" # 'your.domain.com' for GoogleApps
67 # authentication: :plain
67 # authentication: :plain
68 # user_name: "your_email@gmail.com"
68 # user_name: "your_email@gmail.com"
69 # password: "your_password"
69 # password: "your_password"
70 #
70 #
71 #
71 #
72 # === More configuration options
72 # === More configuration options
73 #
73 #
74 # See the "Configuration options" at the following website for a list of the
74 # See the "Configuration options" at the following website for a list of the
75 # full options allowed:
75 # full options allowed:
76 #
76 #
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
77 # http://wiki.rubyonrails.org/rails/pages/HowToSendEmailsWithActionMailer
78
78
79
79
80 # default configuration options for all environments
80 # default configuration options for all environments
81 default:
81 default:
82 # Outgoing emails configuration (see examples above)
82 # Outgoing emails configuration (see examples above)
83 email_delivery:
83 email_delivery:
84 delivery_method: :smtp
84 delivery_method: :smtp
85 smtp_settings:
85 smtp_settings:
86 address: smtp.example.net
86 address: smtp.example.net
87 port: 25
87 port: 25
88 domain: example.net
88 domain: example.net
89 authentication: :login
89 authentication: :login
90 user_name: "redmine@example.net"
90 user_name: "redmine@example.net"
91 password: "redmine"
91 password: "redmine"
92
92
93 # Absolute path to the directory where attachments are stored.
93 # Absolute path to the directory where attachments are stored.
94 # The default is the 'files' directory in your Redmine instance.
94 # The default is the 'files' directory in your Redmine instance.
95 # Your Redmine instance needs to have write permission on this
95 # Your Redmine instance needs to have write permission on this
96 # directory.
96 # directory.
97 # Examples:
97 # Examples:
98 # attachments_storage_path: /var/redmine/files
98 # attachments_storage_path: /var/redmine/files
99 # attachments_storage_path: D:/redmine/files
99 # attachments_storage_path: D:/redmine/files
100 attachments_storage_path:
100 attachments_storage_path:
101
101
102 # Configuration of the autologin cookie.
102 # Configuration of the autologin cookie.
103 # autologin_cookie_name: the name of the cookie (default: autologin)
103 # autologin_cookie_name: the name of the cookie (default: autologin)
104 # autologin_cookie_path: the cookie path (default: /)
104 # autologin_cookie_path: the cookie path (default: /)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
105 # autologin_cookie_secure: true sets the cookie secure flag (default: false)
106 autologin_cookie_name:
106 autologin_cookie_name:
107 autologin_cookie_path:
107 autologin_cookie_path:
108 autologin_cookie_secure:
108 autologin_cookie_secure:
109
109
110 # Configuration of SCM executable command.
110 # Configuration of SCM executable command.
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
111 # Absolute path (e.g. /usr/local/bin/hg) or command name (e.g. hg.exe, bzr.exe)
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
112 # On Windows, *.cmd, *.bat (e.g. hg.cmd, bzr.bat) does not work.
113 # Examples:
113 # Examples:
114 # scm_subversion_command: svn # (default: svn)
114 # scm_subversion_command: svn # (default: svn)
115 # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg)
115 # scm_mercurial_command: C:\Program Files\TortoiseHg\hg.exe # (default: hg)
116 # scm_git_command: /usr/local/bin/git # (default: git)
116 # scm_git_command: /usr/local/bin/git # (default: git)
117 # scm_cvs_command: cvs # (default: cvs)
117 # scm_cvs_command: cvs # (default: cvs)
118 # scm_bazaar_command: bzr.exe # (default: bzr)
118 # scm_bazaar_command: bzr.exe # (default: bzr)
119 # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
119 # scm_darcs_command: darcs-1.0.9-i386-linux # (default: darcs)
120 scm_subversion_command:
120 scm_subversion_command:
121 scm_mercurial_command:
121 scm_mercurial_command:
122 scm_git_command:
122 scm_git_command:
123 scm_cvs_command:
123 scm_cvs_command:
124 scm_bazaar_command:
124 scm_bazaar_command:
125 scm_darcs_command:
125 scm_darcs_command:
126
126
127 # Key used to encrypt sensitive data in the database (SCM and LDAP passwords).
128 # If you don't want to enable data encryption, just leave it blank.
129 # WARNING: losing/changing this key will make encrypted data unreadable.
130 #
131 # If you want to encrypt existing passwords in your database:
132 # * set the cipher key here in your configuration file
133 # * encrypt data using 'rake db:encrypt RAILS_ENV=production'
134 #
135 # If you have encrypted data and want to change this key, you have to:
136 # * decrypt data using 'rake db:decrypt RAILS_ENV=production' first
137 # * change the cipher key here in your configuration file
138 # * encrypt data using 'rake db:encrypt RAILS_ENV=production'
139 database_cipher_key:
140
127 # specific configuration options for production environment
141 # specific configuration options for production environment
128 # that overrides the default ones
142 # that overrides the default ones
129 production:
143 production:
130
144
131 # specific configuration options for development environment
145 # specific configuration options for development environment
132 # that overrides the default ones
146 # that overrides the default ones
133 development:
147 development:
General Comments 0
You need to be logged in to leave comments. Login now