diff --git a/charmcraft.yaml b/charmcraft.yaml index 533f853..ba93782 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -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: diff --git a/src/charm.py b/src/charm.py index 40aca1f..f25182f 100755 --- a/src/charm.py +++ b/src/charm.py @@ -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 @@ -273,12 +281,12 @@ 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 @@ -286,7 +294,7 @@ def on_config_changed(self, event: ops.ConfigChangedEvent): 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: @@ -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. diff --git a/src/incus.py b/src/incus.py index 3a6fafe..5d2439c 100644 --- a/src/incus.py +++ b/src/incus.py @@ -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: diff --git a/tests/unit/test_config_changed.py b/tests/unit/test_config_changed.py index 4e7b945..df6baa6 100644 --- a/tests/unit/test_config_changed.py +++ b/tests/unit/test_config_changed.py @@ -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}"}), ] ) @@ -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}"}), ] ) @@ -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}"}), ] ) @@ -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}"}), ] ) @@ -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", + } + ) + ] + )