Show More
@@ -0,0 +1,11 b'' | |||||
|
1 | [default] | |||
|
2 | api_url = http://your.rhodecode.server:5000/_admin/api | |||
|
3 | api_user = admin | |||
|
4 | api_key = XXXXXXXXXXXX | |||
|
5 | ||||
|
6 | ldap_uri = ldap://your.ldap.server:389 | |||
|
7 | ldap_user = cn=rhodecode,ou=binders,dc=linaro,dc=org | |||
|
8 | ldap_key = XXXXXXXXX | |||
|
9 | base_dn = dc=linaro,dc=org | |||
|
10 | ||||
|
11 | sync_users = True No newline at end of file |
@@ -0,0 +1,237 b'' | |||||
|
1 | # This program is free software: you can redistribute it and/or modify | |||
|
2 | # it under the terms of the GNU General Public License as published by | |||
|
3 | # the Free Software Foundation, either version 3 of the License, or | |||
|
4 | # (at your option) any later version. | |||
|
5 | # | |||
|
6 | # This program is distributed in the hope that it will be useful, | |||
|
7 | # but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
|
8 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |||
|
9 | # GNU General Public License for more details. | |||
|
10 | # | |||
|
11 | # You should have received a copy of the GNU General Public License | |||
|
12 | # along with this program. If not, see <http://www.gnu.org/licenses/>. | |||
|
13 | ||||
|
14 | import ldap | |||
|
15 | import urllib2 | |||
|
16 | import uuid | |||
|
17 | import json | |||
|
18 | ||||
|
19 | from ConfigParser import ConfigParser | |||
|
20 | ||||
|
21 | config = ConfigParser() | |||
|
22 | config.read('ldap_sync.conf') | |||
|
23 | ||||
|
24 | ||||
|
25 | class InvalidResponseIDError(Exception): | |||
|
26 | """ Request and response don't have the same UUID. """ | |||
|
27 | ||||
|
28 | ||||
|
29 | class RhodecodeResponseError(Exception): | |||
|
30 | """ Response has an error, something went wrong with request execution. """ | |||
|
31 | ||||
|
32 | ||||
|
33 | class UserAlreadyInGroupError(Exception): | |||
|
34 | """ User is already a member of the target group. """ | |||
|
35 | ||||
|
36 | ||||
|
37 | class UserNotInGroupError(Exception): | |||
|
38 | """ User is not a member of the target group. """ | |||
|
39 | ||||
|
40 | ||||
|
41 | class RhodecodeAPI(): | |||
|
42 | ||||
|
43 | def __init__(self, url, key): | |||
|
44 | self.url = url | |||
|
45 | self.key = key | |||
|
46 | ||||
|
47 | def get_api_data(self, uid, method, args): | |||
|
48 | """Prepare dict for API post.""" | |||
|
49 | return { | |||
|
50 | "id": uid, | |||
|
51 | "api_key": self.key, | |||
|
52 | "method": method, | |||
|
53 | "args": args | |||
|
54 | } | |||
|
55 | ||||
|
56 | def rhodecode_api_post(self, method, args): | |||
|
57 | """Send a generic API post to Rhodecode. | |||
|
58 | ||||
|
59 | This will generate the UUID for validation check after the | |||
|
60 | response is returned. Handle errors and get the result back. | |||
|
61 | """ | |||
|
62 | uid = str(uuid.uuid1()) | |||
|
63 | data = self.get_api_data(uid, method, args) | |||
|
64 | ||||
|
65 | data = json.dumps(data) | |||
|
66 | headers = {'content-type': 'text/plain'} | |||
|
67 | req = urllib2.Request(self.url, data, headers) | |||
|
68 | ||||
|
69 | response = urllib2.urlopen(req) | |||
|
70 | response = json.load(response) | |||
|
71 | ||||
|
72 | if uid != response["id"]: | |||
|
73 | raise InvalidResponseIDError("UUID does not match.") | |||
|
74 | ||||
|
75 | if response["error"] != None: | |||
|
76 | raise RhodecodeResponseError(response["error"]) | |||
|
77 | ||||
|
78 | return response["result"] | |||
|
79 | ||||
|
80 | def create_group(self, name, active=True): | |||
|
81 | """Create the Rhodecode user group.""" | |||
|
82 | args = { | |||
|
83 | "group_name": name, | |||
|
84 | "active": str(active) | |||
|
85 | } | |||
|
86 | self.rhodecode_api_post("create_users_group", args) | |||
|
87 | ||||
|
88 | def add_membership(self, group, username): | |||
|
89 | """Add specific user to a group.""" | |||
|
90 | args = { | |||
|
91 | "usersgroupid": group, | |||
|
92 | "userid": username | |||
|
93 | } | |||
|
94 | result = self.rhodecode_api_post("add_user_to_users_group", args) | |||
|
95 | if not result["success"]: | |||
|
96 | raise UserAlreadyInGroupError("User %s already in group %s." % | |||
|
97 | (username, group)) | |||
|
98 | ||||
|
99 | def remove_membership(self, group, username): | |||
|
100 | """Remove specific user from a group.""" | |||
|
101 | args = { | |||
|
102 | "usersgroupid": group, | |||
|
103 | "userid": username | |||
|
104 | } | |||
|
105 | result = self.rhodecode_api_post("remove_user_from_users_group", args) | |||
|
106 | if not result["success"]: | |||
|
107 | raise UserNotInGroupError("User %s not in group %s." % | |||
|
108 | (username, group)) | |||
|
109 | ||||
|
110 | def get_group_members(self, name): | |||
|
111 | """Get the list of member usernames from a user group.""" | |||
|
112 | args = {"usersgroupid": name} | |||
|
113 | members = self.rhodecode_api_post("get_users_group", args)['members'] | |||
|
114 | member_list = [] | |||
|
115 | for member in members: | |||
|
116 | member_list.append(member["username"]) | |||
|
117 | return member_list | |||
|
118 | ||||
|
119 | def get_group(self, name): | |||
|
120 | """Return group info.""" | |||
|
121 | args = {"usersgroupid": name} | |||
|
122 | return self.rhodecode_api_post("get_users_group", args) | |||
|
123 | ||||
|
124 | def get_user(self, username): | |||
|
125 | """Return user info.""" | |||
|
126 | args = {"userid": username} | |||
|
127 | return self.rhodecode_api_post("get_user", args) | |||
|
128 | ||||
|
129 | ||||
|
130 | class LdapClient(): | |||
|
131 | ||||
|
132 | def __init__(self, uri, user, key, base_dn): | |||
|
133 | self.client = ldap.initialize(uri, trace_level=0) | |||
|
134 | self.client.set_option(ldap.OPT_REFERRALS, 0) | |||
|
135 | self.client.simple_bind(user, key) | |||
|
136 | self.base_dn = base_dn | |||
|
137 | ||||
|
138 | def __del__(self): | |||
|
139 | self.client.unbind() | |||
|
140 | ||||
|
141 | def get_groups(self): | |||
|
142 | """Get all the groups in form of dict {group_name: group_info,...}.""" | |||
|
143 | searchFilter = "objectClass=groupOfUniqueNames" | |||
|
144 | result = self.client.search_s(self.base_dn, ldap.SCOPE_SUBTREE, | |||
|
145 | searchFilter) | |||
|
146 | ||||
|
147 | groups = {} | |||
|
148 | for group in result: | |||
|
149 | groups[group[1]['cn'][0]] = group[1] | |||
|
150 | ||||
|
151 | return groups | |||
|
152 | ||||
|
153 | def get_group_users(self, groups, group): | |||
|
154 | """Returns all the users belonging to a single group. | |||
|
155 | ||||
|
156 | Based on the list of groups and memberships, returns all the | |||
|
157 | users belonging to a single group, searching recursively. | |||
|
158 | """ | |||
|
159 | users = [] | |||
|
160 | for member in groups[group]["uniqueMember"]: | |||
|
161 | member = self.parse_member_string(member) | |||
|
162 | if member[0] == "uid": | |||
|
163 | users.append(member[1]) | |||
|
164 | elif member[0] == "cn": | |||
|
165 | users += self.get_group_users(groups, member[1]) | |||
|
166 | ||||
|
167 | return users | |||
|
168 | ||||
|
169 | def parse_member_string(self, member): | |||
|
170 | """Parses the member string and returns a touple of type and name. | |||
|
171 | ||||
|
172 | Unique member can be either user or group. Users will have 'uid' as | |||
|
173 | prefix while groups will have 'cn'. | |||
|
174 | """ | |||
|
175 | member = member.split(",")[0] | |||
|
176 | return member.split('=') | |||
|
177 | ||||
|
178 | ||||
|
179 | class LdapSync(object): | |||
|
180 | ||||
|
181 | def __init__(self): | |||
|
182 | self.ldap_client = LdapClient(config.get("default", "ldap_uri"), | |||
|
183 | config.get("default", "ldap_user"), | |||
|
184 | config.get("default", "ldap_key"), | |||
|
185 | config.get("default", "base_dn")) | |||
|
186 | self.rhodocode_api = RhodecodeAPI(config.get("default", "api_url"), | |||
|
187 | config.get("default", "api_key")) | |||
|
188 | ||||
|
189 | def update_groups_from_ldap(self): | |||
|
190 | """Add all the groups from LDAP to Rhodecode.""" | |||
|
191 | added = existing = 0 | |||
|
192 | groups = self.ldap_client.get_groups() | |||
|
193 | for group in groups: | |||
|
194 | try: | |||
|
195 | self.rhodecode_api.create_group(group) | |||
|
196 | added += 1 | |||
|
197 | except Exception: | |||
|
198 | existing += 1 | |||
|
199 | ||||
|
200 | return added, existing | |||
|
201 | ||||
|
202 | def update_memberships_from_ldap(self, group): | |||
|
203 | """Update memberships in rhodecode based on the LDAP groups.""" | |||
|
204 | groups = self.ldap_client.get_groups() | |||
|
205 | group_users = self.ldap_client.get_group_users(groups, group) | |||
|
206 | ||||
|
207 | # Delete memberships first from each group which are not part | |||
|
208 | # of the group any more. | |||
|
209 | rhodecode_members = self.rhodecode_api.get_group_members(group) | |||
|
210 | for rhodecode_member in rhodecode_members: | |||
|
211 | if rhodecode_member not in group_users: | |||
|
212 | try: | |||
|
213 | self.rhodocode_api.remove_membership(group, | |||
|
214 | rhodecode_member) | |||
|
215 | except UserNotInGroupError: | |||
|
216 | pass | |||
|
217 | ||||
|
218 | # Add memberships. | |||
|
219 | for member in group_users: | |||
|
220 | try: | |||
|
221 | self.rhodecode_api.add_membership(group, member) | |||
|
222 | except UserAlreadyInGroupError: | |||
|
223 | # TODO: handle somehow maybe.. | |||
|
224 | pass | |||
|
225 | ||||
|
226 | ||||
|
227 | if __name__ == '__main__': | |||
|
228 | sync = LdapSync() | |||
|
229 | print sync.update_groups_from_ldap() | |||
|
230 | ||||
|
231 | for gr in sync.ldap_client.get_groups(): | |||
|
232 | # TODO: exception when user does not exist during add membership... | |||
|
233 | # How should we handle this.. Either sync users as well at this step, | |||
|
234 | # or just ignore those who don't exist. If we want the second case, | |||
|
235 | # we need to find a way to recognize the right exception (we always get | |||
|
236 | # RhodecodeResponseError with no error code so maybe by return msg (?) | |||
|
237 | sync.update_memberships_from_ldap(gr) |
General Comments 0
You need to be logged in to leave comments.
Login now