*** 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 %>
<%= f.text_field :lastname, :label => :field_name %>
<% @group.custom_field_values.each do |value| %><%= custom_field_tag_with_label :group, value %>
<% end %> --- 1,7 ---- <%= error_messages_for :group %><%= f.text_field :lastname, :label => :field_name, :disabled => (@group.auth_source_id.to_i > 0) %> <%= @group.updated_by_string || "" %>
<% @group.custom_field_values.each do |value| %><%= custom_field_tag_with_label :group, value %>
<% 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 **** <% @groups.each do |group| %> !<%= text_field 'auth_source', 'attr_mail', :size => 20 %>
+ ++ <%= text_field 'auth_source', 'attr_groups', :size => 20 %>
+ ++ <%= text_field 'auth_source', 'attr_groups2', :size => 20 %>
+ + + + *** 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