Remove referenced read access when teams are removed
[gitorious:thomas-mainline.git] / app / models / ldap_group.rb
1 # encoding: utf-8
2 #--
3 #   Copyright (C) 2012 Gitorious AS
4 #
5 #   This program is free software: you can redistribute it and/or modify
6 #   it under the terms of the GNU Affero General Public License as published by
7 #   the Free Software Foundation, either version 3 of the License, or
8 #   (at your option) any later version.
9 #
10 #   This program is distributed in the hope that it will be useful,
11 #   but WITHOUT ANY WARRANTY; without even the implied warranty of
12 #   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 #   GNU Affero General Public License for more details.
14 #
15 #   You should have received a copy of the GNU Affero General Public License
16 #   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 #++
18
19 require "net/ldap"
20 require "finders/group_finder"
21 require "finders/ldap_group_finder"
22 class LdapGroup < ActiveRecord::Base
23   extend GroupBehavior
24   include GroupBehavior::InstanceMethods
25
26   Paperclip.interpolates('group_name'){|attachment,style| attachment.instance.name}
27
28   avatar_local_path = '/system/group_avatars/:group_name/:style/:basename.:extension'
29   has_attached_file :avatar,
30     :default_url  =>'/images/default_group_avatar.png',
31     :styles => { :normal => "300x300>", :medium => "64x64>", :thumb => '32x32>', :icon => '16x16>' },
32     :url => avatar_local_path,
33     :path => ":rails_root/public#{avatar_local_path}"
34
35
36   serialize :member_dns
37
38   validate :validate_ldap_dns
39   
40   def validate_ldap_dns
41     configurator = self.class.ldap_configurator
42     Gitorious::Authorization::LDAP::Connection.new({
43         :host => configurator.server,
44         :port => configurator.port,
45         :encryption => configurator.encryption}).bind_as(configurator.bind_username, configurator.bind_password) do |connection|
46       Array(member_dns).each do |dn|
47         if ldap_dn_in_base?(dn, configurator.group_search_dn)
48           errors.add(:member_dns, "LDAP DN #{dn} is part of the LDAP search base #{configurator.group_search_dn}")
49         end
50         result = connection.search(
51           :base => configurator.group_search_dn,
52           :filter => generate_ldap_filters_from_dn(dn),
53           :return_result => true)
54         errors.add(:member_dns, "#{dn} not found") if result.empty?
55       end
56     end
57   end
58
59   # We don't want member DNs to contain the base DN to search
60   def ldap_dn_in_base?(dn, base)
61     dn =~ /#{base}/
62   end
63
64   def generate_ldap_filters_from_dn(dn)
65     filters = dn.split(",").map do |pair|
66       attribute, value = pair.split("=")
67       Net::LDAP::Filter.eq(attribute, value)
68     end
69     filters.inject(filters.shift) do |memo, obj|
70       memo & obj
71     end
72   end
73
74   def ldap_group_names
75     Array(member_dns).join("\n")
76   end
77
78   def ldap_group_names=(newline_separated_list)
79     self.member_dns = newline_separated_list.split("\n")
80   end
81   
82   def members
83     []
84   end
85   
86   def to_param
87     name
88   end
89
90   def breadcrumb_parent
91     nil
92   end
93   
94   def title
95     name
96   end
97
98   def deletable?
99     projects.empty?
100   end
101
102   def user_role(candidate)
103     if candidate == creator
104       Role.admin
105     end
106   end
107
108   def self.ldap_configurator
109     auth_configuration_path = File.join(Rails.root, "config", "authentication.yml")
110     configuration = YAML::load_file(auth_configuration_path)[RAILS_ENV]["methods"].detect do |m|
111       m["adapter"] == "Gitorious::Authentication::LDAPAuthentication"
112     end
113     raise Gitorious::Authorization::LDAP::LdapError, "No LDAP configuration found for current environment (#{Rails.env}) in #{auth_configuration_path}" unless configuration
114     Gitorious::Authentication::LDAPConfigurator.new(configuration)
115   end
116
117   def self.ldap_group_names_for_user(user)
118     return [] unless user.is_a?(User)
119     configurator = ldap_configurator
120     membership_attribute = ldap_configurator.membership_attribute_name
121     Gitorious::Authorization::LDAP::Connection.new({
122         :host => configurator.server,
123         :port => configurator.port,
124         :encryption => configurator.encryption}).bind_as(configurator.bind_username, configurator.bind_password) do |connection|
125       entries = connection.search(
126         :base => configurator.group_search_dn,
127         :filter => Net::LDAP::Filter.eq(configurator.login_attribute, user.login),
128         :attributes => [membership_attribute])
129       if !entries.blank?
130         return entries.first[membership_attribute]
131       end
132     end
133   end
134
135   def member?(user)
136     self.class.groups_for_user(user).include?(self)
137   end
138
139   # Do an LDAP lookup for all member DNs in a given group
140   def self.user_dns_in_group(group_name, member_attribute_name)
141     Rails.cache.fetch(["ldap_group", "members", group_name], :expires_in => 60.minutes) do
142       uncached_dns_in_group(group_name, member_attribute_name)
143     end
144   end
145
146   def self.uncached_dns_in_group(group_name, member_attribute_name)
147     configurator = ldap_configurator
148     attribute, value = group_name.split("=")
149     Gitorious::Authorization::LDAP::Connection.new({
150         :host => configurator.server,
151         :port => configurator.port,
152         :encryption => configurator.encryption}).bind_as(configurator.bind_username, configurator.bind_password) do |connection|
153       entries = connection.search(
154         :base => configurator.group_search_dn,
155         :filter => Net::LDAP::Filter.eq(attribute, value),
156         :attributes => [member_attribute_name])
157       if !entries.blank?
158         return entries.first[member_attribute_name]
159       end    
160     end
161   end
162
163   class MembershipsWrapper
164     def initialize(members)
165       @members = members
166     end
167
168     def paginate(which, options={})
169       self
170     end
171
172     def total_pages
173       1
174     end
175     
176     def each(&blk)
177       @members.each {|m| yield m}
178     end
179
180     def count
181       @members.count
182     end
183   end
184   
185   def memberships
186     memberships = members.map do |member|
187       Membership.new(:user_id => member.id, :created_at => Time.now, :role => Role.member)
188     end
189     MembershipsWrapper.new(memberships)
190   end
191
192   # Load all Users who are members of this group
193   # Nested groups are not supported, only entries with a [login_attribute] 
194   # value matching a User with the given username will be returned.
195   def members
196     configurator = self.class.ldap_configurator
197     usernames = member_dns.map do |dn|
198       self.class.user_dns_in_group(dn, configurator.members_attribute_name)
199     end
200
201     usernames.compact.flatten.map do |dn|
202       username = dn.split(",").detect do |pair|
203         k,v = pair.split("=")
204         v if k == configurator.login_attribute
205       end
206       attr, username = dn.split(",").first.split("=")
207       User.find_by_login(Gitorious::Authentication::LDAPConfigurator.transform_username(username))
208     end.compact.uniq
209   end
210
211   def self.build_qualified_dn(user_spec)
212     [user_spec, ldap_configurator.group_search_dn].compact.join(",")
213   end
214
215   def self.groups_for_user(user)
216     ldap_group_names = ldap_group_names_for_user(user)    
217     return [] if ldap_group_names.blank?
218     result = []
219     all.each do |group|
220       dns_in_group = group.member_dns.map {|dn| build_qualified_dn(dn)}
221       ldap_group_names.map do |name|
222         if dns_in_group.include?(name)
223           result << group
224         end
225       end
226     end
227     
228     result.compact
229   end
230 end