Welcome to the DjaoDjin Blog!

A place to share experiences in building Software-as-a-Service.

Setting-up Redmine

by Sebastien Mirolo on Sun, 9 Oct 2011

After 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.