ucb_confluence 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.svnignore +6 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +37 -0
- data/INTERNALS.md +12 -0
- data/README.md +41 -0
- data/Rakefile +21 -0
- data/TODO.md +8 -0
- data/bin/ucb_confluence +84 -0
- data/config/.svnignore +1 -0
- data/config/config.skel.yml +21 -0
- data/lib/confluence.rb +57 -0
- data/lib/confluence/config.rb +31 -0
- data/lib/confluence/conn.rb +35 -0
- data/lib/confluence/group.rb +56 -0
- data/lib/confluence/jobs/disable_expired_users.rb +87 -0
- data/lib/confluence/jobs/ist_ldap_sync.rb +154 -0
- data/lib/confluence/user.rb +312 -0
- data/lib/confluence/version.rb +3 -0
- data/spec/confluence/config_spec.rb +18 -0
- data/spec/confluence/confluence_spec.rb +21 -0
- data/spec/confluence/conn_spec.rb +9 -0
- data/spec/confluence/group_spec.rb +45 -0
- data/spec/confluence/jobs/disable_expired_users_spec.rb +51 -0
- data/spec/confluence/jobs/ist_ldap_sync_spec.rb +77 -0
- data/spec/confluence/user_spec.rb +221 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +12 -0
- data/ucb_confluence.gemspec +31 -0
- metadata +197 -0
@@ -0,0 +1,154 @@
|
|
1
|
+
|
2
|
+
##
|
3
|
+
# Sync our confluence instance with LDAP so people in LDAP that are part
|
4
|
+
# of IST will be part of the ucb-ist group in confluence
|
5
|
+
#
|
6
|
+
module Confluence
|
7
|
+
module Jobs
|
8
|
+
class IstLdapSync
|
9
|
+
|
10
|
+
IST_GROUP = 'ucb-ist'
|
11
|
+
|
12
|
+
def initialize()
|
13
|
+
@new_users = []
|
14
|
+
@modified_users = []
|
15
|
+
end
|
16
|
+
|
17
|
+
##
|
18
|
+
# Run the job
|
19
|
+
#
|
20
|
+
def execute()
|
21
|
+
@new_users.clear()
|
22
|
+
@modified_users.clear()
|
23
|
+
sync_ist_from_ldap()
|
24
|
+
sync_ist_from_confluence()
|
25
|
+
log_job()
|
26
|
+
end
|
27
|
+
|
28
|
+
##
|
29
|
+
# If the IST LDAP person is not in confluence, add them. If they are in
|
30
|
+
# confluence but not part of the IST_GROUP, give them membership.
|
31
|
+
#
|
32
|
+
def sync_ist_from_ldap()
|
33
|
+
ist_people.each do |ldap_person|
|
34
|
+
next unless eligible_for_confluence?(ldap_person)
|
35
|
+
|
36
|
+
user = find_or_new_user(ldap_person.uid())
|
37
|
+
|
38
|
+
if user.new_record?
|
39
|
+
user.save()
|
40
|
+
user.join_group(Confluence::User::DEFAULT_GROUP)
|
41
|
+
@new_users << user
|
42
|
+
end
|
43
|
+
|
44
|
+
unless user.groups.include?(IST_GROUP)
|
45
|
+
user.join_group(IST_GROUP)
|
46
|
+
@modified_users << user
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Remove a confluene user from the IST_GROUP if LDAP indicates they are
|
53
|
+
# no longer part of IST
|
54
|
+
#
|
55
|
+
def sync_ist_from_confluence()
|
56
|
+
confluence_user_names.each do |name|
|
57
|
+
next if name == "conflusa"
|
58
|
+
|
59
|
+
ldap_person = find_in_ldap(name)
|
60
|
+
next if ldap_person.nil?
|
61
|
+
|
62
|
+
if !in_ist?(ldap_person)
|
63
|
+
user = find_in_confluence(name)
|
64
|
+
next if user.nil?
|
65
|
+
user.leave_group(IST_GROUP)
|
66
|
+
@modified_users << user
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def log_job()
|
72
|
+
msg = "#{self.class.name}\n\n"
|
73
|
+
|
74
|
+
msg.concat("Modified Users\n\n")
|
75
|
+
@modified_users.each { |u| msg.concat(u) }
|
76
|
+
msg.concat("\n")
|
77
|
+
|
78
|
+
msg.concat("New Users\n\n")
|
79
|
+
@new_users.each { |u| msg.concat(u) }
|
80
|
+
msg.concat("\n")
|
81
|
+
|
82
|
+
logger.info(msg)
|
83
|
+
end
|
84
|
+
|
85
|
+
def logger()
|
86
|
+
Confluence.logger
|
87
|
+
end
|
88
|
+
|
89
|
+
##
|
90
|
+
# @return [Array<String>] confluence user names.
|
91
|
+
#
|
92
|
+
def confluence_user_names()
|
93
|
+
Confluence::User.active.map(&:name)
|
94
|
+
end
|
95
|
+
|
96
|
+
##
|
97
|
+
# All of the people in IST.
|
98
|
+
#
|
99
|
+
# @return [Array<UCB::LDAP::Person>]
|
100
|
+
#
|
101
|
+
def ist_people(str = "UCBKL-AVCIS-VRIST-*")
|
102
|
+
UCB::LDAP::Person.search(:filter => {"berkeleyedudeptunithierarchystring" => str})
|
103
|
+
end
|
104
|
+
|
105
|
+
##
|
106
|
+
# Retrieves the user if they already exist in Confluence. Otherwise,
|
107
|
+
# returns a new record that has not yet been persisted to Confluence.
|
108
|
+
#
|
109
|
+
# @param [String] the user's ldap uid
|
110
|
+
# @return [Confluence::User]
|
111
|
+
#
|
112
|
+
def find_or_new_user(ldap_uid)
|
113
|
+
Confluence::User.find_or_new_from_ldap(ldap_uid)
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# @param [String] user's confluence account name.
|
118
|
+
# @return [Confluence::User, nil]
|
119
|
+
#
|
120
|
+
def find_in_confluence(name)
|
121
|
+
Confluence::User.find_by_name(name)
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# @param [String] user's ldap uid
|
126
|
+
# @return [UCB::LDAP::Person, nil]
|
127
|
+
#
|
128
|
+
def find_in_ldap(ldap_uid)
|
129
|
+
UCB::LDAP::Person.find_by_uid(ldap_uid)
|
130
|
+
end
|
131
|
+
|
132
|
+
def in_ist?(person)
|
133
|
+
person.berkeleyEduDeptUnitHierarchyString.each do |str|
|
134
|
+
return true if str =~ /UCBKL-AVCIS-VRIST-.*/
|
135
|
+
end
|
136
|
+
false
|
137
|
+
end
|
138
|
+
|
139
|
+
def eligible_for_confluence?(person)
|
140
|
+
valid_affiliations = person.affiliations.inject([]) do |accum, aff|
|
141
|
+
if aff =~ /AFFILIATE-TYPE.*(ALUMNUS|RETIREE|EXPIRED)/
|
142
|
+
accum
|
143
|
+
elsif aff =~ /AFFILIATE-TYPE.*/
|
144
|
+
accum << aff
|
145
|
+
end
|
146
|
+
accum
|
147
|
+
end
|
148
|
+
|
149
|
+
person.employee? || !valid_affiliations.empty?
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
@@ -0,0 +1,312 @@
|
|
1
|
+
module Confluence
|
2
|
+
class User
|
3
|
+
DISABLED_SUFFIX = "(ACCOUNT DISABLED)"
|
4
|
+
DEFAULT_GROUP = 'confluence-users'
|
5
|
+
VALID_ATTRS = [:name, :fullname, :email]
|
6
|
+
|
7
|
+
class LdapPersonNotFound < StandardError; end;
|
8
|
+
|
9
|
+
attr_accessor :name, :fullname, :email
|
10
|
+
|
11
|
+
##
|
12
|
+
# Unrecognized attributes are ignored
|
13
|
+
#
|
14
|
+
def initialize(attrs = {})
|
15
|
+
@new_record = true
|
16
|
+
@errors = []
|
17
|
+
VALID_ATTRS.each do |attr|
|
18
|
+
self.send("#{attr}=", attrs[attr] || attrs[attr.to_s])
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.new_from_ldap(ldap_person)
|
23
|
+
@new_record = true
|
24
|
+
@errors = []
|
25
|
+
self.new({
|
26
|
+
:name => ldap_person.uid,
|
27
|
+
:fullname => "#{ldap_person.first_name} + #{ldap_person.last_name}",
|
28
|
+
:email => ldap_person.email || "test@berkeley.edu"
|
29
|
+
})
|
30
|
+
end
|
31
|
+
|
32
|
+
##
|
33
|
+
# Lets confluence XML-RPC access this object as if it was a Hash.
|
34
|
+
# returns nil if key is not in VALID_ATTRS
|
35
|
+
#
|
36
|
+
def [](key)
|
37
|
+
self.send(key) if VALID_ATTRS.include?(key.to_sym)
|
38
|
+
end
|
39
|
+
|
40
|
+
##
|
41
|
+
# Name can only be set if the user has not yet been saved to confluence
|
42
|
+
# users table. Once they have been saved, the name is immutable. This is
|
43
|
+
# a restriction enforced by Confluence's API.
|
44
|
+
#
|
45
|
+
def name=(n)
|
46
|
+
@name = n if new_record?
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Predicate that determines if this [User] record has been persisted.
|
51
|
+
#
|
52
|
+
# @return [true, false] evaluates to true if the record has not been
|
53
|
+
# persisted, evaluates to false if it has not been persisted.
|
54
|
+
#
|
55
|
+
def new_record?
|
56
|
+
@new_record
|
57
|
+
end
|
58
|
+
|
59
|
+
def to_s()
|
60
|
+
"name=#{name}, fullname=#{fullname}, email=#{email}"
|
61
|
+
end
|
62
|
+
|
63
|
+
##
|
64
|
+
# Creates a [Hash] representation of this user object.
|
65
|
+
#
|
66
|
+
# @example
|
67
|
+
# user.to_hash
|
68
|
+
# #=> {"name" => "runner", "fullname" => "Steven Hansen", "runner@b.e"}
|
69
|
+
#
|
70
|
+
# @return [Hash<String,String>]
|
71
|
+
#
|
72
|
+
def to_hash()
|
73
|
+
{"name" => name, "fullname" => fullname, "email" => email}
|
74
|
+
end
|
75
|
+
|
76
|
+
##
|
77
|
+
# List of all groups this user has membership in.
|
78
|
+
#
|
79
|
+
# @return [Array<String>] names of all groups.
|
80
|
+
#
|
81
|
+
def groups()
|
82
|
+
return [] if new_record?
|
83
|
+
conn.getUserGroups(self.name)
|
84
|
+
end
|
85
|
+
|
86
|
+
##
|
87
|
+
# Gives user membership in a group.
|
88
|
+
#
|
89
|
+
# @param [String] the name of the group
|
90
|
+
# @return [true, false] result of whether group membership was successful.
|
91
|
+
#
|
92
|
+
def join_group(grp)
|
93
|
+
@errors.clear
|
94
|
+
unless groups.include?(grp)
|
95
|
+
conn.addUserToGroup(self.name, grp)
|
96
|
+
logger.debug("User [#{self}] added to group: #{grp}")
|
97
|
+
return true
|
98
|
+
else
|
99
|
+
@errors << "User is already in group: #{grp}"
|
100
|
+
return false
|
101
|
+
end
|
102
|
+
rescue(RuntimeError) => e
|
103
|
+
logger.debug(e.message)
|
104
|
+
@errors << e.message
|
105
|
+
return false
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Removes user from a group.
|
110
|
+
#
|
111
|
+
# @param [String] the name of the group
|
112
|
+
# @return [true, false] result of whether removal from group was successful.
|
113
|
+
#
|
114
|
+
def leave_group(grp)
|
115
|
+
@errors.clear
|
116
|
+
if groups.include?(grp)
|
117
|
+
conn.removeUserFromGroup(self.name, grp)
|
118
|
+
logger.debug("User [#{self}] removed from group: #{grp}")
|
119
|
+
return true
|
120
|
+
else
|
121
|
+
@errors << "User not in group: #{grp}"
|
122
|
+
return false
|
123
|
+
end
|
124
|
+
rescue(RuntimeError) => e
|
125
|
+
logger.debug(e.message)
|
126
|
+
@errors << e.message
|
127
|
+
return false
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Persists any changes to this user. If the user record is new, a new record
|
132
|
+
# is created.
|
133
|
+
#
|
134
|
+
# @return [true, false] result of whether operation was successful.
|
135
|
+
#
|
136
|
+
def save()
|
137
|
+
@errors.clear
|
138
|
+
if new_record?
|
139
|
+
conn.addUser(self.to_hash, Confluence.config[:user_default_password])
|
140
|
+
@new_record = false
|
141
|
+
else
|
142
|
+
conn.editUser(self.to_hash)
|
143
|
+
end
|
144
|
+
return true
|
145
|
+
rescue(RuntimeError) => e
|
146
|
+
logger.debug(e.message)
|
147
|
+
@errors << e.message
|
148
|
+
return false
|
149
|
+
end
|
150
|
+
|
151
|
+
##
|
152
|
+
# Deletes the user from Confluence.
|
153
|
+
#
|
154
|
+
# @return [true, false] result of whether operation was successful.
|
155
|
+
#
|
156
|
+
def delete()
|
157
|
+
@errors.clear
|
158
|
+
conn.removeUser(name.to_s)
|
159
|
+
self.freeze
|
160
|
+
return true
|
161
|
+
rescue(RuntimeError) => e
|
162
|
+
logger.debug(e.message)
|
163
|
+
@errors << e.message
|
164
|
+
return false
|
165
|
+
end
|
166
|
+
|
167
|
+
##
|
168
|
+
# Flags this user as disabled (inactive) and removes them from all
|
169
|
+
# groups. Update happens immediately.
|
170
|
+
#
|
171
|
+
# @return [true, false] true if the operation was successfull, otherwise
|
172
|
+
# false
|
173
|
+
#
|
174
|
+
def disable()
|
175
|
+
@errors.clear
|
176
|
+
if disabled?
|
177
|
+
logger.debug("#{self} has already been disabled")
|
178
|
+
return true
|
179
|
+
end
|
180
|
+
|
181
|
+
groups.each { |grp| leave_group(grp) }
|
182
|
+
self.fullname = "#{self.fullname} #{DISABLED_SUFFIX}"
|
183
|
+
result = self.save()
|
184
|
+
logger.debug("Disabled user: #{self}")
|
185
|
+
result
|
186
|
+
end
|
187
|
+
|
188
|
+
##
|
189
|
+
# Predicate indicating if the current user is disabled (inactive)
|
190
|
+
#
|
191
|
+
# @return [true, false]
|
192
|
+
#
|
193
|
+
def disabled?
|
194
|
+
fullname.include?(DISABLED_SUFFIX) && groups.empty?
|
195
|
+
end
|
196
|
+
|
197
|
+
def logger()
|
198
|
+
self.class.logger
|
199
|
+
end
|
200
|
+
|
201
|
+
def conn()
|
202
|
+
self.class.conn
|
203
|
+
end
|
204
|
+
|
205
|
+
##
|
206
|
+
# List of errors associated with this record.
|
207
|
+
#
|
208
|
+
# @return [Array<String>]
|
209
|
+
#
|
210
|
+
def errors()
|
211
|
+
@errors ||= []
|
212
|
+
end
|
213
|
+
|
214
|
+
class << self
|
215
|
+
def conn()
|
216
|
+
Confluence.conn
|
217
|
+
end
|
218
|
+
|
219
|
+
def logger()
|
220
|
+
Confluence.logger
|
221
|
+
end
|
222
|
+
|
223
|
+
##
|
224
|
+
# Finds an existing Confluence user by their name (which also happens
|
225
|
+
# to be their ldap_uid). If they do not exist in Confluence, we look
|
226
|
+
# them up in LDAP and then add them to Confluence finally returning
|
227
|
+
# the newly created user object.
|
228
|
+
#
|
229
|
+
def find_or_create_from_ldap(name)
|
230
|
+
user = find_or_new_from_ldap(name)
|
231
|
+
user.save if user.new_record?
|
232
|
+
user
|
233
|
+
end
|
234
|
+
|
235
|
+
def find_or_new_from_ldap(name)
|
236
|
+
if (u = find_by_name(name))
|
237
|
+
return u
|
238
|
+
elsif (p = UCB::LDAP::Person.find_by_uid(name)).nil?
|
239
|
+
msg = "User not found in LDAP: #{name}"
|
240
|
+
logger.debug(msg)
|
241
|
+
raise(LdapPersonNotFound, msg)
|
242
|
+
else
|
243
|
+
self.new({
|
244
|
+
:name => p.uid.to_s,
|
245
|
+
:fullname => "#{p.first_name} + #{p.last_name}",
|
246
|
+
:email => p.email || "test@berkeley.edu"
|
247
|
+
})
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
##
|
252
|
+
# Retrieves all users where their accoutns have been disabled.
|
253
|
+
#
|
254
|
+
# @return [Array<Confluence::User>]
|
255
|
+
#
|
256
|
+
def expired()
|
257
|
+
self.all.select { |u| u[:fullname].include?("ACCOUNT DISABLED") }
|
258
|
+
end
|
259
|
+
|
260
|
+
##
|
261
|
+
# Retrieves all users where their accounts are currently enabled.
|
262
|
+
#
|
263
|
+
# @return [Array<Confluence::User>]
|
264
|
+
#
|
265
|
+
def active()
|
266
|
+
self.all.reject { |u| u[:fullname].include?("ACCOUNT DISABLED") }
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Returns a list of all Confluence user names.
|
271
|
+
#
|
272
|
+
# @return [Array<String>] where each entry is the user's name
|
273
|
+
# in Confluence.
|
274
|
+
#
|
275
|
+
def all_names()
|
276
|
+
conn.getActiveUsers(true)
|
277
|
+
end
|
278
|
+
|
279
|
+
##
|
280
|
+
# Retrieves all users, both expired and active.
|
281
|
+
#
|
282
|
+
# @return [Array<Confluence::User>]
|
283
|
+
#
|
284
|
+
def all()
|
285
|
+
all_names.map { |name| find_by_name(name) }
|
286
|
+
end
|
287
|
+
|
288
|
+
##
|
289
|
+
# Finds a given Confluence user by their username.
|
290
|
+
#
|
291
|
+
# @param [String] the username.
|
292
|
+
# @return [Confluence::User, nil] the found record, otherwise returns
|
293
|
+
# nil.
|
294
|
+
#
|
295
|
+
def find_by_name(name)
|
296
|
+
begin
|
297
|
+
u = self.new(conn.getUser(name.to_s))
|
298
|
+
u.instance_variable_set(:@new_record, false)
|
299
|
+
u
|
300
|
+
rescue(RuntimeError) => e
|
301
|
+
logger.debug(e.message)
|
302
|
+
return nil
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def exists?(name)
|
307
|
+
conn.hasUser(name)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|