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
Authorization grant policy (#288)
Co-authored-by: Hugh Nimmo-Smith <hughns@matrix.org>
This commit is contained in:
@ -196,6 +196,7 @@ impl Options {
|
||||
config.policy.data.clone().unwrap_or_default(),
|
||||
config.policy.register_entrypoint.clone(),
|
||||
config.policy.client_registration_entrypoint.clone(),
|
||||
config.policy.authorization_grant_entrypoint.clone(),
|
||||
)
|
||||
.await
|
||||
.context("failed to load the policy")?;
|
||||
|
@ -29,6 +29,10 @@ fn default_register_endpoint() -> String {
|
||||
"register/violation".to_string()
|
||||
}
|
||||
|
||||
fn default_authorization_grant_endpoint() -> String {
|
||||
"authorization_grant/violation".to_string()
|
||||
}
|
||||
|
||||
/// Application secrets
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@ -45,6 +49,10 @@ pub struct PolicyConfig {
|
||||
#[serde(default = "default_register_endpoint")]
|
||||
pub register_entrypoint: String,
|
||||
|
||||
/// Entrypoint to use when evaluating authorization grants
|
||||
#[serde(default = "default_authorization_grant_endpoint")]
|
||||
pub authorization_grant_entrypoint: String,
|
||||
|
||||
/// Arbitrary data to pass to the policy
|
||||
#[serde(default)]
|
||||
pub data: Option<serde_json::Value>,
|
||||
@ -56,6 +64,7 @@ impl Default for PolicyConfig {
|
||||
wasm_module: None,
|
||||
client_registration_entrypoint: default_client_registration_endpoint(),
|
||||
register_entrypoint: default_register_endpoint(),
|
||||
authorization_grant_entrypoint: default_authorization_grant_endpoint(),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use axum::{
|
||||
extract::Path,
|
||||
@ -24,6 +26,7 @@ use hyper::StatusCode;
|
||||
use mas_axum_utils::SessionInfoExt;
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{AuthorizationGrant, BrowserSession, TokenType};
|
||||
use mas_policy::PolicyFactory;
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
use mas_storage::{
|
||||
oauth2::{
|
||||
@ -105,6 +108,7 @@ impl From<CallbackDestinationError> for RouteError {
|
||||
}
|
||||
|
||||
pub(crate) async fn get(
|
||||
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
@ -129,7 +133,7 @@ pub(crate) async fn get(
|
||||
return Ok((cookie_jar, mas_router::Login::and_then(continue_grant).go()).into_response());
|
||||
};
|
||||
|
||||
match complete(grant, session, txn).await {
|
||||
match complete(grant, session, &policy_factory, txn).await {
|
||||
Ok(params) => {
|
||||
let res = callback_destination.go(&templates, params).await?;
|
||||
Ok((cookie_jar, res).into_response())
|
||||
@ -139,7 +143,7 @@ pub(crate) async fn get(
|
||||
mas_router::Reauth::and_then(continue_grant).go(),
|
||||
)
|
||||
.into_response()),
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
Err(GrantCompletionError::RequiresConsent | GrantCompletionError::PolicyViolation) => {
|
||||
let next = mas_router::Consent(grant_id);
|
||||
Ok((cookie_jar, next.go()).into_response())
|
||||
}
|
||||
@ -165,6 +169,9 @@ pub enum GrantCompletionError {
|
||||
|
||||
#[error("client lacks consent")]
|
||||
RequiresConsent,
|
||||
|
||||
#[error("denied by the policy")]
|
||||
PolicyViolation,
|
||||
}
|
||||
|
||||
impl From<sqlx::Error> for GrantCompletionError {
|
||||
@ -182,6 +189,7 @@ impl From<InvalidRedirectUriError> for GrantCompletionError {
|
||||
pub(crate) async fn complete(
|
||||
grant: AuthorizationGrant<PostgresqlBackend>,
|
||||
browser_session: BrowserSession<PostgresqlBackend>,
|
||||
policy_factory: &PolicyFactory,
|
||||
mut txn: Transaction<'_, Postgres>,
|
||||
) -> Result<AuthorizationResponse<Option<AccessTokenResponse>>, GrantCompletionError> {
|
||||
// Verify that the grant is in a pending stage
|
||||
@ -195,6 +203,16 @@ pub(crate) async fn complete(
|
||||
return Err(GrantCompletionError::RequiresReauth);
|
||||
}
|
||||
|
||||
// Run through the policy
|
||||
let mut policy = policy_factory.instantiate().await?;
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(&grant, &browser_session.user)
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
return Err(GrantCompletionError::PolicyViolation);
|
||||
}
|
||||
|
||||
let current_consent =
|
||||
fetch_client_consent(&mut txn, &browser_session.user, &grant.client).await?;
|
||||
|
||||
|
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Context};
|
||||
use axum::{
|
||||
extract::{Extension, Form},
|
||||
@ -23,6 +25,7 @@ use mas_axum_utils::SessionInfoExt;
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::{AuthorizationCode, Pkce};
|
||||
use mas_iana::oauth::OAuthAuthorizationEndpointResponseType;
|
||||
use mas_policy::PolicyFactory;
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
use mas_storage::oauth2::{
|
||||
authorization_grant::new_authorization_grant,
|
||||
@ -31,7 +34,7 @@ use mas_storage::oauth2::{
|
||||
use mas_templates::Templates;
|
||||
use oauth2_types::{
|
||||
errors::{
|
||||
CONSENT_REQUIRED, INTERACTION_REQUIRED, INVALID_REQUEST, LOGIN_REQUIRED,
|
||||
ACCESS_DENIED, CONSENT_REQUIRED, INTERACTION_REQUIRED, INVALID_REQUEST, LOGIN_REQUIRED,
|
||||
REGISTRATION_NOT_SUPPORTED, REQUEST_NOT_SUPPORTED, REQUEST_URI_NOT_SUPPORTED, SERVER_ERROR,
|
||||
UNAUTHORIZED_CLIENT,
|
||||
},
|
||||
@ -157,6 +160,7 @@ fn resolve_response_mode(
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub(crate) async fn get(
|
||||
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
@ -306,7 +310,8 @@ pub(crate) async fn get(
|
||||
// Else, we immediately try to complete the authorization grant
|
||||
(Some(user_session), Some(Prompt::None)) => {
|
||||
// With prompt=none, we should get back to the client immediately
|
||||
match self::complete::complete(grant, user_session, txn).await {
|
||||
match self::complete::complete(grant, user_session, &policy_factory, txn).await
|
||||
{
|
||||
Ok(params) => callback_destination.go(&templates, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
callback_destination
|
||||
@ -318,6 +323,9 @@ pub(crate) async fn get(
|
||||
.go(&templates, INTERACTION_REQUIRED)
|
||||
.await?
|
||||
}
|
||||
Err(GrantCompletionError::PolicyViolation) => {
|
||||
callback_destination.go(&templates, ACCESS_DENIED).await?
|
||||
}
|
||||
Err(GrantCompletionError::Anyhow(a)) => return Err(RouteError::Anyhow(a)),
|
||||
Err(GrantCompletionError::Internal(e)) => {
|
||||
return Err(RouteError::Internal(e))
|
||||
@ -331,9 +339,17 @@ pub(crate) async fn get(
|
||||
(Some(user_session), _) => {
|
||||
let grant_id = grant.data;
|
||||
// Else, we show the relevant reauth/consent page if necessary
|
||||
match self::complete::complete(grant, user_session, txn).await {
|
||||
match self::complete::complete(grant, user_session, &policy_factory, txn).await
|
||||
{
|
||||
Ok(params) => callback_destination.go(&templates, params).await?,
|
||||
Err(GrantCompletionError::RequiresConsent) => {
|
||||
Err(
|
||||
GrantCompletionError::RequiresConsent
|
||||
| GrantCompletionError::PolicyViolation,
|
||||
) => {
|
||||
// We're redirecting to the consent URI in both 'consent required' and
|
||||
// 'policy violation' cases, because we reevaluate the policy on this
|
||||
// page, and show the error accordingly
|
||||
// XXX: is this the right approach?
|
||||
mas_router::Consent(grant_id).go().into_response()
|
||||
}
|
||||
Err(GrantCompletionError::RequiresReauth) => {
|
||||
|
@ -12,6 +12,8 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use axum::{
|
||||
extract::{Extension, Form, Path},
|
||||
@ -25,12 +27,13 @@ use mas_axum_utils::{
|
||||
};
|
||||
use mas_config::Encrypter;
|
||||
use mas_data_model::AuthorizationGrantStage;
|
||||
use mas_policy::PolicyFactory;
|
||||
use mas_router::{PostAuthAction, Route};
|
||||
use mas_storage::oauth2::{
|
||||
authorization_grant::{get_grant_by_id, give_consent_to_grant},
|
||||
consent::insert_client_consent,
|
||||
};
|
||||
use mas_templates::{ConsentContext, TemplateContext, Templates};
|
||||
use mas_templates::{ConsentContext, PolicyViolationContext, TemplateContext, Templates};
|
||||
use sqlx::PgPool;
|
||||
use thiserror::Error;
|
||||
|
||||
@ -47,6 +50,7 @@ impl IntoResponse for RouteError {
|
||||
}
|
||||
|
||||
pub(crate) async fn get(
|
||||
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
|
||||
Extension(templates): Extension<Templates>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
@ -73,16 +77,34 @@ pub(crate) async fn get(
|
||||
if let Some(session) = maybe_session {
|
||||
let (csrf_token, cookie_jar) = cookie_jar.csrf_token();
|
||||
|
||||
let ctx = ConsentContext::new(grant, PostAuthAction::continue_grant(grant_id))
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value());
|
||||
let mut policy = policy_factory.instantiate().await?;
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(&grant, &session.user)
|
||||
.await?;
|
||||
|
||||
let content = templates
|
||||
.render_consent(&ctx)
|
||||
.await
|
||||
.context("failed to render template")?;
|
||||
if res.valid() {
|
||||
let ctx = ConsentContext::new(grant, PostAuthAction::continue_grant(grant_id))
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value());
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
let content = templates
|
||||
.render_consent(&ctx)
|
||||
.await
|
||||
.context("failed to render template")?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
} else {
|
||||
let ctx = PolicyViolationContext::new(grant, PostAuthAction::continue_grant(grant_id))
|
||||
.with_session(session)
|
||||
.with_csrf(csrf_token.form_value());
|
||||
|
||||
let content = templates
|
||||
.render_policy_violation(&ctx)
|
||||
.await
|
||||
.context("failed to render template")?;
|
||||
|
||||
Ok((cookie_jar, Html(content)).into_response())
|
||||
}
|
||||
} else {
|
||||
let login = mas_router::Login::and_continue_grant(grant_id);
|
||||
Ok((cookie_jar, login.go()).into_response())
|
||||
@ -90,6 +112,7 @@ pub(crate) async fn get(
|
||||
}
|
||||
|
||||
pub(crate) async fn post(
|
||||
Extension(policy_factory): Extension<Arc<PolicyFactory>>,
|
||||
Extension(pool): Extension<PgPool>,
|
||||
cookie_jar: PrivateCookieJar<Encrypter>,
|
||||
Path(grant_id): Path<i64>,
|
||||
@ -121,6 +144,15 @@ pub(crate) async fn post(
|
||||
return Ok((cookie_jar, login.go()).into_response());
|
||||
};
|
||||
|
||||
let mut policy = policy_factory.instantiate().await?;
|
||||
let res = policy
|
||||
.evaluate_authorization_grant(&grant, &session.user)
|
||||
.await?;
|
||||
|
||||
if !res.valid() {
|
||||
return Err(anyhow::anyhow!("policy violation").into());
|
||||
}
|
||||
|
||||
// Do not consent for the "urn:matrix:device:*" scope
|
||||
let scope_without_device = grant
|
||||
.scope
|
||||
|
@ -9,8 +9,12 @@ else
|
||||
OPA_RW := docker run -i -v $(shell pwd):/policies -w /policies --rm docker.io/openpolicyagent/opa:0.41.0
|
||||
endif
|
||||
|
||||
policy.wasm: client_registration.rego register.rego
|
||||
$(OPA_RW) build -t wasm -e "client_registration/violation" -e "register/violation" $^
|
||||
policy.wasm: client_registration.rego register.rego authorization_grant.rego
|
||||
$(OPA_RW) build -t wasm \
|
||||
-e "client_registration/violation" \
|
||||
-e "register/violation" \
|
||||
-e "authorization_grant/violation" \
|
||||
$^
|
||||
tar xzf bundle.tar.gz /policy.wasm
|
||||
$(RM) bundle.tar.gz
|
||||
touch $@
|
||||
|
38
crates/policy/policies/authorization_grant.rego
Normal file
38
crates/policy/policies/authorization_grant.rego
Normal file
@ -0,0 +1,38 @@
|
||||
package authorization_grant
|
||||
|
||||
import future.keywords.in
|
||||
|
||||
default allow := false
|
||||
|
||||
allow {
|
||||
count(violation) == 0
|
||||
}
|
||||
|
||||
# Special case to make empty scope work
|
||||
allowed_scope("") = true
|
||||
|
||||
allowed_scope("openid") = true
|
||||
|
||||
allowed_scope("email") = true
|
||||
|
||||
allowed_scope("urn:synapse:admin:*") {
|
||||
some user in data.admin_users
|
||||
input.user.username == user
|
||||
}
|
||||
|
||||
allowed_scope(scope) {
|
||||
regex.match("urn:matrix:device:[A-Za-z0-9-]{10,}", scope)
|
||||
}
|
||||
|
||||
allowed_scope("urn:matrix:api:*") = true
|
||||
|
||||
violation[{"msg": msg}] {
|
||||
some scope in split(input.authorization_grant.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, " ")
|
||||
count({key | scope_list[key]; startswith(scope_list[key], "urn:matrix:device:")}) > 1
|
||||
}
|
63
crates/policy/policies/authorization_grant_test.rego
Normal file
63
crates/policy/policies/authorization_grant_test.rego
Normal file
@ -0,0 +1,63 @@
|
||||
package authorization_grant
|
||||
|
||||
user := {"username": "john"}
|
||||
|
||||
test_standard_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "openid"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "email"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "openid email"}
|
||||
|
||||
# Not supported yet
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "phone"}
|
||||
|
||||
# Not supported yet
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "profile"}
|
||||
}
|
||||
|
||||
test_matrix_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:api:*"}
|
||||
}
|
||||
|
||||
test_device_scopes {
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AAbbCCdd01"}
|
||||
|
||||
allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AAbbCCdd01-asdasdsa1-2313"}
|
||||
|
||||
# Invalid characters
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AABB:CCDDEE"}
|
||||
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AABB*CCDDEE"}
|
||||
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AABB!CCDDEE"}
|
||||
|
||||
# Too short
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:abcd"}
|
||||
|
||||
# Multiple device scope
|
||||
not allow with input.user as user
|
||||
with input.authorization_grant as {"scope": "urn:matrix:device:AAbbCCdd01 urn:matrix:device:AAbbCCdd02"}
|
||||
}
|
||||
|
||||
test_synapse_admin_scopes {
|
||||
allow with input.user as user
|
||||
with data.admin_users as ["john"]
|
||||
with input.authorization_grant as {"scope": "urn:synapse:admin:*"}
|
||||
|
||||
not allow with input.user as user
|
||||
with data.admin_users as []
|
||||
with input.authorization_grant as {"scope": "urn:synapse:admin:*"}
|
||||
}
|
@ -15,6 +15,7 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::bail;
|
||||
use mas_data_model::{AuthorizationGrant, StorageBackend, User};
|
||||
use oauth2_types::registration::ClientMetadata;
|
||||
use opa_wasm::Runtime;
|
||||
use serde::Deserialize;
|
||||
@ -52,6 +53,7 @@ pub struct PolicyFactory {
|
||||
data: serde_json::Value,
|
||||
register_entrypoint: String,
|
||||
client_registration_entrypoint: String,
|
||||
authorization_grant_endpoint: String,
|
||||
}
|
||||
|
||||
impl PolicyFactory {
|
||||
@ -60,6 +62,7 @@ impl PolicyFactory {
|
||||
data: serde_json::Value,
|
||||
register_entrypoint: String,
|
||||
client_registration_entrypoint: String,
|
||||
authorization_grant_endpoint: String,
|
||||
) -> Result<Self, LoadError> {
|
||||
let mut config = Config::default();
|
||||
config.async_support(true);
|
||||
@ -84,6 +87,7 @@ impl PolicyFactory {
|
||||
data,
|
||||
register_entrypoint,
|
||||
client_registration_entrypoint,
|
||||
authorization_grant_endpoint,
|
||||
};
|
||||
|
||||
// Try to instanciate
|
||||
@ -105,6 +109,7 @@ impl PolicyFactory {
|
||||
for e in [
|
||||
self.register_entrypoint.as_str(),
|
||||
self.client_registration_entrypoint.as_str(),
|
||||
self.authorization_grant_endpoint.as_str(),
|
||||
] {
|
||||
if !entrypoints.contains(e) {
|
||||
bail!("missing entrypoint {e}")
|
||||
@ -118,6 +123,7 @@ impl PolicyFactory {
|
||||
instance,
|
||||
register_entrypoint: self.register_entrypoint.clone(),
|
||||
client_registration_entrypoint: self.client_registration_entrypoint.clone(),
|
||||
authorization_grant_endpoint: self.authorization_grant_endpoint.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -146,6 +152,7 @@ pub struct Policy {
|
||||
instance: opa_wasm::Policy,
|
||||
register_entrypoint: String,
|
||||
client_registration_entrypoint: String,
|
||||
authorization_grant_endpoint: String,
|
||||
}
|
||||
|
||||
impl Policy {
|
||||
@ -193,6 +200,27 @@ impl Policy {
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn evaluate_authorization_grant<T: StorageBackend + std::fmt::Debug>(
|
||||
&mut self,
|
||||
authorization_grant: &AuthorizationGrant<T>,
|
||||
user: &User<T>,
|
||||
) -> Result<EvaluationResult, anyhow::Error> {
|
||||
let authorization_grant = serde_json::to_value(authorization_grant)?;
|
||||
let user = serde_json::to_value(user)?;
|
||||
let input = serde_json::json!({
|
||||
"authorization_grant": authorization_grant,
|
||||
"user": user,
|
||||
});
|
||||
|
||||
let [res]: [EvaluationResult; 1] = self
|
||||
.instance
|
||||
.evaluate(&mut self.store, &self.authorization_grant_endpoint, &input)
|
||||
.await?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -209,6 +237,7 @@ mod tests {
|
||||
}),
|
||||
"register/violation".to_string(),
|
||||
"client_registration/violation".to_string(),
|
||||
"authorization_grant/violation".to_string(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -414,6 +414,37 @@ impl ConsentContext {
|
||||
}
|
||||
}
|
||||
|
||||
/// Context used by the `policy_violation.html` template
|
||||
#[derive(Serialize)]
|
||||
pub struct PolicyViolationContext {
|
||||
grant: AuthorizationGrant<()>,
|
||||
action: PostAuthAction,
|
||||
}
|
||||
|
||||
impl TemplateContext for PolicyViolationContext {
|
||||
fn sample() -> Vec<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
// TODO
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
impl PolicyViolationContext {
|
||||
/// Constructs a context for the policy violation page
|
||||
#[must_use]
|
||||
pub fn new<T>(grant: T, action: PostAuthAction) -> Self
|
||||
where
|
||||
T: Into<AuthorizationGrant<()>>,
|
||||
{
|
||||
Self {
|
||||
grant: grant.into(),
|
||||
action,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields of the reauthentication form
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
|
@ -47,9 +47,9 @@ pub use self::{
|
||||
context::{
|
||||
AccountContext, AccountEmailsContext, CompatSsoContext, ConsentContext, EmailAddContext,
|
||||
EmailVerificationContext, EmailVerificationPageContext, EmptyContext, ErrorContext,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, PostAuthContext,
|
||||
ReauthContext, ReauthFormField, RegisterContext, RegisterFormField, TemplateContext,
|
||||
WithCsrf, WithOptionalSession, WithSession,
|
||||
FormPostContext, IndexContext, LoginContext, LoginFormField, PolicyViolationContext,
|
||||
PostAuthContext, ReauthContext, ReauthFormField, RegisterContext, RegisterFormField,
|
||||
TemplateContext, WithCsrf, WithOptionalSession, WithSession,
|
||||
},
|
||||
forms::{FieldError, FormError, FormField, FormState, ToFormState},
|
||||
};
|
||||
@ -299,7 +299,10 @@ register_templates! {
|
||||
/// Render the client consent page
|
||||
pub fn render_consent(WithCsrf<WithSession<ConsentContext>>) { "pages/consent.html" }
|
||||
|
||||
/// Render the client consent page
|
||||
/// Render the policy violation page
|
||||
pub fn render_policy_violation(WithCsrf<WithSession<PolicyViolationContext>>) { "pages/policy_violation.html" }
|
||||
|
||||
/// Render the legacy SSO login consent page
|
||||
pub fn render_sso_login(WithCsrf<WithSession<CompatSsoContext>>) { "pages/sso.html" }
|
||||
|
||||
/// Render the home page
|
||||
@ -346,6 +349,7 @@ impl Templates {
|
||||
check::render_login(self).await?;
|
||||
check::render_register(self).await?;
|
||||
check::render_consent(self).await?;
|
||||
check::render_policy_violation(self).await?;
|
||||
check::render_sso_login(self).await?;
|
||||
check::render_index(self).await?;
|
||||
check::render_account_index(self).await?;
|
||||
|
53
crates/templates/src/res/pages/policy_violation.html
Normal file
53
crates/templates/src/res/pages/policy_violation.html
Normal file
@ -0,0 +1,53 @@
|
||||
{#
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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.
|
||||
#}
|
||||
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<section class="flex items-center justify-center flex-1">
|
||||
<div class="w-96 m-2">
|
||||
<div class="grid grid-cols-1 gap-6">
|
||||
<h1 class="text-xl font-bold">The authorization request was denied the policy enforced by this service.</h1>
|
||||
<p>This might be because of the client which authored the request, the currently logged in user, or the request itself.</p>
|
||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
||||
<div class="bg-white rounded w-16 h-16 overflow-hidden mx-auto">
|
||||
{% if grant.client.logo_uri %}
|
||||
<img class="w-16 h-16" src="{{ grant.client.logo_uri }}" />
|
||||
{% endif %}
|
||||
</div>
|
||||
<h1 class="text-lg text-center font-medium flex-1"><a target="_blank" href="{{ grant.client.client_uri }}" class="text-accent">{{ grant.client.client_name | default(value=grant.client.client_id) }}</a></h1>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg bg-grey-25 dark:bg-grey-450 p-2 flex items-center">
|
||||
<div class="text-center flex-1">
|
||||
Logged as <span class="font-bold">{{ current_session.user.username }}</span>
|
||||
</div>
|
||||
|
||||
{{ logout::button(text="Sign out", class=button::plain_error_class(), csrf_token=csrf_token, post_logout_action=action) }}
|
||||
</div>
|
||||
|
||||
{{ back_to_client::link(
|
||||
text="Cancel",
|
||||
class=button::outline_error_class(),
|
||||
uri=grant.redirect_uri,
|
||||
mode=grant.response_mode,
|
||||
params=dict(error="access_denied", state=grant.state)
|
||||
) }}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
Reference in New Issue
Block a user