diff --git a/connaisseur/alert.py b/connaisseur/alert.py index a8fdef4bf..1a7def4af 100644 --- a/connaisseur/alert.py +++ b/connaisseur/alert.py @@ -48,6 +48,169 @@ def alerting_required(self, event_category: str) -> bool: return bool(self.config.get(event_category)) +class AlertReceiverAuthentication: + """ + Class to store authentication information for securely sending events to the alert receiver. + """ + + authentication_config: dict = None + authentication_scheme: str = None + + class AlertReceiverAuthenticationInterface: + def __init__(self, alert_receiver_config: dict, authentication_config_key: str): + if authentication_config_key is not None: + try: + self.authentication_config = alert_receiver_config[ + authentication_config_key + ] + except KeyError as err: + raise ConfigurationError( + "No authentication configuration found for dictionary key:" + f"{authentication_config_key}." + ) from err + + self.authentication_scheme = self.authentication_config.get( + "authentication_scheme", self.authentication_scheme + ) + self._validate_authentication_scheme() + + def _validate_authentication_scheme(self) -> None: + if not self.authentication_scheme: + raise ConfigurationError( + "The authentication scheme cannot be null or empty." + ) + + if not self.authentication_scheme.isalpha(): + raise ConfigurationError( + "The authentication scheme must contain only letters." + ) + + def get_header(self) -> dict: + return {} + + class AlertReceiverNoneAuthentication(AlertReceiverAuthenticationInterface): + """ + Placeholder class for AlertReceiver without authentication. + """ + + def __init__(self, alert_receiver_config: dict): + super().__init__(alert_receiver_config, None) + + class AlertReceiverBasicAuthentication(AlertReceiverAuthenticationInterface): + """ + Class to store authentication information for basic authentication type + with username and password. + """ + + username: str + password: str + authentication_scheme: str = "Basic" + + def __init__(self, alert_receiver_config: dict): + super().__init__(alert_receiver_config, "receiver_authentication_basic") + + try: + username_env = self.authentication_config["username_env"] + password_env = self.authentication_config["password_env"] + except KeyError as err: + raise ConfigurationError( + "No username_env or password_env configuration found." + ) from err + + try: + self.username = os.environ[username_env] + self.password = os.environ[password_env] + except KeyError as err: + raise ConfigurationError( + "No username or password found from environmental variables " + f"{username_env} and {password_env}." + ) from err + + def get_header(self) -> dict: + return { + "Authorization": f"{self.authentication_scheme} {self.username}:{self.password}" + } + + class AlertReceiverBearerAuthentication(AlertReceiverAuthenticationInterface): + """ + Class to store authentication information for bearer authentication type which uses a token. + """ + + token: str + authentication_scheme: str = "Bearer" # default is bearer + + def __init__(self, alert_receiver_config: dict): + super().__init__(alert_receiver_config, "receiver_authentication_bearer") + + token_env = self.authentication_config.get("token_env") + token_file = self.authentication_config.get("token_file") + + if ( + token_env is None and token_file is None + ): # This should not happen since it is included in the json validation + raise ConfigurationError( + "No token_env and token_file configuration found." + ) + + if ( + token_env is not None and token_file is not None + ): # This should not happen since it is included in the json validation + raise ConfigurationError( + "Both token_env and token_file configuration found. Only one can be given." + ) + + if token_env is not None: + try: + self.token = os.environ[token_env] + except KeyError as err: + raise ConfigurationError( + f"No token found from environmental variable {token_env}." + ) from err + else: + try: + with open(token_file, "r", encoding="utf-8") as token_file_handler: + self.token = token_file_handler.read() + except FileNotFoundError as err: + raise ConfigurationError( + f"No token file found at {token_file}." + ) from err + except Exception as err: + raise ConfigurationError( + f"An error occurred while loading the token file {token_file}: {str(err)}" + ) from err + + def get_header(self) -> dict: + return {"Authorization": f"{self.authentication_scheme} {self.token}"} + + init_map = { + "basic": AlertReceiverBasicAuthentication, + "bearer": AlertReceiverBearerAuthentication, + "none": AlertReceiverNoneAuthentication, + } + + _authentication_instance = None + + def __init__(self, alert_receiver_config: dict): + self.authentication_type = alert_receiver_config.get( + "receiver_authentication_type", "none" + ) + self.__init_authentication_instance(alert_receiver_config) + + def __init_authentication_instance(self, alert_receiver_config: dict): + try: + self._authentication_instance = self.init_map[self.authentication_type]( + alert_receiver_config + ) + except KeyError as err: + raise ConfigurationError( + "No authentication type found. Valid values are " + f"{list(AlertReceiverAuthentication.init_map.keys())}" + ) from err # hopefully this never happens + + def get_auth_header(self) -> dict: + return self._authentication_instance.get_header() + + class Alert: """ Class to store image information about an alert as attributes and a sending @@ -59,6 +222,7 @@ class Alert: template: str receiver_url: str + receiver_authentication: AlertReceiverAuthentication payload: str headers: dict @@ -101,12 +265,13 @@ def __init__( "images": images, } self.receiver_url = receiver_config["receiver_url"] + self.receiver_authentication = AlertReceiverAuthentication(receiver_config) self.template = receiver_config["template"] self.throw_if_alert_sending_fails = receiver_config.get( "fail_if_alert_sending_fails", False ) self.payload = self.__construct_payload(receiver_config) - self.headers = self.__get_headers(receiver_config) + self.headers = self.__get_headers(receiver_config, self.receiver_authentication) def __construct_payload(self, receiver_config: dict) -> str: try: @@ -153,13 +318,18 @@ def send_alert(self) -> Optional[requests.Response]: return response @staticmethod - def __get_headers(receiver_config): + def __get_headers( + receiver_config: dict, receiver_authentication: AlertReceiverAuthentication + ) -> dict: headers = {"Content-Type": "application/json"} additional_headers = receiver_config.get("custom_headers") if additional_headers is not None: for header in additional_headers: key, value = header.split(":", 1) headers.update({key.strip(): value.strip()}) + auth_header = receiver_authentication.get_auth_header() + if auth_header: # not None and not empty + headers.update(auth_header) return headers diff --git a/connaisseur/res/alertconfig_schema.json b/connaisseur/res/alertconfig_schema.json index 8a894b2b1..e13b50d2a 100644 --- a/connaisseur/res/alertconfig_schema.json +++ b/connaisseur/res/alertconfig_schema.json @@ -18,6 +18,50 @@ "receiver_url": { "type": "string" }, + "receiver_authentication_type": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer" + ] + }, + "receiver_authentication_basic": { + "type": "object", + "properties": { + "username_env": { + "type": "string" + }, + "password_env": { + "type": "string" + }, + "authentication_scheme": { + "type": "string" + } + }, + "required": [ + "username_env", + "password_env" + ] + }, + "receiver_authentication_bearer": { + "type": "object", + "oneOf": [ + {"required": ["token_file"]}, + {"required": ["token_env"]} + ], + "properties": { + "authentication_scheme": { + "type": "string" + }, + "token_file": { + "type": "string" + }, + "token_env": { + "type": "string" + } + } + }, "priority": { "type": "integer" }, @@ -35,6 +79,35 @@ "type": "boolean" } }, + "anyOf": [ + { + "properties": { + "receiver_authentication_type": { + "const": "basic" + } + }, + "required": [ + "receiver_authentication_basic" + ] + }, + { + "properties": { + "receiver_authentication_type": { + "const": "bearer" + } + }, + "required": [ + "receiver_authentication_bearer" + ] + }, + { + "properties": { + "receiver_authentication_type": { + "const": "none" + } + } + } + ], "required": [ "template", "receiver_url" @@ -60,6 +133,50 @@ "receiver_url": { "type": "string" }, + "receiver_authentication_type": { + "type": "string", + "enum": [ + "none", + "basic", + "bearer" + ] + }, + "receiver_authentication_basic": { + "type": "object", + "properties": { + "username_env": { + "type": "string" + }, + "password_env": { + "type": "string" + }, + "authentication_scheme": { + "type": "string" + } + }, + "required": [ + "username_env", + "password_env" + ] + }, + "receiver_authentication_bearer": { + "type": "object", + "oneOf": [ + {"required": ["token_file"]}, + {"required": ["token_env"]} + ], + "properties": { + "authentication_scheme": { + "type": "string" + }, + "token_file": { + "type": "string" + }, + "token_env": { + "type": "string" + } + } + }, "priority": { "type": "integer" }, @@ -77,6 +194,35 @@ "type": "boolean" } }, + "anyOf": [ + { + "properties": { + "receiver_authentication_type": { + "const": "basic" + } + }, + "required": [ + "receiver_authentication_basic" + ] + }, + { + "properties": { + "receiver_authentication_type": { + "const": "bearer" + } + }, + "required": [ + "receiver_authentication_bearer" + ] + }, + { + "properties": { + "receiver_authentication_type": { + "const": "none" + } + } + } + ], "required": [ "template", "receiver_url" diff --git a/docs/features/alerting.md b/docs/features/alerting.md index cb08b6748..0870049d5 100644 --- a/docs/features/alerting.md +++ b/docs/features/alerting.md @@ -16,15 +16,15 @@ alerts at the same time. Currently, Connaisseur supports alerting on either admittance of images, denial of images or both. These event categories can be configured independently of each other under the relevant category (i.e. `admit_request` or `reject_request`): -| Key | Accepted values | Default | Required | Description | -| -------------------------------------------------- | ---------------------------------------------------- | ----------------- | ------------------ | -------------------------------------------------------------------------------------------------- | -| `alerting.cluster_identifier` | string | `"not specified"` | | Cluster identifier used in alert payload to distinguish between alerts from different clusters. | -| `alerting..template` | `opsgenie`, `slack`, `keybase`, `ecs-1-12-0` or custom* | - | :heavy_check_mark: | File in `helm/alert_payload_templates/` to be used as alert payload template. | -| `alerting..receiver_url` | string | - | :heavy_check_mark: | URL of alert-receiving endpoint. | -| `alerting..priority` | int | `3` | | Priority of alert (to enable fitting Connaisseur alerts into alerts from other sources). | -| `alerting..custom_headers` | list[string] | - | | Additional headers required by alert-receiving endpoint. | -| `alerting..payload_fields` | subyaml | - | | Additional (`yaml`) key-value pairs to be appended to alert payload (as `json`). | -| `alerting..fail_if_alert_sending_fails` | bool | `False` | | Whether to make Connaisseur deny images if the corresponding alert cannot be successfully sent. | +| Key | Accepted values | Default | Required | Description | +| --------------------------------------------------- | ----------------------------------------------------- | ----------------- | ------------------ | --------------------------------------------------------------------------------------------------- | +| `alerting.cluster_identifier` | string | `"not specified"` | | Cluster identifier used in alert payload to distinguish between alerts from different clusters. | +| `alerting..template` | `opsgenie`, `slack`, `keybase`, `ecs-1-12-0` or custom* | - | :heavy_check_mark: | File in `helm/alert_payload_templates/` to be used as alert payload template. | +| `alerting..receiver_url` | string | - | :heavy_check_mark: | URL of alert-receiving endpoint. | +| `alerting..priority` | int | `3` | | Priority of alert (to enable fitting Connaisseur alerts into alerts from other sources). | +| `alerting..custom_headers` | list[string] | - | | Additional headers required by alert-receiving endpoint. | +| `alerting..payload_fields` | subyaml | - | | Additional (`yaml`) key-value pairs to be appended to alert payload (as `json`). | +| `alerting..fail_if_alert_sending_fails` | bool | `False` | | Whether to make Connaisseur deny images if the corresponding alert cannot be successfully sent. | *basename of the custom template file in `helm/alerting_payload_templates` without file extension @@ -44,7 +44,7 @@ one it needs to be one of `slack`, `keybase`, `opsgenie` or `ecs-1-12-0`. For example, if you would like to receive notifications in Keybase whenever Connaisseur admits a request to your cluster, your alerting configuration would look similar to the following snippet: -``` +```yaml alerting: admit_request: templates: @@ -52,6 +52,7 @@ alerting: receiver_url: https://bots.keybase.io/webhookbot/ ``` + ## Additional notes ### Creating a custom template @@ -76,3 +77,91 @@ specify them as described above in `custom_headers`. Feel free to make a PR to share with the community if you add new neat templates for other third parties :pray: + +### Webhook Authentication + + +#### Configuration options + +Currently, Connaisseur supports alerting on either admittance of images, denial of images or both. These event categories can be configured independently of each other under the relevant category (i.e. `admit_request` or `reject_request`): + +| Key | Accepted values | Default | Required | Description | +| --------------------------------------------------- | ----------------------------------------------------- | ----------------- | ------------------ | --------------------------------------------------------------------------------------------------- | +| `alerting..receiver_authentication_type` | string enum `basic`, `bearer`, `none` | `none` | | Authentication type of the alert-receiving webhook endpoint . | +| `alerting..receiver_authentication_basic` | object | - | only when `receiver_authentication_type` is `basic` | Authentication credentials for basic authentication. | +| `alerting..receiver_authentication_basic.username_env` | string | - | only when `receiver_authentication_type` is `basic` | Username Environmental variable for basic authentication. | +| `alerting..receiver_authentication_basic.password_env` | string | - | only when `receiver_authentication_type` is `basic` | Password Environmental variable for basic authentication. | +| `alerting..receiver_authentication_basic.authentication_scheme` | string (without spaces) | `Basic` | | Prefix for Authorization header for basic authentication. | +| `alerting..receiver_authentication_bearer` | object | - | only when `receiver_authentication_type` is `bearer` | Authentication credentials for bearer authentication. | +| `alerting..receiver_authentication_bearer.token_env` | string | - | only when `receiver_authentication_type` is `bearer` | Token Environmental variable for bearer authentication (Exclusive with `token_file`). | +| `alerting..receiver_authentication_bearer.token_file` | string | - | only when `receiver_authentication_type` is `bearer` | Token file for bearer authentication (Exclusive with `token_env`). | +| `alerting..receiver_authentication_bearer.authentication_scheme` | string (without spaces) | `Bearer` | | Prefix for Authorization header for bearer authentication. | + + +#### Examples + +It is possible to provide credentials for authentication of webhook requests beyond a hard to guess URL. + +For example, if you would like to receive notifications in your custom webhook authenticated with a bearer token taken from an environmental variable whenever Connaisseur admits a request to your cluster, your alerting configuration would look similar to the following snippet: + +```yaml +alerting: + admit_request: + templates: + - template: ecs-1-12-0 + receiver_url: https://your.custom.domain.com/webhook/admit + receiver_authentication_type: bearer + receiver_authentication_bearer: + token_env: CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_TOKEN +``` + +You then have to set the `CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_TOKEN` environment variable referencing the bearer token secret you want to use into the connaisseur deployment. + +Or if you need to load the token from a file you can use the following snipped: + +```yaml +alerting: + admit_request: + templates: + - template: ecs-1-12-0 + receiver_url: https://your.custom.domain.com/webhook/admit + receiver_authentication_type: bearer + receiver_authentication_bearer: + token_file: /etc/webhook/your-token +``` + +Finally in case of basic authentication, you can use the following snippet: + +```yaml +alerting: + admit_request: + templates: + - template: ecs-1-12-0 + receiver_url: https://your.custom.domain.com/webhook/admit + receiver_authentication_type: basic + receiver_authentication_basic: + username_env: CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_USERNAME + password_env: CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_PASSWORD +``` + +You then have to set the `CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_USERNAME` and `CONNAISSEUR_ADMIT_REQUEST_WEBHOOK_AUTH_PASSWORD` environment variables referencing the secret you want to use into the connaisseur deployment. +This is an example of values configuration for basic authentication: +The referenced secret `connaisseur-webhook-user` should already exist. + +```yaml +deployment: + # Add environmental variables to the pod in the deployment + # referencing secrets, configMaps, or fields. + envValueFrom: + - name: CONNAISSSEUR_WEBHOOK_USERNAME + valueFrom: + secretKeyRef: + name: connaisseur-webhook-user + key: username + - name: CONNAISSSEUR_WEBHOOK_PASSWORD + valueFrom: + secretKeyRef: + name: connaisseur-webhook-user + key: password +``` + diff --git a/helm/templates/deployment.yaml b/helm/templates/deployment.yaml index 855d3914b..2a7029aea 100644 --- a/helm/templates/deployment.yaml +++ b/helm/templates/deployment.yaml @@ -80,6 +80,30 @@ spec: valueFrom: fieldRef: fieldPath: metadata.name + {{- if .Values.deployment.env }} + {{- range $e := .Values.deployment.env }} + - name: {{ $e.name }} + value: {{ $e.value }} + {{- end }} + {{- end }} + {{- if .Values.deployment.envValueFrom}} + {{- range $e := .Values.deployment.envValueFrom }} + - name: {{ $e.name }} + valueFrom: + {{- if $e.valueFrom.secretKeyRef }} + secretKeyRef: + name: {{ $e.valueFrom.secretKeyRef.name }} + key: {{ $e.valueFrom.secretKeyRef.key }} + {{- else if $e.valueFrom.configMapKeyRef }} + configMapKeyRef: + name: {{ $e.valueFrom.configMapKeyRef.name }} + key: {{ $e.valueFrom.configMapKeyRef.key }} + {{- else if $e.valueFrom.fieldRef }} + fieldRef: + fieldPath: {{ $e.valueFrom.fieldRef.fieldPath }} + {{- end }} + {{- end }} + {{- end }} resources: {{- toYaml .Values.deployment.resources | nindent 12 }} securityContext: diff --git a/tests/data/alerting/misconfigured_config/alertconfig_basic_1.json b/tests/data/alerting/misconfigured_config/alertconfig_basic_1.json new file mode 100644 index 000000000..9421486d8 --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_basic_1.json @@ -0,0 +1,13 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic" + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_basic_2.json b/tests/data/alerting/misconfigured_config/alertconfig_basic_2.json new file mode 100644 index 000000000..cadb6bbf1 --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_basic_2.json @@ -0,0 +1,16 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username": "" + } + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_basic_3.json b/tests/data/alerting/misconfigured_config/alertconfig_basic_3.json new file mode 100644 index 000000000..3a8e95b2b --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_basic_3.json @@ -0,0 +1,13 @@ +{ + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic" + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_basic_4.json b/tests/data/alerting/misconfigured_config/alertconfig_basic_4.json new file mode 100644 index 000000000..8178a785a --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_basic_4.json @@ -0,0 +1,16 @@ +{ + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username": "" + } + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_bearer_1.json b/tests/data/alerting/misconfigured_config/alertconfig_bearer_1.json new file mode 100644 index 000000000..4f5d5877a --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_bearer_1.json @@ -0,0 +1,17 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "123", + "token_file": "123" + } + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_bearer_2.json b/tests/data/alerting/misconfigured_config/alertconfig_bearer_2.json new file mode 100644 index 000000000..08b83eb78 --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_bearer_2.json @@ -0,0 +1,17 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_basic": { + "username": "", + "password": "" + } + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_bearer_3.json b/tests/data/alerting/misconfigured_config/alertconfig_bearer_3.json new file mode 100644 index 000000000..6516904fb --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_bearer_3.json @@ -0,0 +1,17 @@ +{ + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "123", + "token_file": "123" + } + } + ] + } +} diff --git a/tests/data/alerting/misconfigured_config/alertconfig_bearer_4.json b/tests/data/alerting/misconfigured_config/alertconfig_bearer_4.json new file mode 100644 index 000000000..12d56eeec --- /dev/null +++ b/tests/data/alerting/misconfigured_config/alertconfig_bearer_4.json @@ -0,0 +1,17 @@ +{ + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_basic": { + "username": "", + "password": "" + } + } + ] + } +} diff --git a/tests/data/alerting/valid_config/alertconfig_basic_1.json b/tests/data/alerting/valid_config/alertconfig_basic_1.json new file mode 100644 index 000000000..0751fe4c6 --- /dev/null +++ b/tests/data/alerting/valid_config/alertconfig_basic_1.json @@ -0,0 +1,32 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username_env": "", + "password_env": "" + } + } + ] + }, + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username_env": "", + "password_env": "" + } + } + ] + } +} diff --git a/tests/data/alerting/valid_config/alertconfig_bearer_1.json b/tests/data/alerting/valid_config/alertconfig_bearer_1.json new file mode 100644 index 000000000..44ae7fba2 --- /dev/null +++ b/tests/data/alerting/valid_config/alertconfig_bearer_1.json @@ -0,0 +1,30 @@ +{ + "reject_request": { + "message": "CONNAISSEUR rejected a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "" + } + } + ] + }, + "admit_request": { + "message": "CONNAISSEUR admitted a request", + "templates": [ + { + "priority": 3, + "receiver_url": "https://hooks.slack.com/services/123", + "template": "slack", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_file": "" + } + } + ] + } +} diff --git a/tests/test_alert.py b/tests/test_alert.py index e3ec661aa..b8f08823e 100644 --- a/tests/test_alert.py +++ b/tests/test_alert.py @@ -1,3 +1,4 @@ +import os import pytest from datetime import datetime, timedelta import json @@ -57,6 +58,76 @@ "template": "custom", } +receiver_config_bearer_env = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "CONNAISSEUR_ALERTING_TOKEN", + }, + "template": "slack", +} + +receiver_config_bearer_env_invalid_scheme = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "CONNAISSEUR_ALERTING_TOKEN", + "authentication_scheme": "", + }, + "template": "slack", +} + +receiver_config_bearer_file = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_file": "/tmp/token123456", + }, + "template": "slack", +} + +receiver_config_bearer_env_scheme = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "bearer", + "receiver_authentication_bearer": { + "token_env": "CONNAISSEUR_ALERTING_TOKEN", + "authentication_scheme": "Newscheme", + }, + "template": "slack", +} + +receiver_config_basic = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username_env": "CONNAISSEUR_ALERTING_USERNAME", + "password_env": "CONNAISSEUR_ALERTING_PASSWORD", + }, + "template": "slack", +} + +receiver_config_basic_invalid_scheme = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username_env": "CONNAISSEUR_ALERTING_USERNAME", + "password_env": "CONNAISSEUR_ALERTING_PASSWORD", + "authentication_scheme": "Ba sic", + }, + "template": "slack", +} + +receiver_config_basic_scheme = { + "receiver_url": "this.is.a.testurl.conn", + "receiver_authentication_type": "basic", + "receiver_authentication_basic": { + "username_env": "CONNAISSEUR_ALERTING_USERNAME", + "password_env": "CONNAISSEUR_ALERTING_PASSWORD", + "authentication_scheme": "Newscheme", + }, + "template": "slack", +} + keybase_receiver_config = { "custom_headers": ["Content-Language: de-DE"], "fail_if_alert_sending_fails": True, @@ -129,6 +200,57 @@ pytest.raises(ConfigurationError, match=r".*invalid format.*"), ), ("/", True, pytest.raises(ConfigurationError, match=r".*error occurred.*")), + ( + "tests/data/alerting/misconfigured_config/alertconfig_bearer_1.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_bearer_2.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_bearer_3.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_bearer_4.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_basic_1.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_basic_2.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_basic_3.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/misconfigured_config/alertconfig_basic_4.json", + True, + pytest.raises(ConfigurationError, match=r".*invalid format.*"), + ), + ( + "tests/data/alerting/valid_config/alertconfig_bearer_1.json", + False, + fix.no_exc(), + ), + ( + "tests/data/alerting/valid_config/alertconfig_basic_1.json", + False, + fix.no_exc(), + ), + ("tests/data/alerting/missing.json", True, fix.no_exc()), ], ) def test_alert_config_init( @@ -259,6 +381,147 @@ def test_alert_init( assert alert_payload == json.loads(alert_.payload) +@pytest.mark.parametrize( + "receiver_config, envs, files, headers_count, header, exception", + [ + ( + receiver_config_bearer_env, + {"CONNAISSEUR_ALERTING_TOKEN": "AAABBBCCCDDD"}, + {}, + 2, + {"Authorization": "Bearer AAABBBCCCDDD"}, + fix.no_exc(), + ), + ( + receiver_config_bearer_env_scheme, + {"CONNAISSEUR_ALERTING_TOKEN": "AAABBBCCCDDD"}, + {}, + 2, + {"Authorization": "Newscheme AAABBBCCCDDD"}, + fix.no_exc(), + ), + ( + receiver_config_bearer_file, + {}, + {"/tmp/token123456": "AAABBBCCCDDDEEE"}, + 2, + {"Authorization": "Bearer AAABBBCCCDDDEEE"}, + fix.no_exc(), + ), + ( + receiver_config_bearer_env, + {"CONNAISSEUR_ALERTING_TOKEN": ""}, + {}, + 1, + {}, + pytest.raises( + ConfigurationError, + match=r"No token found from environmental variable.*", + ), + ), + ( + receiver_config_bearer_env_invalid_scheme, + {}, + {}, + 1, + {}, + pytest.raises( + ConfigurationError, + match=r"The authentication scheme cannot be null or empty.", + ), + ), + ( + receiver_config_bearer_file, + {}, + {}, + 1, + {}, + pytest.raises(ConfigurationError, match=r"No token file found.*"), + ), + ( + receiver_config_basic, + { + "CONNAISSEUR_ALERTING_USERNAME": "user", + "CONNAISSEUR_ALERTING_PASSWORD": "password", + }, + {}, + 2, + {"Authorization": "Basic user:password"}, + fix.no_exc(), + ), + ( + receiver_config_basic_scheme, + { + "CONNAISSEUR_ALERTING_USERNAME": "user", + "CONNAISSEUR_ALERTING_PASSWORD": "password", + }, + {}, + 2, + {"Authorization": "Newscheme user:password"}, + fix.no_exc(), + ), + ( + receiver_config_basic, + {"CONNAISSEUR_ALERTING_USERNAME": "", "CONNAISSEUR_ALERTING_PASSWORD": ""}, + {}, + 1, + {}, + pytest.raises( + ConfigurationError, + match=r"No username or password found from environmental variables.*", + ), + ), + ( + receiver_config_basic_invalid_scheme, + {}, + {}, + 1, + {}, + pytest.raises( + ConfigurationError, + match=r"The authentication scheme must contain only letters.", + ), + ), + ], +) +def test_alert_init_auth( + monkeypatch, + m_ad_schema_path, + m_alerting_without_send, + receiver_config: dict, + envs: dict, + files: dict, + headers_count: int, + header: dict, + exception, +): + alert_message = "Alert Message" + admission_request = admission_request_deployment + + for env, value in envs.items(): + if value == "": + if env in os.environ: + del os.environ[env] + else: + os.environ[env] = envs[env] + # monkeypatch.setenv(os.environ[env], envs[env]) + + for filepath, content in files.items(): + with open(filepath, "w") as f: + f.write(content) + + with exception: + alert_ = alert.Alert( + alert_message, receiver_config, AdmissionRequest(admission_request) + ) + assert len(alert_.headers) == headers_count + assert header.items() <= alert_.headers.items() + # assert list(header.keys())[0] in alert_.headers.keys() + + for filepath in files: + os.remove(filepath) + + @pytest.mark.parametrize( "receiver_config, key, status, out, exception", [