diff --git a/conftest.py b/conftest.py index c8ecd63d0c35..1a39069a5e13 100644 --- a/conftest.py +++ b/conftest.py @@ -80,13 +80,13 @@ def api_client_inject_project_id(cloud_config): the project ID from cloud_config.""" import googleapiclient.http - class ProjectIdInjectingHttpRequest(googleapiclient.http.HttpRequest): - def __init__(self, http, postproc, uri, *args, **kwargs): - uri = uri.replace('YOUR_PROJECT_ID', cloud_config.project) - super(ProjectIdInjectingHttpRequest, self).__init__( - http, postproc, uri, *args, **kwargs) + old_execute = googleapiclient.http.HttpRequest.execute + + def new_execute(self, http=None, num_retries=0): + self.uri = self.uri.replace('YOUR_PROJECT_ID', cloud_config.project) + return old_execute(self, http=http, num_retries=num_retries) with mock.patch( - 'googleapiclient.http.HttpRequest', - new=ProjectIdInjectingHttpRequest): + 'googleapiclient.http.HttpRequest.execute', + new=new_execute): yield diff --git a/kms/api-client/README.rst b/kms/api-client/README.rst new file mode 100644 index 000000000000..4c660232385b --- /dev/null +++ b/kms/api-client/README.rst @@ -0,0 +1,109 @@ +.. This file is automatically generated. Do not edit this file directly. + +Google Cloud KMS API Python Samples +=============================================================================== + +This directory contains samples for Google Cloud KMS API. The `Google Cloud KMS API`_ is a service that allows you to keep encryption keys centrally in the cloud, for direct use by cloud services. + + + + +.. _Google Cloud KMS API: https://cloud.google.com/kms/docs/ + +Setup +------------------------------------------------------------------------------- + + +Authentication +++++++++++++++ + +Authentication is typically done through `Application Default Credentials`_, +which means you do not have to change the code to authenticate as long as +your environment has credentials. You have a few options for setting up +authentication: + +#. When running locally, use the `Google Cloud SDK`_ + + .. code-block:: bash + + gcloud beta auth application-default login + + +#. When running on App Engine or Compute Engine, credentials are already + set-up. However, you may need to configure your Compute Engine instance + with `additional scopes`_. + +#. You can create a `Service Account key file`_. This file can be used to + authenticate to Google Cloud Platform services from any environment. To use + the file, set the ``GOOGLE_APPLICATION_CREDENTIALS`` environment variable to + the path to the key file, for example: + + .. code-block:: bash + + export GOOGLE_APPLICATION_CREDENTIALS=/path/to/service_account.json + +.. _Application Default Credentials: https://cloud.google.com/docs/authentication#getting_credentials_for_server-centric_flow +.. _additional scopes: https://cloud.google.com/compute/docs/authentication#using +.. _Service Account key file: https://developers.google.com/identity/protocols/OAuth2ServiceAccount#creatinganaccount + +Install Dependencies +++++++++++++++++++++ + +#. Install `pip`_ and `virtualenv`_ if you do not already have them. + +#. Create a virtualenv. Samples are compatible with Python 2.7 and 3.4+. + + .. code-block:: bash + + $ virtualenv env + $ source env/bin/activate + +#. Install the dependencies needed to run the samples. + + .. code-block:: bash + + $ pip install -r requirements.txt + +.. _pip: https://pip.pypa.io/ +.. _virtualenv: https://virtualenv.pypa.io/ + +Samples +------------------------------------------------------------------------------- + +Quickstart ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python quickstart.py + + +Snippets ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + + + +To run this sample: + +.. code-block:: bash + + $ python snippets.py + + usage: snippets.py [-h] + {create_keyring,create_cryptokey,encrypt,decrypt,disable_cryptokey_version,destroy_cryptokey_version,add_member_to_cryptokey_policy,get_keyring_policy} + ... + + positional arguments: + {create_keyring,create_cryptokey,encrypt,decrypt,disable_cryptokey_version,destroy_cryptokey_version,add_member_to_cryptokey_policy,get_keyring_policy} + + optional arguments: + -h, --help show this help message and exit + + + + +.. _Google Cloud SDK: https://cloud.google.com/sdk/ \ No newline at end of file diff --git a/kms/api/README.rst.in b/kms/api-client/README.rst.in similarity index 87% rename from kms/api/README.rst.in rename to kms/api-client/README.rst.in index 68901cf11543..9489051c2c63 100644 --- a/kms/api/README.rst.in +++ b/kms/api-client/README.rst.in @@ -15,3 +15,6 @@ setup: samples: - name: Quickstart file: quickstart.py +- name: Snippets + file: snippets.py + show_help: True diff --git a/kms/api/quickstart.py b/kms/api-client/quickstart.py similarity index 100% rename from kms/api/quickstart.py rename to kms/api-client/quickstart.py diff --git a/kms/api/quickstart_test.py b/kms/api-client/quickstart_test.py similarity index 83% rename from kms/api/quickstart_test.py rename to kms/api-client/quickstart_test.py index d5a196eb3950..0db901e6bf3e 100644 --- a/kms/api/quickstart_test.py +++ b/kms/api-client/quickstart_test.py @@ -13,9 +13,7 @@ # limitations under the License. -def test_quickstart(api_client_inject_project_id, capsys): +def test_quickstart(api_client_inject_project_id): import quickstart quickstart.run_quickstart() - out, _ = capsys.readouterr() - assert 'No key rings found' in out diff --git a/kms/api/requirements.txt b/kms/api-client/requirements.txt similarity index 100% rename from kms/api/requirements.txt rename to kms/api-client/requirements.txt diff --git a/kms/api-client/snippets.py b/kms/api-client/snippets.py new file mode 100644 index 000000000000..9469b9398dd8 --- /dev/null +++ b/kms/api-client/snippets.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python + +# Copyright 2017 Google, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and + +import argparse +import base64 +import io + +from googleapiclient import discovery + + +# [START kms_create_keyring] +def create_keyring(project_id, location, keyring): + """Creates a KeyRing in the given location (e.g. global).""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the location associated with the KeyRing. + parent = 'projects/{}/locations/{}'.format(project_id, location) + + # Create KeyRing + request = kms_client.projects().locations().keyRings().create( + parent=parent, body={}, keyRingId=keyring) + response = request.execute() + + print('Created KeyRing {}.'.format(response['name'])) +# [END kms_create_keyring] + + +# [START kms_create_cryptokey] +def create_cryptokey(project_id, location, keyring, cryptokey): + """Creates a CryptoKey within a KeyRing in the given location.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the KeyRing associated with the CryptoKey. + parent = 'projects/{}/locations/{}/keyRings/{}'.format( + project_id, location, keyring) + + # Create a CryptoKey for the given KeyRing. + request = kms_client.projects().locations().keyRings().cryptoKeys().create( + parent=parent, body={'purpose': 'ENCRYPT_DECRYPT'}, + cryptoKeyId=cryptokey) + response = request.execute() + + print('Created CryptoKey {}.'.format(response['name'])) +# [END kms_create_cryptokey] + + +# [START kms_encrypt] +def encrypt(project_id, location, keyring, cryptokey, plaintext_file_name, + encrypted_file_name): + """Encrypts data from a plaintext_file_name using the provided CryptoKey + and saves it to an encrypted_file_name so it can only be recovered with a + call to decrypt.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the CryptoKey. + name = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format( + project_id, location, keyring, cryptokey) + + # Read text from the input file. + with io.open(plaintext_file_name, 'rb') as plaintext_file: + plaintext = plaintext_file.read() + encoded_text = base64.b64encode(plaintext) + + # Use the KMS API to encrypt the text. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + request = cryptokeys.encrypt( + name=name, body={'plaintext': encoded_text.decode('utf-8')}) + response = request.execute() + + # Write the encrypted text to a file. + with io.open(encrypted_file_name, 'wb') as encrypted_file: + encrypted_file.write(response['ciphertext'].encode('utf-8')) + + print('Saved encrypted text to {}.'.format(encrypted_file_name)) +# [END kms_encrypt] + + +# [START kms_decrypt] +def decrypt(project_id, location, keyring, cryptokey, encrypted_file_name, + decrypted_file_name): + """Decrypts data from encrypted_file_name that was previously encrypted + using the CryptoKey with a call to encrypt. Outputs decrypted data to + decrpyted_file_name.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the CryptoKey. + name = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format( + project_id, location, keyring, cryptokey) + + # Read cipher text from the input file. + with io.open(encrypted_file_name, 'rb') as encrypted_file: + cipher_text = encrypted_file.read() + + # Use the KMS API to decrypt the text. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + request = cryptokeys.decrypt( + name=name, body={'ciphertext': cipher_text.decode('utf-8')}) + response = request.execute() + + # Write the plain text to a file. + with io.open(decrypted_file_name, 'wb') as decrypted_file: + plaintext_encoded = response['plaintext'] + plaintext_decoded = base64.b64decode(plaintext_encoded) + decrypted_file.write(plaintext_decoded) + + print('Saved decrypted text to {}.'.format(decrypted_file_name)) +# [END kms_decrypt] + + +# [START kms_disable_cryptokey_version] +def disable_cryptokey_version(project_id, location, keyring, cryptokey, + version): + """Disables a CryptoKeyVersion associated with a given CryptoKey and + KeyRing.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # Construct the resource name of the CryptoKeyVersion. + name = ( + 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/' + 'cryptoKeyVersions/{}' + .format(project_id, location, keyring, cryptokey, version)) + + # Use the KMS API to disable the CryptoKeyVersion. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + request = cryptokeys.cryptoKeyVersions().patch( + name=name, body={'state': 'DISABLED'}, updateMask='state') + response = request.execute() + + print('CryptoKeyVersion {}\'s state has been set to {}.'.format( + name, response['state'])) +# [END kms_disable_cryptokey_version] + + +# [START kms_destroy_cryptokey_version] +def destroy_cryptokey_version( + project_id, location, keyring, cryptokey, version): + """Schedules a CryptoKeyVersion associated with a given CryptoKey and + KeyRing for destruction 24 hours in the future.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # Construct the resource name of the CryptoKeyVersion. + name = ( + 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/' + 'cryptoKeyVersions/{}' + .format(project_id, location, keyring, cryptokey, version)) + + # Use the KMS API to schedule the CryptoKeyVersion for destruction. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + request = cryptokeys.cryptoKeyVersions().destroy(name=name, body={}) + response = request.execute() + + print('CryptoKeyVersion {}\'s state has been set to {}.'.format( + name, response['state'])) +# [END kms_destroy_cryptokey_version] + + +# [START kms_add_member_to_cryptokey_policy] +def add_member_to_cryptokey_policy( + project_id, location, keyring, cryptokey, member, role): + """Adds a member with a given role to the Identity and Access Management + (IAM) policy for a given CryptoKey associated with a KeyRing.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the CryptoKey. + parent = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format( + project_id, location, keyring, cryptokey) + + # Get the current IAM policy and add the new member to it. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + policy_request = cryptokeys.getIamPolicy(resource=parent) + policy_response = policy_request.execute() + bindings = [] + if 'bindings' in policy_response.keys(): + bindings = policy_response['bindings'] + members = [] + members.append(member) + new_binding = dict() + new_binding['role'] = role + new_binding['members'] = members + bindings.append(new_binding) + policy_response['bindings'] = bindings + + # Set the new IAM Policy. + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + request = cryptokeys.setIamPolicy( + resource=parent, body={'policy': policy_response}) + request.execute() + + print_msg = ( + 'Member {} added with role {} to policy for CryptoKey {} in KeyRing {}' + .format(member, role, cryptokey, keyring)) + print(print_msg) +# [END kms_add_member_to_cryptokey_policy] + + +# [START kms_get_keyring_policy] +def get_keyring_policy(project_id, location, keyring): + """Gets the Identity and Access Management (IAM) policy for a given KeyRing + and prints out roles and the members assigned to those roles.""" + + # Creates an API client for the KMS API. + kms_client = discovery.build('cloudkms', 'v1beta1') + + # The resource name of the KeyRing. + parent = 'projects/{}/locations/{}/keyRings/{}'.format( + project_id, location, keyring) + + # Get the current IAM policy. + request = kms_client.projects().locations().keyRings().getIamPolicy( + resource=parent) + response = request.execute() + + if 'bindings' in response.keys(): + print('Printing IAM policy for resource {}:'.format(parent)) + for binding in response['bindings']: + print('') + print('Role: {}'.format(binding['role'])) + print('Members:') + for member in binding['members']: + print(member) + print('') + else: + print('No roles found for resource {}.'.format(parent)) +# [END kms_get_keyring_policy] + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + subparsers = parser.add_subparsers(dest='command') + + create_keyring_parser = subparsers.add_parser('create_keyring') + create_keyring_parser.add_argument('project_id') + create_keyring_parser.add_argument('location') + create_keyring_parser.add_argument('keyring') + + create_cryptokey_parser = subparsers.add_parser('create_cryptokey') + create_cryptokey_parser.add_argument('project_id') + create_cryptokey_parser.add_argument('location') + create_cryptokey_parser.add_argument('keyring') + create_cryptokey_parser.add_argument('cryptokey') + + encrypt_parser = subparsers.add_parser('encrypt') + encrypt_parser.add_argument('project_id') + encrypt_parser.add_argument('location') + encrypt_parser.add_argument('keyring') + encrypt_parser.add_argument('cryptokey') + encrypt_parser.add_argument('infile') + encrypt_parser.add_argument('outfile') + + decrypt_parser = subparsers.add_parser('decrypt') + decrypt_parser.add_argument('project_id') + decrypt_parser.add_argument('location') + decrypt_parser.add_argument('keyring') + decrypt_parser.add_argument('cryptokey') + decrypt_parser.add_argument('infile') + decrypt_parser.add_argument('outfile') + + disable_cryptokey_version_parser = subparsers.add_parser( + 'disable_cryptokey_version') + disable_cryptokey_version_parser.add_argument('project_id') + disable_cryptokey_version_parser.add_argument('location') + disable_cryptokey_version_parser.add_argument('keyring') + disable_cryptokey_version_parser.add_argument('cryptokey') + disable_cryptokey_version_parser.add_argument('version') + + destroy_cryptokey_version_parser = subparsers.add_parser( + 'destroy_cryptokey_version') + destroy_cryptokey_version_parser.add_argument('project_id') + destroy_cryptokey_version_parser.add_argument('location') + destroy_cryptokey_version_parser.add_argument('keyring') + destroy_cryptokey_version_parser.add_argument('cryptokey') + destroy_cryptokey_version_parser.add_argument('version') + + add_member_to_cryptokey_policy_parser = subparsers.add_parser( + 'add_member_to_cryptokey_policy') + add_member_to_cryptokey_policy_parser.add_argument('project_id') + add_member_to_cryptokey_policy_parser.add_argument('location') + add_member_to_cryptokey_policy_parser.add_argument('keyring') + add_member_to_cryptokey_policy_parser.add_argument('cryptokey') + add_member_to_cryptokey_policy_parser.add_argument('member') + add_member_to_cryptokey_policy_parser.add_argument('role') + + get_keyring_policy_parser = subparsers.add_parser('get_keyring_policy') + get_keyring_policy_parser.add_argument('project_id') + get_keyring_policy_parser.add_argument('location') + get_keyring_policy_parser.add_argument('keyring') + + args = parser.parse_args() + + if args.command == 'create_keyring': + create_keyring( + args.project_id, + args.location, + args.keyring) + elif args.command == 'create_cryptokey': + create_cryptokey( + args.project_id, + args.location, + args.keyring, + args.cryptokey) + elif args.command == 'encrypt': + encrypt( + args.project_id, + args.location, + args.keyring, + args.cryptokey, + args.infile, + args.outfile) + elif args.command == 'decrypt': + decrypt( + args.project_id, + args.location, + args.keyring, + args.cryptokey, + args.infile, + args.outfile) + elif args.command == 'disable_cryptokey_version': + disable_cryptokey_version( + args.project_id, + args.location, + args.keyring, + args.cryptokey, + args.version) + elif args.command == 'destroy_cryptokey_version': + destroy_cryptokey_version( + args.project_id, + args.location, + args.keyring, + args.cryptokey, + args.version) + elif args.command == 'add_member_to_cryptokey_policy': + add_member_to_cryptokey_policy( + args.project_id, + args.location, + args.keyring, + args.cryptokey, + args.member, + args.role) + elif args.command == 'get_keyring_policy': + get_keyring_policy( + args.project_id, + args.location, + args.keyring) diff --git a/kms/api-client/snippets_test.py b/kms/api-client/snippets_test.py new file mode 100644 index 000000000000..e4deda1c1280 --- /dev/null +++ b/kms/api-client/snippets_test.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python + +# Copyright 2017 Google, Inc +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and + +from googleapiclient import discovery + +import snippets + + +# Your Google Cloud Platform Key Location +LOCATION = 'global' + +# Your Google Cloud Platform KeyRing name +KEYRING = 'sample-keyring-43' + +# Your Google Cloud Platform CryptoKey name +CRYPTOKEY = 'sample-key-43' + +# Your Google Cloud Platform CryptoKeyVersion name +VERSION = 1 + +# A member to add to our IAM policy +MEMBER = 'user:ryanmats@google.com' + +# The role we want our new member to have for our IAM policy +ROLE = 'roles/owner' + + +def test_create_keyring(capsys, cloud_config): + snippets.create_keyring(cloud_config.project, LOCATION, KEYRING) + out, _ = capsys.readouterr() + expected = 'Created KeyRing projects/{}/locations/{}/keyRings/{}.'.format( + cloud_config.project, LOCATION, KEYRING) + assert expected in out + + +def test_create_cryptokey(capsys, cloud_config): + snippets.create_cryptokey( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY) + out, _ = capsys.readouterr() + expected = ( + 'Created CryptoKey projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}.' + .format(cloud_config.project, LOCATION, KEYRING, CRYPTOKEY)) + assert expected in out + + +def test_encrypt_decrypt(capsys, cloud_config, tmpdir): + # Write to a plaintext file. + tmpdir.join('in.txt').write('SampleText') + + # Construct temporary files. + plaintext_file = tmpdir.join('in.txt') + encrypted_file = tmpdir.join('out.txt') + decrypted_file = tmpdir.join('out2.txt') + + # Encrypt text and then decrypt it. + snippets.encrypt( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, + str(plaintext_file), str(encrypted_file)) + snippets.decrypt( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, + str(encrypted_file), str(decrypted_file)) + + # Make sure the decrypted text matches the original text. + decrypted_text = decrypted_file.read() + assert decrypted_text == 'SampleText' + + # Make sure other output is as expected. + out, _ = capsys.readouterr() + assert 'Saved encrypted text to {}.'.format(str(encrypted_file)) in out + assert 'Saved decrypted text to {}.'.format(str(decrypted_file)) in out + + +def test_disable_cryptokey_version(capsys, cloud_config): + snippets.disable_cryptokey_version( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, VERSION) + out, _ = capsys.readouterr() + expected = ( + 'CryptoKeyVersion projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/' + 'cryptoKeyVersions/{}\'s state has been set to {}.' + .format( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, VERSION, + 'DISABLED')) + assert expected in out + + +def test_destroy_cryptokey_version(capsys, cloud_config): + snippets.destroy_cryptokey_version( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, VERSION) + out, _ = capsys.readouterr() + expected = ( + 'CryptoKeyVersion projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}/' + 'cryptoKeyVersions/{}\'s state has been set to {}.' + .format( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, VERSION, + 'DESTROY_SCHEDULED')) + assert expected in out + + +def test_add_member_to_cryptokey_policy(capsys, cloud_config): + snippets.add_member_to_cryptokey_policy( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY, MEMBER, ROLE) + out, _ = capsys.readouterr() + expected = ( + 'Member {} added with role {} to policy for CryptoKey {} in KeyRing {}' + .format(MEMBER, ROLE, CRYPTOKEY, KEYRING)) + assert expected in out + + kms_client = discovery.build('cloudkms', 'v1beta1') + parent = 'projects/{}/locations/{}/keyRings/{}/cryptoKeys/{}'.format( + cloud_config.project, LOCATION, KEYRING, CRYPTOKEY) + cryptokeys = kms_client.projects().locations().keyRings().cryptoKeys() + policy_request = cryptokeys.getIamPolicy(resource=parent) + policy_response = policy_request.execute() + assert 'bindings' in policy_response.keys() + bindings = policy_response['bindings'] + found_member_role_pair = False + for binding in bindings: + if binding['role'] == ROLE: + for user in binding['members']: + if user == MEMBER: + found_member_role_pair = True + assert found_member_role_pair + + +def test_get_keyring_policy(capsys, cloud_config): + project_id = cloud_config.project + snippets.get_keyring_policy(project_id, LOCATION, KEYRING) + out, _ = capsys.readouterr() + expected_roles_exist = ( + 'Printing IAM policy for resource projects/{}/locations/{}/keyRings/{}' + ':'.format(project_id, LOCATION, KEYRING)) + expected_no_roles = ( + 'No roles found for resource projects/{}/locations/{}/keyRings/{}.' + .format(project_id, LOCATION, KEYRING)) + assert (expected_roles_exist in out) or (expected_no_roles in out)