[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:
parent
e61db5564c
commit
998b263517
148 changed files with 8247 additions and 1315 deletions
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
@ -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
342
Cargo.lock
generated
|
@ -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"
|
||||
|
|
15
Makefile
15
Makefile
|
@ -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:
|
||||
|
|
|
@ -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" ] }
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -51,7 +51,7 @@ async fn api() {
|
|||
.unwrap();
|
||||
|
||||
// signin
|
||||
let _: () = DB
|
||||
let _: Jwt = DB
|
||||
.signin(Root {
|
||||
username: "root",
|
||||
password: "root",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ¶ms[..] {
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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::*;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
94
lib/src/iam/auth.rs
Normal 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())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
57
lib/src/iam/entities/action.rs
Normal file
57
lib/src/iam/entities/action.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
9
lib/src/iam/entities/mod.rs
Normal file
9
lib/src/iam/entities/mod.rs
Normal 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::*;
|
127
lib/src/iam/entities/resources/actor.rs
Normal file
127
lib/src/iam/entities/resources/actor.rs
Normal 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)
|
||||
}
|
||||
}
|
192
lib/src/iam/entities/resources/level.rs
Normal file
192
lib/src/iam/entities/resources/level.rs
Normal 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()
|
||||
}
|
||||
}
|
7
lib/src/iam/entities/resources/mod.rs
Normal file
7
lib/src/iam/entities/resources/mod.rs
Normal file
|
@ -0,0 +1,7 @@
|
|||
mod actor;
|
||||
mod level;
|
||||
mod resource;
|
||||
|
||||
pub use actor::*;
|
||||
pub use level::*;
|
||||
pub use resource::*;
|
150
lib/src/iam/entities/resources/resource.rs
Normal file
150
lib/src/iam/entities/resources/resource.rs
Normal 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())
|
||||
}
|
||||
}
|
81
lib/src/iam/entities/roles.rs
Normal file
81
lib/src/iam/entities/roles.rs
Normal 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()
|
||||
}
|
||||
}
|
97
lib/src/iam/entities/schema.rs
Normal file
97
lib/src/iam/entities/schema.rs
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
96
lib/src/iam/policies/mod.rs
Normal file
96
lib/src/iam/policies/mod.rs
Normal 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);
|
||||
}
|
||||
}
|
40
lib/src/iam/policies/policy_set.rs
Normal file
40
lib/src/iam/policies/policy_set.rs
Normal 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()
|
||||
});
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,4 +7,5 @@ pub mod sc;
|
|||
pub mod tb;
|
||||
pub mod tk;
|
||||
pub mod ts;
|
||||
pub mod us;
|
||||
pub mod vs;
|
||||
|
|
|
@ -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> {
|
||||
|
|
77
lib/src/key/database/us.rs
Normal file
77
lib/src/key/database/us.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -2,3 +2,4 @@ pub mod all;
|
|||
pub mod db;
|
||||
pub mod lg;
|
||||
pub mod tk;
|
||||
pub mod us;
|
||||
|
|
72
lib/src/key/namespace/us.rs
Normal file
72
lib/src/key/namespace/us.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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
65
lib/src/key/root/us.rs
Normal 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");
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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>>;
|
||||
|
|
|
@ -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!(),
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
//
|
||||
|
|
|
@ -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?;
|
||||
|
|
|
@ -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);
|
||||
//
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
//
|
||||
|
|
|
@ -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);
|
||||
//
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue