Skip to content

Commit 4e53164

Browse files
authored
Implement shadow user creation by org manager (#4113)
* Add username and origin to user create message * Add uaa client for creating shadow users * Enable UAA shadow user creation for POST /v3/users * Add error handling in case UAA is unavailable * Disallow using origin 'uaa' when creating user by username * Allow org managers to create users by username * Add config flag * Adjust error handling and add unit tests * Implement shadow user creation in /v3/roles endpoint * Allow empty array in config for uaa->clients * Prevent calling UAA twice when creating new user through /v3/users * Add unit test for scenario feature off but admin can still create shadow user using username and origin * First check whether user exists in UAA before trying to create in /v3/users * First check whether user exists in UAA before trying to create in /v3/roles * Improve user-create-message error messages
1 parent 36682b3 commit 4e53164

File tree

16 files changed

+1059
-158
lines changed

16 files changed

+1059
-158
lines changed

app/actions/user_create.rb

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@ class Error < StandardError
44
end
55

66
def create(message:)
7-
user = User.create(guid: message.guid)
7+
if message.username && message.origin
8+
existing_user_guid = User.get_user_id_by_username_and_origin(message.username, message.origin)
9+
10+
shadow_user = User.create_uaa_shadow_user(message.username, message.origin) unless existing_user_guid
11+
12+
user_guid = existing_user_guid || shadow_user['id']
13+
else
14+
user_guid = message.guid
15+
end
16+
user = User.create(guid: user_guid)
817
User.db.transaction do
918
MetadataUpdate.update(user, message)
1019
end
20+
1121
user
1222
rescue Sequel::ValidationFailed => e
1323
validation_error!(message, e)
@@ -16,7 +26,11 @@ def create(message:)
1626
private
1727

1828
def validation_error!(message, error)
19-
error!("User with guid '#{message.guid}' already exists.") if error.errors.on(:guid)&.any? { |e| [:unique].include?(e) }
29+
error!("User with guid '#{message.guid}' already exists.") if message.guid && error.errors.on(:guid)&.any? { |e| [:unique].include?(e) }
30+
31+
if !message.guid && error.errors.on(:guid)&.any? { |e| [:unique].include?(e) }
32+
error!("User with username '#{message.username}' and origin '#{message.origin}' already exists.")
33+
end
2034

2135
error!(error.message)
2236
end

app/controllers/v3/roles_controller.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,11 @@ def create_org_role(message)
114114
unauthorized! unless permission_queryer.can_write_to_active_org?(org.id)
115115
suspended! unless permission_queryer.is_org_active?(org.id)
116116

117-
user_guid = message.user_guid || lookup_user_guid_in_uaa(message.username, message.user_origin)
117+
user_guid = if message.username && message.user_origin && message.user_origin != 'uaa' && org_managers_can_create_users?
118+
create_or_get_uaa_user(message)
119+
else
120+
message.user_guid || lookup_user_guid_in_uaa(message.username, message.user_origin)
121+
end
118122

119123
user = User.first(guid: user_guid) || create_cc_user(user_guid)
120124

@@ -140,6 +144,23 @@ def create_cc_user(user_guid)
140144
UserCreate.new.create(message:)
141145
end
142146

147+
def create_or_get_uaa_user(message)
148+
user_create_message = UserCreateMessage.new(username: message.username, origin: message.user_origin)
149+
unprocessable!(user_create_message.errors.full_messages) unless user_create_message.valid?
150+
151+
existing_user_id = get_uaa_user_id(user_create_message)
152+
user = create_uaa_shadow_user(user_create_message) unless existing_user_id
153+
existing_user_id || user['id']
154+
end
155+
156+
def get_uaa_user_id(message)
157+
User.get_user_id_by_username_and_origin(message.username, message.origin)
158+
end
159+
160+
def create_uaa_shadow_user(message)
161+
User.create_uaa_shadow_user(message.username, message.origin)
162+
end
163+
143164
def readable_users
144165
current_user.readable_users(permission_queryer.can_read_globally?)
145166
end
@@ -203,4 +224,8 @@ def lookup_user_guid_in_uaa(username, given_origin, creating_space_role: false)
203224
def uaa_username_lookup_client
204225
CloudController::DependencyLocator.instance.uaa_username_lookup_client
205226
end
227+
228+
def org_managers_can_create_users?
229+
VCAP::CloudController::Config.config.get(:allow_user_creation_by_org_manager) && FeatureFlag.raise_unless_enabled!(:set_roles_by_username)
230+
end
206231
end

app/controllers/v3/users_controller.rb

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,24 @@ def show
3838
end
3939

4040
def create
41-
unauthorized! unless permission_queryer.can_write_globally?
42-
4341
message = UserCreateMessage.new(hashed_params[:body])
42+
unauthorized! unless permission_queryer.can_write_globally? || org_managers_can_create_users?
4443
unprocessable!(message.errors.full_messages) unless message.valid?
44+
45+
# prevent org_managers from creating users by guid
46+
unauthorized! if !permission_queryer.can_write_globally? && !(!message.guid && org_managers_can_create_users?)
47+
4548
user = UserCreate.new.create(message:)
4649

47-
render status: :created, json: Presenters::V3::UserPresenter.new(user, uaa_users: User.uaa_users_info([user.guid]))
50+
if message.username && message.origin
51+
render status: :created,
52+
json: Presenters::V3::UserPresenter.new(user,
53+
uaa_users: { user.guid => { 'username' => message.username, 'id' => user.guid, 'origin' => message.origin } })
54+
else
55+
render status: :created, json: Presenters::V3::UserPresenter.new(user, uaa_users: User.uaa_users_info([user.guid]))
56+
end
57+
rescue VCAP::CloudController::UaaUnavailable
58+
raise CloudController::Errors::ApiError.new_from_details('UaaUnavailable')
4859
rescue UserCreate::Error => e
4960
unprocessable!(e)
5061
end
@@ -91,4 +102,8 @@ def fetch_user_if_readable(desired_guid)
91102
def user_not_found!
92103
resource_not_found!(:user)
93104
end
105+
106+
def org_managers_can_create_users?
107+
VCAP::CloudController::Config.config.get(:allow_user_creation_by_org_manager) && permission_queryer.is_org_manager?
108+
end
94109
end

app/messages/user_create_message.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,27 @@
22

33
module VCAP::CloudController
44
class UserCreateMessage < MetadataBaseMessage
5-
register_allowed_keys [:guid]
5+
register_allowed_keys %i[guid origin username]
6+
7+
class UserCreateValidator < ActiveModel::Validator
8+
def validate(record)
9+
if record.guid
10+
record.errors.add(:username, message: "cannot be provided with 'guid'") if record.username
11+
record.errors.add(:origin, message: "cannot be provided with 'guid'") if record.origin
12+
elsif record.username || record.origin
13+
record.errors.add(:origin, message: "can only be provided together with 'username'") unless record.username
14+
record.errors.add(:username, message: "can only be provided together with 'origin'") unless record.origin
15+
record.errors.add(:origin, message: "cannot be 'uaa' when creating a user by username") unless record.origin != 'uaa'
16+
else
17+
record.errors.add(:guid, message: "or 'username' and 'origin' must be provided")
18+
end
19+
end
20+
end
621

722
validates_with NoAdditionalKeysValidator
8-
validates :guid, guid: true
23+
validates :guid, guid: true, allow_nil: true
24+
validates :origin, string: true, allow_nil: true
25+
validates :username, string: true, allow_nil: true
26+
validates_with UserCreateValidator
927
end
1028
end

app/models/runtime/user.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,16 @@ def self.uaa_users_info(user_guids)
252252
uaa_username_lookup_client.users_for_ids(user_guids)
253253
end
254254

255+
def self.get_user_id_by_username_and_origin(username, origin)
256+
uaa_username_lookup_client = CloudController::DependencyLocator.instance.uaa_username_lookup_client
257+
uaa_username_lookup_client.ids_for_usernames_and_origins([username], [origin]).first
258+
end
259+
260+
def self.create_uaa_shadow_user(username, origin)
261+
uaa_shadow_user_creation_client = CloudController::DependencyLocator.instance.uaa_shadow_user_creation_client
262+
uaa_shadow_user_creation_client.create_shadow_user(username, origin)
263+
end
264+
255265
def self.user_visibility_filter(_)
256266
full_dataset_filter
257267
end

lib/cloud_controller/config_schemas/base/api_schema.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,18 @@ class ApiSchema < VCAP::Config
5858
optional(:ca_file) => String,
5959
:client_timeout => Integer,
6060
optional(:symmetric_secret) => String,
61-
optional(:symmetric_secret2) => String
61+
optional(:symmetric_secret2) => String,
62+
optional(:clients) => enum([
63+
{
64+
'name' => String,
65+
'id' => String,
66+
'secret' => String
67+
}
68+
], NilClass)
6269
},
6370

71+
optional(:allow_user_creation_by_org_manager) => bool,
72+
6473
logging: {
6574
level: String, # debug, info, etc.
6675
file: String, # Log file to use

lib/cloud_controller/dependency_locator.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,19 @@ def uaa_username_lookup_client
288288
)
289289
end
290290

291+
def uaa_shadow_user_creation_client
292+
client = config.get(:uaa, :clients)&.find { |client_config| client_config['name'] == 'cloud_controller_shadow_user_creation' }
293+
294+
return unless client
295+
296+
UaaClient.new(
297+
uaa_target: config.get(:uaa, :internal_url),
298+
client_id: client['id'],
299+
secret: client['secret'],
300+
ca_file: config.get(:uaa, :ca_file)
301+
)
302+
end
303+
291304
def routing_api_client
292305
return RoutingApi::DisabledClient.new if config.get(:routing_api).nil?
293306

lib/cloud_controller/permissions.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,10 @@ def can_write_globally?
9797
roles.admin?
9898
end
9999

100+
def is_org_manager?
101+
VCAP::CloudController::OrganizationManager.where(user_id: @user.id).any?
102+
end
103+
100104
def readable_org_guids
101105
readable_org_guids_query.select_map(:guid)
102106
end

lib/cloud_controller/uaa/uaa_client.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ def origins_for_username(username)
9696
raise UaaUnavailable
9797
end
9898

99+
def create_shadow_user(username, origin)
100+
with_cache_retry { scim.add(:user, { username: username, origin: origin, emails: [{ primary: true, value: username }] }) }
101+
rescue CF::UAA::TargetError => e
102+
raise e unless e.info['error'] == 'scim_resource_already_exists'
103+
104+
{ 'id' => e.info['user_id'] }
105+
rescue CF::UAA::UAAError => e
106+
logger.error("UAA request for creating a user failed: #{e.inspect}")
107+
raise UaaUnavailable
108+
end
109+
99110
def info
100111
CF::UAA::Info.new(uaa_target, uaa_connection_opts)
101112
end

0 commit comments

Comments
 (0)