Skip to content

Commit 0966525

Browse files
committed
first commit
0 parents  commit 0966525

File tree

11 files changed

+984
-0
lines changed

11 files changed

+984
-0
lines changed

LICENSE

+674
Large diffs are not rendered by default.

README.md

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
Redmin Ldap Sync
2+
================
3+
4+
This plugins extends redmine's ldap authentication to perform group synchronization.
5+
In addition it provides a rake task to perform full user group synchronization.
6+
7+
The following should be noted:
8+
9+
* The plugin has only been tested with Active Directory.
10+
* It detects and disables users that have been marked as disabled on LDAP (see [MS KB Article 305144][uacf] for more details).
11+
* An user will only be removed from groups that exist on LDAP. This means that both ldap and non-ldap groups can coexist.
12+
* Deleted groups on LDAP will not be deleted on redmine.
13+
14+
Installation
15+
------------
16+
17+
Follow the plugin installation procedure described at www.redmine.org/wiki/redmine/Plugins
18+
19+
Usage
20+
-----
21+
22+
### Configuration
23+
24+
Open Administration > Plugins and on the plugin configuration page you'll be able to set for each LDAP authentication:
25+
26+
* *Active* - Enable/Disable user/group synchronization for this LDAP authentication
27+
* *Group base DN* - The path to where the groups located. Eg, `ou=people,dc=smokeyjoe,dc=com`
28+
* *Group name* - The ldap attribute from where to fetch the group's name. Eg, `sAMAccountName`
29+
* *Group regex filter* - (optional) An RegExp that should match up with the name of the groups that should be imported. Eg, `\.team$`.
30+
* *Domain group* - (optional) A group to wich all the users created from this LDAP authentication will added upon creation. The group should not exist on LDAP.
31+
32+
### Full user/group synchronization with rake
33+
34+
To do the full user synchronization execute the following:
35+
36+
rake redmine:plugins:redmine_ldap_sync:sync_users RAILS_ENV=production
37+
38+
39+
An alternative is to do it periodically with a cron task:
40+
41+
# Synchronize users with ldap @ every 60 minutes
42+
35 * * * * root /usr/bin/rake -f /opt/redmine/Rakefile --silent redmine:plugins:redmine_ldap_sync:sync_users RAILS_ENV=production
43+
44+
License
45+
-------
46+
This plugin is released under the GPL v3 license. See LICENSE for more information.
47+
48+
[uacf]: http://support.microsoft.com/kb/305144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<% AuthSourceLdap.all.each do |ldap| -%>
2+
<fieldset class="collapsible">
3+
<legend onclick="toggleFieldset(this);"><%= ldap.name %></legend>
4+
<div>
5+
<p>
6+
<label><%= l(:ldap_domain_label_active)%></label>
7+
<%= check_box_tag "settings[#{ldap.name}][active]", 'yes', (@settings[ldap.name][:active] if @settings[ldap.name]) %>
8+
</p>
9+
10+
<p>
11+
<label><%= l(:ldap_domain_label_groups_base_dn) %> <span class="required">*</span></label>
12+
<%= text_field_tag "settings[#{ldap.name}][groups_base_dn]", (@settings[ldap.name][:groups_base_dn] if @settings[ldap.name]), :size => 60 %>
13+
</p>
14+
15+
<p>
16+
<label><%= l(:ldap_domain_label_attr_groupname) %> <span class="required">*</span></label>
17+
<%= text_field_tag "settings[#{ldap.name}][attr_groupname]", (@settings[ldap.name][:attr_groupname] if @settings[ldap.name]), :size => 15 %>
18+
</p>
19+
20+
<p>
21+
<label><%= l(:ldap_domain_label_groupname_filter) %></label>
22+
<%= text_field_tag "settings[#{ldap.name}][groupname_filter]", (@settings[ldap.name][:groupname_filter] if @settings[ldap.name]), :size => 15 %>
23+
</p>
24+
<p>
25+
<label><%= l(:ldap_domain_label_domain_group) %></label>
26+
<%= text_field_tag "settings[#{ldap.name}][domain_group]", (@settings[ldap.name][:domain_group] if @settings[ldap.name]), :size => 15 %>
27+
</p>
28+
</div>
29+
</fieldset>
30+
<%- end %>

config/locales/en.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
en:
2+
ldap_domain_label_active: "Active"
3+
ldap_domain_label_groups_base_dn: "Groups base DN"
4+
ldap_domain_label_attr_groupname: "Group name"
5+
ldap_domain_label_groupname_filter: "Group regex filter"
6+
ldap_domain_label_domain_group: "Domain group"

config/locales/es.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
es:
2+
ldap_domain_label_active: "Activo"
3+
ldap_domain_label_groups_base_dn: "Base DN de grupos"
4+
ldap_domain_label_attr_groupname: "Nombre del grupo"
5+
ldap_domain_label_groupname_filter: "Filtro regex de grupos"
6+
ldap_domain_label_domain_group: "Grupo del dominio"

config/locales/pt.yml

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pt:
2+
ldap_domain_label_active: "Activo"
3+
ldap_domain_label_groups_base_dn: "Base DN de grupos"
4+
ldap_domain_label_attr_groupname: "Nome do grupo"
5+
ldap_domain_label_groupname_filter: "Filtro regex de grupos"
6+
ldap_domain_label_domain_group: "Grupo do dominio"

init.rb

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
require 'redmine'
2+
require 'dispatcher'
3+
4+
RAILS_DEFAULT_LOGGER.info 'Starting Redmine Ldap Sync plugin for RedMine'
5+
6+
Redmine::Plugin.register :redmine_ldap_sync do
7+
name 'Redmine - Ldap Sync'
8+
author 'Ricardo Santos'
9+
author_url 'mailto:Ricardo Santos <[email protected]>?subject=redmine_ldap_sync'
10+
description 'Syncs users and groups with ldap'
11+
url 'https://github.com/thorin/redmine_ldap_sync'
12+
version '1.0.0'
13+
requires_redmine :version_or_higher => '1.1.0'
14+
15+
16+
settings :default => HashWithIndifferentAccess.new(), :partial => 'settings/ldap_sync_settings'
17+
end
18+
19+
Dispatcher.to_prepare :redmine_ldap_sync do
20+
unless AuthSourceLdap.include? RedmineLdapSync::RedmineExt::AuthSourceLdapPatch
21+
AuthSourceLdap.send(:include, RedmineLdapSync::RedmineExt::AuthSourceLdapPatch)
22+
end
23+
unless User.include? RedmineLdapSync::RedmineExt::UserPatch
24+
User.send(:include, RedmineLdapSync::RedmineExt::UserPatch)
25+
end
26+
end
27+
28+
# Hooks
29+
require 'redmine_ldap_sync/redmine_hooks'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
module RedmineLdapSync
2+
module RedmineExt
3+
module AuthSourceLdapPatch
4+
def self.included(base)
5+
base.class_eval do
6+
7+
public
8+
def member_of?(login, group)
9+
ldap_con = initialize_ldap_con(self.account, self.account_password)
10+
login_filter = Net::LDAP::Filter.eq( self.attr_login, login )
11+
user_filter = Net::LDAP::Filter.eq( 'objectClass', 'user' )
12+
13+
ldap_con.search(:base => self.base_dn,
14+
:filter => user_filter & login_filter,
15+
:attributes => ['memberof'],
16+
:return_result => false) do |entry|
17+
return entry['memberof'].include? group
18+
end
19+
20+
end
21+
22+
23+
def sync_groups(user)
24+
return unless ldapsync_active?
25+
26+
changes = groups_changes(user)
27+
changes[:added].each do |groupname|
28+
next if user.groups.detect { |g| g.to_s == groupname }
29+
30+
group = Group.find_by_lastname(groupname)
31+
group = Group.create(:lastname => groupname, :auth_source_id => self.id) unless group
32+
group.users << user
33+
end
34+
35+
changes[:deleted].each do |groupname|
36+
next unless group = user.groups.detect { |g| g.to_s == groupname }
37+
38+
group.users.delete(user)
39+
end
40+
end
41+
42+
def sync_users
43+
return unless ldapsync_active?
44+
45+
ldap_users[:disabled].each do |login|
46+
user = User.find_by_login(login)
47+
48+
user.lock! if user
49+
end
50+
ldap_users[:enabled].each do |login|
51+
user = User.find_by_login(login)
52+
53+
unless user
54+
attrs = get_user_dn(login)
55+
user = User.create(attrs.except(:dn)) do |user|
56+
user.login = login
57+
user.language = Setting.default_language
58+
end
59+
end
60+
61+
sync_groups(user)
62+
end
63+
end
64+
65+
protected
66+
def ldap_users
67+
ldap_con = initialize_ldap_con(self.account, self.account_password)
68+
user_filter = Net::LDAP::Filter.eq( 'objectClass', 'user' )
69+
attr_enabled = 'userAccountControl'
70+
users = {:enabled => [], :disabled => []}
71+
72+
ldap_con.search(:base => self.base_dn,
73+
:filter => user_filter,
74+
:attributes => [self.attr_login, attr_enabled],
75+
:return_result => false) do |entry|
76+
if entry[attr_enabled][0].to_i & 2 == 0
77+
users[:enabled] << entry[self.attr_login][0]
78+
else
79+
users[:disabled] << entry[self.attr_login][0]
80+
end
81+
end
82+
83+
users
84+
end
85+
86+
def groups_changes(user)
87+
return unless ldapsync_active?
88+
changes = { :added => [], :deleted => [] }
89+
90+
ldap_con = initialize_ldap_con(self.account, self.account_password)
91+
login_filter = Net::LDAP::Filter.eq( self.attr_login, user.login )
92+
user_filter = Net::LDAP::Filter.eq( 'objectClass', 'user' )
93+
group_filter = Net::LDAP::Filter.eq( 'objectClass', 'group' )
94+
attr_groupname = Setting.plugin_redmine_ldap_sync[self.name][:attr_groupname]
95+
groupname_filter = /#{Setting.plugin_redmine_ldap_sync[self.name][:groupname_filter]}/
96+
groups_base_dn = Setting.plugin_redmine_ldap_sync[self.name][:groups_base_dn]
97+
98+
# Faster, but requires all groups to be added to redmine with sync_groups
99+
#changes[:deleted] = user.groups.reject{|g| g.auth_source_id != self.id}.map(&:to_s) if user.groups
100+
ldap_con.open do |ldap|
101+
user_groups = user.groups.select {|g| groupname_filter =~ g.to_s}
102+
names_filter = user_groups.map {|g| Net::LDAP::Filter.eq( attr_groupname, g.to_s )}.reduce(:|)
103+
ldap.search(:base => groups_base_dn,
104+
:filter => group_filter & names_filter,
105+
:attributes => [attr_groupname],
106+
:return_result => false) do |entry|
107+
changes[:deleted] << entry[attr_groupname][0]
108+
end if names_filter
109+
110+
groups = []
111+
ldap.search(:base => self.base_dn,
112+
:filter => user_filter & login_filter,
113+
:attributes => ['memberof'],
114+
:return_result => false) do |entry|
115+
groups = entry['memberof'].select {|g| g.end_with?(groups_base_dn)}
116+
end
117+
118+
names_filter = groups.map{|g| Net::LDAP::Filter.eq( 'distinguishedName', g )}.reduce(:|)
119+
ldap.search(:base => groups_base_dn,
120+
:filter => group_filter & names_filter,
121+
:attributes => [attr_groupname],
122+
:return_result => false) do |entry|
123+
group = entry[attr_groupname][0]
124+
changes[:added] << group if groupname_filter =~ group
125+
end if names_filter
126+
end
127+
128+
changes[:deleted].reject! {|g| changes[:added].include?(g)}
129+
130+
changes
131+
end
132+
133+
def ldapsync_active?
134+
Setting.plugin_redmine_ldap_sync[self.name].present? && Setting.plugin_redmine_ldap_sync[self.name][:active]
135+
end
136+
end
137+
end
138+
end
139+
end
140+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
module RedmineLdapSync
2+
module RedmineExt
3+
module UserPatch
4+
def self.included(base)
5+
base.class_eval do
6+
after_create :add_to_domain_group
7+
8+
def add_to_domain_group
9+
return unless auth_source && auth_source.auth_method_name == 'LDAP'
10+
11+
group_name = Setting.plugin_redmine_ldap_sync[auth_source.name][:domain_group]
12+
return unless group_name.present?
13+
14+
domain_group = Group.find_by_lastname(group_name)
15+
domain_group = Group.create(:lastname => group_name) unless domain_group
16+
domain_group.users << self
17+
18+
save
19+
end
20+
21+
end
22+
end
23+
end
24+
end
25+
end
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
class RedmineLdapSyncRedmineHooks < Redmine::Hook::Listener
2+
def controller_account_success_authentication_after(context)
3+
user = context[:user]
4+
5+
if user.auth_source && user.auth_source.auth_method_name == 'LDAP'
6+
user.auth_source.sync_groups(user)
7+
end
8+
end
9+
end

lib/tasks/sync_users.rake

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
namespace :redmine do
2+
namespace :plugins do
3+
namespace :redmine_ldap_sync do
4+
desc "Synchronize redmine's users and groups with those on LDAP"
5+
task :sync_users => :environment do
6+
AuthSourceLdap.all.each {|as| as.sync_users}
7+
end
8+
end
9+
end
10+
end
11+

0 commit comments

Comments
 (0)