Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 20 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ eyre = { version = "0.6" }
fernet = { version = "0.2" }
futures-util = { version = "0.3" }
mockall_double = { version = "0.3" }
opa-wasm = { version = "^0.1" }
opa-wasm = { version = "^0.1", optional = true }
openidconnect = { version = "4.0" }
regex = { version = "1.11"}
reqwest = { version = "0.12", features = ["json"] }
Expand Down Expand Up @@ -77,6 +77,10 @@ thirtyfour = "0.36.0"
tracing-test = { version = "0.2" }
url = { version = "2.5" }

[features]
default = []
wasm = ["dep:opa-wasm"]

[profile.release]
strip = true
debug = false
Expand Down
3 changes: 2 additions & 1 deletion doc/src/policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ crate happening for the big policy files. The investigation is in progress, so
it is preferred not to rely on this method anyway. While running OPA as a WASM
eliminates any networking communication, it heavily reduces feature set. In
particular hot policy reload, decision logging, external calls done by the
policies themselves are not possible by design.
policies themselves are not possible by design. Using this way of policy
enforcement requires `wasm` feature enabled.

All the policies currently are using the same policy names and definitions as
the original Keystone to keep the deviation as less as possible. For the newly
Expand Down
9 changes: 7 additions & 2 deletions src/bin/keystone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ use uuid::Uuid;

use openstack_keystone::api;
use openstack_keystone::config::Config;
use openstack_keystone::error::KeystoneError;
use openstack_keystone::federation::FederationApi;
use openstack_keystone::keystone::{Service, ServiceState};
use openstack_keystone::plugin_manager::PluginManager;
Expand Down Expand Up @@ -115,8 +116,12 @@ async fn main() -> Result<(), Report> {
let policy = if let Some(opa_base_url) = &cfg.api_policy.opa_base_url {
PolicyFactory::http(opa_base_url.clone()).await?
} else {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("policy.wasm");
PolicyFactory::from_wasm(&path).await?
#[cfg(feature = "wasm")]
{
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("policy.wasm");
PolicyFactory::from_wasm(&path).await?
}
return Err(KeystoneError::PolicyEnforcementNotAvailable)?;
};

let shared_state = Arc::new(Service::new(cfg, conn, provider, policy)?);
Expand Down
4 changes: 4 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ pub enum KeystoneError {
source: PolicyError,
},

/// Policy engine is not available.
#[error("policy enforcement is requested, but not available with the enabled features")]
PolicyEnforcementNotAvailable,

#[error(transparent)]
ResourceError {
#[from]
Expand Down
81 changes: 53 additions & 28 deletions src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

#[cfg(test)]
use mockall::mock;
#[cfg(feature = "wasm")]
use opa_wasm::{
Runtime,
wasmtime::{Config, Engine, Module, OptLevel, Store},
Expand All @@ -22,12 +23,13 @@ use reqwest::{Client, Url};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[cfg(feature = "wasm")]
use std::path::Path;
use std::sync::Arc;
use std::time::SystemTime;
use thiserror::Error;
use tokio::io::AsyncRead;
use tokio::io::AsyncReadExt;
#[cfg(feature = "wasm")]
use tokio::io::{AsyncRead, AsyncReadExt};
use tracing::{Level, debug, trace};

use crate::token::Token;
Expand Down Expand Up @@ -62,6 +64,7 @@ pub enum PolicyError {
#[error(transparent)]
UrlParse(#[from] url::ParseError),

#[cfg(feature = "wasm")]
#[error(transparent)]
Wasm(#[from] opa_wasm::wasmtime::Error),
}
Expand All @@ -73,37 +76,42 @@ pub struct PolicyFactory {
http_client: Option<Arc<Client>>,
/// OPA url address.
base_url: Option<Url>,
#[cfg(feature = "wasm")]
/// WASM engine.
engine: Option<Engine>,
#[cfg(feature = "wasm")]
/// WASM module.
module: Option<Module>,
}

impl PolicyFactory {
#[cfg(feature = "wasm")]
#[tracing::instrument(name = "policy.from_defaults", err)]
pub async fn from_defaults() -> Result<Self, PolicyError> {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("policy.wasm");
let file = tokio::fs::File::open(path).await?;
PolicyFactory::load(file).await
}

#[cfg(feature = "wasm")]
#[tracing::instrument(name = "policy.from_wasm", err)]
pub async fn from_wasm(path: &Path) -> Result<Self, PolicyError> {
let file = tokio::fs::File::open(path).await?;
PolicyFactory::load(file).await
}

#[allow(clippy::needless_update)]
#[tracing::instrument(name = "policy.http", err)]
pub async fn http(url: Url) -> Result<Self, PolicyError> {
let client = Client::new();
Ok(Self {
http_client: Some(Arc::new(client)),
base_url: Some(url.join("/v1/data/")?),
engine: None,
module: None,
..Default::default()
})
}

#[cfg(feature = "wasm")]
#[tracing::instrument(name = "policy.load", skip(source), err)]
pub async fn load(
mut source: impl AsyncRead + std::marker::Unpin,
Expand Down Expand Up @@ -138,27 +146,32 @@ impl PolicyFactory {
Ok(factory)
}

#[allow(clippy::needless_update)]
#[tracing::instrument(name = "policy.instantiate", level = Level::TRACE, skip_all, err)]
pub async fn instantiate(&self) -> Result<Policy, PolicyError> {
if let (Some(engine), Some(module)) = (&self.engine, &self.module) {
let mut store = Store::new(engine, ());
let runtime = Runtime::new(&mut store, module).await?;

let instance = runtime.without_data(&mut store).await?;
Ok(Policy {
http_client: self.http_client.clone(),
base_url: self.base_url.clone(),
store: Some(store),
instance: Some(instance),
})
} else {
Ok(Policy {
http_client: self.http_client.clone(),
base_url: self.base_url.clone(),
store: None,
instance: None,
})
#[cfg(feature = "wasm")]
{
if let (Some(engine), Some(module)) = (&self.engine, &self.module) {
let mut store = Store::new(engine, ());
let runtime = Runtime::new(&mut store, module).await?;

let instance = runtime.without_data(&mut store).await?;
return Ok(Policy {
http_client: self.http_client.clone(),
base_url: self.base_url.clone(),
#[cfg(feature = "wasm")]
store: Some(store),
#[cfg(feature = "wasm")]
instance: Some(instance),
});
}
}

Ok(Policy {
http_client: self.http_client.clone(),
base_url: self.base_url.clone(),
..Default::default()
})
}
}

Expand All @@ -182,10 +195,13 @@ mock! {
}
}

#[derive(Default)]
pub struct Policy {
http_client: Option<Arc<Client>>,
base_url: Option<Url>,
#[cfg(feature = "wasm")]
store: Option<Store<()>>,
#[cfg(feature = "wasm")]
instance: Option<opa_wasm::Policy<opa_wasm::DefaultContext>>,
}

Expand Down Expand Up @@ -248,13 +264,22 @@ impl Policy {
});
let span = tracing::Span::current();

let res = if let (Some(store), Some(instance)) = (&mut self.store, &self.instance) {
tracing::Span::current().record("input", serde_json::to_string(&input)?);
let [res]: [OpaResponse; 1] = instance
.evaluate(store, policy_name.as_ref(), &input)
.await?;
let wasm_res: Option<PolicyEvaluationResult> = None;

res.result
#[cfg(feature = "wasm")]
{
opa_res = if let (Some(store), Some(instance)) = (&mut self.store, &self.instance) {
tracing::Span::current().record("input", serde_json::to_string(&input)?);
let [res]: [OpaResponse; 1] = instance
.evaluate(store, policy_name.as_ref(), &input)
.await?;

Some(res.result)
};
}

let res = if let Some(opa_res) = wasm_res {
opa_res
} else if let (Some(client), Some(base_url)) = (&self.http_client, &self.base_url) {
trace!("checking policy decision with OPA using http");
let url = base_url.join(policy_name.as_ref())?;
Expand Down
Loading