Allow defining a maximum session duration on DEFINE USER ()

This commit is contained in:
Gerard Guillemas Martos 2024-05-31 09:40:27 +02:00 committed by GitHub
parent 876f281be5
commit d2821461b7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 127 additions and 59 deletions
core/src
iam
kvs
sql
statements/define
value/serde/ser/statement/define
syn/parser/stmt
lib/tests

View file

@ -235,8 +235,7 @@ pub async fn db_user(
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
// Check the authentication token
match enc {
@ -279,8 +278,7 @@ pub async fn ns_user(
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
// Check the authentication token
match enc {
@ -321,8 +319,7 @@ pub async fn root_user(
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Root).into());
// Check the authentication token
match enc {

View file

@ -1,5 +1,6 @@
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::issue::expiration;
#[cfg(feature = "jwks")]
use crate::iam::jwks;
use crate::iam::{token::Claims, Actor, Auth, Level, Role};
@ -98,8 +99,7 @@ pub async fn basic(
(Some(ns), Some(db)) => match verify_db_creds(kvs, ns, db, user, pass).await {
Ok(u) => {
debug!("Authenticated as database user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Database(ns.to_owned(), db.to_owned())).into());
Ok(())
}
@ -109,8 +109,7 @@ pub async fn basic(
(Some(ns), None) => match verify_ns_creds(kvs, ns, user, pass).await {
Ok(u) => {
debug!("Authenticated as namespace user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Namespace(ns.to_owned())).into());
Ok(())
}
@ -120,8 +119,7 @@ pub async fn basic(
(None, None) => match verify_root_creds(kvs, user, pass).await {
Ok(u) => {
debug!("Authenticated as root user '{}'", user);
// TODO(gguillemas): Enforce expiration once session lifetime can be customized.
session.exp = None;
session.exp = expiration(u.session)?;
session.au = Arc::new((&u, Level::Root).into());
Ok(())
}
@ -494,7 +492,7 @@ mod tests {
#[tokio::test]
async fn test_basic_root() {
//
// Test without roles defined
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
@ -520,14 +518,18 @@ mod tests {
}
//
// Test with roles defined
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
.await
.unwrap();
ds.execute(
"DEFINE USER user ON ROOT PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
&sess,
None,
)
.await
.unwrap();
let mut sess = Session {
..Default::default()
@ -544,7 +546,15 @@ mod tests {
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 have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::days(1) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
}
// Test invalid password
@ -565,7 +575,7 @@ mod tests {
#[tokio::test]
async fn test_basic_ns() {
//
// Test without roles defined
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
@ -592,14 +602,18 @@ mod tests {
}
//
// Test with roles defined
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
.await
.unwrap();
ds.execute(
"DEFINE USER user ON NS PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
&sess,
None,
)
.await
.unwrap();
let mut sess = Session {
ns: Some("test".to_string()),
@ -617,7 +631,15 @@ mod tests {
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 have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::days(1) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
}
// Test invalid password
@ -638,7 +660,7 @@ mod tests {
#[tokio::test]
async fn test_basic_db() {
//
// Test without roles defined
// Test without roles or expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
@ -666,14 +688,18 @@ mod tests {
}
//
// Test with roles defined
// Test with roles and expiration defined
//
{
let ds = Datastore::new("memory").await.unwrap();
let sess = Session::owner().with_ns("test").with_db("test");
ds.execute("DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER", &sess, None)
.await
.unwrap();
ds.execute(
"DEFINE USER user ON DB PASSWORD 'pass' ROLES EDITOR, OWNER SESSION 1d",
&sess,
None,
)
.await
.unwrap();
let mut sess = Session {
ns: Some("test".to_string()),
@ -692,7 +718,15 @@ mod tests {
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 have Editor role");
assert!(sess.au.has_role(&Role::Owner), "Auth user expected to have Owner role");
assert_eq!(sess.exp, None, "Default system user expiration is expected to be None");
// Expiration has been set explicitly
let exp = sess.exp.unwrap();
// Expiration should match the current time plus session duration with some margin
let min_exp = (Utc::now() + Duration::days(1) - Duration::seconds(10)).timestamp();
let max_exp = (Utc::now() + Duration::days(1) + Duration::seconds(10)).timestamp();
assert!(
exp > min_exp && exp < max_exp,
"Session expiration is expected to match the defined duration"
);
}
// Test invalid password

View file

@ -59,6 +59,9 @@ const LQ_CHANNEL_SIZE: usize = 100;
// The batch size used for non-paged operations (i.e. if there are more results, they are ignored)
const NON_PAGED_BATCH_SIZE: u32 = 100_000;
// The role assigned to the initial user created when starting the server with credentials for the first time
const INITIAL_USER_ROLE: &str = "owner";
/// The underlying datastore instance which stores the dataset.
#[allow(dead_code)]
#[non_exhaustive]
@ -482,8 +485,9 @@ impl Datastore {
Ok(v) if v.is_empty() => {
// Display information in the logs
info!("Credentials were provided, and no root users were found. The root user '{}' will be created", username);
// Create and save a new root users
let stm = DefineUserStatement::from((Base::Root, username, password));
// Create and save a new root user
let stm =
DefineUserStatement::from((Base::Root, username, password, INITIAL_USER_ROLE));
let ctx = Context::default().set_transaction(txn.clone());
let opt = Options::new().with_auth(Arc::new(Auth::for_root(Role::Owner)));
let _ = stm.compute(&ctx, &opt, None).await?;

View file

@ -4,7 +4,7 @@ use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::statements::info::InfoStructure;
use crate::sql::{escape::quote_str, fmt::Fmt, Base, Ident, Object, Strand, Value};
use crate::sql::{escape::quote_str, fmt::Fmt, Base, Duration, Ident, Object, Strand, Value};
use argon2::{
password_hash::{PasswordHasher, SaltString},
Argon2,
@ -15,7 +15,7 @@ use revision::revisioned;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display};
#[revisioned(revision = 2)]
#[revisioned(revision = 3)]
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Store, Hash)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[non_exhaustive]
@ -25,13 +25,15 @@ pub struct DefineUserStatement {
pub hash: String,
pub code: String,
pub roles: Vec<Ident>,
#[revision(start = 3)]
pub session: Option<Duration>,
pub comment: Option<Strand>,
#[revision(start = 2)]
pub if_not_exists: bool,
}
impl From<(Base, &str, &str)> for DefineUserStatement {
fn from((base, user, pass): (Base, &str, &str)) -> Self {
impl From<(Base, &str, &str, &str)> for DefineUserStatement {
fn from((base, user, pass, role): (Base, &str, &str, &str)) -> Self {
DefineUserStatement {
base,
name: user.into(),
@ -44,7 +46,8 @@ impl From<(Base, &str, &str)> for DefineUserStatement {
.take(128)
.map(char::from)
.collect::<String>(),
roles: vec!["owner".into()],
roles: vec![role.into()],
session: None,
comment: None,
if_not_exists: false,
}
@ -52,11 +55,17 @@ impl From<(Base, &str, &str)> for DefineUserStatement {
}
impl DefineUserStatement {
pub(crate) fn from_parsed_values(name: Ident, base: Base, roles: Vec<Ident>) -> Self {
pub(crate) fn from_parsed_values(
name: Ident,
base: Base,
roles: Vec<Ident>,
session: Option<Duration>,
) -> Self {
DefineUserStatement {
name,
base,
roles, // New users get the viewer role by default
roles, // New users get the viewer role by default
session, // Sessions for system users do not expire by default
code: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(128)
@ -77,6 +86,10 @@ impl DefineUserStatement {
self.hash = passhash;
}
pub(crate) fn set_session(&mut self, session: Option<Duration>) {
self.session = session;
}
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
@ -191,8 +204,11 @@ impl Display for DefineUserStatement {
quote_str(&self.hash),
Fmt::comma_separated(
&self.roles.iter().map(|r| r.to_string().to_uppercase()).collect::<Vec<String>>()
)
),
)?;
if let Some(ref v) = self.session {
write!(f, " SESSION {v}")?
}
if let Some(ref v) = self.comment {
write!(f, " COMMENT {v}")?
}
@ -207,6 +223,7 @@ impl InfoStructure for DefineUserStatement {
base,
hash,
roles,
session,
comment,
..
} = self;
@ -223,6 +240,10 @@ impl InfoStructure for DefineUserStatement {
Value::Array(roles.into_iter().map(|r| r.structure()).collect()),
);
if let Some(session) = session {
acc.insert("session".to_string(), session.into());
}
if let Some(comment) = comment {
acc.insert("comment".to_string(), comment.into());
}

View file

@ -2,6 +2,7 @@ use crate::err::Error;
use crate::sql::statements::DefineUserStatement;
use crate::sql::value::serde::ser;
use crate::sql::Base;
use crate::sql::Duration;
use crate::sql::Ident;
use crate::sql::Strand;
use ser::Serializer as _;
@ -44,6 +45,7 @@ pub struct SerializeDefineUserStatement {
hash: String,
code: String,
roles: Vec<Ident>,
session: Option<Duration>,
comment: Option<Strand>,
if_not_exists: bool,
}
@ -72,6 +74,10 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
"roles" => {
self.roles = value.serialize(ser::ident::vec::Serializer.wrap())?;
}
"session" => {
self.session =
value.serialize(ser::duration::opt::Serializer.wrap())?.map(Into::into);
}
"comment" => {
self.comment = value.serialize(ser::strand::opt::Serializer.wrap())?;
}
@ -94,6 +100,7 @@ impl serde::ser::SerializeStruct for SerializeDefineUserStatement {
hash: self.hash,
code: self.code,
roles: self.roles,
session: self.session,
comment: self.comment,
if_not_exists: self.if_not_exists,
})

View file

@ -182,6 +182,7 @@ impl Parser<'_> {
name,
base,
vec!["Viewer".into()], // New users get the viewer role by default
None, // Sessions for system users do not expire by default
);
if if_not_exists {
@ -209,6 +210,10 @@ impl Parser<'_> {
res.roles.push(self.next_token_value()?);
}
}
t!("SESSION") => {
self.pop_peek();
res.set_session(Some(self.next_token_value()?));
}
_ => break,
}
}

View file

@ -1782,13 +1782,13 @@ async fn permissions_checks_define_access_db() {
async fn permissions_checks_define_user_root() {
let scenario = HashMap::from([
("prepare", ""),
("test", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER"),
("test", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("check", "INFO FOR ROOT"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER\" } }"],
vec!["{ namespaces: { }, users: { user: \"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
vec!["{ namespaces: { }, users: { } }"]
];
@ -1824,13 +1824,13 @@ async fn permissions_checks_define_user_root() {
async fn permissions_checks_define_user_ns() {
let scenario = HashMap::from([
("prepare", ""),
("test", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER"),
("test", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("check", "INFO FOR NS"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\" } }"],
vec!["{ accesses: { }, databases: { }, users: { user: \"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
vec!["{ accesses: { }, databases: { }, users: { } }"]
];
@ -1866,13 +1866,13 @@ async fn permissions_checks_define_user_ns() {
async fn permissions_checks_define_user_db() {
let scenario = HashMap::from([
("prepare", ""),
("test", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER"),
("test", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("check", "INFO FOR DB"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\" } }"],
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { user: \"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\" } }"],
vec!["{ accesses: { }, analyzers: { }, functions: { }, models: { }, params: { }, tables: { }, users: { } }"]
];
@ -2601,8 +2601,8 @@ async fn redefining_existing_table_with_if_not_exists_should_error() -> Result<(
#[tokio::test]
async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
let sql = "
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER;
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER;
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
DEFINE USER example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");
@ -2621,8 +2621,8 @@ async fn redefining_existing_user_should_not_error() -> Result<(), Error> {
#[tokio::test]
async fn redefining_existing_user_with_if_not_exists_should_error() -> Result<(), Error> {
let sql = "
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER;
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER;
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
DEFINE USER IF NOT EXISTS example ON ROOT PASSWORD \"example\" ROLES OWNER SESSION 1d;
";
let dbs = new_ds().await?;
let ses = Session::owner().with_ns("test").with_db("test");

View file

@ -363,15 +363,15 @@ async fn permissions_checks_info_table() {
#[tokio::test]
async fn permissions_checks_info_user_root() {
let scenario = HashMap::from([
("prepare", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER"),
("prepare", "DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("test", "INFO FOR USER user ON ROOT"),
("check", "INFO FOR USER user ON ROOT"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
vec!["\"DEFINE USER user ON ROOT PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
];
let test_cases = [
@ -405,15 +405,15 @@ async fn permissions_checks_info_user_root() {
#[tokio::test]
async fn permissions_checks_info_user_ns() {
let scenario = HashMap::from([
("prepare", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER"),
("prepare", "DEFINE USER user ON NS PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("test", "INFO FOR USER user ON NS"),
("check", "INFO FOR USER user ON NS"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
vec!["\"DEFINE USER user ON NAMESPACE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
];
let test_cases = [
@ -447,15 +447,15 @@ async fn permissions_checks_info_user_ns() {
#[tokio::test]
async fn permissions_checks_info_user_db() {
let scenario = HashMap::from([
("prepare", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER"),
("prepare", "DEFINE USER user ON DB PASSHASH 'secret' ROLES VIEWER SESSION 1d"),
("test", "INFO FOR USER user ON DB"),
("check", "INFO FOR USER user ON DB"),
]);
// Define the expected results for the check statement when the test statement succeeded and when it failed
let check_results = [
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER\""],
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
vec!["\"DEFINE USER user ON DATABASE PASSHASH 'secret' ROLES VIEWER SESSION 1d\""],
];
let test_cases = [