Setting-up Redmine
by Sebastien Mirolo on Sun, 9 Oct 2011After a few days battling with trac, I decided to give redmine a shot.
Redmine is a ruby-on-rails wiki engine for software development that supports ldap authentication out-of-the-box, one of the main features I struggled with trying to setup trac. It also has a great theme framework.
I decided to get redmine working in a thin ruby webapp and use nginx for the main webserver and load balancer.
Web server and webapp container
First let's setup ningx as a proxy to thin and get them to communicate through unix sockets.
$ cat /etc/nginx/proxy.include proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; client_max_body_size 10m; client_body_buffer_size 128k; proxy_connect_timeout 90; proxy_send_timeout 90; proxy_read_timeout 90; proxy_buffer_size 4k; proxy_buffers 4 32k; proxy_busy_buffers_size 64k; proxy_temp_file_write_size 64k; # 2. Configuration for nginx $ apt-get install nginx $ cat /etc/nginx/sites-available/domainname.conf upstream thin_cluster { server unix:/tmp/thin.0.sock; server unix:/tmp/thin.1.sock; server unix:/tmp/thin.2.sock; server unix:/tmp/thin.3.sock; } server { listen 80; server_name domainname; location / { rewrite ^/(.*)$ https://domainname/$1 redirect; } } server { listen 443; server_name domainname; ssl on; ssl_certificate /etc/ssl/certs/domainname.pem; ssl_certificate_key /etc/ssl/private/domainname.key; ssl_session_timeout 5m; ssl_protocols SSLv3 TLSv1; ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP; ssl_prefer_server_ciphers on; include proxy.include; root /var/www/redmine; proxy_redirect off; location / { try_files $uri/index.html $uri.html $uri @cluster; } location @cluster { proxy_pass http://thin_cluster; } } $ cd /etc/nginx/sites-enabled/ $ ln -fs ../sites-available/domainname.conf 000-domainname $ /etc/init.d/nginx restart # 3. Configuration for thin $ apt-get install thin libi18n-ruby rails $ cat /etc/thin/redmine.yml pid: tmp/pids/thin.pid group: www-data timeout: 30 log: log/thin.log max_conns: 1024 require: [] environment: production max_persistent_conns: 512 servers: 4 daemonize: true user: redmine socket: /tmp/thin.sock chdir: /var/www/redmine $ chgrp www-data /usr/bin/thin $ chmod g+s /usr/bin/thin # 4. Reminder, creating the self-signed SSL key and certificate $ openssl genrsa -rand /swap -passout "pass:" \ -out /etc/ssl/private/domainname.key 1024 $ openssl req -new -sha1 -subj "/CN=domainname" \ -key /etc/ssl/private/domainname.key \ -out domainname.csr $ openssl x509 -req -days 365 -outform PEM \ -in domainname.csr \ -signkey /etc/ssl/private/domainname.key \ -out /etc/ssl/certs/domainname.pem
Content database
I picked postgresql for the database back-end.
$ apt-get install postgresql $ /etc/init.d/postgresql initdb $ /etc/init.d/postgresql start $ sudo -u postgres psql =$ CREATE ROLE redmine LOGIN ENCRYPTED PASSWORD 'my_password' \ NOINHERIT VALID UNTIL 'infinity'; =$ CREATE DATABASE redmine WITH ENCODING='UTF8' OWNER=redmine; =$^D
It seems redmine will try to authenticate with the postgresql database using an IP connection. For some reason, even with what seems like correct login/password in config/database.yml, I kept getting an authentication error.
FATAL: Ident authentication failed for user "redmine"
Google mostly returned noise about the problem so I decided to switch the postgresql settings from ident to trust and rely on the firewall to prevent connection from outside the box.
$ diff -u pg_hba.prev pg_hba.conf # TYPE DATABASE USER CIDR-ADDRESS METHOD # "local" is for Unix domain socket connections only -local all all ident +local all all trust # IPv4 local connections: -host all all 127.0.0.1/32 ident +host all all 127.0.0.1/32 trust # IPv6 local connections: -host all all ::1/128 ident +host all all ::1/128 trust $ /etc/init.d/postgresql restart
Redmine webapp
Finally it is time to deploy the actual redmine ruby webapp.
$ apt-get install git-core rake postgresql-server-dev-8.4 $ mkdir -p /var/www/redmine $ cd /var/www $ chgrp www-data redmine $ git clone git://github.com/edavis10/redmine.git $ cd redmine $ cp config/database.yml.example config/database.yml $ diff -u config/database.yml.example config/database.yml production: - adapter: mysql + adapter: postgresql database: redmine host: localhost - username: root - password: + username: redmine + password: redmine_password $ gem install -v=0.4.2 i18n $ gem install -v=2.3.11 rails $ gem install activerecord-jdbcpostgresql-adapter $ gem install postgres $ diff -u config/environment.rb.prev config/environment.rb config.gem 'rubytree', :lib => 'tree' config.gem 'coderay', :version => '~>0.9.7' + config.gem "postgres" $ rake --trace generate_session_store $ RAILS_ENV=production rake --trace db:migrate $ RAILS_ENV=production rake --trace redmine:load_default_data # Starting redmine $ /usr/bin/thin start -D --all /etc/thin $ cat log/thin.0.log
After this point most of the configuration is done through the web interface. Login as admin and use the default password (admin). Go to my account, update your first and last name, email address and, oh yes, change your password.
Click through the links "Administration > Settings > Authentication" and change the settings as follow:
Authentication required: yes Self-registration: disabled # LDAP Authentication Host: localhost port: 389 BaseDN: dc=domain,dc=com On-the-fly user creation: yes Login: uid First name: givenName Last name: sn Email: mail
Patch to update LDAP password from within redmine
Most of my users only have access to the website (no shell access to the machine). Since redmine is also the only application deployed at this point, it just made sense to let users change their LDAP password from within redmine. At best this is currently work-in-progress and does not come packaged in the official redmine distribution. I found the following discussion and patch. There were only a few modifications to the patch I had to do in order to make it work with the repository head.
diff --git a/app/controllers/my_controller.rb \ b/app/controllers/my_controller.rb index a4a4b51..d921412 100644 --- a/app/controllers/my_controller.rb +++ b/app/controllers/my_controller.rb @@ -78,10 +78,19 @@ class MyController < ApplicationController end if request.post? if @user.check_password?(params[:password]) - @user.password, @user.password_confirmation \ = params[:new_password], params[:new_password_confirmation] - if @user.save - flash[:notice] = l(:notice_account_password_updated) - redirect_to :action => 'account' + if @user.isExternal? + if @user.changeExternalPassword(params[:password], params[:new_password], params[:new_password_confirmation]) + flash[:notice] = l(:notice_account_password_updated) + redirect_to :action => 'account' + else + flash[:error] = l(:notice_external_password_error) + end + else + @user.password, @user.password_confirmation \ = params[:new_password], params[:new_password_confirmation] + if @user.save + flash[:notice] = l(:notice_account_password_updated) + redirect_to :action => 'account' + end end else flash[:error] = l(:notice_account_wrong_password) diff --git a/app/helpers/auth_sources_helper.rb \ b/app/helpers/auth_sources_helper.rb index 90f5954..b5ad791 100644 --- a/app/helpers/auth_sources_helper.rb +++ b/app/helpers/auth_sources_helper.rb @@ -16,4 +16,11 @@ module AuthSourcesHelper + + module Encryption + # Return an array of password encryptions + def self.encryptiontypes + ["MD5","SSHA","CLEAR"] + end + end end diff --git a/app/models/auth_source_ldap.rb \ b/app/models/auth_source_ldap.rb index b7ab0cf..d42d01d 100644 --- a/app/models/auth_source_ldap.rb +++ b/app/models/auth_source_ldap.rb @@ -17,6 +17,8 @@ require 'net/ldap' require 'iconv' +require 'digest' +require 'base64' class AuthSourceLdap < AuthSource validates_presence_of :host, :port, :attr_login @@ -55,6 +57,50 @@ class AuthSourceLdap < AuthSource "LDAP" end + def allow_password_changes? + return self.enabled_passwd + end + + def encode_password(clear_password) + chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a + salt = '' + 10.times { |i| salt << chars[rand(chars.size-1)] } + + if self.password_encryption == "MD5" + logger.debug "Encode as md5" + return "{MD5}"+Base64.encode64(Digest::MD5.digest(clear_password)).chomp! + end + if self.password_encryption == "SSHA" + logger.debug "Encode as ssha" + return "{SSHA}"+Base64.encode64(Digest::SHA1.digest(clear_password+salt)+salt).chomp! + end + + if self.password_encryption == "CLEAR" + logger.debug "Encode as cleartype" + return clear_password + end + end + + # change password + def change_password(login,password,newPassword) + begin + attrs = get_user_dn(login) + if attrs + if self.account.blank? || self.account_password.blank? + logger.debug "Binding with user account" + ldap_con = initialize_ldap_con(attrs[:dn], password) + else + logger.debug "Binding with administrator account" + ldap_con = initialize_ldap_con(self.account, self.account_password) + end + return ldap_con.replace_attribute attrs[:dn], :userPassword, encode_password(newPassword) + end + rescue + return false + end + return false + end + private def strip_ldap_attributes diff --git a/app/models/user.rb \ b/app/models/user.rb index b362202..4499360 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -545,6 +545,19 @@ class User < Principal end end + def isExternal? + return auth_source_id.present? + end + + def changeExternalPassword(password,newPassword,newPasswordConfirm) + return false if newPassword == "" || newPassword.length < 4 + return false if newPassword != newPasswordConfirm + if (self.isExternal?) + return self.auth_source.change_password(self.login,password,newPassword) + end + return false + end + protected def validate_password_length diff --git a/app/views/ldap_auth_sources/_form.html.erb \ b/app/views/ldap_auth_sources/_form.html.erb index 9ffffaf..e125a54 100644 --- a/app/views/ldap_auth_sources/_form.html.erb +++ b/app/views/ldap_auth_sources/_form.html.erb @@ -25,6 +25,8 @@ <p><label for="auth_source_onthefly_register"><%=l(:field_onthefly)%></label> <%= check_box 'auth_source', 'onthefly_register' %></p> +<p><label for="auth_source_enabled_passwd"><%=l(:field_enabled_passwd)%></label> +<%= check_box 'auth_source', 'enabled_passwd' %></p> </div> <fieldset class="box"><legend><%=l(:label_attribute_plural)%></legend> @@ -39,6 +41,9 @@ <p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label> <%= text_field 'auth_source', 'attr_mail', :size => 20 %></p> +<p><label for="auth_source_password_encryption"><%=l(:field_password_encryption)%></label> +<%= select 'auth_source', 'password_encryption', AuthSourcesHelper::Encryption.encryptiontypes %> +</p> </fieldset> <!--[eoform:auth_source]--> diff --git a/config/locales/en.yml b/config/locales/en.yml index f8a1c25..dae359b 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -140,6 +140,7 @@ en: general_pdf_encoding: UTF-8 general_first_day_of_week: '7' + notice_external_password_error: External password changing goes wrong notice_account_updated: Account was successfully updated. notice_account_invalid_creditentials: Invalid user or password notice_account_password_updated: Password was successfully updated. @@ -270,6 +271,8 @@ en: field_attr_lastname: Lastname attribute field_attr_mail: Email attribute field_onthefly: On-the-fly user creation + field_password_encryption: Encyrption + field_enabled_passwd: Enabled password changing field_start_date: Start date field_done_ratio: "% Done" field_auth_source: Authentication mode
Still learning about redmine, I had to look the error in the production.log and figure out I just had to do a db:migrate to make everything work.
$ tail -200 log/production.log $ RAILS_ENV=production rake db:migrate
That's it now users can enjoy redmine, login using LDAP authentication and change their password from within redmine.