Add decryptData yaql function to murano engine
Adds a new yaql function 'decryptData' which pairs with 'encryptData' on the dashboard side. Requires a valid secret storage backend (e.g. Barbican) to be configured via Castellan in murano.conf, e.g. [key_manager] auth_type = keystone_password auth_url = <auth_url> username = <username> password = <password> project_id = <project_id> user_domain_name = <user_domain_name> Murano will still work fine without this config but the encrypt/decrypt functions will be unavailable. Partially-Implements blueprint: allow-encrypting-of-muranopl-properties Depends-On: I1be3a1e11e3f4c2170062927ad359bf679eb25d9 Change-Id: I09416b6d35ed2dafa823eca98262a4e23081e6eb
This commit is contained in:
parent
9cb54690e9
commit
9248605e67
|
@ -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))
|
|
@ -0,0 +1,10 @@
|
||||||
|
Application:
|
||||||
|
?:
|
||||||
|
type: com.paul.EncryptionDemo
|
||||||
|
my_password: encryptData($.instanceConfiguration.my_password)
|
||||||
|
|
||||||
|
Forms:
|
||||||
|
- instanceConfiguration:
|
||||||
|
fields:
|
||||||
|
- name: my_password
|
||||||
|
type: string
|
|
@ -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
|
|
@ -20,4 +20,5 @@ Application Developer Guide
|
||||||
use_cases
|
use_cases
|
||||||
app_development_framework
|
app_development_framework
|
||||||
app_debugging
|
app_debugging
|
||||||
garbage_collection
|
garbage_collection
|
||||||
|
encrypting_properties
|
||||||
|
|
|
@ -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 = <keystone_url>
|
||||||
|
username = <username>
|
||||||
|
password = <password>
|
||||||
|
project_id = <project_id>
|
||||||
|
user_domain_name = <domain_name>
|
||||||
|
|
||||||
|
Similarly, place the following configuration into `_50_murano.py` to configure
|
||||||
|
the murano-dashboard end::
|
||||||
|
|
||||||
|
KEY_MANAGER = {
|
||||||
|
'auth_url': <keystone_url>/v3',
|
||||||
|
'username': <username>,
|
||||||
|
'user_domain_name': <domain_name>,
|
||||||
|
'password': <password>,
|
||||||
|
'project_name': <project_name>,
|
||||||
|
'project_domain_name': <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
|
|
@ -8,3 +8,4 @@ namespace = oslo.messaging
|
||||||
namespace = oslo.middleware.cors
|
namespace = oslo.middleware.cors
|
||||||
namespace = oslo.policy
|
namespace = oslo.policy
|
||||||
namespace = oslo.service.service
|
namespace = oslo.service.service
|
||||||
|
namespace = castellan.config
|
||||||
|
|
|
@ -21,6 +21,7 @@ import time
|
||||||
|
|
||||||
import jsonpatch
|
import jsonpatch
|
||||||
import jsonpointer
|
import jsonpointer
|
||||||
|
from oslo_log import log as logging
|
||||||
from oslo_serialization import base64
|
from oslo_serialization import base64
|
||||||
import six
|
import six
|
||||||
from yaql.language import specs
|
from yaql.language import specs
|
||||||
|
@ -33,6 +34,11 @@ from murano.dsl import dsl
|
||||||
from murano.dsl import helpers
|
from murano.dsl import helpers
|
||||||
from murano.dsl import yaql_integration
|
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
|
_random_string_counter = None
|
||||||
|
|
||||||
|
@ -203,6 +209,25 @@ def logger(context, logger_name):
|
||||||
return log
|
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
|
@helpers.memoize
|
||||||
def get_context(runtime_version):
|
def get_context(runtime_version):
|
||||||
context = yaql_integration.create_empty_context()
|
context = yaql_integration.create_empty_context()
|
||||||
|
@ -213,6 +238,7 @@ def get_context(runtime_version):
|
||||||
context.register_function(random_name)
|
context.register_function(random_name)
|
||||||
context.register_function(patch_)
|
context.register_function(patch_)
|
||||||
context.register_function(logger)
|
context.register_function(logger)
|
||||||
|
context.register_function(decrypt_data, 'decryptData')
|
||||||
|
|
||||||
if runtime_version <= constants.RUNTIME_VERSION_1_1:
|
if runtime_version <= constants.RUNTIME_VERSION_1_1:
|
||||||
context.register_function(substr)
|
context.register_function(substr)
|
||||||
|
|
|
@ -318,3 +318,10 @@ Methods:
|
||||||
# Thus this assignment tests the fix for bug #1597452
|
# Thus this assignment tests the fix for bug #1597452
|
||||||
- $this.target: id($newObject)
|
- $this.target: id($newObject)
|
||||||
- Return: $this.target = $newObject
|
- Return: $this.target = $newObject
|
||||||
|
|
||||||
|
testDecryptData:
|
||||||
|
Arguments:
|
||||||
|
- value:
|
||||||
|
Contract: $.string().notNull()
|
||||||
|
Body:
|
||||||
|
- Return: decryptData($value)
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import mock
|
||||||
import six
|
import six
|
||||||
from testtools import matchers
|
from testtools import matchers
|
||||||
from yaql.language import exceptions as yaql_exceptions
|
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 object_model as om
|
||||||
from murano.tests.unit.dsl.foundation import test_case
|
from murano.tests.unit.dsl.foundation import test_case
|
||||||
|
|
||||||
|
from castellan.common import exception as castellan_exception
|
||||||
|
|
||||||
|
|
||||||
class TestEngineYaqlFunctions(test_case.DslTestCase):
|
class TestEngineYaqlFunctions(test_case.DslTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -249,3 +252,26 @@ class TestEngineYaqlFunctions(test_case.DslTestCase):
|
||||||
|
|
||||||
def test_new_object_assignment(self):
|
def test_new_object_assignment(self):
|
||||||
self.assertTrue(self._runner.testNewObjectAssignment())
|
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()
|
||||||
|
|
|
@ -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.
|
|
@ -46,3 +46,4 @@ oslo.utils>=3.20.0 # Apache-2.0
|
||||||
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
|
oslo.i18n!=3.15.2,>=2.1.0 # Apache-2.0
|
||||||
oslo.log>=3.22.0 # Apache-2.0
|
oslo.log>=3.22.0 # Apache-2.0
|
||||||
semantic-version>=2.3.1 # BSD
|
semantic-version>=2.3.1 # BSD
|
||||||
|
castellan>=0.7.0 # Apache-2.0
|
||||||
|
|
|
@ -61,6 +61,7 @@ oslo.config.opts =
|
||||||
murano = murano.opts:list_opts
|
murano = murano.opts:list_opts
|
||||||
keystone_authtoken = keystonemiddleware.opts:list_auth_token_opts
|
keystone_authtoken = keystonemiddleware.opts:list_auth_token_opts
|
||||||
murano.cfapi = murano.opts:list_cfapi_opts
|
murano.cfapi = murano.opts:list_cfapi_opts
|
||||||
|
castellan.config = castellan.options:list_opts
|
||||||
|
|
||||||
oslo.config.opts.defaults =
|
oslo.config.opts.defaults =
|
||||||
murano = murano.common.config:set_middleware_defaults
|
murano = murano.common.config:set_middleware_defaults
|
||||||
|
|
Loading…
Reference in New Issue