Implement AUTHENTICATE
clause on RECORD
access (#4161)
Co-authored-by: Gerard Guillemas Martos <gguillemas@users.noreply.github.com> Co-authored-by: Gerard Guillemas Martos <gerard.guillemas@surrealdb.com>
This commit is contained in:
parent
7cbb0d4b0d
commit
01a05aea90
10 changed files with 1055 additions and 68 deletions
core/src
iam
sql
syn
|
@ -140,11 +140,11 @@ pub async fn db_access(
|
|||
Ok(val) => {
|
||||
match val.record() {
|
||||
// There is a record returned
|
||||
Some(rid) => {
|
||||
Some(mut rid) => {
|
||||
// Create the authentication key
|
||||
let key = config(iss.alg, iss.key)?;
|
||||
// Create the authentication claim
|
||||
let val = Claims {
|
||||
let claims = Claims {
|
||||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
|
@ -156,13 +156,40 @@ pub async fn db_access(
|
|||
id: Some(rid.to_raw()),
|
||||
..Claims::default()
|
||||
};
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = at.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(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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Log the authenticated access method info
|
||||
trace!("Signing in with access method `{}`", ac);
|
||||
// Create the authentication token
|
||||
let enc =
|
||||
encode(&Header::new(iss.alg.into()), &val, &key);
|
||||
encode(&Header::new(iss.alg.into()), &claims, &key);
|
||||
// Set the authentication on the session
|
||||
session.tk = Some(val.into());
|
||||
session.tk = Some(claims.into());
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
|
@ -1179,4 +1206,275 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
assert!(res.is_err(), "Unexpected successful signin: {:?}", res);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_signin_record_and_authenticate_clause() {
|
||||
// Test with correct credentials
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE (
|
||||
-- Simple example increasing the record identifier by one
|
||||
SELECT * FROM type::thing('user', meta::id($auth) + 1)
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1, user:2;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", 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_eq!(sess.au.id(), "user:2");
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert_eq!(sess.au.level().id(), Some("user:2"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 credentials and "realistic" scenario
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS owner ON DATABASE TYPE RECORD
|
||||
SIGNUP (
|
||||
-- Allow anyone to sign up as a new company
|
||||
-- This automatically creates an owner with the same credentials
|
||||
CREATE company CONTENT {
|
||||
email: $email,
|
||||
pass: crypto::argon2::generate($pass),
|
||||
owner: (CREATE employee CONTENT {
|
||||
email: $email,
|
||||
pass: $pass,
|
||||
}),
|
||||
}
|
||||
)
|
||||
SIGNIN (
|
||||
-- Allow company owners to log in directly with the company account
|
||||
SELECT * FROM company WHERE email = $email AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
AUTHENTICATE (
|
||||
-- If logging in with a company account, the session will be authenticated as the first owner
|
||||
IF meta::tb($auth) = "company" {
|
||||
RETURN SELECT VALUE owner FROM company WHERE id = $auth
|
||||
}
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE company:1 CONTENT {
|
||||
email: "info@example.com",
|
||||
pass: crypto::argon2::generate("company-password"),
|
||||
owner: employee:2,
|
||||
};
|
||||
CREATE employee:1 CONTENT {
|
||||
email: "member@example.com",
|
||||
pass: crypto::argon2::generate("member-password"),
|
||||
};
|
||||
CREATE employee:2 CONTENT {
|
||||
email: "owner@example.com",
|
||||
pass: crypto::argon2::generate("owner-password"),
|
||||
};
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("email", "info@example.com".into());
|
||||
vars.insert("pass", "company-password".into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"owner".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.db, Some("test".to_string()));
|
||||
assert_eq!(sess.ac, Some("owner".to_string()));
|
||||
assert_eq!(sess.au.id(), "employee:2");
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert_eq!(sess.au.level().id(), Some("employee:2"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 being able to fail authentication
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE {
|
||||
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
|
||||
IF !$auth.enabled {
|
||||
THROW "This user is not enabled";
|
||||
};
|
||||
|
||||
-- Always need to return the user id back, otherwise auth generically fails
|
||||
RETURN $auth;
|
||||
}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1 SET enabled = false;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Err(Error::Thrown(e)) if e == "This user is not enabled" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to user not being enabled, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test AUTHENTICATE clause not returning a value
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE {}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Err(Error::InvalidAuth) => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to generally fail, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -79,11 +79,11 @@ pub async fn db_access(
|
|||
Ok(val) => {
|
||||
match val.record() {
|
||||
// There is a record returned
|
||||
Some(rid) => {
|
||||
Some(mut rid) => {
|
||||
// Create the authentication key
|
||||
let key = config(iss.alg, iss.key)?;
|
||||
// Create the authentication claim
|
||||
let val = Claims {
|
||||
let claims = Claims {
|
||||
iss: Some(SERVER_NAME.to_owned()),
|
||||
iat: Some(Utc::now().timestamp()),
|
||||
nbf: Some(Utc::now().timestamp()),
|
||||
|
@ -95,13 +95,40 @@ pub async fn db_access(
|
|||
id: Some(rid.to_raw()),
|
||||
..Claims::default()
|
||||
};
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = at.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(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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Log the authenticated access method info
|
||||
trace!("Signing up with access method `{}`", ac);
|
||||
// Create the authentication token
|
||||
let enc =
|
||||
encode(&Header::new(iss.alg.into()), &val, &key);
|
||||
encode(&Header::new(iss.alg.into()), &claims, &key);
|
||||
// Set the authentication on the session
|
||||
session.tk = Some(val.into());
|
||||
session.tk = Some(claims.into());
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
|
@ -415,4 +442,271 @@ dn/RsYEONbwQSjIfMPkvxF+8HQ==
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_signup_record_and_authenticate_clause() {
|
||||
// Test with correct credentials
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP (
|
||||
CREATE type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE (
|
||||
-- Simple example increasing the record identifier by one
|
||||
SELECT * FROM type::thing('user', meta::id($auth) + 1)
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:2;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signup with credentials: {:?}", 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_eq!(sess.au.id(), "user:2");
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert_eq!(sess.au.level().id(), Some("user:2"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 credentials and "realistic" scenario
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS owner ON DATABASE TYPE RECORD
|
||||
SIGNUP (
|
||||
-- Allow anyone to sign up as a new company
|
||||
-- This automatically creates an owner with the same credentials
|
||||
CREATE company CONTENT {
|
||||
email: $email,
|
||||
pass: crypto::argon2::generate($pass),
|
||||
owner: (CREATE employee CONTENT {
|
||||
email: $email,
|
||||
pass: $pass,
|
||||
}),
|
||||
}
|
||||
)
|
||||
SIGNIN (
|
||||
-- Allow company owners to log in directly with the company account
|
||||
SELECT * FROM company WHERE email = $email AND crypto::argon2::compare(pass, $pass)
|
||||
)
|
||||
AUTHENTICATE (
|
||||
-- If logging in with a company account, the session will be authenticated as the first owner
|
||||
IF meta::tb($auth) = "company" {
|
||||
RETURN SELECT VALUE owner FROM company WHERE id = $auth
|
||||
}
|
||||
)
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE company:1 CONTENT {
|
||||
email: "info@example.com",
|
||||
pass: crypto::argon2::generate("company-password"),
|
||||
owner: employee:2,
|
||||
};
|
||||
CREATE employee:1 CONTENT {
|
||||
email: "member@example.com",
|
||||
pass: crypto::argon2::generate("member-password"),
|
||||
};
|
||||
CREATE employee:2 CONTENT {
|
||||
email: "owner@example.com",
|
||||
pass: crypto::argon2::generate("owner-password"),
|
||||
};
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("email", "info@example.com".into());
|
||||
vars.insert("pass", "company-password".into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"owner".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(res.is_ok(), "Failed to signin with credentials: {:?}", res);
|
||||
assert_eq!(sess.ns, Some("test".to_string()));
|
||||
assert_eq!(sess.db, Some("test".to_string()));
|
||||
assert_eq!(sess.ac, Some("owner".to_string()));
|
||||
assert!(sess.au.id().starts_with("employee:"));
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert!(sess.au.level().id().unwrap().starts_with("employee:"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 being able to fail authentication
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP (
|
||||
CREATE type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE {
|
||||
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
|
||||
IF !$auth.enabled {
|
||||
THROW "This user is not enabled";
|
||||
};
|
||||
|
||||
-- Always need to return the user id back, otherwise auth generically fails
|
||||
RETURN $auth;
|
||||
}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Err(Error::Thrown(e)) if e == "This user is not enabled" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to user not being enabled, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test AUTHENTICATE clause not returning a value
|
||||
{
|
||||
let ds = Datastore::new("memory").await.unwrap();
|
||||
let sess = Session::owner().with_ns("test").with_db("test");
|
||||
ds.execute(
|
||||
r#"
|
||||
DEFINE ACCESS user ON DATABASE TYPE RECORD
|
||||
SIGNUP (
|
||||
CREATE type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE {}
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
"#,
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Signin with the user
|
||||
let mut sess = Session {
|
||||
ns: Some("test".to_string()),
|
||||
db: Some("test".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
let mut vars: HashMap<&str, Value> = HashMap::new();
|
||||
vars.insert("id", 1.into());
|
||||
let res = db_access(
|
||||
&ds,
|
||||
&mut sess,
|
||||
"test".to_string(),
|
||||
"test".to_string(),
|
||||
"user".to_string(),
|
||||
vars.into(),
|
||||
)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Err(Error::InvalidAuth) => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to generally fail, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use crate::cnf::INSECURE_FORWARD_RECORD_ACCESS_ERRORS;
|
||||
use crate::dbs::Session;
|
||||
use crate::err::Error;
|
||||
#[cfg(feature = "jwks")]
|
||||
|
@ -150,7 +151,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
}
|
||||
}
|
||||
// Check the token authentication claims
|
||||
match token_data.claims {
|
||||
match token_data.claims.clone() {
|
||||
// Check if this is record access
|
||||
Claims {
|
||||
ns: Some(ns),
|
||||
|
@ -164,28 +165,58 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Create a new readonly transaction
|
||||
let mut tx = kvs.transaction(Read, Optimistic).await?;
|
||||
// Parse the record id
|
||||
let id = syn::thing(&id)?;
|
||||
let mut rid = syn::thing(&id)?;
|
||||
// Get the database access method
|
||||
let de = tx.get_db_access(&ns, &db, &ac).await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
let cf = match de.kind {
|
||||
AccessType::Record(ac) => match ac.jwt.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key),
|
||||
#[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 (au, cf) = match de.kind {
|
||||
AccessType::Record(at) => {
|
||||
let cf = match at.jwt.verify.clone() {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key),
|
||||
#[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),
|
||||
},
|
||||
#[cfg(not(feature = "jwks"))]
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?;
|
||||
|
||||
(at.authenticate, cf)
|
||||
}
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?;
|
||||
};
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
if let Some(au) = au {
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Log the success
|
||||
debug!("Authenticated with record access method `{}`", ac);
|
||||
// Set the session
|
||||
|
@ -193,16 +224,17 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.rd = Some(Value::from(id.to_owned()));
|
||||
session.rd = Some(Value::from(rid.to_owned()));
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
id.to_string(),
|
||||
rid.to_string(),
|
||||
Default::default(),
|
||||
Level::Record(ns, db, id.to_string()),
|
||||
Level::Record(ns, db, rid.to_string()),
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
// Check if this is database access
|
||||
// This can also be record access with an authenticate clause
|
||||
Claims {
|
||||
ns: Some(ns),
|
||||
db: Some(db),
|
||||
|
@ -216,49 +248,112 @@ pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Resul
|
|||
// Get the database access method
|
||||
let de = tx.get_db_access(&ns, &db, &ac).await?;
|
||||
// Obtain the configuration to verify the token based on the access method
|
||||
let cf = match de.kind {
|
||||
AccessType::Jwt(ac) => match ac.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key),
|
||||
#[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()))
|
||||
match de.kind {
|
||||
// If the access type is Jwt, this is database access
|
||||
AccessType::Jwt(at) => {
|
||||
let cf = match at.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key),
|
||||
#[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),
|
||||
}?;
|
||||
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// Parse the roles
|
||||
let roles = match token_data.claims.roles {
|
||||
// If no role is provided, grant the viewer role
|
||||
None => vec![Role::Viewer],
|
||||
// If roles are provided, parse them
|
||||
Some(roles) => roles
|
||||
.iter()
|
||||
.map(|r| -> Result<Role, Error> {
|
||||
Role::from_str(r.as_str()).map_err(Error::IamError)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
};
|
||||
// Log the success
|
||||
debug!("Authenticated to database `{}` with access method `{}`", db, ac);
|
||||
// Set the session
|
||||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
de.name.to_string(),
|
||||
roles,
|
||||
Level::Database(ns, db),
|
||||
)));
|
||||
}
|
||||
// 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 {
|
||||
Some(au) => {
|
||||
trace!("Access method `{}` is record access with authenticate clause", ac);
|
||||
let cf = match at.jwt.verify {
|
||||
JwtAccessVerify::Key(key) => config(key.alg, key.key),
|
||||
#[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),
|
||||
}?;
|
||||
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// AUTHENTICATE clause
|
||||
// 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);
|
||||
// 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),
|
||||
}
|
||||
}
|
||||
};
|
||||
// Log the success
|
||||
debug!("Authenticated with record access method `{}`", ac);
|
||||
// Set the session
|
||||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.rd = Some(Value::from(rid.to_owned()));
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
rid.to_string(),
|
||||
Default::default(),
|
||||
Level::Record(ns, db, rid.to_string()),
|
||||
)));
|
||||
}
|
||||
#[cfg(not(feature = "jwks"))]
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
},
|
||||
_ => return Err(Error::AccessMethodMismatch),
|
||||
}?;
|
||||
// Verify the token
|
||||
decode::<Claims>(token, &cf.0, &cf.1)?;
|
||||
// Parse the roles
|
||||
let roles = match token_data.claims.roles {
|
||||
// If no role is provided, grant the viewer role
|
||||
None => vec![Role::Viewer],
|
||||
// If roles are provided, parse them
|
||||
Some(roles) => roles
|
||||
.iter()
|
||||
.map(|r| -> Result<Role, Error> {
|
||||
Role::from_str(r.as_str()).map_err(Error::IamError)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
};
|
||||
// Log the success
|
||||
debug!("Authenticated to database `{}` with access method `{}`", db, ac);
|
||||
// Set the session
|
||||
session.tk = Some(value);
|
||||
session.ns = Some(ns.to_owned());
|
||||
session.db = Some(db.to_owned());
|
||||
session.ac = Some(ac.to_owned());
|
||||
session.exp = expiration(de.duration.session)?;
|
||||
session.au = Arc::new(Auth::new(Actor::new(
|
||||
de.name.to_string(),
|
||||
roles,
|
||||
Level::Database(ns, db),
|
||||
)));
|
||||
Ok(())
|
||||
}
|
||||
// Check if this is database authentication with user credentials
|
||||
|
@ -1347,7 +1442,7 @@ mod tests {
|
|||
// Create the token
|
||||
let enc = match encode(&HEADER, &claims, &key) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to decode token: {:?}", err),
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
// Signin with the token
|
||||
let mut sess = Session::default();
|
||||
|
@ -1653,4 +1748,276 @@ mod tests {
|
|||
|
||||
assert!(res.is_err(), "Unexpected success signing in with expired token: {:?}", res);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_token_db_record_and_authenticate_clause() {
|
||||
// Test with an "id" claim
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: 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()),
|
||||
id: Some("user:1".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 RECORD
|
||||
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
|
||||
;
|
||||
|
||||
CREATE user:1, user:2;
|
||||
"#
|
||||
)
|
||||
.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_eq!(sess.au.id(), "user:2");
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert_eq!(sess.au.level().id(), Some("user:2"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 without an "id" claim
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
|
||||
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 RECORD
|
||||
SIGNIN (
|
||||
SELECT * FROM type::thing('user', $id)
|
||||
)
|
||||
AUTHENTICATE (
|
||||
SELECT id FROM user WHERE email = $token.email
|
||||
)
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1 SET email = "info@surrealdb.com";
|
||||
"#
|
||||
)
|
||||
.as_str(),
|
||||
&sess,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let now = Utc::now().timestamp();
|
||||
let later = (Utc::now() + Duration::hours(1)).timestamp();
|
||||
let claims_json = format!(
|
||||
r#"
|
||||
{{
|
||||
"iss": "surrealdb-test",
|
||||
"iat": {now},
|
||||
"nbf": {now},
|
||||
"exp": {later},
|
||||
"ns": "test",
|
||||
"db": "test",
|
||||
"ac": "user",
|
||||
"email": "info@surrealdb.com"
|
||||
}}
|
||||
"#
|
||||
);
|
||||
let claims = serde_json::from_str::<Claims>(&claims_json).unwrap();
|
||||
// Create the token
|
||||
let enc = match encode(&HEADER, &claims, &key) {
|
||||
Ok(enc) => enc,
|
||||
Err(err) => panic!("Failed to encode token: {:?}", err),
|
||||
};
|
||||
// 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_eq!(sess.au.id(), "user:1");
|
||||
assert!(sess.au.is_record());
|
||||
assert_eq!(sess.au.level().ns(), Some("test"));
|
||||
assert_eq!(sess.au.level().db(), Some("test"));
|
||||
assert_eq!(sess.au.level().id(), Some("user:1"));
|
||||
// Record users should not have roles
|
||||
assert!(!sess.au.has_role(&Role::Viewer), "Auth user expected to not 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 being able to fail authentication
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: 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()),
|
||||
id: Some("user:1".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 RECORD
|
||||
AUTHENTICATE {{
|
||||
-- Not just signin, this clause runs across signin, signup and authenticate, which makes it a nice place to centralize logic
|
||||
IF !$auth.enabled {{
|
||||
THROW "This user is not enabled";
|
||||
}};
|
||||
|
||||
-- Always need to return the user id back, otherwise auth generically fails
|
||||
RETURN $auth;
|
||||
}}
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1 SET enabled = false;
|
||||
"#).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 == "This user is not enabled" => {} // ok
|
||||
res => panic!(
|
||||
"Expected authentication to failed due to user not being enabled, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Test AUTHENTICATE clause not returning a value
|
||||
{
|
||||
let secret = "jwt_secret";
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let claims = Claims {
|
||||
iss: 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()),
|
||||
id: Some("user:test".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 RECORD
|
||||
AUTHENTICATE {{}}
|
||||
WITH JWT ALGORITHM HS512 KEY '{secret}'
|
||||
DURATION FOR SESSION 2h
|
||||
;
|
||||
|
||||
CREATE user:1;
|
||||
"#
|
||||
)
|
||||
.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 authentication to generally fail, but instead received: {:?}",
|
||||
res
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,6 +40,9 @@ 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)?;
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +62,7 @@ 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(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
@ -262,6 +266,7 @@ pub struct JwtAccessVerifyJwks {
|
|||
pub struct RecordAccess {
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub authenticate: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
}
|
||||
|
||||
|
@ -270,6 +275,7 @@ impl Default for RecordAccess {
|
|||
Self {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
..Default::default()
|
||||
},
|
||||
|
|
|
@ -83,6 +83,7 @@ impl ser::Serializer for SerializerRecord {
|
|||
pub struct SerializeRecord {
|
||||
pub signup: Option<Value>,
|
||||
pub signin: Option<Value>,
|
||||
pub authenticate: Option<Value>,
|
||||
pub jwt: JwtAccess,
|
||||
}
|
||||
|
||||
|
@ -101,6 +102,9 @@ 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())?;
|
||||
}
|
||||
|
@ -115,6 +119,7 @@ impl serde::ser::SerializeStruct for SerializeRecord {
|
|||
Ok(RecordAccess {
|
||||
signup: self.signup,
|
||||
signin: self.signin,
|
||||
authenticate: self.authenticate,
|
||||
jwt: self.jwt,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -67,6 +67,7 @@ pub(crate) static KEYWORDS: phf::Map<UniCase<&'static str>, TokenKind> = phf_map
|
|||
UniCase::ascii("ASCII") => TokenKind::Keyword(Keyword::Ascii),
|
||||
UniCase::ascii("ASSERT") => TokenKind::Keyword(Keyword::Assert),
|
||||
UniCase::ascii("AT") => TokenKind::Keyword(Keyword::At),
|
||||
UniCase::ascii("AUTHENTICATE") => TokenKind::Keyword(Keyword::Authenticate),
|
||||
UniCase::ascii("BEFORE") => TokenKind::Keyword(Keyword::Before),
|
||||
UniCase::ascii("BEGIN") => TokenKind::Keyword(Keyword::Begin),
|
||||
UniCase::ascii("BLANK") => TokenKind::Keyword(Keyword::Blank),
|
||||
|
|
|
@ -302,6 +302,11 @@ 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,
|
||||
}
|
||||
}
|
||||
|
@ -515,6 +520,10 @@ 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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -963,11 +963,11 @@ fn parse_define_access_record() {
|
|||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
// Session duration and signing queries are explicitly defined.
|
||||
// Session duration, signing and authentication queries are explicitly defined.
|
||||
{
|
||||
let res = test_parse!(
|
||||
parse_stmt,
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD SIGNUP true SIGNIN false DURATION FOR SESSION 7d"#
|
||||
r#"DEFINE ACCESS a ON DB TYPE RECORD SIGNUP true SIGNIN false AUTHENTICATE true DURATION FOR SESSION 7d"#
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -993,6 +993,7 @@ 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);
|
||||
|
@ -1024,6 +1025,7 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Hs384,
|
||||
|
@ -1061,6 +1063,7 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Ps512,
|
||||
|
@ -1097,6 +1100,7 @@ fn parse_define_access_record() {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::Rs256,
|
||||
|
@ -1145,6 +1149,7 @@ 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,
|
||||
|
|
|
@ -200,6 +200,7 @@ fn statements() -> Vec<Statement> {
|
|||
kind: AccessType::Record(RecordAccess {
|
||||
signup: None,
|
||||
signin: None,
|
||||
authenticate: None,
|
||||
jwt: JwtAccess {
|
||||
verify: JwtAccessVerify::Key(JwtAccessVerifyKey {
|
||||
alg: Algorithm::EdDSA,
|
||||
|
|
|
@ -35,6 +35,7 @@ keyword! {
|
|||
Ascii => "ASCII",
|
||||
Assert => "ASSERT",
|
||||
At => "AT",
|
||||
Authenticate => "AUTHENTICATE",
|
||||
Before => "BEFORE",
|
||||
Begin => "BEGIN",
|
||||
Blank => "BLANK",
|
||||
|
|
Loading…
Reference in a new issue