Move AUTHENTICATE
clause to DEFINE ACCESS
statement root (#4385)
This commit is contained in:
parent
63fcde0b80
commit
d030c7d498
12 changed files with 807 additions and 123 deletions
|
@ -35,9 +35,9 @@ pub static EXPORT_BATCH_SIZE: Lazy<u32> = lazy_env_parse!("SURREAL_EXPORT_BATCH_
|
|||
pub static MAX_STREAM_BATCH_SIZE: Lazy<u32> =
|
||||
lazy_env_parse!("SURREAL_MAX_STREAM_BATCH_SIZE", u32, 1000);
|
||||
|
||||
/// Forward all signup/signin query errors to a client performing record access. Do not use in production.
|
||||
pub static INSECURE_FORWARD_RECORD_ACCESS_ERRORS: Lazy<bool> =
|
||||
lazy_env_parse!("SURREAL_INSECURE_FORWARD_RECORD_ACCESS_ERRORS", bool, false);
|
||||
/// Forward all signup/signin/authenticate query errors to a client performing authentication. Do not use in production.
|
||||
pub static INSECURE_FORWARD_ACCESS_ERRORS: Lazy<bool> =
|
||||
lazy_env_parse!("SURREAL_INSECURE_FORWARD_ACCESS_ERRORS", bool, false);
|
||||
|
||||
#[cfg(any(
|
||||
feature = "kv-mem",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use super::verify::{verify_db_creds, verify_ns_creds, verify_root_creds};
|
||||
use super::{Actor, Level};
|
||||
use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
|
||||
use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
|
||||
use crate::dbs::Session;
|
||||
use crate::err::Error;
|
||||
use crate::iam::issue::{config, expiration};
|
||||
|
@ -152,7 +152,7 @@ pub async fn db_access(
|
|||
..Claims::default()
|
||||
};
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = at.authenticate {
|
||||
if let Some(au) = &av.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess =
|
||||
Session::editor().with_ns(&ns).with_db(&db);
|
||||
|
@ -161,21 +161,21 @@ pub async fn db_access(
|
|||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
// Compute the value with the params
|
||||
match kvs.evaluate(au, &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
match kvs.evaluate(au.clone(), &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
Some(id) => {
|
||||
// Update rid with result from AUTHENTICATE clause
|
||||
rid = id;
|
||||
}
|
||||
_ => return Err(Error::InvalidAuth),
|
||||
},
|
||||
Err(e) => {
|
||||
return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
Err(e) => return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
_ => Err(Error::InvalidAuth),
|
||||
},
|
||||
}
|
||||
}
|
||||
// Log the authenticated access method info
|
||||
|
@ -207,7 +207,7 @@ pub async fn db_access(
|
|||
}
|
||||
Err(e) => match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::AccessRecordSigninQueryFailed),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::cnf::{INSECURE_FORWARD_RECORD_ACCESS_ERRORS, SERVER_NAME};
|
||||
use crate::cnf::{INSECURE_FORWARD_ACCESS_ERRORS, SERVER_NAME};
|
||||
use crate::dbs::Session;
|
||||
use crate::err::Error;
|
||||
use crate::iam::issue::{config, expiration};
|
||||
|
@ -96,7 +96,7 @@ pub async fn db_access(
|
|||
..Claims::default()
|
||||
};
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = at.authenticate {
|
||||
if let Some(au) = &av.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess =
|
||||
Session::editor().with_ns(&ns).with_db(&db);
|
||||
|
@ -105,21 +105,21 @@ pub async fn db_access(
|
|||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
// Compute the value with the params
|
||||
match kvs.evaluate(au, &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
match kvs.evaluate(au.clone(), &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
Some(id) => {
|
||||
// Update rid with result from AUTHENTICATE clause
|
||||
rid = id;
|
||||
}
|
||||
_ => return Err(Error::InvalidAuth),
|
||||
},
|
||||
Err(e) => {
|
||||
return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
Err(e) => return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
_ => Err(Error::InvalidAuth),
|
||||
},
|
||||
}
|
||||
}
|
||||
// Log the authenticated access method info
|
||||
|
@ -151,7 +151,7 @@ pub async fn db_access(
|
|||
}
|
||||
Err(e) => match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::AccessRecordSignupQueryFailed),
|
||||
},
|
||||
}
|
||||
|
|
|
@ -20,6 +20,10 @@ pub struct Claims {
|
|||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub iss: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sub: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub aud: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub jti: Option<String>,
|
||||
#[serde(alias = "ns")]
|
||||
#[serde(alias = "NS")]
|
||||
|
@ -70,6 +74,14 @@ impl From<Claims> for Value {
|
|||
if let Some(iss) = v.iss {
|
||||
out.insert("iss".to_string(), iss.into());
|
||||
}
|
||||
// Add sub field if set
|
||||
if let Some(sub) = v.sub {
|
||||
out.insert("sub".to_string(), sub.into());
|
||||
}
|
||||
// Add aud field if set
|
||||
if let Some(aud) = v.aud {
|
||||
out.insert("aud".to_string(), aud.into());
|
||||
}
|
||||
// Add iat field if set
|
||||
if let Some(iat) = v.iat {
|
||||
out.insert("iat".to_string(), iat.into());
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::cnf::INSECURE_FORWARD_RECORD_ACCESS_ERRORS;
|
||||
use crate::cnf::INSECURE_FORWARD_ACCESS_ERRORS;
|
||||
use crate::dbs::Session;
|
||||
use crate::err::Error;
|
||||
#[cfg(feature = "jwks")]
|
||||
|
@ -6,7 +6,7 @@ use crate::iam::jwks;
|
|||
use crate::iam::{issue::expiration, token::Claims, Actor, Auth, Level, Role};
|
||||
use crate::kvs::{Datastore, LockType::*, TransactionType::*};
|
||||
use crate::sql::access_type::{AccessType, JwtAccessVerify};
|
||||
use crate::sql::{statements::DefineUserStatement, Algorithm, Value};
|
||||
use crate::sql::{statements::DefineUserStatement, Algorithm, Thing, Value};
|
||||
use crate::syn;
|
||||
use argon2::{Argon2, PasswordHash, PasswordVerifier};
|
||||
use chrono::Utc;
|
||||
|
@ -157,53 +157,33 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Ensure that the transaction is cancelled
|
||||
tx.cancel().await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
let (au, cf) = match de.kind.clone() {
|
||||
AccessType::Record(at) => {
|
||||
let cf = match at.jwt.verify.clone() {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
if let Some(kid) = token_data.header.kid {
|
||||
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
|
||||
} else {
|
||||
Err(Error::MissingTokenHeader("kid".to_string()))
|
||||
}
|
||||
let cf = match &de.kind {
|
||||
AccessType::Record(at) => match &at.jwt.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
if let Some(kid) = token_data.header.kid {
|
||||
jwks::config(kvs, &kid, &jwks.url, token_data.header.alg).await
|
||||
} else {
|
||||
Err(Error::MissingTokenHeader("kid".to_string()))
|
||||
}
|
||||
#[cfg(not(feature = "jwks"))]
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?;
|
||||
|
||||
(at.authenticate, cf)
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "jwks"))]
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?,
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
};
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = au {
|
||||
if let Some(au) = &de.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
|
||||
sess.rd = Some(rid.clone().into());
|
||||
sess.tk = Some(token_data.claims.clone().into());
|
||||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
// Compute the value with the params
|
||||
match kvs.evaluate(au, &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
Some(id) => {
|
||||
// Update rid with result from AUTHENTICATE clause
|
||||
rid = id;
|
||||
}
|
||||
_ => return Err(Error::InvalidAuth),
|
||||
},
|
||||
Err(e) => {
|
||||
return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
}
|
||||
}
|
||||
}
|
||||
rid = authenticate_record(kvs, &sess, au.clone()).await?;
|
||||
}
|
||||
// Log the success
|
||||
debug!("Authenticated with record access method `{}`", ac);
|
||||
|
@ -238,10 +218,10 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Ensure that the transaction is cancelled
|
||||
tx.cancel().await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
match de.kind.clone() {
|
||||
match &de.kind {
|
||||
// If the access type is Jwt, this is database access
|
||||
AccessType::Jwt(at) => {
|
||||
let cf = match at.verify {
|
||||
let cf = match &at.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
|
@ -254,9 +234,17 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
#[cfg(not(feature = "jwks"))]
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?;
|
||||
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = &de.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess = Session::editor().with_ns(&ns).with_db(&db);
|
||||
sess.tk = Some(token_data.claims.clone().into());
|
||||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
authenticate_jwt(kvs, &sess, au.clone()).await?;
|
||||
}
|
||||
// Parse the roles
|
||||
let roles = match token_data.claims.roles {
|
||||
// If no role is provided, grant the viewer role
|
||||
|
@ -286,10 +274,10 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// If the access type is Record, this is record access
|
||||
// Record access without an "id" claim is only possible if there is an AUTHENTICATE clause
|
||||
// The clause can make up for the missing "id" claim by resolving other claims to a specific record
|
||||
AccessType::Record(at) => match at.authenticate {
|
||||
AccessType::Record(at) => match &de.authenticate {
|
||||
Some(au) => {
|
||||
trace!("Access method `{}` is record access with authenticate clause", ac);
|
||||
let cf = match at.jwt.verify {
|
||||
let cf = match &at.jwt.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
|
@ -311,21 +299,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
sess.tk = Some(token_data.claims.clone().into());
|
||||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
// Compute the value with the params
|
||||
let rid = match kvs.evaluate(au, &sess, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
// If found, return record identifier from AUTHENTICATE clause
|
||||
Some(id) => id,
|
||||
_ => return Err(Error::InvalidAuth),
|
||||
},
|
||||
Err(e) => {
|
||||
return match e {
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_RECORD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
}
|
||||
}
|
||||
};
|
||||
let rid = authenticate_record(kvs, &sess, au.clone()).await?;
|
||||
// Log the success
|
||||
debug!("Authenticated with record access method `{}`", ac);
|
||||
// Set the session
|
||||
|
@ -397,8 +371,8 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Ensure that the transaction is cancelled
|
||||
tx.cancel().await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
let cf = match de.kind.clone() {
|
||||
AccessType::Jwt(ac) => match ac.verify {
|
||||
let cf = match &de.kind {
|
||||
AccessType::Jwt(ac) => match &ac.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
|
@ -415,6 +389,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
}?;
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = &de.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess = Session::editor().with_ns(&ns);
|
||||
sess.tk = Some(token_data.claims.clone().into());
|
||||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
authenticate_jwt(kvs, &sess, au.clone()).await?;
|
||||
}
|
||||
// Parse the roles
|
||||
let roles = match token_data.claims.roles {
|
||||
// If no role is provided, grant the viewer role
|
||||
|
@ -486,8 +469,8 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Ensure that the transaction is cancelled
|
||||
tx.cancel().await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
let cf = match de.kind.clone() {
|
||||
AccessType::Jwt(ac) => match ac.verify {
|
||||
let cf = match &de.kind {
|
||||
AccessType::Jwt(ac) => match &ac.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key.as_bytes()),
|
||||
#[cfg(feature = "jwks")]
|
||||
JwtAccessVerify::Jwks(jwks) => {
|
||||
|
@ -504,6 +487,15 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
}?;
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = &de.authenticate {
|
||||
// Setup the system session for finding the signin record
|
||||
let mut sess = Session::editor();
|
||||
sess.tk = Some(token_data.claims.clone().into());
|
||||
sess.ip.clone_from(&session.ip);
|
||||
sess.or.clone_from(&session.or);
|
||||
authenticate_jwt(kvs, &sess, au.clone()).await?;
|
||||
}
|
||||
// Parse the roles
|
||||
let roles = match token_data.claims.roles {
|
||||
// If no role is provided, grant the viewer role
|
||||
|
@ -641,6 +633,52 @@ fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
|
|||
}
|
||||
}
|
||||
|
||||
// Execute the AUTHENTICATE clause for a record access method
|
||||
async fn authenticate_record(
|
||||
kvs: &Datastore,
|
||||
session: &Session,
|
||||
authenticate: Value,
|
||||
) -> Result<Thing, Error> {
|
||||
match kvs.evaluate(authenticate, session, None).await {
|
||||
Ok(val) => match val.record() {
|
||||
// If the AUTHENTICATE clause returns a record, authentication continues with that record
|
||||
Some(id) => Ok(id),
|
||||
// If the AUTHENTICATE clause returns anything else, authentication fails generically
|
||||
_ => Err(Error::InvalidAuth),
|
||||
},
|
||||
Err(e) => match e {
|
||||
// If the AUTHENTICATE clause throws a specific error, authentication fails with that error
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Execute the AUTHENTICATE clause for a JWT access method
|
||||
async fn authenticate_jwt(
|
||||
kvs: &Datastore,
|
||||
session: &Session,
|
||||
authenticate: Value,
|
||||
) -> Result<(), Error> {
|
||||
match kvs.evaluate(authenticate, session, None).await {
|
||||
Ok(val) => {
|
||||
match val {
|
||||
// If the AUTHENTICATE clause returns nothing, authentication continues
|
||||
Value::None => Ok(()),
|
||||
// If the AUTHENTICATE clause returns anything else, authentication fails generically
|
||||
_ => Err(Error::InvalidAuth),
|
||||
}
|
||||
}
|
||||
Err(e) => match e {
|
||||
// If the AUTHENTICATE clause throws a specific error, authentication fails with that error
|
||||
Error::Thrown(_) => Err(e),
|
||||
e if *INSECURE_FORWARD_ACCESS_ERRORS => Err(e),
|
||||
_ => Err(Error::InvalidAuth),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -1943,11 +1981,11 @@ mod tests {
|
|||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE (
|
||||
-- Simple example increasing the record identifier by one
|
||||
SELECT * FROM type::thing('user', meta::id($auth) + 1)
|
||||
)
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
|
@ -2008,10 +2046,10 @@ mod tests {
|
|||
SIGNIN (
|
||||
SELECT * FROM type::thing('user', $id)
|
||||
)
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE (
|
||||
SELECT id FROM user WHERE email = $token.email
|
||||
)
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
|
@ -2096,6 +2134,7 @@ mod tests {
|
|||
ds.execute(
|
||||
format!(r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
|
||||
IF !$auth.enabled {{
|
||||
|
@ -2105,7 +2144,6 @@ mod tests {
|
|||
-- Always need to return the user id back, otherwise auth generically fails
|
||||
RETURN $auth;
|
||||
}}
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
|
@ -2157,8 +2195,8 @@ mod tests {
|
|||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
AUTHENTICATE {{}}
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
|
@ -2190,4 +2228,547 @@ mod tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_db_authenticate_clause() {
|
||||
// Test with correct "iss" and "aud" claims
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("surrealdb-test".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.db, Some("test".to_string()));
|
||||
assert_eq!(sess.ac, Some("user".to_string()));
|
||||
assert!(sess.au.is_db());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
// Record users should not have roles
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to follow the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to invalid token audience, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
// In this case, something is returned by the clause, which returns a generic error
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::InvalidAuth) => {} // ok
|
||||
res => panic!(
|
||||
"Expected a generic authentication error, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_ns_authenticate_clause() {
|
||||
// Test with correct "iss" and "aud" claims
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("surrealdb-test".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON NAMESPACE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.ac, Some("user".to_string()));
|
||||
assert!(sess.au.is_ns());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), None);
|
||||
// Record users should not have roles
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to follow the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON NAMESPACE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to invalid token audience, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
// In this case, something is returned by the clause, which returns a generic error
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ns: Some("test".to_string()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON NAMESPACE TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::InvalidAuth) => {} // ok
|
||||
res => panic!(
|
||||
"Expected a generic authentication error, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_root_authenticate_clause() {
|
||||
// Test with correct "iss" and "aud" claims
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("surrealdb-test".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON ROOT TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with token: {:?}", res);
|
||||
assert_eq!(sess.ac, Some("user".to_string()));
|
||||
assert!(sess.au.is_root());
|
||||
assert_eq!(sess.au.level().ns(), None);
|
||||
assert_eq!(sess.au.level().db(), None);
|
||||
// Record users should not have roles
|
||||
assert!(sess.au.has_role(&Role::Viewer), "Auth user expected to have Viewer role");
|
||||
assert!(!sess.au.has_role(&Role::Editor), "Auth user expected to not have Editor role");
|
||||
assert!(!sess.au.has_role(&Role::Owner), "Auth user expected to not have Owner role");
|
||||
// Expiration should match the defined duration
|
||||
let exp = sess.exp.unwrap();
|
||||
// Expiration should match the current time plus session duration with some margin
|
||||
let min_exp = (Utc::now() + Duration::hours(2) - Duration::seconds(10)).timestamp();
|
||||
let max_exp = (Utc::now() + Duration::hours(2) + Duration::seconds(10)).timestamp();
|
||||
assert!(
|
||||
exp > min_exp && exp < max_exp,
|
||||
"Session expiration is expected to follow the defined duration"
|
||||
);
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON ROOT TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ THROW "Invalid token issuer" }};
|
||||
IF $token.aud != "surrealdb-test" {{ THROW "Invalid token audience" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::Thrown(e)) if e == "Invalid token audience" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to invalid token audience, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test with correct "iss" claim but incorrect "aud" claim
|
||||
// In this case, something is returned by the clause, which returns a generic error
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: Some("surrealdb-test".to_string()),
|
||||
aud: Some("invalid".to_string()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
|
||||
ac: Some("user".to_string()),
|
||||
..Claims::default()
|
||||
};
|
||||
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
format!(
|
||||
r#"
|
||||
DEFINE ACCESS user ON ROOT TYPE JWT
|
||||
ALGORITHM HS512 KEY '{secret}'
|
||||
AUTHENTICATE {{
|
||||
IF $token.iss != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
IF $token.aud != "surrealdb-test" {{ RETURN "FAIL" }};
|
||||
}}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Prepare the claims object
|
||||
let mut claims = claims.clone();
|
||||
claims.roles = None;
|
||||
// Create the token
|
||||
let enc = encode(&HEADER, &claims, &key).unwrap();
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
let res = token(&ds, &mut sess, &enc).await;
|
||||
|
||||
match res {
|
||||
Err(Error::InvalidAuth) => {} // ok
|
||||
res => panic!(
|
||||
"Expected a generic authentication error, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ use crate::sql::statements::info::InfoStructure;
|
|||
use crate::sql::statements::DefineAccessStatement;
|
||||
use crate::sql::{escape::quote_str, Algorithm};
|
||||
use revision::revisioned;
|
||||
use revision::Error as RevisionError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::fmt::Display;
|
||||
|
@ -40,9 +41,6 @@ impl Display for AccessType {
|
|||
if let Some(ref v) = ac.signin {
|
||||
write!(f, " SIGNIN {v}")?
|
||||
}
|
||||
if let Some(ref v) = ac.authenticate {
|
||||
write!(f, " AUTHENTICATE {v}")?
|
||||
}
|
||||
write!(f, " WITH JWT {}", ac.jwt)?;
|
||||
}
|
||||
}
|
||||
|
@ -62,7 +60,6 @@ impl InfoStructure for AccessType {
|
|||
"jwt".to_string() => v.jwt.structure(),
|
||||
"signup".to_string(), if let Some(v) = v.signup => v.structure(),
|
||||
"signin".to_string(), if let Some(v) = v.signin => v.structure(),
|
||||
"authenticate".to_string(), if let Some(v) = v.authenticate => v.structure(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -260,26 +257,42 @@ pub struct JwtAccessVerifyJwks {
|
|||
pub url: String,
|
||||
}
|
||||
|
||||
#[revisioned(revision = 2)]
|
||||
#[revisioned(revision = 3)]
|
||||
#[derive(Debug, Serialize, Deserialize, Hash, Clone, Eq, PartialEq, PartialOrd)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
pub struct RecordAccess {
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
#[revision(start = 2)]
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
#[revision(start = 2, end = 3, convert_fn = "authenticate_revision")]
|
||||
pub authenticate: Option<Value>,
|
||||
}
|
||||
|
||||
impl RecordAccess {
|
||||
fn authenticate_revision(
|
||||
&self,
|
||||
_revision: u16,
|
||||
_value: Option<Value>,
|
||||
) -> Result<(), RevisionError> {
|
||||
Err(RevisionError::Conversion(
|
||||
"The \"AUTHENTICATE\" clause has been moved to \"DEFINE ACCESS\"".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RecordAccess {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
..Default::default()
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ use revision::revisioned;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{self, Display};
|
||||
|
||||
#[revisioned(revision = 1)]
|
||||
#[revisioned(revision = 2)]
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
|
||||
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
||||
#[non_exhaustive]
|
||||
|
@ -20,6 +20,8 @@ pub struct DefineAccessStatement {
|
|||
pub name: Ident,
|
||||
pub base: Base,
|
||||
pub kind: AccessType,
|
||||
#[revision(start = 2)]
|
||||
pub authenticate: Option<Value>,
|
||||
pub duration: AccessDuration,
|
||||
pub comment: Option<Strand>,
|
||||
pub if_not_exists: bool,
|
||||
|
@ -168,6 +170,10 @@ impl Display for DefineAccessStatement {
|
|||
}
|
||||
// The specific access method definition is displayed by AccessType
|
||||
write!(f, " {} ON {} TYPE {}", self.name, self.base, self.kind)?;
|
||||
// The additional authentication clause
|
||||
if let Some(ref v) = self.authenticate {
|
||||
write!(f, " AUTHENTICATE {v}")?
|
||||
}
|
||||
// Always print relevant durations so defaults can be changed in the future
|
||||
// If default values were not printed, exports would not be forward compatible
|
||||
// None values need to be printed, as they are different from the default values
|
||||
|
@ -212,6 +218,7 @@ impl InfoStructure for DefineAccessStatement {
|
|||
Value::from(map! {
|
||||
"name".to_string() => self.name.structure(),
|
||||
"base".to_string() => self.base.structure(),
|
||||
"authenticate".to_string(), if let Some(v) = self.authenticate => v.structure(),
|
||||
"duration".to_string() => Value::from(map!{
|
||||
"session".to_string() => self.duration.session.into(),
|
||||
"grant".to_string(), if self.kind.can_issue_grants() => self.duration.grant.into(),
|
||||
|
|
|
@ -83,8 +83,10 @@ impl ser::Serializer for SerializerRecord {
|
|||
pub struct SerializeRecord {
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub authenticate: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
pub authenticate: Option<Value>,
|
||||
}
|
||||
|
||||
impl serde::ser::SerializeStruct for SerializeRecord {
|
||||
|
@ -102,12 +104,14 @@ impl serde::ser::SerializeStruct for SerializeRecord {
|
|||
"signin" => {
|
||||
self.signin = value.serialize(ser::value::opt::Serializer.wrap())?;
|
||||
}
|
||||
"authenticate" => {
|
||||
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
|
||||
}
|
||||
"jwt" => {
|
||||
self.jwt = value.serialize(SerializerJwt.wrap())?;
|
||||
}
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
"authenticate" => {
|
||||
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
|
||||
}
|
||||
key => {
|
||||
return Err(Error::custom(format!("unexpected field `RecordAccess::{key}`")));
|
||||
}
|
||||
|
@ -119,8 +123,10 @@ impl serde::ser::SerializeStruct for SerializeRecord {
|
|||
Ok(RecordAccess {
|
||||
signup: self.signup,
|
||||
signin: self.signin,
|
||||
authenticate: self.authenticate,
|
||||
jwt: self.jwt,
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ use crate::sql::Base;
|
|||
use crate::sql::Duration;
|
||||
use crate::sql::Ident;
|
||||
use crate::sql::Strand;
|
||||
use crate::sql::Value;
|
||||
use ser::Serializer as _;
|
||||
use serde::ser::Error as _;
|
||||
use serde::ser::Impossible;
|
||||
|
@ -45,6 +46,7 @@ pub struct SerializeDefineAccessStatement {
|
|||
name: Ident,
|
||||
base: Base,
|
||||
kind: AccessType,
|
||||
authenticate: Option<Value>,
|
||||
duration: AccessDuration,
|
||||
comment: Option<Strand>,
|
||||
if_not_exists: bool,
|
||||
|
@ -68,6 +70,9 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
|
|||
"kind" => {
|
||||
self.kind = value.serialize(ser::access_type::Serializer.wrap())?;
|
||||
}
|
||||
"authenticate" => {
|
||||
self.authenticate = value.serialize(ser::value::opt::Serializer.wrap())?;
|
||||
}
|
||||
"duration" => {
|
||||
self.duration = value.serialize(SerializerDuration.wrap())?;
|
||||
}
|
||||
|
@ -91,6 +96,7 @@ impl serde::ser::SerializeStruct for SerializeDefineAccessStatement {
|
|||
name: self.name,
|
||||
base: self.base,
|
||||
kind: self.kind,
|
||||
authenticate: self.authenticate,
|
||||
duration: self.duration,
|
||||
comment: self.comment,
|
||||
if_not_exists: self.if_not_exists,
|
||||
|
|
|
@ -310,11 +310,6 @@ impl Parser<'_> {
|
|||
ac.signin =
|
||||
Some(stk.run(|stk| self.parse_value(stk)).await?);
|
||||
}
|
||||
t!("AUTHENTICATE") => {
|
||||
self.pop_peek();
|
||||
ac.authenticate =
|
||||
Some(stk.run(|stk| self.parse_value(stk)).await?);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
@ -327,6 +322,10 @@ impl Parser<'_> {
|
|||
_ => break,
|
||||
}
|
||||
}
|
||||
t!("AUTHENTICATE") => {
|
||||
self.pop_peek();
|
||||
res.authenticate = Some(stk.run(|stk| self.parse_value(stk)).await?);
|
||||
}
|
||||
t!("DURATION") => {
|
||||
self.pop_peek();
|
||||
while self.eat(t!("FOR")) {
|
||||
|
@ -528,10 +527,6 @@ impl Parser<'_> {
|
|||
self.pop_peek();
|
||||
ac.signin = Some(stk.run(|stk| self.parse_value(stk)).await?);
|
||||
}
|
||||
t!("AUTHENTICATE") => {
|
||||
self.pop_peek();
|
||||
ac.authenticate = Some(stk.run(|stk| self.parse_value(stk)).await?);
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -366,6 +366,7 @@ fn parse_define_token() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -441,6 +442,7 @@ fn parse_define_token_jwks() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -566,6 +568,7 @@ fn parse_define_access_jwt_key() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -599,6 +602,41 @@ fn parse_define_access_jwt_key() {
|
|||
key: "bar".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_hours(1)),
|
||||
session: None,
|
||||
},
|
||||
comment: None,
|
||||
if_not_exists: false,
|
||||
})),
|
||||
)
|
||||
}
|
||||
// Asymmetric verify and issue with authenticate clause.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DATABASE TYPE JWT ALGORITHM EDDSA KEY "foo" WITH ISSUER KEY "bar" AUTHENTICATE true"#
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
res,
|
||||
Statement::Define(DefineStatement::Access(DefineAccessStatement {
|
||||
name: Ident("a".to_string()),
|
||||
base: Base::Db,
|
||||
kind: AccessType::Jwt(JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::EdDSA,
|
||||
key: "foo".to_string(),
|
||||
}),
|
||||
issue: Some(JwtAccessIssue {
|
||||
alg: Algorithm::EdDSA,
|
||||
key: "bar".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: Some(Value::Bool(true)),
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -632,6 +670,7 @@ fn parse_define_access_jwt_key() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -665,6 +704,7 @@ fn parse_define_access_jwt_key() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -697,6 +737,7 @@ fn parse_define_access_jwt_key() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -775,6 +816,7 @@ fn parse_define_access_jwt_key() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -805,6 +847,7 @@ fn parse_define_access_jwt_key() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -838,6 +881,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
}),
|
||||
issue: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -870,6 +914,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -902,6 +947,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -933,6 +979,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
@ -965,6 +1012,7 @@ fn parse_define_access_jwt_jwks() {
|
|||
key: "foo".to_string(),
|
||||
}),
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -992,6 +1040,7 @@ fn parse_define_access_record() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db);
|
||||
assert_eq!(stmt.authenticate, None);
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
// Default durations.
|
||||
|
@ -1023,7 +1072,7 @@ fn parse_define_access_record() {
|
|||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
// Session duration, signing and authentication queries are explicitly defined.
|
||||
// Session duration, signing and authenticate clauses are explicitly defined.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
|
@ -1039,6 +1088,7 @@ fn parse_define_access_record() {
|
|||
|
||||
assert_eq!(stmt.name, Ident("a".to_string()));
|
||||
assert_eq!(stmt.base, Base::Db);
|
||||
assert_eq!(stmt.authenticate, Some(Value::Bool(true)));
|
||||
assert_eq!(
|
||||
stmt.duration,
|
||||
AccessDuration {
|
||||
|
@ -1053,7 +1103,6 @@ fn parse_define_access_record() {
|
|||
AccessType::Record(ac) => {
|
||||
assert_eq!(ac.signup, Some(Value::Bool(true)));
|
||||
assert_eq!(ac.signin, Some(Value::Bool(false)));
|
||||
assert_eq!(ac.authenticate, Some(Value::Bool(true)));
|
||||
match ac.jwt.verify {
|
||||
JwtAccessVerify::Key(key) => {
|
||||
assert_eq!(key.alg, Algorithm::Hs512);
|
||||
|
@ -1085,7 +1134,6 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Hs384,
|
||||
|
@ -1096,8 +1144,12 @@ fn parse_define_access_record() {
|
|||
// Issuer key matches verification key by default in symmetric algorithms.
|
||||
key: "foo".to_string(),
|
||||
}),
|
||||
}
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -1123,7 +1175,6 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Ps512,
|
||||
|
@ -1133,8 +1184,12 @@ fn parse_define_access_record() {
|
|||
alg: Algorithm::Ps512,
|
||||
key: "bar".to_string(),
|
||||
}),
|
||||
}
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -1160,7 +1215,6 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Rs256,
|
||||
|
@ -1170,8 +1224,12 @@ fn parse_define_access_record() {
|
|||
alg: Algorithm::Rs256,
|
||||
key: "bar".to_string(),
|
||||
}),
|
||||
}
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None
|
||||
}),
|
||||
authenticate: None,
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
token: Some(Duration::from_secs(10)),
|
||||
|
@ -1231,15 +1289,18 @@ fn parse_define_access_record_with_jwt() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::EdDSA,
|
||||
key: "foo".to_string(),
|
||||
}),
|
||||
issue: None,
|
||||
}
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
|
|
@ -200,7 +200,6 @@ fn statements() -> Vec<Statement> {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::EdDSA,
|
||||
|
@ -208,7 +207,11 @@ fn statements() -> Vec<Statement> {
|
|||
}),
|
||||
issue: None,
|
||||
},
|
||||
// TODO(gguillemas): Field kept to gracefully handle breaking change.
|
||||
// Remove when "revision" crate allows doing so.
|
||||
authenticate: None,
|
||||
}),
|
||||
authenticate: None,
|
||||
// Default durations.
|
||||
duration: AccessDuration {
|
||||
grant: None,
|
||||
|
|
Loading…
Reference in a new issue