Skip to content

Commit 258267a

Browse files
authored
Implement SECRET token (#571)
* JIRA-WDT-370 Implement @@secret token with basic directory structure * JIRA-WDT-370 Implement @@secret token lookup using WDT_MODEL_SECRETS_NAME_DIR_PAIRS * JIRA-WDT-370 Implement @@secret token lookup using WDT_MODEL_SECRETS_NAME_DIR_PAIRS
1 parent 6ec7f77 commit 258267a

File tree

7 files changed

+227
-33
lines changed

7 files changed

+227
-33
lines changed

README.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,9 +176,9 @@ As the example above shows, the `SecurityConfiguration` element has no named sub
176176

177177
The model allows the use of tokens that are substituted with text values as the model is processed. This section describes several types of tokens.
178178

179-
**Variable placeholders** are declared with the syntax `@@PROP:<variable>@@`. This type of token represents a value that is resolved at runtime using a variables file in a standard Java properties file format. Variables can be used for any value and for some names. For example, to automate standing up an environment with one or more applications in the Oracle Java Cloud Service, service provisioning does not allow the provisioning script to specify the server names. For example, if the application being deployed immediately following provisioning needs to tweak the Server Start arguments to specify a Java system property, the model can use a variable placeholder in place of the server name and populate the variable file with the provisioned server names dynamically between provisioning and application deployment.
179+
**Variable tokens** are declared with the syntax `@@PROP:<variable>@@`. This type of token represents a value that is resolved at runtime using a variables file in a standard Java properties file format. Variables can be used for any value and for some names. For example, to automate standing up an environment with one or more applications in the Oracle Java Cloud Service, service provisioning does not allow the provisioning script to specify the server names. For example, if the application being deployed immediately following provisioning needs to tweak the Server Start arguments to specify a Java system property, the model can use a variable placeholder in place of the server name and populate the variable file with the provisioned server names dynamically between provisioning and application deployment.
180180

181-
**File placeholders** are declared with the syntax `@@FILE:<filename>@@`. This type of token is similar to a variable placeholder, but the token references a single value that is read from the specified file. For example, the model may reference a password attribute as follows:
181+
**File tokens** are declared with the syntax `@@FILE:<filename>@@`. This type of token is similar to a variable token, but it references a single value that is read from the specified file. For example, the model may reference a password attribute as follows:
182182
```yaml
183183
PasswordEncrypted: '@@FILE:/home/me/dbcs1.txt@@'
184184
```
@@ -194,6 +194,27 @@ PasswordEncrypted: '@@FILE:/dir/@@PROP:name@@.txt@@'
194194
PasswordEncrypted: '@@FILE:@@ORACLE_HOME@@/dir/name.txt@@'
195195
```
196196

197+
**Environment tokens** are declared with the syntax `@@ENV:<name>@@`. This type of token is resolved by looking up the system environment variable `<name>`, and substituting that value for the token.
198+
199+
**Secret tokens** are declared with the syntax `@@SECRET:<name>:<key>@@`. This type of token is resolved by determining the location of a Kubernetes secret file, and reading the first line from that file. That line is substituted for the token.
200+
201+
There are two methods for deriving the location of the Kubernetes secret file. The first method involves using one or more configured root directories, and looking for the secret file in the path `<root-directory>/<name>/<key>`.
202+
203+
The root directories are configured as a comma-separated list of directories, using the environment variable `WDT_MODEL_SECRETS_DIRS`. For example, if `WDT_MODEL_SECRETS_DIRS` is set to `/etc/my-secrets,/etc/your-secrets`, then the token `@@SECRET:secrets:the-secret@@` will search the following locations:
204+
```
205+
/etc/my-secrets/secrets/the-secret
206+
/etc/your-secrets/secrets/the-secret
207+
```
208+
If either of these files is found, the secret is read from that file and substituted in the model.
209+
210+
The second method for locating the Kubernetes secret file is to use the environment variable `WDT_MODEL_SECRETS_NAME_DIR_PAIRS` to map `<name>` values to specific directory locations. For example, if `WDT_MODEL_SECRETS_NAME_DIR_PAIRS` is set to `my-root=/etc/my-secrets,your-root=/etc/your-secrets`, then the token `@@SECRET:your-root:the-secret@@` will look for the secrets file at:
211+
```
212+
/etc/your-secrets/the-secret
213+
```
214+
If the `<name>` value has a corresponding mapped directory in `WDT_MODEL_SECRETS_NAME_DIR_PAIRS`, then that directory will take precedence over any roots specified in `WDT_MODEL_SECRETS_DIRS`.
215+
216+
NOTE: It is important that the secrets directories contain only secrets files, because those files are examined to create a list of available name/key pairs.
217+
197218
**Path tokens** are tokens that reference known values, and can be used to make the model more portable. For example, a model may reference a WebLogic library source path as:
198219
```yaml
199220
SourcePath: '@@WL_HOME@@/common/deployable-libraries/jsf-2.0.war'

core/src/main/python/validate.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from oracle.weblogic.deploy.util import CLAException
1313
from oracle.weblogic.deploy.util import TranslateException
14+
from oracle.weblogic.deploy.util import VariableException
1415
from oracle.weblogic.deploy.util import WebLogicDeployToolingVersion
1516
from oracle.weblogic.deploy.validate import ValidateException
1617

@@ -186,7 +187,7 @@ def __perform_model_file_validation(model_file_name, model_context):
186187

187188
model_validator.validate_in_standalone_mode(model_dictionary, variable_map,
188189
model_context.get_archive_file_name())
189-
except TranslateException, te:
190+
except (TranslateException, VariableException), te:
190191
__logger.severe('WLSDPLY-20009', _program_name, model_file_name, te.getLocalizedMessage(),
191192
error=te, class_name=_class_name, method_name=_method_name)
192193
ex = exception_helper.create_validate_exception(te.getLocalizedMessage(), error=te)

core/src/main/python/wlsdeploy/util/variables.py

Lines changed: 164 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@
2020
from wlsdeploy.util import path_utils
2121
from wlsdeploy.exception import exception_helper
2222
from wlsdeploy.logging import platform_logger
23+
from wlsdeploy.util import dictionary_utils
2324

2425
_class_name = "variables"
2526
_logger = platform_logger.PlatformLogger('wlsdeploy.variables')
2627
_file_variable_pattern = re.compile("@@FILE:[\w.\\\/:-]+@@")
2728
_property_pattern = re.compile("(@@PROP:([\\w.-]+)@@)")
2829
_environment_pattern = re.compile("(@@ENV:([\\w.-]+)@@)")
30+
_secret_pattern = re.compile("(@@SECRET:([\\w.-]+):([\\w.-]+)@@)")
2931
_file_nested_variable_pattern = re.compile("@@FILE:@@[\w]+@@[\w.\\\/:-]+@@")
3032

33+
_secret_dirs_variable = "WDT_MODEL_SECRETS_DIRS"
34+
_secret_dirs_default = "/weblogic-operator/config-overrides-secrets"
35+
_secret_dir_pairs_variable="WDT_MODEL_SECRETS_NAME_DIR_PAIRS"
36+
37+
_secret_token_map = None
38+
3139

3240
def load_variables(file_path):
3341
"""
@@ -207,10 +215,10 @@ def _process_node(nodes, variables, model_context):
207215

208216
def _substitute(text, variables, model_context):
209217
"""
210-
Substitute the variable placeholders with the variable value.
211-
:param text: the text to process for variable placeholders
218+
Substitute token placeholders with their derived values.
219+
:param text: the text to process for token placeholders
212220
:param variables: the variables to use
213-
:param model_context: used to resolve variables in file paths
221+
:param model_context: used to determine the validation method (strict, lax, etc.)
214222
:return: the replaced text
215223
"""
216224
method_name = '_substitute'
@@ -223,14 +231,8 @@ def _substitute(text, variables, model_context):
223231
for token, key in matches:
224232
# log, or throw an exception if key is not found.
225233
if key not in variables:
226-
if model_context.get_validation_method() == 'strict':
227-
_logger.severe('WLSDPLY-01732', key, class_name=_class_name, method_name=method_name)
228-
ex = exception_helper.create_variable_exception('WLSDPLY-01732', key)
229-
_logger.throwing(ex, class_name=_class_name, method_name=method_name)
230-
raise ex
231-
else:
232-
_logger.info('WLSDPLY-01732', key, class_name=_class_name, method_name=method_name)
233-
continue
234+
_report_token_issue('WLSDPLY-01732', method_name, model_context, key)
235+
continue
234236

235237
value = variables[key]
236238
text = text.replace(token, value)
@@ -240,18 +242,24 @@ def _substitute(text, variables, model_context):
240242
for token, key in matches:
241243
# log, or throw an exception if key is not found.
242244
if not os.environ.has_key(key):
243-
if model_context.get_validation_method() == 'strict':
244-
_logger.severe('WLSDPLY-01737', key, class_name=_class_name, method_name=method_name)
245-
ex = exception_helper.create_variable_exception('WLSDPLY-01737', key)
246-
_logger.throwing(ex, class_name=_class_name, method_name=method_name)
247-
raise ex
248-
else:
249-
_logger.info('WLSDPLY-01737', key, class_name=_class_name, method_name=method_name)
250-
continue
245+
_report_token_issue('WLSDPLY-01737', method_name, model_context, key)
246+
continue
251247

252248
value = os.environ.get(key)
253249
text = text.replace(token, value)
254250

251+
# check secret variables before @@FILE:/dir/@@SECRET:name:key@@.txt@@
252+
matches = _secret_pattern.findall(text)
253+
for token, name, key in matches:
254+
value = _resolve_secret_token(name, key, model_context)
255+
if value is None:
256+
secret_token = name + ':' + key
257+
known_tokens = _list_known_secret_tokens()
258+
_report_token_issue('WLSDPLY-01739', method_name, model_context, secret_token, known_tokens)
259+
continue
260+
261+
text = text.replace(token, value)
262+
255263
tokens = _file_variable_pattern.findall(text)
256264
if tokens:
257265
for token in tokens:
@@ -285,16 +293,8 @@ def _read_value_from_file(file_path, model_context):
285293
line = file_reader.readLine()
286294
file_reader.close()
287295
except IOException, e:
288-
if model_context.get_validation_method() == 'strict':
289-
_logger.severe('WLSDPLY-01733', file_path, e.getLocalizedMessage(), class_name=_class_name,
290-
method_name=method_name)
291-
ex = exception_helper.create_variable_exception('WLSDPLY-01733', file_path, e.getLocalizedMessage(), error=e)
292-
_logger.throwing(ex, class_name=_class_name, method_name=method_name)
293-
raise ex
294-
else:
295-
_logger.info('WLSDPLY-01733', file_path, e.getLocalizedMessage(), error=e, class_name=_class_name,
296-
method_name=method_name)
297-
line = ''
296+
_report_token_issue('WLSDPLY-01733', method_name, model_context, file_path, e.getLocalizedMessage())
297+
line = ''
298298

299299
if line is None:
300300
ex = exception_helper.create_variable_exception('WLSDPLY-01734', file_path)
@@ -304,6 +304,141 @@ def _read_value_from_file(file_path, model_context):
304304
return str(line).strip()
305305

306306

307+
def _resolve_secret_token(name, key, model_context):
308+
"""
309+
Return the value associated with the specified secret name and key.
310+
If the name and key are found in the directory map, return the associated value.
311+
:param name: the name of the secret (a directory name or mapped name)
312+
:param key: the name of the file containing the secret
313+
:param model_context: used to determine the validation method (strict, lax, etc.)
314+
:return: the secret value, or None if it is not found
315+
"""
316+
method_name = '_resolve_secret_token'
317+
global _secret_token_map
318+
319+
if _secret_token_map is None:
320+
_init_secret_token_map(model_context)
321+
322+
secret_token = name + ':' + key
323+
return dictionary_utils.get_element(_secret_token_map, secret_token)
324+
325+
326+
def _init_secret_token_map(model_context):
327+
"""
328+
Initialize a global map of name/value tokens to secret values.
329+
The map includes secrets found below the directories specified in WDT_MODEL_SECRETS_DIRS,
330+
and in WDT_MODEL_SECRETS_NAME_DIR_PAIRS assignments.
331+
:param model_context: used to determine the validation method (strict, lax, etc.)
332+
"""
333+
method_name = '_init_secret_token_map'
334+
global _secret_token_map
335+
336+
log_method = _logger.info
337+
if model_context.get_validation_method() == 'strict':
338+
log_method = _logger.warning
339+
340+
_secret_token_map = dict()
341+
342+
# add name/key pairs for files in sub-directories of directories in WDT_MODEL_SECRETS_DIRS.
343+
344+
locations = os.environ.get(_secret_dirs_variable, _secret_dirs_default)
345+
for dir in locations.split(","):
346+
if not os.path.isdir(dir):
347+
# log at WARN or INFO, but no exception is thrown
348+
log_method('WLSDPLY-01738', _secret_dirs_variable, dir, class_name=_class_name, method_name=method_name)
349+
continue
350+
351+
for subdir_name in os.listdir(dir):
352+
subdir_path = os.path.join(dir, subdir_name)
353+
if os.path.isdir(subdir_path):
354+
_add_file_secrets_to_map(subdir_path, subdir_name, model_context)
355+
356+
# add name/key pairs for files in directories assigned in WDT_MODEL_SECRETS_NAME_DIR_PAIRS.
357+
# these pairs will override if they were previously added as sub-directory pairs.
358+
359+
dir_pairs_text = os.environ.get(_secret_dir_pairs_variable, None)
360+
if dir_pairs_text is not None:
361+
dir_pairs = dir_pairs_text.split(',')
362+
for dir_pair in dir_pairs:
363+
result = dir_pair.split('=')
364+
if len(result) != 2:
365+
log_method('WLSDPLY-01735', _secret_dir_pairs_variable, dir_pair, class_name=_class_name,
366+
method_name=method_name)
367+
continue
368+
369+
dir = result[1]
370+
if not os.path.isdir(dir):
371+
log_method('WLSDPLY-01738', _secret_dir_pairs_variable, dir, class_name=_class_name,
372+
method_name=method_name)
373+
continue
374+
375+
name = result[0]
376+
_add_file_secrets_to_map(dir, name, model_context)
377+
378+
379+
def _clear_secret_token_map():
380+
"""
381+
Used by unit tests to force reload of map.
382+
"""
383+
global _secret_token_map
384+
_secret_token_map = None
385+
386+
387+
def _add_file_secrets_to_map(dir, name, model_context):
388+
"""
389+
Add the secret from each file in the specified directory to the map.
390+
:param dir: the directory to be examined
391+
:param name: the name to be used in the map token
392+
:param model_context: used to determine the validation method (strict, lax, etc.)
393+
"""
394+
global _secret_token_map
395+
396+
for file_name in os.listdir(dir):
397+
file_path = os.path.join(dir, file_name)
398+
if os.path.isfile(file_path):
399+
token = name + ":" + file_name
400+
_secret_token_map[token] = _read_value_from_file(file_path, model_context)
401+
402+
403+
def _list_known_secret_tokens():
404+
"""
405+
Returns a string representation of the available secret name/path tokens.
406+
"""
407+
global _secret_token_map
408+
409+
keys = list(_secret_token_map.keys())
410+
keys.sort()
411+
412+
ret = ''
413+
for key in keys:
414+
if ret != '':
415+
ret += ', '
416+
ret += "'" + key + "'"
417+
return ret
418+
419+
420+
def _report_token_issue(message_key, method_name, model_context, *args):
421+
"""
422+
Log a message at the level corresponding to the validation method (SEVERE for strict, INFO otherwise).
423+
Throw a variable exception if the level is strict.
424+
The lax validation method can be used to verify the model without resolving tokens.
425+
:param message_key: the message key to be logged and used for exceptions
426+
:param method_name: the name of the calling method for logging
427+
:param model_context: used to determine the validation method
428+
:param args: arguments for use in the message
429+
"""
430+
log_method = _logger.info
431+
if model_context.get_validation_method() == 'strict':
432+
log_method = _logger.severe
433+
434+
log_method(message_key, class_name=_class_name, method_name=method_name, *args)
435+
436+
if model_context.get_validation_method() == 'strict':
437+
ex = exception_helper.create_variable_exception(message_key, *args)
438+
_logger.throwing(ex, class_name=_class_name, method_name=method_name)
439+
raise ex
440+
441+
307442
def substitute_key(text, variables):
308443
"""
309444
Substitute any @@PROP values in the text and return.

core/src/main/resources/oracle/weblogic/deploy/messages/wlsdeploy_rb.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,9 +291,11 @@ WLSDPLY-01731=Variables updated by {0}
291291
WLSDPLY-01732=Variable {0} is not found in properties file
292292
WLSDPLY-01733=Variable file {0} cannot be read: {1}
293293
WLSDPLY-01734=No value in variable file {0}
294-
WLSDPLY-01735=Variable substitution for {0} is deprecated, use @@PROP:{1}@@
294+
WLSDPLY-01735=Assignment in {0} is not formatted as X=Y: {1}
295295
WLSDPLY-01736=Default variable file name {0}
296296
WLSDPLY-01737=Environment variable "{0}" was not found
297+
WLSDPLY-01738=Path in {0} is not a directory: {1}
298+
WLSDPLY-01739=Error: Could not resolve secret token "{0}". Known secrets include: {1}
297299

298300
# wlsdeploy/util/weblogic_helper.py
299301
WLSDPLY-01740=Encryption failed: Unable to locate SerializedSystemIni

core/src/test/python/variables_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,39 @@ def testEnvironmentVariableNotFound(self):
111111
else:
112112
self.fail('Test must raise VariableException when variable file is not found')
113113

114+
def testSecretToken(self):
115+
"""
116+
Verify that the WDT_MODEL_SECRETS_DIRS variable can be used to find a secret.
117+
Put two paths in the variable, the second is .../resources/secrets.
118+
It should find the file .../resources/secrets/my-secrets/secret2, containing "mySecret2".
119+
"""
120+
os.environ['WDT_MODEL_SECRETS_DIRS'] = "/noDir/noSubdir," + self._resources_dir + "/secrets"
121+
model = {'domainInfo': {'AdminUserName': '@@SECRET:my-secrets:secret2@@'}}
122+
variables._clear_secret_token_map()
123+
variables.substitute(model, {}, self.model_context)
124+
self.assertEqual(model['domainInfo']['AdminUserName'], 'mySecret2')
125+
126+
def testSecretTokenPairs(self):
127+
"""
128+
Verify that the WDT_MODEL_SECRETS_NAME_DIR_PAIRS variable can be used to find a secret.
129+
Put two path assignments in the variable, the second is dirY=.../resources/secrets.
130+
It should find the file .../resources/secrets/secret1, containing "mySecret1".
131+
"""
132+
os.environ['WDT_MODEL_SECRETS_NAME_DIR_PAIRS'] = "dirX=/noDir,dirY=" + self._resources_dir + "/secrets"
133+
model = {'domainInfo': {'AdminUserName': '@@SECRET:dirY:secret1@@'}}
134+
variables._clear_secret_token_map()
135+
variables.substitute(model, {}, self.model_context)
136+
self.assertEqual(model['domainInfo']['AdminUserName'], 'mySecret1')
137+
138+
def testSecretTokenNotFound(self):
139+
try:
140+
model = {'domainInfo': {'AdminUserName': '@@SECRET:noName:noKey@@'}}
141+
variables.substitute(model, {}, self.model_context)
142+
except VariableException:
143+
pass
144+
else:
145+
self.fail('Test must raise VariableException when secret token is not found')
146+
114147

115148
if __name__ == '__main__':
116149
unittest.main()
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mySecret2
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mySecret1

0 commit comments

Comments
 (0)