diff --git a/contrib/packages/EncryptionDemo/Classes/EncryptionDemo.yaml b/contrib/packages/EncryptionDemo/Classes/EncryptionDemo.yaml new file mode 100644 index 000000000..89e939bd9 --- /dev/null +++ b/contrib/packages/EncryptionDemo/Classes/EncryptionDemo.yaml @@ -0,0 +1,18 @@ +Namespaces: + =: com.paul + std: io.murano + res: io.murano.resources + +Name: EncryptionDemo + +Extends: std:Application + +Properties: + my_password: + Contract: $.string() + +Methods: + deploy: + Body: + - $reporter: $this.find(std:Environment).reporter + - $reporter.report($this, decryptData($.my_password)) diff --git a/contrib/packages/EncryptionDemo/UI/ui.yaml b/contrib/packages/EncryptionDemo/UI/ui.yaml new file mode 100644 index 000000000..1d296aec2 --- /dev/null +++ b/contrib/packages/EncryptionDemo/UI/ui.yaml @@ -0,0 +1,10 @@ +Application: + ?: + type: com.paul.EncryptionDemo + my_password: encryptData($.instanceConfiguration.my_password) + +Forms: + - instanceConfiguration: + fields: + - name: my_password + type: string diff --git a/contrib/packages/EncryptionDemo/manifest.yaml b/contrib/packages/EncryptionDemo/manifest.yaml new file mode 100644 index 000000000..66942dc8d --- /dev/null +++ b/contrib/packages/EncryptionDemo/manifest.yaml @@ -0,0 +1,6 @@ +FullName: com.paul.EncryptionDemo +Type: Application +Description: Simple app to demonstrate Murano encryption +Author: Paul Bourke +Classes: + com.paul.EncryptionDemo: EncryptionDemo.yaml diff --git a/doc/source/admin/appdev-guide/developer_index.rst b/doc/source/admin/appdev-guide/developer_index.rst index 74f7b5a5b..23a9d1462 100644 --- a/doc/source/admin/appdev-guide/developer_index.rst +++ b/doc/source/admin/appdev-guide/developer_index.rst @@ -20,4 +20,5 @@ Application Developer Guide use_cases app_development_framework app_debugging - garbage_collection \ No newline at end of file + garbage_collection + encrypting_properties diff --git a/doc/source/admin/appdev-guide/encrypting_properties.rst b/doc/source/admin/appdev-guide/encrypting_properties.rst new file mode 100644 index 000000000..8333aea92 --- /dev/null +++ b/doc/source/admin/appdev-guide/encrypting_properties.rst @@ -0,0 +1,60 @@ +.. _encrypting-properties: + +================================ +Managing Senstive Data in Murano +================================ + +Overview +-------- +If you are developing a Murano application that manages sensitive data such as +passwords, user data, etc, you may want to ensure this is stored in a secure +manner in the Murano backend. + +Murano offers two `yaql` functions to do this, `encryptData` and +`decryptData`. + +.. note:: Barbican or a similar compatible secret storage backend must be + configured to use this feature. + +Configuring +----------- +Murano makes use of Castellan_ to manage encryption using a supported secret +storage backend. As of OpenStack Pike, Barbican_ is the only supported +backend, and hence is the one tested by the Murano community. + +To configure Murano to use Barbican, place the following configuration into +`murano-engine.conf`:: + + [key_manager] + auth_type = 'keystone_password' + auth_url = + username = + password = + project_id = + user_domain_name = + +Similarly, place the following configuration into `_50_murano.py` to configure +the murano-dashboard end:: + + KEY_MANAGER = { + 'auth_url': /v3', + 'username': , + 'user_domain_name': , + 'password': , + 'project_name': , + 'project_domain_name': + } + +Example +------- +`encryptData(foo)`: Call to encrypt string `foo` in storage. Will return a +`uuid` which is used to retrieve the encrypted value. + +`decryptData(foo_key)`: Call to decrypt and retrieve the value represented by +`foo_key` from storage. + +There is an example application available in the murano repository_. + +.. _Castellan: https://github.com/openstack/castellan +.. _Barbican: https://github.com/openstack/barbican +.. _repository: https://git.openstack.org/cgit/openstack/murano/tree/contrib/packages/EncryptionDemo diff --git a/etc/oslo-config-generator/murano.conf b/etc/oslo-config-generator/murano.conf index b4fff6b1c..66b560f35 100644 --- a/etc/oslo-config-generator/murano.conf +++ b/etc/oslo-config-generator/murano.conf @@ -8,3 +8,4 @@ namespace = oslo.messaging namespace = oslo.middleware.cors namespace = oslo.policy namespace = oslo.service.service +namespace = castellan.config diff --git a/murano/engine/system/yaql_functions.py b/murano/engine/system/yaql_functions.py index 54c3ae392..568413b48 100644 --- a/murano/engine/system/yaql_functions.py +++ b/murano/engine/system/yaql_functions.py @@ -21,6 +21,7 @@ import time import jsonpatch import jsonpointer +from oslo_log import log as logging from oslo_serialization import base64 import six from yaql.language import specs @@ -33,6 +34,11 @@ from murano.dsl import dsl from murano.dsl import helpers from murano.dsl import yaql_integration +from castellan.common import exception as castellan_exception +from castellan.common import utils as castellan_utils +from castellan import key_manager + +LOG = logging.getLogger(__name__) _random_string_counter = None @@ -203,6 +209,25 @@ def logger(context, logger_name): return log +@specs.parameter('value', yaqltypes.String()) +@specs.extension_method +def decrypt_data(value): + manager = key_manager.API() + try: + context = castellan_utils.credential_factory(conf=cfg.CONF) + except castellan_exception.AuthTypeInvalidError as e: + LOG.exception(e) + LOG.error("Castellan must be correctly configured in order to use " + "decryptData()") + raise + try: + data = manager.get(context, value).get_encoded() + except castellan_exception.KeyManagerError as e: + LOG.exception(e) + raise + return data + + @helpers.memoize def get_context(runtime_version): context = yaql_integration.create_empty_context() @@ -213,6 +238,7 @@ def get_context(runtime_version): context.register_function(random_name) context.register_function(patch_) context.register_function(logger) + context.register_function(decrypt_data, 'decryptData') if runtime_version <= constants.RUNTIME_VERSION_1_1: context.register_function(substr) diff --git a/murano/tests/unit/dsl/meta/TestEngineFunctions.yaml b/murano/tests/unit/dsl/meta/TestEngineFunctions.yaml index a84adc6db..d0c369b7b 100644 --- a/murano/tests/unit/dsl/meta/TestEngineFunctions.yaml +++ b/murano/tests/unit/dsl/meta/TestEngineFunctions.yaml @@ -318,3 +318,10 @@ Methods: # Thus this assignment tests the fix for bug #1597452 - $this.target: id($newObject) - Return: $this.target = $newObject + + testDecryptData: + Arguments: + - value: + Contract: $.string().notNull() + Body: + - Return: decryptData($value) diff --git a/murano/tests/unit/dsl/test_engine_yaql_functions.py b/murano/tests/unit/dsl/test_engine_yaql_functions.py index f3bc35845..cd539a77b 100644 --- a/murano/tests/unit/dsl/test_engine_yaql_functions.py +++ b/murano/tests/unit/dsl/test_engine_yaql_functions.py @@ -12,6 +12,7 @@ # License for the specific language governing permissions and limitations # under the License. +import mock import six from testtools import matchers from yaql.language import exceptions as yaql_exceptions @@ -19,6 +20,8 @@ from yaql.language import exceptions as yaql_exceptions from murano.tests.unit.dsl.foundation import object_model as om from murano.tests.unit.dsl.foundation import test_case +from castellan.common import exception as castellan_exception + class TestEngineYaqlFunctions(test_case.DslTestCase): def setUp(self): @@ -249,3 +252,26 @@ class TestEngineYaqlFunctions(test_case.DslTestCase): def test_new_object_assignment(self): self.assertTrue(self._runner.testNewObjectAssignment()) + + @mock.patch('murano.engine.system.yaql_functions.key_manager') + @mock.patch('murano.engine.system.yaql_functions.castellan_utils') + def test_decrypt_data(self, mock_castellan_utils, mock_key_manager): + dummy_context = mock.MagicMock() + mock_castellan_utils.credential_factory.return_value = dummy_context + + encrypted_value = '91f784d0-5ef1-4b6f-9311-9b5a33d828d8' + decrypted_value = 'secret_password' + + mock_key_manager.API().get.return_value.get_encoded.return_value =\ + decrypted_value + self.assertEqual(decrypted_value, + self._runner.testDecryptData(encrypted_value)) + mock_key_manager.API().get.assert_called_once_with(dummy_context, + encrypted_value) + + @mock.patch('murano.engine.system.yaql_functions.LOG') + def test_decrypt_data_not_configured(self, mock_log): + encrypted_value = '91f784d0-5ef1-4b6f-9311-9b5a33d828d8' + self.assertRaises(castellan_exception.AuthTypeInvalidError, + self._runner.testDecryptData, encrypted_value) + mock_log.error.assert_called() diff --git a/releasenotes/notes/decrypt-yaql-function-6651d0f5d73bd58d.yaml b/releasenotes/notes/decrypt-yaql-function-6651d0f5d73bd58d.yaml new file mode 100644 index 000000000..7ace8158c --- /dev/null +++ b/releasenotes/notes/decrypt-yaql-function-6651d0f5d73bd58d.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added a new yaql function 'decryptData' which pairs with 'encryptData' on + the dashboard side. Application authors can use these functions to secure + sensitive input to their Murano applications such as passwords. + + Requires a valid secret storage backend (e.g. Barbican) to be configured + via Castellan. diff --git a/requirements.txt b/requirements.txt index e9781f3be..f666f598d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -46,3 +46,4 @@ oslo.utils>=3.20.0 # Apache-2.0 oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0 oslo.log>=3.22.0 # Apache-2.0 semantic-version>=2.3.1 # BSD +castellan>=0.7.0 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index 8c37a0699..119e5d969 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ oslo.config.opts = murano = murano.opts:list_opts keystone_authtoken = keystonemiddleware.opts:list_auth_token_opts murano.cfapi = murano.opts:list_cfapi_opts + castellan.config = castellan.options:list_opts oslo.config.opts.defaults = murano = murano.common.config:set_middleware_defaults