You've already forked authentication-service
mirror of
https://github.com/matrix-org/matrix-authentication-service.git
synced 2025-07-29 22:01:14 +03:00
Embed the default policy in the binary
This commit is contained in:
@ -30,6 +30,7 @@ use mas_router::UrlBuilder;
|
||||
use mas_storage::MIGRATOR;
|
||||
use mas_tasks::TaskQueue;
|
||||
use mas_templates::Templates;
|
||||
use tokio::io::AsyncRead;
|
||||
use tracing::{error, info};
|
||||
|
||||
#[derive(Parser, Debug, Default)]
|
||||
@ -177,10 +178,19 @@ impl Options {
|
||||
|
||||
let encrypter = config.secrets.encrypter();
|
||||
|
||||
// Load and compile the WASM policies
|
||||
let mut policy = tokio::fs::File::open(&config.policy.wasm_module)
|
||||
.await
|
||||
.context("failed to open OPA WASM policy file")?;
|
||||
// Load and compile the WASM policies (and fallback to the default embedded one)
|
||||
info!("Loading and compiling the policy module");
|
||||
let mut policy: Box<dyn AsyncRead + std::marker::Unpin> =
|
||||
if let Some(path) = &config.policy.wasm_module {
|
||||
Box::new(
|
||||
tokio::fs::File::open(path)
|
||||
.await
|
||||
.context("failed to open OPA WASM policy file")?,
|
||||
)
|
||||
} else {
|
||||
Box::new(mas_policy::default_wasm_policy())
|
||||
};
|
||||
|
||||
let policy_factory = PolicyFactory::load(
|
||||
&mut policy,
|
||||
config.policy.data.clone().unwrap_or_default(),
|
||||
@ -188,7 +198,8 @@ impl Options {
|
||||
config.policy.register_entrypoint.clone(),
|
||||
config.policy.client_registration_entrypoint.clone(),
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
.context("failed to load the policy")?;
|
||||
let policy_factory = Arc::new(policy_factory);
|
||||
|
||||
// Load and compile the templates
|
||||
|
@ -21,10 +21,6 @@ use serde_with::serde_as;
|
||||
|
||||
use super::ConfigurationSection;
|
||||
|
||||
fn default_wasm_module() -> PathBuf {
|
||||
"./policies/policy.wasm".into()
|
||||
}
|
||||
|
||||
fn default_client_registration_endpoint() -> String {
|
||||
"client_registration/allow".to_string()
|
||||
}
|
||||
@ -42,8 +38,8 @@ fn default_register_endpoint() -> String {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PolicyConfig {
|
||||
/// Path to the WASM module
|
||||
#[serde(default = "default_wasm_module")]
|
||||
pub wasm_module: PathBuf,
|
||||
#[serde(default)]
|
||||
pub wasm_module: Option<PathBuf>,
|
||||
|
||||
/// Entrypoint to use when evaluating client registrations
|
||||
#[serde(default = "default_client_registration_endpoint")]
|
||||
@ -65,7 +61,7 @@ pub struct PolicyConfig {
|
||||
impl Default for PolicyConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
wasm_module: default_wasm_module(),
|
||||
wasm_module: None,
|
||||
client_registration_entrypoint: default_client_registration_endpoint(),
|
||||
login_entrypoint: default_login_endpoint(),
|
||||
register_entrypoint: default_register_endpoint(),
|
||||
|
@ -118,7 +118,7 @@ pub(crate) async fn post(
|
||||
return Err(RouteError::InvalidClientMetadata);
|
||||
}
|
||||
|
||||
let mut policy = policy_factory.instanciate().await?;
|
||||
let mut policy = policy_factory.instantiate().await?;
|
||||
let allowed = policy.evaluate_client_registration(&body).await?;
|
||||
if !allowed {
|
||||
return Err(RouteError::PolicyDenied);
|
||||
|
@ -131,7 +131,7 @@ pub(crate) async fn post(
|
||||
state.add_error_on_field(RegisterFormField::PasswordConfirm, FieldError::Unspecified);
|
||||
}
|
||||
|
||||
let mut policy = policy_factory.instanciate().await?;
|
||||
let mut policy = policy_factory.instantiate().await?;
|
||||
let res = policy
|
||||
.evaluate_register(&form.username, &form.email)
|
||||
.await?;
|
||||
|
2
crates/policy/policies/.gitignore
vendored
Normal file
2
crates/policy/policies/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/policy.wasm
|
||||
/bundle.tar.gz
|
27
crates/policy/policies/Makefile
Normal file
27
crates/policy/policies/Makefile
Normal file
@ -0,0 +1,27 @@
|
||||
# Set to 1 to run OPA through Docker
|
||||
DOCKER := 0
|
||||
|
||||
ifeq ($(DOCKER), 0)
|
||||
OPA := opa
|
||||
else
|
||||
OPA := docker run -v $(shell pwd):/policies -w /policies --rm docker.io/openpolicyagent/opa:0.40.0
|
||||
endif
|
||||
|
||||
policy.wasm: client_registration.rego login.rego register.rego
|
||||
$(OPA) build -t wasm -e "client_registration/allow" -e "login/allow" -e "register/allow" $^
|
||||
tar xzf bundle.tar.gz /policy.wasm
|
||||
$(RM) bundle.tar.gz
|
||||
touch $@
|
||||
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
$(OPA) fmt -w .
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
$(OPA) test -v .
|
||||
|
||||
.PHONY: lint
|
||||
lint:
|
||||
$(OPA) fmt -d --fail .
|
||||
$(OPA) check --strict .
|
18
crates/policy/policies/client_registration.rego
Normal file
18
crates/policy/policies/client_registration.rego
Normal file
@ -0,0 +1,18 @@
|
||||
package client_registration
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
secure_url(x) {
|
||||
is_string(x)
|
||||
startswith(x, "https://")
|
||||
}
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
secure_url(input.client_metadata.client_uri)
|
||||
secure_url(input.client_metadata.tos_uri)
|
||||
secure_url(input.client_metadata.policy_uri)
|
||||
some redirect_uri in input.client_metadata.redirect_uris
|
||||
secure_url(redirect_uri)
|
||||
}
|
27
crates/policy/policies/client_registration_test.rego
Normal file
27
crates/policy/policies/client_registration_test.rego
Normal file
@ -0,0 +1,27 @@
|
||||
package client_registration
|
||||
|
||||
test_valid {
|
||||
allow with input.client_metadata as {
|
||||
"client_uri": "https://example.com",
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
}
|
||||
}
|
||||
|
||||
test_missing_client_uri {
|
||||
not allow with input.client_metadata as {
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
}
|
||||
}
|
||||
|
||||
test_insecure_client_uri {
|
||||
not allow with input.client_metadata as {
|
||||
"client_uri": "http://example.com",
|
||||
"tos_uri": "https://example.com/tos",
|
||||
"policy_uri": "https://example.com/policy",
|
||||
"redirect_uris": ["https://example.com/callback"],
|
||||
}
|
||||
}
|
3
crates/policy/policies/login.rego
Normal file
3
crates/policy/policies/login.rego
Normal file
@ -0,0 +1,3 @@
|
||||
package login
|
||||
|
||||
allow := true
|
40
crates/policy/policies/register.rego
Normal file
40
crates/policy/policies/register.rego
Normal file
@ -0,0 +1,40 @@
|
||||
package register
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
violation[{"field": "username", "msg": "username too short"}] {
|
||||
count(input.user.username) <= 2
|
||||
}
|
||||
|
||||
violation[{"field": "username", "msg": "username too long"}] {
|
||||
count(input.user.username) >= 15
|
||||
}
|
||||
|
||||
# Allow any domains if the data.allowed_domains array is not set
|
||||
email_domain_allowed {
|
||||
not data.allowed_domains
|
||||
}
|
||||
|
||||
# Allow an email only if its domain is in the list of allowed domains
|
||||
email_domain_allowed {
|
||||
[_, domain] := split(input.user.email, "@")
|
||||
some allowed_domain in data.allowed_domains
|
||||
glob.match(allowed_domain, ["."], domain)
|
||||
}
|
||||
|
||||
violation[{"field": "email", "msg": "email domain not allowed"}] {
|
||||
not email_domain_allowed
|
||||
}
|
||||
|
||||
# Deny emails with their domain in the domains banlist
|
||||
violation[{"field": "email", "msg": "email domain not allowed"}] {
|
||||
[_, domain] := split(input.user.email, "@")
|
||||
some banned_domain in data.banned_domains
|
||||
glob.match(banned_domain, ["."], domain)
|
||||
}
|
36
crates/policy/policies/register_test.rego
Normal file
36
crates/policy/policies/register_test.rego
Normal file
@ -0,0 +1,36 @@
|
||||
package register
|
||||
|
||||
mock_user := {"username": "hello", "email": "hello@staging.element.io"}
|
||||
|
||||
test_allow_all_domains {
|
||||
allow with input.user as mock_user
|
||||
}
|
||||
|
||||
test_allowed_domain {
|
||||
allow with input.user as mock_user
|
||||
with data.allowed_domains as ["*.element.io"]
|
||||
}
|
||||
|
||||
test_not_allowed_domain {
|
||||
not allow with input.user as mock_user
|
||||
with data.allowed_domains as ["example.com"]
|
||||
}
|
||||
|
||||
test_banned_domain {
|
||||
not allow with input.user as mock_user
|
||||
with data.banned_domains as ["*.element.io"]
|
||||
}
|
||||
|
||||
test_banned_subdomain {
|
||||
not allow with input.user as mock_user
|
||||
with data.allowed_domains as ["*.element.io"]
|
||||
with data.banned_domains as ["staging.element.io"]
|
||||
}
|
||||
|
||||
test_short_username {
|
||||
not allow with input.user as {"username": "a", "email": "hello@element.io"}
|
||||
}
|
||||
|
||||
test_long_username {
|
||||
not allow with input.user as {"username": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "email": "hello@element.io"}
|
||||
}
|
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::bail;
|
||||
use oauth2_types::registration::ClientMetadata;
|
||||
use opa_wasm::Runtime;
|
||||
@ -20,6 +22,12 @@ use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncReadExt};
|
||||
use wasmtime::{Config, Engine, Module, Store};
|
||||
|
||||
const DEFAULT_POLICY: &[u8] = include_bytes!("../policies/policy.wasm");
|
||||
|
||||
pub fn default_wasm_policy() -> impl AsyncRead + std::marker::Unpin {
|
||||
Cursor::new(DEFAULT_POLICY)
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum LoadError {
|
||||
#[error("failed to read module")]
|
||||
@ -33,6 +41,9 @@ pub enum LoadError {
|
||||
|
||||
#[error("failed to compile WASM module")]
|
||||
Compilation(#[source] anyhow::Error),
|
||||
|
||||
#[error("failed to instantiate a test instance")]
|
||||
Instantiate(#[source] anyhow::Error),
|
||||
}
|
||||
|
||||
pub struct PolicyFactory {
|
||||
@ -55,27 +66,39 @@ impl PolicyFactory {
|
||||
let mut config = Config::default();
|
||||
config.async_support(true);
|
||||
config.cranelift_opt_level(wasmtime::OptLevel::Speed);
|
||||
|
||||
let engine = Engine::new(&config).map_err(LoadError::Engine)?;
|
||||
|
||||
// Read and compile the module
|
||||
let mut buf = Vec::new();
|
||||
source.read_to_end(&mut buf).await?;
|
||||
// Compilation is CPU-bound, so spawn that in a blocking task
|
||||
let (engine, module) = tokio::task::spawn_blocking(move || {
|
||||
let module = Module::new(&engine, buf);
|
||||
(engine, module)
|
||||
let module = Module::new(&engine, buf)?;
|
||||
anyhow::Ok((engine, module))
|
||||
})
|
||||
.await?;
|
||||
let module = module.map_err(LoadError::Compilation)?;
|
||||
.await?
|
||||
.map_err(LoadError::Compilation)?;
|
||||
|
||||
Ok(Self {
|
||||
let factory = Self {
|
||||
engine,
|
||||
module,
|
||||
data,
|
||||
login_entrypoint,
|
||||
register_entrypoint,
|
||||
client_registration_entrypoint,
|
||||
})
|
||||
};
|
||||
|
||||
// Try to instanciate
|
||||
factory
|
||||
.instantiate()
|
||||
.await
|
||||
.map_err(LoadError::Instantiate)?;
|
||||
|
||||
Ok(factory)
|
||||
}
|
||||
|
||||
pub async fn instanciate(&self) -> Result<Policy, anyhow::Error> {
|
||||
pub async fn instantiate(&self) -> Result<Policy, anyhow::Error> {
|
||||
let mut store = Store::new(&self.engine, ());
|
||||
let runtime = Runtime::new(&mut store, &self.module).await?;
|
||||
|
||||
|
Reference in New Issue
Block a user