From 8658a3400d941ff7f8c10246da7ce4667e8624f5 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 4 Sep 2023 18:22:50 +0200 Subject: [PATCH] policy: prepare for the client credentials grant --- crates/policy/src/lib.rs | 42 +++++++++- crates/policy/src/model.rs | 26 +++++-- policies/authorization_grant.rego | 20 ++++- policies/authorization_grant_test.rego | 76 ++++++++++++++----- .../schema/authorization_grant_input.json | 22 ++++-- 5 files changed, 150 insertions(+), 36 deletions(-) diff --git a/crates/policy/src/lib.rs b/crates/policy/src/lib.rs index 95f963a7..9529942b 100644 --- a/crates/policy/src/lib.rs +++ b/crates/policy/src/lib.rs @@ -20,7 +20,7 @@ pub mod model; use mas_data_model::{AuthorizationGrant, Client, User}; -use oauth2_types::registration::VerifiedClientMetadata; +use oauth2_types::{registration::VerifiedClientMetadata, scope::Scope}; use opa_wasm::Runtime; use thiserror::Error; use tokio::io::{AsyncRead, AsyncReadExt}; @@ -30,6 +30,7 @@ use self::model::{ AuthorizationGrantInput, ClientRegistrationInput, EmailInput, PasswordInput, RegisterInput, }; pub use self::model::{EvaluationResult, Violation}; +use crate::model::GrantType; #[derive(Debug, Error)] pub enum LoadError { @@ -300,6 +301,7 @@ impl Policy { skip_all, fields( input.authorization_grant.id = %authorization_grant.id, + input.scope = %authorization_grant.scope, input.client.id = %client.id, input.user.id = %user.id, ), @@ -314,7 +316,43 @@ impl Policy { let input = AuthorizationGrantInput { user, client, - authorization_grant, + scope: &authorization_grant.scope, + grant_type: GrantType::AuthorizationCode, + }; + + let [res]: [EvaluationResult; 1] = self + .instance + .evaluate( + &mut self.store, + &self.entrypoints.authorization_grant, + &input, + ) + .await?; + + Ok(res) + } + + #[tracing::instrument( + name = "policy.evaluate.client_credentials_grant", + skip_all, + fields( + input.scope = %scope, + input.client.id = %client.id, + input.user.id = %user.id, + ), + err, + )] + pub async fn evaluate_client_credentials_grant( + &mut self, + scope: &Scope, + client: &Client, + user: &User, + ) -> Result { + let input = AuthorizationGrantInput { + user, + client, + scope, + grant_type: GrantType::ClientCredentials, }; let [res]: [EvaluationResult; 1] = self diff --git a/crates/policy/src/model.rs b/crates/policy/src/model.rs index c17ce041..48adb81e 100644 --- a/crates/policy/src/model.rs +++ b/crates/policy/src/model.rs @@ -12,8 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -use mas_data_model::{AuthorizationGrant, Client, User}; -use oauth2_types::registration::VerifiedClientMetadata; +//! Input and output types for policy evaluation. +//! +//! This is useful to generate JSON schemas for each input type, which can then +//! be type-checked by Open Policy Agent. + +use mas_data_model::{Client, User}; +use oauth2_types::{registration::VerifiedClientMetadata, scope::Scope}; use serde::{Deserialize, Serialize}; /// A single violation of a policy. @@ -87,6 +92,14 @@ pub struct ClientRegistrationInput<'a> { pub client_metadata: &'a VerifiedClientMetadata, } +#[derive(Serialize, Debug)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] +pub enum GrantType { + AuthorizationCode, + ClientCredentials, +} + /// Input for the authorization grant policy. #[derive(Serialize, Debug)] #[serde(rename_all = "snake_case")] @@ -104,11 +117,10 @@ pub struct AuthorizationGrantInput<'a> { )] pub client: &'a Client, - #[cfg_attr( - feature = "jsonschema", - schemars(with = "std::collections::HashMap") - )] - pub authorization_grant: &'a AuthorizationGrant, + #[cfg_attr(feature = "jsonschema", schemars(with = "String"))] + pub scope: &'a Scope, + + pub grant_type: GrantType, } /// Input for the email add policy. diff --git a/policies/authorization_grant.rego b/policies/authorization_grant.rego index d59c6c57..87e4d7d1 100644 --- a/policies/authorization_grant.rego +++ b/policies/authorization_grant.rego @@ -20,6 +20,7 @@ allowed_scope("email") = true # This grants access to Synapse's admin API endpoints allowed_scope("urn:synapse:admin:*") { + input.grant_type == "authorization_code" some user in data.admin_users input.user.username == user } @@ -29,23 +30,36 @@ allowed_scope("urn:mas:graphql:*") = true # This makes it possible to query and do anything in the GraphQL API as an admin allowed_scope("urn:mas:admin") { + input.grant_type == "authorization_code" some user in data.admin_users input.user.username == user } +# This makes it possible to get the admin scope for clients that are allowed +allowed_scope("urn:mas:admin") { + input.grant_type == "client_credentials" + some client in data.admin_clients + input.client.id == client +} + allowed_scope(scope) { + # Grant access to the C-S API only if there is a user + input.grant_type == "authorization_code" regex.match("urn:matrix:org.matrix.msc2967.client:device:[A-Za-z0-9-]{10,}", scope) } -allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") = true +allowed_scope("urn:matrix:org.matrix.msc2967.client:api:*") { + # Grant access to the C-S API only if there is a user + input.grant_type == "authorization_code" +} violation[{"msg": msg}] { - some scope in split(input.authorization_grant.scope, " ") + some scope in split(input.scope, " ") not allowed_scope(scope) msg := sprintf("scope '%s' not allowed", [scope]) } violation[{"msg": "only one device scope is allowed at a time"}] { - scope_list := split(input.authorization_grant.scope, " ") + scope_list := split(input.scope, " ") count({key | scope_list[key]; startswith(scope_list[key], "urn:matrix:org.matrix.msc2967.client:device:")}) > 1 } diff --git a/policies/authorization_grant_test.rego b/policies/authorization_grant_test.rego index 924f97a8..68c2e215 100644 --- a/policies/authorization_grant_test.rego +++ b/policies/authorization_grant_test.rego @@ -2,78 +2,116 @@ package authorization_grant user := {"username": "john"} +client := {"client_id": "client"} + test_standard_scopes { allow with input.user as user - with input.authorization_grant as {"scope": ""} + with input.client as client + with input.scope as "" allow with input.user as user - with input.authorization_grant as {"scope": "openid"} + with input.client as client + with input.scope as "openid" allow with input.user as user - with input.authorization_grant as {"scope": "email"} + with input.client as client + with input.scope as "email" allow with input.user as user - with input.authorization_grant as {"scope": "openid email"} + with input.client as client + with input.scope as "openid email" # Not supported yet not allow with input.user as user - with input.authorization_grant as {"scope": "phone"} + with input.client as client + with input.scope as "phone" # Not supported yet not allow with input.user as user - with input.authorization_grant as {"scope": "profile"} + with input.client as client + with input.scope as "profile" } test_matrix_scopes { allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:api:*"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:api:*" } test_device_scopes { allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01" allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01-asdasdsa1-2313"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01-asdasdsa1-2313" # Invalid characters not allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB:CCDDEE"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AABB:CCDDEE" not allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB*CCDDEE"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AABB*CCDDEE" not allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AABB!CCDDEE"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AABB!CCDDEE" # Too short not allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:abcd"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:abcd" # Multiple device scope not allow with input.user as user - with input.authorization_grant as {"scope": "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01 urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd02"} + with input.client as client + with input.grant_type as "authorization_code" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01 urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd02" + + # Not allowed for the client credentials grant + not allow with input.client as client + with input.grant_type as "client_credentials" + with input.scope as "urn:matrix:org.matrix.msc2967.client:device:AAbbCCdd01" } test_synapse_admin_scopes { allow with input.user as user + with input.client as client with data.admin_users as ["john"] - with input.authorization_grant as {"scope": "urn:synapse:admin:*"} + with input.grant_type as "authorization_code" + with input.scope as "urn:synapse:admin:*" not allow with input.user as user + with input.client as client with data.admin_users as [] - with input.authorization_grant as {"scope": "urn:synapse:admin:*"} + with input.grant_type as "authorization_code" + with input.scope as "urn:synapse:admin:*" } test_mas_scopes { allow with input.user as user - with input.authorization_grant as {"scope": "urn:mas:graphql:*"} + with input.client as client + with input.scope as "urn:mas:graphql:*" allow with input.user as user + with input.client as client with data.admin_users as ["john"] - with input.authorization_grant as {"scope": "urn:mas:admin"} + with input.grant_type as "authorization_code" + with input.scope as "urn:mas:admin" not allow with input.user as user + with input.client as client with data.admin_users as [] - with input.authorization_grant as {"scope": "urn:mas:admin"} + with input.grant_type as "authorization_code" + with input.scope as "urn:mas:admin" } diff --git a/policies/schema/authorization_grant_input.json b/policies/schema/authorization_grant_input.json index afd230c4..0d5f3f9d 100644 --- a/policies/schema/authorization_grant_input.json +++ b/policies/schema/authorization_grant_input.json @@ -4,22 +4,34 @@ "description": "Input for the authorization grant policy.", "type": "object", "required": [ - "authorization_grant", "client", + "grant_type", + "scope", "user" ], "properties": { - "authorization_grant": { - "type": "object", - "additionalProperties": true - }, "client": { "type": "object", "additionalProperties": true }, + "grant_type": { + "$ref": "#/definitions/GrantType" + }, + "scope": { + "type": "string" + }, "user": { "type": "object", "additionalProperties": true } + }, + "definitions": { + "GrantType": { + "type": "string", + "enum": [ + "authorization_code", + "client_credentials" + ] + } } } \ No newline at end of file