*** app/controllers/auth_sources_controller.rb.orig	2010-05-23 05:16:31.000000000 +0200
--- app/controllers/auth_sources_controller.rb	2010-09-17 11:37:38.173218852 +0200
***************
*** 69,74 ****
--- 69,92 ----
      end
      redirect_to :action => 'index'
    end
+   
+   def refresh_groups
+     success_count = 0
+     error_users = []
+     User.active.find(:all, :conditions => ['auth_source_id = ?', params[:id]]).each do |u|
+       if u.refresh_group_memberships.nil?
+         error_users << u.login
+       else
+         success_count = success_count + 1
+       end
+     end
+     if (error_users.size == 0)
+       flash[:notice] = l(:text_group_refreshed, :count => success_count)
+     else
+       flash[:error] = l(:text_group_refresh_failed, :errors => error_users.size, :failures => error_users.join(", "), :success => success_count)
+     end
+     redirect_to :action => 'list'
+   end
  
    def destroy
      @auth_source = AuthSource.find(params[:id])
*** config/locales/de.yml.orig	2010-09-17 11:40:06.705218968 +0200
--- config/locales/de.yml	2010-09-17 11:40:31.329218830 +0200
***************
*** 302,307 ****
--- 302,312 ----
    field_default_value: Standardwert
    field_comments_sorting: Kommentare anzeigen
    field_parent_title: Übergeordnete Seite
+   field_groups: Organisation für Gruppen
+   field_prefix: Präfix für Gruppennamen
+   field_groups2: Funktion für Gruppen
+   field_cross_product: Erzeuge kombinierte Organisations-/Funktionsgruppen
+   field_group_separator: Trenner zwischen Organisation und Funktion
    field_editable: Bearbeitbar
    field_watcher: Beobachter
    field_identity_url: OpenID-URL
***************
*** 759,764 ****
--- 764,770 ----
    label_incoming_emails: Eingehende E-Mails
    label_generate_key: Generieren
    label_issue_watchers: Beobachter
+   label_group_option_plural: Gruppenoptionen
    label_example: Beispiel
    label_display: Anzeige
    label_sort: Sortierung
***************
*** 829,834 ****
--- 835,841 ----
    button_update: Aktualisieren
    button_configure: Konfigurieren
    button_quote: Zitieren
+   button_refresh: Gruppenzugehörigkeit aktualisieren
    button_duplicate: Duplizieren
    button_show: Anzeigen
  
***************
*** 891,896 ****
--- 898,906 ----
    text_email_delivery_not_configured: "Der SMTP-Server ist nicht konfiguriert und Mailbenachrichtigungen sind ausgeschaltet.\nNehmen Sie die Einstellungen für Ihren SMTP-Server in config/email.yml vor und starten Sie die Applikation neu."
    text_repository_usernames_mapping: "Bitte legen Sie die Zuordnung der Redmine-Benutzer zu den Benutzernamen der Commit-Log-Meldungen des Projektarchivs fest.\nBenutzer mit identischen Redmine- und Projektarchiv-Benutzernamen oder -E-Mail-Adressen werden automatisch zugeordnet."
    text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.'
+   text_group_updated_by: "verwaltet durch {{type}}-Anbindung {{name}}"
+   text_group_refreshed: "Gruppenzugehörigkeit von {{count}} aktiven Benutzern aktualisiert"
+   text_group_refresh_failed: "Gruppenzugehörigkeit der folgenden {{errors}} Benutzer fehlgeschlagen: {{failures}} (Aktualisierung für {{success}} aktive Benutzer erfolgreich)"
    text_custom_field_possible_values_info: 'Eine Zeile pro Wert'
    text_wiki_page_destroy_question: "Diese Seite hat {{descendants}} Unterseite(n). Was möchten Sie tun?"
    text_wiki_page_nullify_children: Verschiebe die Unterseiten auf die oberste Ebene
*** config/locales/en.yml.orig	2010-09-17 11:40:17.729218810 +0200
--- config/locales/en.yml	2010-09-17 11:40:35.937218900 +0200
***************
*** 293,298 ****
--- 293,304 ----
    field_group_by: Group results by
    field_sharing: Sharing
    field_parent_issue: Parent task
+   field_groups: Organisation group
+   field_prefix: Group name prefix
+   field_groups2: Function group
+   field_cross_product: Include combined function/organisation groups
+   field_group_separator: Function/group separator
+ 
    
    setting_app_title: Application title
    setting_app_subtitle: Application subtitle
***************
*** 768,773 ****
--- 774,780 ----
    label_api_access_key: API access key
    label_missing_api_access_key: Missing an API access key
    label_api_access_key_created_on: "API access key created {{value}} ago"
+   label_group_option_plural: Grouping options
    label_profile: Profile
    label_subtask_plural: Subtasks
    label_project_copy_notifications: Send email notifications during the project copy
***************
*** 815,820 ****
--- 822,828 ----
    button_quote: Quote
    button_duplicate: Duplicate
    button_show: Show
+   button_refresh: Refresh groups
    
    status_active: active
    status_registered: registered
***************
*** 881,886 ****
--- 889,897 ----
    text_wiki_page_destroy_children: "Delete child pages and all their descendants"
    text_wiki_page_reassign_children: "Reassign child pages to this parent page"
    text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
+   text_group_updated_by: "maintained by {{type}} authentication source {{name}}"
+   text_group_refreshed: "Groups for {{count}} active users refreshed"
+   text_group_refresh_failed: "Group refresh for {{errors}} failed: {{failures}} (updating for {{success}} active users successful)"
    text_zoom_in: Zoom in
    text_zoom_out: Zoom out
    
*** app/views/groups/_form.html.erb.orig	2009-09-05 13:19:00.000000000 +0200
--- app/views/groups/_form.html.erb	2010-09-17 11:44:13.733218883 +0200
***************
*** 1,7 ****
  <%= error_messages_for :group %>
  
  <div class="box tabular">
! 	<p><%= f.text_field :lastname, :label => :field_name %></p>
  	<% @group.custom_field_values.each do |value| %>
  	<p><%= custom_field_tag_with_label :group, value %></p>
    <% end %>
--- 1,7 ----
  <%= error_messages_for :group %>
  
  <div class="box tabular">
! 	<p><%= f.text_field :lastname, :label => :field_name, :disabled => (@group.auth_source_id.to_i > 0) %> <%= @group.updated_by_string || "" %></p>
  	<% @group.custom_field_values.each do |value| %>
  	<p><%= custom_field_tag_with_label :group, value %></p>
    <% end %>
*** app/views/groups/index.html.erb.orig	2009-07-05 13:38:40.000000000 +0200
--- app/views/groups/index.html.erb	2010-09-17 11:44:20.741218938 +0200
***************
*** 13,21 ****
    </tr></thead>
    <tbody>
  <% @groups.each do |group| %>
!   <tr class="<%= cycle 'odd', 'even' %>">
      <td><%= link_to h(group), :action => 'edit', :id => group %></td>
!     <td align="center"><%= group.users.size %></td>
      <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
    </tr>
  <% end %>
--- 13,21 ----
    </tr></thead>
    <tbody>
  <% @groups.each do |group| %>
!   <tr class="<%= cycle 'odd', 'even' %><%= group.auth_source_id.to_i == 0 ? '' : ' auto-group' %>">
      <td><%= link_to h(group), :action => 'edit', :id => group %></td>
!     <td align="left"><%= group.size_and_updated_by_string %></td>
      <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
    </tr>
  <% end %>
*** app/models/group.rb.orig	2009-09-12 10:36:46.000000000 +0200
--- app/models/group.rb	2010-09-17 11:48:21.145219249 +0200
***************
*** 24,30 ****
    validates_presence_of :lastname
    validates_uniqueness_of :lastname, :case_sensitive => false
    validates_length_of :lastname, :maximum => 30
!     
    def to_s
      lastname.to_s
    end
--- 24,41 ----
    validates_presence_of :lastname
    validates_uniqueness_of :lastname, :case_sensitive => false
    validates_length_of :lastname, :maximum => 30
!   
!   # Remove slash- or space-separated entities until it fits into the maxlength
!   # If the last part is too long, just take the ending maxlength chars
!   def self.shorten_lastname(prefix, lastname)
!     save = lastname
!     while !lastname.nil? && lastname.length+prefix.length > 30
!       dummy, lastname = lastname.split(/[ \/]/, 2)
!     end
!     lastname = save[-30+prefix.length,30] if lastname.nil? || lastname.empty?
!     prefix + lastname
!   end
!   
    def to_s
      lastname.to_s
    end
***************
*** 45,48 ****
--- 56,82 ----
                              :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
      end
    end
+   
+   def size_and_updated_by_string
+     updated_by = updated_by_string
+     if updated_by == nil
+       return users.size
+     else
+       return "#{users.size} (#{updated_by_string})"
+     end
+   end
+ 
+   def updated_by_string
+     source_id = auth_source_id.to_i
+     if source_id == 0
+       return nil
+     else
+       source = AuthSource.find_by_id(source_id)
+       if source == nil
+         return l(:text_group_updated_by_unknown, :id => source_id)
+       else
+         return l(:text_group_updated_by, :name => source.name, :type => source.auth_method_name)
+       end
+     end
+   end
  end
*** app/models/auth_source.rb.orig	2010-05-23 05:16:37.000000000 +0200
--- app/models/auth_source.rb	2010-09-17 11:51:32.581218738 +0200
***************
*** 17,22 ****
--- 17,23 ----
  
  class AuthSource < ActiveRecord::Base
    has_many :users
+   has_many :groups
    
    validates_presence_of :name
    validates_uniqueness_of :name
***************
*** 55,58 ****
--- 56,102 ----
      end
      return nil
    end
+ 
+   def self.import(login)
+     auth = get_data(login)
+     logger.debug("auth is #{auth.class.to_s}")
+     if auth && auth.size == 1
+       a = auth.first
+       a.each { |key, value| logger.debug("#{key} => #{value}") }
+       a.delete(:dn)
+       user = User.new(a)
+       user.login = login
+       user.language = Setting.default_language
+       user.admin = false # Just to be sure
+       if user.save
+         logger.debug("successful created")
+         user.refresh_group_memberships
+ 	      return user
+       else
+         logger.debug("failed to create")
+ 	      return nil
+       end
+     else
+       logger.debug("User not found among those sources available for on-the-fly creation")
+       return nil
+     end
+   end
+ 
+   private
+ 
+   # Try to import a user not yet registered against available sources
+   def self.get_data(login)
+     AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
+       begin
+         logger.debug "Importing '#{login}' from '#{source.name}'" if logger && logger.debug?
+ 	logger.debug "Using class #{source.class.to_s}" if logger && logger.debug?
+         attrs = source.get_attributes(login)
+       rescue => e
+         logger.error "Error during import: #{e.message}"
+         attrs = nil
+       end
+       return attrs if attrs
+     end
+     return nil
+   end
  end
*** app/views/ldap_auth_sources/_form.rhtml.orig	2010-05-23 05:16:31.000000000 +0200
--- app/views/ldap_auth_sources/_form.rhtml	2010-09-17 11:54:26.709218875 +0200
***************
*** 39,44 ****
--- 39,64 ----
  
  <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_attr_groups"><%=l(:field_groups)%></label>
+ <%= text_field 'auth_source', 'attr_groups', :size => 20 %></p>
+ 
+ <p><label for="auth_source_attr_groups2"><%=l(:field_groups2)%></label>
+ <%= text_field 'auth_source', 'attr_groups2', :size => 20 %></p>
+ 
  </fieldset>
+ 
+ <fieldset class="box"><legend><%=l(:label_group_option_plural)%></legend>
+ <p><label for="auth_source_group_prefix"><%=l(:field_prefix)%></label>
+ <%= text_field 'auth_source', 'group_prefix', :size => 20  %></p>
+ 
+ <p><label for="auth_source_cross_product"><%=l(:field_cross_product)%></label>
+ <%= check_box 'auth_source', 'cross_product' %></p>
+ 
+ <p><label for="auth_source_group_separator"><%=l(:field_group_separator)%></label>
+ <%= text_field 'auth_source', 'group_separator', :size => 20  %></p>
+ 
+ </fieldset>
+ 
  <!--[eoform:auth_source]-->
  
*** app/controllers/members_controller.rb.orig	2010-06-19 21:51:43.000000000 +0200
--- app/controllers/members_controller.rb	2010-09-17 11:56:00.725468671 +0200
***************
*** 26,31 ****
--- 26,49 ----
      members = []
      if params[:member] && request.post?
        attrs = params[:member].dup
+       # When no user is selected but the name does match a user
+       # in LDAP, which has not yet been imported, then go and import the
+       # user from LDAP and add it to the project. Multiple names may be
+       # separated by whitespace.
+       if (! attrs.has_key?(:user_ids) && ! params[:principal_search].empty?)
+         attrs[:user_ids] = []
+         newUser = nil
+         params[:principal_search].split.each do |login|
+ 	  newUser = AuthSource.import(login)
+           if newUser
+ 	    logger.info("Imported AuthSource as #{newUser}")
+           else
+             newUser = User.first(:conditions => ["login = ?", login])
+           end
+ 	  attrs[:user_ids] << newUser.id if newUser
+           logger.debug("Would join entries #{attrs[:user_ids].inspect}")
+         end
+       end
        if (user_ids = attrs.delete(:user_ids))
          user_ids.each do |user_id|
            members << Member.new(attrs.merge(:user_id => user_id))
*** db/migrate/20100204211355_add_ldap_group_support.rb.orig	2010-09-17 11:59:22.537219129 +0200
--- db/migrate/20100204211355_add_ldap_group_support.rb	2010-09-17 11:59:05.001218974 +0200
***************
*** 0 ****
--- 1,11 ----
+ class AddLdapGroupSupport < ActiveRecord::Migration
+   def self.up
+     add_column :auth_sources, :attr_groups, :string, :limit => 30, :default => "", :null => false
+     add_column :auth_sources, :group_prefix, :string, :limit => 30, :default => "_", :null => false
+   end
+ 
+   def self.down
+     remove_column :auth_sources, :attr_groups
+     remove_column :auth_sources, :group_prefix
+   end
+ end
*** db/migrate/20100207220329_extend_ldap_groups.rb.orig	2010-09-17 11:59:32.121218964 +0200
--- db/migrate/20100207220329_extend_ldap_groups.rb	2010-09-17 11:59:10.629218890 +0200
***************
*** 0 ****
--- 1,13 ----
+ class ExtendLdapGroups < ActiveRecord::Migration
+   def self.up
+     add_column :auth_sources, :attr_groups2, :string, :limit => 30, :default => "", :null => false
+     add_column :auth_sources, :group_separator, :string, :limit => 30, :default => ":", :null => false
+     add_column :auth_sources, :cross_product, :boolean, :default => false, :null => false
+   end
+ 
+   def self.down
+     remove_column :auth_sources, :attr_groups2
+     remove_column :auth_sources, :group_separator
+     remove_column :auth_sources, :cross_product
+   end
+ end
*** config/locales/fr.yml.orig	2010-09-17 14:40:19.421468910 +0200
--- config/locales/fr.yml	2010-09-17 14:52:17.901218757 +0200
***************
*** 301,306 ****
--- 301,311 ----
    field_content: Contenu
    field_group_by: Grouper par
    field_sharing: Partage
+   field_groups: Groupes Organisation
+   field_prefix: Prefixe pour les groupes
+   field_groups2: Groupes Fonctions
+   field_cross_product: Inclure les groupes fonction/organisation combinés
+   field_group_separator: Séparateur Groupe / Fonction
    field_active: Actif
    field_parent_issue: Tâche parente
    
***************
*** 765,770 ****
--- 770,776 ----
    label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
    label_api_access_key: Clé d'accès API
    label_api_access_key_created_on: Clé d'accès API créée il y a {{value}}
+   label_group_option_plural: Options de groupes
    label_feeds_access_key: Clé d'accès RSS
    label_missing_api_access_key: Clé d'accès API manquante
    label_missing_feeds_access_key: Clé d'accès RSS manquante
***************
*** 817,822 ****
--- 823,829 ----
    button_quote: Citer
    button_duplicate: Dupliquer
    button_show: Afficher
+   button_refresh: Rafraichir les groupes
    
    status_active: actif
    status_registered: enregistré
***************
*** 877,883 ****
    text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
    text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
    text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
!   
    default_role_manager: "Manager "
    default_role_developer: "Développeur "
    default_role_reporter: "Rapporteur "
--- 884,893 ----
    text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
    text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
    text_own_membership_delete_confirmation: "Vous allez supprimer tout ou partie de vos permissions sur ce projet et ne serez peut-être plus autorisé à modifier ce projet.\nEtes-vous sûr de vouloir continuer ?"
!  
!   text_group_updated_by: "maintainu par {{type}} source d'authentification {{name}}" 
!   text_group_refreshed: "Groupes pour {{count}} utilisateurs actifs rafraichi"
!   text_group_refresh_failed: "Rafraichissement des groupes pour {{errors}} echoué: {{failures}} (mise à jour pour  {{success}} utilisateurs actif réussi)"
    default_role_manager: "Manager "
    default_role_developer: "Développeur "
    default_role_reporter: "Rapporteur "
*** app/models/auth_source_ldap.rb.orig	2010-02-26 10:13:12.000000000 +0100
--- app/models/auth_source_ldap.rb	2010-09-17 16:18:26.249218878 +0200
***************
*** 63,75 ****
      end
    end
    
!   def initialize_ldap_con(ldap_user, ldap_password)
!     options = { :host => self.host,
!                 :port => self.port,
!                 :encryption => (self.tls ? :simple_tls : nil)
!               }
!     options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
!     Net::LDAP.new options
    end
  
    def get_user_attributes_from_ldap_entry(entry)
--- 63,83 ----
      end
    end
    
!   # By passing an optional third parameter (the login name), the fake classes
!   # below will be used (more comfortable for debugging). Anyone logging in
!   # with a login matching "firstname.lastname" is considered to be in this
!   # fake LDAP, any password matches. Some groups will also be set
!   def initialize_ldap_con(ldap_user, ldap_password, fake = nil)
!     if (fake != nil)
!       FakeLdapCon.new fake
!     else
!       options = { :host => self.host,
!                   :port => self.port,
!                   :encryption => (self.tls ? :simple_tls : nil)
!                 }
!       options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
!       Net::LDAP.new options
!     end
    end
  
    def get_user_attributes_from_ldap_entry(entry)
***************
*** 78,92 ****
       :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
       :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
       :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
       :auth_source_id => self.id
      }
    end
  
    # Return the attributes needed for the LDAP search.  It will only
    # include the user attributes if on-the-fly registration is enabled
    def search_attributes
      if onthefly_register?
!       ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail]
      else
        ['dn']
      end
--- 86,117 ----
       :firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
       :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
       :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
+      :group_names => build_names(AuthSourceLdap.get_attr(entry, self.attr_groups), AuthSourceLdap.get_attr(entry, self.attr_groups2)),
       :auth_source_id => self.id
      }
    end
  
+   def build_names(names, names2)
+     all = []
+     names.each do |n|
+       all << Group.shorten_lastname(self.group_prefix, n)
+     end
+     names2.each do |n|
+       all << Group.shorten_lastname(self.group_prefix, n)
+     end
+     names.each do |n|
+       names2.each do |n2|
+         all << Group.shorten_lastname(self.group_prefix, n + self.group_separator + n2)
+       end
+    end if self.cross_product
+     all
+   end
+ 
    # Return the attributes needed for the LDAP search.  It will only
    # include the user attributes if on-the-fly registration is enabled
    def search_attributes
      if onthefly_register?
!       ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail ,self.attr_groups,self.attr_groups2]
      else
        ['dn']
      end
***************
*** 127,130 ****
--- 152,224 ----
        entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
      end
    end
+ 
+   def self.get_attr_list(entry, attr_name)
+     if !attr_name.blank?
+       entry[attr_name].is_a?(Array) ? entry[attr_name] : [entry[attr_name]]
+     end
+   end
+ end
+ 
+ class FakeLdapCon
+   def initialize(first_dot_last)
+     parts = first_dot_last.split('.')
+     @first = parts[0]
+     @last = parts[1]
+   end
+   
+   def first
+     @first
+   end
+   
+   def last
+     @last
+   end
+   
+   def to_s
+     "FakeLdap(#{@first}.#{last})"
+   end
+   
+   def search(query, &block)
+     yield FakeLdapEntry.new(@first, @last) unless @last.blank?
+   end
+   
+   def bind
+     !@last.blank?
+   end
+ end
+ 
+ class FakeLdapEntry
+   def initialize(first, last)
+     @first = first
+     @last = last
+   end
+   
+   def dn
+     @first + "." + @last
+   end
+   
+   def givenName
+     @first
+   end
+   
+   def sn
+     @last
+   end
+   
+   def mail
+     self.dn + "@example.org"
+   end
+   
+   def ou
+     [@first, @last, "everyone"]
+   end
+   
+   def businessCategory
+     @first.length.odd? ? "S" : "A"
+   end
+   
+   def [](entry)
+     self.send(entry)
+   end
  end
*** app/models/user.rb.orig	2010-09-17 16:24:41.889218962 +0200
--- app/models/user.rb	2010-09-17 16:24:52.833218924 +0200
***************
*** 49,54 ****
--- 49,55 ----
    
    attr_accessor :password, :password_confirmation
    attr_accessor :last_before_login_on
+   attr_accessor :group_names
    # Prevents unauthorized assignments
    attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
  	
***************
*** 106,112 ****
        return nil if !user.active?
        if user.auth_source
          # user has an external authentication method
!         return nil unless user.auth_source.authenticate(login, password)
        else
          # authentication with local password
          return nil unless User.hash_password(password) == user.hashed_password        
--- 107,117 ----
        return nil if !user.active?
        if user.auth_source
          # user has an external authentication method
!         attrs = user.auth_source.authenticate(login, password)
!         logger.debug attrs.inspect
!         return nil unless attrs = user.auth_source.authenticate(login, password)
!  #       user.group_names = attrs.first[:group_names]
!  #       user.refresh_group_memberships
        else
          # authentication with local password
          return nil unless User.hash_password(password) == user.hashed_password        
***************
*** 121,126 ****
--- 126,132 ----
          if user.save
            user.reload
            logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
+ 	  user.refresh_group_memberships
          end
        end
      end    
***************
*** 138,143 ****
--- 144,150 ----
        token = tokens.first
        if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
          token.user.update_attribute(:last_login_on, Time.now)
+         token.user.refresh_group_memberships if token.user.auth_source
          token.user
        end
      end
***************
*** 352,358 ****
        false
      end
    end
!   
    def self.current=(user)
      @current_user = user
    end
--- 359,365 ----
        false
      end
    end
!     
    def self.current=(user)
      @current_user = user
    end
***************
*** 371,376 ****
--- 378,413 ----
      end
      anonymous_user
    end
+ 
+   def refresh_group_memberships
+     return nil if !self.auth_source
+     # (re-)import the information from LDAP if necessary
+     if group_names.nil?
+       attrs = self.auth_source.get_attributes(self.login)
+       if attrs.nil?
+         logger.error("Refreshing group memberships not possible for #{self.login}")
+         return nil
+       end
+       self.group_names = attrs.first[:group_names]
+     end
+     # Add user to groups she is not in yet
+     prefixed_names = {}
+     self.group_names.each do |name|
+       prefixed_names[name] = true
+       group = Group.find_or_create_by_lastname(name)
+       if !group.users.exists?(self)
+         group.auth_source_id = self.auth_source_id # Mark as LDAP-maintained
+         group.users << self
+         group.save
+       end
+     end
+     # Remove user from groups she is no longer in yet; remove empty groups
+     gg = Group.find(:all, :conditions => ["auth_source_id = ?", self.auth_source_id]).each do |g|
+       g.users.delete(self) if g.users.exists?(self) && !prefixed_names.key?(g.lastname)
+       Group.destroy(g.id) if g.users.count == 0
+     end
+     true
+   end
    
    protected
    
