From 9df51e82d5d581f307379282727dd8d811d47d87 Mon Sep 17 00:00:00 2001 From: smndtrl Date: Fri, 6 Dec 2024 14:35:15 +0000 Subject: [PATCH] initial workload identity api --- .devcontainer/devcontainer.json | 34 ++ Cargo.lock | 75 ++++- Cargo.toml | 1 + golem-common/Cargo.toml | 1 + golem-common/src/config.rs | 34 ++ golem-worker-executor-base/Cargo.toml | 7 +- golem-worker-executor-base/build.rs | 1 + .../src/durable_host/golem/v11.rs | 43 ++- .../src/durable_host/mod.rs | 6 + golem-worker-executor-base/src/lib.rs | 7 + .../src/services/golem_config.rs | 3 + .../src/services/mod.rs | 22 ++ .../src/services/rpc.rs | 12 + .../src/services/worker_identity.rs | 105 ++++++ golem-worker-executor-base/src/worker.rs | 3 +- golem-worker-executor-base/src/workerctx.rs | 2 + .../tests/common/mod.rs | 17 +- golem-worker-executor-base/tests/identity.rs | 75 +++++ golem-worker-executor-base/tests/lib.rs | 2 + .../config/worker-executor.toml | 7 + golem-worker-executor/src/context.rs | 3 + golem-worker-executor/src/lib.rs | 5 + golem-worker-service-base/Cargo.toml | 5 + golem-worker-service-base/src/app_config.rs | 7 +- golem-worker-service-base/src/service/mod.rs | 1 + .../src/service/worker_identity.rs | 86 +++++ golem-worker-service/Cargo.toml | 2 + .../config/worker-service.toml | 8 + golem-worker-service/src/api/identity.rs | 314 ++++++++++++++++++ golem-worker-service/src/api/mod.rs | 6 + golem-worker-service/src/service/mod.rs | 15 +- .../src/service/worker_identity.rs | 5 + test-components/build-components.sh | 2 +- test-components/identity.wasm | Bin 0 -> 46733 bytes test-components/identity/Cargo.lock | 25 ++ test-components/identity/Cargo.toml | 22 ++ test-components/identity/src/bindings.rs | 192 +++++++++++ test-components/identity/src/lib.rs | 13 + .../identity/wit/deps/identity/world.wit | 7 + test-components/identity/wit/rust-echo.wit | 11 + 40 files changed, 1173 insertions(+), 13 deletions(-) create mode 100644 .devcontainer/devcontainer.json create mode 100644 golem-worker-executor-base/src/services/worker_identity.rs create mode 100644 golem-worker-executor-base/tests/identity.rs create mode 100644 golem-worker-service-base/src/service/worker_identity.rs create mode 100644 golem-worker-service/src/api/identity.rs create mode 100644 golem-worker-service/src/service/worker_identity.rs create mode 100644 test-components/identity.wasm create mode 100644 test-components/identity/Cargo.lock create mode 100644 test-components/identity/Cargo.toml create mode 100644 test-components/identity/src/bindings.rs create mode 100644 test-components/identity/src/lib.rs create mode 100644 test-components/identity/wit/deps/identity/world.wit create mode 100644 test-components/identity/wit/rust-echo.wit diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..b786c6bdd --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,34 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/rust +{ + "name": "Rust", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/rust:1-1-bookworm", + "features": { + "ghcr.io/devcontainers/features/docker-in-docker:2": {} + } + + // Use 'mounts' to make the cargo cache persistent in a Docker Volume. + // "mounts": [ + // { + // "source": "devcontainer-cargo-cache-${devcontainerId}", + // "target": "/usr/local/cargo", + // "type": "volume" + // } + // ] + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "rustc --version", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/Cargo.lock b/Cargo.lock index b987d48b4..4f0b7d141 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3865,6 +3865,7 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", + "base64 0.22.1", "bincode", "bytes 1.9.0", "chrono", @@ -4045,7 +4046,7 @@ dependencies = [ "derive_more 0.99.18", "dir-diff", "fancy-regex", - "golem-wit", + "golem-wit 1.1.0-rc1", "include_dir", "once_cell", "regex", @@ -4327,6 +4328,10 @@ dependencies = [ "wit-parser 0.221.2", ] +[[package]] +name = "golem-wit" +version = "0.0.0" + [[package]] name = "golem-wit" version = "1.1.0-rc1" @@ -4379,6 +4384,7 @@ dependencies = [ "async-trait", "aws-config", "aws-sdk-s3", + "base64 0.22.1", "bincode", "bitflags 2.6.0", "bytes 1.9.0", @@ -4406,7 +4412,7 @@ dependencies = [ "golem-test-framework", "golem-wasm-ast", "golem-wasm-rpc", - "golem-wit", + "golem-wit 0.0.0", "hex", "http 0.2.12", "http 1.2.0", @@ -4417,6 +4423,7 @@ dependencies = [ "io-extras", "iso8601-timestamp", "itertools 0.13.0", + "jsonwebtoken", "lazy_static 1.5.0", "log 0.4.22", "md5", @@ -4428,6 +4435,7 @@ dependencies = [ "prost 0.12.6", "rand", "redis", + "ring", "ringbuf", "rustls 0.23.19", "serde 1.0.215", @@ -4464,6 +4472,7 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", + "base64 0.22.1", "bincode", "bytes 1.9.0", "console-subscriber", @@ -4482,6 +4491,7 @@ dependencies = [ "http 1.2.0", "humantime-serde", "hyper 1.5.1", + "jsonwebkey", "lazy_static 1.5.0", "nom 7.1.3", "openapiv3", @@ -4517,6 +4527,7 @@ version = "0.0.0" dependencies = [ "anyhow", "async-trait", + "base64 0.22.1", "bigdecimal", "bincode", "bytes 1.9.0", @@ -4524,6 +4535,7 @@ dependencies = [ "conditional-trait-gen", "criterion", "derive_more 0.99.18", + "ecdsa 0.16.9", "fastrand 2.2.0", "figment", "fred", @@ -4539,6 +4551,8 @@ dependencies = [ "http 1.2.0", "humantime-serde", "hyper 1.5.1", + "jsonwebkey", + "jsonwebtoken", "lazy_static 1.5.0", "mime_guess", "nom 7.1.3", @@ -4547,6 +4561,7 @@ dependencies = [ "opentelemetry 0.24.0", "opentelemetry-prometheus", "opentelemetry_sdk", + "p256 0.13.2", "poem", "poem-openapi", "prometheus", @@ -5649,6 +5664,36 @@ dependencies = [ "serde_json", ] +[[package]] +name = "jsonwebkey" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57c852b14147e2bd58c14fde40398864453403ef632b1101db130282ee6e2cc" +dependencies = [ + "base64 0.13.1", + "bitflags 1.3.2", + "generic-array 0.14.7", + "serde 1.0.215", + "serde_json", + "thiserror", + "zeroize", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde 1.0.215", + "serde_json", + "simple_asn1", +] + [[package]] name = "k8s-openapi" version = "0.22.0" @@ -9019,6 +9064,18 @@ dependencies = [ "similar", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits 0.2.19", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -12437,6 +12494,20 @@ name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.90", +] [[package]] name = "zerovec" diff --git a/Cargo.toml b/Cargo.toml index d32402b82..d16f5c7bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,7 @@ exclude = [ "test-components/golem-rust-tests", "test-components/http-client", "test-components/http-client-2", + "test-components/identity", "test-components/interruption", "test-components/initial-file-read-write", "test-components/key-value-service", diff --git a/golem-common/Cargo.toml b/golem-common/Cargo.toml index 9f862a05c..87624c610 100644 --- a/golem-common/Cargo.toml +++ b/golem-common/Cargo.toml @@ -58,6 +58,7 @@ typed-path = { workspace = true } url = { workspace = true } uuid = { workspace = true } wasm-wave = { workspace = true } +base64 = "0.22.1" [dev-dependencies] anyhow = { workspace = true } diff --git a/golem-common/src/config.rs b/golem-common/src/config.rs index 580c04a5d..83c4d6d19 100644 --- a/golem-common/src/config.rs +++ b/golem-common/src/config.rs @@ -467,3 +467,37 @@ pub struct DbPostgresConfig { pub max_connections: u32, pub schema: Option, } + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WorkerIdentityConfig { + pub set: Vec, + pub active_keys: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct WorkerIdentityKey { + pub alg: String, + pub kid: String, + #[serde(deserialize_with = "base64_to_vec")] + pub der: Vec, +} + +impl Default for WorkerIdentityConfig { + fn default() -> Self { + Self { + set: vec![], + active_keys: vec![], + } + } +} + +use serde::Deserializer; + +/// Custom deserializer to decode a Base64 string into `Vec` +fn base64_to_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; // Deserialize as a string + base64::decode(&s).map_err(serde::de::Error::custom) // Decode Base64 into Vec +} diff --git a/golem-worker-executor-base/Cargo.toml b/golem-worker-executor-base/Cargo.toml index 6ba685cdb..cfd4ac660 100644 --- a/golem-worker-executor-base/Cargo.toml +++ b/golem-worker-executor-base/Cargo.toml @@ -49,7 +49,7 @@ fs-set-times = "0.20.1" futures = { workspace = true } futures-util = { workspace = true } gethostname = "0.4.3" -golem-wit = { version = "=1.1.0-rc1" } +golem-wit = { path = "../../golem-wit" } hex = { workspace = true } http = { workspace = true } http_02 = { workspace = true } @@ -68,9 +68,11 @@ nonempty-collections = "0.2.5" prometheus = { workspace = true } prost = { workspace = true } rand = { workspace = true } +ring = "0.17" +base64 = "*" ringbuf = "0.4.1" rustls = { workspace = true } -serde = { workspace = true } +serde = { workspace = true, features = [ "derive" ] } serde_json = { workspace = true } sysinfo = "0.30.12" tempfile = { workspace = true } @@ -92,6 +94,7 @@ wasmtime-wasi-http = { workspace = true } windows-sys = "0.52.0" zstd = "0.13" sqlx = { workspace = true } +jsonwebtoken = "9.3.0" [dev-dependencies] golem-test-framework = { path = "../golem-test-framework", version = "0.0.0" } diff --git a/golem-worker-executor-base/build.rs b/golem-worker-executor-base/build.rs index 454d3626a..22692b56f 100644 --- a/golem-worker-executor-base/build.rs +++ b/golem-worker-executor-base/build.rs @@ -63,6 +63,7 @@ fn preview2_mod_gen(golem_wit_path: &str) -> String { import golem:api/host@0.2.0; import golem:api/host@1.1.0; import golem:api/oplog@1.1.0; + import golem:api/identity@1.1.0; import wasi:blobstore/blobstore; import wasi:blobstore/container; diff --git a/golem-worker-executor-base/src/durable_host/golem/v11.rs b/golem-worker-executor-base/src/durable_host/golem/v11.rs index 15d5685eb..dbac2f072 100644 --- a/golem-worker-executor-base/src/durable_host/golem/v11.rs +++ b/golem-worker-executor-base/src/durable_host/golem/v11.rs @@ -27,19 +27,60 @@ use crate::preview2::golem::api1_1_0::host::{ WorkerMetadata, WorkerNameFilter, WorkerPropertyFilter, WorkerStatus, WorkerStatusFilter, WorkerVersionFilter, }; +use crate::preview2::golem::api1_1_0::identity::Host as IdentityHost; use crate::preview2::golem::api1_1_0::oplog::{ Host as OplogHost, HostGetOplog, HostSearchOplog, OplogEntry, SearchOplog, }; +use crate::services::worker_identity::WorkerClaims; use crate::services::{HasOplogService, HasPlugins}; use crate::workerctx::WorkerCtx; -use anyhow::anyhow; +use anyhow::{anyhow, Context}; use async_trait::async_trait; use golem_common::config::RetryConfig; use golem_common::model::OwnedWorkerId; +use serde::{Deserialize, Serialize}; use std::time::Duration; use wasmtime::component::Resource; use wasmtime_wasi::WasiView; +#[async_trait] +impl IdentityHost for DurableWorkerCtx { + async fn get_token(&mut self) -> anyhow::Result> { + let my_claims = WorkerClaims { + account_id: self.state.owned_worker_id.account_id.value.clone(), + component_id: self + .state + .owned_worker_id + .worker_id + .component_id + .to_string(), + worker_name: self.state.owned_worker_id.worker_id.worker_name.clone(), + }; + let token = self.state.worker_identity_service.sign(my_claims).await?; + + Ok(Some(token)) + } +} + +#[async_trait] +impl IdentityHost for &mut DurableWorkerCtx { + async fn get_token(&mut self) -> anyhow::Result> { + let my_claims = WorkerClaims { + account_id: self.state.owned_worker_id.account_id.value.clone(), + component_id: self + .state + .owned_worker_id + .worker_id + .component_id + .to_string(), + worker_name: self.state.owned_worker_id.worker_id.worker_name.clone(), + }; + let token = self.state.worker_identity_service.sign(my_claims).await?; + + Ok(Some(token)) + } +} + #[async_trait] impl HostGetWorkers for DurableWorkerCtx { async fn new( diff --git a/golem-worker-executor-base/src/durable_host/mod.rs b/golem-worker-executor-base/src/durable_host/mod.rs index 540af0570..e2b01e00a 100644 --- a/golem-worker-executor-base/src/durable_host/mod.rs +++ b/golem-worker-executor-base/src/durable_host/mod.rs @@ -40,6 +40,7 @@ use crate::services::rpc::Rpc; use crate::services::scheduler::SchedulerService; use crate::services::worker::WorkerService; use crate::services::worker_event::WorkerEventService; +use crate::services::worker_identity::WorkerIdentityService; use crate::services::worker_proxy::WorkerProxy; use crate::services::{worker_enumeration, HasAll, HasConfig, HasOplog, HasWorker}; use crate::services::{HasOplogService, HasPlugins}; @@ -142,6 +143,7 @@ impl DurableWorkerCtx { component_metadata: ComponentMetadata, promise_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc< dyn worker_enumeration::WorkerEnumerationService + Send + Sync, >, @@ -221,6 +223,7 @@ impl DurableWorkerCtx { promise_service, scheduler_service, worker_service, + worker_identity_service, worker_enumeration_service, key_value_service, blob_store_service, @@ -1751,6 +1754,7 @@ pub struct PrivateDurableWorkerState { promise_service: Arc, scheduler_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc, key_value_service: Arc, blob_store_service: Arc, @@ -1788,6 +1792,7 @@ impl PrivateDurableWorkerState, scheduler_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc< dyn worker_enumeration::WorkerEnumerationService + Send + Sync, >, @@ -1818,6 +1823,7 @@ impl PrivateDurableWorkerState { component_service: Arc, shard_manager_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc, running_worker_enumeration_service: Arc, promise_service: Arc, @@ -450,6 +452,10 @@ pub trait Bootstrap { plugins.clone(), )); + let worker_identity_service = Arc::new(DefaultWorkerIdentityService::new( + golem_config.worker_identity.clone(), + )); + let worker_service = Arc::new(DefaultWorkerService::new( key_value_storage.clone(), shard_service.clone(), @@ -480,6 +486,7 @@ pub trait Bootstrap { component_service, shard_manager_service, worker_service, + worker_identity_service, worker_enumeration_service, running_worker_enumeration_service, promise_service, diff --git a/golem-worker-executor-base/src/services/golem_config.rs b/golem-worker-executor-base/src/services/golem_config.rs index 35dc1ab6c..a08c31a8a 100644 --- a/golem-worker-executor-base/src/services/golem_config.rs +++ b/golem-worker-executor-base/src/services/golem_config.rs @@ -26,6 +26,7 @@ use url::Url; use golem_common::config::{ ConfigExample, ConfigLoader, DbSqliteConfig, HasConfigExamples, RedisConfig, RetryConfig, + WorkerIdentityConfig, }; use golem_common::tracing::TracingConfig; @@ -44,6 +45,7 @@ pub struct GolemConfig { pub compiled_component_service: CompiledComponentServiceConfig, pub shard_manager_service: ShardManagerServiceConfig, pub plugin_service: PluginServiceConfig, + pub worker_identity: WorkerIdentityConfig, pub oplog: OplogConfig, pub suspend: SuspendConfig, pub active_workers: ActiveWorkersConfig, @@ -354,6 +356,7 @@ impl Default for GolemConfig { suspend: SuspendConfig::default(), scheduler: SchedulerConfig::default(), active_workers: ActiveWorkersConfig::default(), + worker_identity: WorkerIdentityConfig::default(), public_worker_api: WorkerServiceGrpcConfig::default(), memory: MemoryConfig::default(), grpc_address: "0.0.0.0".to_string(), diff --git a/golem-worker-executor-base/src/services/mod.rs b/golem-worker-executor-base/src/services/mod.rs index c483711bb..fa5305134 100644 --- a/golem-worker-executor-base/src/services/mod.rs +++ b/golem-worker-executor-base/src/services/mod.rs @@ -42,6 +42,7 @@ pub mod worker; pub mod worker_activator; pub mod worker_enumeration; pub mod worker_event; +pub mod worker_identity; pub mod worker_proxy; // HasXXX traits for fine-grained control of which dependencies a function needs @@ -147,6 +148,12 @@ pub trait HasOplogProcessorPlugin { fn oplog_processor_plugin(&self) -> Arc; } +pub trait HasWorkerIdentity { + fn worker_identity_service( + &self, + ) -> Arc; +} + /// HasAll is a shortcut for requiring all available service dependencies pub trait HasAll: HasActiveWorkers @@ -170,6 +177,7 @@ pub trait HasAll: + HasFileLoader + HasPlugins<::PluginOwner, Ctx::PluginScope> + HasOplogProcessorPlugin + + HasWorkerIdentity + HasExtraDeps + Clone { @@ -198,6 +206,7 @@ impl< + HasFileLoader + HasPlugins<::PluginOwner, Ctx::PluginScope> + HasOplogProcessorPlugin + + HasWorkerIdentity + HasExtraDeps + Clone, > HasAll for T @@ -235,6 +244,7 @@ pub struct All { + Sync, >, oplog_processor_plugin: Arc, + worker_identity_service: Arc, extra_deps: Ctx::ExtraDeps, } @@ -264,6 +274,7 @@ impl Clone for All { file_loader: self.file_loader.clone(), plugins: self.plugins.clone(), oplog_processor_plugin: self.oplog_processor_plugin.clone(), + worker_identity_service: self.worker_identity_service.clone(), extra_deps: self.extra_deps.clone(), } } @@ -303,6 +314,7 @@ impl All { + Sync, >, oplog_processor_plugin: Arc, + worker_identity_service: Arc, extra_deps: Ctx::ExtraDeps, ) -> Self { Self { @@ -329,6 +341,7 @@ impl All { file_loader, plugins, oplog_processor_plugin, + worker_identity_service, extra_deps, } } @@ -358,6 +371,7 @@ impl All { this.file_loader(), this.plugins(), this.oplog_processor_plugin(), + this.worker_identity_service(), this.extra_deps(), ) } @@ -523,6 +537,14 @@ impl> HasOplogProcessorPlugin for T { } } +impl> HasWorkerIdentity for T { + fn worker_identity_service( + &self, + ) -> Arc { + self.all().worker_identity_service.clone() + } +} + impl> HasExtraDeps for T { fn extra_deps(&self) -> Ctx::ExtraDeps { self.all().extra_deps.clone() diff --git a/golem-worker-executor-base/src/services/rpc.rs b/golem-worker-executor-base/src/services/rpc.rs index ffd333f5f..86bb120e2 100644 --- a/golem-worker-executor-base/src/services/rpc.rs +++ b/golem-worker-executor-base/src/services/rpc.rs @@ -24,6 +24,8 @@ use tokio::runtime::Handle; use tracing::debug; use super::file_loader::FileLoader; +use super::worker_identity::WorkerIdentityService; +use super::HasWorkerIdentity; use crate::error::GolemError; use crate::services::events::Events; use crate::services::oplog::plugin::OplogProcessorPlugin; @@ -283,6 +285,7 @@ pub struct DirectWorkerInvocationRpc { + Sync, >, oplog_processor_plugin: Arc, + worker_identity_service: Arc, extra_deps: Ctx::ExtraDeps, } @@ -311,6 +314,7 @@ impl Clone for DirectWorkerInvocationRpc { file_loader: self.file_loader.clone(), plugins: self.plugins.clone(), oplog_processor_plugin: self.oplog_processor_plugin.clone(), + worker_identity_service: self.worker_identity_service.clone(), extra_deps: self.extra_deps.clone(), } } @@ -469,6 +473,12 @@ impl HasOplogProcessorPlugin for DirectWorkerInvocationRpc } } +impl HasWorkerIdentity for DirectWorkerInvocationRpc { + fn worker_identity_service(&self) -> Arc { + self.worker_identity_service.clone() + } +} + impl DirectWorkerInvocationRpc { #[allow(clippy::too_many_arguments)] pub fn new( @@ -502,6 +512,7 @@ impl DirectWorkerInvocationRpc { + Sync, >, oplog_processor_plugin: Arc, + worker_identity_service: Arc, extra_deps: Ctx::ExtraDeps, ) -> Self { Self { @@ -527,6 +538,7 @@ impl DirectWorkerInvocationRpc { file_loader, plugins, oplog_processor_plugin, + worker_identity_service, extra_deps, } } diff --git a/golem-worker-executor-base/src/services/worker_identity.rs b/golem-worker-executor-base/src/services/worker_identity.rs new file mode 100644 index 000000000..ffd13f703 --- /dev/null +++ b/golem-worker-executor-base/src/services/worker_identity.rs @@ -0,0 +1,105 @@ +use std::{collections::HashMap, sync::Arc}; + +use anyhow::{anyhow, Context}; +use async_trait::async_trait; + +use golem_common::config::WorkerIdentityConfig; +use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey}; +use ring::rand::SystemRandom; +use ring::signature::{EcdsaKeyPair, KeyPair}; +use serde::{Deserialize, Serialize}; +// use rsa::{RsaPrivateKey, RsaPublicKey, pkcs1::{EncodeRsaPrivateKey, EncodeRsaPublicKey}}; +use rand::rngs::OsRng; +use serde_json::json; + +/// Service implementing a persistent key-value store +#[async_trait] +pub trait WorkerIdentityService { + async fn sign(&self, claims: WorkerClaims) -> anyhow::Result; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WorkerClaims { + pub account_id: String, + pub component_id: String, + pub worker_name: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct Claims { + pub iss: String, // Issuer + pub sub: String, // Subject + pub aud: String, // Audience + pub exp: usize, // Expiration time (as a Unix timestamp) + pub nbf: usize, // Not before time (as a Unix timestamp) + pub iat: usize, // Issued at time (as a Unix timestamp) + pub jti: String, // Unique identifier for the token + #[serde(flatten)] + pub worker: WorkerClaims, +} + +impl Claims { + pub fn from_worker(claims: WorkerClaims) -> Self { + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("Time went backwards") + .as_secs() as usize; + + Self { + iss: "123".to_string(), + sub: "123".to_string(), + aud: "123".to_string(), + exp: now + 60, + nbf: now, + iat: now, + jti: "123".to_string(), + worker: claims, + } + } +} + +#[derive(Clone)] +pub struct DefaultWorkerIdentityService { + config: WorkerIdentityConfig, +} + +impl DefaultWorkerIdentityService { + pub fn new(config: WorkerIdentityConfig) -> Self { + Self { config } + } +} + +#[async_trait] +impl WorkerIdentityService for DefaultWorkerIdentityService { + async fn sign(&self, worker_claims: WorkerClaims) -> anyhow::Result { + let claims = Claims::from_worker(worker_claims); + + let mut active_keys = self + .config + .set + .iter() + .filter(|s| self.config.active_keys.contains(&s.kid)); + + let valid_key = active_keys.next().context("no valid keys found")?; + + let alg = match valid_key.alg.as_str() { + "ES256" => Algorithm::ES256, + "HS256" => Algorithm::HS256, + _ => todo!(), + }; + + let key = match alg { + Algorithm::ES256 => jsonwebtoken::EncodingKey::from_ec_der(&valid_key.der), + Algorithm::HS256 => jsonwebtoken::EncodingKey::from_secret(&valid_key.der), + _ => todo!(), + }; + + let mut header = jsonwebtoken::Header::new(alg); + header.kid = Some(valid_key.kid.clone()); + + let token = + jsonwebtoken::encode(&header, &claims, &key).context("token generation failed")?; + + return Ok(token); + } +} diff --git a/golem-worker-executor-base/src/worker.rs b/golem-worker-executor-base/src/worker.rs index b8aedec54..431531f60 100644 --- a/golem-worker-executor-base/src/worker.rs +++ b/golem-worker-executor-base/src/worker.rs @@ -35,7 +35,7 @@ use crate::services::{ All, HasActiveWorkers, HasAll, HasBlobStoreService, HasComponentService, HasConfig, HasEvents, HasExtraDeps, HasFileLoader, HasKeyValueService, HasOplog, HasOplogService, HasPlugins, HasPromiseService, HasRpc, HasSchedulerService, HasWasmtimeEngine, HasWorker, - HasWorkerEnumerationService, HasWorkerProxy, HasWorkerService, UsesAllDeps, + HasWorkerEnumerationService, HasWorkerIdentity, HasWorkerProxy, HasWorkerService, UsesAllDeps, }; use crate::workerctx::{PublicWorkerIo, WorkerCtx}; use anyhow::anyhow; @@ -1322,6 +1322,7 @@ impl RunningWorker { component_metadata, parent.promise_service(), parent.worker_service(), + parent.worker_identity_service(), parent.worker_enumeration_service(), parent.key_value_service(), parent.blob_store_service(), diff --git a/golem-worker-executor-base/src/workerctx.rs b/golem-worker-executor-base/src/workerctx.rs index a46ac4cf1..9021a68cb 100644 --- a/golem-worker-executor-base/src/workerctx.rs +++ b/golem-worker-executor-base/src/workerctx.rs @@ -39,6 +39,7 @@ use crate::services::rpc::Rpc; use crate::services::scheduler::SchedulerService; use crate::services::worker::WorkerService; use crate::services::worker_event::WorkerEventService; +use crate::services::worker_identity::WorkerIdentityService; use crate::services::worker_proxy::WorkerProxy; use crate::services::{ worker_enumeration, HasAll, HasConfig, HasOplog, HasOplogService, HasWorker, @@ -106,6 +107,7 @@ pub trait WorkerCtx: component_metadata: ComponentMetadata, promise_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc< dyn worker_enumeration::WorkerEnumerationService + Send + Sync, >, diff --git a/golem-worker-executor-base/tests/common/mod.rs b/golem-worker-executor-base/tests/common/mod.rs index 0d5d83f1d..68240d27c 100644 --- a/golem-worker-executor-base/tests/common/mod.rs +++ b/golem-worker-executor-base/tests/common/mod.rs @@ -48,6 +48,7 @@ use golem_worker_executor_base::services::shard_manager::ShardManagerService; use golem_worker_executor_base::services::worker::WorkerService; use golem_worker_executor_base::services::worker_activator::WorkerActivator; use golem_worker_executor_base::services::worker_event::WorkerEventService; +use golem_worker_executor_base::services::worker_identity::WorkerIdentityService; use golem_worker_executor_base::services::{plugins, All, HasAll, HasConfig, HasOplogService}; use golem_worker_executor_base::wasi_host::create_linker; use golem_worker_executor_base::workerctx::{ @@ -61,7 +62,7 @@ use tokio::runtime::Handle; use tokio::task::JoinSet; use golem::api0_2_0; -use golem_common::config::RedisConfig; +use golem_common::config::{RedisConfig, WorkerIdentityConfig, WorkerIdentityKey}; use golem_api_grpc::proto::golem::workerexecutor::v1::{ get_running_workers_metadata_response, get_workers_metadata_response, @@ -327,6 +328,14 @@ pub async fn start_limited( system_memory_override, ..Default::default() }, + worker_identity: WorkerIdentityConfig { + active_keys: vec!["1".to_string()], + set: vec![WorkerIdentityKey { + alg: "HS256".to_string(), + kid: "1".to_string(), + der: "secret".as_bytes().to_vec(), + }], + }, ..Default::default() }; @@ -634,6 +643,7 @@ impl WorkerCtx for TestWorkerCtx { component_metadata: ComponentMetadata, promise_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc, key_value_service: Arc, blob_store_service: Arc, @@ -662,6 +672,7 @@ impl WorkerCtx for TestWorkerCtx { component_metadata, promise_service, worker_service, + worker_identity_service, worker_enumeration_service, key_value_service, blob_store_service, @@ -800,6 +811,7 @@ impl Bootstrap for ServerBootstrap { component_service: Arc, shard_manager_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc, running_worker_enumeration_service: Arc, promise_service: Arc, @@ -842,6 +854,7 @@ impl Bootstrap for ServerBootstrap { file_loader.clone(), plugins.clone(), oplog_processor_plugin.clone(), + worker_identity_service.clone(), (), )); Ok(All::new( @@ -868,6 +881,7 @@ impl Bootstrap for ServerBootstrap { file_loader, plugins, oplog_processor_plugin, + worker_identity_service, (), )) } @@ -876,6 +890,7 @@ impl Bootstrap for ServerBootstrap { let mut linker = create_linker(engine, get_durable_ctx)?; api0_2_0::host::add_to_linker_get_host(&mut linker, get_durable_ctx)?; api1_1_0::host::add_to_linker_get_host(&mut linker, get_durable_ctx)?; + api1_1_0::identity::add_to_linker_get_host(&mut linker, get_durable_ctx)?; golem_wasm_rpc::golem::rpc::types::add_to_linker_get_host(&mut linker, get_durable_ctx)?; Ok(linker) } diff --git a/golem-worker-executor-base/tests/identity.rs b/golem-worker-executor-base/tests/identity.rs new file mode 100644 index 000000000..55993eff7 --- /dev/null +++ b/golem-worker-executor-base/tests/identity.rs @@ -0,0 +1,75 @@ +// Copyright 2024 Golem Cloud +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jsonwebtoken::{Algorithm, Validation}; +use test_r::{inherit_test_dep, test}; + +use crate::common::{start, TestContext}; +use crate::{LastUniqueId, Tracing, WorkerExecutorTestDependencies}; +use assert2::check; +use golem_test_framework::dsl::TestDslUnsafe; +use golem_wasm_rpc::Value; +use golem_worker_executor_base::services::worker_identity::{Claims, WorkerClaims}; + +inherit_test_dep!(WorkerExecutorTestDependencies); +inherit_test_dep!(LastUniqueId); +inherit_test_dep!(Tracing); + +#[test] +#[tracing::instrument] +async fn get_identity( + last_unique_id: &LastUniqueId, + deps: &WorkerExecutorTestDependencies, + _tracing: &Tracing, +) { + let context = TestContext::new(last_unique_id); + let executor = start(deps, &context).await.unwrap(); + + let component_id = executor.store_component("identity").await; + let worker_name = "identity-1"; + let worker_id = executor.start_worker(&component_id, worker_name).await; + + let token = executor + .invoke_and_await( + &worker_id, + "golem:it/api.{echo}", + vec![Value::String("hello".to_string())], + ) + .await + .unwrap(); + + let Value::String(jwt) = token.first().unwrap() else { + panic!() + }; + + drop(executor); + + tracing::debug!("jwt {}", jwt); + + let mut validation = Validation::new(Algorithm::HS256); + validation.leeway = 5; + validation.validate_nbf = true; + validation.set_audience(&["123"]); // a single string + validation.set_issuer(&["123"]); // a single string + + let decoded = jsonwebtoken::decode::( + jwt, + &jsonwebtoken::DecodingKey::from_secret("secret".as_ref()), + &validation, + ) + .expect("decoding failure"); + + assert_eq!(decoded.claims.worker.component_id, component_id.to_string()); + assert_eq!(decoded.claims.worker.worker_name, worker_name.to_string()); +} diff --git a/golem-worker-executor-base/tests/lib.rs b/golem-worker-executor-base/tests/lib.rs index e5adf099d..89ca87640 100644 --- a/golem-worker-executor-base/tests/lib.rs +++ b/golem-worker-executor-base/tests/lib.rs @@ -50,6 +50,7 @@ pub mod guest_languages1; pub mod guest_languages2; pub mod guest_languages3; pub mod hot_update; +pub mod identity; pub mod indexed_storage; pub mod key_value_storage; pub mod keyvalue; @@ -67,6 +68,7 @@ test_r::enable!(); tag_suite!(api, group1); tag_suite!(blobstore, group1); tag_suite!(keyvalue, group1); +tag_suite!(identity, group1); tag_suite!(guest_languages1, group2); diff --git a/golem-worker-executor/config/worker-executor.toml b/golem-worker-executor/config/worker-executor.toml index a5272949b..b04e6bfc3 100644 --- a/golem-worker-executor/config/worker-executor.toml +++ b/golem-worker-executor/config/worker-executor.toml @@ -170,6 +170,13 @@ span_events_active = false span_events_full = false without_time = false +[worker_identity] +active_keys = ["1"] + +[[worker_identity.set]] +kid = "1" +alg = "ES256" +der = "MHcCAQEEIFJwEp6moU1kCBNRBogEmh2P5cB/kyUkFAzKt3VdMxD5oAoGCCqGSM49AwEHoUQDQgAE4O1X2mxySsWcf5cy7hsg+aaKCV9C5FEt8Tv0Wr4cDPcsCCKxHnAqwtUGQwr9WQ8Tpbut0r3had+Xg+UIE+OuGw==" ## Generated from example config: with redis indexed_storage, s3 blob storage, single shard manager service # grpc_address = "0.0.0.0" diff --git a/golem-worker-executor/src/context.rs b/golem-worker-executor/src/context.rs index b83fc81ab..a5e8ff282 100644 --- a/golem-worker-executor/src/context.rs +++ b/golem-worker-executor/src/context.rs @@ -47,6 +47,7 @@ use golem_worker_executor_base::services::rpc::Rpc; use golem_worker_executor_base::services::scheduler::SchedulerService; use golem_worker_executor_base::services::worker::WorkerService; use golem_worker_executor_base::services::worker_event::WorkerEventService; +use golem_worker_executor_base::services::worker_identity::WorkerIdentityService; use golem_worker_executor_base::services::worker_proxy::WorkerProxy; use golem_worker_executor_base::services::{ worker_enumeration, HasAll, HasConfig, HasOplogService, @@ -294,6 +295,7 @@ impl WorkerCtx for Context { component_metadata: ComponentMetadata, promise_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc< dyn worker_enumeration::WorkerEnumerationService + Send + Sync, >, @@ -324,6 +326,7 @@ impl WorkerCtx for Context { component_metadata, promise_service, worker_service, + worker_identity_service, worker_enumeration_service, key_value_service, blob_store_service, diff --git a/golem-worker-executor/src/lib.rs b/golem-worker-executor/src/lib.rs index 2ca77341d..39764013a 100644 --- a/golem-worker-executor/src/lib.rs +++ b/golem-worker-executor/src/lib.rs @@ -44,6 +44,7 @@ use golem_worker_executor_base::services::worker_activator::WorkerActivator; use golem_worker_executor_base::services::worker_enumeration::{ RunningWorkerEnumerationService, WorkerEnumerationService, }; +use golem_worker_executor_base::services::worker_identity::WorkerIdentityService; use golem_worker_executor_base::services::worker_proxy::WorkerProxy; use golem_worker_executor_base::services::{plugins, All}; use golem_worker_executor_base::wasi_host::create_linker; @@ -92,6 +93,7 @@ impl Bootstrap for ServerBootstrap { component_service: Arc, shard_manager_service: Arc, worker_service: Arc, + worker_identity_service: Arc, worker_enumeration_service: Arc, running_worker_enumeration_service: Arc, promise_service: Arc, @@ -136,6 +138,7 @@ impl Bootstrap for ServerBootstrap { file_loader.clone(), plugins.clone(), oplog_processor_plugin.clone(), + worker_identity_service.clone(), additional_deps.clone(), )); @@ -163,6 +166,7 @@ impl Bootstrap for ServerBootstrap { file_loader.clone(), plugins.clone(), oplog_processor_plugin, + worker_identity_service, additional_deps, )) } @@ -171,6 +175,7 @@ impl Bootstrap for ServerBootstrap { let mut linker = create_linker(engine, get_durable_ctx)?; api0_2_0::host::add_to_linker_get_host(&mut linker, get_durable_ctx)?; api1_1_0::host::add_to_linker_get_host(&mut linker, get_durable_ctx)?; + api1_1_0::identity::add_to_linker_get_host(&mut linker, get_durable_ctx)?; golem_wasm_rpc::golem::rpc::types::add_to_linker_get_host(&mut linker, get_durable_ctx)?; Ok(linker) } diff --git a/golem-worker-service-base/Cargo.toml b/golem-worker-service-base/Cargo.toml index 55d7ce6aa..ea534eb15 100644 --- a/golem-worker-service-base/Cargo.toml +++ b/golem-worker-service-base/Cargo.toml @@ -28,6 +28,7 @@ golem-wasm-rpc = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } +base64 = "*" bincode = { workspace = true } bigdecimal = { workspace = true } bytes = { workspace = true } @@ -47,6 +48,7 @@ mime_guess = "2.0.5" nom = { workspace = true } openapiv3 = { workspace = true } openidconnect = { workspace = true } +jsonwebtoken = "*" opentelemetry = { workspace = true } opentelemetry-prometheus = { workspace = true } opentelemetry_sdk = { workspace = true } @@ -84,6 +86,9 @@ tracing-subscriber = { workspace = true } url = { workspace = true } uuid = { workspace = true } wasm-wave = { workspace = true } +ecdsa = "0.16.9" +jsonwebkey = "0.3.5" +p256 = "0.13.2" [dev-dependencies] criterion = { version = "0.3", features = ["html_reports"] } diff --git a/golem-worker-service-base/src/app_config.rs b/golem-worker-service-base/src/app_config.rs index 9a0754482..41f3da042 100644 --- a/golem-worker-service-base/src/app_config.rs +++ b/golem-worker-service-base/src/app_config.rs @@ -17,11 +17,14 @@ use std::time::Duration; use golem_service_base::config::BlobStorageConfig; use http::Uri; +use openidconnect::JsonWebKey; use serde::{Deserialize, Serialize}; use url::Url; use uuid::Uuid; -use golem_common::config::{ConfigExample, HasConfigExamples, RedisConfig, RetryConfig}; +use golem_common::config::{ + ConfigExample, HasConfigExamples, RedisConfig, RetryConfig, WorkerIdentityConfig, +}; use golem_common::config::{DbConfig, DbSqliteConfig}; use golem_common::tracing::TracingConfig; use golem_service_base::service::routing_table::RoutingTableConfig; @@ -42,6 +45,7 @@ pub struct WorkerServiceBaseConfig { pub routing_table: RoutingTableConfig, pub worker_executor_retries: RetryConfig, pub blob_storage: BlobStorageConfig, + pub worker_identity: WorkerIdentityConfig, } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -91,6 +95,7 @@ impl Default for WorkerServiceBaseConfig { max_jitter_factor: Some(0.15), }, blob_storage: BlobStorageConfig::default(), + worker_identity: WorkerIdentityConfig::default(), } } } diff --git a/golem-worker-service-base/src/service/mod.rs b/golem-worker-service-base/src/service/mod.rs index d73cb50d6..f6eece347 100644 --- a/golem-worker-service-base/src/service/mod.rs +++ b/golem-worker-service-base/src/service/mod.rs @@ -15,6 +15,7 @@ pub mod component; pub mod gateway; pub mod worker; +pub mod worker_identity; pub fn with_metadata(request: T, metadata: I) -> tonic::Request where diff --git a/golem-worker-service-base/src/service/worker_identity.rs b/golem-worker-service-base/src/service/worker_identity.rs new file mode 100644 index 000000000..f8b19eff6 --- /dev/null +++ b/golem-worker-service-base/src/service/worker_identity.rs @@ -0,0 +1,86 @@ +use async_trait::async_trait; +use golem_common::config::WorkerIdentityConfig; +use jsonwebkey::ByteArray; +use jsonwebkey::JsonWebKey; +use poem::web::Json; +// use openidconnect::jwk::{Jwk, RsaKey, EcKey, HmacKey}; +use anyhow::anyhow; +use anyhow::Result; +use rsa::{pkcs1::DecodeRsaPublicKey, RsaPublicKey}; + +#[async_trait] +pub trait WorkerIdentityService { + async fn get_jwks(&self) -> Result>; +} + +pub struct WorkerIdentityServiceDefault { + config: WorkerIdentityConfig, +} + +impl WorkerIdentityServiceDefault { + pub fn new(config: WorkerIdentityConfig) -> Self { + Self { config } + } +} + +#[async_trait] +impl WorkerIdentityService for WorkerIdentityServiceDefault { + async fn get_jwks(&self) -> Result> { + self.config + .set + .iter() + .map(|a| { + let der_encoded_key = &a.der; + match &a.alg { + // "RS256" | "RS384" | "RS512" => { + // // Parse RSA public key + // let public_key = RsaPublicKey::from_pkcs1_der(&der_encoded_key)?; + // let jwk = RsaKey::from(&public_key); + // let jwk_json = serde_json::to_string(&jwk)?; + // Ok(jwk_json) + // }, + "ES256" => { + // Parse the DER-encoded private key into a SigningKey (which internally contains the private key 'd') + let signing_key = p256::SecretKey::from_sec1_der(&der_encoded_key)?; + let public_key = signing_key.public_key(); + let pub_key_bytes = public_key.to_sec1_bytes(); + let (x, y) = split_sec1_public_key(&pub_key_bytes).unwrap(); // P256 public keys are 64 bytes (x and y each 32 bytes) + + let mut jwk = JsonWebKey::new(jsonwebkey::Key::EC { + curve: { + jsonwebkey::Curve::P256 { + d: None, + x: ByteArray::from_slice(x), + y: ByteArray::from_slice(y), + } + }, + }); + jwk.key_id = Some(a.kid.clone()); + let _ = jwk.set_algorithm(a.alg); + + Ok(jwk) + } + // "HS256" | "HS384" | "HS512" => { + // // For HMAC keys, you'd need a secret key (e.g., from a shared secret) + // let hmac_key = HmacKey::from(&der_encoded_key); + // let jwk_json = serde_json::to_string(&hmac_key)?; + // Ok(jwk_json) + // }, + _ => Err(anyhow!("Unsupported algorithm")), // Handle other algorithms or errors + } + }) + .collect() + } +} + +fn split_sec1_public_key(sec1_key: &[u8]) -> Result<(Vec, Vec), String> { + if sec1_key.len() != 65 || sec1_key[0] != 0x04 { + return Err("Invalid SEC1 public key format".to_string()); + } + + // Extract x and y coordinates + let x = sec1_key[1..33].to_vec(); // Bytes 1 to 32 (x-coordinate) + let y = sec1_key[33..65].to_vec(); // Bytes 33 to 64 (y-coordinate) + + Ok((x, y)) +} diff --git a/golem-worker-service/Cargo.toml b/golem-worker-service/Cargo.toml index 841160e2a..f1c9bc0b5 100644 --- a/golem-worker-service/Cargo.toml +++ b/golem-worker-service/Cargo.toml @@ -65,6 +65,8 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true } uuid = { workspace = true } +jsonwebkey = "0.3.5" +base64 = "0.22.1" [dev-dependencies] test-r = { workspace = true } diff --git a/golem-worker-service/config/worker-service.toml b/golem-worker-service/config/worker-service.toml index e2f90997a..006e33690 100644 --- a/golem-worker-service/config/worker-service.toml +++ b/golem-worker-service/config/worker-service.toml @@ -89,6 +89,14 @@ max_jitter_factor = 0.15 min_delay = "10ms" multiplier = 10.0 +[worker_identity] +active_keys = ["1"] + +[[worker_identity.set]] +kid = "1" +alg = "ES256" +der = "MHcCAQEEIFJwEp6moU1kCBNRBogEmh2P5cB/kyUkFAzKt3VdMxD5oAoGCCqGSM49AwEHoUQDQgAE4O1X2mxySsWcf5cy7hsg+aaKCV9C5FEt8Tv0Wr4cDPcsCCKxHnAqwtUGQwr9WQ8Tpbut0r3had+Xg+UIE+OuGw==" + ## Generated from example config: with postgres # custom_request_port = 9006 diff --git a/golem-worker-service/src/api/identity.rs b/golem-worker-service/src/api/identity.rs new file mode 100644 index 000000000..c6506fb01 --- /dev/null +++ b/golem-worker-service/src/api/identity.rs @@ -0,0 +1,314 @@ +use golem_service_base::api_tags::ApiTags; + +use golem_worker_service_base::api::WorkerApiBaseError; + +use jwk::JsonWebKey; +use payload::{Binary, PlainText}; +use poem::{IntoResponse, Request, Response}; + +use poem_openapi::payload::Json; +use poem_openapi::*; +use registry::{MetaMediaType, MetaSchema}; +use serde::{Deserialize, Serialize}; + +use crate::{service::worker_identity::WorkerIdentityService, WorkerService}; + +pub struct IdentityApi { + pub worker_identity_service: WorkerIdentityService, +} + +type Result = std::result::Result; + +#[OpenApi(prefix_path = "/", tag = ApiTags::Worker)] +impl IdentityApi { + /// Launch a new worker. + /// + /// Creates a new worker. The worker initially is in `Idle`` status, waiting to be invoked. + /// + /// The parameters in the request are the following: + /// - `name` is the name of the created worker. This has to be unique, but only for a given component + /// - `args` is a list of strings which appear as command line arguments for the worker + /// - `env` is a list of key-value pairs (represented by arrays) which appear as environment variables for the worker + #[oai( + path = "/.well-known/jwks.json", + method = "get", + operation_id = "get_jwks" + )] + async fn get_jwk(&self) -> Result>> { + let keys = self + .worker_identity_service + .get_jwks() + .await + .unwrap() + .iter() + .map(|j| JsonWebKey::from(j)) + .collect(); + + // let serialized = serde_json::to_string(&keys).unwrap(); + + // poem::Response::builder() + // .header("Content-Type", "application/json") + // .body(serialized) + + Ok(Json(keys)) + } + + /// Delete a worker + /// + /// Interrupts and deletes an existing worker. + #[oai( + path = "/.well-known/openid-configuration", + method = "get", + operation_id = "get_oidc_configuration" + )] + async fn get_oidc_configuration(&self, req: &Request) -> Result> { + // Get the scheme (http or https) + let scheme = req.scheme().as_str(); + + // Get the host from the "Host" header + let host = req.header("host").unwrap_or(""); // Default if Host is missing + + // Construct the base URL + let base_url = format!("{scheme}://{host}"); + + Ok(Json(OidcDiscovery { + issuer: base_url.clone(), // Base URL of your OIDC provider + authorization_endpoint: format!("{}/unused", base_url), + token_endpoint: format!("{}/unused", base_url), + jwks_uri: format!("{}/.well-known/jwks.json", base_url), + response_types_supported: vec![ + "code".to_string(), + "token".to_string(), + "id_token".to_string(), + ], + subject_types_supported: vec!["public".to_string()], + id_token_signing_alg_values_supported: vec!["RS256".to_string(), "ES256".to_string()], + })) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Object)] +struct OidcDiscovery { + issuer: String, + authorization_endpoint: String, + token_endpoint: String, + jwks_uri: String, + response_types_supported: Vec, + subject_types_supported: Vec, + id_token_signing_alg_values_supported: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Object)] +struct Jwk { + kty: String, + alg: String, + use_: String, + kid: String, + n: Option, // RSA modulus + e: Option, // RSA exponent + x: Option, // EC x-coordinate + y: Option, // EC y-coordinate + d: Option, // Private key (optional, not exposed in public JWK) + crv: Option, // EC curve name +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Object)] +struct JwkSet { + keys: Vec, +} + +fn generate_jwk_set() -> JwkSet { + let rsa_jwk = Jwk { + kty: "RSA".to_string(), + alg: "RS256".to_string(), + use_: "sig".to_string(), + kid: "rsa-key-id".to_string(), + n: Some("base64url-modulus".to_string()), // Replace with actual base64url-encoded modulus + e: Some("base64url-exponent".to_string()), // Replace with actual base64url-encoded exponent + x: None, + y: None, + d: None, + crv: None, + }; + + let ec_jwk = Jwk { + kty: "EC".to_string(), + alg: "ES256".to_string(), + use_: "sig".to_string(), + kid: "ec-key-id".to_string(), + n: None, + e: None, + x: Some("base64url-x".to_string()), // Replace with actual base64url-encoded x-coordinate + y: Some("base64url-y".to_string()), // Replace with actual base64url-encoded y-coordinate + d: None, + crv: Some("P-256".to_string()), // Curve name + }; + + JwkSet { + keys: vec![rsa_jwk, ec_jwk], + } +} + +mod jwk { + + impl From<&jsonwebkey::JsonWebKey> for JsonWebKey { + fn from(value: &jsonwebkey::JsonWebKey) -> Self { + Self { + key: value.key.clone().into(), + key_use: value.key_use.map(|t| (&t).into()), + // key_ops: value.key_ops, + key_id: value.key_id.clone(), + algorithm: value.algorithm.map(|t| (&t).into()), + // x5: value.x5, + } + } + } + + use poem_openapi::{Enum, Object, Union}; + + impl From> for Key { + fn from(value: Box) -> Self { + match *value { + jsonwebkey::Key::EC { curve } => Key::EC(curve.into()), + jsonwebkey::Key::RSA { public, private } => todo!(), + jsonwebkey::Key::Symmetric { key } => todo!(), + } + } + } + + #[derive(Clone, Debug, PartialEq, Eq, Union)] + #[oai(discriminator_name = "kty")] + #[allow(clippy::upper_case_acronyms)] + pub enum Key { + /// An elliptic curve, as per [RFC 7518 §6.2](https://tools.ietf.org/html/rfc7518#section-6.2). + EC(Curve), + // /// An elliptic curve, as per [RFC 7518 §6.3](https://tools.ietf.org/html/rfc7518#section-6.3). + // /// See also: [RFC 3447](https://tools.ietf.org/html/rfc3447). + // RSA { + // #[oai(flatten)] + // public: RsaPublic, + // #[oai(flatten, default, skip_serializing_if = "Option::is_none")] + // private: Option, + // }, + // /// A symmetric key, as per [RFC 7518 §6.4](https://tools.ietf.org/html/rfc7518#section-6.4). + // #[oai(rename = "oct")] + // Symmetric { + // #[oai(rename = "k")] + // key: ByteVec, + // }, + } + + impl From for Curve { + fn from(value: jsonwebkey::Curve) -> Self { + match value { + jsonwebkey::Curve::P256 { d, x, y } => { + return Curve::P256(CurveP256 { + d: None, + x: base64::encode(x.as_ref()), + y: base64::encode(y.as_ref()), + }) + } + } + } + } + + #[derive(Clone, Debug, PartialEq, Eq, Union)] + #[oai(discriminator_name = "crv")] + pub enum Curve { + /// Parameters of the prime256v1 (P256) curve. + // #[oai(rename = "P-256")] + P256(CurveP256), + } + + #[derive(Clone, Debug, PartialEq, Eq, Object)] + #[oai(rename = "P-256")] + pub struct CurveP256 { + /// The private scalar. + #[oai(skip_serializing_if = "Option::is_none")] + d: Option, + /// The curve point x coordinate. + x: String, + /// The curve point y coordinate. + y: String, + } + + #[derive(Clone, Debug, Default, PartialEq, Eq, Object)] + pub struct KeyOps { + ops: Vec, + } + + impl KeyOps { + fn is_empty(&self) -> bool { + self.ops.is_empty() + } + } + + impl From<&jsonwebkey::KeyUse> for KeyUse { + fn from(value: &jsonwebkey::KeyUse) -> Self { + match value { + jsonwebkey::KeyUse::Signing => KeyUse::Signing, + jsonwebkey::KeyUse::Encryption => KeyUse::Encryption, + } + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Enum)] + pub enum KeyUse { + #[oai(rename = "sig")] + Signing, + #[oai(rename = "enc")] + Encryption, + } + + impl From<&jsonwebkey::Algorithm> for Algorithm { + fn from(value: &jsonwebkey::Algorithm) -> Self { + match value { + jsonwebkey::Algorithm::HS256 => Algorithm::HS256, + jsonwebkey::Algorithm::RS256 => Algorithm::RS256, + jsonwebkey::Algorithm::ES256 => Algorithm::ES256, + } + } + } + + #[derive(Clone, Copy, Debug, PartialEq, Eq, Enum)] + #[allow(clippy::upper_case_acronyms)] + pub enum Algorithm { + HS256, + RS256, + ES256, + } + + #[derive(Clone, Debug, PartialEq, Eq, Object)] + pub struct JsonWebKey { + #[oai(flatten)] + pub key: Key, + + #[oai(default, rename = "use", skip_serializing_if = "Option::is_none")] + pub key_use: Option, + + // #[oai(default, skip_serializing_if = "KeyOps::is_empty")] + // pub key_ops: KeyOps, + #[oai(default, rename = "kid", skip_serializing_if = "Option::is_none")] + pub key_id: Option, + + #[oai(default, rename = "alg", skip_serializing_if = "Option::is_none")] + pub algorithm: Option, + // #[oai(default, flatten, skip_serializing_if = "X509Params::is_empty")] + // pub x5: X509Params, + } + + // #[derive(Clone, Debug, Default, PartialEq, Eq, Object)] + // pub struct X509Params { + // #[oai(default, rename = "x5u", skip_serializing_if = "Option::is_none")] + // url: Option, + + // #[oai(default, rename = "x5c", skip_serializing_if = "Option::is_none")] + // cert_chain: Option>, + + // #[oai(default, rename = "x5t", skip_serializing_if = "Option::is_none")] + // thumbprint: Option, + + // #[oai(default, rename = "x5t#S256", skip_serializing_if = "Option::is_none")] + // thumbprint_sha256: Option, + // } +} diff --git a/golem-worker-service/src/api/mod.rs b/golem-worker-service/src/api/mod.rs index 3755c9508..88d53be96 100644 --- a/golem-worker-service/src/api/mod.rs +++ b/golem-worker-service/src/api/mod.rs @@ -1,5 +1,6 @@ pub mod api_definition; pub mod api_deployment; +pub mod identity; mod security_scheme; pub mod worker; pub mod worker_connect; @@ -8,6 +9,7 @@ use crate::api::worker::WorkerApi; use crate::service::Services; use golem_worker_service_base::api::CustomHttpRequestApi; use golem_worker_service_base::api::HealthcheckApi; +use identity::IdentityApi; use poem::endpoint::PrometheusExporter; use poem::{get, EndpointExt, Route}; use poem_openapi::OpenApiService; @@ -19,6 +21,7 @@ pub type ApiServices = ( api_deployment::ApiDeploymentApi, security_scheme::SecuritySchemeApi, HealthcheckApi, + IdentityApi, ); pub fn combined_routes(prometheus_registry: Registry, services: &Services) -> Route { @@ -63,6 +66,9 @@ pub fn make_open_api_service(services: &Services) -> OpenApiService + Sync + Send>, pub definition_service: @@ -136,6 +138,10 @@ impl Services { routing_table_service.clone(), )); + let worker_identity_service: worker_identity::WorkerIdentityService = Arc::new( + WorkerIdentityServiceDefault::new(config.worker_identity.clone()), + ); + let worker_to_http_service: Arc< dyn GatewayWorkerRequestExecutor + Sync + Send, > = Arc::new(UnauthorisedWorkerRequestExecutor::new( @@ -275,6 +281,7 @@ impl Services { Ok(Services { worker_service, + worker_identity_service, definition_service, security_scheme_service, deployment_service, diff --git a/golem-worker-service/src/service/worker_identity.rs b/golem-worker-service/src/service/worker_identity.rs new file mode 100644 index 000000000..15c569123 --- /dev/null +++ b/golem-worker-service/src/service/worker_identity.rs @@ -0,0 +1,5 @@ +use std::sync::Arc; + +pub type WorkerIdentityService = Arc< + dyn golem_worker_service_base::service::worker_identity::WorkerIdentityService + Sync + Send, +>; diff --git a/test-components/build-components.sh b/test-components/build-components.sh index 127e41216..26d69478c 100755 --- a/test-components/build-components.sh +++ b/test-components/build-components.sh @@ -2,7 +2,7 @@ rust_test_components=("write-stdout" "write-stderr" "read-stdin" "clocks" "shopping-cart" "file-write-read-delete" "file-service" "http-client" "directories" "environment-service" "promise" "interruption" "clock-service" "option-service" "flags-service" "http-client-2" "stdio-cc" "failing-component" "variant-service" "key-value-service" "blob-store-service" "runtime-service" "networking" "shopping-cart-resource" -"update-test-v1" "update-test-v2" "update-test-v3" "update-test-v4" "rust-echo" "golem-rust-tests" "durability-overhead" "logging" "oplog-processor") +"update-test-v1" "update-test-v2" "update-test-v3" "update-test-v4" "rust-echo" "golem-rust-tests" "durability-overhead" "logging" "oplog-processor" "identity") zig_test_components=("zig-3") tinygo_test_components=("tinygo-wasi" "tinygo-wasi-http") grain_test_components=("grain-1") diff --git a/test-components/identity.wasm b/test-components/identity.wasm new file mode 100644 index 0000000000000000000000000000000000000000..7547501fdc5f502680cde09b31f81466b4124d75 GIT binary patch literal 46733 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 Option<_rt::String> { + unsafe { + #[repr(align(4))] + struct RetArea([::core::mem::MaybeUninit; 12]); + let mut ret_area = RetArea([::core::mem::MaybeUninit::uninit(); 12]); + let ptr0 = ret_area.0.as_mut_ptr().cast::(); + #[cfg(target_arch = "wasm32")] + #[link(wasm_import_module = "golem:api/identity@1.1.0")] + extern "C" { + #[link_name = "get-token"] + fn wit_import(_: *mut u8); + } + #[cfg(not(target_arch = "wasm32"))] + fn wit_import(_: *mut u8) { + unreachable!() + } + wit_import(ptr0); + let l1 = i32::from(*ptr0.add(0).cast::()); + match l1 { + 0 => None, + 1 => { + let e = { + let l2 = *ptr0.add(4).cast::<*mut u8>(); + let l3 = *ptr0.add(8).cast::(); + let len4 = l3; + let bytes4 = _rt::Vec::from_raw_parts( + l2.cast(), + len4, + len4, + ); + _rt::string_lift(bytes4) + }; + Some(e) + } + _ => _rt::invalid_enum_discriminant(), + } + } + } + } + } +} +#[allow(dead_code)] +pub mod exports { + #[allow(dead_code)] + pub mod golem { + #[allow(dead_code)] + pub mod it { + #[allow(dead_code, clippy::all)] + pub mod api { + #[used] + #[doc(hidden)] + static __FORCE_SECTION_REF: fn() = super::super::super::super::__link_custom_section_describing_imports; + use super::super::super::super::_rt; + #[doc(hidden)] + #[allow(non_snake_case)] + pub unsafe fn _export_echo_cabi( + arg0: *mut u8, + arg1: usize, + ) -> *mut u8 { + #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); + let len0 = arg1; + let bytes0 = _rt::Vec::from_raw_parts(arg0.cast(), len0, len0); + let result1 = T::echo(_rt::string_lift(bytes0)); + let ptr2 = _RET_AREA.0.as_mut_ptr().cast::(); + let vec3 = (result1.into_bytes()).into_boxed_slice(); + let ptr3 = vec3.as_ptr().cast::(); + let len3 = vec3.len(); + ::core::mem::forget(vec3); + *ptr2.add(4).cast::() = len3; + *ptr2.add(0).cast::<*mut u8>() = ptr3.cast_mut(); + ptr2 + } + #[doc(hidden)] + #[allow(non_snake_case)] + pub unsafe fn __post_return_echo(arg0: *mut u8) { + let l0 = *arg0.add(0).cast::<*mut u8>(); + let l1 = *arg0.add(4).cast::(); + _rt::cabi_dealloc(l0, l1, 1); + } + pub trait Guest { + fn echo(input: _rt::String) -> _rt::String; + } + #[doc(hidden)] + macro_rules! __export_golem_it_api_cabi { + ($ty:ident with_types_in $($path_to_types:tt)*) => { + const _ : () = { #[export_name = "golem:it/api#echo"] unsafe + extern "C" fn export_echo(arg0 : * mut u8, arg1 : usize,) -> * + mut u8 { $($path_to_types)*:: _export_echo_cabi::<$ty > (arg0, + arg1) } #[export_name = "cabi_post_golem:it/api#echo"] unsafe + extern "C" fn _post_return_echo(arg0 : * mut u8,) { + $($path_to_types)*:: __post_return_echo::<$ty > (arg0) } }; + }; + } + #[doc(hidden)] + pub(crate) use __export_golem_it_api_cabi; + #[repr(align(4))] + struct _RetArea([::core::mem::MaybeUninit; 8]); + static mut _RET_AREA: _RetArea = _RetArea( + [::core::mem::MaybeUninit::uninit(); 8], + ); + } + } + } +} +mod _rt { + pub use alloc_crate::string::String; + pub use alloc_crate::vec::Vec; + pub unsafe fn string_lift(bytes: Vec) -> String { + if cfg!(debug_assertions) { + String::from_utf8(bytes).unwrap() + } else { + String::from_utf8_unchecked(bytes) + } + } + pub unsafe fn invalid_enum_discriminant() -> T { + if cfg!(debug_assertions) { + panic!("invalid enum discriminant") + } else { + core::hint::unreachable_unchecked() + } + } + #[cfg(target_arch = "wasm32")] + pub fn run_ctors_once() { + wit_bindgen_rt::run_ctors_once(); + } + pub unsafe fn cabi_dealloc(ptr: *mut u8, size: usize, align: usize) { + if size == 0 { + return; + } + let layout = alloc::Layout::from_size_align_unchecked(size, align); + alloc::dealloc(ptr, layout); + } + extern crate alloc as alloc_crate; + pub use alloc_crate::alloc; +} +/// Generates `#[no_mangle]` functions to export the specified type as the +/// root implementation of all generated traits. +/// +/// For more information see the documentation of `wit_bindgen::generate!`. +/// +/// ```rust +/// # macro_rules! export{ ($($t:tt)*) => (); } +/// # trait Guest {} +/// struct MyType; +/// +/// impl Guest for MyType { +/// // ... +/// } +/// +/// export!(MyType); +/// ``` +#[allow(unused_macros)] +#[doc(hidden)] +macro_rules! __export_rust_echo_impl { + ($ty:ident) => { + self::export!($ty with_types_in self); + }; + ($ty:ident with_types_in $($path_to_types_root:tt)*) => { + $($path_to_types_root)*:: + exports::golem::it::api::__export_golem_it_api_cabi!($ty with_types_in + $($path_to_types_root)*:: exports::golem::it::api); + }; +} +#[doc(inline)] +pub(crate) use __export_rust_echo_impl as export; +#[cfg(target_arch = "wasm32")] +#[link_section = "component-type:wit-bindgen:0.35.0:golem:it:rust-echo:encoded world"] +#[doc(hidden)] +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 251] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07|\x01A\x02\x01A\x04\x01\ +B\x03\x01ks\x01@\0\0\0\x04\0\x09get-token\x01\x01\x03\0\x18golem:api/identity@1.\ +1.0\x05\0\x01B\x02\x01@\x01\x05inputs\0s\x04\0\x04echo\x01\0\x04\0\x0cgolem:it/a\ +pi\x05\x01\x04\0\x12golem:it/rust-echo\x04\0\x0b\x0f\x01\0\x09rust-echo\x03\0\0\0\ +G\x09producers\x01\x0cprocessed-by\x02\x0dwit-component\x070.220.0\x10wit-bindge\ +n-rust\x060.35.0"; +#[inline(never)] +#[doc(hidden)] +pub fn __link_custom_section_describing_imports() { + wit_bindgen_rt::maybe_link_cabi_realloc(); +} diff --git a/test-components/identity/src/lib.rs b/test-components/identity/src/lib.rs new file mode 100644 index 000000000..7a2734df6 --- /dev/null +++ b/test-components/identity/src/lib.rs @@ -0,0 +1,13 @@ +mod bindings; + +use crate::bindings::exports::golem::it::api::*; + +struct Component; + +impl Guest for Component { + fn echo(input: String) -> String { + crate::bindings::golem::api::identity::get_token().unwrap() + } +} + +bindings::export!(Component with_types_in bindings); diff --git a/test-components/identity/wit/deps/identity/world.wit b/test-components/identity/wit/deps/identity/world.wit new file mode 100644 index 000000000..7464147d0 --- /dev/null +++ b/test-components/identity/wit/deps/identity/world.wit @@ -0,0 +1,7 @@ +package golem:api@1.1.0; + + +interface identity { + /// Get identity token + get-token: func() -> option; +} \ No newline at end of file diff --git a/test-components/identity/wit/rust-echo.wit b/test-components/identity/wit/rust-echo.wit new file mode 100644 index 000000000..946dccaca --- /dev/null +++ b/test-components/identity/wit/rust-echo.wit @@ -0,0 +1,11 @@ +package golem:it; + +interface api { + + echo: func(input: string) -> string; +} + +world rust-echo { + import golem:api/identity@1.1.0; + export api; +} \ No newline at end of file