Skip to content

Commit

Permalink
Add skip_ssl_verify flag for certificate trust verification (#63)
Browse files Browse the repository at this point in the history
* feat: add skip_ssl_verify flag for certificate trust verification

* docs: add skip_ssl_verify flag to readmes

* chore: fix charmcraft build issues
  • Loading branch information
natalian98 authored Aug 13, 2024
1 parent 8a43631 commit 0bb0e04
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 11 deletions.
11 changes: 11 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,14 @@ bases:
run-on:
- name: ubuntu
channel: "22.04"
parts:
charm:
override-build: |
rustup default stable
craftctl default
build-snaps:
- rustup
build-packages:
- libffi-dev
- libssl-dev
- pkg-config
4 changes: 4 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,7 @@ options:
domain:
description: The domain used by the sent emails from SMTP relay
type: string
skip_ssl_verify:
description: Specifies if certificate trust verification is skipped in the SMTP relay
type: boolean
default: false
20 changes: 17 additions & 3 deletions lib/charms/smtp_integrator/v0/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,15 @@ def _on_config_changed(self, _) -> None:

# Increment this PATCH version before using `charmcraft publish-lib` or reset
# to 0 if you are raising the major API version
LIBPATCH = 9
LIBPATCH = 10

PYDEPS = ["pydantic>=2"]

# pylint: disable=wrong-import-position
import itertools
import logging
import typing
from ast import literal_eval
from enum import Enum
from typing import Dict, Optional

Expand Down Expand Up @@ -127,7 +128,8 @@ class SmtpRelationData(BaseModel):
password_id: The secret ID where the SMTP AUTH password for the SMTP relay is stored.
auth_type: The type used to authenticate with the SMTP relay.
transport_security: The security protocol to use for the outgoing SMTP relay.
domain: The domain used by the sent emails from SMTP relay.
domain: The domain used by the emails sent from SMTP relay.
skip_ssl_verify: Specifies if certificate trust verification is skipped in the SMTP relay.
"""

host: str = Field(..., min_length=1)
Expand All @@ -138,6 +140,7 @@ class SmtpRelationData(BaseModel):
auth_type: AuthType
transport_security: TransportSecurity
domain: Optional[str] = None
skip_ssl_verify: bool = False

def to_relation_data(self) -> Dict[str, str]:
"""Convert an instance of SmtpRelationData to the relation representation.
Expand All @@ -150,6 +153,7 @@ def to_relation_data(self) -> Dict[str, str]:
"port": str(self.port),
"auth_type": self.auth_type.value,
"transport_security": self.transport_security.value,
"skip_ssl_verify": str(self.skip_ssl_verify),
}
if self.domain:
result["domain"] = self.domain
Expand All @@ -173,7 +177,8 @@ class SmtpDataAvailableEvent(ops.RelationEvent):
password_id: The secret ID where the SMTP AUTH password for the SMTP relay is stored.
auth_type: The type used to authenticate with the SMTP relay.
transport_security: The security protocol to use for the outgoing SMTP relay.
domain: The domain used by the sent emails from SMTP relay.
domain: The domain used by the emails sent from SMTP relay.
skip_ssl_verify: Specifies if certificate trust verification is skipped in the SMTP relay.
"""

@property
Expand Down Expand Up @@ -224,6 +229,14 @@ def domain(self) -> str:
assert self.relation.app
return typing.cast(str, self.relation.data[self.relation.app].get("domain"))

@property
def skip_ssl_verify(self) -> bool:
"""Fetch the skip_ssl_verify flag from the relation."""
assert self.relation.app
return literal_eval(
typing.cast(str, self.relation.data[self.relation.app].get("skip_ssl_verify"))
)


class SmtpRequiresEvents(ops.CharmEvents):
"""SMTP events.
Expand Down Expand Up @@ -287,6 +300,7 @@ def _get_relation_data_from_relation(self, relation: ops.Relation) -> SmtpRelati
auth_type=AuthType(relation_data.get("auth_type")),
transport_security=TransportSecurity(relation_data.get("transport_security")),
domain=relation_data.get("domain"),
skip_ssl_verify=typing.cast(bool, relation_data.get("skip_ssl_verify")),
)

def _is_relation_data_valid(self, relation: ops.Relation) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion src-docs/charm.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ SMTP Integrator Charm service.
## <kbd>class</kbd> `SmtpIntegratorOperatorCharm`
Charm the service.

<a href="../src/charm.py#L24"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm.py#L25"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down
34 changes: 29 additions & 5 deletions src-docs/charm_state.py.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Exception raised when a charm configuration is found to be invalid.

- <b>`msg`</b> (str): Explanation of the error.

<a href="../src/charm_state.py#L46"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm_state.py#L48"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand Down Expand Up @@ -53,9 +53,10 @@ Represents the state of the SMTP Integrator charm.
- <b>`password`</b>: The SMTP AUTH password to use for the outgoing SMTP relay.
- <b>`auth_type`</b>: The type used to authenticate with the SMTP relay.
- <b>`transport_security`</b>: The security protocol to use for the outgoing SMTP relay.
- <b>`domain`</b>: The domain used by the sent emails from SMTP relay.
- <b>`domain`</b>: The domain used by the emails sent from SMTP relay.
- <b>`skip_ssl_verify`</b>: Specifies if certificate trust verification is skipped in the SMTP relay.

<a href="../src/charm_state.py#L77"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm_state.py#L81"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>function</kbd> `__init__`

Expand All @@ -76,7 +77,7 @@ Initialize a new instance of the CharmState class.

---

<a href="../src/charm_state.py#L91"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>
<a href="../src/charm_state.py#L96"><img align="right" style="float:right;" src="https://img.shields.io/badge/-source-cccccc?style=flat-square"></a>

### <kbd>classmethod</kbd> `from_charm`

Expand Down Expand Up @@ -116,9 +117,32 @@ Represent charm builtin configuration values.
- <b>`password`</b>: The SMTP AUTH password to use for the outgoing SMTP relay.
- <b>`auth_type`</b>: The type used to authenticate with the SMTP relay.
- <b>`transport_security`</b>: The security protocol to use for the outgoing SMTP relay.
- <b>`domain`</b>: The domain used by the sent emails from SMTP relay.
- <b>`domain`</b>: The domain used by the emails sent from SMTP relay.
- <b>`skip_ssl_verify`</b>: Specifies if certificate trust verification is skipped in the SMTP relay.


---

#### <kbd>property</kbd> model_extra

Get extra fields set during validation.



**Returns:**
A dictionary of extra fields, or `None` if `config.extra` is not set to `"allow"`.

---

#### <kbd>property</kbd> model_fields_set

Returns the set of fields that have been explicitly set on this model instance.



**Returns:**
A set of strings representing the fields that have been set, i.e. that were not filled from defaults.




2 changes: 2 additions & 0 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ def _get_legacy_smtp_data(self) -> smtp.SmtpRelationData:
auth_type=self._charm_state.auth_type,
transport_security=self._charm_state.transport_security,
domain=self._charm_state.domain,
skip_ssl_verify=self._charm_state.skip_ssl_verify,
)

def _get_smtp_data(self) -> smtp.SmtpRelationData:
Expand All @@ -162,6 +163,7 @@ def _get_smtp_data(self) -> smtp.SmtpRelationData:
auth_type=self._charm_state.auth_type,
transport_security=self._charm_state.transport_security,
domain=self._charm_state.domain,
skip_ssl_verify=self._charm_state.skip_ssl_verify,
)

def _has_secrets(self) -> bool:
Expand Down
9 changes: 7 additions & 2 deletions src/charm_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ class SmtpIntegratorConfig(BaseModel):
password: The SMTP AUTH password to use for the outgoing SMTP relay.
auth_type: The type used to authenticate with the SMTP relay.
transport_security: The security protocol to use for the outgoing SMTP relay.
domain: The domain used by the sent emails from SMTP relay.
domain: The domain used by the emails sent from SMTP relay.
skip_ssl_verify: Specifies if certificate trust verification is skipped in the SMTP relay.
"""

host: str = Field(..., min_length=1)
Expand All @@ -34,6 +35,7 @@ class SmtpIntegratorConfig(BaseModel):
auth_type: smtp.AuthType | None = None
transport_security: smtp.TransportSecurity | None = None
domain: Optional[str] = None
skip_ssl_verify: bool = False


class CharmConfigInvalidError(Exception):
Expand Down Expand Up @@ -63,7 +65,8 @@ class CharmState: # pylint: disable=too-many-instance-attributes
password: The SMTP AUTH password to use for the outgoing SMTP relay.
auth_type: The type used to authenticate with the SMTP relay.
transport_security: The security protocol to use for the outgoing SMTP relay.
domain: The domain used by the sent emails from SMTP relay.
domain: The domain used by the emails sent from SMTP relay.
skip_ssl_verify: Specifies if certificate trust verification is skipped in the SMTP relay.
"""

host: str
Expand All @@ -73,6 +76,7 @@ class CharmState: # pylint: disable=too-many-instance-attributes
auth_type: Optional[smtp.AuthType]
transport_security: Optional[smtp.TransportSecurity]
domain: Optional[str]
skip_ssl_verify: bool

def __init__(self, *, smtp_integrator_config: SmtpIntegratorConfig):
"""Initialize a new instance of the CharmState class.
Expand All @@ -87,6 +91,7 @@ def __init__(self, *, smtp_integrator_config: SmtpIntegratorConfig):
self.auth_type = smtp_integrator_config.auth_type
self.transport_security = smtp_integrator_config.transport_security
self.domain = smtp_integrator_config.domain
self.skip_ssl_verify = smtp_integrator_config.skip_ssl_verify

@classmethod
def from_charm(cls, charm: "ops.CharmBase") -> "CharmState":
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/test_library_smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

"""SMTP library unit tests"""
import secrets
from ast import literal_eval

import ops
import pytest
Expand Down Expand Up @@ -34,6 +35,7 @@
"auth_type": "plain",
"transport_security": "tls",
"domain": "domain",
"skip_ssl_verify": "False",
}

SAMPLE_LEGACY_RELATION_DATA = {
Expand Down Expand Up @@ -112,13 +114,15 @@ def test_smtp_provider_update_relation_data():
port=25,
auth_type="plain",
transport_security="tls",
skip_ssl_verify=False,
)
harness.charm.smtp_legacy.update_relation_data(relation, smtp_data)
data = relation.data[harness.model.app]
assert data["host"] == smtp_data.host
assert data["port"] == str(smtp_data.port)
assert data["auth_type"] == smtp_data.auth_type
assert data["transport_security"] == smtp_data.transport_security
assert data["skip_ssl_verify"] == str(smtp_data.skip_ssl_verify)


def test_smtp_relation_data_to_relation_data():
Expand All @@ -136,6 +140,7 @@ def test_smtp_relation_data_to_relation_data():
auth_type="plain",
transport_security="tls",
domain="domain",
skip_ssl_verify=False,
)
relation_data = smtp_data.to_relation_data()
expected_relation_data = {
Expand All @@ -147,6 +152,7 @@ def test_smtp_relation_data_to_relation_data():
"auth_type": "plain",
"transport_security": "tls",
"domain": "domain",
"skip_ssl_verify": "False",
}
assert relation_data == expected_relation_data

Expand Down Expand Up @@ -204,6 +210,9 @@ def test_legacy_requirer_charm_with_valid_relation_data_emits_event(is_leader):
== SAMPLE_LEGACY_RELATION_DATA["transport_security"]
)
assert harness.charm.events[0].domain == SAMPLE_LEGACY_RELATION_DATA["domain"]
assert harness.charm.events[0].skip_ssl_verify == literal_eval(
SAMPLE_LEGACY_RELATION_DATA["skip_ssl_verify"]
)

retrieved_relation_data = harness.charm.smtp_legacy.get_relation_data()
assert retrieved_relation_data.host == SAMPLE_LEGACY_RELATION_DATA["host"]
Expand All @@ -216,6 +225,9 @@ def test_legacy_requirer_charm_with_valid_relation_data_emits_event(is_leader):
== SAMPLE_LEGACY_RELATION_DATA["transport_security"]
)
assert retrieved_relation_data.domain == SAMPLE_LEGACY_RELATION_DATA["domain"]
assert retrieved_relation_data.skip_ssl_verify == literal_eval(
SAMPLE_LEGACY_RELATION_DATA["skip_ssl_verify"]
)


@pytest.mark.parametrize("is_leader", [True, False])
Expand All @@ -238,6 +250,9 @@ def test_requirer_charm_with_valid_relation_data_emits_event(is_leader):
assert harness.charm.events[0].auth_type == SAMPLE_RELATION_DATA["auth_type"]
assert harness.charm.events[0].transport_security == SAMPLE_RELATION_DATA["transport_security"]
assert harness.charm.events[0].domain == SAMPLE_RELATION_DATA["domain"]
assert harness.charm.events[0].skip_ssl_verify == literal_eval(
SAMPLE_RELATION_DATA["skip_ssl_verify"]
)


@pytest.mark.parametrize("is_leader", [True, False])
Expand All @@ -254,6 +269,7 @@ def test_requirer_charm_with_invalid_relation_data_doesnt_emit_event(is_leader):
"auth_type": "plain",
"transport_security": "tls",
"domain": "domain",
"skip_ssl_verify": "False",
}

harness = Harness(SmtpRequirerCharm, meta=REQUIRER_METADATA)
Expand Down

0 comments on commit 0bb0e04

Please sign in to comment.