From 172b8d0aa7d5334f1f3263e307c0f678b1c503ab Mon Sep 17 00:00:00 2001 From: "Marc A. Paradise" Date: Thu, 17 Oct 2024 17:24:45 -0400 Subject: [PATCH] WIP - allow org-admins to modify organizations In addition to give org-admins permissions to CRUD organizations, this removes the requirement that in order to modify an organization, the actor must be a member of the organization. However, they must still have appropriate permissions to perform any CRUD action related to an organization. This supports the multi-tenancy case where a customer has many organizations to manage but does not necessarily need to admins to be a part of those organizations. The primary use case is SaaS offering , in which customers have full control over a chef server installation but do not have local/chef-server-ctl access, and must keep the pivotal key locked down for security purposes. This functionality is already available using the pivotal/superuser key, but the pivotal key should not be widely distributed. This functionality was also originally intended to be available to org-admins but the completion of that work was never prioritized. Signed-off-by: Marc A. Paradise --- .../libraries/chef_server_data_bootstrap.rb | 12 ++++++++--- .../apps/oc_chef_wm/src/oc_chef_wm_base.erl | 20 ++++++++++++++++++- .../src/oc_chef_wm_named_organization.erl | 9 ++++++++- .../src/oc_chef_wm_org_associations.erl | 20 +++++++++++-------- .../config/chef_server_data_bootstrap.rb | 16 ++++++++++----- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/omnibus/files/server-ctl-cookbooks/infra-server/libraries/chef_server_data_bootstrap.rb b/omnibus/files/server-ctl-cookbooks/infra-server/libraries/chef_server_data_bootstrap.rb index 3c8bce6d32..7b3955a3e7 100644 --- a/omnibus/files/server-ctl-cookbooks/infra-server/libraries/chef_server_data_bootstrap.rb +++ b/omnibus/files/server-ctl-cookbooks/infra-server/libraries/chef_server_data_bootstrap.rb @@ -38,7 +38,7 @@ def bootstrap @superuser_authz_id = create_actor_in_authz(bifrost_superuser_id) users_authz_id = create_container_in_authz(superuser_authz_id) orgs_authz_id = create_container_in_authz(superuser_authz_id) - create_server_admins_global_group_in_bifrost(users_authz_id) + create_server_admins_global_group_in_bifrost(orgs_authz_id, users_authz_id) # put pivotal in server-admins global group insert_authz_actor_into_group(server_admins_authz_id, superuser_authz_id) @@ -61,13 +61,19 @@ def bootstrap private # Create and set up permissions for the server admins group. - def create_server_admins_global_group_in_bifrost(users_authz_id) + def create_server_admins_global_group_in_bifrost(orgs_container_authz_id, users_container_authz_id) @server_admins_authz_id = create_group_in_authz(bifrost_superuser_id) %w(create read update delete).each do |permission| # grant server admins group permission on the users container, # as the erchef superuser. - grant_authz_object_permission(permission, 'groups', 'containers', users_authz_id, + grant_authz_object_permission(permission, 'groups', 'containers', users_container_authz_id, server_admins_authz_id, superuser_authz_id) + + # grant server admins group permission on the organizations container, + # as the erchef superuser. + grant_authz_object_permission(permission, 'groups', 'containers', orgs_container_authz_id, + server_admins_authz_id, superuser_authz_id) + # grant superuser actor permissions on the server admin group, # as the bifrost superuser grant_authz_object_permission(permission, 'actors', 'groups', server_admins_authz_id, diff --git a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_base.erl b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_base.erl index e644104d2f..26ed56e1aa 100644 --- a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_base.erl +++ b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_base.erl @@ -29,6 +29,8 @@ content_types_provided/2, finish_request/2, forbidden/2, + is_authorized_no_membership/2, %% verify request signature only + is_authorized_no_membership/3, %% verify request signature only with custom extractor is_authorized/2, %% verify request signature and org membership if appropriate is_authorized/3, %% verify request signature and org membership if appropriate, with custom extractor fun malformed_request/2, %% verify request headers and size requirements, call module verify_request @@ -441,7 +443,23 @@ is_authorized(Req, State, Extractor) -> end; {false, ReqOther, StateOther} -> %% FIXME: the supported version is determined by the chef_authn application - %% also, see: https://wiki.corp.chef.io/display/CORP/RFC+Authentication+Version+Negotiation + {"X-Ops-Sign version=\"1.0\" version=\"1.1\"", ReqOther, StateOther}; + {{halt, _Code}, _Req, _State} = Halt -> Halt + end. + +is_authorized_no_membership(Req, State) -> + is_authorized_no_membership(Req, State, fun authorization_data_extractor/3). + +%% Verify request signature but do not verify org membership. Modules +%% can invoke this directly instead of mixing in the default is_verified/2 +%% if they rely only on permissions for authorization and not org membership. +-spec is_authorized_no_membership(wm_req(), #base_state{}, extractor()) -> any(). +is_authorized_no_membership(Req, State, Extractor) -> + case verify_request_signature(Req, State, Extractor) of + {true, _, _} = Result -> + Result; + {false, ReqOther, StateOther} -> + %% FIXME: the supported version is determined by the chef_authn application {"X-Ops-Sign version=\"1.0\" version=\"1.1\"", ReqOther, StateOther}; {{halt, _Code}, _Req, _State} = Halt -> Halt end. diff --git a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_named_organization.erl b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_named_organization.erl index 15c01bb986..5da8c1a1d2 100644 --- a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_named_organization.erl +++ b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_named_organization.erl @@ -14,7 +14,6 @@ malformed_request/2, ping/2, forbidden/2, - is_authorized/2, service_available/2]}]). -export([allowed_methods/2, @@ -28,6 +27,7 @@ -export([auth_info/2, init/1, init_resource_state/1, + is_authorized/2, malformed_request_message/3, request_type/0, validate_request/3]). @@ -47,6 +47,13 @@ request_type() -> allowed_methods(Req, State) -> {['GET', 'PUT', 'DELETE'], Req, State}. + +is_authorized(Req, State) -> + %% To support management of multi-tenant installations by users w/ appropriate permissions + %% who are not the superuser, the user does not need to be a member of the organization + %% to operate on the organization. + oc_chef_wm_base:is_authorized_no_membership(Req, State). + -spec validate_request(chef_wm:http_verb(), wm_req(), chef_wm:base_state()) -> {wm_req(), chef_wm:base_state()}. validate_request(Method, Req, State = #base_state{organization_guid = OrgId}) when Method == 'GET'; diff --git a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_associations.erl b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_associations.erl index 00fff1b12d..d728ccc439 100644 --- a/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_associations.erl +++ b/src/oc_erchef/apps/oc_chef_wm/src/oc_chef_wm_org_associations.erl @@ -28,7 +28,6 @@ malformed_request/2, ping/2, forbidden/2, - is_authorized/2, service_available/2]}]). -export([allowed_methods/2, @@ -43,6 +42,7 @@ -export([auth_info/2, init/1, init_resource_state/1, + is_authorized/2, malformed_request_message/3, create_path/2, request_type/0, @@ -67,6 +67,13 @@ allowed_methods(Req, State) -> end, {Allowed, Req, State}. +is_authorized(Req, State) -> + % To support management of multi-tenant installations by users w/ appropriate permissions + % who are not superusers, a user with appropriate ACLs does not need to be a member of + % the organization to operate on the organization - that includes the operations of this endpoint, + % CRD against org members. + chef_wm_base:is_authorized_no_membership(Req, State). + -spec validate_request(chef_wm:http_verb(), wm_req(), chef_wm:base_state()) -> {wm_req(), chef_wm:base_state()}. validate_request('POST', Req, #base_state{chef_db_context = DbContext} = State) -> @@ -117,14 +124,11 @@ auth_info(Req, #base_state{organization_name = OrgName, auth_info(Req, #base_state{requestor_id = RequestorAuthzId, organization_authz_id = OrgAuthzId, resource_state = #association_state{user = User} } = State) -> - case wrq:method(Req) of - 'POST' -> - % Only the superuser can force-create an org-user association - {superuser_only, Req, State}; - Method -> - {auth_type_for_method(Method, User, OrgAuthzId, RequestorAuthzId), Req, State} - end. + {auth_type_for_method(wrq:method(req), User, OrgAuthzId, RequestorAuthzId), Req, State}. +auth_type_for_method('POST', #chef_user{ authz_id = UserAuthzId }, OrgAuthzId, RequestorAuthzId) -> + %% requestor must have: update permissions in org and read permissions on user + [{object, OrgAuthzId, update}, {actor, UserAuthzId, read}]; auth_type_for_method('DELETE', #chef_user{ authz_id = UserAuthzId }, _OrgAuthzId, UserAuthzId) -> %% permissions-wise, user can always disassociate his or her own org association %% though we'll have additional safety checks below as well. diff --git a/src/oc_erchef/habitat/config/chef_server_data_bootstrap.rb b/src/oc_erchef/habitat/config/chef_server_data_bootstrap.rb index 296b23b946..5fa42feb76 100644 --- a/src/oc_erchef/habitat/config/chef_server_data_bootstrap.rb +++ b/src/oc_erchef/habitat/config/chef_server_data_bootstrap.rb @@ -23,7 +23,7 @@ def self.with_connection(database = 'template1', opts = {}) postgres['db_superuser']="{{cfg.sql_user}}" postgres['db_superuser_password']="{{cfg.sql_password}}" {{/if}} - + connection = nil # Some callers expect failure - this gives the option to suppress @@ -118,7 +118,7 @@ def bootstrap users_authz_id = create_container_in_authz(superuser_authz_id) orgs_authz_id = create_container_in_authz(superuser_authz_id) - create_server_admins_global_group_in_bifrost(users_authz_id) + create_server_admins_global_group_in_bifrost(orgs_authz_id, users_authz_id) # put pivotal in server-admins global group insert_authz_actor_into_group(server_admins_authz_id, superuser_authz_id) @@ -142,13 +142,19 @@ def bootstrap private # Create and set up permissions for the server admins group. - def create_server_admins_global_group_in_bifrost(users_authz_id) + def create_server_admins_global_group_in_bifrost(orgs_container_authz_id, users_container_authz_id) @server_admins_authz_id = create_group_in_authz(bifrost_superuser_id) %w{create read update delete}.each do |permission| # grant server admins group permission on the users container, # as the erchef superuser. - grant_authz_object_permission(permission, "groups", "containers", users_authz_id, + grant_authz_object_permission(permission, "groups", "containers", users_container_authz_id, server_admins_authz_id, superuser_authz_id) + + # grant server admins group permission on the organizations container, + # as the erchef superuser. + grant_authz_object_permission(permission, 'groups', 'containers', orgs_container_authz_id, + server_admins_authz_id, superuser_authz_id) + # grant superuser actor permissions on the server admin group, # as the bifrost superuser grant_authz_object_permission(permission, "actors", "groups", server_admins_authz_id, @@ -381,7 +387,7 @@ def load_superuser_public_key() @superuser_public_key = "DUMMY KEY FROM BOOTSTRAP" {{/if}} end - + def load_bifrost() bifrost={} {{#if bind.oc_bifrost}}