Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added more automatic generator functions (#694) #866

Merged
merged 11 commits into from
Nov 10, 2022
7 changes: 6 additions & 1 deletion docs/secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,12 @@ When referencing your secret in the inventory during compile, you can use the fo
- `ed25519` - Generates a ed25519 private key (PKCS#8).
- `publickey` - Derives the public key from a revealed private key i.e. `||reveal:path/to/encrypted_private_key|publickey`
- `rsapublic` - Derives an RSA public key from a revealed private key i.e. `||reveal:path/to/encrypted_private_key|rsapublic` (deprecated, use `publickey` instead)
- `loweralphanum` - Generates a DNS-compliant text string (a-z and 0-9), containing lower alphanum chars `||`
- `randomint` - Generates a random number. You can optionally pass the length you want i.e `||randomint:64`
- `loweralphanum` - Generates a DNS-compliant text string (a-z and 0-9), containing lower alphanum chars i.e. `||loweralphanum:32`
- `upperalphanum` - Generates a text string (A-Z and 0-9) containing uppercase letters and numbers i.e. `||upperalphanum:32`
- `loweralpha` - Generates a text string (a-z) containing lowercase letters i.e. `||loweralpha:32`
- `upperalpha` - Generates a text string (A-Z) containing uppercase letters i.e. `||upperalpha:32`
- `alphanumspec` - Generates a text string containing alphanumeric and given special characters i.e. `||alphanumspec:32:!$%&/(){}[]`. default is `string.punctuation`. Note: you can't pass `|` or `:` manually

*Note*: The first operator here `||` is more similar to a logical OR. If the secret file doesn't exist, kapitan will generate it and apply the functions after the `||`. If the secret file already exists, no functions will run.

Expand Down
20 changes: 16 additions & 4 deletions kapitan/refs/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import yaml

from kapitan.errors import RefBackendError, RefError, RefFromFuncError, RefHashMismatchError
from kapitan.refs.functions import eval_func
from kapitan.refs.functions import eval_func, get_func_lookup
from kapitan.utils import PrettyDumper, list_all_paths

try:
Expand All @@ -31,7 +31,7 @@

# e.g. ?{ref:my/secret/token} or ?{ref:my/secret/token||func:param1:param2}
# e.g ?{ref:basepayloadhere==:embedded} (for embedded refs)
REF_TOKEN_TAG_PATTERN = r"(\?{([\w\:\.\-\/@=]+)([\|\|\w\:\.\-\/]+)?=*})"
REF_TOKEN_TAG_PATTERN = r"(\?{(\w+:[\w\-\.\@\=\/\:]+)(\|(?:(?:\|\w+)(?::\S*)*)+)?\=*})"
REF_TOKEN_SUBVAR_PATTERN = r"(@[\w\.\-\_]+)"


Expand Down Expand Up @@ -480,7 +480,10 @@ def tag_params(self, tag):
tag, token, func_str = match.groups()
return tag, token, func_str
else:
raise RefError("{}: is not a valid tag".format(tag))
raise RefError(
"{}: is not a valid tag".format(tag),
"\ntry something like: ?{ref:path/to/secret||function:param1:param2}",
)

def token_type(self, token):
"returns ref type for token"
Expand Down Expand Up @@ -581,18 +584,27 @@ def _eval_func_str(self, ctx, func_str):
evals and updates context ctx for func_str
returns evaluated ctx
"""
# parse functions
assert func_str.startswith("||")
funcs = func_str[2:].split("|")

for func in funcs:
# parse parameters for function
func_name, *func_params = func.strip().split(":")
if func_name == "base64": # not a real function
ctx.encode_base64 = True
else:
try:
# call function with parameters and set generated secret to ctx.data
eval_func(func_name, ctx, *func_params)
except KeyError:
raise RefError(f"{func_name}: unknown ref function used.")
raise RefError(
"{}: unknown ref function used. Choose one of: {}".format(
func_name, [key for key in get_func_lookup()]
)
)
except TypeError:
raise RefError("{}: too many arguments for function {}".format(func_params, func_name))

return ctx

Expand Down
84 changes: 74 additions & 10 deletions kapitan/refs/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,30 @@


def eval_func(func_name, ctx, *func_params):
"""calls specific function which generates the secret"""
func_lookup = get_func_lookup()

func_lookup = {
return func_lookup[func_name](ctx, *func_params)


def get_func_lookup():
"""returns the lookup-table for the generator functions"""
return {
"randomstr": randomstr,
"sha256": sha256,
"ed25519": ed25519_private_key,
"rsa": rsa_private_key,
"rsapublic": rsa_public_key,
"publickey": public_key,
"reveal": reveal,
"loweralphanum": loweralphanum,
"randomint": random_int,
"loweralpha": lower_alpha,
"loweralphanum": lower_alpha_num,
"upperalpha": upper_alpha,
"upperalphanum": upper_alpha_num,
"alphanumspec": alphanumspec,
}

return func_lookup[func_name](ctx, *func_params)


def randomstr(ctx, nbytes=""):
"""
Expand Down Expand Up @@ -144,13 +154,67 @@ def reveal(ctx, secret_path):
)


def loweralphanum(ctx, chars="8"):
def random_int(ctx, ndigits="16"):
"""generates a number, containing ndigits random digits"""
pool = string.digits
generic_alphanum(ctx, ndigits, pool)


def lower_alpha(ctx, nchars="8"):
"""generator function for lowercase letters (a-z)"""
pool = string.ascii_lowercase
generic_alphanum(ctx, nchars, pool)


def lower_alpha_num(ctx, nchars="8"):
"""generator function for lowercase letters and numbers (a-z and 0-9)"""
pool = string.ascii_lowercase + string.digits
generic_alphanum(ctx, nchars, pool)


def upper_alpha(ctx, nchars="8"):
"""generator function for uppercase letters (A-Z)"""
pool = string.ascii_uppercase
generic_alphanum(ctx, nchars, pool)


def upper_alpha_num(ctx, nchars="8"):
"""generator function for uppercase letters and numbers (A-Z and 0-9)"""
pool = string.ascii_uppercase + string.digits
generic_alphanum(ctx, nchars, pool)


def alphanumspec(ctx, nchars="8", special_chars=string.punctuation):
"""
generates a DNS-compliant text string (a-z and 0-9), containing lower alphanum chars
generator function for alphanumeric characters and given special characters
usage: ?{base64:path/to/secret||alphanumspec:32:#./&}
default is !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~
NOTE: '|' and ':' are used for arg parsing and aren't allowed manually, in default they work
"""
pool = string.ascii_lowercase + string.digits
# make sure that each allowed character is include only once or not at all
special_chars = "".join(set(special_chars).intersection(string.punctuation))
pool = string.ascii_letters + string.digits + special_chars
generic_alphanum(ctx, nchars, pool)


def generic_alphanum(ctx, nchars, pool):
"""
generates a text string, containing nchars from pool
default for nchars is 8 chars
sets it to ctx.data
"""
# check input
try:
chars = int(chars)
nchars = int(nchars)
except ValueError:
raise RefError(f"Ref error: eval_func: {chars} cannot be converted into integer.")
ctx.data = "".join(secrets.choice(pool) for i in range(chars))
raise RefError(f"Ref error: eval_func: {nchars} cannot be converted into integer.")

allowed_pool = string.ascii_letters + string.digits + string.punctuation
if not set(pool).issubset(allowed_pool):
raise RefError("{}: invalid elements in pool".format(set(pool).difference(set(allowed_pool))))

# generate string based on given pool
generated_str = "".join(secrets.choice(pool) for i in range(nchars))

# set ctx.data to generated string
ctx.data = generated_str
111 changes: 111 additions & 0 deletions tests/test_refs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import base64
import os
import string
import tempfile
import unittest

Expand Down Expand Up @@ -440,3 +441,113 @@ def test_ref_function_loweralphanum(self):
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16)

def test_ref_function_upperalphanum(self):
"write upperalphanum to secret, confirm ref file exists, reveal and check"

tag = "?{plain:ref/upperalphanum||upperalphanum}"
REF_CONTROLLER[tag] = RefParams()
self.assertTrue(os.path.isfile(os.path.join(REFS_HOME, "ref/upperalphanum")))

file_with_tags = tempfile.mktemp()
with open(file_with_tags, "w") as fp:
fp.write("?{plain:ref/upperalphanum}")
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 8) # default length of upperalphanum string is 8

# Test with parameter nchars=16, correlating with string length 16
tag = "?{plain:ref/upperalphanum||upperalphanum:16}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16)

def test_ref_function_loweralpha(self):
"write loweralpha to secret, confirm ref file exists, reveal and check"

tag = "?{plain:ref/loweralpha||loweralpha}"
REF_CONTROLLER[tag] = RefParams()
self.assertTrue(os.path.isfile(os.path.join(REFS_HOME, "ref/loweralpha")))

file_with_tags = tempfile.mktemp()
with open(file_with_tags, "w") as fp:
fp.write("?{plain:ref/loweralpha}")
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 8) # default length of loweralpha string is 8

# Test with parameter nchars=16, correlating with string length 16
tag = "?{plain:ref/loweralpha||loweralpha:16}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16)

def test_ref_function_upperalpha(self):
"write upperalpha to secret, confirm ref file exists, reveal and check"

tag = "?{plain:ref/upperalpha||upperalpha}"
REF_CONTROLLER[tag] = RefParams()
self.assertTrue(os.path.isfile(os.path.join(REFS_HOME, "ref/upperalpha")))

file_with_tags = tempfile.mktemp()
with open(file_with_tags, "w") as fp:
fp.write("?{plain:ref/upperalpha}")
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 8) # default length of upperalpha string is 8

# Test with parameter nchars=16, correlating with string length 16
tag = "?{plain:ref/upperalpha||upperalpha:16}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16)

def test_ref_function_randomint(self):
"write randomint to secret, confirm ref file exists, reveal and check"

tag = "?{plain:ref/randomint||randomint}"
REF_CONTROLLER[tag] = RefParams()
self.assertTrue(os.path.isfile(os.path.join(REFS_HOME, "ref/randomint")))

file_with_tags = tempfile.mktemp()
with open(file_with_tags, "w") as fp:
fp.write("?{plain:ref/randomint}")
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16) # default length of randomint string is 16

# Test with parameter nchars=32, correlating with string length 32
tag = "?{plain:ref/randomint||randomint:32}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 32)

def test_ref_function_alphanumspec(self):
"write alphanumspec to secret, confirm ref file exists, reveal and check"
tag = "?{plain:ref/alphanumspec||alphanumspec}"
REF_CONTROLLER[tag] = RefParams()
self.assertTrue(os.path.isfile(os.path.join(REFS_HOME, "ref/alphanumspec")))

file_with_tags = tempfile.mktemp()
with open(file_with_tags, "w") as fp:
fp.write("?{plain:ref/alphanumspec}")
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 8) # default length of alphanumspec string is 8

# Test with parameter nchars=16, correlating with string length 16
tag = "?{plain:ref/alphanumspec||alphanumspec:16}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 16)

# Test with every allowed special char, nchars=32
allowed_special_chars = set(string.punctuation).difference(":|")
for special_char in allowed_special_chars:
tag = "?{plain:ref/alphanumspec||alphanumspec:32:" + special_char + "}"
REF_CONTROLLER[tag] = RefParams()
REVEALER._reveal_tag_without_subvar.cache_clear()
revealed = REVEALER.reveal_raw_file(file_with_tags)
self.assertEqual(len(revealed), 32)
intersection = set(string.punctuation).intersection(revealed)
self.assertTrue(intersection.issubset(set(special_char)))