Skip to content

Commit

Permalink
feat: add auth providers config options
Browse files Browse the repository at this point in the history
Adds config options related to OIDC and OpenFGA to the charm. This
allows users to configure Incus to authenticate with external
authentication and authorization providers without requiring any charm
integrations.

Signed-off-by: Luís Simas <[email protected]>
  • Loading branch information
luissimas committed Mar 10, 2025
1 parent b9c7175 commit 3092eca
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 11 deletions.
56 changes: 56 additions & 0 deletions charmcraft.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,62 @@ config:
Whether to enable the Incus Web UI.
default: false
type: boolean
oidc-audience:
description: |
The expected audience value for the application.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openid-connect-configuration
type: string
oidc-claim:
description: |
OpenID Connect claim to use as the username.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openid-connect-configuration
type: string
oidc-client-id:
description: |
OpenID Connect client ID for the application.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openid-connect-configuration
type: string
oidc-issuer:
description: |
OpenID Connect Discovery URL for the provider.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openid-connect-configuration
type: string
oidc-scopes:
description: |
Comma separated list of OpenID Connect scopes.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openid-connect-configuration
type: string
openfga-api-token:
description: |
API Token of the OpenFGA server.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openfga-configuration
type: string
openfga-api-url:
description: |
URL of the OpenFGA server.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openfga-configuration
type: string
openfga-store-id:
description: |
ID of the OpenFGA permission store.
For more information, refer to the Incus documentation:
https://linuxcontainers.org/incus/docs/main/server_config/#openfga-configuration
type: string

actions:
add-trusted-certificate:
Expand Down
27 changes: 24 additions & 3 deletions src/charm.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ class IncusConfig(data_models.BaseConfigModel):
package_repository_gpg_key: str
set_failure_domain: bool
enable_web_ui: bool
oidc_audience: Optional[str] = None
oidc_claim: Optional[str] = None
oidc_client_id: Optional[str] = None
oidc_issuer: Optional[str] = None
oidc_scopes: Optional[str] = None
openfga_api_token: Optional[str] = None
openfga_api_url: Optional[str] = None
openfga_store_id: Optional[str] = None

@validator("server_port", "cluster_port", "metrics_port")
@classmethod
Expand Down Expand Up @@ -273,20 +281,20 @@ def on_config_changed(self, event: ops.ConfigChangedEvent):
public_binding = self.model.get_binding("public")
if public_binding and public_binding.network.bind_address:
public_address = str(public_binding.network.bind_address)
incus.set_config("core.https_address", f"{public_address}:{server_port}")
incus.set_config({"core.https_address": f"{public_address}:{server_port}"})

# NOTE: Incus does not support changing the cluster.https_address if the
# server is already part of a cluster.
if not incus.is_clustered():
incus.set_config("cluster.https_address", self._cluster_address)
incus.set_config({"cluster.https_address": self._cluster_address})

metrics_address = ""
metrics_port = self.config.metrics_port
monitoring_binding = self.model.get_binding("monitoring")
if monitoring_binding and monitoring_binding.network.bind_address:
metrics_address = str(monitoring_binding.network.bind_address)
if metrics_address != public_address:
incus.set_config("core.metrics_address", f"{metrics_address}:{metrics_port}")
incus.set_config({"core.metrics_address": f"{metrics_address}:{metrics_port}"})
self.unit.set_ports(ops.Port("tcp", metrics_port), ops.Port("tcp", server_port))

if incus.is_clustered() and self.config.set_failure_domain:
Expand Down Expand Up @@ -324,6 +332,19 @@ def on_config_changed(self, event: ops.ConfigChangedEvent):
"ceph", {"ceph.rbd.features": self.config.ceph_rbd_features}
)

incus.set_config(
{
"oidc.audience": self.config.oidc_audience,
"oidc.claim": self.config.oidc_claim,
"oidc.client.id": self.config.oidc_client_id,
"oidc.issuer": self.config.oidc_issuer,
"oidc.scopes": self.config.oidc_scopes,
"openfga.api.token": self.config.openfga_api_token,
"openfga.api.url": self.config.openfga_api_url,
"openfga.store.id": self.config.openfga_store_id,
}
)

def on_start(self, event: ops.StartEvent):
"""Handle start event.
Expand Down
10 changes: 7 additions & 3 deletions src/incus.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,13 @@ def is_retryable(self) -> bool:
return any(message in self.message for message in self.retryable_error_messages)


def set_config(key: str, value: Union[str, int]):
"""Set config `key` to `value` in the Incus daemon."""
run_command("config", "set", key, str(value))
def set_config(configs: Dict[str, Optional[Union[str, int]]]):
"""Set the given configs in the Incus daemon.
If any config value is `None`, the config will be unset in Incus.
"""
keypairs = [f"{key}={value if value is not None else ''}" for key, value in configs.items()]
run_command("config", "set", *keypairs)


def is_clustered() -> bool:
Expand Down
72 changes: 67 additions & 5 deletions tests/unit/test_config_changed.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ def test_config_changed_not_clustered(server_port, cluster_port):

set_config.assert_has_calls(
[
call("core.https_address", f"10.0.0.1:{server_port}"),
call("cluster.https_address", f"10.0.0.2:{cluster_port}"),
call({"core.https_address": f"10.0.0.1:{server_port}"}),
call({"cluster.https_address": f"10.0.0.2:{cluster_port}"}),
]
)

Expand Down Expand Up @@ -88,7 +88,7 @@ def test_config_changed_not_clustered_expose_metrics_endpoints(metrics_port):

set_config.assert_has_calls(
[
call("core.metrics_address", f"10.0.0.3:{metrics_port}"),
call({"core.metrics_address": f"10.0.0.3:{metrics_port}"}),
]
)

Expand Down Expand Up @@ -123,7 +123,7 @@ def test_config_changed_clustered(server_port, cluster_port):

set_config.assert_has_calls(
[
call("core.https_address", f"10.0.0.1:{server_port}"),
call({"core.https_address": f"10.0.0.1:{server_port}"}),
]
)

Expand Down Expand Up @@ -157,7 +157,7 @@ def test_config_changed_clustered_expose_metrics_endpoints(metrics_port):

set_config.assert_has_calls(
[
call("core.metrics_address", f"10.0.0.3:{metrics_port}"),
call({"core.metrics_address": f"10.0.0.3:{metrics_port}"}),
]
)

Expand Down Expand Up @@ -587,3 +587,65 @@ def test_config_changed_disable_web_ui(package_installed):
else:
uninstall_packages.assert_not_called()
install_packages.assert_not_called()


def test_config_changed_auth():
"""Test the config-changed event when auth related config options are set.
The unit should set the config options in Incus.
"""
with (
patch("charm.IncusCharm._package_installed", return_value=False),
patch("charm.IncusCharm._install_packages"),
patch("charm.IncusCharm._uninstall_packages"),
patch("charm.incus.set_config") as set_config,
patch("charm.incus.is_clustered", return_value=True),
patch("charm.incus.get_cluster_member_info"),
):
cluster_relation = scenario.PeerRelation(
endpoint="cluster",
local_app_data={
"tokens": "{}",
"created-storage": "[]",
"cluster-certificate": "any-cluster-certificate",
},
)
ctx = scenario.Context(IncusCharm)
state = scenario.State(
leader=True,
relations={cluster_relation},
config={
"oidc-audience": "any-audience",
"oidc-claim": "any-claim",
"oidc-client-id": "any-client-id",
"oidc-issuer": "any-issuer",
"oidc-scopes": "any-scopes",
"openfga-api-token": "any-token",
"openfga-api-url": "any-url",
"openfga-store-id": "any-store-id",
},
)

ctx.run(ctx.on.config_changed(), state)

assert ctx.unit_status_history == [
scenario.UnknownStatus(),
scenario.MaintenanceStatus("Changing config"),
]

set_config.assert_has_calls(
[
call(
{
"oidc.audience": "any-audience",
"oidc.claim": "any-claim",
"oidc.client.id": "any-client-id",
"oidc.issuer": "any-issuer",
"oidc.scopes": "any-scopes",
"openfga.api.token": "any-token",
"openfga.api.url": "any-url",
"openfga.store.id": "any-store-id",
}
)
]
)

0 comments on commit 3092eca

Please sign in to comment.