Skip to content

Commit

Permalink
🔒 initial namespace allowlist implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
mshade committed Sep 12, 2023
1 parent 99de2e7 commit e588694
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 53 deletions.
42 changes: 24 additions & 18 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,27 @@

app = Flask(__name__, static_url_path="", static_folder="static")


# A namespace filter decorator
def namespace_filter(func):
@wraps(func)
def wrapper(namespace, *args, **kwargs):
if config.ALLOW_NAMESPACES:
if namespace in config.ALLOW_NAMESPACES.split(','):
if namespace in config.ALLOW_NAMESPACES.split(","):
return func(namespace, *args, **kwargs)
else:
return func(namespace, *args, **kwargs)

data = {
"error": f"Request to {namespace} denied due to KRONIC_ALLOW_NAMESPACES setting",
"namespace": namespace,
"allowed_namespaces": config.ALLOW_NAMESPACES
"allowed_namespaces": config.ALLOW_NAMESPACES,
}
if request.headers.get("content-type", None)== "application/json":
if request.headers.get("content-type", None) == "application/json":
return data, 403
else:
return render_template("denied.html", data=data)

return wrapper


Expand All @@ -53,6 +55,7 @@ def _strip_immutable_fields(spec):
def healthz():
return {"status": "ok"}


@app.route("/")
@app.route("/namespaces/")
def index():
Expand All @@ -64,6 +67,7 @@ def index():

return render_template("index.html", namespaces=namespaces)


@app.route("/namespaces/<namespace>")
@namespace_filter
def view_namespace(namespace):
Expand All @@ -82,6 +86,7 @@ def view_namespace(namespace):
"namespace.html", cronjobs=cronjobs_with_details, namespace=namespace
)


@app.route("/namespaces/<namespace>/cronjobs/<cronjob_name>", methods=["GET", "POST"])
@namespace_filter
def view_cronjob(namespace, cronjob_name):
Expand All @@ -102,30 +107,31 @@ def view_cronjob(namespace, cronjob_name):
cronjob = {
"apiVersion": "batch/v1",
"kind": "CronJob",
"metadata": {
"name": cronjob_name,
"namespace": namespace
},
"metadata": {"name": cronjob_name, "namespace": namespace},
"spec": {
"schedule": "*/10 * * * *",
"jobTemplate": {
"spec": {
"template": {
"spec": {
"containers": [{
"name": "example",
"image": "busybox:latest",
"imagePullPolicy": "IfNotPresent",
"command": [
"/bin/sh", "-c", "echo hello; date"
]
}],
"restartPolicy": "OnFailure"
"containers": [
{
"name": "example",
"image": "busybox:latest",
"imagePullPolicy": "IfNotPresent",
"command": [
"/bin/sh",
"-c",
"echo hello; date",
],
}
],
"restartPolicy": "OnFailure",
}
}
}
}
}
},
},
}

cronjob_yaml = yaml.dump(cronjob)
Expand Down
71 changes: 44 additions & 27 deletions kron.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,20 @@
generic = client.ApiClient()


def _filter_namespaces(item_list: List[dict]) -> List[dict]:
cleaned_items = [_clean_api_object(item) for item in item_list]
return [item for item in cleaned_items if item["metadata"]["namespace"] in config.ALLOW_NAMESPACES or not config.ALLOW_NAMESPACES]
def namespace_filter(func):
def wrapper(namespace: str = None, *args, **kwargs):
if config.ALLOW_NAMESPACES and namespace:
if namespace in config.ALLOW_NAMESPACES.split(","):
return func(namespace, *args, **kwargs)
else:
return func(namespace, *args, **kwargs)

return False

return wrapper


def _filter_object_fields(
response: object, fields: List[str] = ["name"]
) -> List[object]:
def _filter_dict_fields(items: List[dict], fields: List[str] = ["name"]) -> List[dict]:
"""
Filter a given list of API object down to only the metadata fields listed.
Expand All @@ -45,21 +51,8 @@ def _filter_object_fields(
provided.
"""
# if isinstance(response, list):
# return [
# {field: item.get("metadata", None).get(field, None) for field in fields}
# for item in response
# ]
# else:
return [
{field: getattr(item.metadata, field) for field in fields}
for item in response.items
]

def _filter_dict_fields(items, fields=["name"]):
return [
{field: item.get("metadata").get(field) for field in fields}
for item in items
{field: item.get("metadata").get(field) for field in fields} for item in items
]


Expand Down Expand Up @@ -134,7 +127,8 @@ def _is_owned_by(object: object, owner_name: str) -> bool:
return any(owner_ref["name"] == owner_name for owner_ref in owner_refernces)


def get_cronjobs(namespace: str = "") -> List[dict]:
@namespace_filter
def get_cronjobs(namespace: str = None) -> List[dict]:
"""Get names of cronjobs in a given namespace. If namespace is not provided, return CronJobs
from all namespaces.
Expand All @@ -146,22 +140,35 @@ def get_cronjobs(namespace: str = "") -> List[dict]:
"""
try:
cronjobs = []
if namespace == "":
if not namespace:
if not config.ALLOW_NAMESPACES:
cronjobs = [_clean_api_object(item) for item in batch.list_cron_job_for_all_namespaces().items]
cronjobs = [
_clean_api_object(item)
for item in batch.list_cron_job_for_all_namespaces().items
]
else:
cronjobs = []
for allowed in config.ALLOW_NAMESPACES.split(','):
cronjobs.extend([_clean_api_object(item) for item in batch.list_namespaced_cron_job(namespace=allowed).items])
for allowed in config.ALLOW_NAMESPACES.split(","):
cronjobs.extend(
[
_clean_api_object(item)
for item in batch.list_namespaced_cron_job(
namespace=allowed
).items
]
)
else:
cronjobs = [_clean_api_object(item) for item in batch.list_namespaced_cron_job(namespace=namespace).items]
cronjobs = [
_clean_api_object(item)
for item in batch.list_namespaced_cron_job(namespace=namespace).items
]

fields = ["name", "namespace"]
sorted_cronjobs = sorted(
_filter_dict_fields(cronjobs, fields), key=lambda x: x["name"]
)
return sorted_cronjobs

except ApiException as e:
log.error(e)
response = {
Expand All @@ -175,6 +182,7 @@ def get_cronjobs(namespace: str = "") -> List[dict]:
return response


@namespace_filter
def get_cronjob(namespace: str, cronjob_name: str) -> dict:
"""Get the details of a given CronJob as a dict
Expand All @@ -192,6 +200,7 @@ def get_cronjob(namespace: str, cronjob_name: str) -> dict:
return False


@namespace_filter
def get_jobs(namespace: str, cronjob_name: str) -> List[dict]:
"""Return jobs belonging to a given CronJob name
Expand Down Expand Up @@ -231,6 +240,7 @@ def get_jobs(namespace: str, cronjob_name: str) -> List[dict]:
return response


@namespace_filter
def get_pods(namespace: str, job_name: str = None) -> List[dict]:
"""Return pods related to jobs in a namespace
Expand Down Expand Up @@ -266,6 +276,7 @@ def get_pods(namespace: str, job_name: str = None) -> List[dict]:
return response


@namespace_filter
def get_jobs_and_pods(namespace: str, cronjob_name: str) -> List[dict]:
"""Get jobs and their pods under a `pods` element for display purposes
Expand All @@ -283,6 +294,7 @@ def get_jobs_and_pods(namespace: str, cronjob_name: str) -> List[dict]:
return jobs


@namespace_filter
def get_pod_logs(namespace: str, pod_name: str) -> str:
"""Return plain text logs for <pod_name> in <namespace>"""
try:
Expand All @@ -296,6 +308,7 @@ def get_pod_logs(namespace: str, pod_name: str) -> str:
return f"Kronic> Error fetching logs: {e.reason}"


@namespace_filter
def trigger_cronjob(namespace: str, cronjob_name: str) -> dict:
try:
# Retrieve the CronJob template
Expand Down Expand Up @@ -332,6 +345,7 @@ def trigger_cronjob(namespace: str, cronjob_name: str) -> dict:
return response


@namespace_filter
def toggle_cronjob_suspend(namespace: str, cronjob_name: str) -> dict:
"""Toggle a CronJob's suspend flag on or off
Expand Down Expand Up @@ -365,6 +379,7 @@ def toggle_cronjob_suspend(namespace: str, cronjob_name: str) -> dict:
return response


@namespace_filter
def update_cronjob(namespace: str, spec: str) -> dict:
"""Update/edit a CronJob configuration via patch
Expand Down Expand Up @@ -396,6 +411,7 @@ def update_cronjob(namespace: str, spec: str) -> dict:
return response


@namespace_filter
def delete_cronjob(namespace: str, cronjob_name: str) -> dict:
"""Delete a CronJob
Expand Down Expand Up @@ -423,6 +439,7 @@ def delete_cronjob(namespace: str, cronjob_name: str) -> dict:
return response


@namespace_filter
def delete_job(namespace: str, job_name: str) -> dict:
"""Delete a Job
Expand Down
13 changes: 5 additions & 8 deletions tests/test_kron.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,20 @@ def test_get_human_readable_time_difference_invalid_format():
kron._get_time_since("invalid_timestamp")


def test_filter_object_fields(cronjob_list):
def test_filter_dict_fields():
cron_dict_list = [
{"metadata": {"name": "first", "namespace": "test"}},
{"metadata": {"name": "second", "namespace": "test"}},
]

assert kron._filter_object_fields(cronjob_list) == [
assert kron._filter_dict_fields(cron_dict_list) == [
{"name": "first"},
{"name": "second"},
{"name": "third"},
{"name": "fourth"},
{"name": "fifth"},
]
assert kron._filter_object_fields(cron_dict_list) == [

assert kron._filter_dict_fields(cron_dict_list) == [
{"name": "first"},
{"name": "second"}
{"name": "second"},
]


Expand Down

0 comments on commit e588694

Please sign in to comment.