Project

General

Profile

Patch #4755 » ldap-auto-groups.patch

Marcel Waldvogel, 2010-02-08 01:35

View differences:

app/models/auth_source_ldap.rb (Arbeitskopie)
33 33
  
34 34
  def authenticate(login, password)
35 35
    return nil if login.blank? || password.blank?
36
    attrs = get_attributes(login)
37
    dn = attrs.first.delete(:dn) unless attrs.nil?
38
    # authenticate user
39
    ldap_con = initialize_ldap_con(dn, password) # add ", login" for fake LDAP tests
40
    return nil unless ldap_con.bind
41
    # return user's attributes
42
    logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
43
    attrs    
44
  rescue  Net::LDAP::LdapError => text
45
    raise "LdapError: " + text
46
  end
47
  
48
  def get_attributes(login)
49
    return nil if login.blank?
36 50
    attrs = []
37 51
    # get user's DN
38
    ldap_con = initialize_ldap_con(self.account, self.account_password)
52
    ldap_con = initialize_ldap_con(self.account, self.account_password) # add ", login" for fake LDAP tests
39 53
    login_filter = Net::LDAP::Filter.eq( self.attr_login, login ) 
40 54
    object_filter = Net::LDAP::Filter.eq( "objectClass", "*" ) 
55
    # Only ask for attributes that will be used
56
    query_attrs = ['dn']
57
    query_attrs << [self.attr_firstname, self.attr_lastname, self.attr_mail] if onthefly_register?
58
    query_attrs << [self.attr_groups] unless self.attr_groups.blank?
59
    query_attrs << [self.attr_groups2] unless self.attr_groups2.blank?
41 60
    dn = String.new
42 61
    ldap_con.search( :base => self.base_dn, 
43 62
                     :filter => object_filter & login_filter, 
44
                     # only ask for the DN if on-the-fly registration is disabled
45
                     :attributes=> (onthefly_register? ? ['dn', self.attr_firstname, self.attr_lastname, self.attr_mail] : ['dn'])) do |entry|
63
                     :attributes=> query_attrs) do |entry|
64
                       logger.debug "yielded"
46 65
      dn = entry.dn
47 66
      attrs = [:firstname => AuthSourceLdap.get_attr(entry, self.attr_firstname),
48 67
               :lastname => AuthSourceLdap.get_attr(entry, self.attr_lastname),
49 68
               :mail => AuthSourceLdap.get_attr(entry, self.attr_mail),
50
               :auth_source_id => self.id ] if onthefly_register?
69
               :group_names => build_names(AuthSourceLdap.get_attr_list(entry, self.attr_groups),
70
                                           AuthSourceLdap.get_attr_list(entry, self.attr_groups2)),
71
               :auth_source_id => self.id ] if onthefly_register?      
51 72
    end
52 73
    return nil if dn.empty?
53 74
    logger.debug "DN found for #{login}: #{dn}" if logger && logger.debug?
54
    # authenticate user
55
    ldap_con = initialize_ldap_con(dn, password)
56
    return nil unless ldap_con.bind
57
    # return user's attributes
58
    logger.debug "Authentication successful for '#{login}'" if logger && logger.debug?
59
    attrs    
75
    attrs.first[:dn] = dn
76
    attrs
60 77
  rescue  Net::LDAP::LdapError => text
61 78
    raise "LdapError: " + text
62 79
  end
63 80

  
81
  def build_names(names, names2)
82
    all = []
83
    names.each do |n|
84
      all << Group.shorten_lastname(self.group_prefix, n)
85
    end
86
    names2.each do |n|
87
      all << Group.shorten_lastname(self.group_prefix, n)
88
    end
89
    names.each do |n|
90
      names2.each do |n2|
91
        all << Group.shorten_lastname(self.group_prefix, n + self.group_separator + n2)
92
      end
93
    end if self.cross_product
94
    all
95
  end
96

  
64 97
  # test the connection to the LDAP
65 98
  def test_connection
66 99
    ldap_con = initialize_ldap_con(self.account, self.account_password)
......
81 114
    end
82 115
  end
83 116
  
84
  def initialize_ldap_con(ldap_user, ldap_password)
85
    options = { :host => self.host,
86
                :port => self.port,
87
                :encryption => (self.tls ? :simple_tls : nil)
88
              }
89
    options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
90
    Net::LDAP.new options
117
  # By passing an optional third parameter (the login name), the fake classes
118
  # below will be used (more comfortable for debugging). Anyone logging in
119
  # with a login matching "firstname.lastname" is considered to be in this
120
  # fake LDAP, any password matches. Some groups will also be set
121
  def initialize_ldap_con(ldap_user, ldap_password, fake = nil)
122
    if (fake != nil)
123
      FakeLdapCon.new fake
124
    else
125
      options = { :host => self.host,
126
                  :port => self.port,
127
                  :encryption => (self.tls ? :simple_tls : nil)
128
                }
129
      options.merge!(:auth => { :method => :simple, :username => ldap_user, :password => ldap_password }) unless ldap_user.blank? && ldap_password.blank?
130
      Net::LDAP.new options
131
    end
91 132
  end
92 133
  
93 134
  def self.get_attr(entry, attr_name)
......
95 136
      entry[attr_name].is_a?(Array) ? entry[attr_name].first : entry[attr_name]
96 137
    end
97 138
  end
139

  
140
  def self.get_attr_list(entry, attr_name)
141
    if !attr_name.blank?
142
      entry[attr_name].is_a?(Array) ? entry[attr_name] : [entry[attr_name]]
143
    end
144
  end
98 145
end
146

  
147
class FakeLdapCon
148
  def initialize(first_dot_last)
149
    parts = first_dot_last.split('.')
150
    @first = parts[0]
151
    @last = parts[1]
152
  end
153
  
154
  def first
155
    @first
156
  end
157
  
158
  def last
159
    @last
160
  end
161
  
162
  def to_s
163
    "FakeLdap(#{@first}.#{last})"
164
  end
165
  
166
  def search(query, &block)
167
    yield FakeLdapEntry.new(@first, @last) unless @last.blank?
168
  end
169
  
170
  def bind
171
    !@last.blank?
172
  end
173
end
174

  
175
class FakeLdapEntry
176
  def initialize(first, last)
177
    @first = first
178
    @last = last
179
  end
180
  
181
  def dn
182
    @first + "." + @last
183
  end
184
  
185
  def givenName
186
    @first
187
  end
188
  
189
  def sn
190
    @last
191
  end
192
  
193
  def mail
194
    self.dn + "@example.org"
195
  end
196
  
197
  def ou
198
    [@first, @last, "everyone"]
199
  end
200
  
201
  def businessCategory
202
    @first.length.odd? ? "S" : "A"
203
  end
204
  
205
  def [](entry)
206
    self.send(entry)
207
  end
208
end
app/models/user.rb (Arbeitskopie)
49 49
  
50 50
  attr_accessor :password, :password_confirmation
51 51
  attr_accessor :last_before_login_on
52
  attr_accessor :group_names
52 53
  # Prevents unauthorized assignments
53 54
  attr_protected :login, :admin, :password, :password_confirmation, :hashed_password, :group_ids
54 55
	
......
102 103
      return nil if !user.active?
103 104
      if user.auth_source
104 105
        # user has an external authentication method
105
        return nil unless user.auth_source.authenticate(login, password)
106
        attrs = user.auth_source.authenticate(login, password)
107
        logger.debug attrs.inspect
108
        return nil unless attrs = user.auth_source.authenticate(login, password)
109
        user.group_names = attrs.first[:group_names]
110
        user.refresh_group_memberships
106 111
      else
107 112
        # authentication with local password
108 113
        return nil unless User.hash_password(password) == user.hashed_password        
......
117 122
        if user.save
118 123
          user.reload
119 124
          logger.info("User '#{user.login}' created from the LDAP") if logger
125
          user.refresh_group_memberships
120 126
        end
121 127
      end
122 128
    end    
......
134 140
      token = tokens.first
135 141
      if (token.created_on > Setting.autologin.to_i.day.ago) && token.user && token.user.active?
136 142
        token.user.update_attribute(:last_login_on, Time.now)
143
        token.user.refresh_group_memberships if token.user.auth_source
137 144
        token.user
138 145
      end
139 146
    end
......
302 309
      false
303 310
    end
304 311
  end
305
  
312
    
306 313
  def self.current=(user)
307 314
    @current_user = user
308 315
  end
......
321 328
    end
322 329
    anonymous_user
323 330
  end
331

  
332
  def refresh_group_memberships
333
    return nil if !self.auth_source
334
    # (re-)import the information from LDAP if necessary
335
    if group_names.nil?
336
      attrs = self.auth_source.get_attributes(self.login)
337
      if attrs.nil?
338
        logger.error("Refreshing group memberships not possible for #{self.login}")
339
        return nil
340
      end
341
      self.group_names = attrs.first[:group_names]
342
    end
343
    # Add user to groups she is not in yet
344
    prefixed_names = {}
345
    self.group_names.each do |name|
346
      prefixed_names[name] = true
347
      group = Group.find_or_create_by_lastname(name)
348
      if !group.users.exists?(self)
349
        group.auth_source_id = self.auth_source_id # Mark as LDAP-maintained
350
        group.users << self
351
        group.save
352
      end
353
    end
354
    # Remove user from groups she is no longer in yet; remove empty groups
355
    gg = Group.find(:all, :conditions => ["auth_source_id = ?", self.auth_source_id]).each do |g|
356
      g.users.delete(self) if g.users.exists?(self) && !prefixed_names.key?(g.lastname)
357
      Group.destroy(g.id) if g.users.count == 0
358
    end
359
    true
360
  end
324 361
  
325 362
  protected
326 363
  
app/models/auth_source.rb (Arbeitskopie)
17 17

  
18 18
class AuthSource < ActiveRecord::Base
19 19
  has_many :users
20
  has_many :groups
20 21
  
21 22
  validates_presence_of :name
22 23
  validates_uniqueness_of :name
......
46 47
    end
47 48
    return nil
48 49
  end
50

  
51
  def self.import(login)
52
    auth = get_data(login)
53
    logger.debug("auth is #{auth.class.to_s}")
54
    if auth && auth.size == 1
55
      a = auth.first
56
      a.each { |key, value| logger.debug("#{key} => #{value}") }
57
      a.delete(:dn)
58
      user = User.new(a)
59
      user.login = login
60
      user.language = Setting.default_language
61
      user.admin = false # Just to be sure
62
      if user.save
63
        logger.debug("successful created")
64
        user.refresh_group_memberships
65
	      return user
66
      else
67
        logger.debug("failed to create")
68
	      return nil
69
      end
70
    else
71
      logger.debug("User not found among those sources available for on-the-fly creation")
72
      return nil
73
    end
74
  end
75

  
76
  private
77

  
78
  # Try to import a user not yet registered against available sources
79
  def self.get_data(login)
80
    AuthSource.find(:all, :conditions => ["onthefly_register=?", true]).each do |source|
81
      begin
82
        logger.debug "Importing '#{login}' from '#{source.name}'" if logger && logger.debug?
83
	logger.debug "Using class #{source.class.to_s}" if logger && logger.debug?
84
        attrs = source.get_attributes(login)
85
      rescue => e
86
        logger.error "Error during import: #{e.message}"
87
        attrs = nil
88
      end
89
      return attrs if attrs
90
    end
91
    return nil
92
  end
49 93
end
app/models/group.rb (Arbeitskopie)
24 24
  validates_presence_of :lastname
25 25
  validates_uniqueness_of :lastname, :case_sensitive => false
26 26
  validates_length_of :lastname, :maximum => 30
27
    
27
  
28
  # Remove slash- or space-separated entities until it fits into the maxlength
29
  # If the last part is too long, just take the ending maxlength chars
30
  def self.shorten_lastname(prefix, lastname)
31
    save = lastname
32
    while !lastname.nil? && lastname.length+prefix.length > 30
33
      dummy, lastname = lastname.split(/[ \/]/, 2)
34
    end
35
    lastname = save[-30+prefix.length,30] if lastname.nil? || lastname.empty?
36
    prefix + lastname
37
  end
38
  
28 39
  def to_s
29 40
    lastname.to_s
30 41
  end
......
45 56
                            :conditions => ["#{Member.table_name}.user_id = ? AND #{MemberRole.table_name}.inherited_from IN (?)", user.id, member.member_role_ids]).each(&:destroy)
46 57
    end
47 58
  end
59
  
60
  def size_and_updated_by_string
61
    updated_by = updated_by_string
62
    if updated_by == nil
63
      return users.size
64
    else
65
      return "#{users.size} (#{updated_by_string})"
66
    end
67
  end
68

  
69
  def updated_by_string
70
    source_id = auth_source_id.to_i
71
    if source_id == 0
72
      return nil
73
    else
74
      source = AuthSource.find_by_id(source_id)
75
      if source == nil
76
        return l(:text_group_updated_by_unknown, :id => source_id)
77
      else
78
        return l(:text_group_updated_by, :name => source.name, :type => source.auth_method_name)
79
      end
80
    end
81
  end
48 82
end
app/controllers/members_controller.rb (Arbeitskopie)
24 24
    members = []
25 25
    if params[:member] && request.post?
26 26
      attrs = params[:member].dup
27
      # When no user is selected but the name does match a user
28
      # in LDAP, which has not yet been imported, then go and import the
29
      # user from LDAP and add it to the project. Multiple names may be
30
      # separated by whitespace.
31
      if (! attrs.has_key?(:user_ids) && ! params[:principal_search].empty?)
32
        attrs[:user_ids] = []
33
        newUser = nil
34
        params[:principal_search].split.each do |login|
35
	  newUser = AuthSource.import(login)
36
          if newUser
37
	    logger.info("Imported AuthSource as #{newUser}")
38
          else
39
            newUser = User.first(:conditions => ["login = ?", login])
40
          end
41
	  attrs[:user_ids] << newUser.id if newUser
42
          logger.debug("Would join entries #{attrs[:user_ids].inspect}")
43
        end
44
      end
27 45
      if (user_ids = attrs.delete(:user_ids))
28 46
        user_ids.each do |user_id|
29 47
          members << Member.new(attrs.merge(:user_id => user_id))
app/controllers/auth_sources_controller.rb (Arbeitskopie)
72 72
    end
73 73
    redirect_to :action => 'list'
74 74
  end
75
  
76
  def refresh_groups
77
    success_count = 0
78
    error_users = []
79
    User.active.find(:all, :conditions => ['auth_source_id = ?', params[:id]]).each do |u|
80
      if u.refresh_group_memberships.nil?
81
        error_users << u.login
82
      else
83
        success_count = success_count + 1
84
      end
85
    end
86
    if (error_users.size == 0)
87
      flash[:notice] = l(:text_group_refreshed, :count => success_count)
88
    else
89
      flash[:error] = l(:text_group_refresh_failed, :errors => error_users.size, :failures => error_users.join(", "), :success => success_count)
90
    end
91
    redirect_to :action => 'list'
92
  end
75 93

  
76 94
  def destroy
77 95
    @auth_source = AuthSource.find(params[:id])
app/views/auth_sources/list.rhtml (Arbeitskopie)
20 20
    <td align="center"><%= source.host %></td>    
21 21
    <td align="center"><%= source.users.count %></td>
22 22
    <td class="buttons">
23
    	<%= link_to l(:button_refresh), :action => 'refresh_groups', :id => source unless source.attr_groups.blank? %>
23 24
    	<%= link_to l(:button_test), :action => 'test_connection', :id => source %>
24 25
    	<%= link_to l(:button_delete), { :action => 'destroy', :id => source },
25 26
    																										:method => :post,
app/views/auth_sources/_form.rhtml (Arbeitskopie)
39 39

  
40 40
<p><label for="auth_source_attr_mail"><%=l(:field_mail)%></label>
41 41
<%= text_field 'auth_source', 'attr_mail', :size => 20  %></p>
42

  
43
<p><label for="auth_source_attr_groups"><%=l(:field_groups)%></label>
44
<%= text_field 'auth_source', 'attr_groups', :size => 20 %></p>
45

  
46
<p><label for="auth_source_attr_groups2"><%=l(:field_groups2)%></label>
47
<%= text_field 'auth_source', 'attr_groups2', :size => 20 %></p>
48

  
42 49
</fieldset>
50

  
51
<fieldset class="box"><legend><%=l(:label_group_option_plural)%></legend>
52
<p><label for="auth_source_group_prefix"><%=l(:field_prefix)%></label>
53
<%= text_field 'auth_source', 'group_prefix', :size => 20  %></p>
54

  
55
<p><label for="auth_source_cross_product"><%=l(:field_cross_product)%></label>
56
<%= check_box 'auth_source', 'cross_product' %></p>
57

  
58
<p><label for="auth_source_group_separator"><%=l(:field_group_separator)%></label>
59
<%= text_field 'auth_source', 'group_separator', :size => 20  %></p>
60
</fieldset>
61

  
43 62
<!--[eoform:auth_source]-->
44 63

  
app/views/groups/index.html.erb (Arbeitskopie)
13 13
  </tr></thead>
14 14
  <tbody>
15 15
<% @groups.each do |group| %>
16
  <tr class="<%= cycle 'odd', 'even' %>">
16
  <tr class="<%= cycle 'odd', 'even' %><%= group.auth_source_id.to_i == 0 ? '' : ' auto-group' %>">
17 17
    <td><%= link_to h(group), :action => 'edit', :id => group %></td>
18
    <td align="center"><%= group.users.size %></td>
18
    <td align="left"><%= group.size_and_updated_by_string %></td>
19 19
    <td class="buttons"><%= link_to l(:button_delete), group, :confirm => l(:text_are_you_sure), :method => :delete, :class => 'icon icon-del' %></td>
20 20
  </tr>
21 21
<% end %>
app/views/groups/_form.html.erb (Arbeitskopie)
1 1
<%= error_messages_for :group %>
2 2

  
3 3
<div class="box tabular">
4
	<p><%= f.text_field :lastname, :label => :field_name %></p>
4
	<p><%= f.text_field :lastname, :label => :field_name, :disabled => (@group.auth_source_id.to_i > 0) %> <%= @group.updated_by_string || "" %></p>
5 5
	<% @group.custom_field_values.each do |value| %>
6 6
	<p><%= custom_field_tag_with_label :group, value %></p>
7 7
  <% end %>
config/locales/en.yml (Arbeitskopie)
276 276
  field_content: Content
277 277
  field_group_by: Group results by
278 278
  field_sharing: Sharing
279
  field_groups: Organisation group
280
  field_prefix: Group name prefix
281
  field_groups2: Function group
282
  field_cross_product: Include combined function/organisation groups
283
  field_group_separator: Function/group separator
279 284
  
280 285
  setting_app_title: Application title
281 286
  setting_app_subtitle: Application subtitle
......
743 748
  label_api_access_key: API access key
744 749
  label_missing_api_access_key: Missing an API access key
745 750
  label_api_access_key_created_on: "API access key created {{value}} ago"
751
  label_group_option_plural: Grouping options
746 752
  
747 753
  button_login: Login
748 754
  button_submit: Submit
......
787 793
  button_quote: Quote
788 794
  button_duplicate: Duplicate
789 795
  button_show: Show
796
  button_refresh: Refresh groups
790 797
  
791 798
  status_active: active
792 799
  status_registered: registered
......
853 860
  text_wiki_page_destroy_children: "Delete child pages and all their descendants"
854 861
  text_wiki_page_reassign_children: "Reassign child pages to this parent page"
855 862
  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?"
856
  
863
  text_group_updated_by: "maintained by {{type}} authentication source {{name}}"
864
  text_group_refreshed: "Groups for {{count}} active users refreshed"
865
  text_group_refresh_failed: "Group refresh for {{errors}} failed: {{failures}} (updating for {{success}} active users successful)"
866
    
857 867
  default_role_manager: Manager
858 868
  default_role_developper: Developer
859 869
  default_role_reporter: Reporter
config/locales/de.yml (Arbeitskopie)
279 279
  field_default_value: Standardwert
280 280
  field_comments_sorting: Kommentare anzeigen
281 281
  field_parent_title: Übergeordnete Seite
282
  field_groups: Organisation für Gruppen
283
  field_prefix: Präfix für Gruppennamen
284
  field_groups2: Funktion für Gruppen
285
  field_cross_product: Erzeuge kombinierte Organisations-/Funktionsgruppen
286
  field_group_separator: Trenner zwischen Organisation und Funktion
282 287
  
283 288
  setting_app_title: Applikations-Titel
284 289
  setting_app_subtitle: Applikations-Untertitel
......
693 698
  label_generate_key: Generieren
694 699
  label_issue_watchers: Beobachter
695 700
  label_example: Beispiel
701
  label_group_option_plural: Gruppenoptionen
696 702
  
697 703
  button_login: Anmelden
698 704
  button_submit: OK
......
732 738
  button_update: Aktualisieren
733 739
  button_configure: Konfigurieren
734 740
  button_quote: Zitieren
741
  button_refresh: Gruppenzugehörigkeit aktualisieren
735 742
  
736 743
  status_active: aktiv
737 744
  status_registered: angemeldet
......
781 788
  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."
782 789
  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."
783 790
  text_diff_truncated: '... Dieser Diff wurde abgeschnitten, weil er die maximale Anzahl anzuzeigender Zeilen überschreitet.'
791
  text_group_updated_by: "verwaltet durch {{type}}-Anbindung {{name}}"
792
  text_group_refreshed: "Gruppenzugehörigkeit von {{count}} aktiven Benutzern aktualisiert"
793
  text_group_refresh_failed: "Gruppenzugehörigkeit der folgenden {{errors}} Benutzer fehlgeschlagen: {{failures}} (Aktualisierung für {{success}} aktive Benutzer erfolgreich)"
784 794
  
785 795
  default_role_manager: Manager
786 796
  default_role_developper: Entwickler
db/schema.rb (Arbeitskopie)
9 9
#
10 10
# It's strongly recommended to check this file into your version control system.
11 11

  
12
ActiveRecord::Schema.define(:version => 20091227112908) do
12
ActiveRecord::Schema.define(:version => 20100207220329) do
13 13

  
14 14
  create_table "attachments", :force => true do |t|
15 15
    t.integer  "container_id",                 :default => 0,  :null => false
......
43 43
    t.string  "attr_mail",         :limit => 30
44 44
    t.boolean "onthefly_register",               :default => false, :null => false
45 45
    t.boolean "tls",                             :default => false, :null => false
46
    t.string  "attr_groups",       :limit => 30, :default => "",    :null => false
47
    t.string  "group_prefix",      :limit => 30, :default => "_",   :null => false
48
    t.string  "attr_groups2",      :limit => 30, :default => "",    :null => false
49
    t.string  "group_separator",   :limit => 30, :default => ":",   :null => false
50
    t.boolean "cross_product",                   :default => false, :null => false
46 51
  end
47 52

  
48 53
  add_index "auth_sources", ["id", "type"], :name => "index_auth_sources_on_id_and_type"
......
473 478

  
474 479
  add_index "users", ["auth_source_id"], :name => "index_users_on_auth_source_id"
475 480
  add_index "users", ["id", "type"], :name => "index_users_on_id_and_type"
481
  add_index "users", [nil], :name => "index_users_on_lower_login"
476 482

  
477 483
  create_table "versions", :force => true do |t|
478 484
    t.integer  "project_id",      :default => 0,      :null => false
db/migrate/20100207220329_extend_ldap_groups.rb (Revision 67)
1
class ExtendLdapGroups < ActiveRecord::Migration
2
  def self.up
3
    add_column :auth_sources, :attr_groups2, :string, :limit => 30, :default => "", :null => false
4
    add_column :auth_sources, :group_separator, :string, :limit => 30, :default => ":", :null => false
5
    add_column :auth_sources, :cross_product, :boolean, :default => false, :null => false
6
  end
7

  
8
  def self.down
9
    remove_column :auth_sources, :attr_groups2
10
    remove_column :auth_sources, :group_separator
11
    remove_column :auth_sources, :cross_product
12
  end
13
end
db/migrate/20100204211355_add_ldap_group_support.rb (Revision 67)
1
class AddLdapGroupSupport < ActiveRecord::Migration
2
  def self.up
3
    add_column :auth_sources, :attr_groups, :string, :limit => 30, :default => "", :null => false
4
    add_column :auth_sources, :group_prefix, :string, :limit => 30, :default => "_", :null => false
5
  end
6

  
7
  def self.down
8
    remove_column :auth_sources, :attr_groups
9
    remove_column :auth_sources, :group_prefix
10
  end
11
end
(1-1/3)