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

Add skip_ssl_verify flag for certificate trust verification #63

Merged
merged 3 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading