def test_service_principal_auth_client_secret(self): sp_auth = ServicePrincipalAuth('verySecret!') result = sp_auth.get_entry_to_persist('sp_id1', 'tenant1') self.assertEqual(result, { 'servicePrincipalId': 'sp_id1', 'servicePrincipalTenant': 'tenant1', 'accessToken': 'verySecret!' })
def test_service_principal_auth_client_cert(self): curr_dir = os.path.dirname(os.path.realpath(__file__)) test_cert_file = os.path.join(curr_dir, 'sp_cert.pem') sp_auth = ServicePrincipalAuth(test_cert_file) result = sp_auth.get_entry_to_persist('sp_id1', 'tenant1') self.assertEqual(result, { 'servicePrincipalId': 'sp_id1', 'servicePrincipalTenant': 'tenant1', 'certificateFile': test_cert_file, 'thumbprint': 'F0:6A:53:84:8B:BE:71:4A:42:90:D6:9D:33:52:79:C1:D0:10:73:FD' })
def test_find_subscriptions_from_service_principal_id(self, mock_auth_context): mock_auth_context.acquire_token_with_client_credentials.return_value = self.token_entry1 mock_arm_client = mock.MagicMock() mock_arm_client.subscriptions.list.return_value = [self.subscription1] finder = SubscriptionFinder(lambda _, _2: mock_auth_context, None, lambda _: mock_arm_client) mgmt_resource = 'https://management.core.windows.net/' # action subs = finder.find_from_service_principal_id('my app', ServicePrincipalAuth('my secret'), self.tenant_id, mgmt_resource) # assert self.assertEqual([self.subscription1], subs) mock_arm_client.tenants.list.assert_not_called() mock_auth_context.acquire_token.assert_not_called() mock_auth_context.acquire_token_with_client_credentials.assert_called_once_with( mgmt_resource, 'my app', 'my secret')
def test_find_subscriptions_from_service_principal_using_cert(self, mock_auth_context): mock_auth_context.acquire_token_with_client_certificate.return_value = self.token_entry1 mock_arm_client = mock.MagicMock() mock_arm_client.subscriptions.list.return_value = [self.subscription1] finder = SubscriptionFinder(lambda _, _2: mock_auth_context, None, lambda _: mock_arm_client) mgmt_resource = 'https://management.core.windows.net/' curr_dir = os.path.dirname(os.path.realpath(__file__)) test_cert_file = os.path.join(curr_dir, 'sp_cert.pem') # action subs = finder.find_from_service_principal_id('my app', ServicePrincipalAuth(test_cert_file), self.tenant_id, mgmt_resource) # assert self.assertEqual([self.subscription1], subs) mock_arm_client.tenants.list.assert_not_called() mock_auth_context.acquire_token.assert_not_called() mock_auth_context.acquire_token_with_client_certificate.assert_called_once_with( mgmt_resource, 'my app', mock.ANY, mock.ANY)
def check_service_principal(cli_ctx, resource_id, namespace, resource_type, root_type=None): from azure.cli.core._profile import ( ServicePrincipalAuth, _authentication_context_factory, ) try: # Get SP token sp_auth = ServicePrincipalAuth(SELF_DESTRUCT["clientSecret"]) context = _authentication_context_factory(cli_ctx, SELF_DESTRUCT["tenantId"], None) resource = cli_ctx.cloud.endpoints.active_directory_resource_id token = sp_auth.acquire_token(context, resource, SELF_DESTRUCT["clientId"]) # Check permissions for resource uri = "https://management.azure.com{}/providers/Microsoft.Authorization/permissions?api-version=2015-07-01".format( resource_id) headers = {"Authorization": "Bearer {}".format(token["accessToken"])} r = requests.get(uri, headers=headers) # TODO: Check expiration date of SP # If we can't read permissions - it may or may not be an actual problem # It's perfectly valid to create a "Delete Storage Accounts in resource group X only" cleanup role that can't read roleAssignments # I'd prefer always-correct behavior - so fail fast here if r.status_code != 200: error = r.json() LOGGER.error( "There was a failure when checking service principal permissions. Your self-destruct sequence cannot be activated!" ) LOGGER.error("%s: %s", error["error"]["code"], error["error"]["message"]) return False except Exception: # pylint: disable=broad-except LOGGER.error( "There was a failure when checking service principal permissions. Your self-destruct sequence cannot be activated!" ) return False # This is a mine field of complex behavior # action: '*' means everything, unless there's an overriding notAction # There can be wildcards inlined in both actions and notActions # Namespaces often don't match product names, etc permissions = r.json()["value"][0] reqs = { "*", "{}/*".format(namespace), # Microsoft.Storage/* "{}/*/delete".format(namespace), # Microsoft.Storage/*/delete "{}/{}/*".format(namespace, resource_type), # Microsoft.Storage/storageAccounts "{}/{}/delete".format( namespace, resource_type), # Microsoft.Storage/storageAccounts/delete } if root_type: reqs.add("{}/{}/{}/*".format(namespace, root_type, resource_type)) reqs.add("{}/{}/{}/delete".format(namespace, root_type, resource_type)) actions = set(permissions["actions"]) not_actions = set(permissions["notActions"]) if not reqs.intersection(actions) or reqs.intersection(not_actions): LOGGER.error( "Service principal delete permissions could not be verified. Your self-destruct sequence cannot be activated!" ) LOGGER.error("Needed: one of %s", reqs) LOGGER.error("Allow: %s", actions) LOGGER.error("Deny: %s", not_actions) return False return True
def check_service_principal(cli_ctx, resource_id, namespace, resource_type, root_type=None): from azure.cli.core._profile import ServicePrincipalAuth, _authentication_context_factory try: # Get SP token sp_auth = ServicePrincipalAuth(self_destruct['clientSecret']) context = _authentication_context_factory(cli_ctx, self_destruct['tenantId'], None) resource = cli_ctx.cloud.endpoints.active_directory_resource_id token = sp_auth.acquire_token(context, resource, self_destruct['clientId']) # Check permissions for resource uri = "https://management.azure.com{}/providers/Microsoft.Authorization/permissions?api-version=2015-07-01".format( resource_id) headers = {'Authorization': 'Bearer {}'.format(token['accessToken'])} r = requests.get(uri, headers=headers) # TODO: Check expiration date of SP # If we can't read permissions - it may or may not be an actual problem # It's perfectly valid to create a "Delete Storage Accounts in resource group X only" cleanup role that can't read roleAssignments # I'd prefer always-correct behavior - so fail fast here if r.status_code != 200: error = r.json() logger.error( 'There was a failure when checking service principal permissions. Your self-destruct sequence cannot be activated!' ) logger.error('{}: {}'.format(error['error']['code'], error['error']['message'])) return False except Exception: logger.error( 'There was a failure when checking service principal permissions. Your self-destruct sequence cannot be activated!' ) return False # This is a mine field of complex behavior # action: '*' means everything, unless there's an overriding notAction # There can be wildcards inlined in both actions and notActions # Namespaces often don't match product names, etc permissions = r.json()['value'][0] reqs = { '*', '{}/*'.format(namespace), # Microsoft.Storage/* '{}/*/delete'.format(namespace), # Microsoft.Storage/*/delete '{}/{}/*'.format(namespace, resource_type), # Microsoft.Storage/storageAccounts '{}/{}/delete'.format( namespace, resource_type) # Microsoft.Storage/storageAccounts/delete } if root_type: reqs.add('{}/{}/{}/*'.format(namespace, root_type, resource_type)) reqs.add('{}/{}/{}/delete'.format(namespace, root_type, resource_type)) actions = set(permissions['actions']) not_actions = set(permissions['notActions']) if not reqs.intersection(actions) or reqs.intersection(not_actions): logger.error( 'Service principal delete permissions could not be verified. Your self-destruct sequence cannot be activated!' ) logger.error('Needed: one of {}'.format(reqs)) logger.error('Allow: {}'.format(actions)) logger.error('Deny: {}'.format(not_actions)) return False return True