Skip to content

Commit

Permalink
feat: enable StartTLS support
Browse files Browse the repository at this point in the history
  • Loading branch information
wood-push-melon committed Jan 24, 2024
1 parent 4d96a5a commit 1dd8509
Show file tree
Hide file tree
Showing 15 changed files with 198 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repos:
- id: requirements-txt-fixer
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.13
rev: v0.1.14
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
Expand Down
46 changes: 34 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,6 @@ $ juju integrate glauth-k8s postgresql-k8s

## Integrations

### `postgresql_client` Integration

The `glauth-k8s` charmed operator requires the integration with the
`postgres-k8s` charmed operator following the [`postgresql_client` interface
protocol](https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/postgresql_client/v0).

```shell
$ juju integrate glauth-k8s postgresql-k8s
```

### `ldap` Integration

The `glauth-k8s` charmed operator offers the `ldap` integration with any
Expand All @@ -60,7 +50,38 @@ the [`glauth-utils` charmed operator](https://github.com/canonical/glauth-utils)
to deliver necessary auxiliary configurations.

```shell
$ juju integrate glauth-k8s glauth-utils
$ juju integrate glauth-utils glauth-k8s
```

### `certificate_transfer` Integration

The `glauth-k8s` charmed operator provides the `certificate_transfer`
integration with any charmed operator following the [`certificate_transfer`
interface protocol](https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/certificate_transfer/v0).

```shell
$ juju integrate <client-charm> glauth-k8s
```

### `postgresql_client` Integration

The `glauth-k8s` charmed operator requires the integration with the
`postgres-k8s` charmed operator following the [`postgresql_client` interface
protocol](https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/postgresql_client/v0).

```shell
$ juju integrate glauth-k8s postgresql-k8s
```

### `tls_certificates` Integration

The `glauth-k8s` charmed operator requires the `tls-certificates`
integration with any charmed operator following the [`tls_certificates`
interface protocol](https://github.com/canonical/charm-relation-interfaces/tree/main/interfaces/tls_certificates/v0).
Take the `self-signed-certificates-operator` as an example:

```shell
$ juju integrate glauth-k8s self-signed-certificates
```

## Configurations
Expand All @@ -72,11 +93,12 @@ options.
|:-------------------:|------------------------------------------------------------------|------------------------------------------------------|
| `base_dn` | The portion of the DIT in which to search for matching entries | `juju config <charm-app> base-dn="dc=glauth,dc=com"` |
| `hostname` | The hostname of the LDAP server in `glauth-k8s` charmed operator | `juju config <charm-app> hostname="ldap.glauth.com"` |
| `starttls_enabled` | The switch to enable/disable StartTLS support | `juju config <charm-app> starttls_enabled=true` |

> ⚠️ **NOTE**
>
> - The `hostname` should **NOT** contain the ldap scheme (e.g. `ldap://`) and
> port.
port.
> - Please refer to the `config.yaml` for more details about the configurations.
## Contributing
Expand Down
5 changes: 5 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ options:
The hostname should NOT contain the LDAP scheme (e.g. ldap://) and port.
default: "ldap.glauth.com"
type: string
starttls_enabled:
description: |
Enable the StartTLS support or not.
default: true
type: boolean
16 changes: 8 additions & 8 deletions lib/charms/glauth_k8s/v0/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def _on_ldap_requested(self, event: LdapRequestedEvent) -> None:

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

PYDEPS = ["pydantic~=2.5.3"]

Expand Down Expand Up @@ -187,6 +187,7 @@ class LdapProviderBaseData(BaseModel):

url: str
base_dn: str
starttls: StrictBool

@field_validator("url")
@classmethod
Expand All @@ -196,13 +197,6 @@ def validate_ldap_url(cls, v: str) -> str:

return v


class LdapProviderData(LdapProviderBaseData):
bind_dn: str
bind_password_secret: str
auth_method: Literal["simple"]
starttls: StrictBool

@field_validator("starttls", mode="before")
@classmethod
def deserialize_bool(cls, v: str | bool) -> bool:
Expand All @@ -216,6 +210,12 @@ def serialize_bool(self, starttls: bool) -> str:
return str(starttls)


class LdapProviderData(LdapProviderBaseData):
bind_dn: str
bind_password_secret: str
auth_method: Literal["simple"]


class LdapRequirerData(BaseModel):
model_config = ConfigDict(frozen=True)

Expand Down
2 changes: 1 addition & 1 deletion metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ resources:
oci-image:
type: oci-image
description: GLAuth oci-image
upstream-source: ghcr.io/canonical/glauth:2.2.1
upstream-source: ghcr.io/canonical/glauth:2.3.0

requires:
pg-database:
Expand Down
19 changes: 10 additions & 9 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from charms.observability_libs.v0.cert_handler import CertChanged
from charms.observability_libs.v0.kubernetes_service_patch import KubernetesServicePatch
from charms.prometheus_k8s.v0.prometheus_scrape import MetricsEndpointProvider
from configs import ConfigFile, DatabaseConfig, pebble_layer
from configs import ConfigFile, DatabaseConfig, StartTLSConfig, pebble_layer
from constants import (
CERTIFICATES_TRANSFER_INTEGRATION_NAME,
DATABASE_INTEGRATION_NAME,
Expand Down Expand Up @@ -58,6 +58,7 @@
from utils import (
after_config_updated,
block_on_missing,
demand_tls_certificates,
leader_unit,
validate_container_connectivity,
validate_database_resource,
Expand Down Expand Up @@ -140,7 +141,10 @@ def __init__(self, *args: Any):
self._on_promtail_error,
)

self.config_file = ConfigFile(base_dn=self.config.get("base_dn"))
self.config_file = ConfigFile(
base_dn=self.config.get("base_dn"),
starttls_config=StartTLSConfig.load(self),
)
self._ldap_integration = LdapIntegration(self)
self._auxiliary_integration = AuxiliaryIntegration(self)

Expand All @@ -155,6 +159,7 @@ def _restart_glauth_service(self) -> None:
)

@validate_container_connectivity
@demand_tls_certificates
@validate_integration_exists(DATABASE_INTEGRATION_NAME, on_missing=block_on_missing)
@validate_database_resource
def _handle_event_update(self, event: HookEvent) -> None:
Expand Down Expand Up @@ -207,13 +212,7 @@ def _on_remove(self, event: RemoveEvent) -> None:
self._configmap.delete()

def _on_database_created(self, event: DatabaseCreatedEvent) -> None:
self.config_file.database_config = DatabaseConfig.load(self.database_requirer)
self._update_glauth_config()

self._container.add_layer(WORKLOAD_CONTAINER, pebble_layer, combine=True)
self._restart_glauth_service()
self.unit.status = ActiveStatus()

self._handle_event_update(event)
self.auxiliary_provider.update_relation_app_data(
data=self._auxiliary_integration.auxiliary_data,
)
Expand All @@ -226,6 +225,7 @@ def _on_database_changed(self, event: DatabaseEndpointsChangedEvent) -> None:

def _on_config_changed(self, event: ConfigChangedEvent) -> None:
self.config_file.base_dn = self.config.get("base_dn")
self.config_file.starttls_config = StartTLSConfig.load(self)
self._handle_event_update(event)
self.ldap_provider.update_relations_app_data(
data=self._ldap_integration.provider_base_data
Expand Down Expand Up @@ -268,6 +268,7 @@ def _on_cert_changed(self, event: CertChanged) -> None:
)
return

self._handle_event_update(event)
self._certs_transfer_integration.transfer_certificates(
self._certs_integration.cert_data,
)
Expand Down
20 changes: 20 additions & 0 deletions src/configs.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Optional

from constants import (
GLAUTH_COMMANDS,
LOG_FILE,
POSTGRESQL_DSN_TEMPLATE,
SERVER_CERT,
SERVER_KEY,
WORKLOAD_SERVICE,
)
from jinja2 import Template
from ops.charm import CharmBase
from ops.pebble import Layer


Expand Down Expand Up @@ -43,10 +47,24 @@ def load(cls, requirer: Any) -> "DatabaseConfig":
)


@dataclass
class StartTLSConfig:
enabled: bool = True
tls_key: Path = SERVER_KEY
tls_cert: Path = SERVER_CERT

@classmethod
def load(cls, charm: CharmBase) -> "StartTLSConfig":
return StartTLSConfig(
enabled=charm.config.get("starttls_enabled", True),
)


@dataclass
class ConfigFile:
base_dn: Optional[str] = None
database_config: Optional[DatabaseConfig] = None
starttls_config: Optional[StartTLSConfig] = None

@property
def content(self) -> str:
Expand All @@ -57,9 +75,11 @@ def render(self) -> str:
template = Template(file.read())

database_config = self.database_config or DatabaseConfig()
starttls_config = self.starttls_config or StartTLSConfig()
rendered = template.render(
base_dn=self.base_dn,
database=asdict(database_config),
starttls=asdict(starttls_config),
)
return rendered

Expand Down
2 changes: 1 addition & 1 deletion src/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class User(Base):


class Group(Base):
__tablename__ = "groups"
__tablename__ = "ldapgroups"

id = mapped_column(Integer, primary_key=True)
name: Mapped[str] = mapped_column(name="name", unique=True)
Expand Down
7 changes: 6 additions & 1 deletion src/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,16 @@ def ldap_url(self) -> str:
def base_dn(self) -> str:
return self._charm.config.get("base_dn")

@property
def starttls_enabled(self) -> bool:
return self._charm.config.get("starttls_enabled", True)

@property
def provider_base_data(self) -> LdapProviderBaseData:
return LdapProviderBaseData(
url=self.ldap_url,
base_dn=self.base_dn,
starttls=self.starttls_enabled,
)

@property
Expand All @@ -103,7 +108,7 @@ def provider_data(self) -> Optional[LdapProviderData]:
bind_dn=f"cn={self._bind_account.cn},ou={self._bind_account.ou},{self.base_dn}",
bind_password_secret=self._bind_account.password or "",
auth_method="simple",
starttls=True,
starttls=self.starttls_enabled,
)


Expand Down
34 changes: 27 additions & 7 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from functools import wraps
from typing import Any, Callable, Optional

from constants import GLAUTH_CONFIG_FILE
from constants import GLAUTH_CONFIG_FILE, SERVER_CERT, SERVER_KEY
from ops.charm import CharmBase, EventBase
from ops.model import BlockedStatus, WaitingStatus
from tenacity import Retrying, TryAgain, wait_fixed
Expand Down Expand Up @@ -40,12 +40,12 @@ def validate_container_connectivity(func: Callable) -> Callable:
@wraps(func)
def wrapper(charm: CharmBase, *args: EventBase, **kwargs: Any) -> Optional[Any]:
event, *_ = args
logger.debug(f"Handling event: {event}")
logger.debug(f"Handling event: {event}.")
if not charm._container.can_connect():
logger.debug(f"Cannot connect to container, defer event {event}.")
event.defer()

charm.unit.status = WaitingStatus("Waiting to connect to container.")
charm.unit.status = WaitingStatus("Waiting to connect to container")
return None

return func(charm, *args, **kwargs)
Expand All @@ -62,7 +62,7 @@ def decorator(func: Callable) -> Callable:
@wraps(func)
def wrapper(charm: CharmBase, *args: EventBase, **kwargs: Any) -> Optional[Any]:
event, *_ = args
logger.debug(f"Handling event: {event}")
logger.debug(f"Handling event: {event}.")

if not charm.model.relations[integration_name]:
on_missing_request(charm, event, integration_name=integration_name)
Expand All @@ -79,10 +79,10 @@ def validate_database_resource(func: Callable) -> Callable:
@wraps(func)
def wrapper(charm: CharmBase, *args: EventBase, **kwargs: Any) -> Optional[Any]:
event, *_ = args
logger.debug(f"Handling event: {event}")
logger.debug(f"Handling event: {event}.")

if not charm.database_requirer.is_resource_created():
logger.debug(f"Database has not been created yet, defer event {event}")
logger.debug(f"Database has not been created yet, defer event {event}.")
event.defer()

charm.unit.status = WaitingStatus("Waiting for database creation")
Expand All @@ -93,10 +93,30 @@ def wrapper(charm: CharmBase, *args: EventBase, **kwargs: Any) -> Optional[Any]:
return wrapper


def demand_tls_certificates(func: Callable) -> Callable:
@wraps(func)
def wrapper(charm: CharmBase, *args: Any, **kwargs: Any) -> Optional[Any]:
event, *_ = args
logger.debug(f"Handling event: {event}.")

if charm.config.get("starttls_enabled", True) and not (
charm._container.exists(SERVER_KEY) and charm._container.exists(SERVER_CERT)
):
logger.debug(f"TLS certificate and private key not ready. defer event {event}.")
event.defer()

charm.unit.status = BlockedStatus("Missing required TLS certificate and private key")
return None

return func(charm, *args, **kwargs)

return wrapper


def after_config_updated(func: Callable) -> Callable:
@wraps(func)
def wrapper(charm: CharmBase, *args: Any, **kwargs: Any) -> Optional[Any]:
charm.unit.status = WaitingStatus("Waiting for configuration to be updated.")
charm.unit.status = WaitingStatus("Waiting for configuration to be updated")

for attempt in Retrying(
wait=wait_fixed(3),
Expand Down
3 changes: 3 additions & 0 deletions templates/glauth.cfg.j2
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ structuredlog = true
[ldap]
enabled = true
listen = "0.0.0.0:3893"
tls = {{ starttls.enabled|tojson }}
tlsKeyPath = "{{ starttls.tls_key|default("/etc/ssl/private/glauth.key", true) }}"
tlsCertPath = "{{ starttls.tls_cert|default("/usr/local/share/ca-certificates/glauth.crt", true) }}"

[ldaps]
enabled = false
Expand Down
1 change: 1 addition & 0 deletions tests/integration/test_charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ async def test_build_and_deploy(ops_test: OpsTest) -> None:
str(charm_path),
resources={"oci-image": GLAUTH_IMAGE},
application_name=GLAUTH_APP,
config={"starttls_enabled": False},
trust=True,
series="jammy",
)
Expand Down
Loading

0 comments on commit 1dd8509

Please sign in to comment.