[iam] RBAC and multiple root users (#2176)

Co-authored-by: Przemyslaw Hugh Kaznowski <hughkaznowski@protonmail.com>
Co-authored-by: Tobie Morgan Hitchcock <tobie@surrealdb.com>
This commit is contained in:
Salvador Girones Gil 2023-07-29 20:47:25 +02:00 committed by GitHub
parent e61db5564c
commit 998b263517
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
148 changed files with 8247 additions and 1315 deletions

View file

@ -165,7 +165,18 @@ jobs:
uses: taiki-e/install-action@cargo-llvm-cov
- name: Run cargo test (+coverage)
run: cargo llvm-cov --html --locked --no-default-features --features storage-mem,scripting,http --workspace -- --skip api_integration --skip cli
run: |
(set -x; df -h)
# Free up some disk space by removing unused files
(set -x; sudo rm -rf /imagegeneration || true)
(set -x; sudo rm -rf /opt/az || true)
(set -x; sudo rm -rf /opt/hostedtoolcache || true)
(set -x; sudo rm -rf /opt/google || true)
(set -x; sudo rm -rf /opt/pipx || true)
(set -x; df -h)
cargo llvm-cov --html --locked --no-default-features --features storage-mem,scripting,http --workspace -- --skip api_integration --skip cli_integration --skip http_integration
(set -x; df -h)
- name: Upload coverage report
uses: actions/upload-artifact@v3
@ -201,7 +212,7 @@ jobs:
- name: Run cargo test
run: |
cargo build --locked --no-default-features --features storage-fdb
(&>/dev/null ./target/debug/surreal start --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
(&>/dev/null ./target/debug/surreal start --auth --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
cargo test --locked --package surrealdb --no-default-features --features protocol-ws --test api api_integration::ws
http-engine:
@ -231,7 +242,7 @@ jobs:
- name: Run cargo test
run: |
cargo build --locked --no-default-features --features storage-fdb
(&>/dev/null ./target/debug/surreal start --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
(&>/dev/null ./target/debug/surreal start --auth --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
cargo test --locked --package surrealdb --no-default-features --features protocol-http --test api api_integration::http
mem-engine:
@ -392,5 +403,5 @@ jobs:
- name: Run cargo test
run: |
cargo build --locked --no-default-features --features storage-fdb
(&>/dev/null ./target/debug/surreal start --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
(&>/dev/null ./target/debug/surreal start --auth --log trace --user root --pass root fdb:/etc/foundationdb/fdb.cluster &)
cargo test --locked --package surrealdb --no-default-features --features protocol-http --test api api_integration::any

342
Cargo.lock generated
View file

@ -355,6 +355,15 @@ dependencies = [
"num-traits",
]
[[package]]
name = "arbitrary"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arc-swap"
version = "1.6.0"
@ -379,6 +388,15 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "ascii-canvas"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6"
dependencies = [
"term",
]
[[package]]
name = "assert-json-diff"
version = "2.0.2"
@ -775,6 +793,21 @@ dependencies = [
"which",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -1001,6 +1034,64 @@ dependencies = [
"jobserver",
]
[[package]]
name = "cedar-policy"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32299af284dbd45b026c6d53398e76d6b88acfee6afb0a23cd750220a7b416a6"
dependencies = [
"cedar-policy-core",
"cedar-policy-validator",
"itertools",
"lalrpop-util",
"ref-cast",
"serde",
"serde_json",
"smol_str",
"thiserror",
]
[[package]]
name = "cedar-policy-core"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "934c765b197bddf1065c0cd6bcb613b1b56d20c2c674342b8b2b744c4f20f504"
dependencies = [
"arbitrary",
"either",
"ipnet",
"itertools",
"lalrpop",
"lalrpop-util",
"lazy_static",
"regex",
"rustc_lexer",
"serde",
"serde_json",
"serde_with",
"smol_str",
"stacker",
"thiserror",
]
[[package]]
name = "cedar-policy-validator"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df000fd0dc596449e2a6516a1cfe24b17c7c3b6b189d0219282033ccd7af8126"
dependencies = [
"arbitrary",
"cedar-policy-core",
"itertools",
"serde",
"serde_json",
"serde_with",
"smol_str",
"stacker",
"thiserror",
"unicode-security",
]
[[package]]
name = "cexpr"
version = "0.6.0"
@ -1327,6 +1418,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.6"
@ -1430,6 +1527,17 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e0efad4403bfc52dc201159c4b842a246a14b98c64b55dfd0f2d89729dfeb8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -1449,6 +1557,12 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c1bba4f227a4a53d12b653f50ca7bf10c9119ae2aba56aff9e0338b5c98f36a"
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "difflib"
version = "0.4.0"
@ -1530,6 +1644,15 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "ena"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c533630cf40e9caa44bd91aadc88a75d75a4c3a12b4cfde353cbed41daa1e1f1"
dependencies = [
"log",
]
[[package]]
name = "encoding_rs"
version = "0.8.32"
@ -1650,6 +1773,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "fixedbitset"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.26"
@ -2510,6 +2639,37 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "lalrpop"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b"
dependencies = [
"ascii-canvas",
"bit-set",
"diff",
"ena",
"is-terminal",
"itertools",
"lalrpop-util",
"petgraph",
"regex",
"regex-syntax 0.6.29",
"string_cache",
"term",
"tiny-keccak",
"unicode-xid",
]
[[package]]
name = "lalrpop-util"
version = "0.19.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed"
dependencies = [
"regex",
]
[[package]]
name = "language-tags"
version = "0.3.2"
@ -2809,6 +2969,12 @@ dependencies = [
"tempfile",
]
[[package]]
name = "new_debug_unreachable"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
[[package]]
name = "nibble_vec"
version = "0.1.0"
@ -3144,6 +3310,16 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]]
name = "petgraph"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4"
dependencies = [
"fixedbitset",
"indexmap 1.9.3",
]
[[package]]
name = "pharos"
version = "0.5.3"
@ -3154,6 +3330,15 @@ dependencies = [
"rustc_version",
]
[[package]]
name = "phf_shared"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
dependencies = [
"siphasher",
]
[[package]]
name = "pin-project"
version = "1.1.2"
@ -3248,6 +3433,12 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "precomputed-hash"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "predicates"
version = "3.0.3"
@ -3404,6 +3595,15 @@ version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac"
[[package]]
name = "psm"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874"
dependencies = [
"cc",
]
[[package]]
name = "ptr_meta"
version = "0.1.4"
@ -3602,6 +3802,26 @@ dependencies = [
"thiserror",
]
[[package]]
name = "ref-cast"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43faa91b1c8b36841ee70e97188a869d37ae21759da6846d4be66de5bf7b12c"
dependencies = [
"ref-cast-impl",
]
[[package]]
name = "ref-cast-impl"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d2275aab483050ab2a7364c1a46604865ee7d6906684e08db0f090acf74f9e7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.27",
]
[[package]]
name = "regex"
version = "1.9.1"
@ -3929,6 +4149,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_lexer"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c86aae0c77166108c01305ee1a36a1e77289d7dc6ca0a3cd91ff4992de2d16a5"
dependencies = [
"unicode-xid",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -4226,6 +4455,7 @@ version = "1.0.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
@ -4264,6 +4494,34 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_with"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f02d8aa6e3c385bf084924f660ce2a3a6bd333ba55b35e8590b321f35d88513"
dependencies = [
"base64 0.21.2",
"chrono",
"hex",
"indexmap 1.9.3",
"serde",
"serde_json",
"serde_with_macros",
"time 0.3.23",
]
[[package]]
name = "serde_with_macros"
version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc7d5d3932fb12ce722ee5e64dd38c504efba37567f0c402f6ca728c3b8b070"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.27",
]
[[package]]
name = "serial_test"
version = "2.0.0"
@ -4364,6 +4622,12 @@ dependencies = [
"time 0.3.23",
]
[[package]]
name = "siphasher"
version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
[[package]]
name = "slab"
version = "0.4.8"
@ -4379,6 +4643,15 @@ version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "smol_str"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74212e6bbe9a4352329b2f68ba3130c15a3f26fe88ff22dbdc6cdd58fa85e99c"
dependencies = [
"serde",
]
[[package]]
name = "snap"
version = "1.1.0"
@ -4426,6 +4699,19 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "stacker"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce"
dependencies = [
"cc",
"cfg-if",
"libc",
"psm",
"winapi",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
@ -4456,6 +4742,19 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9091b6114800a5f2141aee1d1b9d6ca3592ac062dc5decb3764ec5895a47b4eb"
[[package]]
name = "string_cache"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b"
dependencies = [
"new_debug_unreachable",
"once_cell",
"parking_lot",
"phf_shared",
"precomputed-hash",
]
[[package]]
name = "strsim"
version = "0.10.0"
@ -4535,6 +4834,7 @@ dependencies = [
"bincode",
"bung",
"bytes",
"cedar-policy",
"chrono",
"criterion",
"deunicode",
@ -4725,6 +5025,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "term"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f"
dependencies = [
"dirs-next",
"rustversion",
"winapi",
]
[[package]]
name = "termcolor"
version = "1.2.0"
@ -4835,6 +5146,15 @@ dependencies = [
"time-core",
]
[[package]]
name = "tiny-keccak"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237"
dependencies = [
"crunchy",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
@ -5329,6 +5649,22 @@ dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-script"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d817255e1bed6dfd4ca47258685d14d2bdcfbc64fdc9e3819bd5848057b8ecc"
[[package]]
name = "unicode-security"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ef5756b3097992b934b06608c69f48448a0fbe804bb1e72b982f6d7983e9e63"
dependencies = [
"unicode-normalization",
"unicode-script",
]
[[package]]
name = "unicode-segmentation"
version = "1.10.1"
@ -5341,6 +5677,12 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unicode-xid"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c"
[[package]]
name = "untrusted"
version = "0.7.1"

View file

@ -1,4 +1,13 @@
DEV_FEATURES := --no-default-features --features storage-mem,http,scripting
DEV_FEATURES ?= storage-mem,http,scripting
SURREAL_LOG ?= trace
SURREAL_USER ?= root
SURREAL_PASS ?= root
SURREAL_AUTH ?= true
SURREAL_PATH ?= memory
SURREAL_NAMESPACE ?= test
SURREAL_DATABASE ?= test
SHELL := env SURREAL_PATH=$(SURREAL_PATH) SURREAL_LOG=$(SURREAL_LOG) SURREAL_AUTH=$(SURREAL_AUTH) SURREAL_USER=$(SURREAL_USER) SURREAL_PASS=$(SURREAL_PASS) $(SHELL)
.PHONY: default
default:
@ -35,11 +44,11 @@ bench:
.PHONY: serve
serve:
cargo run $(DEV_FEATURES) -- start --log trace --user root --pass root memory
cargo run --no-default-features --features $(DEV_FEATURES) -- start
.PHONY: sql
sql:
cargo run $(DEV_FEATURES) -- sql --conn ws://0.0.0.0:8000 --user root --pass root --ns test --db test --multi --pretty
cargo run --no-default-features --features $(DEV_FEATURES) -- sql --conn ws://0.0.0.0:8000 --multi --pretty
.PHONY: quick
quick:

View file

@ -112,6 +112,7 @@ trice = "0.3.1"
ulid = { version = "1.0.0", features = ["serde"] }
url = "2.4.0"
bytes = "1.4.0"
cedar-policy = "2.2.0"
[dev-dependencies]
criterion = { version="0.4", features= [ "async_futures" ] }

View file

@ -10,7 +10,7 @@ macro_rules! query {
$c.bench_function(stringify!($name), |b| {
let (dbs, ses) = futures::executor::block_on(async {
let dbs = Datastore::new("memory").await.unwrap();
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let setup = $setup;
if !setup.is_empty() {
dbs.execute(setup, &ses, None).await.unwrap();

View file

@ -52,7 +52,7 @@ struct Input {
async fn prepare_data() -> Input {
let dbs = Datastore::new("memory").await.unwrap();
let ses = Session::for_kv().with_ns("bench").with_db("bench");
let ses = Session::owner().with_ns("bench").with_db("bench");
let sql = format!(
r"DEFINE INDEX number ON item FIELDS number;
DEFINE ANALYZER simple TOKENIZERS blank,class;

View file

@ -125,6 +125,7 @@
"UPDATE"
"UPPERCASE"
"USE"
"USER"
"VALUE"
"VALUES"
"VERSION"
@ -369,4 +370,3 @@
"vector::similarity::pearson("
"vector::similarity::spearman("
# TODO: Add Javascript keywords

View file

@ -14,7 +14,7 @@ fuzz_target!(|commands: &str| {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap().block_on(async {
let dbs = Datastore::new("memory").await.unwrap();
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
for command in commands.iter() {
for blacklisted_string in blacklisted_command_strings.iter() {
if command.contains(blacklisted_string) {

View file

@ -125,6 +125,7 @@
"UPDATE"
"UPPERCASE"
"USE"
"USER"
"VALUE"
"VALUES"
"VERSION"
@ -368,4 +369,3 @@
"vector::similarity::pearson("
"vector::similarity::spearman("
# TODO: Add Javascript keywords

View file

@ -94,6 +94,7 @@ use crate::api::err::Error;
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
@ -122,7 +123,7 @@ use crate::api::opt::Tls;
use crate::api::Connect;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Level;
use crate::iam::Level;
use std::marker::PhantomData;
use url::Url;
@ -256,6 +257,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
@ -265,6 +267,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
)))
@ -276,7 +279,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (address, root) = self;
let mut endpoint = IntoEndpoint::into_endpoint(address.into())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)
@ -287,6 +290,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
@ -296,6 +300,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
)))
@ -347,6 +352,7 @@ where
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-fdb",
feature = "kv-speedb",
feature = "kv-indxdb",
),
feature = "native-tls",
@ -358,6 +364,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
@ -418,6 +425,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
@ -430,6 +438,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
@ -529,6 +538,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
@ -541,6 +551,7 @@ where
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
),
@ -641,3 +652,77 @@ pub fn connect(address: impl IntoEndpoint) -> Connect<'static, Any, Surreal<Any>
response_type: PhantomData,
}
}
#[cfg(all(test, feature = "kv-mem"))]
mod tests {
use super::*;
use crate::opt::auth::Root;
use crate::sql::{test::Parse, value::Value};
#[tokio::test]
async fn local_engine_without_auth() {
// Instantiate an in-memory instance without root credentials
let db = connect("memory").await.unwrap();
db.use_ns("N").use_db("D").await.unwrap();
// The client has access to everything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_ok(),
"client should have access to ROOT"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_ok(),
"client should have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_ok(),
"client should have access to DB"
);
// There are no users in the datastore
let mut res = db.query("INFO FOR ROOT").await.unwrap();
let users: Value = res.take("users").unwrap();
assert_eq!(users, Value::parse("[{}]"), "there should be no users in the system");
}
#[tokio::test]
async fn local_engine_with_auth() {
// Instantiate an in-memory instance with root credentials
let creds = Root {
username: "root",
password: "root",
};
let db = connect(("memory", creds)).await.unwrap();
db.use_ns("N").use_db("D").await.unwrap();
// The client needs to sign in before it can access anything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_err(),
"client should not have access to KV"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_err(),
"client should not have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_err(),
"client should not have access to DB"
);
// It can sign in
assert!(db.signin(creds).await.is_ok(), "client should be able to sign in");
// After the sign in, the client has access to everything
assert!(
db.query("INFO FOR ROOT").await.unwrap().check().is_ok(),
"client should have access to KV"
);
assert!(
db.query("INFO FOR NS").await.unwrap().check().is_ok(),
"client should have access to NS"
);
assert!(
db.query("INFO FOR DB").await.unwrap().check().is_ok(),
"client should have access to DB"
);
}
}

View file

@ -46,7 +46,6 @@ use crate::channel;
use crate::dbs::Response;
use crate::dbs::Session;
use crate::kvs::Datastore;
use crate::opt::auth::Root;
use crate::opt::IntoEndpoint;
use crate::sql::Array;
use crate::sql::Query;
@ -335,9 +334,6 @@ pub struct TiKv;
pub struct FDb;
/// An embedded database
///
/// Authentication methods (`signup`, `signin`, `authentication` and `invalidate`) are not availabe
/// on `Db`
#[derive(Debug, Clone)]
pub struct Db {
pub(crate) method: crate::api::conn::Method,
@ -400,7 +396,6 @@ async fn take(one: bool, responses: Vec<Response>) -> Result<Value> {
async fn router(
(_, method, param): (i64, Method, Param),
kvs: &Datastore,
configured_root: &Option<Root<'_>>,
session: &mut Session,
vars: &mut BTreeMap<String, Value>,
) -> Result<DbResponse> {
@ -436,8 +431,7 @@ async fn router(
[Value::Object(credentials)] => mem::take(credentials),
_ => unreachable!(),
};
let response =
crate::iam::signin::signin(kvs, configured_root, session, credentials).await?;
let response = crate::iam::signin::signin(kvs, session, credentials).await?;
Ok(DbResponse::Other(response.into()))
}
Method::Authenticate => {
@ -445,7 +439,7 @@ async fn router(
[Value::Strand(Strand(token))] => mem::take(token),
_ => unreachable!(),
};
crate::iam::verify::token(kvs, session, token).await?;
crate::iam::verify::token(kvs, session, &token).await?;
Ok(DbResponse::Other(Value::None))
}
Method::Invalidate => {
@ -518,17 +512,23 @@ async fn router(
// Write to channel.
async fn export_with_err(
kvs: &Datastore,
sess: &Session,
ns: String,
db: String,
chn: channel::Sender<Vec<u8>>,
) -> std::result::Result<(), crate::Error> {
kvs.export(ns, db, chn).await.map_err(|error| {
error!("{error}");
let export = kvs.prepare_export(sess, ns, db, chn).await.map_err(|error| {
error!("Error preparing export: {error}");
crate::Error::Db(error)
})?;
export.await.map_err(|error| {
error!("Error processing export: {error}");
crate::Error::Db(error)
})
}
let export = export_with_err(kvs, ns, db, tx);
let export = export_with_err(kvs, session, ns, db, tx);
// Read from channel and write to pipe.
let bridge = async move {

View file

@ -10,8 +10,8 @@ use crate::api::opt::Endpoint;
use crate::api::ExtraFeatures;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Level;
use crate::dbs::Session;
use crate::iam::Level;
use crate::kvs::Datastore;
use crate::opt::auth::Root;
use flume::Receiver;
@ -89,6 +89,13 @@ pub(crate) fn router(
) {
tokio::spawn(async move {
let url = address.endpoint;
let configured_root = match address.auth {
Level::Root => Some(Root {
username: &address.username,
password: &address.password,
}),
_ => None,
};
let kvs = {
let path = match url.scheme() {
@ -106,8 +113,15 @@ pub(crate) fn router(
match Datastore::new(&path).await {
Ok(kvs) => {
// If a root user is specified, setup the initial datastore credentials
if let Some(root) = configured_root {
if let Err(error) = kvs.setup_initial_creds(root).await {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
}
let _ = conn_tx.into_send_async(Ok(())).await;
kvs
kvs.with_auth_enabled(configured_root.is_some())
}
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
@ -128,25 +142,10 @@ pub(crate) fn router(
let mut vars = BTreeMap::new();
let mut stream = route_rx.into_stream();
let configured_root = match address.auth {
Level::Kv => Some(Root {
username: &address.username,
password: &address.password,
}),
_ => None,
};
let mut session = if configured_root.is_some() {
// If a root user is specified, lock down the database
Session::default()
} else {
// If no root user is specified, the database should be open
Session::for_kv()
};
let mut session = Session::default();
while let Some(Some(route)) = stream.next().await {
match super::router(route.request, &kvs, &configured_root, &mut session, &mut vars)
.await
{
match super::router(route.request, &kvs, &mut session, &mut vars).await {
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}

View file

@ -8,8 +8,8 @@ use crate::api::engine::local::Db;
use crate::api::opt::Endpoint;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Level;
use crate::dbs::Session;
use crate::iam::Level;
use crate::kvs::Datastore;
use crate::opt::auth::Root;
use flume::Receiver;
@ -85,6 +85,13 @@ pub(crate) fn router(
) {
spawn_local(async move {
let url = address.endpoint;
let configured_root = match address.auth {
Level::Root => Some(Root {
username: &address.username,
password: &address.password,
}),
_ => None,
};
let path = match url.scheme() {
"mem" => "memory",
@ -93,8 +100,16 @@ pub(crate) fn router(
let kvs = match Datastore::new(path).await {
Ok(kvs) => {
// If a root user is specified, setup the initial datastore credentials
if let Some(root) = configured_root {
if let Err(error) = kvs.setup_initial_creds(root).await {
let _ = conn_tx.into_send_async(Err(error.into())).await;
return;
}
}
let _ = conn_tx.into_send_async(Ok(())).await;
kvs
kvs.with_auth_enabled(configured_root.is_some())
}
Err(error) => {
let _ = conn_tx.into_send_async(Err(error.into())).await;
@ -114,25 +129,10 @@ pub(crate) fn router(
let mut vars = BTreeMap::new();
let mut stream = route_rx.into_stream();
let configured_root = match address.auth {
Level::Kv => Some(Root {
username: &address.username,
password: &address.password,
}),
_ => None,
};
let mut session = if configured_root.is_some() {
// If a root user is specified, lock down the database
Session::default()
} else {
// If no root user is specified, the database should be open
Session::for_kv()
};
let mut session = Session::default();
while let Some(Some(route)) = stream.next().await {
match super::router(route.request, &kvs, &configured_root, &mut session, &mut vars)
.await
{
match super::router(route.request, &kvs, &mut session, &mut vars).await {
Ok(value) => {
let _ = route.response.into_send_async(Ok(value)).await;
}

View file

@ -18,11 +18,11 @@ pub enum Error {
Query(String),
/// There was an error processing a remote HTTP request
#[error("There was an error processing a remote HTTP request")]
#[error("There was an error processing a remote HTTP request: {0}")]
Http(String),
/// There was an error processing a remote WS request
#[error("There was an error processing a remote WS request")]
#[error("There was an error processing a remote WS request: {0}")]
Ws(String),
/// The specified scheme does not match any supported protocol or storage engine

View file

@ -449,8 +449,8 @@ where
/// // Select the namespace/database to use
/// db.use_ns("namespace").use_db("database").await?;
///
/// // Define the login
/// let sql = "DEFINE LOGIN johndoe ON NAMESPACE PASSWORD 'password123'";
/// // Define the user
/// let sql = "DEFINE USER johndoe ON NAMESPACE PASSWORD 'password123'";
/// db.query(sql).await?.check()?;
///
/// // Sign a user in
@ -485,8 +485,8 @@ where
/// // Select the namespace/database to use
/// db.use_ns("namespace").use_db("database").await?;
///
/// // Define the login
/// let sql = "DEFINE LOGIN johndoe ON DATABASE PASSWORD 'password123'";
/// // Define the user
/// let sql = "DEFINE USER johndoe ON DATABASE PASSWORD 'password123'";
/// db.query(sql).await?.check()?;
///
/// // Sign a user in

View file

@ -51,7 +51,7 @@ async fn api() {
.unwrap();
// signin
let _: () = DB
let _: Jwt = DB
.signin(Root {
username: "root",
password: "root",

View file

@ -11,7 +11,7 @@ use crate::api::Connect;
use crate::api::ExtraFeatures;
use crate::api::Result;
use crate::api::Surreal;
use crate::dbs::Level;
use crate::iam::Level;
use flume::Receiver;
use once_cell::sync::OnceCell;
use std::collections::HashSet;

View file

@ -1,16 +1,13 @@
use super::types::Root;
use super::types::User;
use crate::api::conn::DbResponse;
use crate::api::conn::Method;
use crate::api::conn::Route;
use crate::api::opt::from_value;
use crate::api::Response as QueryResponse;
use crate::sql::to_value;
use crate::sql::Array;
use crate::sql::Value;
use flume::Receiver;
use futures::StreamExt;
use std::mem;
pub(super) fn mock(route_rx: Receiver<Option<Route>>) {
tokio::spawn(async move {
@ -48,12 +45,7 @@ pub(super) fn mock(route_rx: Receiver<Option<Route>>) {
_ => unreachable!(),
},
Method::Signup | Method::Signin => match &mut params[..] {
[credentials] => match from_value(mem::take(credentials)) {
Ok(Root {
..
}) => Ok(DbResponse::Other(Value::None)),
_ => Ok(DbResponse::Other("jwt".to_owned().into())),
},
[_] => Ok(DbResponse::Other("jwt".to_owned().into())),
_ => unreachable!(),
},
Method::Set => match &params[..] {

View file

@ -27,7 +27,7 @@ pub struct Root<'a> {
pub password: &'a str,
}
impl Credentials<Signin, ()> for Root<'_> {}
impl Credentials<Signin, Jwt> for Root<'_> {}
/// Credentials for the namespace user
#[derive(Debug, Clone, Copy, Serialize)]

View file

@ -7,7 +7,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use std::path::Path;
use url::Url;
@ -74,7 +74,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (path, root) = self;
let mut endpoint = IntoEndpoint::<FDb>::into_endpoint(path.as_ref())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)

View file

@ -7,7 +7,7 @@ use crate::api::opt::IntoEndpoint;
use crate::api::opt::Tls;
use crate::api::Endpoint;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use std::net::SocketAddr;
use url::Url;

View file

@ -7,7 +7,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use url::Url;
impl IntoEndpoint<IndxDb> for &str {
@ -55,7 +55,7 @@ impl IntoEndpoint<IndxDb> for (&str, Root<'_>) {
fn into_endpoint(self) -> Result<Endpoint> {
let (name, root) = self;
let mut endpoint = IntoEndpoint::<IndxDb>::into_endpoint(name)?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)

View file

@ -5,7 +5,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use crate::opt::Config;
use url::Url;
@ -50,7 +50,7 @@ impl IntoEndpoint<Mem> for Root<'_> {
fn into_endpoint(self) -> Result<Endpoint> {
let mut endpoint = IntoEndpoint::<Mem>::into_endpoint(())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = self.username.to_owned();
endpoint.password = self.password.to_owned();
Ok(endpoint)

View file

@ -18,7 +18,7 @@ mod tikv;
use crate::api::Connection;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use url::Url;
use super::Config;

View file

@ -8,7 +8,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use std::path::Path;
use url::Url;
@ -75,7 +75,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (path, root) = self;
let mut endpoint = IntoEndpoint::<RocksDb>::into_endpoint(path.as_ref())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)
@ -173,7 +173,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (path, root) = self;
let mut endpoint = IntoEndpoint::<File>::into_endpoint(path.as_ref())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)

View file

@ -6,7 +6,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use crate::opt::auth::Root;
use std::path::Path;
use url::Url;
@ -74,7 +74,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (path, root) = self;
let mut endpoint = IntoEndpoint::<SpeeDb>::into_endpoint(path.as_ref())?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)

View file

@ -7,7 +7,7 @@ use crate::api::opt::Endpoint;
use crate::api::opt::IntoEndpoint;
use crate::api::opt::Strict;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use std::fmt::Display;
use std::net::SocketAddr;
use url::Url;
@ -99,7 +99,7 @@ where
fn into_endpoint(self) -> Result<Endpoint> {
let (address, root) = self;
let mut endpoint = address.into_endpoint()?;
endpoint.auth = Level::Kv;
endpoint.auth = Level::Root;
endpoint.username = root.username.to_owned();
endpoint.password = root.password.to_owned();
Ok(endpoint)

View file

@ -7,7 +7,7 @@ use crate::api::opt::IntoEndpoint;
#[cfg(any(feature = "native-tls", feature = "rustls"))]
use crate::api::opt::Tls;
use crate::api::Result;
use crate::dbs::Level;
use crate::iam::Level;
use std::net::SocketAddr;
use url::Url;

View file

@ -1,68 +0,0 @@
/// The authentication level for a datastore execution context.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd)]
pub enum Level {
No,
Kv,
Ns,
Db,
Sc,
}
/// Specifies the current authentication for the datastore execution context.
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd)]
pub enum Auth {
/// Specifies that the user is not authenticated
#[default]
No,
/// Specifies that the user is authenticated with full root permissions
Kv,
/// Specifies that the user is has full permissions for a particular Namespace
Ns(String),
/// Specifies that the user is has full permissions for a particular Namespace and Database
Db(String, String),
/// Specifies that the user is has full permissions for a particular Namespace, Database, and Scope
Sc(String, String, String),
}
impl Auth {
/// Checks whether the current authentication has root level permissions
pub fn is_kv(&self) -> bool {
self.check(Level::Kv)
}
/// Checks whether the current authentication has namespace level permissions
pub fn is_ns(&self) -> bool {
self.check(Level::Ns)
}
/// Checks whether the current authentication has database level permissions
pub fn is_db(&self) -> bool {
self.check(Level::Db)
}
/// Checks whether the current authentication has scope level permissions
pub fn is_sc(&self) -> bool {
self.check(Level::Sc)
}
/// Checks whether the current authentication is unauthenticated
pub fn is_no(&self) -> bool {
self.check(Level::Sc)
}
/// Checks whether permissions clauses need to be processed
pub(crate) fn perms(&self) -> bool {
match self {
Auth::No => true,
Auth::Sc(_, _, _) => true,
Auth::Db(_, _) => false,
Auth::Ns(_) => false,
Auth::Kv => false,
}
}
/// Checks whether the current authentication matches the required level
pub(crate) fn check(&self, level: Level) -> bool {
match self {
Auth::No => matches!(level, Level::No),
Auth::Sc(_, _, _) => matches!(level, Level::No | Level::Sc),
Auth::Db(_, _) => matches!(level, Level::No | Level::Sc | Level::Db),
Auth::Ns(_) => matches!(level, Level::No | Level::Sc | Level::Db | Level::Ns),
Auth::Kv => true,
}
}
}

View file

@ -1,18 +1,20 @@
use crate::cnf::PROTECTED_PARAM_NAMES;
use crate::ctx::Context;
use crate::dbs::response::Response;
use crate::dbs::Level;
use crate::dbs::Notification;
use crate::dbs::Options;
use crate::dbs::QueryType;
use crate::dbs::Transaction;
use crate::dbs::{Auth, QueryType};
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::kvs::Datastore;
use crate::sql::paths::DB;
use crate::sql::paths::NS;
use crate::sql::query::Query;
use crate::sql::statement::Statement;
use crate::sql::value::Value;
use crate::sql::Base;
use channel::Receiver;
use futures::lock::Mutex;
use std::sync::Arc;
@ -206,10 +208,8 @@ impl<'a> Executor<'a> {
let res = match stm {
// Specify runtime options
Statement::Option(mut stm) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Option, &Base::Db)?;
// Convert to uppercase
stm.name.0.make_ascii_uppercase();
// Process the option
@ -252,32 +252,10 @@ impl<'a> Executor<'a> {
// Switch to a different NS or DB
Statement::Use(stm) => {
if let Some(ref ns) = stm.ns {
match &*opt.auth {
Auth::No => self.set_ns(&mut ctx, &mut opt, ns).await,
Auth::Kv => self.set_ns(&mut ctx, &mut opt, ns).await,
Auth::Ns(v) if v == ns => self.set_ns(&mut ctx, &mut opt, ns).await,
Auth::Db(v, _) if v == ns => self.set_ns(&mut ctx, &mut opt, ns).await,
_ => {
opt.set_ns(None);
return Err(Error::NsNotAllowed {
ns: ns.to_owned(),
});
}
}
self.set_ns(&mut ctx, &mut opt, ns).await;
}
if let Some(ref db) = stm.db {
match &*opt.auth {
Auth::No => self.set_db(&mut ctx, &mut opt, db).await,
Auth::Kv => self.set_db(&mut ctx, &mut opt, db).await,
Auth::Ns(_) => self.set_db(&mut ctx, &mut opt, db).await,
Auth::Db(_, v) if v == db => self.set_db(&mut ctx, &mut opt, db).await,
_ => {
opt.set_db(None);
return Err(Error::DbNotAllowed {
db: db.to_owned(),
});
}
}
self.set_db(&mut ctx, &mut opt, db).await;
}
Ok(Value::None)
}
@ -434,3 +412,86 @@ impl<'a> Executor<'a> {
Ok(out)
}
}
#[cfg(test)]
mod tests {
use crate::{dbs::Session, iam::Role, kvs::Datastore};
#[tokio::test]
async fn check_execute_option_permissions() {
let tests = vec![
// Root level
(Session::for_level(().into(), Role::Owner).with_ns("NS").with_db("DB"), true, "owner at root level should be able to set options"),
(Session::for_level(().into(), Role::Editor).with_ns("NS").with_db("DB"), true, "editor at root level should be able to set options"),
(Session::for_level(().into(), Role::Viewer).with_ns("NS").with_db("DB"), false, "viewer at root level should not be able to set options"),
// Namespace level
(Session::for_level(("NS",).into(), Role::Owner).with_ns("NS").with_db("DB"), true, "owner at namespace level should be able to set options on its namespace"),
(Session::for_level(("NS",).into(), Role::Owner).with_ns("OTHER_NS").with_db("DB"), false, "owner at namespace level should not be able to set options on another namespace"),
(Session::for_level(("NS",).into(), Role::Editor).with_ns("NS").with_db("DB"), true, "editor at namespace level should be able to set options on its namespace"),
(Session::for_level(("NS",).into(), Role::Editor).with_ns("OTHER_NS").with_db("DB"), false, "editor at namespace level should not be able to set options on another namespace"),
(Session::for_level(("NS",).into(), Role::Viewer).with_ns("NS").with_db("DB"), false, "viewer at namespace level should not be able to set options on its namespace"),
// Database level
(Session::for_level(("NS", "DB").into(), Role::Owner).with_ns("NS").with_db("DB"), true, "owner at database level should be able to set options on its database"),
(Session::for_level(("NS", "DB").into(), Role::Owner).with_ns("NS").with_db("OTHER_DB"), false, "owner at database level should not be able to set options on another database"),
(Session::for_level(("NS", "DB").into(), Role::Owner).with_ns("OTHER_NS").with_db("DB"), false, "owner at database level should not be able to set options on another namespace even if the database name matches"),
(Session::for_level(("NS", "DB").into(), Role::Editor).with_ns("NS").with_db("DB"), true, "editor at database level should be able to set options on its database"),
(Session::for_level(("NS", "DB").into(), Role::Editor).with_ns("NS").with_db("OTHER_DB"), false, "editor at database level should not be able to set options on another database"),
(Session::for_level(("NS", "DB").into(), Role::Editor).with_ns("OTHER_NS").with_db("DB"), false, "editor at database level should not be able to set options on another namespace even if the database name matches"),
(Session::for_level(("NS", "DB").into(), Role::Viewer).with_ns("NS").with_db("DB"), false, "viewer at database level should not be able to set options on its database"),
];
let statement = "OPTION FIELDS = false";
for test in tests.iter() {
let (session, should_succeed, msg) = test;
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(true);
let res = ds.execute(statement, &session, None).await;
if *should_succeed {
assert!(res.is_ok(), "{}: {:?}", msg, res);
} else {
let err = res.unwrap_err().to_string();
assert!(
err.contains("Not enough permissions to perform this action"),
"{}: {}",
msg,
err
)
}
}
}
// Anonymous with auth enabled
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(true);
let res =
ds.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None).await;
let err = res.unwrap_err().to_string();
assert!(
err.contains("Not enough permissions to perform this action"),
"anonymous user should not be able to set options: {}",
err
)
}
// Anonymous with auth disabled
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(false);
let res =
ds.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None).await;
assert!(
res.is_ok(),
"anonymous user should be able to set options when auth is disabled: {:?}",
res
)
}
}
}

View file

@ -2,7 +2,6 @@
//! In this module we essentially manage the entire lifecycle of a database request acting as the
//! glue between the API and the response. In this module we use channels as a transport layer
//! and executors to process the operations. This module also gives a `context` to the transaction.
mod auth;
mod distinct;
mod executor;
mod explanation;
@ -15,7 +14,6 @@ mod statement;
mod transaction;
mod variables;
pub use self::auth::*;
pub use self::notification::*;
pub use self::options::*;
pub use self::response::*;

View file

@ -1,8 +1,8 @@
use crate::cnf;
use crate::dbs::Auth;
use crate::dbs::Level;
use crate::dbs::Notification;
use crate::err::Error;
use crate::iam::{Action, Auth, ResourceKind, Role};
use crate::sql::Base;
use channel::Sender;
use std::sync::Arc;
@ -24,6 +24,8 @@ pub struct Options {
dive: u8,
/// Connection authentication data
pub auth: Arc<Auth>,
/// Is authentication enabled?
pub auth_enabled: bool,
/// Whether live queries are allowed?
pub live: bool,
/// Should we force tables/events to re-run?
@ -69,8 +71,9 @@ impl Options {
tables: true,
indexes: true,
futures: false,
auth_enabled: true,
sender: None,
auth: Arc::new(Auth::No),
auth: Arc::new(Auth::default()),
}
}
@ -182,6 +185,12 @@ impl Options {
self
}
/// Create a new Options object with auth enabled
pub fn with_auth_enabled(mut self, auth_enabled: bool) -> Self {
self.auth_enabled = auth_enabled;
self
}
// --------------------------------------------------
/// Create a new Options object for a subquery
@ -305,6 +314,16 @@ impl Options {
}
}
// Get currently selected base
pub fn selected_base(&self) -> Result<Base, Error> {
match (self.ns.as_ref(), self.db.as_ref()) {
(None, None) => Ok(Base::Root),
(Some(_), None) => Ok(Base::Ns),
(Some(_), Some(_)) => Ok(Base::Db),
(None, Some(_)) => Err(Error::NsEmpty),
}
}
/// Create a new Options object for a function/subquery/future/etc.
///
/// The parameter is the approximate cost of the operation (more concretely, the size of the
@ -352,22 +371,90 @@ impl Options {
Ok(())
}
/// Check whether the authentication permissions are ok
pub fn check(&self, level: Level) -> Result<(), Error> {
if !self.auth.check(level) {
return Err(Error::QueryPermissions);
// Validate Options for Namespace
pub fn valid_for_ns(&self) -> Result<(), Error> {
if self.ns.is_none() {
return Err(Error::NsEmpty);
}
Ok(())
}
/// Check whether the necessary NS / DB options have been set
pub fn needs(&self, level: Level) -> Result<(), Error> {
if self.ns.is_none() && matches!(level, Level::Ns | Level::Db) {
return Err(Error::NsEmpty);
}
if self.db.is_none() && matches!(level, Level::Db) {
// Validate Options for Database
pub fn valid_for_db(&self) -> Result<(), Error> {
self.valid_for_ns()?;
if self.db.is_none() {
return Err(Error::DbEmpty);
}
Ok(())
}
/// Check if the current auth is allowed to perform an action on a given resource
pub fn is_allowed(&self, action: Action, res: ResourceKind, base: &Base) -> Result<(), Error> {
// If auth is disabled, allow all actions for anonymous users
if !self.auth_enabled && self.auth.is_anon() {
return Ok(());
}
let res = match base {
Base::Root => res.on_root(),
Base::Ns => {
self.valid_for_ns()?;
res.on_ns(self.ns())
}
Base::Db => {
self.valid_for_db()?;
res.on_db(self.ns(), self.db())
}
Base::Sc(sc) => {
self.valid_for_db()?;
res.on_scope(self.ns(), self.db(), sc)
}
};
self.auth.is_allowed(action, &res).map_err(Error::IamError)
}
/// Whether or not to check table permissions
///
/// TODO: This method is called a lot during data operations, so we decided to bypass the system's authorization mechanism.
/// This is a temporary solution, until we optimize the new authorization system.
pub fn check_perms(&self, action: Action) -> bool {
// If permissions are disabled, don't check permissions
if !self.perms {
return false;
}
// If auth is disabled and actor is anonymous, don't check permissions
if !self.auth_enabled && self.auth.is_anon() {
return false;
}
// Is the actor allowed to view?
let can_view =
[Role::Viewer, Role::Editor, Role::Owner].iter().any(|r| self.auth.has_role(r));
// Is the actor allowed to edit?
let can_edit = [Role::Editor, Role::Owner].iter().any(|r| self.auth.has_role(r));
// Is the target database in the actor's level?
let db_in_actor_level = self.auth.is_root()
|| self.auth.is_ns() && self.auth.level().ns().unwrap() == self.ns()
|| self.auth.is_db()
&& self.auth.level().ns().unwrap() == self.ns()
&& self.auth.level().db().unwrap() == self.db();
// Is the actor allowed to do the action on the selected database?
let is_allowed = match action {
Action::View => {
// Today all users have at least View permissions, so if the target database belongs to the user's level, don't check permissions
can_view && db_in_actor_level
}
Action::Edit => {
// Editor and Owner roles are allowed to edit, but only if the target database belongs to the user's level
can_edit && db_in_actor_level
}
};
// Check permissions if the autor is not already allowed to do the action
!is_allowed
}
}

View file

@ -1,12 +1,13 @@
use crate::ctx::Context;
use crate::dbs::Auth;
use crate::iam::Auth;
use crate::iam::{Level, Role};
use crate::sql::value::Value;
use std::sync::Arc;
/// Specifies the current session information when processing a query.
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct Session {
/// The current [`Auth`] information
/// The current session [`Auth`] information
pub au: Arc<Auth>,
/// Whether realtime queries are supported
pub rt: bool,
@ -29,49 +30,6 @@ pub struct Session {
}
impl Session {
/// Create a session with root authentication
pub fn for_kv() -> Session {
Session {
au: Arc::new(Auth::Kv),
..Session::default()
}
}
/// Create a session with namespace authentication
pub fn for_ns<S>(ns: S) -> Session
where
S: Into<String> + Clone,
{
Session {
ns: Some(ns.clone().into()),
au: Arc::new(Auth::Ns(ns.into())),
..Session::default()
}
}
/// Create a session with database authentication
pub fn for_db<S>(ns: S, db: S) -> Session
where
S: Into<String> + Clone,
{
Session {
ns: Some(ns.clone().into()),
db: Some(db.clone().into()),
au: Arc::new(Auth::Db(ns.into(), db.into())),
..Session::default()
}
}
/// Create a session with scope authentication
pub fn for_sc<S>(ns: S, db: S, sc: S) -> Session
where
S: Into<String> + Clone,
{
Session {
ns: Some(ns.clone().into()),
db: Some(db.clone().into()),
sc: Some(sc.clone().into()),
au: Arc::new(Auth::Sc(ns.into(), db.into(), sc.into())),
..Session::default()
}
}
/// Set the selected namespace for the session
pub fn with_ns(mut self, ns: &str) -> Session {
self.ns = Some(ns.to_owned());
@ -96,9 +54,9 @@ impl Session {
}
/// Convert a session into a runtime
pub(crate) fn context<'a>(&self, mut ctx: Context<'a>) -> Context<'a> {
// Add auth data
// Add scope auth data
let val: Value = self.sd.to_owned().into();
ctx.add_value("auth", val);
ctx.add_value("scope_auth", val);
// Add scope data
let val: Value = self.sc.to_owned().into();
ctx.add_value("scope", val);
@ -120,4 +78,41 @@ impl Session {
// Output context
ctx
}
/// Create a system session for a given level and role
pub fn for_level(level: Level, role: Role) -> Session {
let mut sess = Session::default();
match level {
Level::Root => {
sess.au = Arc::new(Auth::for_root(role));
}
Level::Namespace(ns) => {
sess.au = Arc::new(Auth::for_ns(role, &ns));
sess.ns = Some(ns);
}
Level::Database(ns, db) => {
sess.au = Arc::new(Auth::for_db(role, &ns, &db));
sess.ns = Some(ns);
sess.db = Some(db);
}
_ => {}
}
sess
}
/// Create a system session for the root level with Owner role
pub fn owner() -> Session {
Session::for_level(Level::Root, Role::Owner)
}
/// Create a system session for the root level with Editor role
pub fn editor() -> Session {
Session::for_level(Level::Root, Role::Editor)
}
/// Create a system session for the root level with Viwer role
pub fn viewer() -> Session {
Session::for_level(Level::Root, Role::Viewer)
}
}

View file

@ -1,12 +1,14 @@
use crate::ctx::Context;
use crate::dbs::{Auth, Options, Transaction};
use crate::dbs::{Options, Transaction};
use crate::iam::Auth;
use crate::iam::Role;
use crate::kvs::Datastore;
use futures::lock::Mutex;
use std::sync::Arc;
pub async fn mock<'a>() -> (Context<'a>, Options, Transaction) {
let ctx = Context::default();
let opt = Options::default().with_auth(Arc::new(Auth::Kv));
let opt = Options::default().with_auth(Arc::new(Auth::for_root(Role::Owner)));
let kvs = Datastore::new("memory").await.unwrap();
let txn = kvs.transaction(true, false).await.unwrap();
let txn = Arc::new(Mutex::new(txn));

View file

@ -16,7 +16,7 @@ impl<'a> Document<'a> {
// Check if this record exists
if self.id.is_some() {
// Should we run permissions checks?
if opt.perms && opt.auth.perms() {
if opt.check_perms(stm.into()) {
// Get the table
let tb = self.tb(opt, txn).await?;
// Get the permission clause

View file

@ -1,8 +1,9 @@
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Transaction;
use crate::dbs::Workable;
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::idx::ft::docids::DocId;
use crate::idx::planner::executor::IteratorRef;
use crate::sql::statements::define::DefineEventStatement;
@ -12,6 +13,7 @@ use crate::sql::statements::define::DefineTableStatement;
use crate::sql::statements::live::LiveStatement;
use crate::sql::thing::Thing;
use crate::sql::value::Value;
use crate::sql::Base;
use std::borrow::Cow;
use std::fmt::{Debug, Formatter};
use std::sync::Arc;
@ -125,16 +127,16 @@ impl<'a> Document<'a> {
// The table doesn't exist
Err(Error::TbNotFound {
value: _,
}) => match opt.auth.check(Level::Db) {
}) => {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Table, &Base::Db)?;
// We can create the table automatically
true => {
run.add_and_cache_ns(opt.ns(), opt.strict).await?;
run.add_and_cache_db(opt.ns(), opt.db(), opt.strict).await?;
run.add_and_cache_tb(opt.ns(), opt.db(), &rid.tb, opt.strict).await
}
// We can't create the table so error
false => Err(Error::QueryPermissions),
},
run.add_and_cache_ns(opt.ns(), opt.strict).await?;
run.add_and_cache_db(opt.ns(), opt.db(), opt.strict).await?;
run.add_and_cache_tb(opt.ns(), opt.db(), &rid.tb, opt.strict).await
}
// There was an error
Err(err) => Err(err),
// The table exists

View file

@ -3,6 +3,7 @@ use crate::dbs::Statement;
use crate::dbs::{Options, Transaction};
use crate::doc::Document;
use crate::err::Error;
use crate::iam::Action;
use crate::sql::permission::Permission;
use crate::sql::value::Value;
@ -96,7 +97,7 @@ impl<'a> Document<'a> {
}
}
// Check for a PERMISSIONS clause
if opt.perms && opt.auth.perms() {
if opt.check_perms(Action::Edit) {
// Get the permission clause
let perms = if self.is_new() {
&fd.permissions.create

View file

@ -3,6 +3,7 @@ use crate::dbs::Statement;
use crate::dbs::{Options, Transaction};
use crate::doc::Document;
use crate::err::Error;
use crate::iam::Action;
use crate::sql::idiom::Idiom;
use crate::sql::output::Output;
use crate::sql::paths::META;
@ -59,7 +60,7 @@ impl<'a> Document<'a> {
// Check if this record exists
if self.id.is_some() {
// Should we run permissions checks?
if opt.perms && opt.auth.perms() {
if opt.check_perms(Action::View) {
// Loop through all field statements
for fd in self.fd(opt, txn).await?.iter() {
// Loop over each field in document

View file

@ -1,3 +1,4 @@
use crate::iam::Error as IamError;
use crate::idx::ft::MatchRef;
use crate::sql::idiom::Idiom;
use crate::sql::value::Value;
@ -27,6 +28,10 @@ pub enum Error {
#[error("The database encountered unreachable logic")]
Unreachable,
/// Statement has been deprecated
#[error("{0}")]
Deprecated(String),
/// There was a problem with the underlying datastore
#[error("There was a problem with the underlying datastore: {0}")]
Ds(String),
@ -185,10 +190,6 @@ pub enum Error {
message: String,
},
/// The permissions do not allow for performing the specified query
#[error("You don't have permission to perform this query type")]
QueryPermissions,
/// The permissions do not allow for changing to the specified namespace
#[error("You don't have permission to change to the {ns} namespace")]
NsNotAllowed {
@ -303,6 +304,27 @@ pub enum Error {
value: String,
},
/// The requested root user does not exist
#[error("The root user '{value}' does not exist")]
UserRootNotFound {
value: String,
},
/// The requested namespace user does not exist
#[error("The user '{value}' does not exist in the namespace '{ns}'")]
UserNsNotFound {
value: String,
ns: String,
},
/// The requested database user does not exist
#[error("The user '{value}' does not exist in the database '{db}'")]
UserDbNotFound {
value: String,
ns: String,
db: String,
},
/// Unable to perform the realtime query
#[error("Unable to perform the realtime query")]
RealtimeDisabled,
@ -453,11 +475,11 @@ pub enum Error {
TryFrom(String, &'static str),
/// There was an error processing a remote HTTP request
#[error("There was an error processing a remote HTTP request")]
#[error("There was an error processing a remote HTTP request: {0}")]
Http(String),
/// There was an error processing a value in parallel
#[error("There was an error processing a value in parallel")]
#[error("There was an error processing a value in parallel: {0}")]
Channel(String),
/// Represents an underlying error with Serde encoding / decoding
@ -530,6 +552,14 @@ pub enum Error {
#[error("Versionstamp in key is corrupted: {0}")]
CorruptedVersionstampInKey(#[from] VersionstampError),
/// Invalid level
#[error("Invalid level '{0}'")]
InvalidLevel(String),
/// Represents an underlying IAM error
#[error("IAM error: {0}")]
IamError(#[from] IamError),
}
impl From<Error> for String {

View file

@ -400,7 +400,7 @@ mod tests {
let sql =
format!("RETURN function() {{ return typeof surrealdb.functions.{name}; }}");
let dbs = crate::kvs::Datastore::new("memory").await.unwrap();
let ses = crate::dbs::Session::for_kv().with_ns("test").with_db("test");
let ses = crate::dbs::Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(&sql, &ses, None).await.unwrap();
let tmp = res.remove(0).result.unwrap();
if tmp == Value::from("object") {

94
lib/src/iam/auth.rs Normal file
View file

@ -0,0 +1,94 @@
use crate::sql::statements::{DefineTokenStatement, DefineUserStatement};
use serde::{Deserialize, Serialize};
use super::{is_allowed, Action, Actor, Error, Level, Resource, Role};
/// Specifies the current authentication for the datastore execution context.
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct Auth {
actor: Actor,
}
impl Auth {
pub fn new(actor: Actor) -> Self {
Self {
actor,
}
}
pub fn id(&self) -> &str {
self.actor.id()
}
/// Return current authentication level
pub fn level(&self) -> &Level {
self.actor.level()
}
/// Check if the current auth is anonymous
pub fn is_anon(&self) -> bool {
matches!(self.level(), Level::No)
}
/// Check if the current level is Root
pub fn is_root(&self) -> bool {
matches!(self.level(), Level::Root)
}
/// Check if the current level is Namespace
pub fn is_ns(&self) -> bool {
matches!(self.level(), Level::Namespace(_))
}
/// Check if the current level is Database
pub fn is_db(&self) -> bool {
matches!(self.level(), Level::Database(_, _))
}
/// Check if the current level is Scope
pub fn is_scope(&self) -> bool {
matches!(self.level(), Level::Scope(_, _, _))
}
/// System Auth helpers
///
/// These are not stored in the database and are used for internal operations
/// Do not use for authentication
pub fn for_root(role: Role) -> Self {
Self::new(Actor::new("system_auth".into(), vec![role], Level::Root))
}
pub fn for_ns(role: Role, ns: &str) -> Self {
Self::new(Actor::new("system_auth".into(), vec![role], (ns,).into()))
}
pub fn for_db(role: Role, ns: &str, db: &str) -> Self {
Self::new(Actor::new("system_auth".into(), vec![role], (ns, db).into()))
}
//
// Permission checks
//
/// Checks if the current auth is allowed to perform an action on a given resource
pub fn is_allowed(&self, action: Action, res: &Resource) -> Result<(), Error> {
is_allowed(&self.actor, &action, res, None)
}
/// Checks if the current actor has a given role
pub fn has_role(&self, role: &Role) -> bool {
self.actor.has_role(role)
}
}
impl std::convert::From<(&DefineUserStatement, Level)> for Auth {
fn from(val: (&DefineUserStatement, Level)) -> Self {
Self::new((val.0, val.1).into())
}
}
impl std::convert::From<(&DefineTokenStatement, Level)> for Auth {
fn from(val: (&DefineTokenStatement, Level)) -> Self {
Self::new((val.0, val.1).into())
}
}

View file

@ -1,10 +1,10 @@
use crate::dbs::Auth;
use super::Auth;
use crate::dbs::Session;
use crate::err::Error;
use std::sync::Arc;
pub fn clear(session: &mut Session) -> Result<(), Error> {
session.au = Arc::new(Auth::No);
session.au = Arc::new(Auth::default());
session.tk = None;
session.sc = None;
session.sd = None;

View file

@ -0,0 +1,57 @@
use std::str::FromStr;
use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid};
use crate::dbs::Statement;
// TODO(sgirones): For now keep it simple. In the future, we will allow for custom roles and policies using a more exhaustive list of actions and resources.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd)]
pub enum Action {
View,
Edit,
}
impl std::fmt::Display for Action {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Action::View => write!(f, "View"),
Action::Edit => write!(f, "Edit"),
}
}
}
impl Action {
pub fn id(&self) -> String {
self.to_string()
}
}
impl std::convert::From<&Action> for EntityUid {
fn from(action: &Action) -> Self {
EntityUid::from_type_name_and_id(
EntityTypeName::from_str("Action").unwrap(),
EntityId::from_str(&action.id()).unwrap(),
)
}
}
impl std::convert::From<&Action> for Entity {
fn from(action: &Action) -> Self {
Entity::new(action.into(), Default::default(), Default::default())
}
}
impl From<&Statement<'_>> for Action {
fn from(stmt: &Statement) -> Self {
match stmt {
Statement::Live(_) => Action::View,
Statement::Select(_) => Action::View,
Statement::Show(_) => Action::View,
Statement::Create(_) => Action::Edit,
Statement::Update(_) => Action::Edit,
Statement::Relate(_) => Action::Edit,
Statement::Delete(_) => Action::Edit,
Statement::Insert(_) => Action::Edit,
}
}
}

View file

@ -0,0 +1,9 @@
mod action;
mod resources;
mod roles;
mod schema;
pub use self::action::*;
pub use self::resources::*;
pub use self::roles::*;
pub use self::schema::*;

View file

@ -0,0 +1,127 @@
use std::collections::{HashMap, HashSet};
use std::ops::Deref;
use std::str::FromStr;
use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
use serde::{Deserialize, Serialize};
use super::{Level, Resource, ResourceKind};
use crate::iam::Role;
use crate::sql::statements::{DefineTokenStatement, DefineUserStatement};
//
// User
//
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize)]
pub struct Actor {
res: Resource,
roles: Vec<Role>,
}
impl Default for Actor {
fn default() -> Self {
Self {
res: ResourceKind::Actor.on_level(Level::No),
roles: Vec::new(),
}
}
}
impl std::fmt::Display for Actor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.res.level() == &Level::No {
return write!(f, "Actor::Anonymous");
}
write!(
f,
"{}{}::{}({})",
self.res.level(),
self.res.kind(),
self.res.id(),
self.roles.iter().map(|r| format!("{}", r)).collect::<Vec<String>>().join(", ")
)
}
}
impl Actor {
pub fn new(id: String, roles: Vec<Role>, level: Level) -> Self {
Self {
res: Resource::new(id, super::ResourceKind::Actor, level),
roles,
}
}
/// Checks if the actor has the given role.
pub fn has_role(&self, role: &Role) -> bool {
self.roles.contains(role)
}
// Cedar policy helpers
pub fn cedar_attrs(&self) -> HashMap<String, RestrictedExpression> {
[
("type", self.kind().into()),
("level", self.level().into()),
("roles", RestrictedExpression::new_set(self.roles.iter().map(|r| r.into()))),
]
.into_iter()
.map(|(x, v)| (x.into(), v))
.collect()
}
pub fn cedar_parents(&self) -> HashSet<EntityUid> {
let mut parents = HashSet::with_capacity(1);
parents.insert(self.res.level().into());
parents
}
pub fn cedar_entities(&self) -> Vec<Entity> {
let mut entities = Vec::new();
entities.push(self.into());
for role in self.roles.iter() {
entities.push(role.into());
}
for level in self.res.level().cedar_entities() {
entities.push(level);
}
entities
}
}
impl Deref for Actor {
type Target = Resource;
fn deref(&self) -> &Self::Target {
&self.res
}
}
impl std::convert::From<&Actor> for EntityUid {
fn from(actor: &Actor) -> Self {
EntityUid::from_type_name_and_id(
EntityTypeName::from_str("Actor").unwrap(),
EntityId::from_str(actor.id()).unwrap(),
)
}
}
impl std::convert::From<&Actor> for Entity {
fn from(actor: &Actor) -> Self {
Entity::new(actor.into(), actor.cedar_attrs(), actor.cedar_parents())
}
}
impl std::convert::From<(&DefineUserStatement, Level)> for Actor {
fn from(val: (&DefineUserStatement, Level)) -> Self {
Self::new(val.0.name.to_string(), val.0.roles.iter().map(Role::from).collect(), val.1)
}
}
impl std::convert::From<(&DefineTokenStatement, Level)> for Actor {
fn from(val: (&DefineTokenStatement, Level)) -> Self {
Self::new(val.0.name.to_string(), Vec::default(), val.1)
}
}

View file

@ -0,0 +1,192 @@
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use cedar_policy::{Entity, EntityTypeName, EntityUid, RestrictedExpression};
use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Deserialize, Serialize, Hash)]
pub enum Level {
#[default]
No,
Root,
Namespace(String),
Database(String, String),
Scope(String, String, String),
}
impl std::fmt::Display for Level {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Level::No => write!(f, "No"),
Level::Root => write!(f, "/"),
Level::Namespace(ns) => write!(f, "/ns:{ns}/"),
Level::Database(ns, db) => write!(f, "/ns:{ns}/db:{db}/"),
Level::Scope(ns, db, scope) => write!(f, "/ns:{ns}/db:{db}/scope:{scope}/"),
}
}
}
impl Level {
pub fn level_name(&self) -> &str {
match self {
Level::No => "No",
Level::Root => "Root",
Level::Namespace(_) => "Namespace",
Level::Database(_, _) => "Database",
Level::Scope(_, _, _) => "Scope",
}
}
pub fn ns(&self) -> Option<&str> {
match self {
Level::Namespace(ns) => Some(ns),
Level::Database(ns, _) => Some(ns),
Level::Scope(ns, _, _) => Some(ns),
_ => None,
}
}
pub fn db(&self) -> Option<&str> {
match self {
Level::Database(_, db) => Some(db),
Level::Scope(_, db, _) => Some(db),
_ => None,
}
}
pub fn scope(&self) -> Option<&str> {
match self {
Level::Scope(_, _, scope) => Some(scope),
_ => None,
}
}
fn parent(&self) -> Option<Level> {
match self {
Level::No => None,
Level::Root => None,
Level::Namespace(_) => Some(Level::Root),
Level::Database(ns, _) => Some(Level::Namespace(ns.to_owned())),
Level::Scope(ns, db, _) => Some(Level::Database(ns.to_owned(), db.to_owned())),
}
}
// Cedar policy helpers
pub fn cedar_attrs(&self) -> HashMap<String, RestrictedExpression> {
let mut attrs = HashMap::with_capacity(5);
attrs.insert("type".into(), RestrictedExpression::new_string(self.level_name().to_owned()));
if let Some(ns) = self.ns() {
attrs.insert("ns".into(), RestrictedExpression::new_string(ns.to_owned()));
}
if let Some(db) = self.db() {
attrs.insert("db".into(), RestrictedExpression::new_string(db.to_owned()));
}
if let Some(scope) = self.scope() {
attrs.insert("scope".into(), RestrictedExpression::new_string(scope.to_owned()));
}
attrs
}
pub fn cedar_parents(&self) -> HashSet<EntityUid> {
if let Some(parent) = self.parent() {
return HashSet::from([parent.into()]);
}
HashSet::with_capacity(0)
}
pub fn cedar_entities(&self) -> Vec<Entity> {
let mut entities = Vec::new();
entities.push(self.into());
// Find all the parents
let mut parent = self.parent();
while let Some(p) = parent {
parent = p.parent();
entities.push(p.into());
}
entities
}
}
impl From<()> for Level {
fn from(_: ()) -> Self {
Level::Root
}
}
impl From<(&str,)> for Level {
fn from(val: (&str,)) -> Self {
Level::Namespace(val.0.to_owned())
}
}
impl From<(&str, &str)> for Level {
fn from(val: (&str, &str)) -> Self {
Level::Database(val.0.to_owned(), val.1.to_owned())
}
}
impl From<(&str, &str, &str)> for Level {
fn from(val: (&str, &str, &str)) -> Self {
Level::Scope(val.0.to_owned(), val.1.to_owned(), val.2.to_owned())
}
}
impl From<(Option<&str>, Option<&str>, Option<&str>)> for Level {
fn from(val: (Option<&str>, Option<&str>, Option<&str>)) -> Self {
match val {
(None, None, None) => ().into(),
(Some(ns), None, None) => (ns,).into(),
(Some(ns), Some(db), None) => (ns, db).into(),
(Some(ns), Some(db), Some(scope)) => (ns, db, scope).into(),
_ => Level::No,
}
}
}
impl std::convert::From<Level> for EntityUid {
fn from(level: Level) -> Self {
EntityUid::from_type_name_and_id(
EntityTypeName::from_str("Level").unwrap(),
format!("{}", level).parse().unwrap(),
)
}
}
impl std::convert::From<&Level> for EntityUid {
fn from(level: &Level) -> Self {
level.to_owned().into()
}
}
impl std::convert::From<Level> for Entity {
fn from(level: Level) -> Self {
Entity::new(level.to_owned().into(), level.cedar_attrs(), level.cedar_parents())
}
}
impl std::convert::From<&Level> for Entity {
fn from(level: &Level) -> Self {
level.to_owned().into()
}
}
impl std::convert::From<Level> for RestrictedExpression {
fn from(level: Level) -> Self {
format!("{}", EntityUid::from(level)).parse().unwrap()
}
}
impl std::convert::From<&Level> for RestrictedExpression {
fn from(level: &Level) -> Self {
level.to_owned().into()
}
}

View file

@ -0,0 +1,7 @@
mod actor;
mod level;
mod resource;
pub use actor::*;
pub use level::*;
pub use resource::*;

View file

@ -0,0 +1,150 @@
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
use super::Level;
use cedar_policy::{Entity, EntityId, EntityTypeName, EntityUid, RestrictedExpression};
use serde::{Deserialize, Serialize};
#[derive(Clone, Default, Debug, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize)]
pub enum ResourceKind {
#[default]
Any,
Namespace,
Database,
Scope,
Table,
Document,
Option,
Function,
Analyzer,
Parameter,
Event,
Field,
Index,
// IAM
Actor,
}
impl std::fmt::Display for ResourceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ResourceKind::Any => write!(f, "Any"),
ResourceKind::Namespace => write!(f, "Namespace"),
ResourceKind::Database => write!(f, "Database"),
ResourceKind::Scope => write!(f, "Scope"),
ResourceKind::Table => write!(f, "Table"),
ResourceKind::Document => write!(f, "Document"),
ResourceKind::Option => write!(f, "Option"),
ResourceKind::Function => write!(f, "Function"),
ResourceKind::Analyzer => write!(f, "Analyzer"),
ResourceKind::Parameter => write!(f, "Parameter"),
ResourceKind::Event => write!(f, "Event"),
ResourceKind::Field => write!(f, "Field"),
ResourceKind::Index => write!(f, "Index"),
ResourceKind::Actor => write!(f, "Actor"),
}
}
}
impl ResourceKind {
// Helpers for building default resources for specific levels. Useful for authorization checks.
pub fn on_level(self, level: Level) -> Resource {
Resource::new("".into(), self, level)
}
pub fn on_root(self) -> Resource {
self.on_level(Level::Root)
}
pub fn on_ns(self, ns: &str) -> Resource {
self.on_level((ns,).into())
}
pub fn on_db(self, ns: &str, db: &str) -> Resource {
self.on_level((ns, db).into())
}
pub fn on_scope(self, ns: &str, db: &str, scope: &str) -> Resource {
self.on_level((ns, db, scope).into())
}
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize)]
pub struct Resource(String, ResourceKind, Level);
impl std::fmt::Display for Resource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let Resource(id, kind, level) = self;
write!(f, "{}{}:\"{}\"", level, kind, id)
}
}
impl Resource {
pub fn new(id: String, kind: ResourceKind, level: Level) -> Self {
Self(id, kind, level)
}
pub fn id(&self) -> &str {
&self.0
}
pub fn kind(&self) -> &ResourceKind {
&self.1
}
pub fn level(&self) -> &Level {
&self.2
}
// Cedar policy helpers
pub fn cedar_attrs(&self) -> HashMap<String, RestrictedExpression> {
[("type", self.kind().into()), ("level", self.level().into())]
.into_iter()
.map(|(x, v)| (x.into(), v))
.collect()
}
pub fn cedar_parents(&self) -> HashSet<EntityUid> {
HashSet::from([self.level().into()])
}
pub fn cedar_entities(&self) -> Vec<Entity> {
let mut entities = Vec::new();
entities.push(self.into());
entities.extend(self.level().cedar_entities());
entities
}
}
impl std::convert::From<&Resource> for EntityUid {
fn from(res: &Resource) -> Self {
EntityUid::from_type_name_and_id(
EntityTypeName::from_str(&res.kind().to_string()).unwrap(),
EntityId::from_str(res.id()).unwrap(),
)
}
}
impl std::convert::From<&Resource> for Entity {
fn from(res: &Resource) -> Self {
Entity::new(res.into(), res.cedar_attrs(), res.cedar_parents())
}
}
impl std::convert::From<&Resource> for RestrictedExpression {
fn from(res: &Resource) -> Self {
format!("{}", EntityUid::from(res)).parse().unwrap()
}
}
impl std::convert::From<&ResourceKind> for RestrictedExpression {
fn from(kind: &ResourceKind) -> Self {
RestrictedExpression::new_string(kind.to_string())
}
}

View file

@ -0,0 +1,81 @@
use crate::iam::Error;
use crate::sql::Ident;
use cedar_policy::{Entity, EntityTypeName, EntityUid, RestrictedExpression};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
// In the future, we will allow for custom roles. For now, provide predefined roles.
#[derive(Hash, Clone, Default, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum Role {
#[default]
Viewer,
Editor,
Owner,
}
impl std::fmt::Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::Viewer => write!(f, "Viewer"),
Self::Editor => write!(f, "Editor"),
Self::Owner => write!(f, "Owner"),
}
}
}
impl FromStr for Role {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"viewer" => Ok(Self::Viewer),
"editor" => Ok(Self::Editor),
"owner" => Ok(Self::Owner),
_ => Err(Error::InvalidRole(s.to_string())),
}
}
}
impl std::convert::From<&str> for Role {
fn from(s: &str) -> Self {
Self::from_str(s).unwrap()
}
}
impl std::convert::From<String> for Role {
fn from(s: String) -> Self {
Self::from_str(&s).unwrap()
}
}
impl std::convert::From<&Ident> for Role {
fn from(id: &Ident) -> Self {
Role::from_str(id).unwrap()
}
}
impl std::convert::From<Role> for Ident {
fn from(role: Role) -> Self {
role.to_string().into()
}
}
impl std::convert::From<&Role> for EntityUid {
fn from(role: &Role) -> Self {
EntityUid::from_type_name_and_id(
EntityTypeName::from_str("Role").unwrap(),
format!("{}", role).parse().unwrap(),
)
}
}
impl std::convert::From<&Role> for Entity {
fn from(role: &Role) -> Self {
Entity::new(role.into(), Default::default(), Default::default())
}
}
impl std::convert::From<&Role> for RestrictedExpression {
fn from(role: &Role) -> Self {
format!("{}", EntityUid::from(role)).parse().unwrap()
}
}

View file

@ -0,0 +1,97 @@
use cedar_policy::Schema;
use once_cell::sync::Lazy;
pub static DEFAULT_CEDAR_SCHEMA: Lazy<serde_json::Value> = Lazy::new(|| {
serde_json::json!(
{
"": {
"commonTypes": {
// Represents a Resource
"Resource": {
"type": "Record",
"attributes": {
"type": { "type": "String", "required": true },
"level" : { "type": "Entity", "name": "Level", "required": true },
}
},
},
"entityTypes": {
// Represents the Root, Namespace, Database and Scope levels
"Level": {
"shape": {
"type": "Record",
"attributes": {
"type": { "type": "String", "required": true },
"ns": { "type": "String", "required": false },
"db": { "type": "String", "required": false },
"scope": { "type": "String", "required": false },
"table": { "type": "String", "required": false },
"level" : { "type": "Entity", "name": "Level", "required": true },
}
},
"memberOfTypes": ["Level"],
},
// Base resource types
"Any": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Namespace": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Database": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Scope": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Table": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Document": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Option": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Function": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Analyzer": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Parameter": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Event": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Field": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
"Index": {"shape": {"type": "Resource"}, "memberOfTypes": ["Level"]},
// IAM resource types
"Role": {},
"Actor": {
"shape": {
"type": "Record",
"attributes": {
"type": { "type": "String", "required": true },
"level" : { "type": "Entity", "name": "Level", "required": true },
"roles": { "type": "Set", "element": { "type": "Entity", "name": "Role" }, "required": true},
},
},
"memberOfTypes": ["Level"],
},
},
"actions": {
"View": {
"appliesTo": {
"principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
},
},
"Edit": {
"appliesTo": {
"principalTypes": [ "Actor" ],
"resourceTypes": [ "Any", "Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index", "Actor" ],
},
},
},
}
}
)
});
pub fn default_schema() -> Schema {
Schema::from_json_value(DEFAULT_CEDAR_SCHEMA.to_owned()).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_schema() {
let schema = default_schema();
assert_eq!(schema.action_entities().unwrap().iter().count(), 2);
}
}

View file

@ -1,9 +1,56 @@
use cedar_policy::Context;
pub use entities::Level;
use thiserror::Error;
pub mod auth;
pub mod base;
pub mod clear;
pub mod parse;
pub mod entities;
pub mod policies;
pub mod signin;
pub mod signup;
pub mod token;
pub mod verify;
pub const TOKEN: &str = "Bearer ";
pub use self::auth::*;
pub use self::entities::*;
#[derive(Error, Debug)]
pub enum Error {
#[error("Invalid role '{0}'")]
InvalidRole(String),
#[error("Not enough permissions to perform this action")]
NotAllowed {
actor: String,
action: String,
resource: String,
},
}
impl From<Error> for String {
fn from(e: Error) -> String {
e.to_string()
}
}
pub fn is_allowed(
actor: &Actor,
action: &Action,
resource: &Resource,
ctx: Option<Context>,
) -> Result<(), Error> {
match policies::is_allowed(actor, action, resource, ctx.unwrap_or(Context::empty())) {
(allowed, _) if allowed => Ok(()),
_ => {
let err = Error::NotAllowed {
actor: actor.to_string(),
action: action.to_string(),
resource: format!("{}", resource),
};
trace!("{}", err);
Err(err)
}
}
}

View file

@ -1,16 +0,0 @@
use crate::err::Error;
use crate::iam::base::{Engine, BASE64};
use crate::sql::json;
use crate::sql::Value;
use std::str;
pub fn parse(value: &str) -> Result<Value, Error> {
// Extract the middle part of the token
let value = value.splitn(3, '.').skip(1).take(1).next().ok_or(Error::InvalidAuth)?;
// Decode the base64 token data content
let value = BASE64.decode(value).map_err(|_| Error::InvalidAuth)?;
// Convert the decoded data to a string
let value = str::from_utf8(&value).map_err(|_| Error::InvalidAuth)?;
// Parse the token data into SurrealQL
json(value).map_err(|_| Error::InvalidAuth)
}

View file

@ -0,0 +1,96 @@
use cedar_policy::{Authorizer, Context, Decision, Entities, Entity, EntityUid, Request, Response};
mod policy_set;
use policy_set::*;
use crate::iam::{Action, Actor, Resource};
/// Checks if the actor is allowed to do the action on the resource, given the context and based on the default policy set.
pub fn is_allowed(
actor: &Actor,
action: &Action,
resource: &Resource,
context: Context,
) -> (bool, Response) {
_is_allowed(
Some(actor.into()),
Some(action.into()),
Some(resource.into()),
Entities::from_entities(_get_entities(Some(actor), Some(resource))).unwrap(),
context,
)
}
fn _get_entities(actor: Option<&Actor>, resource: Option<&Resource>) -> Vec<Entity> {
let mut entities = Vec::new();
if let Some(actor) = actor {
entities.extend(actor.cedar_entities());
}
if let Some(resource) = resource {
entities.extend(resource.cedar_entities());
}
entities
}
fn _is_allowed(
actor: Option<EntityUid>,
action: Option<EntityUid>,
resource: Option<EntityUid>,
entities: Entities,
context: Context,
) -> (bool, Response) {
let policy_set = POLICY_SET.to_owned();
let authorizer = Authorizer::new();
let req = Request::new(actor, action, resource, context);
let res = authorizer.is_authorized(&req, &policy_set, &entities);
(res.decision() == Decision::Allow, res)
}
#[cfg(test)]
mod tests {
use cedar_policy::{ValidationMode, ValidationResult, Validator};
use crate::iam::{default_schema, entities::Level, ResourceKind, Role};
use super::*;
#[test]
fn validate_policy_set() {
let validator = Validator::new(default_schema());
let result = Validator::validate(&validator, &POLICY_SET, ValidationMode::default());
if !ValidationResult::validation_passed(&result) {
let e = ValidationResult::validation_errors(&result);
for err in e {
println!("{}", err);
}
panic!("Policy set validation failed");
}
}
#[test]
fn test_is_allowed() {
// Returns true if the actor is allowed to do the action on the resource
let actor = Actor::new("test".into(), vec![Role::Viewer], Level::Root);
let res = ResourceKind::Namespace.on_root();
let (allowed, _) = is_allowed(&actor, &Action::View, &res, Context::empty());
assert!(allowed);
// Returns false if the actor is not allowed to do the action on the resource
let actor = Actor::new(
"test".into(),
vec![Role::Viewer],
Level::Database("test".into(), "test".into()),
);
let res = ResourceKind::Namespace.on_root();
let (allowed, _) = is_allowed(&actor, &Action::View, &res, Context::empty());
assert!(!allowed);
}
}

View file

@ -0,0 +1,40 @@
use std::str::FromStr;
use cedar_policy::PolicySet;
use once_cell::sync::Lazy;
pub static POLICY_SET: Lazy<PolicySet> = Lazy::new(|| {
PolicySet::from_str(
r#"
// All roles can view all resources on the same level hierarchy or below
permit(
principal,
action == Action::"View",
resource
) when {
principal.roles.containsAny([Role::"Viewer", Role::"Editor", Role::"Owner"]) &&
resource.level in principal.level
};
// Editor role can edit all non-IAM resources on the same level hierarchy or below
permit(
principal,
action == Action::"Edit",
resource
) when {
principal.roles.contains(Role::"Editor") &&
resource.level in principal.level &&
["Namespace", "Database", "Scope", "Table", "Document", "Option", "Function", "Analyzer", "Parameter", "Event", "Field", "Index"].contains(resource.type)
};
// Owner role can edit all resources on the same level hierarchy or below
permit(
principal,
action == Action::"Edit",
resource
) when {
principal.roles.contains(Role::"Owner") &&
resource.level in principal.level
};
"#).unwrap()
});

View file

@ -1,21 +1,20 @@
use crate::cnf::SERVER_NAME;
use crate::dbs::Auth;
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::token::{Claims, HEADER};
use crate::iam::Auth;
use crate::kvs::Datastore;
use crate::opt::auth::Root;
use crate::sql::Object;
use crate::sql::Value;
use argon2::password_hash::{PasswordHash, PasswordVerifier};
use argon2::Argon2;
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey};
use std::sync::Arc;
use super::verify::verify_creds;
use super::{Actor, Level};
pub async fn signin(
kvs: &Datastore,
configured_root: &Option<Root<'_>>,
session: &mut Session,
vars: Object,
) -> Result<Option<String>, Error> {
@ -25,6 +24,7 @@ pub async fn signin(
let sc = vars.get("SC").or_else(|| vars.get("sc"));
// Check if the parameters exist
match (ns, db, sc) {
// SCOPE signin
(Some(ns), Some(db), Some(sc)) => {
// Process the provided values
let ns = ns.to_raw_string();
@ -33,6 +33,7 @@ pub async fn signin(
// Attempt to signin to specified scope
super::signin::sc(kvs, session, ns, db, sc, vars).await
}
// DB signin
(Some(ns), Some(db), None) => {
// Get the provided user and pass
let user = vars.get("user");
@ -53,6 +54,7 @@ pub async fn signin(
_ => Err(Error::InvalidAuth),
}
}
// NS signin
(Some(ns), None, None) => {
// Get the provided user and pass
let user = vars.get("user");
@ -72,6 +74,7 @@ pub async fn signin(
_ => Err(Error::InvalidAuth),
}
}
// KV signin
(None, None, None) => {
// Get the provided user and pass
let user = vars.get("user");
@ -83,9 +86,8 @@ pub async fn signin(
// Process the provided values
let user = user.to_raw_string();
let pass = pass.to_raw_string();
// Attempt to signin to namespace
super::signin::su(configured_root, session, user, pass)?;
Ok(None)
super::signin::kv(kvs, session, user, pass).await
}
// There is no username or password
_ => Err(Error::InvalidAuth),
@ -105,7 +107,7 @@ pub async fn sc(
) -> Result<Option<String>, Error> {
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Check if the supplied NS Login exists
// Check if the supplied DB Scope exists
match tx.get_sc(&ns, &db, &sc).await {
Ok(sv) => {
match sv.signin {
@ -113,8 +115,8 @@ pub async fn sc(
Some(val) => {
// Setup the query params
let vars = Some(vars.0);
// Setup the query session
let sess = Session::for_db(&ns, &db);
// Setup the system session for finding the signin record
let sess = Session::viewer().with_ns(&ns).with_db(&db);
// Compute the value with the params
match kvs.compute(val, &sess, vars).await {
// The signin value succeeded
@ -150,8 +152,12 @@ pub async fn sc(
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(rid));
session.au = Arc::new(Auth::Sc(ns, db, sc));
session.sd = Some(Value::from(rid.to_owned()));
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
// Check the authentication token
match enc {
// The auth token was created successfully
@ -184,49 +190,37 @@ pub async fn db(
user: String,
pass: String,
) -> Result<Option<String>, Error> {
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Check if the supplied DB Login exists
match tx.get_dl(&ns, &db, &user).await {
Ok(dl) => {
// Compute the hash and verify the password
let hash = PasswordHash::new(&dl.hash).unwrap();
// Attempt to verify the password using Argon2
match Argon2::default().verify_password(pass.as_ref(), &hash) {
Ok(_) => {
// Create the authentication key
let key = EncodingKey::from_secret(dl.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
id: Some(user),
..Claims::default()
};
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.au = Arc::new(Auth::Db(ns, db));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
// There was an error creating the token
_ => Err(Error::InvalidAuth),
}
}
// The password did not verify
match verify_creds(kvs, Some(&ns), Some(&db), &user, &pass).await {
Ok((auth, u)) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
ns: Some(ns.to_owned()),
db: Some(db.to_owned()),
id: Some(user),
..Claims::default()
};
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.au = Arc::new(auth);
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
// There was an error creating the token
_ => Err(Error::InvalidAuth),
}
}
// The specified user login does not exist
// The password did not verify
_ => Err(Error::InvalidAuth),
}
}
@ -238,64 +232,70 @@ pub async fn ns(
user: String,
pass: String,
) -> Result<Option<String>, Error> {
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Check if the supplied NS Login exists
match tx.get_nl(&ns, &user).await {
Ok(nl) => {
// Compute the hash and verify the password
let hash = PasswordHash::new(&nl.hash).unwrap();
// Attempt to verify the password using Argon2
match Argon2::default().verify_password(pass.as_ref(), &hash) {
Ok(_) => {
// Create the authentication key
let key = EncodingKey::from_secret(nl.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
ns: Some(ns.to_owned()),
id: Some(user),
..Claims::default()
};
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.au = Arc::new(Auth::Ns(ns));
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
// There was an error creating the token
_ => Err(Error::InvalidAuth),
}
}
// The password did not verify
match verify_creds(kvs, Some(&ns), None, &user, &pass).await {
Ok((auth, u)) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
ns: Some(ns.to_owned()),
id: Some(user),
..Claims::default()
};
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.ns = Some(ns.to_owned());
session.au = Arc::new(auth);
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
// There was an error creating the token
_ => Err(Error::InvalidAuth),
}
}
// The specified user login does not exist
_ => Err(Error::InvalidAuth),
Err(e) => Err(e),
}
}
pub fn su(
configured_root: &Option<Root<'_>>,
pub async fn kv(
kvs: &Datastore,
session: &mut Session,
user: String,
pass: String,
) -> Result<(), Error> {
// Attempt to verify the root user
if let Some(root) = configured_root {
if user == root.username && pass == root.password {
session.au = Arc::new(Auth::Kv);
return Ok(());
) -> Result<Option<String>, Error> {
match verify_creds(kvs, None, None, &user, &pass).await {
Ok((auth, u)) => {
// Create the authentication key
let key = EncodingKey::from_secret(u.code.as_ref());
// Create the authentication claim
let val = Claims {
iss: Some(SERVER_NAME.to_owned()),
iat: Some(Utc::now().timestamp()),
nbf: Some(Utc::now().timestamp()),
exp: Some((Utc::now() + Duration::hours(1)).timestamp()),
id: Some(user),
..Claims::default()
};
// Create the authentication token
let enc = encode(&HEADER, &val, &key);
// Set the authentication on the session
session.tk = Some(val.into());
session.au = Arc::new(auth);
// Check the authentication token
match enc {
// The auth token was created successfully
Ok(tk) => Ok(Some(tk)),
// There was an error creating the token
_ => Err(Error::InvalidAuth),
}
}
Err(e) => Err(e),
}
// The specified user login does not exist
Err(Error::InvalidAuth)
}

View file

@ -1,8 +1,9 @@
use crate::cnf::SERVER_NAME;
use crate::dbs::Auth;
use crate::dbs::Session;
use crate::err::Error;
use crate::iam::token::{Claims, HEADER};
use crate::iam::Auth;
use crate::iam::{Actor, Level};
use crate::kvs::Datastore;
use crate::sql::Object;
use crate::sql::Value;
@ -26,7 +27,7 @@ pub async fn signup(
let ns = ns.to_raw_string();
let db = db.to_raw_string();
let sc = sc.to_raw_string();
// Attempt to signin to specified scope
// Attempt to signup to specified scope
super::signup::sc(kvs, session, ns, db, sc, vars).await
}
_ => Err(Error::InvalidAuth),
@ -43,16 +44,16 @@ pub async fn sc(
) -> Result<Option<String>, Error> {
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Check if the supplied NS Login exists
// Check if the supplied Scope login exists
match tx.get_sc(&ns, &db, &sc).await {
Ok(sv) => {
match sv.signup {
// This scope allows signin
// This scope allows signup
Some(val) => {
// Setup the query params
let vars = Some(vars.0);
// Setup the query session
let sess = Session::for_db(&ns, &db);
// Setup the system session for creating the signup record
let sess = Session::editor().with_ns(&ns).with_db(&db);
// Compute the value with the params
match kvs.compute(val, &sess, vars).await {
// The signin value succeeded
@ -88,8 +89,12 @@ pub async fn sc(
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(rid));
session.au = Arc::new(Auth::Sc(ns, db, sc));
session.sd = Some(Value::from(rid.to_owned()));
session.au = Arc::new(Auth::new(Actor::new(
rid.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
// Create the authentication token
match enc {
// The auth token was created successfully
@ -101,11 +106,11 @@ pub async fn sc(
// No record was returned
_ => Err(Error::InvalidAuth),
},
// The signin query failed
_ => Err(Error::InvalidAuth),
// The signup query failed
Err(_) => Err(Error::InvalidAuth),
}
}
// This scope does not allow signin
// This scope does not allow signup
_ => Err(Error::InvalidAuth),
}
}

View file

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
pub static HEADER: Lazy<Header> = Lazy::new(|| Header::new(Algorithm::HS512));
#[derive(Debug, Default, Serialize, Deserialize)]
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
pub struct Claims {
#[serde(skip_serializing_if = "Option::is_none")]
pub iat: Option<i64>,
@ -51,6 +51,13 @@ pub struct Claims {
#[serde(alias = "https://surrealdb.com/record")]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(alias = "rl")]
#[serde(alias = "RL")]
#[serde(rename = "RL")]
#[serde(alias = "https://surrealdb.com/rl")]
#[serde(alias = "https://surrealdb.com/roles")]
#[serde(skip_serializing_if = "Option::is_none")]
pub roles: Option<Vec<String>>,
}
impl From<Claims> for Value {
@ -93,6 +100,10 @@ impl From<Claims> for Value {
if let Some(id) = v.id {
out.insert("ID".to_string(), id.into());
}
// Add RL field if set
if let Some(role) = v.roles {
out.insert("RL".to_string(), role.into());
}
// Return value
out.into()
}

View file

@ -1,16 +1,26 @@
use crate::dbs::Auth;
use crate::dbs::Session;
use crate::err::Error;
use crate::error;
use crate::iam::token::Claims;
use crate::iam::TOKEN;
use crate::iam::Auth;
use crate::iam::{Actor, Level, Role};
use crate::kvs::Datastore;
use crate::sql::json;
use crate::sql::statements::DefineUserStatement;
use crate::sql::Algorithm;
use crate::sql::Value;
use argon2::Argon2;
use argon2::PasswordHash;
use argon2::PasswordVerifier;
use base64_lib::Engine;
use chrono::Utc;
use jsonwebtoken::{decode, DecodingKey, Validation};
use once_cell::sync::Lazy;
use std::str::{self, FromStr};
use std::sync::Arc;
use super::base::BASE64;
fn config(algo: Algorithm, code: String) -> Result<(DecodingKey, Validation), Error> {
match algo {
Algorithm::Hs256 => Ok((
@ -78,31 +88,59 @@ static DUD: Lazy<Validation> = Lazy::new(|| {
validation
});
pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Result<(), Error> {
pub async fn basic(
kvs: &Datastore,
session: &mut Session,
user: &str,
pass: &str,
) -> Result<(), Error> {
// Log the authentication type
trace!("Attempting basic authentication");
match verify_creds(kvs, session.ns.as_ref(), session.db.as_ref(), user, pass).await {
Ok((au, _)) if au.is_root() => {
debug!("Authenticated as root user '{}'", user);
session.au = Arc::new(au);
Ok(())
}
Ok((au, _)) if au.is_ns() => {
debug!("Authenticated as namespace user '{}'", user);
session.au = Arc::new(au);
Ok(())
}
Ok((au, _)) if au.is_db() => {
debug!("Authenticated as database user '{}'", user);
session.au = Arc::new(au);
Ok(())
}
Ok(_) => Err(Error::InvalidAuth),
Err(e) => Err(e),
}
}
pub async fn token(kvs: &Datastore, session: &mut Session, token: &str) -> Result<(), Error> {
// Log the authentication type
trace!("Attempting token authentication");
// Retrieve just the auth data
let auth = auth.trim_start_matches(TOKEN).trim();
// Decode the token without verifying
let token = decode::<Claims>(auth, &KEY, &DUD)?;
let token_data = decode::<Claims>(token, &KEY, &DUD)?;
// Parse the token and catch any errors
let value = super::parse::parse(auth)?;
let value = parse(token)?;
// Check if the auth token can be used
if let Some(nbf) = token.claims.nbf {
if let Some(nbf) = token_data.claims.nbf {
if nbf > Utc::now().timestamp() {
trace!("The 'nbf' field in the authentication token was invalid");
return Err(Error::InvalidAuth);
}
}
// Check if the auth token has expired
if let Some(exp) = token.claims.exp {
if let Some(exp) = token_data.claims.exp {
if exp < Utc::now().timestamp() {
trace!("The 'exp' field in the authentication token was invalid");
return Err(Error::InvalidAuth);
}
}
// Check the token authentication claims
match token.claims {
match token_data.claims {
// Check if this is scope token authentication
Claims {
ns: Some(ns),
@ -125,7 +163,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
let de = tx.get_st(&ns, &db, &sc, &tk).await?;
let cf = config(de.kind, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success
debug!("Authenticated to scope `{}` with token `{}`", sc, tk);
// Set the session
@ -134,7 +172,11 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.au = Arc::new(Auth::Sc(ns, db, sc));
session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
Ok(())
}
// Check if this is scope authentication
@ -155,7 +197,7 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
let de = tx.get_sc(&ns, &db, &sc).await?;
let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success
debug!("Authenticated to scope `{}`", sc);
// Set the session
@ -163,8 +205,12 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.sc = Some(sc.to_owned());
session.sd = Some(Value::from(id));
session.au = Arc::new(Auth::Sc(ns, db, sc));
session.sd = Some(Value::from(id.to_owned()));
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
Default::default(),
Level::Scope(ns, db, sc),
)));
Ok(())
}
// Check if this is database token authentication
@ -182,14 +228,30 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
let de = tx.get_dt(&ns, &db, &tk).await?;
let cf = config(de.kind, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
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 token `{}`", db, tk);
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.au = Arc::new(Auth::Db(ns, db));
session.au = Arc::new(Auth::new(Actor::new(
de.name.to_string(),
roles,
Level::Database(ns, db),
)));
Ok(())
}
// Check if this is database authentication
@ -200,21 +262,25 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
..
} => {
// Log the decoded authentication claims
trace!("Authenticating to database `{}` with login `{}`", db, id);
trace!("Authenticating to database `{}` with user `{}`", db, id);
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Get the database login
let de = tx.get_dl(&ns, &db, &id).await?;
// Get the database user
let de = tx.get_db_user(&ns, &db, &id).await?;
let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success
debug!("Authenticated to database `{}` with login `{}`", db, id);
debug!("Authenticated to database `{}` with user `{}`", db, id);
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.db = Some(db.to_owned());
session.au = Arc::new(Auth::Db(ns, db));
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
de.roles.iter().map(|r| r.into()).collect(),
Level::Database(ns, db),
)));
Ok(())
}
// Check if this is namespace token authentication
@ -231,13 +297,26 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
let de = tx.get_nt(&ns, &tk).await?;
let cf = config(de.kind, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
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
trace!("Authenticated to namespace `{}` with token `{}`", ns, tk);
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.au = Arc::new(Auth::Ns(ns));
session.au =
Arc::new(Auth::new(Actor::new(de.name.to_string(), roles, Level::Namespace(ns))));
Ok(())
}
// Check if this is namespace authentication
@ -247,23 +326,752 @@ pub async fn token(kvs: &Datastore, session: &mut Session, auth: String) -> Resu
..
} => {
// Log the decoded authentication claims
trace!("Authenticating to namespace `{}` with login `{}`", ns, id);
trace!("Authenticating to namespace `{}` with user `{}`", ns, id);
// Create a new readonly transaction
let mut tx = kvs.transaction(false, false).await?;
// Get the namespace login
let de = tx.get_nl(&ns, &id).await?;
// Get the namespace user
let de = tx.get_ns_user(&ns, &id).await?;
let cf = config(Algorithm::Hs512, de.code)?;
// Verify the token
decode::<Claims>(auth, &cf.0, &cf.1)?;
decode::<Claims>(token, &cf.0, &cf.1)?;
// Log the success
trace!("Authenticated to namespace `{}` with login `{}`", ns, id);
trace!("Authenticated to namespace `{}` with user `{}`", ns, id);
// Set the session
session.tk = Some(value);
session.ns = Some(ns.to_owned());
session.au = Arc::new(Auth::Ns(ns));
session.au = Arc::new(Auth::new(Actor::new(
id.to_string(),
de.roles.iter().map(|r| r.into()).collect(),
Level::Namespace(ns),
)));
Ok(())
}
// There was an auth error
_ => Err(Error::InvalidAuth),
}
}
pub fn parse(value: &str) -> Result<Value, Error> {
// Extract the middle part of the token
let value = value.splitn(3, '.').skip(1).take(1).next().ok_or(Error::InvalidAuth)?;
// Decode the base64 token data content
let value = BASE64.decode(value).map_err(|_| Error::InvalidAuth)?;
// Convert the decoded data to a string
let value = str::from_utf8(&value).map_err(|_| Error::InvalidAuth)?;
// Parse the token data into SurrealQL
json(value).map_err(|_| Error::InvalidAuth)
}
pub async fn verify_creds(
ds: &Datastore,
ns: Option<&String>,
db: Option<&String>,
user: &str,
pass: &str,
) -> Result<(Auth, DefineUserStatement), Error> {
if user.is_empty() || pass.is_empty() {
return Err(Error::InvalidAuth);
}
// TODO(sgirones): Keep the same behaviour as before, where it would try to authenticate as a KV first, then NS and then DB.
// In the future, we want the client to specify the type of user it wants to authenticate as, so we can remove this chain.
// Try to authenticate as a ROOT user
match verify_root_creds(ds, user, pass).await {
Ok(u) => Ok(((&u, Level::Root).into(), u)),
Err(_) => {
// Try to authenticate as a NS user
match ns {
Some(ns) => {
match verify_ns_creds(ds, ns, user, pass).await {
Ok(u) => Ok(((&u, Level::Namespace(ns.to_owned())).into(), u)),
Err(_) => {
// Try to authenticate as a DB user
match db {
Some(db) => match verify_db_creds(ds, ns, db, user, pass).await {
Ok(u) => Ok((
(&u, Level::Database(ns.to_owned(), db.to_owned())).into(),
u,
)),
Err(_) => Err(Error::InvalidAuth),
},
None => Err(Error::InvalidAuth),
}
}
}
}
None => Err(Error::InvalidAuth),
}
}
}
}
async fn verify_root_creds(
ds: &Datastore,
user: &str,
pass: &str,
) -> Result<DefineUserStatement, Error> {
let mut tx = ds.transaction(false, false).await?;
let user_res = tx.get_root_user(user).await?;
verify_pass(pass, user_res.hash.as_ref())?;
Ok(user_res)
}
async fn verify_ns_creds(
ds: &Datastore,
ns: &str,
user: &str,
pass: &str,
) -> Result<DefineUserStatement, Error> {
let mut tx = ds.transaction(false, false).await?;
let user_res = match tx.get_ns_user(ns, user).await {
Ok(u) => Ok(u),
// TODO(sgirones): Remove this fallback once we remove LOGIN from the system completely. We are backwards compatible with LOGIN for now.
// If the USER resource is not found in the namespace, try to find the LOGIN resource
Err(error::Db::UserNsNotFound {
ns: _,
value: _,
}) => match tx.get_nl(ns, user).await {
Ok(u) => Ok(DefineUserStatement {
base: u.base,
name: u.name,
hash: u.hash,
code: u.code,
roles: vec![Role::Editor.into()],
}),
Err(e) => Err(e),
},
Err(e) => Err(e),
}?;
verify_pass(pass, user_res.hash.as_ref())?;
Ok(user_res)
}
async fn verify_db_creds(
ds: &Datastore,
ns: &str,
db: &str,
user: &str,
pass: &str,
) -> Result<DefineUserStatement, Error> {
let mut tx = ds.transaction(false, false).await?;
let user_res = match tx.get_db_user(ns, db, user).await {
Ok(u) => Ok(u),
// TODO(sgirones): Remove this fallback once we remove LOGIN from the system completely. We are backwards compatible with LOGIN for now.
// If the USER resource is not found in the database, try to find the LOGIN resource
Err(error::Db::UserDbNotFound {
ns: _,
db: _,
value: _,
}) => match tx.get_dl(ns, db, user).await {
Ok(u) => Ok(DefineUserStatement {
base: u.base,
name: u.name,
hash: u.hash,
code: u.code,
roles: vec![Role::Editor.into()],
}),
Err(e) => Err(e),
},
Err(e) => Err(e),
}?;
verify_pass(pass, user_res.hash.as_ref())?;
Ok(user_res)
}
fn verify_pass(pass: &str, hash: &str) -> Result<(), Error> {
// Compute the hash and verify the password
let hash = PasswordHash::new(hash).unwrap();
// Attempt to verify the password using Argon2
match Argon2::default().verify_password(pass.as_ref(), &hash) {
Ok(_) => Ok(()),
// The password did not verify
_ => Err(Error::InvalidAuth),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{iam::token::HEADER, kvs::Datastore};
use argon2::password_hash::{PasswordHasher, SaltString};
use chrono::Duration;
use jsonwebtoken::{encode, EncodingKey};
#[tokio::test]
async fn test_basic_root() {
//
// Test without roles 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'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, None);
assert_eq!(sess.db, None);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_root());
assert_eq!(sess.au.level().ns(), None);
assert_eq!(sess.au.level().db(), None);
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");
}
//
// Test with roles 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();
let mut sess = Session {
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, None);
assert_eq!(sess.db, None);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_root());
assert_eq!(sess.au.level().ns(), None);
assert_eq!(sess.au.level().db(), None);
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");
}
// Test invalid password
{
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'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "invalid").await;
assert!(res.is_err(), "Unexpect successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_basic_ns() {
//
// Test without roles 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'", &sess, None).await.unwrap();
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, None);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), None);
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");
}
//
// Test with roles 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();
let mut sess = Session {
ns: Some("test".to_string()),
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, None);
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), None);
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");
}
// Test invalid password
{
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'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "invalid").await;
assert!(res.is_err(), "Unexpect successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_basic_db() {
//
// Test without roles 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'", &sess, None).await.unwrap();
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
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");
}
//
// Test with roles 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();
let mut sess = Session {
ns: Some("test".to_string()),
db: Some("test".to_string()),
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "pass").await;
assert!(res.is_ok(), "Failed to signin with ROOT user: {:?}", res);
assert_eq!(sess.ns, Some("test".to_string()));
assert_eq!(sess.db, Some("test".to_string()));
assert_eq!(sess.au.id(), "user");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
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");
}
// Test invalid password
{
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'", &sess, None).await.unwrap();
let mut sess = Session {
..Default::default()
};
let res = basic(&ds, &mut sess, "user", "invalid").await;
assert!(res.is_err(), "Unexpect successful signin: {:?}", res);
}
}
#[tokio::test]
async fn test_token_ns() {
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()),
tk: Some("token".to_string()),
ns: Some("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!("DEFINE TOKEN token ON NS TYPE HS512 VALUE '{secret}'").as_str(),
&sess,
None,
)
.await
.unwrap();
//
// Test without roles defined
//
{
// 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, None);
assert_eq!(sess.au.id(), "token");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
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");
}
//
// Test with roles defined
//
{
// Prepare the claims object
let mut claims = claims.clone();
claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]);
// 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, None);
assert_eq!(sess.au.id(), "token");
assert!(sess.au.is_ns());
assert_eq!(sess.au.level().ns(), Some("test"));
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");
}
//
// Test with invalid token
//
{
// Prepare the claims object
let claims = claims.clone();
// Create the token
let key = EncodingKey::from_secret("invalid".as_ref());
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_err(), "Unexpected success signing in with token: {:?}", res);
}
//
// Test with valid token invalid ns
//
{
// Prepare the claims object
let mut claims = claims.clone();
claims.ns = Some("invalid".to_string());
// 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_err(), "Unexpected success signing in with token: {:?}", res);
}
}
#[tokio::test]
async fn test_token_db() {
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()),
tk: Some("token".to_string()),
ns: Some("test".to_string()),
db: Some("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!("DEFINE TOKEN token ON DB TYPE HS512 VALUE '{secret}'").as_str(),
&sess,
None,
)
.await
.unwrap();
//
// Test without roles defined
//
{
// 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.au.id(), "token");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
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");
}
//
// Test with roles defined
//
{
// Prepare the claims object
let mut claims = claims.clone();
claims.roles = Some(vec!["editor".to_string(), "owner".to_string()]);
// 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.au.id(), "token");
assert!(sess.au.is_db());
assert_eq!(sess.au.level().ns(), Some("test"));
assert_eq!(sess.au.level().db(), Some("test"));
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");
}
//
// Test with invalid token
//
{
// Prepare the claims object
let claims = claims.clone();
// Create the token
let key = EncodingKey::from_secret("invalid".as_ref());
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_err(), "Unexpected success signing in with token: {:?}", res);
}
//
// Test with valid token invalid db
//
{
// Prepare the claims object
let mut claims = claims.clone();
claims.db = Some("invalid".to_string());
// 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_err(), "Unexpected success signing in with token: {:?}", res);
}
}
#[test]
fn test_verify_pass() {
let salt = SaltString::generate(&mut rand::thread_rng());
let hash = Argon2::default().hash_password("test".as_bytes(), &salt).unwrap().to_string();
// Verify with the matching password
assert!(verify_pass("test", &hash).is_ok());
// Verify with a non matching password
assert!(verify_pass("nonmatching", &hash).is_err());
}
#[tokio::test]
async fn test_verify_creds_invalid() {
let ds = Datastore::new("memory").await.unwrap();
let ns = "N".to_string();
let db = "D".to_string();
// Reject empty username or password
{
assert!(verify_creds(&ds, None, None, "", "").await.is_err());
assert!(verify_creds(&ds, None, None, "test", "").await.is_err());
assert!(verify_creds(&ds, None, None, "", "test").await.is_err());
}
// Reject invalid KV credentials
{
assert!(verify_creds(&ds, None, None, "test", "test").await.is_err());
}
// Reject invalid NS credentials
{
assert!(verify_creds(&ds, Some(&ns), None, "test", "test").await.is_err());
}
// Reject invalid DB credentials
{
assert!(verify_creds(&ds, Some(&ns), Some(&db), "test", "test").await.is_err());
}
}
#[tokio::test]
async fn test_verify_creds_valid() {
let ds = Datastore::new("memory").await.unwrap();
let ns = "N".to_string();
let db = "D".to_string();
// Define users
{
let sess = Session::owner();
let sql = "DEFINE USER kv ON ROOT PASSWORD 'kv'";
ds.execute(sql, &sess, None).await.unwrap();
let sql = "USE NS N; DEFINE USER ns ON NS PASSWORD 'ns'";
ds.execute(sql, &sess, None).await.unwrap();
let sql = "USE NS N DB D; DEFINE USER db ON DB PASSWORD 'db'";
ds.execute(sql, &sess, None).await.unwrap();
}
// Accept KV user
{
let res = verify_creds(&ds, None, None, "kv", "kv").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Root);
assert_eq!(auth.id(), "kv");
}
// Accept NS user
{
let res = verify_creds(&ds, Some(&ns), None, "ns", "ns").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Namespace(ns.to_owned()));
assert_eq!(auth.id(), "ns");
}
// Accept DB user
{
let res = verify_creds(&ds, Some(&ns), Some(&db), "db", "db").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Database(ns.to_owned(), db.to_owned()));
assert_eq!(auth.id(), "db");
}
}
#[tokio::test]
async fn test_verify_creds_chain() {
let ds = Datastore::new("memory").await.unwrap();
let ns = "N".to_string();
let db = "D".to_string();
// Define users
{
let sess = Session::owner();
let sql = "DEFINE USER kv ON ROOT PASSWORD 'kv'";
ds.execute(sql, &sess, None).await.unwrap();
let sql = "USE NS N; DEFINE USER ns ON NS PASSWORD 'ns'";
ds.execute(sql, &sess, None).await.unwrap();
let sql = "USE NS N DB D; DEFINE USER db ON DB PASSWORD 'db'";
ds.execute(sql, &sess, None).await.unwrap();
}
// Accept KV user even with NS and DB defined
{
let res = verify_creds(&ds, Some(&ns), Some(&db), "kv", "kv").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Root);
assert_eq!(auth.id(), "kv");
}
// Accept NS user even with DB defined
{
let res = verify_creds(&ds, Some(&ns), Some(&db), "ns", "ns").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Namespace(ns.to_owned()));
assert_eq!(auth.id(), "ns");
}
// Accept DB user
{
let res = verify_creds(&ds, Some(&ns), Some(&db), "db", "db").await;
assert!(res.is_ok());
let (auth, _) = res.unwrap();
assert_eq!(auth.level(), &Level::Database(ns.to_owned(), db.to_owned()));
assert_eq!(auth.id(), "db");
}
}
}

View file

@ -7,4 +7,5 @@ pub mod sc;
pub mod tb;
pub mod tk;
pub mod ts;
pub mod us;
pub mod vs;

View file

@ -1,4 +1,4 @@
/// Stores a DEFINE LOGIN ON DATABASE config definition
/// Stores a DEFINE TOKEN ON DATABASE config definition
use derive::Key;
use serde::{Deserialize, Serialize};
@ -15,8 +15,8 @@ pub struct Tk<'a> {
pub tk: &'a str,
}
pub fn new<'a>(ns: &'a str, db: &'a str, tb: &'a str) -> Tk<'a> {
Tk::new(ns, db, tb)
pub fn new<'a>(ns: &'a str, db: &'a str, tk: &'a str) -> Tk<'a> {
Tk::new(ns, db, tk)
}
pub fn prefix(ns: &str, db: &str) -> Vec<u8> {

View file

@ -0,0 +1,77 @@
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
pub struct Us<'a> {
__: u8,
_a: u8,
pub ns: &'a str,
_b: u8,
pub db: &'a str,
_c: u8,
_d: u8,
_e: u8,
pub user: &'a str,
}
pub fn new<'a>(ns: &'a str, db: &'a str, user: &'a str) -> Us<'a> {
Us::new(ns, db, user)
}
pub fn prefix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0x00]);
k
}
pub fn suffix(ns: &str, db: &str) -> Vec<u8> {
let mut k = super::all::new(ns, db).encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0xff]);
k
}
impl<'a> Us<'a> {
pub fn new(ns: &'a str, db: &'a str, user: &'a str) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'*',
db,
_c: b'!',
_d: b'u',
_e: b's',
user,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Us::new(
"testns",
"testdb",
"testuser",
);
let enc = Us::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\x00*testdb\x00!ustestuser\x00");
let dec = Us::decode(&enc).unwrap();
assert_eq!(val, dec);
}
#[test]
fn test_prefix() {
let val = super::prefix("testns", "testdb");
assert_eq!(val, b"/*testns\0*testdb\0!us\0");
}
#[test]
fn test_suffix() {
let val = super::suffix("testns", "testdb");
assert_eq!(val, b"/*testns\0*testdb\0!us\xff");
}
}

View file

@ -2,3 +2,4 @@ pub mod all;
pub mod db;
pub mod lg;
pub mod tk;
pub mod us;

View file

@ -0,0 +1,72 @@
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
pub struct Us<'a> {
__: u8,
_a: u8,
pub ns: &'a str,
_b: u8,
_c: u8,
_d: u8,
pub user: &'a str,
}
pub fn new<'a>(ns: &'a str, user: &'a str) -> Us<'a> {
Us::new(ns, user)
}
pub fn prefix(ns: &str) -> Vec<u8> {
let mut k = super::all::new(ns).encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0x00]);
k
}
pub fn suffix(ns: &str) -> Vec<u8> {
let mut k = super::all::new(ns).encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0xff]);
k
}
impl<'a> Us<'a> {
pub fn new(ns: &'a str, user: &'a str) -> Self {
Self {
__: b'/',
_a: b'*',
ns,
_b: b'!',
_c: b'u',
_d: b's',
user,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Us::new(
"testns",
"testuser",
);
let enc = Us::encode(&val).unwrap();
assert_eq!(enc, b"/*testns\x00!ustestuser\x00");
let dec = Us::decode(&enc).unwrap();
assert_eq!(val, dec);
}
#[test]
fn test_prefix() {
let val = super::prefix("testns");
assert_eq!(val, b"/*testns\0!us\0");
}
#[test]
fn test_suffix() {
let val = super::suffix("testns");
assert_eq!(val, b"/*testns\0!us\xff");
}
}

View file

@ -2,3 +2,4 @@ pub mod all;
pub mod hb;
pub mod nd;
pub mod ns;
pub mod us;

65
lib/src/key/root/us.rs Normal file
View file

@ -0,0 +1,65 @@
use derive::Key;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Serialize, Deserialize, Key)]
pub struct Us<'a> {
__: u8,
_a: u8,
_b: u8,
_c: u8,
pub user: &'a str,
}
pub fn new(user: &str) -> Us<'_> {
Us::new(user)
}
pub fn prefix() -> Vec<u8> {
let mut k = super::all::new().encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0x00]);
k
}
pub fn suffix() -> Vec<u8> {
let mut k = super::all::new().encode().unwrap();
k.extend_from_slice(&[b'!', b'u', b's', 0xff]);
k
}
impl<'a> Us<'a> {
pub fn new(user: &'a str) -> Self {
Self {
__: b'/',
_a: b'!',
_b: b'u',
_c: b's',
user,
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn key() {
use super::*;
#[rustfmt::skip]
let val = Us::new("testuser");
let enc = Us::encode(&val).unwrap();
assert_eq!(enc, b"/!ustestuser\x00");
let dec = Us::decode(&enc).unwrap();
assert_eq!(val, dec);
}
#[test]
fn test_prefix() {
let val = super::prefix();
assert_eq!(val, b"/!us\0");
}
#[test]
fn test_suffix() {
let val = super::suffix();
assert_eq!(val, b"/!us\xff");
}
}

View file

@ -10,7 +10,10 @@ use crate::dbs::Response;
use crate::dbs::Session;
use crate::dbs::Variables;
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::key::root::hb::Hb;
use crate::opt::auth::Root;
use crate::sql;
use crate::sql::Value;
use crate::sql::{Query, Uuid};
@ -18,6 +21,7 @@ use crate::vs;
use channel::Receiver;
use channel::Sender;
use futures::lock::Mutex;
use futures::Future;
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
@ -53,6 +57,8 @@ pub struct Datastore {
vso: Arc<Mutex<vs::Oracle>>,
// Whether this datastore enables live query notifications to subscribers
notification_channel: Option<(Sender<Notification>, Receiver<Notification>)>,
// Whether this datastore authentication is enabled. When disabled, anonymous actors have owner-level access.
auth_enabled: bool,
}
#[allow(clippy::large_enum_variant)]
@ -250,6 +256,7 @@ impl Datastore {
transaction_timeout: None,
vso: Arc::new(Mutex::new(vs::Oracle::systime_counter())),
notification_channel: None,
auth_enabled: false,
})
}
@ -271,6 +278,43 @@ impl Datastore {
self
}
/// Enabled authentication for this Datastore?
pub fn with_auth_enabled(mut self, enabled: bool) -> Self {
self.auth_enabled = enabled;
self
}
pub fn is_auth_enabled(&self) -> bool {
self.auth_enabled
}
/// Setup the initial credentials
pub async fn setup_initial_creds(&self, creds: Root<'_>) -> Result<(), Error> {
let mut txn = self.transaction(false, false).await?;
match txn.all_root_users().await {
Ok(val) if val.is_empty() => {
info!(
"Initial credentials were provided and no existing root-level users were found: create the initial user '{}'.", creds.username
);
let sql = format!(
"DEFINE USER {user} ON ROOT PASSWORD '{pass}' ROLES OWNER",
user = creds.username,
pass = creds.password
);
let sess = Session::owner();
self.execute(&sql, &sess, None).await?;
Ok(())
}
Ok(_) => {
warn!("Initial credentials were provided but existing root-level users were found. Skip the initial user creation.");
warn!("Consider removing the --user/--pass arguments from the server start.");
Ok(())
}
Err(e) => Err(e),
}
}
/// Set a global transaction timeout for this Datastore
pub fn with_transaction_timeout(mut self, duration: Option<Duration>) -> Self {
self.transaction_timeout = duration;
@ -601,7 +645,7 @@ impl Datastore {
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("memory").await?;
/// let ses = Session::for_kv();
/// let ses = Session::owner();
/// let ast = "USE NS test DB test; SELECT * FROM person;";
/// let res = ds.execute(ast, &ses, None).await?;
/// Ok(())
@ -631,7 +675,7 @@ impl Datastore {
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("memory").await?;
/// let ses = Session::for_kv();
/// let ses = Session::owner();
/// let ast = parse("USE NS test DB test; SELECT * FROM person;")?;
/// let res = ds.process(ast, &ses, None).await?;
/// Ok(())
@ -651,6 +695,7 @@ impl Datastore {
.with_db(sess.db())
.with_live(sess.live())
.with_auth(sess.au.clone())
.with_auth_enabled(self.auth_enabled)
.with_strict(self.strict);
// Create a new query executor
let mut exe = Executor::new(self);
@ -684,7 +729,7 @@ impl Datastore {
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("memory").await?;
/// let ses = Session::for_kv();
/// let ses = Session::owner();
/// let val = Value::Future(Box::new(Future::from(Value::Bool(true))));
/// let res = ds.compute(val, &ses, None).await?;
/// Ok(())
@ -704,6 +749,7 @@ impl Datastore {
.with_db(sess.db())
.with_live(sess.live())
.with_auth(sess.au.clone())
.with_auth_enabled(self.auth_enabled)
.with_strict(self.strict);
// Start a new transaction
let txn = self.transaction(val.writeable(), false).await?;
@ -744,7 +790,7 @@ impl Datastore {
/// #[tokio::main]
/// async fn main() -> Result<(), Error> {
/// let ds = Datastore::new("memory").await?.with_notifications();
/// let ses = Session::for_kv();
/// let ses = Session::owner();
/// if let Some(channel) = ds.notifications() {
/// while let Ok(v) = channel.recv().await {
/// println!("Received notification: {v}");
@ -759,13 +805,45 @@ impl Datastore {
}
/// Performs a full database export as SQL
#[instrument(skip(self, chn))]
pub async fn export(&self, ns: String, db: String, chn: Sender<Vec<u8>>) -> Result<(), Error> {
// Start a new transaction
#[instrument(skip(self, sess, chn))]
pub async fn prepare_export(
&self,
sess: &Session,
ns: String,
db: String,
chn: Sender<Vec<u8>>,
) -> Result<impl Future<Output = Result<(), Error>>, Error> {
let mut txn = self.transaction(false, false).await?;
// Process the export
txn.export(&ns, &db, chn).await?;
// Everything ok
Ok(())
// Skip auth for Anonymous users if auth is disabled
let skip_auth = !self.is_auth_enabled() && sess.au.is_anon();
if !skip_auth {
sess.au.is_allowed(Action::View, &ResourceKind::Any.on_db(&ns, &db))?;
}
Ok(async move {
// Start a new transaction
// Process the export
let ns = ns.to_owned();
let db = db.to_owned();
txn.export(&ns, &db, chn).await?;
// Everything ok
Ok(())
})
}
/// Performs a database import from SQL
#[instrument(skip(self, sess, sql))]
pub async fn import(&self, sql: &str, sess: &Session) -> Result<Vec<Response>, Error> {
// Skip auth for Anonymous users if auth is disabled
let skip_auth = !self.is_auth_enabled() && sess.au.is_anon();
if !skip_auth {
sess.au.is_allowed(
Action::Edit,
&ResourceKind::Any.on_level(sess.au.level().to_owned()),
)?;
}
self.execute(sql, sess, None).await
}
}

View file

@ -67,7 +67,7 @@ async fn expired_nodes_get_live_queries_archived() {
test.bootstrap_at_time(old_node.clone(), old_time.clone()).await.unwrap();
// Set up live query
let ses = Session::for_kv()
let ses = Session::owner()
.with_ns(test.test_str("testns").as_str())
.with_db(test.test_str("testdb").as_str());
let table = "my_table";

View file

@ -13,6 +13,7 @@ use crate::sql;
use crate::sql::paths::EDGE;
use crate::sql::paths::IN;
use crate::sql::paths::OUT;
use crate::sql::statements::DefineUserStatement;
use crate::sql::thing::Thing;
use crate::sql::Strand;
use crate::sql::Value;
@ -1236,6 +1237,15 @@ impl Transaction {
})
}
/// Retrieve all namespace user definitions for a specific namespace.
pub async fn all_ns_users(&mut self, ns: &str) -> Result<Arc<[DefineUserStatement]>, Error> {
let beg = crate::key::namespace::us::prefix(ns);
let end = crate::key::namespace::us::suffix(ns);
let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into();
Ok(val)
}
/// Retrieve all namespace token definitions for a specific namespace.
pub async fn all_nt(&mut self, ns: &str) -> Result<Arc<[DefineTokenStatement]>, Error> {
let key = crate::key::namespace::tk::prefix(ns);
@ -1297,6 +1307,19 @@ impl Transaction {
})
}
/// Retrieve all database user definitions for a specific database.
pub async fn all_db_users(
&mut self,
ns: &str,
db: &str,
) -> Result<Arc<[DefineUserStatement]>, Error> {
let beg = crate::key::database::us::prefix(ns, db);
let end = crate::key::database::us::suffix(ns, db);
let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into();
Ok(val)
}
/// Retrieve all database token definitions for a specific database.
pub async fn all_dt(
&mut self,
@ -1390,7 +1413,7 @@ impl Transaction {
})
}
/// Retrieve all scope definitions for a specific database.
/// Retrieve all param definitions for a specific database.
pub async fn all_pa(
&mut self,
ns: &str,
@ -1602,6 +1625,16 @@ impl Transaction {
})
}
/// Retrieve all ROOT users.
pub async fn all_root_users(&mut self) -> Result<Arc<[DefineUserStatement]>, Error> {
let beg = crate::key::root::us::prefix();
let end = crate::key::root::us::suffix();
let val = self.getr(beg..end, u32::MAX).await?;
let val = val.convert().into();
Ok(val)
}
/// Retrieve a specific namespace definition.
pub async fn get_ns(&mut self, ns: &str) -> Result<DefineNamespaceStatement, Error> {
let key = crate::key::root::ns::new(ns);
@ -1794,6 +1827,46 @@ impl Transaction {
})?;
Ok(val.into())
}
/// Retrieve a specific user definition from ROOT.
pub async fn get_root_user(&mut self, user: &str) -> Result<DefineUserStatement, Error> {
let key = crate::key::root::us::new(user);
let val = self.get(key).await?.ok_or(Error::UserRootNotFound {
value: user.to_owned(),
})?;
Ok(val.into())
}
/// Retrieve a specific user definition from a namespace.
pub async fn get_ns_user(
&mut self,
ns: &str,
user: &str,
) -> Result<DefineUserStatement, Error> {
let key = crate::key::namespace::us::new(ns, user);
let val = self.get(key).await?.ok_or(Error::UserNsNotFound {
value: user.to_owned(),
ns: ns.to_owned(),
})?;
Ok(val.into())
}
/// Retrieve a specific user definition from a database.
pub async fn get_db_user(
&mut self,
ns: &str,
db: &str,
user: &str,
) -> Result<DefineUserStatement, Error> {
let key = crate::key::database::us::new(ns, db, user);
let val = self.get(key).await?.ok_or(Error::UserDbNotFound {
value: user.to_owned(),
ns: ns.to_owned(),
db: db.to_owned(),
})?;
Ok(val.into())
}
/// Add a namespace with a default configuration, only if we are in dynamic mode.
pub async fn add_ns(
&mut self,
@ -2418,3 +2491,161 @@ impl Transaction {
Ok(None)
}
}
#[cfg(test)]
#[cfg(feature = "kv-mem")]
mod tests {
use crate::{
kvs::Datastore,
sql::{statements::DefineUserStatement, Base},
};
#[tokio::test]
async fn test_get_root_user() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// Retrieve non-existent KV user
let res = txn.get_root_user("nonexistent").await;
assert_eq!(res.err().unwrap().to_string(), "The root user 'nonexistent' does not exist");
// Create KV user and retrieve it
let data = DefineUserStatement {
name: "user".into(),
base: Base::Root,
..Default::default()
};
let key = crate::key::root::us::new("user");
let _ = txn.set(key, data.to_owned()).await.unwrap();
let res = txn.get_root_user("user").await.unwrap();
assert_eq!(res, data);
}
#[tokio::test]
async fn test_get_ns_user() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// Retrieve non-existent NS user
let res = txn.get_ns_user("ns", "nonexistent").await;
assert_eq!(
res.err().unwrap().to_string(),
"The user 'nonexistent' does not exist in the namespace 'ns'"
);
// Create NS user and retrieve it
let data = DefineUserStatement {
name: "user".into(),
base: Base::Ns,
..Default::default()
};
let key = crate::key::namespace::us::new("ns", "user");
let _ = txn.set(key, data.to_owned()).await.unwrap();
let res = txn.get_ns_user("ns", "user").await.unwrap();
assert_eq!(res, data);
}
#[tokio::test]
async fn test_get_db_user() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// Retrieve non-existent DB user
let res = txn.get_db_user("ns", "db", "nonexistent").await;
assert_eq!(
res.err().unwrap().to_string(),
"The user 'nonexistent' does not exist in the database 'db'"
);
// Create DB user and retrieve it
let data = DefineUserStatement {
name: "user".into(),
base: Base::Db,
..Default::default()
};
let key = crate::key::database::us::new("ns", "db", "user");
let _ = txn.set(key, data.to_owned()).await.unwrap();
let res = txn.get_db_user("ns", "db", "user").await.unwrap();
assert_eq!(res, data);
}
#[tokio::test]
async fn test_all_root_users() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// When there are no users
let res = txn.all_root_users().await.unwrap();
assert_eq!(res.len(), 0);
// When there are users
let data = DefineUserStatement {
name: "user".into(),
base: Base::Root,
..Default::default()
};
let key1 = crate::key::root::us::new("user1");
let key2 = crate::key::root::us::new("user2");
let _ = txn.set(key1, data.to_owned()).await.unwrap();
let _ = txn.set(key2, data.to_owned()).await.unwrap();
let res = txn.all_root_users().await.unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0], data);
}
#[tokio::test]
async fn test_all_ns_users() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// When there are no users
let res = txn.all_ns_users("ns").await.unwrap();
assert_eq!(res.len(), 0);
// When there are users
let data = DefineUserStatement {
name: "user".into(),
base: Base::Ns,
..Default::default()
};
let key1 = crate::key::namespace::us::new("ns", "user1");
let key2 = crate::key::namespace::us::new("ns", "user2");
let _ = txn.set(key1, data.to_owned()).await.unwrap();
let _ = txn.set(key2, data.to_owned()).await.unwrap();
let res = txn.all_ns_users("ns").await.unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0], data);
}
#[tokio::test]
async fn test_db_users() {
let ds = Datastore::new("memory").await.unwrap();
let mut txn = ds.transaction(true, false).await.unwrap();
// When there are no users
let res = txn.all_db_users("ns", "db").await.unwrap();
assert_eq!(res.len(), 0);
// When there are users
let data = DefineUserStatement {
name: "user".into(),
base: Base::Db,
..Default::default()
};
let key1 = crate::key::database::us::new("ns", "db", "user1");
let key2 = crate::key::database::us::new("ns", "db", "user2");
let _ = txn.set(key1, data.to_owned()).await.unwrap();
let _ = txn.set(key2, data.to_owned()).await.unwrap();
let res = txn.all_db_users("ns", "db").await.unwrap();
assert_eq!(res.len(), 2);
assert_eq!(res[0], data);
}
}

View file

@ -124,14 +124,6 @@ pub mod dbs;
pub mod env;
#[doc(hidden)]
pub mod err;
#[cfg(any(
feature = "kv-mem",
feature = "kv-tikv",
feature = "kv-rocksdb",
feature = "kv-speedb",
feature = "kv-fdb",
feature = "kv-indxdb",
))]
#[doc(hidden)]
pub mod iam;
#[doc(hidden)]

View file

@ -9,7 +9,7 @@ use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub enum Base {
Kv,
Root,
Ns,
Db,
Sc(Ident),
@ -17,7 +17,7 @@ pub enum Base {
impl Default for Base {
fn default() -> Self {
Self::Kv
Self::Root
}
}
@ -27,7 +27,7 @@ impl fmt::Display for Base {
Self::Ns => f.write_str("NAMESPACE"),
Self::Db => f.write_str("DATABASE"),
Self::Sc(sc) => write!(f, "SCOPE {sc}"),
Self::Kv => f.write_str("KV"),
Self::Root => f.write_str("ROOT"),
}
}
}
@ -36,8 +36,10 @@ pub fn base(i: &str) -> IResult<&str, Base> {
alt((
map(tag_no_case("NAMESPACE"), |_| Base::Ns),
map(tag_no_case("DATABASE"), |_| Base::Db),
map(tag_no_case("ROOT"), |_| Base::Root),
map(tag_no_case("NS"), |_| Base::Ns),
map(tag_no_case("DB"), |_| Base::Db),
map(tag_no_case("KV"), |_| Base::Root),
))(i)
}
@ -45,8 +47,10 @@ pub fn base_or_scope(i: &str) -> IResult<&str, Base> {
alt((
map(tag_no_case("NAMESPACE"), |_| Base::Ns),
map(tag_no_case("DATABASE"), |_| Base::Db),
map(tag_no_case("ROOT"), |_| Base::Root),
map(tag_no_case("NS"), |_| Base::Ns),
map(tag_no_case("DB"), |_| Base::Db),
map(tag_no_case("KV"), |_| Base::Root),
|i| {
let (i, _) = tag_no_case("SCOPE")(i)?;
let (i, _) = shouldbespace(i)?;

View file

@ -10,6 +10,7 @@ pub enum Error<I> {
Split(I, String),
Order(I, String),
Group(I, String),
Role(I, String),
}
pub type IResult<I, O, E = Error<I>> = Result<(I, O), Err<E>>;

View file

@ -1,5 +1,6 @@
use crate::err::Error;
use crate::sql::error::Error::{Field, Group, Order, Parser, Split};
use crate::iam::Error as IamError;
use crate::sql::error::Error::{Field, Group, Order, Parser, Role, Split};
use crate::sql::error::IResult;
use crate::sql::query::{query, Query};
use crate::sql::subquery::{subquery, Subquery};
@ -83,6 +84,8 @@ fn parse_impl<O>(input: &str, parser: impl Fn(&str) -> IResult<&str, O>) -> Resu
line: locate(input, e).1,
field: f,
},
// There was an error parsing the ROLE
Role(_, role) => Error::IamError(IamError::InvalidRole(role)),
}),
_ => unreachable!(),
},

View file

@ -1,8 +1,9 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::idx::btree::store::BTreeStoreType;
use crate::idx::ft::FtIndex;
use crate::idx::IndexKeyBase;
@ -11,6 +12,7 @@ use crate::sql::error::IResult;
use crate::sql::ident::{ident, Ident};
use crate::sql::index::Index;
use crate::sql::value::Value;
use crate::sql::Base;
use derive::Store;
use nom::bytes::complete::tag_no_case;
use serde::{Deserialize, Serialize};
@ -33,10 +35,8 @@ impl AnalyzeStatement {
) -> Result<Value, Error> {
match self {
AnalyzeStatement::Idx(tb, idx) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::View, ResourceKind::Index, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Read the index

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -50,10 +49,8 @@ impl CreateStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,8 +1,11 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::iam::Role;
use crate::sql::algorithm::{algorithm, Algorithm};
use crate::sql::base::{base, base_or_scope, Base};
use crate::sql::block::{block, Block};
@ -10,11 +13,13 @@ use crate::sql::changefeed::{changefeed, ChangeFeed};
use crate::sql::comment::{mightbespace, shouldbespace};
use crate::sql::common::commas;
use crate::sql::duration::{duration, Duration};
use crate::sql::error::Error as SqlError;
use crate::sql::error::IResult;
use crate::sql::escape::quote_str;
use crate::sql::filter::{filters, Filter};
use crate::sql::fmt::is_pretty;
use crate::sql::fmt::pretty_indent;
use crate::sql::fmt::Fmt;
use crate::sql::ident::{ident, Ident};
use crate::sql::idiom;
use crate::sql::idiom::{Idiom, Idioms};
@ -38,6 +43,7 @@ use nom::combinator::{map, opt};
use nom::multi::many0;
use nom::multi::separated_list0;
use nom::sequence::tuple;
use nom::Err::Failure;
use rand::distributions::Alphanumeric;
use rand::rngs::OsRng;
use rand::Rng;
@ -58,6 +64,7 @@ pub enum DefineStatement {
Event(DefineEventStatement),
Field(DefineFieldStatement),
Index(DefineIndexStatement),
User(DefineUserStatement),
}
impl DefineStatement {
@ -73,7 +80,9 @@ impl DefineStatement {
Self::Namespace(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Database(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Function(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Login(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Login(_) => Err(Error::Deprecated(
"DEFINE LOGIN is no longer supported. Use DEFINE USER instead".to_string(),
)),
Self::Token(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Scope(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Param(ref v) => v.compute(ctx, opt, txn, doc).await,
@ -82,6 +91,7 @@ impl DefineStatement {
Self::Field(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Index(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::Analyzer(ref v) => v.compute(ctx, opt, txn, doc).await,
Self::User(ref v) => v.compute(ctx, opt, txn, doc).await,
}
}
}
@ -93,6 +103,7 @@ impl Display for DefineStatement {
Self::Database(v) => Display::fmt(v, f),
Self::Function(v) => Display::fmt(v, f),
Self::Login(v) => Display::fmt(v, f),
Self::User(v) => Display::fmt(v, f),
Self::Token(v) => Display::fmt(v, f),
Self::Scope(v) => Display::fmt(v, f),
Self::Param(v) => Display::fmt(v, f),
@ -111,6 +122,7 @@ pub fn define(i: &str) -> IResult<&str, DefineStatement> {
map(database, DefineStatement::Database),
map(function, DefineStatement::Function),
map(login, DefineStatement::Login),
map(user, DefineStatement::User),
map(token, DefineStatement::Token),
map(scope, DefineStatement::Scope),
map(param, DefineStatement::Param),
@ -140,10 +152,8 @@ impl DefineNamespaceStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// No need for NS/DB
opt.needs(Level::Kv)?;
// Allowed to run?
opt.check(Level::Kv)?;
opt.is_allowed(Action::Edit, ResourceKind::Namespace, &Base::Root)?;
// Process the statement
let key = crate::key::root::ns::new(&self.name);
// Claim transaction
@ -193,10 +203,8 @@ impl DefineDatabaseStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected NS?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Ns)?;
opt.is_allowed(Action::Edit, ResourceKind::Database, &Base::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -274,10 +282,8 @@ impl DefineFunctionStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Function, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -356,10 +362,8 @@ impl DefineAnalyzerStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Analyzer, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -421,50 +425,6 @@ pub struct DefineLoginStatement {
pub code: String,
}
impl DefineLoginStatement {
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
match self.base {
Base::Ns => {
// Selected DB?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Kv)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::namespace::lg::new(opt.ns(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.set(key, self).await?;
// Ok all good
Ok(Value::None)
}
Base::Db => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::database::lg::new(opt.ns(), opt.db(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.set(key, self).await?;
// Ok all good
Ok(Value::None)
}
_ => unreachable!(),
}
}
}
impl Display for DefineLoginStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "DEFINE LOGIN {} ON {} PASSHASH {}", self.name, self.base, quote_str(&self.hash))
@ -533,6 +493,169 @@ fn login_hash(i: &str) -> IResult<&str, DefineLoginOption> {
// --------------------------------------------------
// --------------------------------------------------
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Store, Hash)]
pub struct DefineUserStatement {
pub name: Ident,
pub base: Base,
pub hash: String,
pub code: String,
pub roles: Vec<Ident>,
}
impl DefineUserStatement {
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match self.base {
Base::Root => {
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::root::us::new(&self.name);
run.set(key, self).await?;
// Ok all good
Ok(Value::None)
}
Base::Ns => {
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::namespace::us::new(opt.ns(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.set(key, self).await?;
// Ok all good
Ok(Value::None)
}
Base::Db => {
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::database::us::new(opt.ns(), opt.db(), &self.name);
run.add_ns(opt.ns(), opt.strict).await?;
run.add_db(opt.ns(), opt.db(), opt.strict).await?;
run.set(key, self).await?;
// Ok all good
Ok(Value::None)
}
// Other levels are not supported
_ => Err(Error::InvalidLevel(self.base.to_string())),
}
}
}
impl Display for DefineUserStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"DEFINE USER {} ON {} PASSHASH {} ROLES {}",
self.name,
self.base,
quote_str(&self.hash),
Fmt::comma_separated(
&self.roles.iter().map(|r| r.to_string().to_uppercase()).collect::<Vec<String>>()
)
)
}
}
fn user(i: &str) -> IResult<&str, DefineUserStatement> {
let (i, _) = tag_no_case("DEFINE")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("USER")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, name) = ident(i)?;
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("ON")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, base) = base(i)?;
let (i, opts) = user_opts(i)?;
let mut res = DefineUserStatement {
name,
base,
roles: vec!["Viewer".into()], // New users get the viewer role by default
code: rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(128)
.map(char::from)
.collect::<String>(),
..Default::default()
};
for opt in opts {
match opt {
DefineUserOption::Password(v) => {
res.hash = Argon2::default()
.hash_password(v.as_ref(), &SaltString::generate(&mut OsRng))
.unwrap()
.to_string()
}
DefineUserOption::Passhash(v) => {
res.hash = v;
}
DefineUserOption::Roles(v) => {
res.roles = v;
}
}
}
Ok((i, res))
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub enum DefineUserOption {
Password(String),
Passhash(String),
Roles(Vec<Ident>),
}
fn user_opts(i: &str) -> IResult<&str, Vec<DefineUserOption>> {
many0(alt((alt((user_pass, user_hash)), user_roles)))(i)
}
fn user_pass(i: &str) -> IResult<&str, DefineUserOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("PASSWORD")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, v) = strand_raw(i)?;
Ok((i, DefineUserOption::Password(v)))
}
fn user_hash(i: &str) -> IResult<&str, DefineUserOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("PASSHASH")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, v) = strand_raw(i)?;
Ok((i, DefineUserOption::Passhash(v)))
}
fn user_roles(i: &str) -> IResult<&str, DefineUserOption> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("ROLES")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, roles) = separated_list0(commas, |i| {
let (i, v) = ident(i)?;
// Verify the role is valid
Role::try_from(v.as_str()).map_err(|_| Failure(SqlError::Role(i, v.to_string())))?;
Ok((i, v))
})(i)?;
Ok((i, DefineUserOption::Roles(roles)))
}
// --------------------------------------------------
// --------------------------------------------------
// --------------------------------------------------
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Store, Hash)]
pub struct DefineTokenStatement {
pub name: Ident,
@ -550,12 +673,10 @@ impl DefineTokenStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match &self.base {
Base::Ns => {
// Selected DB?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Kv)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -566,10 +687,6 @@ impl DefineTokenStatement {
Ok(Value::None)
}
Base::Db => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -581,10 +698,6 @@ impl DefineTokenStatement {
Ok(Value::None)
}
Base::Sc(sc) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -596,7 +709,8 @@ impl DefineTokenStatement {
// Ok all good
Ok(Value::None)
}
_ => unreachable!(),
// Other levels are not supported
_ => Err(Error::InvalidLevel(self.base.to_string())),
}
}
}
@ -665,10 +779,8 @@ impl DefineScopeStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Scope, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -783,10 +895,8 @@ impl DefineParamStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Parameter, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -847,10 +957,8 @@ impl DefineTableStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Table, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -1050,10 +1158,8 @@ impl DefineEventStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Event, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -1134,10 +1240,8 @@ impl DefineFieldStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Field, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
@ -1294,10 +1398,8 @@ impl DefineIndexStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Index, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -50,10 +49,8 @@ impl DeleteStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,26 +1,32 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::sql::base::base;
use crate::sql::comment::shouldbespace;
use crate::sql::error::IResult;
use crate::sql::ident::{ident, Ident};
use crate::sql::object::Object;
use crate::sql::value::Value;
use crate::sql::Base;
use derive::Store;
use nom::branch::alt;
use nom::bytes::complete::tag_no_case;
use nom::combinator::opt;
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Store, Hash)]
pub enum InfoStatement {
Kv,
Root,
Ns,
Db,
Sc(Ident),
Tb(Ident),
User(Ident, Option<Base>),
}
impl InfoStatement {
@ -34,29 +40,31 @@ impl InfoStatement {
) -> Result<Value, Error> {
// Allowed to run?
match self {
InfoStatement::Kv => {
// No need for NS/DB
opt.needs(Level::Kv)?;
InfoStatement::Root => {
// Allowed to run?
opt.check(Level::Kv)?;
// Create the result set
let mut res = Object::default();
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Root)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
// Create the result set
let mut res = Object::default();
// Process the namespaces
let mut tmp = Object::default();
for v in run.all_ns().await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("namespaces".to_owned(), tmp.into());
// Process the users
let mut tmp = Object::default();
for v in run.all_root_users().await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("users".to_owned(), tmp.into());
// Ok all good
Value::from(res).ok()
}
InfoStatement::Ns => {
// Selected NS?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Ns)?;
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
@ -73,6 +81,12 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("logins".to_owned(), tmp.into());
// Process the users
let mut tmp = Object::default();
for v in run.all_ns_users(opt.ns()).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("users".to_owned(), tmp.into());
// Process the tokens
let mut tmp = Object::default();
for v in run.all_nt(opt.ns()).await?.iter() {
@ -83,10 +97,8 @@ impl InfoStatement {
Value::from(res).ok()
}
InfoStatement::Db => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
@ -97,6 +109,12 @@ impl InfoStatement {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("logins".to_owned(), tmp.into());
// Process the users
let mut tmp = Object::default();
for v in run.all_db_users(opt.ns(), opt.db()).await?.iter() {
tmp.insert(v.name.to_string(), v.to_string().into());
}
res.insert("users".to_owned(), tmp.into());
// Process the tokens
let mut tmp = Object::default();
for v in run.all_dt(opt.ns(), opt.db()).await?.iter() {
@ -137,10 +155,8 @@ impl InfoStatement {
Value::from(res).ok()
}
InfoStatement::Sc(sc) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
@ -155,10 +171,8 @@ impl InfoStatement {
Value::from(res).ok()
}
InfoStatement::Tb(tb) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::View, ResourceKind::Any, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Create the result set
@ -190,6 +204,23 @@ impl InfoStatement {
// Ok all good
Value::from(res).ok()
}
InfoStatement::User(user, base) => {
let base = base.clone().unwrap_or(opt.selected_base()?);
// Allowed to run?
opt.is_allowed(Action::View, ResourceKind::Actor, &base)?;
// Claim transaction
let mut run = txn.lock().await;
// Process the user
let res = match base {
Base::Root => run.get_root_user(user).await?,
Base::Ns => run.get_ns_user(opt.ns(), user).await?,
Base::Db => run.get_db_user(opt.ns(), opt.db(), user).await?,
_ => return Err(Error::InvalidLevel(base.to_string())),
};
// Ok all good
Value::from(res.to_string()).ok()
}
}
}
}
@ -197,11 +228,15 @@ impl InfoStatement {
impl fmt::Display for InfoStatement {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Kv => f.write_str("INFO FOR KV"),
Self::Root => f.write_str("INFO FOR ROOT"),
Self::Ns => f.write_str("INFO FOR NAMESPACE"),
Self::Db => f.write_str("INFO FOR DATABASE"),
Self::Sc(ref s) => write!(f, "INFO FOR SCOPE {s}"),
Self::Tb(ref t) => write!(f, "INFO FOR TABLE {t}"),
Self::User(ref u, ref b) => match b {
Some(ref b) => write!(f, "INFO FOR USER {u} ON {b}"),
None => write!(f, "INFO FOR USER {u}"),
},
}
}
}
@ -211,12 +246,12 @@ pub fn info(i: &str) -> IResult<&str, InfoStatement> {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("FOR")(i)?;
let (i, _) = shouldbespace(i)?;
alt((kv, ns, db, sc, tb))(i)
alt((root, ns, db, sc, tb, user))(i)
}
fn kv(i: &str) -> IResult<&str, InfoStatement> {
let (i, _) = tag_no_case("KV")(i)?;
Ok((i, InfoStatement::Kv))
fn root(i: &str) -> IResult<&str, InfoStatement> {
let (i, _) = alt((tag_no_case("ROOT"), tag_no_case("KV")))(i)?;
Ok((i, InfoStatement::Root))
}
fn ns(i: &str) -> IResult<&str, InfoStatement> {
@ -243,11 +278,36 @@ fn tb(i: &str) -> IResult<&str, InfoStatement> {
Ok((i, InfoStatement::Tb(table)))
}
fn user(i: &str) -> IResult<&str, InfoStatement> {
let (i, _) = alt((tag_no_case("USER"), tag_no_case("US")))(i)?;
let (i, _) = shouldbespace(i)?;
let (i, user) = ident(i)?;
let (i, base) = opt(|i| {
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("ON")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, base) = base(i)?;
Ok((i, base))
})(i)?;
Ok((i, InfoStatement::User(user, base)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn info_query_root() {
let sql = "INFO FOR ROOT";
let res = info(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(out, InfoStatement::Root);
assert_eq!("INFO FOR ROOT", format!("{}", out));
}
#[test]
fn info_query_ns() {
let sql = "INFO FOR NAMESPACE";
@ -287,4 +347,35 @@ mod tests {
assert_eq!(out, InfoStatement::Tb(Ident::from("test")));
assert_eq!("INFO FOR TABLE test", format!("{}", out));
}
#[test]
fn info_query_user() {
let sql = "INFO FOR USER test ON ROOT";
let res = info(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(out, InfoStatement::User(Ident::from("test"), Some(Base::Root)));
assert_eq!("INFO FOR USER test ON ROOT", format!("{}", out));
let sql = "INFO FOR USER test ON NS";
let res = info(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(out, InfoStatement::User(Ident::from("test"), Some(Base::Ns)));
assert_eq!("INFO FOR USER test ON NAMESPACE", format!("{}", out));
let sql = "INFO FOR USER test ON DB";
let res = info(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(out, InfoStatement::User(Ident::from("test"), Some(Base::Db)));
assert_eq!("INFO FOR USER test ON DATABASE", format!("{}", out));
let sql = "INFO FOR USER test";
let res = info(sql);
assert!(res.is_ok());
let out = res.unwrap().1;
assert_eq!(out, InfoStatement::User(Ident::from("test"), None));
assert_eq!("INFO FOR USER test", format!("{}", out));
}
}

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -54,10 +53,8 @@ impl InsertStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,6 +1,6 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::sql::comment::shouldbespace;
@ -26,12 +26,10 @@ impl KillStatement {
txn: &Transaction,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Allowed to run?
// Is realtime enabled?
opt.realtime()?;
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Claim transaction
let mut run = txn.lock().await;
// Fetch the live query key

View file

@ -1,6 +1,6 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::sql::comment::shouldbespace;
@ -45,12 +45,10 @@ impl LiveStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Allowed to run?
// Is realtime enabled?
opt.realtime()?;
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Claim transaction
let mut run = txn.lock().await;
// Process the live query table

View file

@ -52,6 +52,7 @@ pub use self::define::DefineScopeStatement;
pub use self::define::DefineStatement;
pub use self::define::DefineTableStatement;
pub use self::define::DefineTokenStatement;
pub use self::define::DefineUserStatement;
pub use self::remove::RemoveDatabaseStatement;
pub use self::remove::RemoveEventStatement;
@ -65,3 +66,4 @@ pub use self::remove::RemoveScopeStatement;
pub use self::remove::RemoveStatement;
pub use self::remove::RemoveTableStatement;
pub use self::remove::RemoveTokenStatement;
pub use self::remove::RemoveUserStatement;

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -63,10 +62,8 @@ impl RelateStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,8 +1,9 @@
use crate::ctx::Context;
use crate::dbs::Options;
use crate::dbs::{Level, Transaction};
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::base::{base, base_or_scope, Base};
use crate::sql::comment::{mightbespace, shouldbespace};
use crate::sql::error::IResult;
@ -35,6 +36,7 @@ pub enum RemoveStatement {
Event(RemoveEventStatement),
Field(RemoveFieldStatement),
Index(RemoveIndexStatement),
User(RemoveUserStatement),
}
impl RemoveStatement {
@ -59,6 +61,7 @@ impl RemoveStatement {
Self::Field(ref v) => v.compute(ctx, opt, txn).await,
Self::Index(ref v) => v.compute(ctx, opt, txn).await,
Self::Analyzer(ref v) => v.compute(ctx, opt, txn).await,
Self::User(ref v) => v.compute(ctx, opt, txn).await,
}
}
}
@ -78,6 +81,7 @@ impl Display for RemoveStatement {
Self::Field(v) => Display::fmt(v, f),
Self::Index(v) => Display::fmt(v, f),
Self::Analyzer(v) => Display::fmt(v, f),
Self::User(v) => Display::fmt(v, f),
}
}
}
@ -96,6 +100,7 @@ pub fn remove(i: &str) -> IResult<&str, RemoveStatement> {
map(field, RemoveStatement::Field),
map(index, RemoveStatement::Index),
map(analyzer, RemoveStatement::Analyzer),
map(user, RemoveStatement::User),
))(i)
}
@ -116,10 +121,8 @@ impl RemoveNamespaceStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// No need for NS/DB
opt.needs(Level::Kv)?;
// Allowed to run?
opt.check(Level::Kv)?;
opt.is_allowed(Action::Edit, ResourceKind::Namespace, &Base::Root)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -170,10 +173,8 @@ impl RemoveDatabaseStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected NS?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Ns)?;
opt.is_allowed(Action::Edit, ResourceKind::Database, &Base::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -224,10 +225,8 @@ impl RemoveFunctionStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Function, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -282,10 +281,8 @@ impl RemoveAnalyzerStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Analyzer, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -335,12 +332,11 @@ impl RemoveLoginStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match self.base {
Base::Ns => {
// Selected NS?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Kv)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -350,10 +346,6 @@ impl RemoveLoginStatement {
Ok(Value::None)
}
Base::Db => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -362,7 +354,7 @@ impl RemoveLoginStatement {
// Ok all good
Ok(Value::None)
}
_ => unreachable!(),
_ => Err(Error::InvalidLevel(self.base.to_string())),
}
}
}
@ -410,12 +402,11 @@ impl RemoveTokenStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match &self.base {
Base::Ns => {
// Selected NS?
opt.needs(Level::Ns)?;
// Allowed to run?
opt.check(Level::Kv)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -425,10 +416,6 @@ impl RemoveTokenStatement {
Ok(Value::None)
}
Base::Db => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Ns)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -438,10 +425,6 @@ impl RemoveTokenStatement {
Ok(Value::None)
}
Base::Sc(sc) => {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -450,7 +433,7 @@ impl RemoveTokenStatement {
// Ok all good
Ok(Value::None)
}
_ => unreachable!(),
_ => Err(Error::InvalidLevel(self.base.to_string())),
}
}
}
@ -497,10 +480,8 @@ impl RemoveScopeStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Scope, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -551,10 +532,8 @@ impl RemoveParamStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Parameter, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -603,10 +582,8 @@ impl RemoveTableStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Table, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -658,10 +635,8 @@ impl RemoveEventStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Event, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -719,10 +694,8 @@ impl RemoveFieldStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Field, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -781,10 +754,8 @@ impl RemoveIndexStatement {
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::Edit, ResourceKind::Index, &Base::Db)?;
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
@ -827,6 +798,86 @@ fn index(i: &str) -> IResult<&str, RemoveIndexStatement> {
))
}
// --------------------------------------------------
// --------------------------------------------------
// --------------------------------------------------
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, Store, Hash)]
#[format(Named)]
pub struct RemoveUserStatement {
pub name: Ident,
pub base: Base,
}
impl RemoveUserStatement {
/// Process this type returning a computed simple Value
pub(crate) async fn compute(
&self,
_ctx: &Context<'_>,
opt: &Options,
txn: &Transaction,
) -> Result<Value, Error> {
// Allowed to run?
opt.is_allowed(Action::Edit, ResourceKind::Actor, &self.base)?;
match self.base {
Base::Root => {
// Claim transaction
let mut run = txn.lock().await;
// Process the statement
let key = crate::key::root::us::new(&self.name);
run.del(key).await?;
// Ok all good
Ok(Value::None)
}
Base::Ns => {
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
let key = crate::key::namespace::us::new(opt.ns(), &self.name);
run.del(key).await?;
// Ok all good
Ok(Value::None)
}
Base::Db => {
// Claim transaction
let mut run = txn.lock().await;
// Delete the definition
let key = crate::key::database::us::new(opt.ns(), opt.db(), &self.name);
run.del(key).await?;
// Ok all good
Ok(Value::None)
}
_ => Err(Error::InvalidLevel(self.base.to_string())),
}
}
}
impl Display for RemoveUserStatement {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
write!(f, "REMOVE USER {} ON {}", self.name, self.base)
}
}
fn user(i: &str) -> IResult<&str, RemoveUserStatement> {
let (i, _) = tag_no_case("REMOVE")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("USER")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, name) = ident(i)?;
let (i, _) = shouldbespace(i)?;
let (i, _) = tag_no_case("ON")(i)?;
let (i, _) = shouldbespace(i)?;
let (i, base) = base(i)?;
Ok((
i,
RemoveUserStatement {
name,
base,
},
))
}
#[cfg(test)]
mod tests {

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -83,10 +82,8 @@ impl SelectStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,14 +1,16 @@
use crate::ctx::Context;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Transaction;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::Action;
use crate::iam::ResourceKind;
use crate::sql::comment::shouldbespace;
use crate::sql::common::take_u64;
use crate::sql::error::IResult;
use crate::sql::table::{table, Table};
use crate::sql::value::Value;
use crate::sql::Base;
use derive::Store;
use nom::branch::alt;
use nom::bytes::complete::tag_no_case;
@ -38,9 +40,7 @@ impl ShowStatement {
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::Db)?;
opt.is_allowed(Action::View, ResourceKind::Table, &Base::Db)?;
// Clone transaction
let txn = txn.clone();
// Claim transaction

View file

@ -1,12 +1,12 @@
use crate::ctx::Context;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::doc::CursorDoc;
use crate::err::Error;
use crate::iam::{Action, ResourceKind};
use crate::sql::comment::shouldbespace;
use crate::sql::duration::duration;
use crate::sql::error::IResult;
use crate::sql::{Duration, Value};
use crate::sql::{Base, Duration, Value};
use derive::Store;
use nom::bytes::complete::tag_no_case;
use serde::{Deserialize, Serialize};
@ -25,10 +25,8 @@ impl SleepStatement {
opt: &Options,
_doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// No need for NS/DB
opt.needs(Level::Kv)?;
// Allowed to run?
opt.check(Level::Kv)?;
opt.is_allowed(Action::Edit, ResourceKind::Table, &Base::Root)?;
// Calculate the sleep duration
let dur = match (ctx.timeout(), self.duration.0) {
(Some(t), d) if t < d => t,
@ -66,7 +64,7 @@ pub fn sleep(i: &str) -> IResult<&str, SleepStatement> {
mod tests {
use super::*;
use crate::dbs::test::mock;
use crate::dbs::Auth;
use crate::iam::{Auth, Role};
use std::sync::Arc;
use std::time::SystemTime;
@ -92,7 +90,7 @@ mod tests {
async fn test_sleep_compute() {
let sql = "SLEEP 500ms";
let time = SystemTime::now();
let opt = Options::default().with_auth(Arc::new(Auth::Kv));
let opt = Options::default().with_auth(Arc::new(Auth::for_root(Role::Owner)));
let (ctx, _, _) = mock().await;
let (_, stm) = sleep(sql).unwrap();
let value = stm.compute(&ctx, &opt, None).await.unwrap();

View file

@ -1,6 +1,5 @@
use crate::ctx::Context;
use crate::dbs::Iterator;
use crate::dbs::Level;
use crate::dbs::Options;
use crate::dbs::Statement;
use crate::dbs::{Iterable, Transaction};
@ -51,10 +50,8 @@ impl UpdateStatement {
txn: &Transaction,
doc: Option<&CursorDoc<'_>>,
) -> Result<Value, Error> {
// Selected DB?
opt.needs(Level::Db)?;
// Allowed to run?
opt.check(Level::No)?;
// Valid options?
opt.valid_for_db()?;
// Create a new iterator
let mut i = Iterator::new();
// Ensure futures are stored

View file

@ -1,10 +1,13 @@
#[allow(unused_imports, dead_code)]
mod api_integration {
use once_cell::sync::Lazy;
use serde::Deserialize;
use serde::Serialize;
use serde_json::json;
use std::borrow::Cow;
use std::ops::Bound;
use std::sync::Arc;
use std::sync::Mutex;
use surrealdb::error::Api as ApiError;
use surrealdb::error::Db as DbError;
use surrealdb::opt::auth::Database;
@ -30,6 +33,9 @@ mod api_integration {
const NS: &str = "test-ns";
const ROOT_USER: &str = "root";
const ROOT_PASS: &str = "root";
// Used to ensure that only one test at a time is setting up the underlaying datastore.
// When auth is enabled, multiple tests may try to create the same root user at the same time.
static SETUP_MUTEX: Lazy<Arc<Mutex<()>>> = Lazy::new(|| Arc::new(Mutex::new(())));
#[derive(Debug, Serialize)]
struct Record<'a> {
@ -72,6 +78,7 @@ mod api_integration {
use surrealdb::engine::remote::ws::Ws;
async fn new_db() -> Surreal<Client> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let db = Surreal::new::<Ws>("127.0.0.1:8000").await.unwrap();
db.signin(Root {
@ -93,6 +100,7 @@ mod api_integration {
use surrealdb::engine::remote::http::Http;
async fn new_db() -> Surreal<Client> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let db = Surreal::new::<Http>("127.0.0.1:8000").await.unwrap();
db.signin(Root {
@ -114,6 +122,7 @@ mod api_integration {
use surrealdb::engine::any;
use surrealdb::engine::local::Db;
use surrealdb::engine::local::Mem;
use surrealdb::iam;
async fn new_db() -> Surreal<Db> {
init_logger();
@ -167,8 +176,9 @@ mod api_integration {
.await
.unwrap();
db.use_ns("namespace").use_db("database").await.unwrap();
let Error::Db(DbError::QueryPermissions) = db.create(Resource::from("item:foo")).await.unwrap_err() else {
panic!("record not found");
let res = db.create(Resource::from("item:foo")).await;
let Error::Db(DbError::IamError(iam::Error::NotAllowed { actor: _, action: _, resource: _ })) = res.unwrap_err() else {
panic!("expected permissions error");
};
}
@ -183,6 +193,7 @@ mod api_integration {
use surrealdb::engine::local::File;
async fn new_db() -> Surreal<Db> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let path = format!("/tmp/{}.db", Ulid::new());
let root = Root {
@ -205,9 +216,16 @@ mod api_integration {
use surrealdb::engine::local::RocksDb;
async fn new_db() -> Surreal<Db> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let path = format!("/tmp/{}.db", Ulid::new());
Surreal::new::<RocksDb>(path.as_str()).await.unwrap()
let root = Root {
username: ROOT_USER,
password: ROOT_PASS,
};
let db = Surreal::new::<RocksDb>((path.as_str(), root)).await.unwrap();
db.signin(root).await.unwrap();
db
}
include!("api/mod.rs");
@ -221,9 +239,16 @@ mod api_integration {
use surrealdb::engine::local::SpeeDb;
async fn new_db() -> Surreal<Db> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let path = format!("/tmp/{}.db", Ulid::new());
Surreal::new::<SpeeDb>(path.as_str()).await.unwrap()
let root = Root {
username: ROOT_USER,
password: ROOT_PASS,
};
let db = Surreal::new::<SpeeDb>((path.as_str(), root)).await.unwrap();
db.signin(root).await.unwrap();
db
}
include!("api/mod.rs");
@ -237,6 +262,7 @@ mod api_integration {
use surrealdb::engine::local::TiKv;
async fn new_db() -> Surreal<Db> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let root = Root {
username: ROOT_USER,
@ -258,6 +284,7 @@ mod api_integration {
use surrealdb::engine::local::FDb;
async fn new_db() -> Surreal<Db> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let root = Root {
username: ROOT_USER,
@ -278,6 +305,7 @@ mod api_integration {
use surrealdb::engine::any::Any;
async fn new_db() -> Surreal<Any> {
let _guard = SETUP_MUTEX.lock().unwrap();
init_logger();
let db = surrealdb::engine::any::connect("http://127.0.0.1:8000").await.unwrap();
db.signin(Root {

View file

@ -35,7 +35,11 @@ async fn invalidate() {
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
db.invalidate().await.unwrap();
let error = db.create::<Option<RecordId>>(("user", "john")).await.unwrap_err();
assert!(error.to_string().contains("You don't have permission to perform this query type"));
assert!(
error.to_string().contains("Not enough permissions to perform this action"),
"Unexpected error: {:?}",
error
);
}
#[tokio::test]
@ -72,7 +76,7 @@ async fn signin_ns() {
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
let sql = format!("DEFINE USER {user} ON NAMESPACE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signin(Namespace {
@ -91,7 +95,7 @@ async fn signin_db() {
db.use_ns(NS).use_db(&database).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON DATABASE PASSWORD '{pass}'");
let sql = format!("DEFINE USER {user} ON DATABASE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
db.signin(Database {
@ -151,7 +155,7 @@ async fn authenticate() {
db.use_ns(NS).use_db(Ulid::new().to_string()).await.unwrap();
let user = Ulid::new().to_string();
let pass = "password123";
let sql = format!("DEFINE LOGIN {user} ON NAMESPACE PASSWORD '{pass}'");
let sql = format!("DEFINE USER {user} ON NAMESPACE PASSWORD '{pass}'");
let response = db.query(sql).await.unwrap();
response.check().unwrap();
let token = db

View file

@ -16,7 +16,7 @@ async fn clear_transaction_cache_table() -> Result<(), Error> {
SELECT * FROM other;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 4);
//
@ -72,7 +72,7 @@ async fn clear_transaction_cache_field() -> Result<(), Error> {
COMMIT;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 6);
//

View file

@ -33,7 +33,7 @@ async fn table_change_feeds() -> Result<(), Error> {
SHOW CHANGES FOR TABLE person SINCE 0;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let start_ts = 0u64;
let end_ts = start_ts + 1;
dbs.tick_at(start_ts).await?;

View file

@ -18,7 +18,7 @@ async fn compare_empty() -> Result<(), Error> {
RETURN 0 = 0.1;
"#;
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 9);
//

View file

@ -202,7 +202,7 @@ async fn run_queries(
Error,
> {
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
dbs.execute(sql, &ses, None).await.map(|v| v.into_iter().map(|res| res.result))
}
@ -215,7 +215,7 @@ fn with_enough_stack(
// Roughly how much stack is allocated for surreal server workers in release mode
#[cfg(not(debug_assertions))]
{
builder = builder.stack_size(8_000_000);
builder = builder.stack_size(10_000_000);
}
// Same for debug mode

View file

@ -2,6 +2,7 @@ mod parse;
use parse::Parse;
use surrealdb::dbs::Session;
use surrealdb::err::Error;
use surrealdb::iam::Role;
use surrealdb::kvs::Datastore;
use surrealdb::sql::Value;
@ -17,7 +18,7 @@ async fn create_with_id() -> Result<(), Error> {
CREATE temperature CONTENT { id: ['London', '2022-09-30T20:25:01.406828Z'], name: 'London' };
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 7);
//
@ -110,7 +111,7 @@ async fn create_on_non_values_with_unique_index() -> Result<(), Error> {
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 3);
//
@ -119,3 +120,278 @@ async fn create_on_non_values_with_unique_index() -> Result<(), Error> {
}
Ok(())
}
//
// Permissions
//
async fn common_permissions_checks(auth_enabled: bool) {
let tests = vec![
// Root level
((().into(), Role::Owner), ("NS", "DB"), true, "owner at root level should be able to create a new record"),
((().into(), Role::Editor), ("NS", "DB"), true, "editor at root level should be able to create a new record"),
((().into(), Role::Viewer), ("NS", "DB"), false, "viewer at root level should not be able to create a new record"),
// Namespace level
((("NS",).into(), Role::Owner), ("NS", "DB"), true, "owner at namespace level should be able to create a new record on its namespace"),
((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at namespace level should not be able to create a new record on another namespace"),
((("NS",).into(), Role::Editor), ("NS", "DB"), true, "editor at namespace level should be able to create a new record on its namespace"),
((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at namespace level should not be able to create a new record on another namespace"),
((("NS",).into(), Role::Viewer), ("NS", "DB"), false, "viewer at namespace level should not be able to create a new record on its namespace"),
((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at namespace level should not be able to create a new record on another namespace"),
// Database level
((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true, "owner at database level should be able to create a new record on its database"),
((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false, "owner at database level should not be able to create a new record on another database"),
((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at database level should not be able to create a new record on another namespace even if the database name matches"),
((("NS", "DB").into(), Role::Editor), ("NS", "DB"), true, "editor at database level should be able to create a new record on its database"),
((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false, "editor at database level should not be able to create a new record on another database"),
((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at database level should not be able to create a new record on another namespace even if the database name matches"),
((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false, "viewer at database level should not be able to create a new record on its database"),
((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false, "viewer at database level should not be able to create a new record on another database"),
((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at database level should not be able to create a new record on another namespace even if the database name matches"),
];
let statement = "CREATE person";
// Test the CREATE statement when the table has to be created
for ((level, role), (ns, db), should_succeed, msg) in tests.into_iter() {
let sess = Session::for_level(level, role).with_ns(ns).with_db(db);
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds.execute(statement, &sess, None).await.unwrap();
let res = resp.remove(0).output();
if should_succeed {
assert!(res.is_ok(), "{}: {:?}", msg, res);
assert_ne!(res.unwrap(), Value::parse("[]"), "{}", msg);
} else if res.is_ok() {
// Permissions clause doesn't allow to query the table
assert_eq!(res.unwrap(), Value::parse("[]"), "{}", msg);
} else {
// Not allowed to create a table
let err = res.unwrap_err().to_string();
assert!(
err.contains("Not enough permissions to perform this action"),
"{}: {}",
msg,
err
)
}
}
// Test the CREATE statement when the table already exists
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute("CREATE person", &Session::owner().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
let mut resp = ds
.execute("CREATE person", &Session::owner().with_ns("OTHER_NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
let mut resp = ds
.execute("CREATE person", &Session::owner().with_ns("NS").with_db("OTHER_DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
// Run the test
let mut resp = ds.execute(statement, &sess, None).await.unwrap();
let res = resp.remove(0).output();
if should_succeed {
assert!(res.is_ok(), "{}: {:?}", msg, res);
assert_ne!(res.unwrap(), Value::parse("[]"), "{}", msg);
} else if res.is_ok() {
// Permissions clause doesn't allow to query the table
assert_eq!(res.unwrap(), Value::parse("[]"), "{}", msg);
} else {
// Not allowed to create a table
let err = res.unwrap_err().to_string();
assert!(
err.contains("Not enough permissions to perform this action"),
"{}: {}",
msg,
err
)
}
}
}
}
#[tokio::test]
async fn check_permissions_auth_enabled() {
let auth_enabled = true;
//
// Test common scenarios
//
common_permissions_checks(auth_enabled).await;
//
// Test Anonymous user
//
// When the table doesn't exist
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
let err = res.unwrap_err().to_string();
assert!(
err.contains("Not enough permissions to perform this action"),
"anonymous user should not be able to create the table: {}",
err
);
}
// When the table exists but grants no permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS NONE",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.unwrap() == Value::parse("[]"), "{}", "anonymous user should not be able to create a new record if the table exists but has no permissions");
}
// When the table exists and grants full permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS FULL",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.unwrap() != Value::parse("[]"), "{}", "anonymous user should be able to create a new record if the table exists and grants full permissions");
}
}
#[tokio::test]
async fn check_permissions_auth_disabled() {
let auth_enabled = false;
//
// Test common scenarios
//
common_permissions_checks(auth_enabled).await;
//
// Test Anonymous user
//
// When the table doesn't exist
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.unwrap() != Value::parse("[]"),
"{}",
"anonymous user should be able to create the table"
);
}
// When the table exists but grants no permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS NONE",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.unwrap() != Value::parse("[]"), "{}", "anonymous user should not be able to create a new record if the table exists but has no permissions");
}
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
// When the table exists and grants full permissions
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS FULL",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let mut resp = ds
.execute("CREATE person", &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.unwrap() != Value::parse("[]"), "{}", "anonymous user should be able to create a new record if the table exists and grants full permissions");
}
}

View file

@ -13,7 +13,7 @@ async fn datetimes_conversion() -> Result<(), Error> {
SELECT * FROM <string> "2012-01-01T08:00:00Z" + "-test";
"#;
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 3);
//

File diff suppressed because it is too large Load diff

View file

@ -1,26 +1,372 @@
mod parse;
use parse::Parse;
use surrealdb::dbs::Session;
use surrealdb::err::Error;
use surrealdb::iam::Role;
use surrealdb::kvs::Datastore;
use surrealdb::sql::Value;
#[tokio::test]
async fn delete_with_idiom_value() -> Result<(), Error> {
async fn delete() -> Result<(), Error> {
let sql = "
let $person = RETURN CREATE person:bob;
DELETE $person.id;
CREATE person:test SET name = 'Tester';
DELETE person:test;
SELECT * FROM person;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 2);
assert_eq!(res.len(), 3);
//
let _ = res.remove(0).result?;
let tmp = res.remove(0).result?;
let val = Value::parse(
"[
{
id: person:test,
name: 'Tester'
}
]",
);
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("[]");
assert_eq!(tmp, val);
//
let tmp = res.remove(0).result?;
let val = Value::parse("[]");
assert_eq!(tmp, val);
//
Ok(())
}
//
// Permissions
//
async fn common_permissions_checks(auth_enabled: bool) {
let tests = vec![
// Root level
((().into(), Role::Owner), ("NS", "DB"), true, "owner at root level should be able to delete a record"),
((().into(), Role::Editor), ("NS", "DB"), true, "editor at root level should be able to delete a record"),
((().into(), Role::Viewer), ("NS", "DB"), false, "viewer at root level should not be able to delete a record"),
// Namespace level
((("NS",).into(), Role::Owner), ("NS", "DB"), true, "owner at namespace level should be able to delete a record on its namespace"),
((("NS",).into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at namespace level should not be able to delete a record on another namespace"),
((("NS",).into(), Role::Editor), ("NS", "DB"), true, "editor at namespace level should be able to delete a record on its namespace"),
((("NS",).into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at namespace level should not be able to delete a record on another namespace"),
((("NS",).into(), Role::Viewer), ("NS", "DB"), false, "viewer at namespace level should not be able to delete a record on its namespace"),
((("NS",).into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at namespace level should not be able to delete a record on another namespace"),
// Database level
((("NS", "DB").into(), Role::Owner), ("NS", "DB"), true, "owner at database level should be able to delete a record on its database"),
((("NS", "DB").into(), Role::Owner), ("NS", "OTHER_DB"), false, "owner at database level should not be able to delete a record on another database"),
((("NS", "DB").into(), Role::Owner), ("OTHER_NS", "DB"), false, "owner at database level should not be able to delete a record on another namespace even if the database name matches"),
((("NS", "DB").into(), Role::Editor), ("NS", "DB"), true, "editor at database level should be able to delete a record on its database"),
((("NS", "DB").into(), Role::Editor), ("NS", "OTHER_DB"), false, "editor at database level should not be able to delete a record on another database"),
((("NS", "DB").into(), Role::Editor), ("OTHER_NS", "DB"), false, "editor at database level should not be able to delete a record on another namespace even if the database name matches"),
((("NS", "DB").into(), Role::Viewer), ("NS", "DB"), false, "viewer at database level should not be able to delete a record on its database"),
((("NS", "DB").into(), Role::Viewer), ("NS", "OTHER_DB"), false, "viewer at database level should not be able to delete a record on another database"),
((("NS", "DB").into(), Role::Viewer), ("OTHER_NS", "DB"), false, "viewer at database level should not be able to delete a record on another namespace even if the database name matches"),
];
let statement = "DELETE person:test";
for ((level, role), (ns, db), should_succeed, msg) in tests.into_iter() {
let sess = Session::for_level(level, role).with_ns(ns).with_db(db);
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute("CREATE person:test", &Session::owner().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
let mut resp = ds
.execute(
"CREATE person:test",
&Session::owner().with_ns("OTHER_NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
let mut resp = ds
.execute(
"CREATE person:test",
&Session::owner().with_ns("NS").with_db("OTHER_DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"unexpected error creating person record"
);
// Run the test
let mut resp = ds.execute(statement, &sess, None).await.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "delete should not fail");
if should_succeed {
// Verify the record has been deleted
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() == Value::parse("[]"), "{}", msg);
} else {
// Verify the record has not been deleted in any DB
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", msg);
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("OTHER_NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", msg);
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("OTHER_DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", msg);
}
}
}
}
#[tokio::test]
async fn check_permissions_auth_enabled() {
let auth_enabled = true;
//
// Test common scenarios
//
common_permissions_checks(auth_enabled).await;
//
// Test Anonymous user
//
let statement = "DELETE person:test";
// When the table exists but grants no permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS NONE; CREATE person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", "failed to create record");
let mut resp = ds
.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "delete should succeed even if it doesn't really delete anything");
// Verify the record has not been deleted
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() != Value::parse("[]"),
"{}",
"anonymous user should not be able to delete a record if the table has no permissions"
);
}
// When the table exists and grants full permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS FULL; CREATE person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", "failed to create record");
let mut resp = ds
.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "delete should succeed even if it doesn't really delete anything");
// Verify the record has been deleted
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() == Value::parse("[]"),
"{}",
"anonymous user should be able to delete a record if the table has full permissions"
);
}
}
#[tokio::test]
async fn check_permissions_auth_disabled() {
let auth_enabled = false;
//
// Test common scenarios
//
common_permissions_checks(auth_enabled).await;
//
// Test Anonymous user
//
let statement = "DELETE person:test";
// When the table exists but grants no permissions
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS NONE; CREATE person:test;",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", "failed to create record");
let mut resp = ds
.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "delete should succeed even if it doesn't really delete anything");
// Verify the record has been deleted
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() == Value::parse("[]"),
"{}",
"anonymous user should be able to delete a record if the table has no permissions"
);
}
{
let ds = Datastore::new("memory").await.unwrap().with_auth_enabled(auth_enabled);
// When the table exists and grants full permissions
let mut resp = ds
.execute(
"DEFINE TABLE person PERMISSIONS FULL; CREATE person:test;",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "failed to create table: {:?}", res);
let res = resp.remove(0).output();
assert!(res.is_ok() && res.unwrap() != Value::parse("[]"), "{}", "failed to create record");
let mut resp = ds
.execute(statement, &Session::default().with_ns("NS").with_db("DB"), None)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(res.is_ok(), "delete should succeed even if it doesn't really delete anything");
// Verify the record has been deleted
let mut resp = ds
.execute(
"SELECT * FROM person:test",
&Session::owner().with_ns("NS").with_db("DB"),
None,
)
.await
.unwrap();
let res = resp.remove(0).output();
assert!(
res.is_ok() && res.unwrap() == Value::parse("[]"),
"{}",
"anonymous user should be able to delete a record if the table has full permissions"
);
}
}

View file

@ -17,7 +17,7 @@ async fn complex_ids() -> Result<(), Error> {
SELECT * FROM person;
"#;
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 7);
//
@ -95,7 +95,7 @@ async fn complex_strings() -> Result<(), Error> {
RETURN "String with some 'single' and \"double\" quoted characters";
"#;
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 5);
//

View file

@ -22,7 +22,7 @@ async fn create_relate_select() -> Result<(), Error> {
SELECT *, ->(bought AS purchases) FROM user FETCH purchases, purchases.out;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 12);
//

View file

@ -19,7 +19,7 @@ async fn field_definition_value_assert_failure() -> Result<(), Error> {
CREATE person:test SET email = 'info@surrealdb.com', other = 'ignore', age = 13;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 9);
//
@ -102,7 +102,7 @@ async fn field_definition_value_assert_success() -> Result<(), Error> {
CREATE person:test SET email = 'info@surrealdb.com', other = 'ignore', age = 22;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 5);
//
@ -151,7 +151,7 @@ async fn field_definition_empty_nested_objects() -> Result<(), Error> {
SELECT * FROM person;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 4);
//
@ -205,7 +205,7 @@ async fn field_definition_empty_nested_arrays() -> Result<(), Error> {
SELECT * FROM person;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 4);
//
@ -257,7 +257,7 @@ async fn field_definition_empty_nested_flexible() -> Result<(), Error> {
SELECT * FROM person;
";
let dbs = Datastore::new("memory").await?;
let ses = Session::for_kv().with_ns("test").with_db("test");
let ses = Session::owner().with_ns("test").with_db("test");
let res = &mut dbs.execute(sql, &ses, None).await?;
assert_eq!(res.len(), 4);
//

Some files were not shown because too many files have changed in this diff Show more