rs_user_policy 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. data/LICENSE.txt +20 -0
  2. data/README.rdoc +74 -0
  3. data/bin/rs_user_policy +188 -0
  4. metadata +89 -0
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Ryan J. Geyer
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ 'Software'), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,74 @@
1
+ = rs_user_policy
2
+
3
+ A useful tool for managing many users across many child accounts in a RightScale Enterprise Edition
4
+
5
+ == Usage
6
+
7
+ The binary contained in this gem accepts two files as inputs to determine it's behavior. The first, is a policy JSON file which specifies the permissions to be applied to users.
8
+
9
+ Policy JSON should be in the following format
10
+
11
+ {
12
+ "role_name": {
13
+ "default": [<permissions_here>],
14
+ "/api/accounts/12345": [<permissions_here>]
15
+ }
16
+ }
17
+
18
+ Here the "role_name" is what can be assigned to a user. The keys of the hash ("default" and "/api/accounts/12345") refer to the account(s) the role should have access to, and the value for those keys is an array of permissions that should be assigned for that role in that account.
19
+
20
+ The "default" account will apply to all accounts encountered.
21
+
22
+ The second input file is the user assignments JSON file which assigns users to roles using their email.
23
+
24
+ User Assignment JSON should be in the following format
25
+
26
+ {
27
+ "ryan.geyer@rightscale.com": "role_name"
28
+ }
29
+
30
+ There are two default roles which do not need to be defined in the policy file. "immutable" which indicates that no changes should be performed on the user, and "delete" which indicates that all permissions should be removed for the user in all accounts.
31
+
32
+ Many email:role pairs can be specified. in the user assignments JSON
33
+
34
+ So, given a policy file like;
35
+
36
+ {
37
+ "team1": {
38
+ "default": ['observer', 'actor'],
39
+ "/api/accounts/12345": ['observer', 'actor', 'server_login', 'admin']
40
+ },
41
+ "team2": {
42
+ "default": ['observer', 'lite_user'],
43
+ "/api/accounts/23456": ['observer', 'actor']
44
+ }
45
+ }
46
+
47
+ And a user assignments file like;
48
+
49
+ {
50
+ "user1@email.com": "team1",
51
+ "user2@email.com": "team2"
52
+ }
53
+
54
+ And operating on the accounts 12345 and 23456;
55
+ user1 will be assigned observer and actor rights on account 23456, and observer, actor, server_login, and admin rights on account 12345
56
+ user2 will be assigned observer and lite_user rights on account 12345, and observer and actor rights on account 23456
57
+
58
+ Got that? Cool!
59
+
60
+ == Output
61
+
62
+ When the script is run, it will produce two JSON files as output.
63
+
64
+ First is the audit_log-<timestamp>.json file. This will contain a history of all actions taken on all users. If --dry-run is specified, it will show the changes which *would* have been performed.
65
+
66
+ Second is the user_assignments-<timestamp>.json file. This will be a combination of the users read in from JSON in file specified by the --user-assignments option, plus any new users discovered in the accounts operated on. New users will be assigned the "immutable" role. This allows you to run rs_user_policy with the --dry-run option, or with no user assignments input to discover users, then assign roles to those users in the produced JSON, then use that file as the --user-assignments input for a subsequent run.
67
+
68
+ == TODO
69
+
70
+ * Allow a user to belong to more than one "role"
71
+
72
+ == Copyright
73
+
74
+ Copyright (c) 2012 Ryan J. Geyer. See LICENSE.txt for further details.
@@ -0,0 +1,188 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (c) 2012 Ryan J. Geyer
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'trollop'
25
+ require 'right_api_client'
26
+ require 'logger'
27
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'audit_log'))
28
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'utilities'))
29
+
30
+ opts = Trollop::options do
31
+ banner = "Manages users across many different child accounts of a RightScale Enterprise Master Account"
32
+
33
+ opt :rs_email, "You RightScale User Email Address", :type => :string, :required => true
34
+ opt :rs_pass, "Your RightScale User Password", :type => :string, :required => true
35
+ opt :rs_acct_num, "A RightScale Enterprise Master Account ID", :type => :string, :required => true
36
+ opt :policy, "The path to a JSON file containing the role to permissions policy to enforce", :type => :string, :required => true
37
+ opt :user_assignments, "The path to a JSON file containing email address => role pairs for user assignments", :type => :string
38
+ opt :dry_run, "A flag indicating that no changes should be made, only the user_assignments.json should be evaluated (or created) and the audit_log.json produced"
39
+ end
40
+
41
+ log = Logger.new(STDOUT)
42
+ timestamp = Time::now.to_i
43
+ deleted_users = []
44
+ accounts = []
45
+ user_href_resource_map = {}
46
+ user_email_resource_map = {}
47
+ permission_delete_order = [
48
+ 'enterprise_admin',
49
+ 'admin',
50
+ 'security_manager',
51
+ 'actor',
52
+ 'billing',
53
+ 'server_login',
54
+ 'publisher',
55
+ 'designer',
56
+ 'library',
57
+ 'lite_user',
58
+ 'observer'
59
+ ]
60
+
61
+ user_assignments_output = "user_assignments-#{timestamp}.json"
62
+
63
+ audit_log = AuditLog.new opts.merge(:timestamp => timestamp)
64
+
65
+ client = RightApi::Client.new(:email => opts[:rs_email], :password => opts[:rs_pass], :account_id => opts[:rs_acct_num])
66
+ master_account = client.accounts(:id => opts[:rs_acct_num]).show()
67
+ client.users().index.each do |user|
68
+ user_href_resource_map[user.href] = user
69
+ user_email_resource_map[user.email] = user
70
+ end
71
+
72
+ accounts << {:client => client, :href => master_account.href, :name => master_account.name}
73
+
74
+ log.info("The dry_run option was selected, no action will be taken, but the user_assignments output and audit_log files will be written reflecting the actions which would have been taken") if opts[:dry_run]
75
+
76
+ log.info("Operating on the Enterprise Master Account #{master_account.name}")
77
+
78
+ if File.exists? opts[:policy]
79
+ # TODO: Validate policy file format
80
+ policy = JSON.parse(File.read(opts[:policy]))
81
+ else
82
+ log.fatal("The policy file named #{opts[:policy]} was not found!")
83
+ exit 1
84
+ end
85
+
86
+ user_assignments = {}
87
+ if opts[:user_assignments]
88
+ if File.exists? opts[:user_assignments]
89
+ user_assignments = JSON.parse(File.read(opts[:user_assignments]))
90
+ else
91
+ log.warn("The user_assigments file named #{opts[:user_assignments]} was not found. All users will be treated as immutable and written to the user_assigments output file.")
92
+ end
93
+ else
94
+ log.warn("No user_assignments file was specified. All users will be treated as immutable and written to the user_assigments output file.")
95
+ end
96
+
97
+ begin
98
+ client.child_accounts().index.each do |child|
99
+ child_client = RightApi::Client.new(:email => opts[:rs_email], :password => opts[:rs_pass], :account_id => Utilities.id_from_href(child.href))
100
+ accounts << {:client => child_client, :href => child.href, :name => child.name}
101
+
102
+ child_client.users().index.each do |user|
103
+ user_href_resource_map[user.href] = user
104
+ user_email_resource_map[user.email] = user
105
+ end
106
+ end
107
+ rescue RightApi::Exceptions::ApiException => e
108
+ if e.message =~ /Permission denied/
109
+ log.warn("#{master_account.name} is not an Enterprise Master, or you do not have the enterprise_manager permission. No child accounts will be operated on.")
110
+ else
111
+ raise e
112
+ end
113
+ end
114
+
115
+ accounts.each do |account|
116
+ child_client = account[:client]
117
+ users = {}
118
+ user_email_resource_map.each do |email,user|
119
+ users[email] = {}
120
+ end
121
+
122
+ log.info("#{account[:name]} - #{account[:href]}")
123
+
124
+ permissions = child_client.permissions().index
125
+ log.info("There are #{permissions.length} unique permissions")
126
+ permissions.each do |permission|
127
+ user_email = user_href_resource_map[permission.user.href].email || permission.user.href
128
+ users[user_email] = {} unless users.key?(user_email)
129
+ users[user_email][permission.role_title] = permission.href
130
+ end
131
+ log.info("There are #{users.length} unique Users")
132
+
133
+ users.each do |email,user|
134
+ unless user_assignments.key?(email)
135
+ user_assignments[email] = 'immutable'
136
+ end
137
+ # Allow some different options about possibly prompting the user etc
138
+ # TODO: Put this in a begin/rescue/end that allows stuff to continue and create an audit_log in the event of an error
139
+ case user_assignments[email]
140
+ when 'immutable'
141
+ # By design, do nothing
142
+ when 'delete'
143
+ # Note: This is an explicit delete across all master and child accounts
144
+ # a user can also be effectively deleted if they have an empty list of
145
+ # permissions for a particular account
146
+ unless opts[:dry_run]
147
+ log.debug("Gonna delete #{JSON.pretty_generate(user)}")
148
+
149
+ Utilities.yield_on_keys_in_order(permission_delete_order, user) do |role_title,perm_href|
150
+ log.debug("I KEELZ U PERMISSION! #{email} - #{role_title}")
151
+ child_client.permissions(:id => Utilities.id_from_href(perm_href)).destroy()
152
+ deleted_users << email
153
+ end
154
+ end
155
+ audit_log.add_entry(email, account[:name], 'deleted', 'deleted')
156
+ else
157
+ user_policy = []
158
+ user_policy = policy[user_assignments[email]][account[:href]] if policy[user_assignments[email]].key?(account[:href])
159
+ user_policy = policy[user_assignments[email]]['default'] if policy[user_assignments[email]].key?('default')
160
+ removed = user.keys - user_policy
161
+ added = user_policy - user.keys
162
+ changes = "-#{removed} +#{added}"
163
+ unless opts[:dry_run]
164
+ # Convert the role_title array into a hash with the hrefs
165
+ remove_hash = Hash[removed.map {|role| [role, user[role]]}]
166
+
167
+ Utilities.yield_on_keys_in_order(permission_delete_order, remove_hash) do |role_title, href|
168
+ child_client.permissions(:id => Utilities.id_from_href(href)).destroy()
169
+ end
170
+
171
+ if added.length > 0
172
+ add_hash = Hash[added.map {|x| [x, nil]}]
173
+ Utilities.yield_on_keys_in_order(['observer'], add_hash) do |role_title,foo|
174
+ child_client.permissions.create({'permission[user_href]' => user_email_resource_map[email].href, 'permission[role_title]' => role_title})
175
+ end
176
+ end
177
+ end
178
+ audit_log.add_entry(email, account[:name], 'update_permissions', changes) unless removed.length + added.length == 0
179
+ end
180
+ end
181
+ end
182
+
183
+ deleted_users.each do |email|
184
+ user_assignments.delete(email)
185
+ end
186
+
187
+ File.open(user_assignments_output, 'w') {|f| f.write(JSON.pretty_generate(user_assignments))}
188
+ audit_log.write_file
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rs_user_policy
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Ryan J. Geyer
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-11-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: right_api_client
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.5.9
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.5.9
30
+ - !ruby/object:Gem::Dependency
31
+ name: trollop
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: '1.16'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: '1.16'
46
+ description: Manages users across many different child accounts of a RightScale Enterprise
47
+ Master Account
48
+ email: ryan.geyer@rightscale.com
49
+ executables:
50
+ - rs_user_policy
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - bin/rs_user_policy
55
+ - LICENSE.txt
56
+ - README.rdoc
57
+ homepage: http://www.rightscale.com
58
+ licenses:
59
+ - MIT
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ segments:
71
+ - 0
72
+ hash: -1869146196787316308
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ segments:
80
+ - 0
81
+ hash: -1869146196787316308
82
+ requirements: []
83
+ rubyforge_project:
84
+ rubygems_version: 1.8.23
85
+ signing_key:
86
+ specification_version: 3
87
+ summary: Manages users across many different child accounts of a RightScale Enterprise
88
+ Master Account
89
+ test_files: []