rally_user_management 0.5.5

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.
@@ -0,0 +1,1937 @@
1
+ # Copyright (c) 2014 Rally Software Development
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module RallyUserManagement
22
+
23
+ require 'rally_api'
24
+ require 'pp'
25
+ require 'date'
26
+
27
+ class UserHelper
28
+
29
+ #Setup constants
30
+ ADMIN = 'Admin'
31
+ USER = 'User'
32
+ # Different READ and CREATE attributes are imposed by WSAPI
33
+ # Hopefully this is a temporary hack
34
+ PROJECTADMIN_READ = ADMIN
35
+ PROJECTADMIN_CREATE = 'Project Admin'
36
+ EDITOR = 'Editor'
37
+ VIEWER = 'Viewer'
38
+ NOACCESS = 'No Access'
39
+ TEAMMEMBER_YES = 'Yes'
40
+ TEAMMEMBER_NO = 'No'
41
+
42
+ # Parameters for cache files
43
+ SUB_CACHE = "_cached_subscription.txt"
44
+ WORKSPACE_CACHE = "_cached_workspaces.txt"
45
+ PROJECT_CACHE = "_cached_projects.txt"
46
+ CACHE_COL_DELIM = "\t"
47
+ CACHE_ROW_DELIM = "\n"
48
+ CACHE_WRITE_MODE = "wb"
49
+
50
+ SUB_CACHE_FIELDS = %w{SubscriptionID Name}
51
+ WORKSPACE_CACHE_FIELDS = %w{ObjectID Name State}
52
+ PROJECT_CACHE_FIELDS = %w{ObjectID ProjectName State WorkspaceName WorkspaceOID}
53
+
54
+ def initialize(config)
55
+
56
+ @rally = config[:rally_api]
57
+ @rally_json_connection = @rally.rally_connection
58
+ @logger = config[:logger]
59
+ @create_flag = config[:create_flag]
60
+ @cached_users = {}
61
+ @cached_sub_id = 0
62
+ @cached_subscription = {}
63
+ @cached_workspaces = {}
64
+ @cached_projects = {}
65
+ @cached_workspaces_by_name = {}
66
+ @cached_projects_by_name = {}
67
+
68
+ # Provides lookup of projects per workspace
69
+ @workspace_hash_of_projects = {}
70
+
71
+ # Maximum age in days of the Workspace/Project cache before refresh
72
+ @max_cache_age = config[:max_cache_age] || 1
73
+
74
+ # upgrade_only_mode - when running in upgrade_only_mode, check existing permissions
75
+ # first, and only apply the change if it represents an upgrade over existing permissions
76
+ @upgrade_only_mode = config[:upgrade_only_mode] || false
77
+
78
+ # File encoding format
79
+ @file_encoding = config[:file_encoding] || "US-ASCII"
80
+
81
+ # User filter for ENABLED users only
82
+ # For purposes of speed/efficiency, summarize Enabled Users ONLY
83
+ @summarize_enabled_only = true
84
+ @enabled_only_filter = "(Disabled = \"False\")"
85
+
86
+ # fetch data
87
+ @initial_user_fetch = "UserName,FirstName,LastName,DisplayName"
88
+ @detail_user_fetch = "UserName,FirstName,LastName,DisplayName,UserPermissions,Name,Role,Workspace,ObjectID,Project,ObjectID,TeamMemberships,UserProfile"
89
+
90
+ end
91
+
92
+ def get_cached_users()
93
+ return @cached_users
94
+ end
95
+
96
+ def get_cached_workspaces()
97
+ return @cached_workspaces
98
+ end
99
+
100
+ def get_cached_projects()
101
+ return @cached_projects
102
+ end
103
+
104
+ def get_workspace_project_hash()
105
+ return @workspace_hash_of_projects
106
+ end
107
+
108
+ def get_cached_workspaces_by_nane()
109
+ return @cached_workspaces_by_name
110
+ end
111
+
112
+ def get_cached_projects_by_name()
113
+ return @cached_projects_by_name
114
+ end
115
+
116
+ # Helper methods
117
+ # Does the user exist? If so, return the user, if not return nil
118
+ # Need to downcase the name since user names are downcased when created. Without downcase, we would not be
119
+ # able to find 'Mark@acme.com'
120
+ def find_user(name)
121
+ if ( name.downcase != name )
122
+ @logger.info "Looking for #{name.downcase} instead of #{name}"
123
+ end
124
+
125
+ if @cached_users.has_key?(name.downcase)
126
+ return @cached_users[name.downcase]
127
+ end
128
+
129
+ single_user_query = RallyAPI::RallyQuery.new()
130
+ single_user_query.type = :user
131
+ single_user_query.fetch = @detail_user_fetch
132
+ single_user_query.page_size = 200 #optional - default is 200
133
+ single_user_query.limit = 90000 #optional - default is 99999
134
+ single_user_query.order = "UserName Asc"
135
+ single_user_query.query_string = "(UserName = \"" + name + "\")"
136
+
137
+ query_results = @rally.find(single_user_query)
138
+
139
+ if query_results.total_result_count == 0
140
+ return nil
141
+ else
142
+ # Cache user for use next time
143
+ this_user = query_results.first
144
+ @cached_users[this_user["UserName"].downcase] = this_user
145
+ @logger.info "Caching User: #{this_user.UserName}"
146
+
147
+ return this_user
148
+ end
149
+ end
150
+
151
+ # Needed to refresh user object after updates, since user cache is stale after
152
+ # user settings are changed by the helper
153
+ def refresh_user(name)
154
+ single_user_query = RallyAPI::RallyQuery.new()
155
+ single_user_query.type = :user
156
+ single_user_query.fetch = @detail_user_fetch
157
+ single_user_query.page_size = 200 #optional - default is 200
158
+ single_user_query.limit = 90000 #optional - default is 99999
159
+ single_user_query.order = "UserName Asc"
160
+ single_user_query.query_string = "(UserName = \"" + name + "\")"
161
+
162
+ query_results = @rally.find(single_user_query)
163
+ if query_results.total_result_count == 0
164
+ return nil
165
+ else
166
+ # Cache user for use next time
167
+ this_user = query_results.first
168
+ @cached_users[this_user["UserName"].downcase] = this_user
169
+ @logger.info "Refreshed User: #{this_user.UserName}"
170
+
171
+ return this_user
172
+ end
173
+ end
174
+
175
+ #==================== Get a list of OPEN projects in Workspace ========================
176
+ #
177
+ def get_open_projects (input_workspace)
178
+ project_query = RallyAPI::RallyQuery.new()
179
+ project_query.workspace = input_workspace
180
+ project_query.project = nil
181
+ project_query.project_scope_up = true
182
+ project_query.project_scope_down = true
183
+ project_query.type = :project
184
+ project_query.fetch = "Name,State,ObjectID,Workspace,ObjectID"
185
+ project_query.query_string = "(State = \"Open\")"
186
+
187
+ begin
188
+ open_projects = @rally.find(project_query)
189
+ rescue Exception => ex
190
+ open_projects = nil
191
+ end
192
+ return (open_projects)
193
+ end
194
+
195
+ #added for performance
196
+ def cache_users()
197
+
198
+ user_query = RallyAPI::RallyQuery.new()
199
+ user_query.type = :user
200
+ user_query.fetch = @initial_user_fetch
201
+ user_query.page_size = 200 #optional - default is 200
202
+ user_query.limit = 90000 #optional - default is 99999
203
+ user_query.order = "UserName Asc"
204
+
205
+ # Filter for enabled only
206
+ if @summarize_enabled_only then
207
+ user_query.query_string = @enabled_only_filter
208
+ number_found_suffix = "Enabled Users."
209
+ else
210
+ number_found_suffix = "Users."
211
+ end
212
+
213
+ initial_query_results = @rally.find(user_query)
214
+
215
+ number_users = initial_query_results.total_result_count
216
+ count = 1
217
+ notify_increment = 25
218
+ @cached_users = {}
219
+ initial_query_results.each do | initial_user |
220
+ notify_remainder=count%notify_increment
221
+ if notify_remainder==0 then @logger.info "Cached #{count} of #{number_users} #{number_found_suffix}" end
222
+
223
+ # Follow-up user-by-user query of Rally for Detailed User Properties
224
+ user_query.fetch = @detail_user_fetch
225
+
226
+ # Setup query parameters for Rally query of detailed user info
227
+ this_user_name = initial_user["UserName"]
228
+ query_string = "(UserName = \"#{this_user_name}\")"
229
+ user_query.query_string = query_string
230
+
231
+ # Query Rally for single-user detailed info, including Permissions, Projects, and
232
+ # Team Memberships
233
+ detail_user_query_results = @rally.find(user_query)
234
+
235
+ # If found, cache the user
236
+ number_found = detail_user_query_results.total_result_count
237
+ if number_found > 0 then
238
+ this_user = detail_user_query_results.first
239
+ @cached_users[this_user.UserName] = this_user
240
+ count+=1
241
+ else
242
+ @logger.warn "User: #{this_user_name} not found in follow-up query. Skipping..."
243
+ next
244
+ end
245
+
246
+ end
247
+ end
248
+
249
+ def find_workspace(object_id)
250
+ if @cached_workspaces.has_key?(object_id)
251
+ # Found workspace in cache, return the cached workspace
252
+ return @cached_workspaces[object_id]
253
+ else
254
+ # workspace not found in cache - go to Rally
255
+ workspace_query = RallyAPI::RallyQuery.new()
256
+ workspace_query.project = nil
257
+ workspace_query.type = :workspace
258
+ workspace_query.fetch = "Name,State,ObjectID"
259
+ workspace_query.query_string = "((ObjectID = \"#{object_id}\") AND (State = \"Open\"))"
260
+
261
+ workspace_results = @rally.find(workspace_query)
262
+
263
+ if workspace_results.total_result_count != 0 then
264
+ # Workspace found via Rally query, return it
265
+ workspace = workspace_results.first()
266
+
267
+ # Cache it for use next time
268
+ @cached_workspaces[workspace["ObjectID"]] = workspace
269
+ @logger.info "Caching Workspace: #{workspace['Name']}"
270
+
271
+ # Return workspace object
272
+ return workspace
273
+ else
274
+ # Workspace not found in Rally _or_ cache - return Nil
275
+ @logger.warn "Rally Workspace: #{object_id} not found"
276
+ return nil
277
+ end
278
+ end
279
+ end
280
+
281
+ def find_workspace_by_name(workspace_name)
282
+ if @cached_workspaces_by_name.has_key?(workspace_name)
283
+ # Found workspace in cache, return the cached workspace
284
+ return @cached_workspaces_by_name[workspace_name]
285
+ else
286
+ # workspace not found in cache - go to Rally
287
+ workspace_query = RallyAPI::RallyQuery.new()
288
+ workspace_query.project = nil
289
+ workspace_query.type = :workspace
290
+ workspace_query.fetch = "Name,State,ObjectID"
291
+ workspace_query.query_string = "((Name = \"#{workspace_name}\") AND (State = \"Open\"))"
292
+
293
+ workspace_results = @rally.find(workspace_query)
294
+
295
+ if workspace_results.total_result_count != 0 then
296
+ # Workspace found via Rally query, return it
297
+ workspace = workspace_results.first()
298
+
299
+ # Cache it for use next time
300
+ @cached_workspaces_by_name[workspace["Name"]] = workspace
301
+ @logger.info "Caching Workspace: #{workspace['Name']}"
302
+
303
+ if workspace_results.total_result_count > 1 then
304
+ # More than one Workspace matching this name found
305
+ @logger.warn "More than one Workspace of name: #{workspace_name} found."
306
+ @logger.warn "Returning only the first instance found!"
307
+ end
308
+
309
+ # Return workspace object
310
+ return workspace
311
+ else
312
+ # Workspace not found in Rally _or_ cache - return Nil
313
+ @logger.warn "Rally Workspace: #{workspace_name} not found"
314
+ return nil
315
+ end
316
+ end
317
+ end
318
+
319
+ def find_project_by_name(project_name)
320
+ if @cached_projects_by_name.has_key?(project_name)
321
+ # Found workspace in cache, return the cached workspace
322
+ return @cached_projects_by_name[project_name]
323
+ else
324
+ # workspace not found in cache - go to Rally
325
+ project_query = RallyAPI::RallyQuery.new()
326
+ project_query.project = nil
327
+ project_query.type = :workspace
328
+ project_query.fetch = "Name,State,ObjectID"
329
+ project_query.query_string = "((Name = \"#{project_name}\") AND (State = \"Open\"))"
330
+
331
+ project_results = @rally.find(project_query)
332
+
333
+ if project_results.total_result_count != 0 then
334
+ # Workspace found via Rally query, return it
335
+ project = project_results.first()
336
+
337
+ # Cache it for use next time
338
+ @cached_projects_by_name[project["Name"]] = project
339
+ @logger.info "Caching Project: #{project['Name']}"
340
+
341
+ if project_results.total_result_count > 1 then
342
+ # More than one Project matching this name found
343
+ @logger.warn "More than one Project of name: #{project_name} found."
344
+ @logger.warn "Returning only the first instance found!"
345
+ end
346
+
347
+ # Return project object
348
+ return project
349
+ else
350
+ # Project not found in Rally _or_ cache - return Nil
351
+ @logger.warn "Rally Project: #{project_name} not found"
352
+ return nil
353
+ end
354
+ end
355
+ end
356
+
357
+ def find_project(object_id)
358
+ if @cached_projects.has_key?(object_id)
359
+ # Found project in cache, return the cached project
360
+ return @cached_projects[object_id]
361
+ else
362
+ # project not found in cache - go to Rally
363
+ project_query = RallyAPI::RallyQuery.new()
364
+ project_query.type = :project
365
+ project_query.fetch = "Name,State,ObjectID,Workspace,ObjectID"
366
+ project_query.query_string = "((ObjectID = \"#{object_id}\") AND (State = \"Open\"))"
367
+
368
+ project_results = @rally.find(project_query)
369
+
370
+ if project_results.total_result_count != 0 then
371
+ # Project found via Rally query, return it
372
+ project = project_results.first()
373
+
374
+ # Cache it for use next time
375
+ @cached_projects[project["ObjectID"]] = project
376
+ @logger.info "Caching Project: #{project['Name']}"
377
+
378
+ # Return it
379
+ return project
380
+ else
381
+ # Project not found in Rally _or_ cache - return Nil
382
+ @logger.warn "Rally Project: #{object_id} not found"
383
+ return nil
384
+ end
385
+ end
386
+ end
387
+
388
+ # Check to see if a project is within a particular workspace.
389
+ def is_project_in_workspace(project, workspace)
390
+ test_project_oid = project["ObjectID"]
391
+ this_workspace_oid = workspace["ObjectID"]
392
+
393
+ object_id_matches = false
394
+ these_projects = @workspace_hash_of_projects[this_workspace_oid]
395
+ these_projects.each do | this_project |
396
+ this_project_oid = this_project["ObjectID"]
397
+ if test_project_oid == this_project_oid then
398
+ object_id_matches = true
399
+ end
400
+ end
401
+ return object_id_matches
402
+ end
403
+
404
+ # Get current SubID
405
+ def get_current_sub_id()
406
+
407
+ subscription_query = RallyAPI::RallyQuery.new()
408
+ subscription_query.type = :subscription
409
+ subscription_query.fetch = "Name,SubscriptionID"
410
+ subscription_query.page_size = 200 #optional - default is 200
411
+ subscription_query.limit = 50000 #optional - default is 99999
412
+ subscription_query.order = "Name Asc"
413
+
414
+ results = @rally.find(subscription_query)
415
+
416
+ this_subscription = results.first
417
+ this_sub_id = this_subscription["SubscriptionID"]
418
+
419
+ return this_sub_id
420
+ end
421
+
422
+ # Given the name of a file, calculates age of that file
423
+ def calc_file_age(filename)
424
+ today = Time.now
425
+
426
+ # Return really big number to prompt refresh if files not found
427
+ file_age = 10000
428
+
429
+ if !FileTest.exist?(filename) then
430
+ # Maintain "really big" age and return
431
+ return file_age
432
+ end
433
+
434
+ file_reference = File.new(filename, "r")
435
+ # Round fractional days up
436
+ file_age = ((today - file_reference.mtime)/86400).ceil
437
+ return file_age
438
+ end
439
+
440
+ # Determine cache age
441
+ def get_cache_age()
442
+
443
+ subscription_cache_filename = File.dirname(__FILE__) + "/" + SUB_CACHE
444
+ workspace_cache_filename = File.dirname(__FILE__) + "/" + WORKSPACE_CACHE
445
+ project_cache_filename = File.dirname(__FILE__) + "/" + PROJECT_CACHE
446
+
447
+ # Default to really big number to prompt refresh if files not found
448
+ cache_age = 10000
449
+
450
+ if !FileTest.exist?(subscription_cache_filename) ||
451
+ !FileTest.exist?(workspace_cache_filename) ||
452
+ !FileTest.exist?(project_cache_filename) then
453
+ # Maintain "really big" age and return
454
+ return cache_age
455
+ end
456
+
457
+ age_array = [
458
+ calc_file_age(subscription_cache_filename),
459
+ calc_file_age(workspace_cache_filename),
460
+ calc_file_age(project_cache_filename)
461
+ ]
462
+ min_age = age_array.min
463
+
464
+ # return the age of the youngest cache file (should all be the same though)
465
+ cache_age = min_age
466
+ return cache_age
467
+ end
468
+
469
+ # Determine whether or not to refresh local sub/workspace/project cache
470
+ def cache_refresh_needed()
471
+
472
+ refresh_needed = false
473
+ reason = ""
474
+ subscription_cache_filename = File.dirname(__FILE__) + "/" + SUB_CACHE
475
+ workspace_cache_filename = File.dirname(__FILE__) + "/" + WORKSPACE_CACHE
476
+ project_cache_filename = File.dirname(__FILE__) + "/" + PROJECT_CACHE
477
+
478
+ if !FileTest.exist?(subscription_cache_filename) ||
479
+ !FileTest.exist?(workspace_cache_filename) ||
480
+ !FileTest.exist?(project_cache_filename) then
481
+ refresh_needed = true
482
+ reason = "One or more cache files is not found."
483
+ return refresh_needed, reason
484
+ end
485
+
486
+ cache_age = get_cache_age()
487
+ if cache_age > @max_cache_age then
488
+ refresh_needed = true
489
+ reason = "Age of workspace/project cache is greater than specified max of #{@max_cache_age}"
490
+ return refresh_needed, reason
491
+ end
492
+
493
+ read_subscription_cache()
494
+ cached_subscription_id = @cached_sub_id
495
+ current_subscription_id = get_current_sub_id()
496
+
497
+ if current_subscription_id != cached_subscription_id then
498
+ refresh_needed = true
499
+ reason = "Specified SubID: #{current_subscription_id} is different from cached SubID: #{cached_subscription_id}"
500
+ return refresh_needed, reason
501
+ end
502
+
503
+ # If we've fallen through to here, no refresh is needed
504
+ reason = "No workspace/project cache refresh currently required"
505
+ return refresh_needed, reason
506
+ end
507
+
508
+ # Create ref from oid
509
+ def make_ref_from_oid(object_type, object_id)
510
+ return "/#{object_type}/#{object_id}"
511
+ end
512
+
513
+ # Add row to subscription cache
514
+ def cache_subscription_entry(header, row)
515
+ subscription_id = row[header[0]].strip
516
+ subscription_name = row[header[1]].strip
517
+
518
+ this_subscription = {}
519
+ this_subscription["SubscriptionID"] = subscription_id
520
+ this_subscription["Name"] = subscription_name
521
+
522
+ @cached_sub_id = subscription_id.to_i
523
+ @cached_subscription[subscription_id] = this_subscription
524
+ end
525
+
526
+ # Read subscription cache
527
+ def read_subscription_cache()
528
+
529
+ subscription_cache_filename = File.dirname(__FILE__) + "/" + SUB_CACHE
530
+ @logger.info "Started reading subscription cache from #{subscription_cache_filename}"
531
+
532
+ # Read in Subscription cache items (should be only 1)
533
+ subscription_cache_input = CSV.read(subscription_cache_filename, {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding})
534
+
535
+ header = subscription_cache_input.first #ignores first line
536
+
537
+ rows = []
538
+ (1...subscription_cache_input.size).each { |i| rows << CSV::Row.new(header, subscription_cache_input[i]) }
539
+
540
+ number_processed = 0
541
+
542
+ rows.each do |row|
543
+ if !row.nil? then
544
+ cache_subscription_entry(header, row)
545
+ number_processed += 1
546
+ end
547
+ end
548
+
549
+ @logger.info "Completed reading subscription cache from #{subscription_cache_filename}"
550
+ end
551
+
552
+ # Add row to workspace cache
553
+ def cache_workspace_entry(header, row)
554
+ workspace_id = row[header[0]].strip
555
+ workspace_name = row[header[1]].strip
556
+ workspace_state = row[header[2]].strip
557
+
558
+ this_workspace = {}
559
+ this_workspace["ObjectID"] = workspace_id
560
+ this_workspace["Name"] = workspace_name
561
+ this_workspace["State"] = workspace_state
562
+ this_workspace["_ref"] = make_ref_from_oid("workspace", workspace_id)
563
+
564
+ @cached_workspaces[workspace_id] = this_workspace
565
+ if !@cached_workspaces_by_name.has_key?(workspace_name) then
566
+ @cached_workspaces_by_name[workspace_name] = this_workspace
567
+ else
568
+ @logger.warn " Warning caching Workspace by Name: #{workspace_name}. Duplicate name found!"
569
+ @logger.warn " Only first instance of this Workspace name will be cached."
570
+ end
571
+ end
572
+
573
+ # Read workspace cache
574
+ def read_workspace_cache()
575
+
576
+ workspace_cache_filename = File.dirname(__FILE__) + "/" + WORKSPACE_CACHE
577
+ @logger.info "Started reading workspace cache from #{workspace_cache_filename}"
578
+
579
+ # Read in workspace cache items (should be only 1)
580
+ workspace_cache_input = CSV.read(workspace_cache_filename, {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding})
581
+
582
+ header = workspace_cache_input.first #ignores first line
583
+
584
+ rows = []
585
+ (1...workspace_cache_input.size).each { |i| rows << CSV::Row.new(header, workspace_cache_input[i]) }
586
+
587
+ number_processed = 0
588
+
589
+ rows.each do |row|
590
+ if !row.nil? then
591
+ cache_workspace_entry(header, row)
592
+ number_processed += 1
593
+ end
594
+ end
595
+ @logger.info "Completed reading workspace cache from #{workspace_cache_filename}"
596
+ @logger.info "Read and cached a total of #{number_processed} workspaces from local cache file."
597
+ end
598
+
599
+ def cache_project_entry(header, row)
600
+ project_id = row[header[0]].strip
601
+ project_name = row[header[1]].strip
602
+ project_state = row[header[2]].strip
603
+ project_workspace_name = row[header[3]].strip
604
+ project_workspace_oid = row[header[4]].strip
605
+
606
+ this_project = {}
607
+ this_project["ObjectID"] = project_id
608
+ this_project["Name"] = project_name
609
+ this_project["State"] = project_state
610
+ this_project["_ref"] = make_ref_from_oid("project", project_id)
611
+ this_workspace = {}
612
+ this_workspace["Name"] = project_workspace_name
613
+ this_workspace["ObjectID"] = project_workspace_oid
614
+ this_workspace["_ref"] = make_ref_from_oid("workspace", project_workspace_oid)
615
+ this_project["Workspace"] = this_workspace
616
+ @cached_projects[project_id] = this_project
617
+
618
+ if !@cached_projects_by_name.has_key?(project_name) then
619
+ @cached_projects_by_name[project_name] = this_project
620
+ else
621
+ @logger.warn " Warning caching Project by Name: #{project_name}. Duplicate name found!"
622
+ @logger.warn " Only first instance of this Project name will be cached."
623
+ end
624
+
625
+ return this_project, project_workspace_oid
626
+
627
+ end
628
+
629
+ # Read project cache
630
+ def read_project_cache()
631
+
632
+ project_cache_filename = File.dirname(__FILE__) + "/" + PROJECT_CACHE
633
+ @logger.info "Started reading project cache from #{project_cache_filename}"
634
+
635
+ # Read in project cache items (should be only 1)
636
+ project_cache_input = CSV.read(project_cache_filename, {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding})
637
+
638
+ header = project_cache_input.first #ignores first line
639
+
640
+ rows = []
641
+ (1...project_cache_input.size).each { |i| rows << CSV::Row.new(header, project_cache_input[i]) }
642
+
643
+ number_processed = 0
644
+
645
+ current_workspace_oid_string = "-9999"
646
+ these_projects = []
647
+
648
+ rows.each do |row|
649
+ if !row.nil? then
650
+ this_project, this_workspace_oid = cache_project_entry(header, row)
651
+
652
+ # make sure workspace OID is a string
653
+ this_workspace_oid_string = this_workspace_oid.to_s
654
+
655
+ if @workspace_hash_of_projects.has_key?(this_workspace_oid_string) then
656
+ these_projects = @workspace_hash_of_projects[this_workspace_oid_string]
657
+ else
658
+ these_projects = []
659
+ end
660
+
661
+ these_projects.push(this_project)
662
+ @workspace_hash_of_projects[this_workspace_oid_string] = these_projects
663
+
664
+ number_processed += 1
665
+ end
666
+
667
+ end
668
+ @logger.info "Completed reading project cache from #{project_cache_filename}"
669
+ @logger.info "Read and cached a total of #{number_processed} projects from local cache file."
670
+ end
671
+
672
+ # Write subscription/workspace/project cache
673
+ def write_subscription_cache()
674
+
675
+ subscription_cache_filename = File.dirname(__FILE__) + "/" + SUB_CACHE
676
+ @logger.info "Started writing subscription cache to #{subscription_cache_filename}"
677
+
678
+ # Output CSV header
679
+ subscription_csv = CSV.open(
680
+ subscription_cache_filename,
681
+ CACHE_WRITE_MODE,
682
+ {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding}
683
+ )
684
+ subscription_csv << SUB_CACHE_FIELDS
685
+
686
+ # Output cache to file
687
+ # Record for CSV output
688
+ @cached_subscription.each_pair do | sub_id, this_subscription |
689
+
690
+ data = []
691
+ @logger.info "sub_id: #{sub_id.inspect}"
692
+ @logger.info "this_subscription: #{this_subscription.inspect}"
693
+
694
+ data << sub_id
695
+ data << this_subscription
696
+
697
+ subscription_csv << CSV::Row.new(SUB_CACHE_FIELDS, data)
698
+ end
699
+
700
+ @logger.info "Finished writing subscription cache to #{subscription_cache_filename}"
701
+ end
702
+
703
+ def write_workspace_cache()
704
+ workspace_cache_filename = File.dirname(__FILE__) + "/" + WORKSPACE_CACHE
705
+ @logger.info "Started writing workspace cache to #{workspace_cache_filename}"
706
+
707
+ # Output CSV header
708
+ workspace_csv = CSV.open(
709
+ workspace_cache_filename,
710
+ CACHE_WRITE_MODE,
711
+ {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding}
712
+ )
713
+ workspace_csv << WORKSPACE_CACHE_FIELDS
714
+
715
+ # Output cache to file
716
+ # Record for CSV output
717
+ @cached_workspaces.each_pair do | workspace_id, this_workspace |
718
+
719
+ data = []
720
+
721
+ workspace_id = this_workspace["ObjectID"]
722
+ workspace_name = this_workspace["Name"]
723
+ workspace_state = this_workspace["State"]
724
+
725
+ data << workspace_id
726
+ data << workspace_name
727
+ data << workspace_state
728
+
729
+ workspace_csv << CSV::Row.new(WORKSPACE_CACHE_FIELDS, data)
730
+ end
731
+
732
+ @logger.info "Finished writing workspace cache to #{workspace_cache_filename}"
733
+ end
734
+
735
+ def write_project_cache()
736
+ project_cache_filename = File.dirname(__FILE__) + "/" + PROJECT_CACHE
737
+ @logger.info "Started writing project cache to #{project_cache_filename}"
738
+
739
+ # The following results in an array of two-element arrays. The first element of each 2-element array
740
+ # is the ProjectOID, or the key for the project hash. The second element is the value part of the hash
741
+ projects_sorted_by_workspace = @cached_projects.sort_by {|key, value| value["WorkspaceOIDNumeric"]}
742
+
743
+ # Output CSV header
744
+ project_csv = CSV.open(
745
+ project_cache_filename,
746
+ CACHE_WRITE_MODE,
747
+ {:col_sep => CACHE_COL_DELIM, :encoding => @file_encoding}
748
+ )
749
+ project_csv << PROJECT_CACHE_FIELDS
750
+
751
+ # Output cache to file
752
+ # Record for CSV output
753
+ projects_sorted_by_workspace.each do | project_element |
754
+
755
+ this_project = project_element[1]
756
+
757
+ data = []
758
+
759
+ project_id = this_project["ObjectID"]
760
+ project_name = this_project["Name"]
761
+ project_state = this_project["State"]
762
+ project_workspace = this_project["Workspace"]
763
+ workspace_name = project_workspace["Name"]
764
+ workspace_oid = project_workspace["ObjectID"]
765
+
766
+ data << project_id
767
+ data << project_name
768
+ data << project_state
769
+ data << workspace_name
770
+ data << workspace_oid
771
+
772
+ project_csv << CSV::Row.new(PROJECT_CACHE_FIELDS, data)
773
+ end
774
+
775
+ @logger.info "Finished writing project cache to #{project_cache_filename}"
776
+ end
777
+
778
+ def read_workspace_project_cache()
779
+ # Caches by OID
780
+ @cached_subscription = {}
781
+ @cached_workspaces = {}
782
+ @cached_projects = {}
783
+
784
+ @cached_workspaces_by_name = {}
785
+ @cached_projects_by_name = {}
786
+
787
+ read_subscription_cache()
788
+ read_workspace_cache()
789
+ read_project_cache()
790
+ end
791
+
792
+ # Added for performance
793
+ def cache_workspaces_projects()
794
+ @cached_subscription = {}
795
+ @cached_workspaces = {}
796
+ @cached_projects = {}
797
+ @workspace_hash_of_projects = {}
798
+
799
+ subscription_query = RallyAPI::RallyQuery.new()
800
+ subscription_query.type = :subscription
801
+ subscription_query.fetch = "Name,SubscriptionID,Workspaces,Name,State,ObjectID"
802
+ subscription_query.page_size = 200 #optional - default is 200
803
+ subscription_query.limit = 50000 #optional - default is 99999
804
+ subscription_query.order = "Name Asc"
805
+
806
+ results = @rally.find(subscription_query)
807
+
808
+ # pre-populate workspace hash
809
+ results.each do | this_subscription |
810
+
811
+ this_subscription_id = this_subscription["SubscriptionID"]
812
+ @cached_subscription[this_subscription_id] = this_subscription
813
+ @cached_sub_id = this_subscription_id
814
+
815
+ @logger.info "This subscription has: #{this_subscription.Workspaces.length} workspaces."
816
+
817
+ workspaces = this_subscription.Workspaces
818
+ workspaces.each do |this_workspace|
819
+
820
+ this_workspace_oid_string = this_workspace["ObjectID"].to_s
821
+
822
+ # Look for open projects within Workspace
823
+ open_projects = get_open_projects(this_workspace)
824
+ @workspace_hash_of_projects[this_workspace_oid_string] = open_projects
825
+
826
+ if this_workspace.State != "Closed" && open_projects != nil then
827
+ @logger.info "Caching Workspace: #{this_workspace['Name']}."
828
+ @cached_workspaces[this_workspace_oid_string] = this_workspace
829
+
830
+ if !@cached_workspaces_by_name.has_key?(this_workspace["Name"]) then
831
+ @cached_workspaces_by_name[this_workspace["Name"]]
832
+ else
833
+ @logger.warn " Warning caching Workspace by Name: #{this_workspace["Name"]}. Duplicate name found!"
834
+ @logger.warn " Only first instance of this Workspace name will be cached."
835
+ end
836
+
837
+ @logger.info "Workspace: #{this_workspace['Name']} has: #{open_projects.length} open projects."
838
+
839
+ # Loop through open projects and Cache
840
+ open_projects.each do | this_project |
841
+ this_project["WorkspaceOIDNumeric"] = this_workspace["ObjectID"]
842
+ @cached_projects[this_project.ObjectID.to_s] = this_project
843
+
844
+ if !@cached_projects_by_name.has_key?(this_project["Name"]) then
845
+ @cached_projects_by_name[this_project["Name"]]
846
+ else
847
+ @logger.warn " Warning caching Project by Name: #{this_project["Name"]}. Duplicate name found!"
848
+ @logger.warn " Only first instance of this Project name will be cached."
849
+ end
850
+ end
851
+ else
852
+ @logger.warn "Workspace: #{this_workspace['Name']} is closed or has no open projects. Not added to cache."
853
+ end
854
+ end
855
+ end
856
+
857
+ write_subscription_cache()
858
+ write_workspace_cache()
859
+ write_project_cache()
860
+ end
861
+
862
+ # Mirrors ProjectPermission set from a source user to a target user
863
+ # Note that creation of a ProjectPermission will automatically create
864
+ # the minimum needed WorkspacePermission for the "owning" Workspace,
865
+ # if a WorkspacePermission does not already exist. Thus there's
866
+ # no need for a sync_workspace_permissions method
867
+ def sync_project_permissions(source_user_id, target_user_id)
868
+ source_user = find_user(source_user_id)
869
+ target_user = find_user(target_user_id)
870
+ if source_user.nil? then
871
+ @logger.warn " Source user: #{source_user_id} Not found. Skipping sync of permissions to #{target_user_id}."
872
+ return
873
+ elsif target_user.nil then
874
+ @logger.warn " Target user: #{target_user_id} Not found. Skipping sync of permissions for #{target_user_id}."
875
+ end
876
+
877
+ permissions_existing = target_user.UserPermissions
878
+ source_permissions = source_user.UserPermissions
879
+
880
+ # build permission hashes by Project ObjectID
881
+ source_permissions_by_project = {}
882
+ source_permissions.each do | this_source_permission |
883
+ if this_source_permission._type == "ProjectPermission" then
884
+ source_permissions_by_project[this_source_permission.Project.ObjectID.to_s] = this_source_permission
885
+ end
886
+ end
887
+
888
+ permissions_existing_by_project = {}
889
+ permissions_existing.each do | this_permission |
890
+ if this_permission._type == "ProjectPermission" then
891
+ permissions_existing_by_project[this_permission.Project.ObjectID.to_s] = this_permission
892
+ end
893
+ end
894
+
895
+ # Prepare arrays of permissions to update, create, or delete
896
+ permissions_to_update = []
897
+ permissions_to_create = []
898
+ permissions_to_delete = []
899
+
900
+ # Check target permissions list for permissions to create and/or update
901
+ source_permissions_by_project.each_pair do | this_source_project_oid, this_source_permission |
902
+
903
+ # If target hash doesn't contain the OID referenced in the source permission set, it's a new
904
+ # permission we need to create
905
+ if !permissions_existing_by_project.has_key?(this_source_project_oid) then
906
+ permissions_to_create.push(this_source_permission)
907
+
908
+ # We found the OID key, so there is an existing permission for this Project. Is it different
909
+ # from the target permission?
910
+ else
911
+ this_source_role = this_source_permission.Role
912
+ this_source_project = find_project(this_source_project_oid)
913
+ this_source_project_name = this_source_project["Name"]
914
+
915
+ if project_permissions_different?(this_source_project, target_user, this_source_role) then
916
+ existing_permission = permissions_existing_by_project[this_source_project_oid]
917
+ this_existing_project = existing_permission.Project
918
+ this_existing_project_name = this_existing_project["Name"]
919
+ this_existing_role = existing_permission.Role
920
+ @logger.info "Existing Permission: #{this_existing_project_name}: #{this_existing_role}"
921
+ @logger.info "Updated Permission: #{this_source_project_name}: #{this_source_role}"
922
+ permissions_to_update.push(this_source_permission)
923
+ end
924
+ end
925
+ end
926
+
927
+ # Loop through target permissions list and check for Project Permissions that don't exist
928
+ # in source permissions template, indicating they need to be removed
929
+ permissions_existing_by_project.each_pair do | this_existing_project_oid, this_existing_permission |
930
+ if !source_permissions_by_project.has_key?(this_existing_project_oid) then
931
+ permissions_to_delete.push(this_existing_permission)
932
+ end
933
+ end
934
+
935
+ # Process creates
936
+ number_new_permissions = 0
937
+ permissions_to_create.each do | this_new_permission |
938
+ this_project = find_project(this_new_permission.Project.ObjectID.to_s)
939
+ if !this_project.nil? then
940
+
941
+ this_project = this_new_permission.Project
942
+ this_workspace = this_project.Workspace
943
+ this_workspace.read
944
+
945
+ this_project_name = this_new_permission.Project.Name
946
+ this_workspace_name = this_workspace.Name
947
+
948
+ this_role = this_new_permission.Role
949
+
950
+ @logger.info "Workspace: #{this_workspace_name}"
951
+ @logger.info "Creating #{this_role} permission on #{this_project_name} from #{source_user_id} to: #{target_user_id}."
952
+ create_project_permission(target_user, this_project, this_role)
953
+ number_new_permissions += 1
954
+ end
955
+ end
956
+
957
+ # Process updates
958
+ number_updated_permissions = 0
959
+ permissions_to_update.each do | this_new_permission |
960
+ this_project = find_project(this_new_permission.Project.ObjectID.to_s)
961
+ if !this_project.nil? then
962
+ this_project = this_new_permission.Project
963
+ this_workspace = this_project.Workspace
964
+ this_workspace.read
965
+
966
+ this_project_name = this_new_permission.Project.Name
967
+ this_workspace_name = this_workspace.Name
968
+
969
+ this_role = this_new_permission.Role
970
+
971
+ @logger.info "Workspace: #{this_workspace_name}"
972
+ @logger.info "Updating #{this_role} permission on #{this_project_name} from #{source_user_id} to: #{target_user_id}."
973
+ create_project_permission(target_user, this_project, this_role)
974
+ number_updated_permissions += 1
975
+ end
976
+ end
977
+
978
+ # Process deletes
979
+ number_removed_permissions = 0
980
+ permissions_to_delete.each do | this_deleted_permission |
981
+ this_project = find_project(this_deleted_permission.Project.ObjectID.to_s)
982
+ if !this_project.nil? then
983
+ this_project = this_deleted_permission.Project
984
+ this_workspace = this_project.Workspace
985
+ this_workspace.read
986
+
987
+ this_project_name = this_deleted_permission.Project.Name
988
+ this_workspace_name = this_workspace.Name
989
+
990
+ this_role = this_deleted_permission.Role
991
+
992
+ @logger.info "Workspace: #{this_workspace_name}"
993
+ @logger.info "Removing #{this_role} permission to #{this_project_name} from #{target_user_id} since it is not present on source: #{source_user_id}."
994
+ if !@upgrade_only_mode then
995
+ delete_project_permission(target_user, this_project)
996
+ number_removed_permissions += 1
997
+ else
998
+ @logger.info " #{target_user_id} - upgrade_only_mode == true."
999
+ @logger.info " Proposed Permission removal would downgrade permissions. No permission removal applied."
1000
+ end
1001
+ end
1002
+ end
1003
+ @logger.info "#{number_new_permissions} Permissions Created; #{number_updated_permissions} Permissions Updated; #{number_removed_permissions} Permissions Removed."
1004
+ end
1005
+
1006
+ # Mirrors team membership set from a source user to a target user
1007
+ def sync_team_memberships(source_user_id, target_user_id)
1008
+ source_user = find_user(source_user_id)
1009
+ target_user = find_user(target_user_id)
1010
+ if source_user.nil? then
1011
+ @logger.warn " Source user: #{source_user_id} Not found. Skipping sync of permissions to #{target_user_id}."
1012
+ return
1013
+ elsif target_user.nil then
1014
+ @logger.warn " Target user: #{target_user_id} Not found. Skipping sync of permissions for #{target_user_id}."
1015
+ end
1016
+
1017
+ memberships_existing = target_user["TeamMemberships"]
1018
+ source_memberships = source_user["TeamMemberships"]
1019
+
1020
+ # build membership lists by Project ObjectID
1021
+ source_membership_oids = []
1022
+ source_memberships.each do | this_source_membership |
1023
+ source_membership_oids.push(get_membership_oid_from_membership(this_source_membership))
1024
+ end
1025
+
1026
+ memberships_existing_oids = []
1027
+ memberships_existing.each do | this_membership |
1028
+ memberships_existing_oids.push(get_membership_oid_from_membership(this_membership))
1029
+ end
1030
+
1031
+ # build Target User Permissions list by Project ObjectID
1032
+ # Needed to make sure that user to whom we're trying to set team membership for
1033
+ # is an editor
1034
+ permissions_existing = target_user.UserPermissions
1035
+ permissions_existing_by_project = {}
1036
+ permissions_existing.each do | this_permission |
1037
+ if this_permission._type == "ProjectPermission" then
1038
+ permissions_existing_by_project[this_permission.Project.ObjectID.to_s] = this_permission
1039
+ end
1040
+ end
1041
+
1042
+ memberships_to_add = []
1043
+ source_membership_oids.each do | this_membership_oid |
1044
+ if !memberships_existing_oids.include?(this_membership_oid) then
1045
+ memberships_to_add.push(this_membership_oid)
1046
+ end
1047
+ end
1048
+
1049
+ memberships_to_remove = []
1050
+ memberships_existing_oids.each do | this_membership_oid |
1051
+ if !source_membership_oids.include?(this_membership_oid) then
1052
+ memberships_to_remove.push(this_membership_oid)
1053
+ end
1054
+ end
1055
+
1056
+ number_memberships_added = 0
1057
+ memberships_to_add.each do | this_membership_oid |
1058
+
1059
+ this_permission = permissions_existing_by_project[this_membership_oid]
1060
+ if !this_permission.nil? then
1061
+ this_role = this_permission.Role
1062
+ if !this_role.eql?(EDITOR) && !this_role.eql(PROJECTADMIN) then
1063
+ @logger.warn " Target User: #{target_user_id} must be an Editor or Project Admin to make them a Team Member. Skipping."
1064
+ return
1065
+ end
1066
+
1067
+ this_project = find_project(this_membership_oid)
1068
+ if !this_project.nil? then
1069
+ number_memberships_added += 1
1070
+ this_project_name = this_project["Name"]
1071
+ @logger.info "Updating TeamMembership on #{this_project_name} from #{source_user_id} to: #{target_user_id}."
1072
+ update_team_membership(target_user, this_membership_oid, this_project_name, TEAMMEMBER_YES)
1073
+ end
1074
+ end
1075
+
1076
+ end
1077
+
1078
+ number_memberships_removed = 0
1079
+ memberships_to_remove.each do | this_membership_oid |
1080
+ this_project = find_project(this_membership_oid)
1081
+ if !this_project.nil? then
1082
+ number_memberships_removed += 1
1083
+ this_project_name = this_project["Name"]
1084
+ @logger.info "Removing TeamMembership on #{this_project_name} from #{target_user_id} since source: #{source_user_id} is not a TeamMember."
1085
+ update_team_membership(target_user, this_membership_oid, this_project_name, TEAMMEMBER_NO)
1086
+ end
1087
+ end
1088
+
1089
+ @logger.info "Team Memberships Added: #{number_memberships_added}; Team Memberships Removed: #{number_memberships_removed}"
1090
+
1091
+ end
1092
+
1093
+ def update_workspace_permissions(workspace, user, permission, new_user)
1094
+ if new_user or workspace_permissions_different?(workspace, user, permission)
1095
+ if @upgrade_only_mode then
1096
+ is_upgrade, existing_permission = is_workspace_permission_upgrade?(workspace, user, permission)
1097
+ if is_upgrade then
1098
+ update_permission_workspacelevel(workspace, user, permission)
1099
+ else
1100
+ @logger.info " #{user["UserName"]} #{workspace["Name"]} - upgrade_only_mode == true."
1101
+ @logger.warn " Existing Permission: #{existing_permission}"
1102
+ @logger.warn " Proposed Permission: #{permission}"
1103
+ @logger.info " Proposed Permission change would downgrade permissions. No permission updates applied."
1104
+ end
1105
+ else
1106
+ update_permission_workspacelevel(workspace, user, permission)
1107
+ end
1108
+ else
1109
+ @logger.info " #{user["UserName"]} #{workspace["Name"]} - Existing/new permission same. No permission updates applied."
1110
+ end
1111
+ end
1112
+
1113
+ def update_project_permissions(project, user, permission, new_user)
1114
+ if new_user or project_permissions_different?(project, user, permission)
1115
+ if @upgrade_only_mode then
1116
+ is_upgrade, existing_permission = is_project_permission_upgrade?(project, user, permission)
1117
+ if is_upgrade then
1118
+ update_permission_projectlevel(project, user, permission)
1119
+ else
1120
+ @logger.warn " #{user["UserName"]} #{project["Name"]} - upgrade_only_mode == true."
1121
+ @logger.warn " Existing Permission: #{existing_permission}"
1122
+ @logger.warn " Proposed Permission: #{permission}"
1123
+ @logger.warn " Proposed Permission change would downgrade permissions. No permission updates applied."
1124
+ end
1125
+ else
1126
+ update_permission_projectlevel(project, user, permission)
1127
+ end
1128
+ else
1129
+ @logger.info " #{user["UserName"]} #{project["Name"]} - Existing/new permission same. No permission updates applied."
1130
+ end
1131
+ end
1132
+
1133
+ def create_user(user_name, fields = user_fields)
1134
+
1135
+ new_user_obj = {}
1136
+
1137
+ new_user_obj["UserName"] = user_name.downcase
1138
+ new_user_obj["EmailAddress"] = user_name.downcase
1139
+
1140
+ fields.each_pair do | key, value |
1141
+ new_user_obj[key] = value
1142
+ end
1143
+
1144
+ new_user = nil
1145
+
1146
+ begin
1147
+ if @create_flag
1148
+ new_user = @rally.create(:user, new_user_obj)
1149
+ end
1150
+ @logger.info "Created Rally user #{user_name.downcase}"
1151
+ rescue
1152
+ @logger.error "Error creating user: #{$!}"
1153
+ raise $!
1154
+ end
1155
+
1156
+ # Grab full object of the created user and return so that we can use it later
1157
+ new_user_query = RallyAPI::RallyQuery.new()
1158
+ new_user_query.type = :user
1159
+ new_user_query.fetch = "UserName,FirstName,LastName,DisplayName,UserPermissions,Name,Role,Workspace,ObjectID,Project,ObjectID,TeamMemberships"
1160
+ new_user_query.query_string = "(UserName = \"#{user_name.downcase}\")"
1161
+ new_user_query.order = "UserName Asc"
1162
+
1163
+ query_results = @rally.find(new_user_query)
1164
+ new_user_created = query_results.first
1165
+
1166
+ # Cache the new user
1167
+ @cached_users[user_name.downcase] = new_user_created
1168
+ return new_user_created
1169
+ end
1170
+
1171
+ def update_user(user, fields = user_fields)
1172
+
1173
+ user_name = user["UserName"]
1174
+ begin
1175
+ if @create_flag
1176
+ updated_user = user.update(fields)
1177
+ end
1178
+ @logger.info "Updated Rally user #{user_name.downcase}"
1179
+ rescue
1180
+ @logger.error "Error creating user: #{$!}"
1181
+ raise $!
1182
+ end
1183
+
1184
+ # Grab full object of the created user and return so that we can use it later
1185
+ updated_user_query = RallyAPI::RallyQuery.new()
1186
+ updated_user_query.type = :user
1187
+ updated_user_query.fetch = "UserName,FirstName,LastName,DisplayName,UserPermissions,Name,Role,Workspace,ObjectID,Project,ObjectID,TeamMemberships"
1188
+ updated_user_query.query_string = "(UserName = \"#{user_name.downcase}\")"
1189
+ updated_user_query.order = "UserName Asc"
1190
+
1191
+ query_results = @rally.find(updated_user_query)
1192
+ updated_user_object = query_results.first
1193
+
1194
+ # Cache the new user
1195
+ @cached_users[user_name.downcase] = updated_user_object
1196
+ return
1197
+ end
1198
+
1199
+ def disable_user(user)
1200
+ if user.Disabled == 'False'
1201
+ if @create_flag
1202
+ fields = {}
1203
+ fields["Disabled"] = 'False'
1204
+ updated_user = @rally.update(:user, user._ref, fields) #by ref
1205
+ end
1206
+
1207
+ @logger.info "#{user["UserName"]} disabled in Rally"
1208
+ else
1209
+ @logger.info "#{user["UserName"]} already disabled from Rally"
1210
+ return false
1211
+ end
1212
+ return true
1213
+ end
1214
+
1215
+ def enable_user(user)
1216
+ if user.Disabled == 'True'
1217
+ fields = {}
1218
+ fields["Disabled"] = 'True'
1219
+ updated_user = @rally.update(:user, user._ref, fields) if @create_flag
1220
+ @logger.info "#{user["UserName"]} enabled in Rally"
1221
+ return true
1222
+ else
1223
+ @logger.info "#{user["UserName"]} already enabled in Rally"
1224
+ return false
1225
+ end
1226
+ end
1227
+
1228
+ # Checks to see if input record is using:
1229
+ # Text string (i.e. Editor, Viewer)
1230
+ # Or
1231
+ # User (i.e. john.doe@company.com)
1232
+ # as source of default permissions
1233
+ def check_default_permission_type(value)
1234
+
1235
+ type = :stringsource
1236
+ if !value.match(/@/).nil? then
1237
+ type = :usersource
1238
+ end
1239
+ return type
1240
+ end
1241
+
1242
+ # Pulls Project OID off of team membership _ref
1243
+ def get_membership_oid_from_membership(team_membership)
1244
+ this_membership_ref = team_membership._ref
1245
+ this_membership_oid = this_membership_ref.split("\/")[-1].split("\.")[0]
1246
+ return this_membership_oid
1247
+ end
1248
+
1249
+ # Checks to see if a user is a Team Member for a particular project
1250
+ def is_team_member(project_oid, user)
1251
+
1252
+ # Default values
1253
+ is_member = false
1254
+ return_value = "No"
1255
+
1256
+ team_memberships = user["TeamMemberships"]
1257
+
1258
+ # First check if team_memberships are nil then loop through and look for a match on
1259
+ # Project OID
1260
+ if team_memberships != nil then
1261
+
1262
+ team_memberships.each do |this_membership|
1263
+
1264
+ # Grab the Project OID off of the ref URL
1265
+ this_membership_oid = get_membership_oid_from_membership(this_membership)
1266
+ if this_membership_oid == project_oid then
1267
+ is_member = true
1268
+ end
1269
+ end
1270
+ end
1271
+
1272
+ if is_member then return_value = "Yes" end
1273
+ return return_value
1274
+ end
1275
+
1276
+ # Updates team membership. Note - this utilizes un-documented and un-supported Rally endpoint
1277
+ # that is not part of WSAPI REST
1278
+ # it also digs down into rally_api to directly PUT against this endpoint
1279
+ # not guaranteed to work forever
1280
+
1281
+ def update_team_membership(user, project_oid, project_name, team_member_setting)
1282
+
1283
+ # look up user
1284
+ these_team_memberships = user["TeamMemberships"]
1285
+ this_user_oid = user["ObjectID"]
1286
+
1287
+ # Default for whether user is member or not
1288
+ is_member = is_team_member(project_oid, user)
1289
+
1290
+ url_base = make_team_member_url(this_user_oid, project_oid)
1291
+
1292
+ # if User isn't a team member and update value is Yes then make them one
1293
+ if is_member == "No" && team_member_setting.downcase == TEAMMEMBER_YES.downcase then
1294
+
1295
+ # Construct payload object
1296
+ my_payload = {}
1297
+ my_team_member_setting = {}
1298
+ my_team_member_setting ["TeamMember"] = "true"
1299
+ my_payload["projectuser"] = my_team_member_setting
1300
+
1301
+ args = {:method => :put}
1302
+ args[:payload] = my_payload
1303
+
1304
+ # @rally_json_connection does a to_json on object to convert
1305
+ # payload object to JSON: {"projectuser":{"TeamMember":"true"}}
1306
+ response = @rally_json_connection.send_request(url_base, args)
1307
+ @logger.info " #{user["UserName"]} #{project_name} - Team Membership set to #{team_member_setting}"
1308
+
1309
+ # if User is a team member and update value is No then remove them from team
1310
+ elsif is_member == "Yes" && team_member_setting.downcase == TEAMMEMBER_NO.downcase then
1311
+
1312
+ # Construct payload object
1313
+ my_payload = {}
1314
+ my_team_member_setting = {}
1315
+ my_team_member_setting ["TeamMember"] = "false"
1316
+ my_payload["projectuser"] = my_team_member_setting
1317
+
1318
+ args = {:method => :put}
1319
+ args[:payload] = my_payload
1320
+
1321
+ # @rally_json_connection will convert payload object to JSON: {"projectuser":{"TeamMember":"false"}}
1322
+ response = @rally_json_connection.send_request(url_base, args)
1323
+ @logger.info " #{user["UserName"]} #{project_name} - Team Membership set to #{team_member_setting}"
1324
+ else
1325
+ @logger.info " #{user["UserName"]} #{project_name} - No creation of or changes to Team Membership"
1326
+ end
1327
+ end
1328
+
1329
+ # Create Admin, User, or Viewer permissions for a Workspace
1330
+ def create_workspace_permission(user, workspace, permission)
1331
+ # Keep backward compatibility of our old permission names
1332
+ if permission == VIEWER || permission == EDITOR
1333
+ permission = USER
1334
+ end
1335
+
1336
+ if permission != NOACCESS
1337
+ new_permission_obj = {}
1338
+ new_permission_obj["Workspace"] = workspace["_ref"]
1339
+ new_permission_obj["User"] = user._ref
1340
+ new_permission_obj["Role"] = permission
1341
+
1342
+ if @create_flag then new_permission = @rally.create(:workspacepermission, new_permission_obj) end
1343
+ end
1344
+ end
1345
+
1346
+ # Check to see if User has _any_ permission within a specific workspace
1347
+ def does_user_have_workspace_permission?(workspace, user)
1348
+ # set default return value
1349
+ workspace_permission_exists = false
1350
+
1351
+ use_cache = false
1352
+
1353
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1354
+ if @cached_users != nil then
1355
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1356
+ end
1357
+
1358
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1359
+ if use_cache then
1360
+
1361
+ this_user = @cached_users[user.UserName]
1362
+
1363
+ # loop through permissions and look to see if there's an existing permission for this
1364
+ # workspace, and if so, has it changed
1365
+ user_permissions = this_user.UserPermissions
1366
+ user_permissions.each do | this_permission |
1367
+ if this_permission._type == "WorkspacePermission" then
1368
+ if this_permission.Workspace.ObjectID.to_s == workspace["ObjectID"].to_s then
1369
+ workspace_permission_exists = true
1370
+ break
1371
+ end
1372
+ end
1373
+ end
1374
+
1375
+ else # Cache does not exist or user isn't in it - query info from Rally
1376
+ workspace_permission_query = RallyAPI::RallyQuery.new()
1377
+ workspace_permission_query.type = :workspacepermission
1378
+ workspace_permission_query.fetch = "Workspace,Name,ObjectID,Role,User"
1379
+ workspace_permission_query.page_size = 200 #optional - default is 200
1380
+ workspace_permission_query.order = "Name Asc"
1381
+ workspace_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1382
+
1383
+ query_results = @rally.find(workspace_permission_query)
1384
+
1385
+ workspace_permission_exists = false
1386
+
1387
+ # Look to see if any existing WorkspacePermissions for this user match the one we're examining
1388
+ # If so, check to see if the workspace permissions are any different
1389
+ query_results.each { |wp|
1390
+ if ( wp.Workspace.ObjectID == workspace["ObjectID"])
1391
+ workspace_permission_exists = true
1392
+ break
1393
+ end
1394
+ }
1395
+ end
1396
+ return workspace_permission_exists
1397
+ end
1398
+
1399
+ # Check to see if User has _any_ permission within a specific project
1400
+ def does_user_have_project_permission?(project, user)
1401
+
1402
+ # set default return value
1403
+ project_permission_exists = false
1404
+
1405
+ use_cache = false
1406
+
1407
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1408
+ if @cached_users != nil then
1409
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1410
+ end
1411
+
1412
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1413
+ if use_cache then
1414
+
1415
+ this_user = @cached_users[user.UserName]
1416
+
1417
+ # loop through permissions and look to see if there's an existing permission for this
1418
+ # workspace, and if so, has it changed
1419
+
1420
+ user_permissions = this_user.UserPermissions
1421
+
1422
+ user_permissions.each do |this_permission|
1423
+
1424
+ if this_permission._type == "ProjectPermission" then
1425
+ # user has existing permissions in this project - let's compare new role against existing
1426
+ if this_permission.Project.ObjectID.to_s == project["ObjectID"].to_s then
1427
+ project_permission_exists = true
1428
+ break
1429
+ end
1430
+ end
1431
+ end
1432
+
1433
+ else # Cache does not exist or user isn't in it - query info from Rally
1434
+
1435
+ project_permission_query = RallyAPI::RallyQuery.new()
1436
+ project_permission_query.type = :projectpermission
1437
+ project_permission_query.fetch = "Project,Name,ObjectID,Role,User"
1438
+ project_permission_query.page_size = 200 #optional - default is 200
1439
+ project_permission_query.order = "Name Asc"
1440
+ project_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1441
+
1442
+ query_results = @rally.find(project_permission_query)
1443
+
1444
+ project_permission_exists = false
1445
+
1446
+ # Look to see if any existing ProjectPermissions for this user match the one we're examining
1447
+ # If so, check to see if the project permissions are any different
1448
+ query_results.each { |pp|
1449
+
1450
+ if ( pp.Project.ObjectID == project["ObjectID"])
1451
+ project_permission_exists = true
1452
+ break
1453
+ end
1454
+ }
1455
+ end
1456
+ return project_permission_exists
1457
+ end
1458
+
1459
+ #--------- Private methods --------------
1460
+ private
1461
+
1462
+ # Takes the name of the permission and returns the last token which is the permission
1463
+ def parse_permission(name)
1464
+ if name.reverse.index(VIEWER.reverse)
1465
+ return VIEWER
1466
+ elsif name.reverse.index(EDITOR.reverse)
1467
+ return EDITOR
1468
+ elsif name.reverse.index(USER.reverse)
1469
+ return USER
1470
+ elsif name.reverse.index(ADMIN.reverse)
1471
+ return ADMIN
1472
+ else
1473
+ @logger.info "Error in parsing permission"
1474
+ end
1475
+ nil
1476
+ end
1477
+
1478
+ # Creates a team membership URL for request against (undocumented, non-WSAPI and non-supported)
1479
+ # team membership endpoint.
1480
+ # Method: PUT
1481
+ # URL Format:
1482
+ # https://rally1.rallydev.com/slm/webservice/x/project/12345678910/projectuser/12345678911.js
1483
+ # Payload: {"projectuser":{"TeamMember":"true"}}
1484
+ # Where 12345678910 => Project OID
1485
+ # And 12345678911 => User OID
1486
+
1487
+ def make_team_member_url(input_user_oid, input_project_oid)
1488
+
1489
+ rally_url = @rally.rally_url + "/webservice/"
1490
+ wsapi_version = @rally.wsapi_version
1491
+
1492
+ make_team_member_url = rally_url + wsapi_version +
1493
+ "/project/" + input_project_oid.to_s +
1494
+ "/projectuser/" + input_user_oid.to_s + ".js"
1495
+
1496
+ return make_team_member_url
1497
+ end
1498
+
1499
+ # check if the new permissions are an upgrade vs. what the user currently has
1500
+ def is_project_permission_upgrade?(project, user, new_permission)
1501
+
1502
+ # set default return value
1503
+ project_permission_upgrade = false
1504
+
1505
+ # set a default existing_permission conservatively at Project Admin
1506
+ existing_permission = PROJECTADMIN_READ
1507
+ use_cache = false
1508
+
1509
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1510
+ if @cached_users != nil then
1511
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1512
+ end
1513
+
1514
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1515
+ if use_cache then
1516
+
1517
+ number_matching_projects = 0
1518
+ this_user = @cached_users[user.UserName]
1519
+
1520
+ # loop through permissions and look to see if there's an existing permission for this
1521
+ # workspace, and if so, is our proposed change an upgrade
1522
+ user_permissions = this_user.UserPermissions
1523
+ user_permissions.each do | this_permission |
1524
+
1525
+ if this_permission._type == "ProjectPermission" then
1526
+ # user has existing permissions in this project - let's compare new role against existing
1527
+ if this_permission.Project.ObjectID.to_s == project["ObjectID"].to_s then
1528
+ existing_permission = this_permission.Role
1529
+ case existing_permission
1530
+
1531
+ # you can't upgrade a PROJECTADMIN, so return false
1532
+ when PROJECTADMIN_READ
1533
+ project_permission_upgrade = false
1534
+ when EDITOR
1535
+ if new_permission.eql?(PROJECTADMIN_READ) then
1536
+ project_permission_upgrade = true
1537
+ end
1538
+ when VIEWER
1539
+ if new_permission.eql?(EDITOR) || new_permission.eql?(PROJECTADMIN_READ) then
1540
+ project_permission_upgrade = true
1541
+ end
1542
+ # NOACCESS is denoted by non-existence of a permission so we don't
1543
+ # check that here
1544
+ end
1545
+ number_matching_projects += 1
1546
+ end
1547
+ end
1548
+ end
1549
+
1550
+ # This is a new project permission - set the changed bit to true
1551
+ if number_matching_projects == 0 then
1552
+ project_permission_upgrade = true
1553
+ end
1554
+
1555
+ else # Cache does not exist or user isn't in it - query info from Rally
1556
+
1557
+ project_permission_query = RallyAPI::RallyQuery.new()
1558
+ project_permission_query.type = :projectpermission
1559
+ project_permission_query.fetch = "Project,Name,ObjectID,Role,User"
1560
+ project_permission_query.page_size = 200 #optional - default is 200
1561
+ project_permission_query.order = "Name Asc"
1562
+ project_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1563
+
1564
+ query_results = @rally.find(project_permission_query)
1565
+
1566
+ project_permission_upgrade = false
1567
+ number_matching_projects = 0
1568
+
1569
+ # Look to see if any existing ProjectPermissions for this user match the one we're examining
1570
+ # If so, check to see if the project permissions are any different
1571
+ query_results.each { |pp|
1572
+
1573
+ if ( pp.Project.ObjectID == project["ObjectID"])
1574
+ number_matching_projects+=1
1575
+ existing_permission = pp.Role
1576
+ case existing_permission
1577
+ # you can't upgrade a PROJECTADMIN, so return false
1578
+ when PROJECTADMIN_READ
1579
+ project_permission_upgrade = false
1580
+ when EDITOR
1581
+ if new_permission.eql?(PROJECTADMIN_READ) then
1582
+ project_permission_upgrade = true
1583
+ end
1584
+ when VIEWER
1585
+ if new_permission.eql?(EDITOR) || new_permission.eql?(PROJECTADMIN_READ) then
1586
+ project_permission_upgrade = true
1587
+ end
1588
+ # NOACCESS is denoted by non-existence of a permission so we don't
1589
+ # check that here
1590
+ end
1591
+ end
1592
+ }
1593
+ # This is a new project permission - set the upgrade bit to true
1594
+ if number_matching_projects == 0 then project_permission_upgrade = true end
1595
+ end
1596
+ return project_permission_upgrade, existing_permission
1597
+ end
1598
+
1599
+ def is_workspace_permission_upgrade?(workspace, user, new_permission)
1600
+
1601
+ # set default return values
1602
+ workspace_permission_upgrade = false
1603
+
1604
+ # set a default existing_permission conservatively at Admin
1605
+ existing_permission = ADMIN
1606
+ use_cache = false
1607
+
1608
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1609
+ if @cached_users != nil then
1610
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1611
+ end
1612
+
1613
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1614
+ if use_cache then
1615
+
1616
+ number_matching_workspaces = 0
1617
+ this_user = @cached_users[user.UserName]
1618
+
1619
+ # loop through permissions and look to see if there's an existing permission for this
1620
+ # workspace, and if so, has it changed
1621
+ user_permissions = this_user.UserPermissions
1622
+ user_permissions.each do | this_permission |
1623
+ if this_permission._type == "WorkspacePermission" then
1624
+ if this_permission.Workspace.ObjectID.to_s == workspace["ObjectID"].to_s then
1625
+ existing_permission = this_permission.Role
1626
+ number_matching_workspaces += 1
1627
+ case existing_permission
1628
+ # Can't upgrade an admin, so return false
1629
+ when ADMIN
1630
+ workspace_permission_upgrade = false
1631
+ when USER
1632
+ if new_permission.eql?(ADMIN) then
1633
+ workspace_permission_upgrade = true
1634
+ end
1635
+ # No-Access is denoted by an absence of a Permission - we don't need to check that here
1636
+ end
1637
+ end
1638
+ end
1639
+ end
1640
+ # This is a new workspace permission - set the changed bit to true
1641
+ if number_matching_workspaces == 0 then workspace_permission_upgrade = true end
1642
+
1643
+ else # Cache does not exist or user isn't in it - query info from Rally
1644
+ workspace_permission_query = RallyAPI::RallyQuery.new()
1645
+ workspace_permission_query.type = :workspacepermission
1646
+ workspace_permission_query.fetch = "Workspace,Name,ObjectID,Role,User"
1647
+ workspace_permission_query.page_size = 200 #optional - default is 200
1648
+ workspace_permission_query.order = "Name Asc"
1649
+ workspace_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1650
+
1651
+ query_results = @rally.find(workspace_permission_query)
1652
+
1653
+ workspace_permission_upgrade = false
1654
+ number_matching_workspaces = 0
1655
+
1656
+ # Look to see if any existing WorkspacePermissions for this user match the one we're examining
1657
+ # If so, check to see if the workspace permissions are any different
1658
+ query_results.each { |wp|
1659
+ if ( wp.Workspace.ObjectID == workspace["ObjectID"])
1660
+ existing_permission = wp.Role
1661
+ number_matching_workspaces+=1
1662
+ case existing_permission
1663
+ # Can't upgrade an admin, so return false
1664
+ when ADMIN
1665
+ workspace_permission_upgrade = false
1666
+ when USER
1667
+ if new_permission.eql?(ADMIN) then
1668
+ workspace_permission_upgrade = true
1669
+ end
1670
+ # No-Access is denoted by an absence of a Permission - we don't need to check that here
1671
+ end
1672
+ end
1673
+ }
1674
+ # This is a new workspace permission - set the changed bit to true
1675
+ if number_matching_workspaces == 0 then workspace_permission_upgrade = true end
1676
+ end
1677
+ return workspace_permission_upgrade, existing_permission
1678
+ end
1679
+
1680
+ # check if the new permissions are different than what the user currently has
1681
+ # if we don't do this, we will delete and recreate permissions each time and that
1682
+ # will make the revision history on user really, really, really, really ugly
1683
+ def project_permissions_different?(project, user, new_permission)
1684
+
1685
+ # set default return value
1686
+ project_permission_changed = false
1687
+ use_cache = false
1688
+
1689
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1690
+ if @cached_users != nil then
1691
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1692
+ end
1693
+
1694
+ if use_cache then
1695
+ number_matching_projects = 0
1696
+ this_user = @cached_users[user.UserName]
1697
+
1698
+ # loop through permissions and look to see if there's an existing permission for this
1699
+ # workspace, and if so, has it changed
1700
+ user_permissions = this_user.UserPermissions
1701
+ user_permissions.each do | this_permission |
1702
+
1703
+ if this_permission._type == "ProjectPermission" then
1704
+ # user has existing permissions in this project - let's compare new role against existing
1705
+ if this_permission.Project.ObjectID.to_s == project["ObjectID"].to_s then
1706
+ number_matching_projects += 1
1707
+ if this_permission.Role != new_permission then
1708
+ project_permission_changed = true
1709
+ end
1710
+ end
1711
+ end
1712
+ end
1713
+
1714
+ # This is a new project permission - set the changed bit to true
1715
+ if number_matching_projects == 0 then
1716
+ project_permission_changed = true
1717
+ end
1718
+
1719
+ else # Cache does not exist or user isn't in it - query info from Rally
1720
+
1721
+ project_permission_query = RallyAPI::RallyQuery.new()
1722
+ project_permission_query.type = :projectpermission
1723
+ project_permission_query.fetch = "Project,Name,ObjectID,Role,User"
1724
+ project_permission_query.page_size = 200 #optional - default is 200
1725
+ project_permission_query.order = "Name Asc"
1726
+ project_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1727
+
1728
+ query_results = @rally.find(project_permission_query)
1729
+
1730
+ project_permission_changed = false
1731
+ number_matching_projects = 0
1732
+
1733
+ # Look to see if any existing ProjectPermissions for this user match the one we're examining
1734
+ # If so, check to see if the project permissions are any different
1735
+ query_results.each { |pp|
1736
+
1737
+ if ( pp.Project.ObjectID == project["ObjectID"])
1738
+ number_matching_projects+=1
1739
+ if pp.Role != new_permission then project_permission_changed = true end
1740
+ end
1741
+ }
1742
+ # This is a new project permission - set the changed bit to true
1743
+ if number_matching_projects == 0 then project_permission_changed = true end
1744
+ end
1745
+ return project_permission_changed
1746
+ end
1747
+
1748
+ # check if the new permissions are different than what the user currently has
1749
+ # if we don't do this, we will delete and recreate permissions each time and that
1750
+ # will make the revision history on user really, really, really, really ugly
1751
+
1752
+ def workspace_permissions_different?(workspace, user, new_permission)
1753
+
1754
+ # set default return value
1755
+ workspace_permission_changed = false
1756
+
1757
+ use_cache = false
1758
+
1759
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1760
+ if @cached_users != nil then
1761
+ if @cached_users.has_key?(user.UserName) then use_cache = true end
1762
+ end
1763
+
1764
+ # first try to lookup against cached user list -- much faster than re-querying Rally
1765
+ if use_cache then
1766
+
1767
+ number_matching_workspaces = 0
1768
+ this_user = @cached_users[user.UserName]
1769
+
1770
+ # loop through permissions and look to see if there's an existing permission for this
1771
+ # workspace, and if so, has it changed
1772
+ user_permissions = this_user.UserPermissions
1773
+ user_permissions.each do | this_permission |
1774
+ if this_permission._type == "WorkspacePermission" then
1775
+ if this_permission.Workspace.ObjectID.to_s == workspace["ObjectID"].to_s then
1776
+ number_matching_workspaces += 1
1777
+ if this_permission.Role != new_permission then workspace_permission_changed = true end
1778
+ end
1779
+ end
1780
+ end
1781
+ # This is a new workspace permission - set the changed bit to true
1782
+ if number_matching_workspaces == 0 then workspace_permission_changed = true end
1783
+
1784
+ else # Cache does not exist or user isn't in it - query info from Rally
1785
+ workspace_permission_query = RallyAPI::RallyQuery.new()
1786
+ workspace_permission_query.type = :workspacepermission
1787
+ workspace_permission_query.fetch = "Workspace,Name,ObjectID,Role,User"
1788
+ workspace_permission_query.page_size = 200 #optional - default is 200
1789
+ workspace_permission_query.order = "Name Asc"
1790
+ workspace_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1791
+
1792
+ query_results = @rally.find(workspace_permission_query)
1793
+
1794
+ workspace_permission_changed = false
1795
+ number_matching_workspaces = 0
1796
+
1797
+ # Look to see if any existing WorkspacePermissions for this user match the one we're examining
1798
+ # If so, check to see if the workspace permissions are any different
1799
+ query_results.each { |wp|
1800
+ if ( wp.Workspace.ObjectID == workspace["ObjectID"])
1801
+ number_matching_workspaces+=1
1802
+ if wp.Role != new_permission then workspace_permission_changed = true end
1803
+ end
1804
+ }
1805
+ # This is a new workspace permission - set the changed bit to true
1806
+ if number_matching_workspaces == 0 then workspace_permission_changed = true end
1807
+ end
1808
+ return workspace_permission_changed
1809
+ end
1810
+
1811
+ # Create User or Viewer permissions for a Project
1812
+ def create_project_permission(user, project, permission)
1813
+ # Keep backward compatibility of our old permission names
1814
+ if permission == USER
1815
+ permission = EDITOR
1816
+ end
1817
+
1818
+ if permission != NOACCESS
1819
+ this_workspace = project["Workspace"]
1820
+ new_permission_obj = {}
1821
+ new_permission_obj["Workspace"] = this_workspace["_ref"]
1822
+ new_permission_obj["Project"] = project["_ref"]
1823
+ new_permission_obj["User"] = user._ref
1824
+ new_permission_obj["Role"] = permission
1825
+
1826
+ if @create_flag then new_permission = @rally.create(:projectpermission, new_permission_obj) end
1827
+ end
1828
+ end
1829
+
1830
+ # Project permissions are automatically deleted in this case
1831
+ def delete_workspace_permission(user, workspace)
1832
+
1833
+ if @upgrade_only_mode then
1834
+ @logger.info " #{user["UserName"]} #{workspace["Name"]} - upgrade_only_mode == true."
1835
+ @logger.warn " Proposed Permission: #{NOACCESS}"
1836
+ @logger.info " Proposed Permission change would downgrade permissions. No permission updates applied."
1837
+ return
1838
+ end
1839
+
1840
+ # queries on permissions are a bit limited - to only one filter parameter
1841
+ workspace_permission_query = RallyAPI::RallyQuery.new()
1842
+ workspace_permission_query.type = :workspacepermission
1843
+ workspace_permission_query.fetch = "Workspace,Name,ObjectID,Role,User,UserName"
1844
+ workspace_permission_query.page_size = 200 #optional - default is 200
1845
+ workspace_permission_query.order = "Name Asc"
1846
+ workspace_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1847
+
1848
+ query_results = @rally.find(workspace_permission_query)
1849
+
1850
+ query_results.each do | this_workspace_permission |
1851
+
1852
+ this_workspace = this_workspace_permission.Workspace
1853
+ this_workspace_oid = this_workspace["ObjectID"].to_s
1854
+
1855
+ if this_workspace_permission != nil && this_workspace_oid == workspace["ObjectID"].to_s
1856
+ begin
1857
+ @rally.delete(this_workspace_permission["_ref"])
1858
+ rescue Exception => ex
1859
+ this_user = this_workspace_permission.User
1860
+ this_user_name = this_user.Name
1861
+
1862
+ @logger.warn "Cannot remove WorkspacePermission: #{this_workspace_permission.Name}."
1863
+ @logger.warn "WorkspacePermission either already NoAccess, or would remove the only WorkspacePermission in Subscription."
1864
+ @logger.warn "User #{this_user_name} must have access to at least one Workspace within the Subscription."
1865
+ end
1866
+ end
1867
+ end
1868
+ end
1869
+
1870
+ def delete_project_permission(user, project)
1871
+
1872
+ if @upgrade_only_mode then
1873
+ @logger.info " #{user["UserName"]} #{project["Name"]} - upgrade_only_mode == true."
1874
+ @logger.warn " Proposed Permission: #{NOACCESS}"
1875
+ @logger.info " Proposed Permission change would downgrade permissions. No permission updates applied."
1876
+ return
1877
+ end
1878
+
1879
+ # queries on permissions are a bit limited - to only one filter parameter
1880
+ project_permission_query = RallyAPI::RallyQuery.new()
1881
+ project_permission_query.type = :projectpermission
1882
+ project_permission_query.fetch = "Project,Name,ObjectID,Role,User,UserName"
1883
+ project_permission_query.page_size = 200 #optional - default is 200
1884
+ project_permission_query.order = "Name Asc"
1885
+ project_permission_query.query_string = "(User.UserName = \"" + user.UserName + "\")"
1886
+
1887
+ query_results = @rally.find(project_permission_query)
1888
+ query_results.each do | this_project_permission |
1889
+
1890
+ this_project = this_project_permission.Project
1891
+ this_project_oid = this_project.ObjectID.to_s
1892
+
1893
+ if this_project_permission != nil && this_project_oid == project["ObjectID"].to_s
1894
+ begin
1895
+ @rally.delete(this_project_permission["_ref"])
1896
+ rescue Exception => ex
1897
+ this_user = this_project_permission.User
1898
+ this_user_name = this_user.Name
1899
+
1900
+ @logger.warn "Cannot remove ProjectPermission: #{this_project_permission.Name}."
1901
+ @logger.warn "ProjectPermission either already NoAccess, or would remove the only ProjectPermission in Workspace."
1902
+ @logger.warn "User #{this_user_name} must have access to at least one Project within the Workspace."
1903
+ end
1904
+ end
1905
+ end
1906
+ end
1907
+
1908
+ def update_permission_workspacelevel(workspace, user, permission)
1909
+ @logger.info " #{user.UserName} #{workspace["Name"]} - Permission set to #{permission}"
1910
+ if permission == ADMIN
1911
+ create_workspace_permission(user, workspace, permission)
1912
+ elsif permission == NOACCESS
1913
+ delete_workspace_permission(user, workspace)
1914
+ elsif permission == USER || permission == VIEWER || permission == EDITOR
1915
+ create_workspace_permission(user, workspace, permission)
1916
+ else
1917
+ @logger.error "Invalid Permission - #{permission}"
1918
+ end
1919
+ end
1920
+
1921
+ def update_permission_projectlevel(project, user, permission)
1922
+ @logger.info " #{user.UserName} #{project["Name"]} - Permission set to #{permission}"
1923
+ if permission == PROJECTADMIN_READ
1924
+ # Hopefully temporary re-cast of "Admin" to "Project Admin"
1925
+ permission = PROJECTADMIN_CREATE
1926
+ create_project_permission(user, project, permission)
1927
+ elsif permission == NOACCESS
1928
+ delete_project_permission(user, project)
1929
+ elsif permission == USER || permission == VIEWER || permission == EDITOR
1930
+ create_project_permission(user, project, permission)
1931
+ else
1932
+ @logger.error "Invalid Permission - #{permission}"
1933
+ end
1934
+ end
1935
+
1936
+ end
1937
+ end