ucb_confluence 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|