rs_user_policy 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +21 -4
- data/bin/rs_user_policy +44 -126
- data/lib/rs_user_policy/audit_log.rb +59 -0
- data/lib/rs_user_policy/policy/json_policy.rb +88 -0
- data/lib/{utilities.rb → rs_user_policy/policy/policy.rb} +11 -29
- data/lib/rs_user_policy/right_api/permission_utilities.rb +129 -0
- data/lib/rs_user_policy/user.rb +119 -0
- data/lib/rs_user_policy/user_assignments/json_user_assignments.rb +114 -0
- data/lib/rs_user_policy/user_assignments/user_assignments.rb +62 -0
- data/lib/rs_user_policy/user_collection.rb +61 -0
- data/lib/rs_user_policy/utilities.rb +60 -0
- data/lib/rs_user_policy.rb +23 -0
- metadata +14 -6
- data/lib/audit_log.rb +0 -59
data/README.rdoc
CHANGED
@@ -27,10 +27,15 @@ The second input file is the user assignments JSON file which assigns users to r
|
|
27
27
|
User Assignment JSON should be in the following format
|
28
28
|
|
29
29
|
{
|
30
|
-
"ryan.geyer@rightscale.com":
|
30
|
+
"ryan.geyer@rightscale.com": {
|
31
|
+
"roles": [
|
32
|
+
"role_name"
|
33
|
+
]
|
34
|
+
}
|
31
35
|
}
|
32
36
|
|
33
37
|
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.
|
38
|
+
Both "immutable" and "delete", if present will take precedence over any other roles assigned to the user.
|
34
39
|
|
35
40
|
Many email:role pairs can be specified. in the user assignments JSON
|
36
41
|
|
@@ -50,8 +55,16 @@ So, given a policy file like;
|
|
50
55
|
And a user assignments file like;
|
51
56
|
|
52
57
|
{
|
53
|
-
"user1@email.com":
|
54
|
-
|
58
|
+
"user1@email.com": {
|
59
|
+
"roles": [
|
60
|
+
"team1"
|
61
|
+
]
|
62
|
+
},
|
63
|
+
"user2@email.com": {
|
64
|
+
"roles": [
|
65
|
+
"team2"
|
66
|
+
]
|
67
|
+
}
|
55
68
|
}
|
56
69
|
|
57
70
|
And operating on the accounts 12345 and 23456;
|
@@ -70,7 +83,11 @@ Second is the user_assignments-<timestamp>.json file. This will be a combinatio
|
|
70
83
|
|
71
84
|
== TODO
|
72
85
|
|
73
|
-
* Allow
|
86
|
+
* Allow users to be added who are not already invited to any RS parent or child account
|
87
|
+
* In absence of a policy.json, create a new policy.json with base roles for each account discovered (I.E. Admin, Observer, Designer, etc)
|
88
|
+
* Perhaps allow a role to inherit from another, or be a concatenation of several?
|
89
|
+
* Handle the race condition where permissions have been deleted in the correct order, but API still throws 422 removing "observer" because the changes have not become consistent server side
|
90
|
+
* HTTP Code: 422, Response body: A user must have the observer role. (RightApi::Exceptions::ApiException)
|
74
91
|
|
75
92
|
== Copyright
|
76
93
|
|
data/bin/rs_user_policy
CHANGED
@@ -24,8 +24,7 @@
|
|
24
24
|
require 'trollop'
|
25
25
|
require 'right_api_client'
|
26
26
|
require 'logger'
|
27
|
-
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', '
|
28
|
-
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'utilities'))
|
27
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'rs_user_policy'))
|
29
28
|
|
30
29
|
opts = Trollop::options do
|
31
30
|
banner = "Manages users across many different child accounts of a RightScale Enterprise Master Account"
|
@@ -42,86 +41,47 @@ log = Logger.new(STDOUT)
|
|
42
41
|
timestamp = Time::now.to_i
|
43
42
|
deleted_users = []
|
44
43
|
accounts = []
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
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]
|
44
|
+
|
45
|
+
if opts[:dry_run]
|
46
|
+
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")
|
47
|
+
end
|
62
48
|
|
63
49
|
user_assignments_output = "user_assignments-#{timestamp}.json"
|
64
50
|
|
65
|
-
audit_log = AuditLog.new opts.merge(:timestamp => timestamp)
|
66
|
-
policy = {}
|
67
|
-
|
68
|
-
def valid_policy_json_file?(json_file, &block)
|
69
|
-
# TODO: Also validate that the policy file is in the correct form.
|
70
|
-
# I.E. {
|
71
|
-
# "policy-name": {
|
72
|
-
# "account-href-or-default": ["list", "of", "permissions"]
|
73
|
-
# }
|
74
|
-
#}
|
75
|
-
yield JSON.parse(File.read(json_file))
|
76
|
-
return true
|
77
|
-
rescue JSON::ParserError
|
78
|
-
return false
|
79
|
-
end
|
51
|
+
audit_log = RsUserPolicy::AuditLog.new opts.merge(:timestamp => timestamp)
|
80
52
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
log.fatal("The policy file named #{opts[:policy]} is not a properly formatted json file!")
|
87
|
-
exit 1
|
88
|
-
end
|
89
|
-
else
|
90
|
-
log.fatal("The policy file named #{opts[:policy]} was not found!")
|
53
|
+
policy = nil
|
54
|
+
begin
|
55
|
+
policy = RsUserPolicy::Policy::JsonPolicy.new(:filename => opts[:policy])
|
56
|
+
rescue Exception => e
|
57
|
+
log.fatal("Unable to initialize policy from filename #{opts[:policy]}. Error: #{e.message}")
|
91
58
|
exit 1
|
92
59
|
end
|
93
60
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
else
|
99
|
-
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.")
|
100
|
-
end
|
101
|
-
else
|
102
|
-
log.warn("No user_assignments file was specified. All users will be treated as immutable and written to the user_assigments output file.")
|
61
|
+
user_assignments_options = opts[:user_assignments] ? { :filename => opts[:user_assignments] } : {}
|
62
|
+
user_assignments = RsUserPolicy::UserAssignments::JsonUserAssignments.new(user_assignments_options)
|
63
|
+
if user_assignments.length == 0
|
64
|
+
log.warn("No user_assignments file was specified or the file could not be found. All users will be treated as immutable and written to the user_assigments output file.")
|
103
65
|
end
|
104
66
|
|
67
|
+
user_collection = RsUserPolicy::UserCollection.new
|
68
|
+
|
105
69
|
client = RightApi::Client.new(:email => opts[:rs_email], :password => opts[:rs_pass], :account_id => opts[:rs_acct_num])
|
106
70
|
master_account = client.accounts(:id => opts[:rs_acct_num]).show()
|
107
|
-
client.users
|
108
|
-
|
109
|
-
user_email_resource_map[user.email] = user
|
110
|
-
end
|
71
|
+
user_collection.add_users(client.users.index)
|
72
|
+
user_collection.add_permissions(master_account.href, client.permissions.index)
|
111
73
|
|
112
74
|
accounts << {:client => client, :href => master_account.href, :name => master_account.name}
|
113
75
|
|
114
76
|
log.info("Operating on the Enterprise Master Account #{master_account.name}")
|
115
77
|
|
116
78
|
begin
|
117
|
-
client.child_accounts
|
118
|
-
child_client = RightApi::Client.new(:email => opts[:rs_email], :password => opts[:rs_pass], :account_id => Utilities.id_from_href(child.href))
|
79
|
+
client.child_accounts.index.each do |child|
|
80
|
+
child_client = RightApi::Client.new(:email => opts[:rs_email], :password => opts[:rs_pass], :account_id => RsUserPolicy::Utilities.id_from_href(child.href))
|
119
81
|
accounts << {:client => child_client, :href => child.href, :name => child.name}
|
120
82
|
|
121
|
-
child_client.users
|
122
|
-
|
123
|
-
user_email_resource_map[user.email] = user
|
124
|
-
end
|
83
|
+
user_collection.add_users(child_client.users.index)
|
84
|
+
user_collection.add_permissions(child.href, child_client.permissions.index)
|
125
85
|
end
|
126
86
|
rescue RightApi::Exceptions::ApiException => e
|
127
87
|
if e.message =~ /Permission denied/
|
@@ -131,80 +91,38 @@ rescue RightApi::Exceptions::ApiException => e
|
|
131
91
|
end
|
132
92
|
end
|
133
93
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
user_email_resource_map.each do |email,user|
|
138
|
-
users[email] = {}
|
139
|
-
end
|
140
|
-
|
141
|
-
log.info("#{account[:name]} - #{account[:href]}")
|
94
|
+
begin
|
95
|
+
accounts.each do |account|
|
96
|
+
child_client = account[:client]
|
142
97
|
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
users[user_email][permission.role_title] = permission.href
|
149
|
-
end
|
150
|
-
log.info("There are #{users.length} unique Users")
|
98
|
+
log.info("#{account[:name]} - #{account[:href]}")
|
99
|
+
user_collection.users.each do |user|
|
100
|
+
email = user.email
|
101
|
+
user_role = user_assignments.get_roles(email)
|
102
|
+
existing_permissions = user.get_api_permissions(account[:href])
|
151
103
|
|
152
|
-
|
153
|
-
|
154
|
-
user_assignments[email] = 'immutable'
|
155
|
-
end
|
156
|
-
# Allow some different options about possibly prompting the user etc
|
157
|
-
# TODO: Put this in a begin/rescue/end that allows stuff to continue and create an audit_log in the event of an error
|
158
|
-
case user_assignments[email]
|
159
|
-
when 'immutable'
|
160
|
-
# By design, do nothing
|
161
|
-
when 'delete'
|
104
|
+
# Allow some different options about possibly prompting the user etc
|
105
|
+
if user_role.include?('delete')
|
162
106
|
# Note: This is an explicit delete across all master and child accounts
|
163
107
|
# a user can also be effectively deleted if they have an empty list of
|
164
108
|
# permissions for a particular account
|
165
109
|
unless opts[:dry_run]
|
166
|
-
log.debug("
|
167
|
-
|
168
|
-
|
169
|
-
log.debug("I KEELZ U PERMISSION! #{email} - #{role_title}")
|
170
|
-
child_client.permissions(:id => Utilities.id_from_href(perm_href)).destroy()
|
171
|
-
deleted_users << email
|
172
|
-
end
|
110
|
+
log.debug("Deleting #{user.email} from #{account[:name]} by removing these permissions #{JSON.pretty_generate(existing_permissions)}")
|
111
|
+
user.clear_permissions(account[:href], child_client)
|
112
|
+
user_assignments.delete(user.email)
|
173
113
|
end
|
174
114
|
audit_log.add_entry(email, account[:name], 'deleted', 'deleted')
|
175
|
-
|
176
|
-
user_policy = []
|
177
|
-
|
178
|
-
|
179
|
-
elsif policy[user_assignments[email]].key?('default')
|
180
|
-
user_policy = policy[user_assignments[email]]['default']
|
181
|
-
end
|
182
|
-
removed = (user_policy.length == 0) ? user.keys : user.keys - user_policy
|
183
|
-
added = user_policy - user.keys
|
184
|
-
changes = "-#{removed} +#{added}"
|
185
|
-
unless opts[:dry_run]
|
186
|
-
# Convert the role_title array into a hash with the hrefs
|
187
|
-
remove_hash = Hash[removed.map {|role| [role, user[role]]}]
|
188
|
-
|
189
|
-
Utilities.yield_on_keys_in_order(permission_delete_order, remove_hash) do |role_title, href|
|
190
|
-
child_client.permissions(:id => Utilities.id_from_href(href)).destroy()
|
191
|
-
end
|
192
|
-
|
193
|
-
if added.length > 0
|
194
|
-
add_hash = Hash[added.map {|x| [x, nil]}]
|
195
|
-
Utilities.yield_on_keys_in_order(['observer'], add_hash) do |role_title,foo|
|
196
|
-
child_client.permissions.create({'permission[user_href]' => user_email_resource_map[email].href, 'permission[role_title]' => role_title})
|
197
|
-
end
|
198
|
-
end
|
199
|
-
end
|
115
|
+
elsif !user_role.include?("immutable")
|
116
|
+
user_policy = policy.get_permissions(user_role, account[:href])
|
117
|
+
removed,added = user.set_api_permissions(user_policy, account[:href], child_client, opts)
|
118
|
+
changes = "-#{removed.values} +#{added.values}"
|
200
119
|
audit_log.add_entry(email, account[:name], 'update_permissions', changes) unless removed.length + added.length == 0
|
120
|
+
end
|
201
121
|
end
|
202
122
|
end
|
123
|
+
rescue RightApi::Exceptions::ApiException => e
|
124
|
+
log.fatal("A RightScale API exception occurred - #{e}")
|
203
125
|
end
|
204
126
|
|
205
|
-
|
206
|
-
user_assignments.delete(email)
|
207
|
-
end
|
208
|
-
|
209
|
-
File.open(user_assignments_output, 'w') {|f| f.write(JSON.pretty_generate(user_assignments))}
|
127
|
+
user_assignments.serialize(:filename => user_assignments_output)
|
210
128
|
audit_log.write_file
|
@@ -0,0 +1,59 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
class AuditLog
|
24
|
+
|
25
|
+
attr_accessor :filename, :audit_log
|
26
|
+
|
27
|
+
# Initializes a new AuditLog
|
28
|
+
#
|
29
|
+
# @param [Hash] options A hash of options that impact the audit log filename.
|
30
|
+
# @option options [String] :timestamp The timestamp to append to the filename
|
31
|
+
# @option options [Bool] :dry_run A boolean indicating if this is a dry run
|
32
|
+
def initialize(options={})
|
33
|
+
timestamp = options[:timestamp] || Time.now.to_i
|
34
|
+
@audit_log = {}
|
35
|
+
@filename = "audit_log#{options[:dry_run] ? '_dryrun' : ''}-#{timestamp}.json"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Adds a new entry to the audit log
|
39
|
+
#
|
40
|
+
# @param [String] email The email address of the user impacted by the change
|
41
|
+
# @param [String] account The account name impacted by the change
|
42
|
+
# @param [String] action The action performed. Expected options are ['update_permissions', 'deleted']
|
43
|
+
# @param [String] changes A free form description of the changes
|
44
|
+
def add_entry(email, account, action, changes)
|
45
|
+
@audit_log[email] = [] unless audit_log[email]
|
46
|
+
@audit_log[email] << {
|
47
|
+
:account => account,
|
48
|
+
:action => action,
|
49
|
+
:changes => changes
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Writes the audit log to a file
|
54
|
+
#
|
55
|
+
def write_file
|
56
|
+
File.open(@filename, 'w') {|f| f.write(JSON.pretty_generate(@audit_log))}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'policy'))
|
23
|
+
require 'json'
|
24
|
+
|
25
|
+
module RsUserPolicy
|
26
|
+
module Policy
|
27
|
+
class JsonPolicy
|
28
|
+
include Policy
|
29
|
+
|
30
|
+
# Initializes a new Policy
|
31
|
+
#
|
32
|
+
# If more than one source is passed into options, the order of preference will be
|
33
|
+
# [:json, :json_str, :filename]
|
34
|
+
#
|
35
|
+
# @param [Hash] options A hash of inputs for the new JSONPolicy
|
36
|
+
# @option options [Hash] :json A hash containing the policy
|
37
|
+
# @option options [String] :json_str A JSON string containing the policy
|
38
|
+
# @option options [String] :filename Path and filename to a file containing the policy in JSON
|
39
|
+
#
|
40
|
+
# @raise [ArgumentError] If neither a filename or json object were supplied
|
41
|
+
# @raise [Errno::ENOENT] If :filename was specified but the policy file does not exist
|
42
|
+
# @raise [JSON::ParseError] If the policy is not valid JSON
|
43
|
+
def initialize(options={})
|
44
|
+
if ([:filename, :json, :json_str] & options.keys()).empty?
|
45
|
+
raise ArgumentError, "You must supply either a filename, JSON string, or a JSON object"
|
46
|
+
end
|
47
|
+
|
48
|
+
if options.has_key?(:json)
|
49
|
+
@policy = options[:json]
|
50
|
+
elsif options.has_key?(:json_str)
|
51
|
+
@policy = JSON.parse(options[:json_str])
|
52
|
+
else
|
53
|
+
@policy = JSON.parse(File.read(options[:filename]))
|
54
|
+
end
|
55
|
+
|
56
|
+
validate()
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an array of permissions for a particular role in a particular RightScale account
|
60
|
+
#
|
61
|
+
# @param [Array<String>] roles An array of role names for which permissions should be fetched
|
62
|
+
# @param [String] account_href A RightScale API 1.5 href for the RightScale account
|
63
|
+
#
|
64
|
+
# @return [Array<String>] A list of permissions for the role and account pair requested. An empty array is returned if no policy exists for the requested pair
|
65
|
+
def get_permissions(roles, account_href)
|
66
|
+
permissions = []
|
67
|
+
roles.each do |role|
|
68
|
+
if @policy.has_key?(role)
|
69
|
+
permissions = permissions + (@policy[role][account_href] || @policy[role]['default'] || [])
|
70
|
+
end
|
71
|
+
end
|
72
|
+
permissions.uniq
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def validate()
|
78
|
+
# TODO: Also validate that the policy file is in the correct form.
|
79
|
+
# I.E. {
|
80
|
+
# "policy-name": {
|
81
|
+
# "account-href-or-default": ["list", "of", "permissions"]
|
82
|
+
# }
|
83
|
+
#}
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -19,36 +19,18 @@
|
|
19
19
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
matches[1] || nil
|
34
|
-
end
|
35
|
-
|
36
|
-
# Operates on the key/value pairs in a hash in the order specified in 'order'
|
37
|
-
# followed by any key/value pairs not specified in the order
|
38
|
-
#
|
39
|
-
# === Parameters
|
40
|
-
# order(Array):: An array containing keys in the order they should be yielded to the block
|
41
|
-
# hash(Hash):: The hash to operate on in the specified order
|
42
|
-
# block(Closure):: A closure to yield to
|
43
|
-
def self.yield_on_keys_in_order(order, hash, &block)
|
44
|
-
order.each do |key|
|
45
|
-
if hash.key?(key)
|
46
|
-
yield key, hash.delete(key)
|
22
|
+
module RsUserPolicy
|
23
|
+
module Policy
|
24
|
+
module Policy
|
25
|
+
# Returns an array of permissions for a particular role in a particular RightScale account
|
26
|
+
#
|
27
|
+
# @param [Array<String>] roles An array of role names for which permissions should be fetched
|
28
|
+
# @param [String] account_href A RightScale API 1.5 href for the RightScale account
|
29
|
+
#
|
30
|
+
# @return [Array<String>] A list of permissions for the role and account pair requested. An empty array is returned if no policy exists for the requested pair
|
31
|
+
def get_permissions(roles, account_href)
|
32
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
47
33
|
end
|
48
34
|
end
|
49
|
-
hash.each do |key,val|
|
50
|
-
yield key, val
|
51
|
-
end
|
52
35
|
end
|
53
|
-
|
54
36
|
end
|
@@ -0,0 +1,129 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
module RightApi
|
24
|
+
# A set of utility methods for manipulating permissions using the RightScale right_api_client gem
|
25
|
+
#
|
26
|
+
# Allows bulk actions on permissions without worrying about the complexity of retrying,
|
27
|
+
# creating/deleting in the correct order, and the like.
|
28
|
+
#
|
29
|
+
class PermissionUtilities
|
30
|
+
|
31
|
+
@@permission_delete_order = [
|
32
|
+
'enterprise_manager',
|
33
|
+
'admin',
|
34
|
+
'security_manager',
|
35
|
+
'actor',
|
36
|
+
'billing',
|
37
|
+
'server_login',
|
38
|
+
'publisher',
|
39
|
+
'designer',
|
40
|
+
'library',
|
41
|
+
'lite_user',
|
42
|
+
'observer'
|
43
|
+
]
|
44
|
+
|
45
|
+
# Destroys all passed in permissions with the specified client.
|
46
|
+
# This method handles deleting permissions in the appropriate order to avoid the dreaded;
|
47
|
+
# RightApi::Exceptions::ApiException: Error: HTTP Code: 422, Response body: A user must have the observer role.
|
48
|
+
# TODO: Handle a 422 resulting from calling delete too quickly and attempting to remove "observer" when other deletes have not been committed
|
49
|
+
#
|
50
|
+
# @param [Array<RightApi::ResourceDetail>] permissions
|
51
|
+
# A hash of permissions where the key is the RightScale API href, and the
|
52
|
+
# value is the role_title. These permissions can be for one or many users, allowing a bulk actions.
|
53
|
+
# @param [RightApi::Client] client
|
54
|
+
# An active RightApi::Client instance for the account referenced in account_href
|
55
|
+
#
|
56
|
+
# @raise [RightApi::Exceptions::ApiException] If an unrecoverable API error has occurred.
|
57
|
+
#
|
58
|
+
# @return [Hash] A hash where the keys are the permission hrefs destroyed, and the values are the role_title of those permissions
|
59
|
+
def self.destroy_permissions(permissions, client)
|
60
|
+
perms_hash = {}
|
61
|
+
permissions.each{|p| perms_hash[p.href] = p.role_title }
|
62
|
+
RsUserPolicy::Utilities.yield_on_values_in_order(@@permission_delete_order, perms_hash) do |perm_href,role_title|
|
63
|
+
client.permissions(:id => RsUserPolicy::Utilities.id_from_href(perm_href)).destroy()
|
64
|
+
end
|
65
|
+
perms_hash
|
66
|
+
end
|
67
|
+
|
68
|
+
# Creates all the passed in permissions using the supplied client.
|
69
|
+
# This method handles creating permissions with "observer" first in order to avoide the dreaded;
|
70
|
+
# RightApi::Exceptions::ApiException: Error: HTTP Code: 422, Response body: A user must have the observer role.
|
71
|
+
#
|
72
|
+
# @param [Hash] permissions
|
73
|
+
# A hash where the key is a RightScale API User href, and the value is a hash where the key is the permission role_title that the user should be granted, and the value is nil.
|
74
|
+
#
|
75
|
+
# @param [RightApi::Client] client
|
76
|
+
# An active RightApi::Client instance for the account referenced in account_href
|
77
|
+
#
|
78
|
+
# @raise [RightApi::Exceptions::ApiException] If an unrecoverable API error has occurred.
|
79
|
+
#
|
80
|
+
# @return [Hash] The permissions input hash, where the nil values have been replaced with the href of the permission which was created.
|
81
|
+
#
|
82
|
+
# @example Create "observer" and "admin" permissions for two users
|
83
|
+
# client = RightApi::Client.new(</snip>)
|
84
|
+
#
|
85
|
+
# permissions = {
|
86
|
+
# '/api/users/123' => {
|
87
|
+
# 'observer' => nil,
|
88
|
+
# 'admin' => nil
|
89
|
+
# },
|
90
|
+
# '/api/users/456' => {
|
91
|
+
# 'observer' => nil,
|
92
|
+
# 'admin' => nil
|
93
|
+
# }
|
94
|
+
# }
|
95
|
+
#
|
96
|
+
# response = RsUserPolicy::RightApi::PermissionUtilities.create_permissions(permissions, client)
|
97
|
+
#
|
98
|
+
# puts JSON.pretty_generate(response)
|
99
|
+
#
|
100
|
+
# # Output would be as follows
|
101
|
+
# {
|
102
|
+
# '/api/users/123' => {
|
103
|
+
# 'observer' => '/api/permissions/1',
|
104
|
+
# 'admin' => '/api/permissions/2'
|
105
|
+
# },
|
106
|
+
# '/api/users/456' => {
|
107
|
+
# 'observer' => '/api/permissions/3',
|
108
|
+
# 'admin' => '/api/permissions/4'
|
109
|
+
# }
|
110
|
+
# }
|
111
|
+
def self.create_permissions(permissions, client)
|
112
|
+
permissions.each do |user_href,perm_ary|
|
113
|
+
user_perms_hash = Hash[perm_ary.keys.map{|p| [p, user_href]}]
|
114
|
+
RsUserPolicy::Utilities.yield_on_values_in_order(['observer'], user_perms_hash) do |role_title,user_href|
|
115
|
+
created_permission = client.permissions.create(
|
116
|
+
{
|
117
|
+
'permission[user_href]' => user_href,
|
118
|
+
'permission[role_title]' => role_title
|
119
|
+
}
|
120
|
+
)
|
121
|
+
permissions[user_href][role_title] = created_permission.href
|
122
|
+
end
|
123
|
+
end
|
124
|
+
permissions
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,119 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
class User
|
24
|
+
attr_reader :email, :href
|
25
|
+
|
26
|
+
# Initializes read only attributes for an RsUserPolicy::User
|
27
|
+
#
|
28
|
+
# @param [String] email The email address of the user
|
29
|
+
# @param [String] href The RightScale API href of the user
|
30
|
+
def initialize(email, href)
|
31
|
+
@email = email
|
32
|
+
@href = href
|
33
|
+
@permissions = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds a single permission for a single RightScale account
|
37
|
+
#
|
38
|
+
# @param [String] account_href The RightScale API href of the account
|
39
|
+
# @param [RightApi::ResourceDetail] permission A single RightApi::ResourceDetail for a permission.
|
40
|
+
def add_permission(account_href, permission)
|
41
|
+
@permissions[account_href] ||= []
|
42
|
+
@permissions[account_href] << permission
|
43
|
+
end
|
44
|
+
|
45
|
+
# Returns the RightScale permissions the user has for the specified account href
|
46
|
+
#
|
47
|
+
# @param [String] account_href The RightScale API href of the account
|
48
|
+
#
|
49
|
+
# @return [Array<RightApi::ResourceDetail>] An array of permission RightApi::ResourceDetail objects
|
50
|
+
def get_api_permissions(account_href)
|
51
|
+
@permissions[account_href] || []
|
52
|
+
end
|
53
|
+
|
54
|
+
# Removes all permissions for the user in the specified rightscale account using the supplied client
|
55
|
+
#
|
56
|
+
# @param [String] account_href The RightScale API href of the account
|
57
|
+
# @param [RightApi::Client] client An active RightApi::Client instance for the account referenced in account_href
|
58
|
+
# @param [Hash] options Optional parameters
|
59
|
+
# @option options [Bool] :dry_run If true, no API calls will be made, but the return value will contain the actions which would have been taken
|
60
|
+
#
|
61
|
+
# @raise [RightApi::Exceptions::ApiException] If an unrecoverable API error has occurred.
|
62
|
+
#
|
63
|
+
# @return [Hash] A hash where the keys are the permission hrefs destroyed, and the keys are the role_title of those permissions
|
64
|
+
def clear_permissions(account_href, client, options={})
|
65
|
+
options = {:dry_run => false}.merge(options)
|
66
|
+
current_permissions = get_api_permissions(account_href)
|
67
|
+
if options[:dry_run]
|
68
|
+
Hash[current_permissions.map{|p| [p.href, p.role_title]}]
|
69
|
+
else
|
70
|
+
RsUserPolicy::RightApi::PermissionUtilities.destroy_permissions(
|
71
|
+
current_permissions,
|
72
|
+
client
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Removes and adds permissions as appropriate so that the users current permissions reflect
|
78
|
+
# the desired set passed in as "permissions"
|
79
|
+
#
|
80
|
+
# @param [Array<String>] permissions The list of desired permissions for the user in the specified account
|
81
|
+
# @param [String] account_href The RightScale API href of the account
|
82
|
+
# @param [RightApi::Client] client An active RightApi::Client instance for the account referenced in account_href
|
83
|
+
# @param [Hash] options Optional parameters
|
84
|
+
# @option options [Bool] :dry_run If true, no API calls will be made, but the return value will contain the actions which would have been taken
|
85
|
+
#
|
86
|
+
# @raise [RightApi::Exceptions::ApiException] If an unrecoverable API error has occurred.
|
87
|
+
#
|
88
|
+
# @return [Hash,Hash] A tuple where two hashes are returned. The keys of the hashes are the href of the permission, and the values are the role_title of the permission. The first hash is the permissions removed, and the second hash is the permissions added
|
89
|
+
def set_api_permissions(permissions, account_href, client, options={})
|
90
|
+
options = {:dry_run => false}.merge(options)
|
91
|
+
existing_api_permissions_response = get_api_permissions(account_href)
|
92
|
+
existing_api_permissions = Hash[existing_api_permissions_response.map{|p| [p.role_title, p] }]
|
93
|
+
if permissions.length == 0
|
94
|
+
return clear_permissions(account_href, client, options), {}
|
95
|
+
else
|
96
|
+
permissions_to_remove = (existing_api_permissions.keys - permissions).map{|p| existing_api_permissions[p]}
|
97
|
+
remove_response = Hash[permissions_to_remove.map{|p| [p.href, p.role_title]}]
|
98
|
+
unless options[:dry_run]
|
99
|
+
remove_response = RsUserPolicy::RightApi::PermissionUtilities.destroy_permissions(permissions_to_remove, client)
|
100
|
+
end
|
101
|
+
|
102
|
+
permissions_to_add = {
|
103
|
+
@href => Hash[(permissions - existing_api_permissions.keys).map{|p| [p,nil]}]
|
104
|
+
}
|
105
|
+
add_response = {}
|
106
|
+
if options[:dry_run]
|
107
|
+
href_idx = 0
|
108
|
+
add_response = {
|
109
|
+
@href => Hash[(permissions - existing_api_permissions.keys).map{|p| [p,(href_idx += 1)]}]
|
110
|
+
}
|
111
|
+
else
|
112
|
+
add_response = RsUserPolicy::RightApi::PermissionUtilities.create_permissions(permissions_to_add, client)
|
113
|
+
end
|
114
|
+
|
115
|
+
return remove_response, Hash[add_response[@href].keys.map{|p| [add_response[@href][p],p]}]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,114 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'user_assignments'))
|
23
|
+
require 'json'
|
24
|
+
|
25
|
+
module RsUserPolicy
|
26
|
+
module UserAssignments
|
27
|
+
class JsonUserAssignments
|
28
|
+
include UserAssignments
|
29
|
+
|
30
|
+
# Initializes a new UserAssignments
|
31
|
+
#
|
32
|
+
# If more than one source is passed into options, the order of preference will be
|
33
|
+
# [:json, :json_str, :filename]
|
34
|
+
#
|
35
|
+
# @param [Hash] options A hash of inputs for the new JsonUserAssignments, where the keys are;
|
36
|
+
# @option options [Hash] :json A hash containing the user assignments
|
37
|
+
# @option options [String] :json_str A JSON string containing the user assignments
|
38
|
+
# @option options [String] :filename Path and filename to a file containing the user assignments in JSON
|
39
|
+
#
|
40
|
+
# @raise [Errno::ENOENT] If :filename was specified but the policy file does not exist
|
41
|
+
# @raise [JSON::ParserError] If the policy is not valid JSON
|
42
|
+
def initialize(options={})
|
43
|
+
begin
|
44
|
+
if options.has_key?(:json)
|
45
|
+
@user_assignments = options[:json]
|
46
|
+
elsif options.has_key?(:json_str)
|
47
|
+
@user_assignments = JSON.parse(options[:json_str])
|
48
|
+
elsif options.has_key?(:filename)
|
49
|
+
@user_assignments = JSON.parse(File.read(options[:filename]))
|
50
|
+
else
|
51
|
+
@user_assignments = {}
|
52
|
+
end
|
53
|
+
rescue Errno::ENOENT, JSON::ParserError
|
54
|
+
@user_assignments = {}
|
55
|
+
end
|
56
|
+
|
57
|
+
validate()
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Int] The number of users in the user assignments object
|
61
|
+
def length
|
62
|
+
@user_assignments.length
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Int] The number of users in the user assignments object
|
66
|
+
def size
|
67
|
+
self.length
|
68
|
+
end
|
69
|
+
|
70
|
+
# Returns the roles assigned to the user. If the user does not exist
|
71
|
+
# they should be automatically created with the role "immutable"
|
72
|
+
#
|
73
|
+
# @param [String] email The email address for the user
|
74
|
+
#
|
75
|
+
# @return [Array<String>] The roles assigned to the user
|
76
|
+
def get_roles(email)
|
77
|
+
# TODO: This seems expensive to do in an accessor?
|
78
|
+
unless @user_assignments.key?(email)
|
79
|
+
@user_assignments[email] = {'roles' => ['immutable']}
|
80
|
+
end
|
81
|
+
@user_assignments[email]['roles']
|
82
|
+
end
|
83
|
+
|
84
|
+
# Deletes a user from the user assignments
|
85
|
+
#
|
86
|
+
# @param [String] email The email address for the user
|
87
|
+
def delete(email)
|
88
|
+
@user_assignments.delete(email)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Commits any changes made to the UserAssignments object back to
|
92
|
+
# it's original data store. This is an opportunity to perform
|
93
|
+
# DB flushes or write back to a source file.
|
94
|
+
#
|
95
|
+
# @param [Hash] options A hash containing only one key;
|
96
|
+
# @option options [String] :filename The filename to write out the JSON state of this JsonUserAssignments object
|
97
|
+
#
|
98
|
+
# @raise [ArgumentError] When no output file is specified
|
99
|
+
def serialize(options={})
|
100
|
+
raise ArgumentError, "You must specify the :filename option" unless options.has_key?(:filename)
|
101
|
+
File.open(options[:filename], 'w') {|f| f.write(JSON.pretty_generate(@user_assignments))}
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def validate()
|
107
|
+
# TODO: Also validate that the user assignments file is in the correct form.
|
108
|
+
# I.E. {
|
109
|
+
# "email@address.com": "role"
|
110
|
+
#}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,62 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
module UserAssignments
|
24
|
+
module UserAssignments
|
25
|
+
# @return [Int] The number of users in the user assignments object
|
26
|
+
def length
|
27
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Int] The number of users in the user assignments object
|
31
|
+
def size
|
32
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns the roles assigned to the user. If the user does not exist
|
36
|
+
# they should be automatically created with the role "immutable"
|
37
|
+
#
|
38
|
+
# @param [String] email The email address for the user
|
39
|
+
#
|
40
|
+
# @return [Array<String>] The roles assigned to the user
|
41
|
+
def get_roles(email)
|
42
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
43
|
+
end
|
44
|
+
|
45
|
+
# Deletes a user from the user assignments
|
46
|
+
#
|
47
|
+
# @param [String] email The email address for the user
|
48
|
+
def delete(email)
|
49
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Commits any changes made to the UserAssignments object back to
|
53
|
+
# it's original data store. This is an opportunity to perform
|
54
|
+
# DB flushes or write back to a source file.
|
55
|
+
#
|
56
|
+
# @param [Hash] options A hash of values which may be required to serialize
|
57
|
+
def serialize(options={})
|
58
|
+
raise NotImplementedError, "Please implement this in your concrete class"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,61 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
class UserCollection
|
24
|
+
|
25
|
+
def initialize
|
26
|
+
@users_by_href = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
# TODO: An .each iterator.. instead of a .users accessor
|
30
|
+
|
31
|
+
# @return [Array<RsUserPolicy::User>] An array of RsUserPolicy::User added to the collection
|
32
|
+
def users
|
33
|
+
@users_by_href.values
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds users to this collection only if the collection does not already
|
37
|
+
# include the specified users. The users RightScale API href is used
|
38
|
+
# as the unique identifier for deduplication
|
39
|
+
#
|
40
|
+
# @param [Array<RightApi::ResourceDetail>] An array of ResourceDetail from the Right API Client for users. Returned by client.users.index
|
41
|
+
def add_users(users)
|
42
|
+
users.each do |user|
|
43
|
+
unless @users_by_href.has_key?(user.href)
|
44
|
+
@users_by_href[user.href] = RsUserPolicy::User.new(user.email, user.href)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def [](idx)
|
50
|
+
@users_by_href[idx]
|
51
|
+
end
|
52
|
+
|
53
|
+
def add_permissions(account_href, permissions)
|
54
|
+
permissions.each do |permission|
|
55
|
+
user_href = permission.user.href
|
56
|
+
add_users([permission.user])
|
57
|
+
@users_by_href[user_href].add_permission(account_href, permission)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module RsUserPolicy
|
23
|
+
class Utilities
|
24
|
+
# Uses a regex to parse a RightScale API resource id from it's relative href
|
25
|
+
#
|
26
|
+
# @param [String] href The relative href of the RightScale API resource
|
27
|
+
# @return [String] The unique ID of the resource
|
28
|
+
def self.id_from_href(href)
|
29
|
+
matches = /.*\/([0-9]*)/.match(href)
|
30
|
+
matches[1] || nil
|
31
|
+
end
|
32
|
+
|
33
|
+
# Operates on the key/value pairs in a hash in the order specified in 'order'
|
34
|
+
# followed by any key/value pairs not specified in the order
|
35
|
+
#
|
36
|
+
# @param [Array] order An array containing keys in the order they should be yielded to the block
|
37
|
+
# @param [Hash] hash The hash to operate on in the specified order
|
38
|
+
# @param [Closure] block A closure to yield to
|
39
|
+
def self.yield_on_keys_in_order(order, hash, &block)
|
40
|
+
order.each do |key|
|
41
|
+
hash.select{|k,v| k == key}.each{|k,v| yield k,v }
|
42
|
+
end
|
43
|
+
hash.select{|k,v| !order.include?(k)}.each{|k,v| yield k,v}
|
44
|
+
end
|
45
|
+
|
46
|
+
# Operates on the key/value pairs in a hash in the order specified in 'order'
|
47
|
+
# followed by any key/value pairs not specified in the order
|
48
|
+
#
|
49
|
+
# @param [Array] order An array containing values in the order they should be yielded to the block
|
50
|
+
# @param [Hash] hash The hash to operate on in the specified order
|
51
|
+
# @param [Closure] block A closure to yield to
|
52
|
+
def self.yield_on_values_in_order(order, hash, &block)
|
53
|
+
order.each do |value|
|
54
|
+
hash.select{|k,v| v == value}.each{|k,v| yield k,v }
|
55
|
+
end
|
56
|
+
hash.select{|k,v| !order.include?(v) }.each{|k,v| yield k,v }
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,23 @@
|
|
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
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
glob_path = File.expand_path(File.join(File.dirname(__FILE__), 'rs_user_policy')) + '/**/*.rb'
|
23
|
+
Dir.glob(glob_path, &method(:require))
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rs_user_policy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-15 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: right_api_client
|
@@ -51,8 +51,16 @@ executables:
|
|
51
51
|
extensions: []
|
52
52
|
extra_rdoc_files: []
|
53
53
|
files:
|
54
|
-
- lib/audit_log.rb
|
55
|
-
- lib/
|
54
|
+
- lib/rs_user_policy/audit_log.rb
|
55
|
+
- lib/rs_user_policy/policy/json_policy.rb
|
56
|
+
- lib/rs_user_policy/policy/policy.rb
|
57
|
+
- lib/rs_user_policy/right_api/permission_utilities.rb
|
58
|
+
- lib/rs_user_policy/user.rb
|
59
|
+
- lib/rs_user_policy/user_assignments/json_user_assignments.rb
|
60
|
+
- lib/rs_user_policy/user_assignments/user_assignments.rb
|
61
|
+
- lib/rs_user_policy/user_collection.rb
|
62
|
+
- lib/rs_user_policy/utilities.rb
|
63
|
+
- lib/rs_user_policy.rb
|
56
64
|
- bin/rs_user_policy
|
57
65
|
- LICENSE.txt
|
58
66
|
- README.rdoc
|
@@ -71,7 +79,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
71
79
|
version: '0'
|
72
80
|
segments:
|
73
81
|
- 0
|
74
|
-
hash: -
|
82
|
+
hash: -4206610464842566721
|
75
83
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
84
|
none: false
|
77
85
|
requirements:
|
@@ -80,7 +88,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
88
|
version: '0'
|
81
89
|
segments:
|
82
90
|
- 0
|
83
|
-
hash: -
|
91
|
+
hash: -4206610464842566721
|
84
92
|
requirements: []
|
85
93
|
rubyforge_project:
|
86
94
|
rubygems_version: 1.8.24
|
data/lib/audit_log.rb
DELETED
@@ -1,59 +0,0 @@
|
|
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
|
17
|
-
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
-
|
22
|
-
class AuditLog
|
23
|
-
|
24
|
-
attr_accessor :filename, :audit_log
|
25
|
-
|
26
|
-
# Initializes a new AuditLog
|
27
|
-
#
|
28
|
-
# === Parameters
|
29
|
-
# options(Hash):: A hash of options that impact the audit log filename. Possible options are;
|
30
|
-
# :timestamp(String):: The timestamp to append to the filename
|
31
|
-
# :dry_run(Bool):: A boolean indicating if this is a dry run
|
32
|
-
def initialize(options={})
|
33
|
-
timestamp = options[:timestamp] || Time.now.to_i
|
34
|
-
@audit_log = {}
|
35
|
-
@filename = "audit_log#{options[:dry_run] ? '_dryrun' : ''}-#{timestamp}.json"
|
36
|
-
end
|
37
|
-
|
38
|
-
# Adds a new entry to the audit log
|
39
|
-
#
|
40
|
-
# === Parameters
|
41
|
-
# email(String):: The email address of the user impacted by the change
|
42
|
-
# account(String):: The account name impacted by the change
|
43
|
-
# action(String):: The action performed. Expected options are ['update_permissions', 'deleted']
|
44
|
-
# changes(String):: A free form description of the changes
|
45
|
-
def add_entry(email, account, action, changes)
|
46
|
-
@audit_log[email] = [] unless audit_log[email]
|
47
|
-
@audit_log[email] << {
|
48
|
-
:account => account,
|
49
|
-
:action => action,
|
50
|
-
:changes => changes
|
51
|
-
}
|
52
|
-
end
|
53
|
-
|
54
|
-
# Writes the audit log to a file
|
55
|
-
#
|
56
|
-
def write_file
|
57
|
-
File.open(@filename, 'w') {|f| f.write(JSON.pretty_generate(@audit_log))}
|
58
|
-
end
|
59
|
-
end
|