Compare commits
1 Commits
password-c
...
auth-daemo
| Author | SHA1 | Date | |
|---|---|---|---|
|
343c3686e1
|
@@ -6,9 +6,6 @@ on:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
env:
|
||||
BINSTALL_DISABLE_TELEMETRY: 'true'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: debian-latest
|
||||
@@ -16,7 +13,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Build
|
||||
run: cargo build --all-features --verbose --release
|
||||
@@ -27,8 +27,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
|
||||
- name: Check code format
|
||||
@@ -41,13 +43,15 @@ jobs:
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install cargo-deny
|
||||
run: cargo binstall -y cargo-deny
|
||||
run: cargo install cargo-deny
|
||||
|
||||
- name: Check licenses
|
||||
run: |
|
||||
@@ -68,7 +72,8 @@ jobs:
|
||||
run: cargo binstall -y cargo-nextest --secure
|
||||
|
||||
- name: Run tests
|
||||
run: cargo nextest run --release --no-fail-fast
|
||||
run: |
|
||||
cargo nextest run --release --no-fail-fast
|
||||
env:
|
||||
RUST_LOG: "trace"
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
@@ -111,7 +116,10 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Build docs
|
||||
run: cargo doc --all-features --document-private-items --release
|
||||
|
||||
@@ -22,9 +22,6 @@ on:
|
||||
- beta
|
||||
default: stable
|
||||
|
||||
env:
|
||||
BINSTALL_DISABLE_TELEMETRY: 'true'
|
||||
|
||||
# TODO: dynamic matrix builds when...
|
||||
# https://github.com/go-gitea/gitea/issues/25179
|
||||
jobs:
|
||||
@@ -36,15 +33,15 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: cargo-bins/cargo-binstall@main
|
||||
|
||||
- name: Install rust toolchain
|
||||
uses: dtolnay/rust-toolchain@master
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ inputs.rust_toolchain }}
|
||||
override: true
|
||||
|
||||
- name: Install cargo-deb
|
||||
run: cargo binstall -y cargo-deb
|
||||
run: cargo install cargo-deb
|
||||
|
||||
- name: Build deb package
|
||||
env:
|
||||
@@ -58,12 +55,12 @@ jobs:
|
||||
CREATE_DEB_ARGS+=("--deb-version" "${{ inputs.deb_version }}")
|
||||
fi
|
||||
|
||||
./scripts/create-deb.sh "${CREATE_DEB_ARGS[@]}"
|
||||
./create-deb.sh "${CREATE_DEB_ARGS[@]}"
|
||||
|
||||
- name: Upload deb package artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: muscl-deb-${{ matrix.os }}-${{ gitea.sha }}.zip
|
||||
name: muscl-deb-${{ matrix.os }}.zip
|
||||
path: target/debian/*.deb
|
||||
if-no-files-found: error
|
||||
retention-days: 30
|
||||
|
||||
25
CHANGELOG.md
25
CHANGELOG.md
@@ -49,17 +49,16 @@ This is the initial release of `muscl`.
|
||||
### Known deviations from `mysql-admutils`' behaviour
|
||||
|
||||
- `--help` output is formatted by clap in a different style.
|
||||
- `mysql-dbadm edit-perm` uses the new privilege editor implementation. The formatting that
|
||||
was used in `mysql-admutils` is no longer present. However, since the editor is purely an
|
||||
interactive tool, there shouldn't have been any scripts relying on the old formatting.
|
||||
- The configuration file is shared for all variants of the program, and `muscl` will use
|
||||
its new logic to look for and parse this file. See the example config and
|
||||
[installation instructions][installation-instructions] for more information about how to
|
||||
configure the software.
|
||||
- The order in which input is validated might be differ from the original
|
||||
(e.g. database ownership checks, invalid character checks, existence checks, ...).
|
||||
This means that running the exact same command might lead to different error messages.
|
||||
- Command-line arguments are de-duplicated. For example, if the user runs
|
||||
- `mysql-dbadm edit-perm` uses the new privilege editor implementation. Replicating
|
||||
the old behaviour
|
||||
there shoulnd't have been any (or at least very few) scripts relying on the old
|
||||
command API or behavior.
|
||||
- The new tools use the new implementation to find it's configuration file, and uses the
|
||||
new configuration format. See the example config and installation instructions for more
|
||||
information about how to configure the software.
|
||||
- The order in which input is validated (e.g. whether you own a user, whether the
|
||||
contains illegal characters, whether the user does or does not exist) might be different
|
||||
from the original program, leading to the same command reporting different errors.
|
||||
- Arguments are de-duplicated, meaning that if you run something like
|
||||
`mysql-dbadm create user_db1 user_db2 user_db1`, the program will only try to create
|
||||
the `user_db1` once. The old program would have attempted to create it twice,
|
||||
failing the second attempt.
|
||||
the `user_db1` once. The old program would attempt to create it twice, failing the second time.
|
||||
|
||||
375
Cargo.lock
generated
375
Cargo.lock
generated
@@ -17,15 +17,6 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
@@ -171,43 +162,11 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info-build"
|
||||
version = "0.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b090e1d116997848529faaf849e1efd592cbe6e9eb44623c0588f017c63bbc4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"bincode 2.0.1",
|
||||
"build-info-common",
|
||||
"cargo_metadata",
|
||||
"chrono",
|
||||
"git2",
|
||||
"glob",
|
||||
"pretty_assertions",
|
||||
"rustc_version",
|
||||
"serde_json",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "build-info-common"
|
||||
version = "0.0.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7a436965b6554ae18aba994745234bf2ed98d2c984e96b58aeb0f845c666969"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"derive_more",
|
||||
"semver",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
@@ -221,44 +180,11 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.23.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.50"
|
||||
version = "1.2.49"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c"
|
||||
checksum = "90583009037521a116abf44494efecd645ba48b6622457080f080b85544e2215"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -278,20 +204,6 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.42"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
|
||||
dependencies = [
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"serde",
|
||||
"wasm-bindgen",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.53"
|
||||
@@ -327,9 +239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.62"
|
||||
version = "4.5.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "004eef6b14ce34759aa7de4aea3217e368f463f46a3ed3764ca4b5a4404003b4"
|
||||
checksum = "39615915e2ece2550c0149addac32fb5bd312c657f43845bb9088cb9c8a7c992"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"clap_lex",
|
||||
@@ -355,27 +267,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
||||
|
||||
[[package]]
|
||||
name = "color-print"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3aa954171903797d5623e047d9ab69d91b493657917bdfb8c2c80ecaf9cdb6f4"
|
||||
dependencies = [
|
||||
"color-print-proc-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color-print-proc-macro"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "692186b5ebe54007e45a59aea47ece9eb4108e141326c304cdc91699a7118a22"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
@@ -393,9 +284,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "console"
|
||||
version = "0.16.2"
|
||||
version = "0.16.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03e45a4a8926227e4197636ba97a9fc9b00477e9f4bd711395687c5f0734bec4"
|
||||
checksum = "b430743a6eb14e9764d4260d4c0d8123087d504eeb9c48f2b2a5e810dd369df4"
|
||||
dependencies = [
|
||||
"encode_unicode",
|
||||
"libc",
|
||||
@@ -439,12 +330,6 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@@ -528,18 +413,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "2.1.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134"
|
||||
checksum = "10b768e943bed7bf2cab53df09f4bc34bfd217cdb57d971e769874c9a6710618"
|
||||
dependencies = [
|
||||
"derive_more-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more-impl"
|
||||
version = "2.1.1"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb"
|
||||
checksum = "6d286bfdaf75e988b4a78e013ecd79c581e06399ab53fbacd2d916c2f904f30b"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
@@ -561,12 +446,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
@@ -697,7 +576,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -890,12 +769,6 @@ dependencies = [
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -967,39 +840,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"log",
|
||||
"wasm-bindgen",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone-haiku"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -1129,7 +969,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1158,9 +998,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.16"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
@@ -1228,13 +1068,13 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
|
||||
|
||||
[[package]]
|
||||
name = "libredox"
|
||||
version = "0.1.11"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df15f6eac291ed1cf25865b1ee60399f57e7c227e7f51bdbd4c5270396a9ed50"
|
||||
checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"libc",
|
||||
"redox_syscall 0.6.0",
|
||||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1311,12 +1151,6 @@ dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.1.1"
|
||||
@@ -1335,24 +1169,19 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-bincode",
|
||||
"bincode 2.0.1",
|
||||
"build-info-build",
|
||||
"chrono",
|
||||
"clap",
|
||||
"clap-verbosity-flag",
|
||||
"clap_complete",
|
||||
"color-print",
|
||||
"const_format",
|
||||
"derive_more",
|
||||
"dialoguer",
|
||||
"futures-util",
|
||||
"git2",
|
||||
"humansize",
|
||||
"indoc",
|
||||
"itertools",
|
||||
"landlock",
|
||||
"nix",
|
||||
"num_cpus",
|
||||
"pretty_assertions",
|
||||
"prettytable",
|
||||
"rand 0.9.2",
|
||||
"regex",
|
||||
@@ -1385,16 +1214,6 @@ dependencies = [
|
||||
"memoffset",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.3"
|
||||
@@ -1496,7 +1315,7 @@ checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"redox_syscall 0.5.18",
|
||||
"redox_syscall",
|
||||
"smallvec",
|
||||
"windows-link",
|
||||
]
|
||||
@@ -1593,16 +1412,6 @@ dependencies = [
|
||||
"zerocopy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pretty_assertions"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
|
||||
dependencies = [
|
||||
"diff",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettytable"
|
||||
version = "0.10.0"
|
||||
@@ -1709,15 +1518,6 @@ dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec96166dafa0886eb81fe1c0a388bece180fbef2135f97c1e2cf8302e74b43b5"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
@@ -1811,7 +1611,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1830,9 +1630,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-pki-types"
|
||||
version = "1.13.2"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282"
|
||||
checksum = "708c0f9d5f54ba0272468c1d306a52c495b31fa155e91bc25371e6df7996908c"
|
||||
dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
@@ -1856,9 +1656,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.21"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -1880,10 +1680,6 @@ name = "semver"
|
||||
version = "1.0.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
@@ -1917,9 +1713,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.146"
|
||||
version = "1.0.145"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "217ca874ae0207aac254aa02c957ded05585a90892cc8d87f9e5fa49669dadd8"
|
||||
checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
@@ -1931,9 +1727,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "e24345aa0fe688594e73770a5f6d1b216508b4f93484c0026d521acd30134392"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -2307,7 +2103,7 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.52.0",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2464,9 +2260,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.10+spec-1.1.0"
|
||||
version = "0.9.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
|
||||
checksum = "f0dc8b1fb61449e27716ec0e1bdf0f6b8f3e8f6b05391e8497b8b6d7804ea6d8"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -2479,33 +2275,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.7.5+spec-1.1.0"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
|
||||
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.44"
|
||||
version = "0.1.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
|
||||
checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
@@ -2526,9 +2322,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.36"
|
||||
version = "0.1.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
|
||||
checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
@@ -2814,65 +2610,12 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.62.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
|
||||
dependencies = [
|
||||
"windows-implement",
|
||||
"windows-interface",
|
||||
"windows-link",
|
||||
"windows-result",
|
||||
"windows-strings",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.59.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.48.0"
|
||||
@@ -3113,12 +2856,6 @@ version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.8.1"
|
||||
@@ -3221,31 +2958,3 @@ dependencies = [
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "7.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
|
||||
dependencies = [
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.16+zstd.1.5.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
51
Cargo.toml
51
Cargo.toml
@@ -2,7 +2,6 @@
|
||||
name = "muscl"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
resolver = "2"
|
||||
license = "BSD-3-Clause"
|
||||
authors = [
|
||||
"oysteikt@pvv.ntnu.no",
|
||||
@@ -22,47 +21,42 @@ autolib = false
|
||||
anyhow = "1.0.100"
|
||||
async-bincode = "0.8.0"
|
||||
bincode = "2.0.1"
|
||||
chrono = { version = "0.4.42", features = ["serde"] }
|
||||
clap = { version = "4.5.53", features = ["cargo", "derive"] }
|
||||
clap-verbosity-flag = { version = "3.0.4", features = [ "tracing" ] }
|
||||
clap_complete = { version = "4.5.62", features = ["unstable-dynamic"] }
|
||||
color-print = "0.3.7"
|
||||
clap_complete = { version = "4.5.61", features = ["unstable-dynamic"] }
|
||||
const_format = "0.2.35"
|
||||
derive_more = { version = "2.1.1", features = ["display", "error"] }
|
||||
derive_more = { version = "2.1.0", features = ["display", "error"] }
|
||||
dialoguer = "0.12.0"
|
||||
futures-util = "0.3.31"
|
||||
humansize = "2.1.3"
|
||||
indoc = "2.0.7"
|
||||
itertools = "0.14.0"
|
||||
nix = { version = "0.30.1", features = ["fs", "process", "socket", "user"] }
|
||||
num_cpus = "1.17.0"
|
||||
prettytable = "0.10.0"
|
||||
rand = "0.9.2"
|
||||
sd-notify = "0.4.5"
|
||||
serde = "1.0.228"
|
||||
serde_json = { version = "1.0.146", features = ["preserve_order"] }
|
||||
serde_json = { version = "1.0.145", features = ["preserve_order"] }
|
||||
sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql", "tls-rustls"] }
|
||||
thiserror = "2.0.17"
|
||||
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "signal"] }
|
||||
tokio-serde = { version = "0.9.0", features = ["bincode"] }
|
||||
tokio-stream = "0.1.17"
|
||||
tokio-util = { version = "0.7.17", features = ["codec", "rt"] }
|
||||
toml = "0.9.10"
|
||||
tracing = { version = "0.1.44", features = ["log"] }
|
||||
toml = "0.9.8"
|
||||
tracing = { version = "0.1.43", features = ["log"] }
|
||||
tracing-journald = "0.3.2"
|
||||
tracing-subscriber = "0.3.22"
|
||||
uuid = { version = "1.19.0", features = ["v4"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
landlock = "0.4.4"
|
||||
sd-notify = "0.4.5"
|
||||
tracing-journald = "0.3.2"
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = "1.0.100"
|
||||
build-info-build = "0.0.42"
|
||||
git2 = { version = "0.20.3", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
regex = "1.12.2"
|
||||
|
||||
[features]
|
||||
@@ -70,19 +64,10 @@ default = ["mysql-admutils-compatibility"]
|
||||
mysql-admutils-compatibility = []
|
||||
suid-sgid-mode = []
|
||||
|
||||
[lib]
|
||||
name = "muscl_lib"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "muscl"
|
||||
bench = false
|
||||
path = "src/entrypoints/muscl.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "muscl-server"
|
||||
bench = false
|
||||
path = "src/entrypoints/muscl_server.rs"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release-lto]
|
||||
inherits = "release"
|
||||
@@ -132,11 +117,6 @@ assets = [
|
||||
"usr/bin/",
|
||||
"755",
|
||||
],
|
||||
[
|
||||
"target/release/muscl-server",
|
||||
"usr/bin/",
|
||||
"755",
|
||||
],
|
||||
[
|
||||
"target/release/mysql-useradm",
|
||||
"usr/bin/",
|
||||
@@ -152,11 +132,6 @@ assets = [
|
||||
"etc/muscl/config.toml",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"assets/debian/group_denylist.txt",
|
||||
"etc/muscl/group_denylist.txt",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"assets/completions/_*",
|
||||
"usr/share/zsh/site-functions/completions/",
|
||||
@@ -172,16 +147,6 @@ assets = [
|
||||
"usr/share/fish/vendor_completions.d/",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"README.md",
|
||||
"usr/share/doc/muscl/",
|
||||
"644",
|
||||
],
|
||||
[
|
||||
"docs/*.md",
|
||||
"usr/share/doc/muscl/docs/",
|
||||
"644",
|
||||
],
|
||||
]
|
||||
preserve-symlinks = true
|
||||
maintainer-scripts = "debian/"
|
||||
|
||||
48
README.md
48
README.md
@@ -3,53 +3,31 @@
|
||||
|
||||
# muscl 💪
|
||||
|
||||
Dropping DBs (dumbbells) and having MySQL spasms since 2024
|
||||
Dropping DBs (dumbbells) and having mysql spasms since 2024
|
||||
|
||||
## What is this?
|
||||
|
||||
`muscl is a secure MySQL administration tool for multi-user systems.
|
||||
It allows unprivileged users to manage their own databases and database users without granting them direct access to the MySQL server.
|
||||
Authorization is handled by a prefix-based model tied to Unix users and groups, making it ideal for shared hosting environments, like university servers, tilde servers, or similar.
|
||||
This is a CLI tool that let's unprivileged users perform administrative operations on a MySQL DBMS, given the are authorized to perform the action on the database or database user in question.
|
||||
The default authorization mechanism is to only let the user perform these actions on databases and database users that are prefixed with their username,
|
||||
or with the name of any unix group that the user is a part of. i.e. `<user>_mydb`, `<user>_mydbuser`, or `<group>_myotherdb`.
|
||||
|
||||
When a user requests an administrative operation, the `muscl` daemon verifies authenticates the user through unix socket peer credentials,
|
||||
and then checks the requested item name against the user's username and group list for authorization.
|
||||
The default authorization mechanism only allows the user to manage items prefixed with either their username or a group name.
|
||||
For example, a user would be allowed to manage items like `<user>_mydb`, `<user>_mydbuser`, or `<group>_myotherdb`.
|
||||
The available administrative actions include:
|
||||
|
||||
The available administrative operations include:
|
||||
- creating/listing/modifying/deleting databases and database users
|
||||
- modifying privileges for a database user on a database
|
||||
- changing the passwords of the database users
|
||||
- locking and unlocking database users
|
||||
- ... more to come
|
||||
|
||||
```bash
|
||||
# Creating, listing, modifying, and deleting databases and database users
|
||||
muscl create-db user_testdb
|
||||
muscl create-user user_testuser --password strongpassword
|
||||
muscl show-db
|
||||
muscl drop-db group_projectdb
|
||||
The software is designed to be run as a client and a server. The server has administrative access to the mysql server,
|
||||
and is responsible for authorizing any requests from the clients.
|
||||
|
||||
# Modifying privileges for a database user on a database
|
||||
muscl edit-privs user_testdb user_testuser +suid
|
||||
muscl edit-privs -p user_testdb:user_testuser:A -p group_projectdb:otheruser:-d
|
||||
muscl show-privs --json
|
||||
|
||||
# Changing the passwords of the database users
|
||||
muscl passwd-user user_testuser
|
||||
muscl passwd-user user_otheruser --stdin <<<"hunter2"
|
||||
|
||||
# Locking and unlocking database users
|
||||
muscl lock-user user_testuser
|
||||
muscl unlock-user user_testuser
|
||||
|
||||
# And more...
|
||||
```
|
||||
|
||||
The software is designed to be run as a client and a server. The clients are run by the unprivileged users,
|
||||
and does not have direct access to the MySQL server. Instead, they communicate with the muscl server
|
||||
over a IPC, which then performs the requested operations on behalf of the clients.
|
||||
This software is designed for multi-user servers, like tilde servers, university servers, etc.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Installation and configuration](docs/installation.md)
|
||||
- [Development and testing](docs/development.md)
|
||||
- [Compiling and packaging](docs/compiling.md)
|
||||
- [Compatibility mode with mysql-admutils](docs/mysql-admutils-compatibility.md)
|
||||
- [Use with NixOS](docs/nixos.md)
|
||||
- [SUID/SGID mode](docs/suid-sgid-mode.md)
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
[authorization]
|
||||
group_denylist_file = "/etc/muscl/group_denylist.txt"
|
||||
|
||||
[mysql]
|
||||
# Hostname and port of the database.
|
||||
host = "localhost"
|
||||
@@ -19,8 +16,7 @@ port = 3306
|
||||
# systemd unit.
|
||||
username = "muscl"
|
||||
# This file gets created by systemd automatically, given you have set
|
||||
# the password with `systemd-creds`. See /usr/share/doc/muscl/docs/installation.md
|
||||
# for more information.
|
||||
# the password with `systemd-creds`.
|
||||
password_file = "/run/credentials/muscl.service/muscl_mysql_password"
|
||||
|
||||
# Database connection timeout in seconds
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
# These are the default system groups on debian.
|
||||
# You can alos add groups by gid by prefixing the line with 'gid:'.
|
||||
|
||||
group:adm
|
||||
group:audio
|
||||
group:avahi
|
||||
group:backup
|
||||
group:bin
|
||||
group:cdrom
|
||||
group:crontab
|
||||
group:daemon
|
||||
group:dialout
|
||||
group:dip
|
||||
group:disk
|
||||
group:fax
|
||||
group:floppy
|
||||
group:games
|
||||
group:gnats
|
||||
group:input
|
||||
group:irc
|
||||
group:kmem
|
||||
group:kvm
|
||||
group:list
|
||||
group:lp
|
||||
group:mail
|
||||
group:man
|
||||
group:mlocate
|
||||
group:netdev
|
||||
group:news
|
||||
group:nogroup
|
||||
group:openldap
|
||||
group:operator
|
||||
group:plocate
|
||||
group:plugdev
|
||||
group:polkitd
|
||||
group:proxy
|
||||
group:render
|
||||
group:root
|
||||
group:sasl
|
||||
group:shadow
|
||||
group:src
|
||||
group:staff
|
||||
group:sudo
|
||||
group:sys
|
||||
group:systemd-journal
|
||||
group:systemd-network
|
||||
group:systemd-resolve
|
||||
group:systemd-timesync
|
||||
group:tape
|
||||
group:tty
|
||||
group:users
|
||||
group:utmp
|
||||
group:uucp
|
||||
group:video
|
||||
group:voice
|
||||
group:www-data
|
||||
@@ -1,11 +1,10 @@
|
||||
[Unit]
|
||||
Description=Muscl MySQL admin tool
|
||||
Requires=muscl.socket
|
||||
After=mysql.service mariadb.service
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/muscl-server --systemd --disable-landlock socket-activate
|
||||
ExecStart=/usr/bin/muscl server --systemd --disable-landlock socket-activate
|
||||
ExecReload=/usr/bin/kill -HUP $MAINPID
|
||||
|
||||
WatchdogSec=15
|
||||
|
||||
11
build.rs
11
build.rs
@@ -23,19 +23,8 @@ fn embed_build_time_info() {
|
||||
.unwrap_or("unknown")
|
||||
.to_string();
|
||||
|
||||
let dependencies = build_info_build::build_script()
|
||||
.collect_runtime_dependencies(build_info_build::DependencyDepth::Depth(1))
|
||||
.build()
|
||||
.crate_info
|
||||
.dependencies
|
||||
.into_iter()
|
||||
.map(|dep| format!("{}: {}", dep.name, dep.version))
|
||||
.collect::<Vec<_>>()
|
||||
.join(";");
|
||||
|
||||
println!("cargo:rustc-env=GIT_COMMIT={}", commit);
|
||||
println!("cargo:rustc-env=BUILD_PROFILE={}", build_profile);
|
||||
println!("cargo:rustc-env=DEPENDENCY_LIST={}", dependencies);
|
||||
}
|
||||
|
||||
fn generate_mysql_admutils_symlinks() -> anyhow::Result<()> {
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
# Compiling and packaging
|
||||
|
||||
This document describes how to compile `muscl` from source code, along with other related tasks.
|
||||
|
||||
## Build
|
||||
|
||||
To just compile `muscl`, there is not many special steps needed.
|
||||
You need to have a working [Rust toolchain](https://www.rust-lang.org/tools/install) installed.
|
||||
|
||||
```bash
|
||||
# Compile in debug mode
|
||||
cargo build
|
||||
ls target/debug # muscl, mysql-dbadm, mysql-useradm, ...
|
||||
|
||||
# Compile in release mode
|
||||
cargo build --release
|
||||
ls target/release # muscl, mysql-dbadm, mysql-useradm, ...
|
||||
|
||||
# Compile in release mode with link time optimization (only used for distribution builds)
|
||||
cargo build --profile release-lto
|
||||
ls target/release-lto # muscl, mysql-dbadm, mysql-useradm, ...
|
||||
```
|
||||
|
||||
## Generating completions
|
||||
|
||||
> [!NOTE]
|
||||
> This happens automatically when building the deb package, so you can skip this step if that's the goal.
|
||||
|
||||
In order to generate shell completions that work correctly, you need to put `muscl` (or alias symlinks) in your `$PATH`.
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
(
|
||||
PATH="$(pwd)/target/release:$PATH"
|
||||
mkdir -p completions/bash
|
||||
mkdir -p completions/zsh
|
||||
mkdir -p completions/fish
|
||||
|
||||
muscl completions --shell bash > completions/bash/muscl.bash
|
||||
muscl completions --shell zsh > completions/zsh/_muscl
|
||||
muscl completions --shell fish > completions/fish/muscl.fish
|
||||
)
|
||||
```
|
||||
|
||||
Due to a [bug in clap](https://github.com/clap-rs/clap/issues/1764), you will also need to edit the completion files for the aliases.
|
||||
|
||||
```bash
|
||||
sed -i 's/muscl/mysql-dbadm/g' assets/completions/{mysql-dbadm.bash,mysql-dbadm.fish,_mysql-dbadm}
|
||||
sed -i 's/muscl/mysql-useradm/g' assets/completions/{mysql-useradm.bash,mysql-useradm.fish,_mysql-useradm}
|
||||
```
|
||||
|
||||
## Bundling into a deb package
|
||||
|
||||
We have a script that automates the process of building a deb package for Debian-based systems.
|
||||
|
||||
Before running this, you will need to install `cargo-deb` and make sure you have `dpkg-deb` available on your system.
|
||||
|
||||
```bash
|
||||
# Install cargo-deb if you don't have it already
|
||||
cargo install cargo-deb
|
||||
|
||||
# Run the script to create the deb package
|
||||
./scripts/create-deb.sh
|
||||
|
||||
# Inspect the resulting deb package
|
||||
dpkg --contents target/debian/muscl_*.deb
|
||||
dpkg --info target/debian/muscl_*.deb
|
||||
```
|
||||
|
||||
The program will be built with the `release-lto` profile, so it can be a bit slower to build than a normal build.
|
||||
|
||||
## Compiling with CI
|
||||
|
||||
We have a pipeline that builds the deb package for a set of different distributions.
|
||||
|
||||
If you have access, you can trigger a build manually here: https://git.pvv.ntnu.no/Projects/muscl/actions?workflow=publish-deb.yml
|
||||
@@ -1,25 +1,25 @@
|
||||
# Development and testing
|
||||
|
||||
Ensure you have a [Rust toolchain](https://www.rust-lang.org/tools/install) installed.
|
||||
Ensure you have a [rust toolchain](https://www.rust-lang.org/tools/install) installed.
|
||||
|
||||
In order to set up a test instance of MariaDB in a docker container, run the following command:
|
||||
In order to set up a test instance of mariadb in a docker container, run the following command:
|
||||
|
||||
```bash
|
||||
docker run --rm --name mariadb -e MYSQL_ROOT_PASSWORD=secret -p 3306:3306 -d mariadb:latest
|
||||
```
|
||||
|
||||
This will start a MariaDB instance with the root password `secret`, and expose the port 3306 on the host machine.
|
||||
This will start a mariadb instance with the root password `secret`, and expose the port 3306 on the host machine.
|
||||
|
||||
Run the following command to create a configuration file with the default settings:
|
||||
|
||||
```bash
|
||||
cp ./assets/example-config.toml ./config.toml
|
||||
cp ./example-config.toml ./config.toml
|
||||
```
|
||||
|
||||
If you used the docker command above, you can use these settings as is, but if you are running MariaDB/MySQL on another host, port or with another password, adjust the corresponding fields in `config.toml`.
|
||||
If you used the docker command above, you can use these settings as is, but if you are running mariadb/mysql on another host, port or with another password, adjust the corresponding fields in `config.toml`.
|
||||
This file will contain your database password, but is ignored by git, so it will not be committed to the repository.
|
||||
|
||||
You should now be able to connect to the MariaDB instance, after building the program and using arguments to specify the config file.
|
||||
You should now be able to connect to the mariadb instance, after building the program and using arguments to specify the config file.
|
||||
|
||||
```bash
|
||||
cargo run -- --config-file ./config.toml <args>
|
||||
@@ -42,8 +42,7 @@ docker stop mariadb
|
||||
If you have nix installed, you can easily test your changes in a NixOS vm by running:
|
||||
|
||||
```bash
|
||||
nix run .#vm # Start a NixOS VM in QEMU with muscl and MariaDB installed
|
||||
nix run .#vm-mysql # Start a NixOS VM in QEMU with muscl and MySQL installed
|
||||
nix run .#vm
|
||||
```
|
||||
|
||||
You can configure the vm in `flake.nix`
|
||||
|
||||
@@ -13,14 +13,14 @@ You can install muscl by adding the [PVV apt repository][pvv-apt-repository] and
|
||||
sudo -i
|
||||
|
||||
# Check the version of your Debian installation
|
||||
VERSION_CODENAME=$(. /etc/os-release && echo "$VERSION_CODENAME")
|
||||
VERSION_CODENAME=$(lsb_release -cs)
|
||||
|
||||
# Add the repository
|
||||
echo "deb [signed-by=/etc/apt/keyrings/pvvgit-projects.asc] https://git.pvv.ntnu.no/api/packages/Projects/debian $VERSION_CODENAME main" | tee -a /etc/apt/sources.list.d/gitea.list
|
||||
|
||||
# Pull the repository key
|
||||
curl https://git.pvv.ntnu.no/api/packages/Projects/debian/repository.key -o /etc/apt/keyrings/pvvgit-projects.asc
|
||||
|
||||
# Add the repository
|
||||
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/pvvgit-projects.asc] https://git.pvv.ntnu.no/api/packages/Projects/debian $VERSION_CODENAME main" | tee -a /etc/apt/sources.list.d/pvv-git.list
|
||||
|
||||
# Update package lists
|
||||
apt update
|
||||
|
||||
@@ -28,38 +28,24 @@ apt update
|
||||
apt install muscl
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> This has been tested on Debian 12 (bookworm) and Debian 13 (trixie) at the time of writing.
|
||||
|
||||
## Creating a database user
|
||||
|
||||
In order for the daemon to be able to do anything interesting on the MySQL server, it needs
|
||||
In order for the daemon to be able to do anything interesting on the mysql server, it needs
|
||||
a database user with sufficient privileges. You can create such a user by running the following commands
|
||||
on the MySQL server as the admin user (or another user with sufficient privileges):
|
||||
on the mysql server as the admin user (or another user with sufficient privileges):
|
||||
|
||||
```sql
|
||||
CREATE USER `muscl`@`localhost` IDENTIFIED BY '<strong_password_here>';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mysql`.* TO `muscl`@`localhost`;
|
||||
GRANT GRANT OPTION, CREATE, DROP ON *.* TO `muscl`@`localhost`;
|
||||
CREATE USER `muscl`@`%` IDENTIFIED BY '<strong_password_here>';
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON `mysql`.* TO `muscl`@`%`;
|
||||
GRANT GRANT OPTION, CREATE, DROP ON *.* TO 'muscl'@'%';
|
||||
FLUSH PRIVILEGES;
|
||||
```
|
||||
|
||||
Make sure to remember the username and password, as we will now need to add them to the muscl configuration.
|
||||
Now you should add the login credentials to the muscl configuration file, typically located at `/etc/muscl/config.toml`.
|
||||
|
||||
If your MySQL server is not running on the same host as the muscl server, you will need to replace `localhost` with the appropriate hostname or IP address in the different commands above. Alternatively, you can use `'%`' to allow connections from any host, but this is not recommended.
|
||||
## Setting the myscl password with `systemd-creds`
|
||||
|
||||
The configuration already comes preconfigured expecting the database user to be named `muscl`.
|
||||
If you named it differently, please edit `/etc/muscl/muscl.conf` accordingly.
|
||||
|
||||
muscl will use the `mysql` database to manage users and databases, and the `*.*` privileges to be able to create, drop and grant privileges on arbitrary databases (restricted by the prefix system).
|
||||
|
||||
For systemd-based setups, we recommend using `systemd-creds` to provide the database password, see the section below.
|
||||
|
||||
## Setting the MySQL password ...
|
||||
|
||||
### ... with `systemd-creds`
|
||||
|
||||
The Debian package assumes that you will provide the password for `muscl`'s database user with `systemd-creds`.
|
||||
The debian package assumes that you will provide the password for `muscl`'s database user with `systemd-creds`.
|
||||
|
||||
You can add the password like this:
|
||||
|
||||
@@ -71,16 +57,12 @@ sudo -i
|
||||
mkdir -p /etc/credstore.encrypted
|
||||
systemd-creds setup
|
||||
|
||||
# Prompt for the muscl MySQL password
|
||||
read -s MUSCL_MYSQL_PASSWORD
|
||||
<... enter strong password here>
|
||||
# Be careful not to leave the password in your shell history!
|
||||
# Add a space before setting the next line to avoid this.
|
||||
export MUSCL_MYSQL_PASSWORD="<strong_password_here>"
|
||||
|
||||
# Now set the muscl MySQL password
|
||||
# Now set the muscl mysql password
|
||||
systemd-creds encrypt --name=muscl_mysql_password <(echo "$MUSCL_MYSQL_PASSWORD") /etc/credstore.encrypted/muscl_mysql_password
|
||||
|
||||
# Restart the muscl service to pick up the new credential
|
||||
systemctl daemon-reload
|
||||
systemctl restart muscl.service
|
||||
```
|
||||
|
||||
If you are running systemd older than version 254 (see `systemctl --version`), you might have to override the service to point to the path of the credential manually, because `ImportCredential=` is not supported. Run `systemctl edit muscl.service` and add the following lines:
|
||||
@@ -90,45 +72,10 @@ If you are running systemd older than version 254 (see `systemctl --version`), y
|
||||
LoadCredentialEncrypted=muscl_mysql_password:/etc/credstore.encrypted/muscl_mysql_password
|
||||
```
|
||||
|
||||
### ... without `systemd-creds`
|
||||
|
||||
If you do not have systemd, or if you do not want to use `systemd-creds`, you can also set the password in any other file on the system.
|
||||
Be careful to ensure that the file is not readable by unprivileged users, as it would yield them too much access to the MySQL server.
|
||||
Edit `/etc/muscl/muscl.conf` and set the `mysql_password_file` option below `[database]` to point to the file containing the password.
|
||||
|
||||
If you are using systemd, you should also create an override to unset the `ImportCredential=` line. Run `systemctl edit muscl.service` and add the following lines:
|
||||
|
||||
```ini
|
||||
[Service]
|
||||
ImportCredential=
|
||||
```
|
||||
|
||||
## Configuring group denylists
|
||||
|
||||
In `/etc/muscl/muscl.conf`, you will find an option below `[authorization]` named `group_denylist_file`,
|
||||
which points to `/etc/muscl/group_denylist.txt` by default.
|
||||
|
||||
In this file, you can add unix group names or GIDs to disallow the groups from being used as prefixes.
|
||||
|
||||
The deb package comes with a default denylist that disallows some common system groups.
|
||||
|
||||
The format of the file is one group name or GID per line. Lines starting with `#` and empty lines are ignored.
|
||||
|
||||
```
|
||||
# Disallow using the 'root' group as a prefix
|
||||
gid:0
|
||||
|
||||
# Disallow using the 'adm' group as a prefix
|
||||
group:adm
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> If a user is named the same as a disallowed group, that user will still be able to use their username as a prefix.
|
||||
|
||||
## A note on minimum version requirements
|
||||
|
||||
The muscl server will work with older versions of systemd, but the recommended version is 254 or newer.
|
||||
|
||||
For full landlock support (disabled by default), you need a Linux kernel version 6.7 or newer.
|
||||
For full landlock support (disabled by default), you need a linux kernel version 6.7 or newer.
|
||||
|
||||
[pvv-apt-repository]: https://git.pvv.ntnu.no/Projects/-/packages/debian/muscl
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Compatibility mode with [mysql-admutils](https://git.pvv.ntnu.no/Projects/mysql-admutils)
|
||||
|
||||
If you enable the `mysql-admutils-compatibility` feature flag when [compiling][compiling] (enabled by default for now), the output directory will contain two symlinks to the `muscl` binary: `mysql-dbadm` and `mysql-useradm`. When you run either of the symlinks, the program will enter a compatibility mode that mimics the behaviour of the corresponding program from the `mysql-admutils` package. These tools try to replicate the behaviour of the original programs as closely as possible.
|
||||
If you enable the feature flag `mysql-admutils-compatibility` (enabled by default for now), the output directory will contain two symlinks to the `musl` binary: `mysql-dbadm` and `mysql-useradm`. When invoked through these symlinks, the binary will react to its `argv[0]` and behave accordingly. These modes strive to behave as similar as possible to the original programs.
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
@@ -8,32 +8,21 @@ cargo build
|
||||
./target/debug/mysql-useradm --help
|
||||
```
|
||||
|
||||
These symlinks are also included in the deb packages by default.
|
||||
These symlinks are also included in the deb packages.
|
||||
|
||||
### Known deviations from `mysql-admutils`' behaviour
|
||||
|
||||
There are some differences between the original programs and the compatibility mode in `muscl`.
|
||||
The known ones are:
|
||||
|
||||
- `--help` output is formatted by clap in a different style.
|
||||
- `mysql-dbadm edit-perm` uses the new privilege editor implementation. The formatting that
|
||||
was used in `mysql-admutils` is no longer present. However, since the editor is purely an
|
||||
interactive tool, there shouldn't have been any scripts relying on the old formatting.
|
||||
- The configuration file is shared for all variants of the program, and `muscl` will use
|
||||
its new logic to look for and parse this file. See the example config and
|
||||
[installation instructions][installation-instructions] for more information about how to
|
||||
configure the software.
|
||||
- The order in which input is validated might be differ from the original
|
||||
(e.g. database ownership checks, invalid character checks, existence checks, ...).
|
||||
This means that running the exact same command might lead to different error messages.
|
||||
- Command-line arguments are de-duplicated. For example, if the user runs
|
||||
- `mysql-dbadm edit-perm` uses the new privilege editor implementation. Replicating
|
||||
the old behaviour
|
||||
there shoulnd't have been any (or at least very few) scripts relying on the old
|
||||
command API or behavior.
|
||||
- The new tools use the new implementation to find it's configuration file, and uses the
|
||||
new configuration format. See the example config and installation instructions for more
|
||||
information about how to configure the software.
|
||||
- The order in which input is validated (e.g. whether you own a user, whether the
|
||||
contains illegal characters, whether the user does or does not exist) might be different
|
||||
from the original program, leading to the same command reporting different errors.
|
||||
- Arguments are de-duplicated, meaning that if you run something like
|
||||
`mysql-dbadm create user_db1 user_db2 user_db1`, the program will only try to create
|
||||
the `user_db1` once. The old program would have attempted to create it twice,
|
||||
failing the second attempt.
|
||||
|
||||
One detail that might be considered a difference but, is that the compatibility mode supports
|
||||
command line completions when the user presses tab. This is not a feature of the original programs,
|
||||
but it does not change any of the previous behaviour either.
|
||||
|
||||
[compiling]: ./compiling.md
|
||||
[installation-instructions]: ./installation.md
|
||||
the `user_db1` once. The old program would attempt to create it twice, failing the second time.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Use with NixOS
|
||||
|
||||
For NixOS, there is a NixOS module available in the nix flake. You can include it in your configuration like this:
|
||||
For NixOS, there is a module available via the nix flake. You can include it in your configuration like this:
|
||||
|
||||
```nix
|
||||
{
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
# (see `systemctl status muscl.socket`)
|
||||
socket_path = "/run/muscl/muscl.sock"
|
||||
|
||||
[authorization]
|
||||
group_denylist_file = "/etc/muscl/group_denylist.txt"
|
||||
|
||||
[mysql]
|
||||
|
||||
# Hostname and port of the database.
|
||||
0
examples/auth_daemon_python/README.md
Normal file
0
examples/auth_daemon_python/README.md
Normal file
53
examples/auth_daemon_python/muscl-auth-daemon.service
Normal file
53
examples/auth_daemon_python/muscl-auth-daemon.service
Normal file
@@ -0,0 +1,53 @@
|
||||
[Unit]
|
||||
Description=Authorization daemon for Muscl
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/local/bin/muscl_auth_daemon.py
|
||||
|
||||
# WatchdogSec=15
|
||||
|
||||
User=muscl
|
||||
Group=muscl
|
||||
DynamicUser=yes
|
||||
|
||||
; ConfigurationDirectory=muscl
|
||||
; RuntimeDirectory=muscl
|
||||
|
||||
; # This is required to read unix user/group details.
|
||||
; PrivateUsers=false
|
||||
|
||||
; # Needed to communicate with MySQL.
|
||||
; PrivateNetwork=false
|
||||
; PrivateIPC=false
|
||||
|
||||
; AmbientCapabilities=
|
||||
; CapabilityBoundingSet=
|
||||
; DeviceAllow=
|
||||
; DevicePolicy=closed
|
||||
; LockPersonality=true
|
||||
; MemoryDenyWriteExecute=true
|
||||
; NoNewPrivileges=true
|
||||
; PrivateDevices=true
|
||||
; PrivateMounts=true
|
||||
; PrivateTmp=yes
|
||||
; ProcSubset=pid
|
||||
; ProtectClock=true
|
||||
; ProtectControlGroups=strict
|
||||
; ProtectHome=true
|
||||
; ProtectHostname=true
|
||||
; ProtectKernelLogs=true
|
||||
; ProtectKernelModules=true
|
||||
; ProtectKernelTunables=true
|
||||
; ProtectProc=invisible
|
||||
; ProtectSystem=strict
|
||||
; RemoveIPC=true
|
||||
; RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
; RestrictNamespaces=true
|
||||
; RestrictRealtime=true
|
||||
; RestrictSUIDSGID=true
|
||||
; SocketBindDeny=any
|
||||
; SystemCallArchitectures=native
|
||||
; SystemCallFilter=@system-service
|
||||
; SystemCallFilter=~@privileged @resources
|
||||
; UMask=0777
|
||||
8
examples/auth_daemon_python/muscl-auth-daemon.socket
Normal file
8
examples/auth_daemon_python/muscl-auth-daemon.socket
Normal file
@@ -0,0 +1,8 @@
|
||||
[Unit]
|
||||
Description=Authorization daemon for Muscl
|
||||
WantedBy=sockets.target
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/muscl/muscl-auth-daemon.socket
|
||||
Accept=no
|
||||
SocketMode=0660
|
||||
84
examples/auth_daemon_python/muscl_auth_daemon.py
Normal file
84
examples/auth_daemon_python/muscl_auth_daemon.py
Normal file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# TODO: create pool of workers to handle requests concurrently
|
||||
# the socket should be a listener socket and each worker should accept connections from it
|
||||
# the socket should accept requests as newline-separated JSON objects
|
||||
# there should be a watchdog to monitor worker health and restart them if they die
|
||||
# graceful shutdown should be implemented for the workers
|
||||
# optional logging of requests and responses
|
||||
# use systemd notify to signal readiness and amount of connections handled
|
||||
|
||||
import json
|
||||
import os
|
||||
from socket import AF_UNIX, SOCK_DGRAM, SOCK_STREAM, fromfd, socket
|
||||
from multiprocessing import Pool
|
||||
|
||||
|
||||
def get_listener_from_systemd() -> socket:
|
||||
listen_fds = int(os.getenv("LISTEN_FDS", "0"))
|
||||
listen_pid = int(os.getenv("LISTEN_PID", "0"))
|
||||
if listen_fds != 1 or listen_pid != os.getpid():
|
||||
raise RuntimeError("No socket passed from systemd")
|
||||
assert listen_fds == 1
|
||||
sock = fromfd(3, AF_UNIX, SOCK_STREAM)
|
||||
sock.setblocking(False)
|
||||
return sock
|
||||
|
||||
|
||||
def get_notify_socket_from_systemd() -> socket:
|
||||
notify_socket_path = os.getenv("NOTIFY_SOCKET")
|
||||
if not notify_socket_path:
|
||||
raise RuntimeError("No notify socket path found in environment")
|
||||
sock = socket(AF_UNIX, SOCK_DGRAM)
|
||||
sock.connect(notify_socket_path)
|
||||
return sock
|
||||
|
||||
|
||||
def run_auth_daemon(sock: socket):
|
||||
sock.listen()
|
||||
print("Auth daemon is running and listening for connections...")
|
||||
with Pool() as worker_pool:
|
||||
with get_notify_socket_from_systemd() as notify_socket:
|
||||
notify_socket.sendall(b"READY=1\n")
|
||||
while True:
|
||||
conn, _ = sock.accept()
|
||||
worker_pool.apply_async(session_handler, args=(conn,))
|
||||
|
||||
|
||||
def session_handler(sock: socket):
|
||||
buffer = ""
|
||||
while True:
|
||||
data = sock.recv(4096).decode("utf-8")
|
||||
if not data:
|
||||
print("Connection closed by client")
|
||||
break
|
||||
buffer += data
|
||||
if buffer.endswith("\n"):
|
||||
requests = buffer.strip().split("\n")
|
||||
buffer = ""
|
||||
for request in requests:
|
||||
try:
|
||||
req_json = json.loads(request)
|
||||
username = req_json.get("username", "")
|
||||
groups = req_json.get("groups", [])
|
||||
resource_type = req_json.get("resource_type", "")
|
||||
resource = req_json.get("resource", "")
|
||||
allowed = process_request(username, groups, resource_type, resource)
|
||||
response = {"allowed": allowed}
|
||||
except json.JSONDecodeError:
|
||||
response = {"error": "Invalid JSON"}
|
||||
sock.sendall((json.dumps(response) + "\n").encode("utf-8"))
|
||||
|
||||
|
||||
def process_request(
|
||||
username: str,
|
||||
groups: list[str],
|
||||
resource_type: str,
|
||||
resource: str,
|
||||
) -> bool:
|
||||
...
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
listener_socket = get_listener_from_systemd()
|
||||
run_auth_daemon(listener_socket)
|
||||
18
flake.lock
generated
18
flake.lock
generated
@@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1766194365,
|
||||
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
|
||||
"lastModified": 1765739568,
|
||||
"narHash": "sha256-gQYx35Of4UDKUjAYvmxjUEh/DdszYeTtT6MDin4loGE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
|
||||
"rev": "67d2baff0f9f677af35db61b32b5df6863bcc075",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -17,11 +17,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1766309749,
|
||||
"narHash": "sha256-3xY8CZ4rSnQ0NqGhMKAy5vgC+2IVK0NoVEzDoOh4DA4=",
|
||||
"lastModified": 1765472234,
|
||||
"narHash": "sha256-9VvC20PJPsleGMewwcWYKGzDIyjckEz8uWmT0vCDYK0=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a6531044f6d0bef691ea18d4d4ce44d0daa6e816",
|
||||
"rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -45,11 +45,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1766457837,
|
||||
"narHash": "sha256-aeBbkQ0HPFNOIsUeEsXmZHXbYq4bG8ipT9JRlCcKHgU=",
|
||||
"lastModified": 1765680428,
|
||||
"narHash": "sha256-fyPmRof9SZeI14ChPk5rVPOm7ISiiGkwGCunkhM+eUg=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "2c7510a559416d07242621d036847152d970612b",
|
||||
"rev": "eb3898d8ef143d4bf0f7f2229105fc51c7731b2f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
fileset = lib.fileset.unions [
|
||||
(craneLib.fileset.commonCargoSources ./.)
|
||||
./assets
|
||||
./examples
|
||||
];
|
||||
};
|
||||
in {
|
||||
|
||||
@@ -84,7 +84,10 @@ buildFunction ({
|
||||
install -Dm444 assets/systemd/muscl.socket -t "$out/lib/systemd/system"
|
||||
install -Dm644 assets/systemd/muscl.service -t "$out/lib/systemd/system"
|
||||
substituteInPlace "$out/lib/systemd/system/muscl.service" \
|
||||
--replace-fail '/usr/bin/muscl-server' "$out/bin/muscl-server"
|
||||
--replace-fail '/usr/bin/muscl' "$out/bin/muscl"
|
||||
|
||||
mkdir -p "$out/share/muscl"
|
||||
cp -r examples "$out/share/muscl"
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
|
||||
112
nix/module.nix
112
nix/module.nix
@@ -27,6 +27,31 @@ in
|
||||
}.${level};
|
||||
};
|
||||
|
||||
authHandler = lib.mkOption {
|
||||
type = with lib.types; nullOr lines;
|
||||
default = null;
|
||||
description = "Custom authentication handler, written in python";
|
||||
example = ''
|
||||
def process_request(
|
||||
username: str,
|
||||
groups: list[str],
|
||||
resource_type: str,
|
||||
resource: str,
|
||||
) -> bool:
|
||||
if resource_type == "database":
|
||||
if resource.startswith(username) or any(
|
||||
resource.startswith(group) for group in groups
|
||||
):
|
||||
return True
|
||||
elif resource_type == "user":
|
||||
if resource.startswith(username) or any(
|
||||
resource.startswith(group) for group in groups
|
||||
):
|
||||
return True
|
||||
return False
|
||||
'';
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
default = { };
|
||||
type = lib.types.submodule {
|
||||
@@ -40,14 +65,6 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
authorization = {
|
||||
group_denylist = lib.mkOption {
|
||||
type = with lib.types; nullOr (listOf str);
|
||||
default = [ "wheel" ];
|
||||
description = "List of groups that are denied access";
|
||||
};
|
||||
};
|
||||
|
||||
mysql = {
|
||||
socket_path = lib.mkOption {
|
||||
type = with lib.types; nullOr path;
|
||||
@@ -89,12 +106,6 @@ in
|
||||
environment.systemPackages = [ cfg.package ];
|
||||
|
||||
environment.etc."muscl/config.toml".source = lib.pipe cfg.settings [
|
||||
# Handle group_denylist_file
|
||||
(conf: lib.recursiveUpdate conf {
|
||||
authorization.group_denylist_file = if (conf.authorization.group_denylist != [ ]) then "/etc/muscl/group-denylist" else null;
|
||||
authorization.group_denylist = null;
|
||||
})
|
||||
|
||||
# Remove nulls
|
||||
(lib.filterAttrsRecursive (_: v: v != null))
|
||||
|
||||
@@ -109,10 +120,6 @@ in
|
||||
(format.generate "muscl.conf")
|
||||
];
|
||||
|
||||
environment.etc."muscl/group-denylist" = lib.mkIf (cfg.settings.authorization.group_denylist != [ ]) {
|
||||
text = lib.concatMapStringsSep "\n" (group: "group:${group}") cfg.settings.authorization.group_denylist;
|
||||
};
|
||||
|
||||
services.mysql.ensureUsers = lib.mkIf cfg.createLocalDatabaseUser [
|
||||
{
|
||||
name = cfg.settings.mysql.username;
|
||||
@@ -132,7 +139,7 @@ in
|
||||
serviceConfig = {
|
||||
ExecStart = [
|
||||
""
|
||||
"${lib.getExe' cfg.package "muscl-server"} ${cfg.logLevel} --systemd --disable-landlock socket-activate"
|
||||
"${lib.getExe cfg.package} ${cfg.logLevel} server --systemd --disable-landlock socket-activate"
|
||||
];
|
||||
|
||||
ExecReload = [
|
||||
@@ -167,5 +174,72 @@ in
|
||||
++ (lib.optionals (cfg.settings.mysql.host != null) [ "AF_INET" "AF_INET6" ]);
|
||||
};
|
||||
};
|
||||
|
||||
systemd.sockets."muscl-auth-daemon" = lib.mkIf (cfg.authHandler != null) {
|
||||
description = "Authorization daemon for Muscl";
|
||||
wantedBy = [ "sockets.target" ];
|
||||
socketConfig = {
|
||||
ListenStream = "/run/muscl/muscl-auth-daemon.sock";
|
||||
Accept = "no";
|
||||
};
|
||||
};
|
||||
|
||||
systemd.services."muscl-auth-daemon" = lib.mkIf (cfg.authHandler != null) {
|
||||
description = "Authorization daemon for Muscl";
|
||||
requires = [ "muscl-auth-daemon.socket" ];
|
||||
serviceConfig = {
|
||||
Type = "notify";
|
||||
ExecStart = let
|
||||
authScript = lib.pipe ../examples/auth_daemon_python/muscl_auth_daemon.py [
|
||||
lib.fileContents
|
||||
(lib.replaceString ''
|
||||
def process_request(
|
||||
username: str,
|
||||
groups: list[str],
|
||||
resource_type: str,
|
||||
resource: str,
|
||||
) -> bool:
|
||||
...
|
||||
'' cfg.authHandler)
|
||||
(pkgs.writers.writePyPy3Bin "muscl-auth-handler.py" { })
|
||||
];
|
||||
in lib.getExe authScript;
|
||||
|
||||
User = "muscl-auth-daemon";
|
||||
Group = "muscl-auth-daemon";
|
||||
DynamicUser = true;
|
||||
|
||||
AmbientCapabilities = [ "" ];
|
||||
CapabilityBoundingSet = [ "" ];
|
||||
DeviceAllow = [ "" ];
|
||||
LockPersonality = true;
|
||||
NoNewPrivileges = true;
|
||||
PrivateDevices = true;
|
||||
PrivateMounts = true;
|
||||
PrivateTmp = "yes";
|
||||
ProcSubset = "pid";
|
||||
ProtectClock = true;
|
||||
ProtectControlGroups = "strict";
|
||||
ProtectHome = true;
|
||||
ProtectHostname = true;
|
||||
ProtectKernelLogs = true;
|
||||
ProtectKernelModules = true;
|
||||
ProtectKernelTunables = true;
|
||||
ProtectProc = "invisible";
|
||||
ProtectSystem = "strict";
|
||||
RemoveIPC = true;
|
||||
UMask = "0777";
|
||||
RestrictNamespaces = true;
|
||||
RestrictRealtime = true;
|
||||
RestrictSUIDSGID = true;
|
||||
SystemCallArchitectures = "native";
|
||||
SocketBindDeny = [ "any" ];
|
||||
SystemCallFilter = [
|
||||
"@system-service"
|
||||
"~@privileged"
|
||||
"~@resources"
|
||||
];
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@@ -56,6 +56,25 @@ nixpkgs.lib.nixosSystem {
|
||||
enable = true;
|
||||
logLevel = "trace";
|
||||
createLocalDatabaseUser = true;
|
||||
authHandler = ''
|
||||
def process_request(
|
||||
username: str,
|
||||
groups: list[str],
|
||||
resource_type: str,
|
||||
resource: str,
|
||||
) -> bool:
|
||||
if resource_type == "database":
|
||||
if resource.startswith(username) or any(
|
||||
resource.startswith(group) for group in groups
|
||||
):
|
||||
return True
|
||||
elif resource_type == "user":
|
||||
if resource.startswith(username) or any(
|
||||
resource.startswith(group) for group in groups
|
||||
):
|
||||
return True
|
||||
return False
|
||||
'';
|
||||
};
|
||||
|
||||
programs.vim = {
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${CREATE_DEB_DEBUG:-}" == "1" ]]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
declare -a COMMANDS=(
|
||||
curl
|
||||
unzip
|
||||
mktemp
|
||||
find
|
||||
)
|
||||
|
||||
for cmd in "${COMMANDS[@]}"; do
|
||||
if ! command -v "$cmd" &> /dev/null; then
|
||||
echo "$cmd could not be found" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo "Usage: $0 <gitea-run-number> <git-sha>" >&2
|
||||
echo "Example:" >&2
|
||||
echo " GITEA_USER=me GITEA_TOKEN=secret ./scripts/download-and-upload-debs.sh 123 \$(git rev-parse HEAD)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${GITEA_USER:-}" ]; then
|
||||
echo "GITEA_USER is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
echo "GITEA_TOKEN is not set" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
declare -r RUN_NUMBER="$1"
|
||||
declare -r GIT_SHA="$2"
|
||||
|
||||
TMPDIR="$(mktemp -d)"
|
||||
|
||||
for variant in debian-bookworm debian-trixie ubuntu-jammy ubuntu-noble; do
|
||||
echo "Downloading and uploading debs for variant: $variant"
|
||||
curl "https://git.pvv.ntnu.no/Projects/muscl/actions/runs/$RUN_NUMBER/artifacts/muscl-deb-$variant-$GIT_SHA.zip" --output "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip"
|
||||
|
||||
unzip "$TMPDIR/muscl-deb-$variant-$GIT_SHA.zip" -d "$TMPDIR/muscl-deb-$variant-$GIT_SHA"
|
||||
|
||||
DISTRO_VERSION_NAME="$(echo "$variant" | cut -d'-' -f2)"
|
||||
|
||||
DEB_NAME=$(find "$TMPDIR/muscl-deb-$variant-$GIT_SHA"/*.deb -print0 | xargs -0 -n1 basename | cut -d'_' -f1 | head -n1)
|
||||
DEB_VERSION=$(find "$TMPDIR/muscl-deb-$variant-$GIT_SHA"/*.deb -print0 | xargs -0 -n1 basename | cut -d'_' -f2 | head -n1)
|
||||
DEB_ARCH=$(find "$TMPDIR/muscl-deb-$variant-$GIT_SHA"/*.deb -print0 | xargs -0 -n1 basename | cut -d'_' -f3 | cut -d'.' -f1 | head -n1)
|
||||
|
||||
curl \
|
||||
-X DELETE \
|
||||
--user "$GITEA_USER:$GITEA_TOKEN" \
|
||||
"https://git.pvv.ntnu.no/api/packages/Projects/debian/pool/$DISTRO_VERSION_NAME/main/$DEB_NAME/$DEB_VERSION/$DEB_ARCH"
|
||||
|
||||
curl \
|
||||
-X PUT \
|
||||
--user "$GITEA_USER:$GITEA_TOKEN" \
|
||||
--upload-file "$TMPDIR/muscl-deb-$variant-$GIT_SHA/${DEB_NAME}_${DEB_VERSION}_${DEB_ARCH}.deb" \
|
||||
"https://git.pvv.ntnu.no/api/packages/Projects/debian/pool/$DISTRO_VERSION_NAME/main/upload"
|
||||
done
|
||||
|
||||
rm -rf "$TMPDIR"
|
||||
@@ -24,56 +24,149 @@ pub use show_privs::*;
|
||||
pub use show_user::*;
|
||||
pub use unlock_user::*;
|
||||
|
||||
use futures_util::SinkExt;
|
||||
use itertools::Itertools;
|
||||
use tokio_stream::StreamExt;
|
||||
use clap::Subcommand;
|
||||
|
||||
use crate::core::protocol::{ClientToServerMessageStream, Request, Response};
|
||||
use crate::core::protocol::{ClientToServerMessageStream, Response};
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
#[command(subcommand_required = true)]
|
||||
pub enum ClientCommand {
|
||||
/// Check whether you are authorized to manage the specified databases or users.
|
||||
CheckAuth(CheckAuthArgs),
|
||||
|
||||
/// Create one or more databases
|
||||
CreateDb(CreateDbArgs),
|
||||
|
||||
/// Delete one or more databases
|
||||
DropDb(DropDbArgs),
|
||||
|
||||
/// Print information about one or more databases
|
||||
///
|
||||
/// If no database name is provided, all databases you have access will be shown.
|
||||
ShowDb(ShowDbArgs),
|
||||
|
||||
/// Print user privileges for one or more databases
|
||||
///
|
||||
/// If no database names are provided, all databases you have access to will be shown.
|
||||
ShowPrivs(ShowPrivsArgs),
|
||||
|
||||
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
||||
///
|
||||
/// This command has two modes of operation:
|
||||
///
|
||||
/// 1. Interactive mode: If nothing else is specified, the user will be prompted to edit the privileges using a text editor.
|
||||
///
|
||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||
///
|
||||
/// Follow the instructions inside the editor for more information.
|
||||
///
|
||||
/// 2. Non-interactive mode: If the `-p` flag is specified, the user can write privileges using arguments.
|
||||
///
|
||||
/// The privilege arguments should be formatted as `<db>:<user>:<op><privileges>`
|
||||
/// where the privileges are a string of characters, each representing a single privilege.
|
||||
/// The character `A` is an exception - it represents all privileges.
|
||||
///
|
||||
/// The `<op>` character is optional and can be either `+` to grant additional privileges
|
||||
/// or `-` to revoke privileges. If omitted, the privileges will be set exactly as specified,
|
||||
/// removing any privileges not listed, and adding any that are.
|
||||
///
|
||||
/// The character-to-privilege mapping is defined as follows:
|
||||
///
|
||||
/// - `s` - SELECT
|
||||
/// - `i` - INSERT
|
||||
/// - `u` - UPDATE
|
||||
/// - `d` - DELETE
|
||||
/// - `c` - CREATE
|
||||
/// - `D` - DROP
|
||||
/// - `a` - ALTER
|
||||
/// - `I` - INDEX
|
||||
/// - `t` - CREATE TEMPORARY TABLES
|
||||
/// - `l` - LOCK TABLES
|
||||
/// - `r` - REFERENCES
|
||||
/// - `A` - ALL PRIVILEGES
|
||||
///
|
||||
/// If you provide a database name, you can omit it from the privilege string,
|
||||
/// e.g. `edit-privs my_db -p my_user:siu` is equivalent to `edit-privs -p my_db:my_user:siu`.
|
||||
/// While it doesn't make much of a difference for a single edit, it can be useful for editing multiple users
|
||||
/// on the same database at once.
|
||||
///
|
||||
/// Example usage of non-interactive mode:
|
||||
///
|
||||
/// Enable privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`:
|
||||
///
|
||||
/// `muscl edit-privs -p my_db:my_user:siu`
|
||||
///
|
||||
/// Enable all privileges for user `my_other_user` on database `my_other_db`:
|
||||
///
|
||||
/// `muscl edit-privs -p my_other_db:my_other_user:A`
|
||||
///
|
||||
/// Set miscellaneous privileges for multiple users on database `my_db`:
|
||||
///
|
||||
/// `muscl edit-privs my_db -p my_user:siu my_other_user:ct``
|
||||
///
|
||||
/// Add the `DELETE` privilege for user `my_user` on database `my_db`:
|
||||
///
|
||||
/// `muscl edit-privs my_db -p my_user:+d
|
||||
///
|
||||
#[command(verbatim_doc_comment)]
|
||||
EditPrivs(EditPrivsArgs),
|
||||
|
||||
/// Create one or more users
|
||||
CreateUser(CreateUserArgs),
|
||||
|
||||
/// Delete one or more users
|
||||
DropUser(DropUserArgs),
|
||||
|
||||
/// Change the MySQL password for a user
|
||||
PasswdUser(PasswdUserArgs),
|
||||
|
||||
/// Print information about one or more users
|
||||
///
|
||||
/// If no username is provided, all users you have access will be shown.
|
||||
ShowUser(ShowUserArgs),
|
||||
|
||||
/// Lock account for one or more users
|
||||
LockUser(LockUserArgs),
|
||||
|
||||
/// Unlock account for one or more users
|
||||
UnlockUser(UnlockUserArgs),
|
||||
}
|
||||
|
||||
pub async fn handle_command(
|
||||
command: ClientCommand,
|
||||
server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
match command {
|
||||
ClientCommand::CheckAuth(args) => check_authorization(args, server_connection).await,
|
||||
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
||||
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
||||
ClientCommand::EditPrivs(args) => edit_database_privileges(args, server_connection).await,
|
||||
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
||||
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
||||
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
||||
ClientCommand::ShowUser(args) => show_users(args, server_connection).await,
|
||||
ClientCommand::LockUser(args) => lock_users(args, server_connection).await,
|
||||
ClientCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle an unexpected or erroneous response from the server.
|
||||
///
|
||||
/// This function checks the provided response and returns an appropriate error message.
|
||||
/// It is typically used in `match` branches for expecting a specific response type from the server.
|
||||
pub fn erroneous_server_response(
|
||||
response: Option<Result<Response, std::io::Error>>,
|
||||
) -> anyhow::Result<()> {
|
||||
match response {
|
||||
Some(Ok(Response::Error(e))) => {
|
||||
anyhow::bail!("Server returned error: {e}");
|
||||
anyhow::bail!("Server returned error: {}", e);
|
||||
}
|
||||
Some(Err(e)) => {
|
||||
anyhow::bail!(e);
|
||||
}
|
||||
Some(response) => {
|
||||
anyhow::bail!("Unexpected response from server: {response:?}");
|
||||
anyhow::bail!("Unexpected response from server: {:?}", response);
|
||||
}
|
||||
None => {
|
||||
anyhow::bail!("No response from server");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Print a hint about which name prefixes the user is authorized to manage
|
||||
/// by querying the server for valid name prefixes.
|
||||
///
|
||||
/// This function should be used when an authorization error occurs,
|
||||
/// to help the user understand which databases or users they are allowed to manage.
|
||||
async fn print_authorization_owner_hint(
|
||||
server_connection: &mut ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
server_connection
|
||||
.send(Request::ListValidNamePrefixes)
|
||||
.await?;
|
||||
|
||||
let response = match server_connection.next().await {
|
||||
Some(Ok(Response::ListValidNamePrefixes(prefixes))) => prefixes,
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
eprintln!(
|
||||
"Note: You are allowed to manage databases and users with the following prefixes:\n{}",
|
||||
response.into_iter().map(|p| format!(" - {p}")).join("\n")
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ use tokio_stream::StreamExt;
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct CheckAuthArgs {
|
||||
/// The `MySQL` database(s) or user(s) to check authorization for
|
||||
/// The MySQL database(s) or user(s) to check authorization for
|
||||
#[arg(num_args = 1.., value_name = "NAME")]
|
||||
name: Vec<String>,
|
||||
|
||||
@@ -63,9 +63,5 @@ pub async fn check_authorization(
|
||||
print_check_authorization_output_status(&result);
|
||||
}
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::prefix_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, CreateDatabaseError, Request, Response,
|
||||
print_create_databases_output_status, print_create_databases_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_create_databases_output_status,
|
||||
print_create_databases_output_status_json,
|
||||
},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
@@ -18,9 +15,8 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct CreateDbArgs {
|
||||
/// The `MySQL` database(s) to create
|
||||
/// The MySQL database(s) to create
|
||||
#[arg(num_args = 1.., value_name = "DB_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(prefix_completer)))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
|
||||
/// Print the information as JSON
|
||||
@@ -36,7 +32,7 @@ pub async fn create_databases(
|
||||
anyhow::bail!("No database names provided");
|
||||
}
|
||||
|
||||
let message = Request::CreateDatabases(args.name.clone());
|
||||
let message = Request::CreateDatabases(args.name.to_owned());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
@@ -44,27 +40,12 @@ pub async fn create_databases(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_create_databases_output_status_json(&result);
|
||||
} else {
|
||||
print_create_databases_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(CreateDatabaseError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,21 +1,14 @@
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::Confirm;
|
||||
use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{
|
||||
erroneous_server_response, interactive_password_dialogue_with_double_check,
|
||||
interactive_password_expiry_dialogue, print_authorization_owner_hint,
|
||||
},
|
||||
client::commands::{erroneous_server_response, read_password_from_stdin_with_double_check},
|
||||
core::{
|
||||
completion::prefix_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, CreateUserError, Request, Response,
|
||||
SetUserPasswordRequest, print_create_users_output_status,
|
||||
ClientToServerMessageStream, Request, Response, print_create_users_output_status,
|
||||
print_create_users_output_status_json, print_set_password_output_status,
|
||||
request_validation::ValidationError,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -23,9 +16,8 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct CreateUserArgs {
|
||||
/// The `MySQL` user(s) to create
|
||||
/// The MySQL user(s) to create
|
||||
#[arg(num_args = 1.., value_name = "USER_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(prefix_completer)))]
|
||||
username: Vec<MySQLUser>,
|
||||
|
||||
/// Do not ask for a password, leave it unset
|
||||
@@ -47,7 +39,7 @@ pub async fn create_users(
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let message = Request::CreateUsers(args.username.clone());
|
||||
let message = Request::CreateUsers(args.username.to_owned());
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
||||
@@ -63,39 +55,23 @@ pub async fn create_users(
|
||||
} else {
|
||||
print_create_users_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(CreateUserError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
|
||||
let successfully_created_users = result
|
||||
.iter()
|
||||
.filter_map(|(username, result)| result.as_ref().ok().map(|()| username))
|
||||
.filter_map(|(username, result)| result.as_ref().ok().map(|_| username))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for username in successfully_created_users {
|
||||
if !args.no_password
|
||||
&& Confirm::new()
|
||||
.with_prompt(format!(
|
||||
"Do you want to set a password for user '{username}'?"
|
||||
"Do you want to set a password for user '{}'?",
|
||||
username
|
||||
))
|
||||
.default(false)
|
||||
.interact()?
|
||||
{
|
||||
let password = interactive_password_dialogue_with_double_check(username)?;
|
||||
let expiry = interactive_password_expiry_dialogue(username)?;
|
||||
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: username.clone(),
|
||||
new_password: Some(password),
|
||||
expiry: expiry,
|
||||
});
|
||||
let password = read_password_from_stdin_with_double_check(username)?;
|
||||
let message = Request::PasswdUser((username.to_owned(), password));
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
@@ -104,7 +80,7 @@ pub async fn create_users(
|
||||
|
||||
match server_connection.next().await {
|
||||
Some(Ok(Response::SetUserPassword(result))) => {
|
||||
print_set_password_output_status(&result, username);
|
||||
print_set_password_output_status(&result, username)
|
||||
}
|
||||
response => return erroneous_server_response(response),
|
||||
}
|
||||
@@ -116,9 +92,5 @@ pub async fn create_users(
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, DropDatabaseError, Request, Response,
|
||||
print_drop_databases_output_status, print_drop_databases_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_drop_databases_output_status,
|
||||
print_drop_databases_output_status_json,
|
||||
},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
@@ -19,7 +18,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropDbArgs {
|
||||
/// The `MySQL` database(s) to drop
|
||||
/// The MySQL database(s) to drop
|
||||
#[arg(num_args = 1.., value_name = "DB_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
@@ -47,22 +46,19 @@ pub async fn drop_databases(
|
||||
"Are you sure you want to drop the databases?\n\n{}\n\nThis action cannot be undone",
|
||||
args.name
|
||||
.iter()
|
||||
.map(|d| format!("- {d}"))
|
||||
.map(|d| format!("- {}", d))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
))
|
||||
.interact()?;
|
||||
|
||||
//
|
||||
if !confirmation {
|
||||
// TODO: should we return with an error code here?
|
||||
println!("Aborting drop operation.");
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let message = Request::DropDatabases(args.name.clone());
|
||||
let message = Request::DropDatabases(args.name.to_owned());
|
||||
server_connection.send(message).await?;
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
@@ -70,28 +66,13 @@ pub async fn drop_databases(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_drop_databases_output_status_json(&result);
|
||||
} else {
|
||||
print_drop_databases_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(DropDatabaseError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -5,13 +5,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, DropUserError, Request, Response,
|
||||
print_drop_users_output_status, print_drop_users_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_drop_users_output_status,
|
||||
print_drop_users_output_status_json,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -19,7 +18,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct DropUserArgs {
|
||||
/// The `MySQL` user(s) to drop
|
||||
/// The MySQL user(s) to drop
|
||||
#[arg(num_args = 1.., value_name = "USER_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
username: Vec<MySQLUser>,
|
||||
@@ -47,21 +46,19 @@ pub async fn drop_users(
|
||||
"Are you sure you want to drop the users?\n\n{}\n\nThis action cannot be undone",
|
||||
args.username
|
||||
.iter()
|
||||
.map(|d| format!("- {d}"))
|
||||
.map(|d| format!("- {}", d))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
))
|
||||
.interact()?;
|
||||
|
||||
if !confirmation {
|
||||
// TODO: should we return with an error code here?
|
||||
println!("Aborting drop operation.");
|
||||
server_connection.send(Request::Exit).await?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let message = Request::DropUsers(args.username.clone());
|
||||
let message = Request::DropUsers(args.username.to_owned());
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
@@ -73,27 +70,12 @@ pub async fn drop_users(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_drop_users_output_status_json(&result);
|
||||
} else {
|
||||
print_drop_users_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(DropUserError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{Args, Parser};
|
||||
use clap::Parser;
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use dialoguer::{Confirm, Editor};
|
||||
use futures_util::SinkExt;
|
||||
@@ -9,19 +9,18 @@ use nix::unistd::{User, getuid};
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::{mysql_database_completer, mysql_user_completer},
|
||||
completion::mysql_database_completer,
|
||||
database_privileges::{
|
||||
DatabasePrivilegeEdit, DatabasePrivilegeEditEntry, DatabasePrivilegeRow,
|
||||
DatabasePrivilegeRowDiff, DatabasePrivilegesDiff, create_or_modify_privilege_rows,
|
||||
diff_privileges, display_privilege_diffs, generate_editor_content_from_privilege_data,
|
||||
DatabasePrivilegeEditEntry, DatabasePrivilegeRow, DatabasePrivilegeRowDiff,
|
||||
DatabasePrivilegesDiff, create_or_modify_privilege_rows, diff_privileges,
|
||||
display_privilege_diffs, generate_editor_content_from_privilege_data,
|
||||
parse_privilege_data_from_editor_content, reduce_privilege_diffs,
|
||||
},
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListDatabasesError, ListUsersError,
|
||||
ModifyDatabasePrivilegesError, Request, Response,
|
||||
print_modify_database_privileges_output_status, request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response,
|
||||
print_modify_database_privileges_output_status,
|
||||
},
|
||||
types::{MySQLDatabase, MySQLUser},
|
||||
},
|
||||
@@ -29,24 +28,20 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct EditPrivsArgs {
|
||||
/// The privileges to set, grant or revoke, in the format `DATABASE:USER:[+-]PRIVILEGES`
|
||||
///
|
||||
/// This option allows for changing privileges for multiple databases and users in batch.
|
||||
///
|
||||
/// This can not be used together with the positional `DB_NAME`, `USER_NAME` and `PRIVILEGES` arguments.
|
||||
/// The MySQL database to edit privileges for
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||
#[arg(value_name = "DB_NAME")]
|
||||
pub name: Option<MySQLDatabase>,
|
||||
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "DB_NAME:USER_NAME:[+-]PRIVILEGES",
|
||||
value_name = "[DATABASE:]USER:[+-]PRIVILEGES",
|
||||
num_args = 0..,
|
||||
value_parser = DatabasePrivilegeEditEntry::parse_from_str,
|
||||
conflicts_with("single_priv"),
|
||||
)]
|
||||
pub privs: Vec<DatabasePrivilegeEditEntry>,
|
||||
|
||||
#[command(flatten)]
|
||||
pub single_priv: Option<SinglePrivilegeEditArgs>,
|
||||
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
pub json: bool,
|
||||
@@ -65,35 +60,10 @@ pub struct EditPrivsArgs {
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
pub struct SinglePrivilegeEditArgs {
|
||||
/// The `MySQL` database to edit privileges for
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||
#[arg(
|
||||
value_name = "DB_NAME",
|
||||
requires = "user_name",
|
||||
requires = "single_priv"
|
||||
)]
|
||||
pub db_name: Option<MySQLDatabase>,
|
||||
|
||||
/// The `MySQL` database to edit privileges for
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
#[arg(value_name = "USER_NAME")]
|
||||
pub user_name: Option<MySQLUser>,
|
||||
|
||||
/// The privileges to set, grant or revoke
|
||||
#[arg(
|
||||
allow_hyphen_values = true,
|
||||
value_name = "[+-]PRIVILEGES",
|
||||
value_parser = DatabasePrivilegeEdit::parse_from_str,
|
||||
)]
|
||||
pub single_priv: Option<DatabasePrivilegeEdit>,
|
||||
}
|
||||
|
||||
async fn users_exist(
|
||||
server_connection: &mut ClientToServerMessageStream,
|
||||
privilege_diff: &BTreeSet<DatabasePrivilegesDiff>,
|
||||
) -> anyhow::Result<BTreeMap<MySQLUser, Result<(), ListUsersError>>> {
|
||||
) -> anyhow::Result<BTreeMap<MySQLUser, bool>> {
|
||||
let user_list = privilege_diff
|
||||
.iter()
|
||||
.map(|diff| diff.get_user_name().clone())
|
||||
@@ -113,7 +83,7 @@ async fn users_exist(
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.map(|(user, user_result)| (user, user_result.map(|_| ())))
|
||||
.map(|(user, user_result)| (user, user_result.is_ok()))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
@@ -122,7 +92,7 @@ async fn users_exist(
|
||||
async fn databases_exist(
|
||||
server_connection: &mut ClientToServerMessageStream,
|
||||
privilege_diff: &BTreeSet<DatabasePrivilegesDiff>,
|
||||
) -> anyhow::Result<BTreeMap<MySQLDatabase, Result<(), ListDatabasesError>>> {
|
||||
) -> anyhow::Result<BTreeMap<MySQLDatabase, bool>> {
|
||||
let database_list = privilege_diff
|
||||
.iter()
|
||||
.map(|diff| diff.get_database_name().clone())
|
||||
@@ -142,51 +112,20 @@ async fn databases_exist(
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.map(|(database, db_result)| (database, db_result.map(|_| ())))
|
||||
.map(|(database, db_result)| (database, db_result.is_ok()))
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// TODO: reduce the complexity of this function
|
||||
pub async fn edit_database_privileges(
|
||||
args: EditPrivsArgs,
|
||||
// NOTE: this is only used for backwards compat with mysql-admutils
|
||||
use_database: Option<MySQLDatabase>,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
let message = Request::ListPrivileges(use_database.clone().map(|db| vec![db]));
|
||||
let message = Request::ListPrivileges(args.name.to_owned().map(|name| vec![name]));
|
||||
|
||||
server_connection.send(message).await?;
|
||||
|
||||
debug_assert!(args.privs.is_empty() ^ args.single_priv.is_none());
|
||||
|
||||
let privs = if let Some(single_priv_entry) = &args.single_priv {
|
||||
let database = single_priv_entry.db_name.clone().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"DB_NAME must be specified when editing privileges in single privilege mode"
|
||||
)
|
||||
})?;
|
||||
let user = single_priv_entry.user_name.clone().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"USER_NAME must be specified when DB_NAME is specified in single privilege mode"
|
||||
)
|
||||
})?;
|
||||
let privilege_edit = single_priv_entry.single_priv.clone().ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"PRIVILEGES must be specified when DB_NAME is specified in single privilege mode"
|
||||
)
|
||||
})?;
|
||||
|
||||
vec![DatabasePrivilegeEditEntry {
|
||||
database,
|
||||
user,
|
||||
privilege_edit,
|
||||
}]
|
||||
} else {
|
||||
args.privs.clone()
|
||||
};
|
||||
|
||||
let existing_privilege_rows = match server_connection.next().await {
|
||||
Some(Ok(Response::ListPrivileges(databases))) => databases
|
||||
.into_iter()
|
||||
@@ -212,17 +151,17 @@ pub async fn edit_database_privileges(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if privs.is_empty() {
|
||||
let privileges_to_change =
|
||||
edit_privileges_with_editor(&existing_privilege_rows, use_database.as_ref())?;
|
||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||
} else {
|
||||
let privileges_to_change = parse_privilege_tables(&privs)?;
|
||||
let diffs: BTreeSet<DatabasePrivilegesDiff> = if !args.privs.is_empty() {
|
||||
let privileges_to_change = parse_privilege_tables_from_args(&args)?;
|
||||
create_or_modify_privilege_rows(&existing_privilege_rows, &privileges_to_change)?
|
||||
} else {
|
||||
let privileges_to_change =
|
||||
edit_privileges_with_editor(&existing_privilege_rows, args.name.as_ref())?;
|
||||
diff_privileges(&existing_privilege_rows, &privileges_to_change)
|
||||
};
|
||||
|
||||
let database_existence_map = databases_exist(&mut server_connection, &diffs).await?;
|
||||
let user_existence_map = users_exist(&mut server_connection, &diffs).await?;
|
||||
let database_existence_map = databases_exist(&mut server_connection, &diffs).await?;
|
||||
|
||||
let diffs = reduce_privilege_diffs(&existing_privilege_rows, diffs)?
|
||||
.into_iter()
|
||||
@@ -230,14 +169,14 @@ pub async fn edit_database_privileges(
|
||||
let database_name = diff.get_database_name();
|
||||
let username = diff.get_user_name();
|
||||
|
||||
if let Some(Err(err)) = database_existence_map.get(database_name) {
|
||||
println!("{}", err.to_error_message(database_name));
|
||||
if let Some(false) = database_existence_map.get(database_name) {
|
||||
println!("Database '{}' does not exist.", database_name);
|
||||
println!("Skipping...");
|
||||
return false;
|
||||
}
|
||||
|
||||
if let Some(Err(err)) = user_existence_map.get(username) {
|
||||
println!("{}", err.to_error_message(username));
|
||||
if let Some(false) = user_existence_map.get(username) {
|
||||
println!("User '{}' does not exist.", username);
|
||||
println!("Skipping...");
|
||||
return false;
|
||||
}
|
||||
@@ -246,26 +185,6 @@ pub async fn edit_database_privileges(
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
if database_existence_map.values().any(|res| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ListDatabasesError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) || user_existence_map.values().any(|res| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ListUsersError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
println!();
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
println!();
|
||||
}
|
||||
|
||||
if diffs.is_empty() {
|
||||
println!("No changes to make.");
|
||||
server_connection.send(Request::Exit).await?;
|
||||
@@ -296,39 +215,23 @@ pub async fn edit_database_privileges(
|
||||
|
||||
print_modify_database_privileges_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ModifyDatabasePrivilegesError::UserValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
) | ModifyDatabasePrivilegesError::DatabaseValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_privilege_tables(
|
||||
privs: &[DatabasePrivilegeEditEntry],
|
||||
fn parse_privilege_tables_from_args(
|
||||
args: &EditPrivsArgs,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>> {
|
||||
debug_assert!(!privs.is_empty());
|
||||
privs
|
||||
debug_assert!(!args.privs.is_empty());
|
||||
args.privs
|
||||
.iter()
|
||||
.map(|priv_edit_entry| {
|
||||
priv_edit_entry
|
||||
.as_database_privileges_diff()
|
||||
.as_database_privileges_diff(args.name.as_ref())
|
||||
.context(format!(
|
||||
"Failed parsing database privileges: `{priv_edit_entry}`"
|
||||
"Failed parsing database privileges: `{}`",
|
||||
priv_edit_entry
|
||||
))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<DatabasePrivilegeRowDiff>>>()
|
||||
@@ -336,7 +239,6 @@ fn parse_privilege_tables(
|
||||
|
||||
fn edit_privileges_with_editor(
|
||||
privilege_data: &[DatabasePrivilegeRow],
|
||||
// NOTE: this is only used for backwards compat with mysql-admtools
|
||||
database_name: Option<&MySQLDatabase>,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
let unix_user = User::from_uid(getuid())
|
||||
@@ -351,7 +253,7 @@ fn edit_privileges_with_editor(
|
||||
|
||||
match result {
|
||||
None => Ok(privilege_data.to_vec()),
|
||||
Some(result) => parse_privilege_data_from_editor_content(&result)
|
||||
Some(result) => parse_privilege_data_from_editor_content(result)
|
||||
.context("Could not parse privilege data from editor"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, LockUserError, Request, Response,
|
||||
print_lock_users_output_status, print_lock_users_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_lock_users_output_status,
|
||||
print_lock_users_output_status_json,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct LockUserArgs {
|
||||
/// The `MySQL` user(s) to loc
|
||||
/// The MySQL user(s) to loc
|
||||
#[arg(num_args = 1.., value_name = "USER_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
username: Vec<MySQLUser>,
|
||||
@@ -36,7 +35,7 @@ pub async fn lock_users(
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let message = Request::LockUsers(args.username.clone());
|
||||
let message = Request::LockUsers(args.username.to_owned());
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
@@ -48,27 +47,12 @@ pub async fn lock_users(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_lock_users_output_status_json(&result);
|
||||
} else {
|
||||
print_lock_users_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(LockUserError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -8,13 +8,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, Request, Response, SetPasswordError,
|
||||
SetUserPasswordRequest, print_set_password_output_status,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, ListUsersError, Request, Response,
|
||||
print_set_password_output_status,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -22,7 +21,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct PasswdUserArgs {
|
||||
/// The `MySQL` user whose password is to be changed
|
||||
/// The MySQL user whose password is to be changed
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
#[arg(value_name = "USER_NAME")]
|
||||
username: MySQLUser,
|
||||
@@ -38,60 +37,25 @@ pub struct PasswdUserArgs {
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
|
||||
/// Set the password to expire on the given date (YYYY-MM-DD)
|
||||
#[arg(short, long, value_name = "DATE", conflicts_with = "no-expire")]
|
||||
expire_on: Option<chrono::NaiveDate>,
|
||||
|
||||
/// Set the password to never expire
|
||||
#[arg(short, long, conflicts_with = "expire_on")]
|
||||
no_expire: bool,
|
||||
|
||||
/// Clear the password for the user instead of setting a new one
|
||||
#[arg(short, long, conflicts_with_all = &["password_file", "stdin", "expire_on", "no-expire"])]
|
||||
clear: bool,
|
||||
}
|
||||
|
||||
pub fn interactive_password_dialogue_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
||||
pub fn read_password_from_stdin_with_double_check(username: &MySQLUser) -> anyhow::Result<String> {
|
||||
Password::new()
|
||||
.with_prompt(format!("New MySQL password for user '{username}'"))
|
||||
.with_prompt(format!("New MySQL password for user '{}'", username))
|
||||
.with_confirmation(
|
||||
format!("Retype new MySQL password for user '{username}'"),
|
||||
format!("Retype new MySQL password for user '{}'", username),
|
||||
"Passwords do not match",
|
||||
)
|
||||
.interact()
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
pub fn interactive_password_expiry_dialogue(username: &MySQLUser) -> anyhow::Result<Option<chrono::NaiveDate>> {
|
||||
let input = dialoguer::Input::<String>::new()
|
||||
.with_prompt(format!(
|
||||
"Enter the password expiry date for user '{username}' (YYYY-MM-DD)"
|
||||
))
|
||||
.allow_empty(true)
|
||||
.validate_with(|input: &String| {
|
||||
chrono::NaiveDate::parse_from_str(input, "%Y-%m-%d")
|
||||
.map(|_| ())
|
||||
.map_err(|_| "Invalid date format. Please use YYYY-MM-DD".to_string())
|
||||
})
|
||||
.interact_text()?;
|
||||
|
||||
if input.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let date = chrono::NaiveDate::parse_from_str(&input, "%Y-%m-%d")
|
||||
.map_err(|e| anyhow::anyhow!("Failed to parse date: {}", e))?;
|
||||
|
||||
Ok(Some(date))
|
||||
}
|
||||
|
||||
pub async fn passwd_user(
|
||||
args: PasswdUserArgs,
|
||||
mut server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: create a "user" exists check" command
|
||||
let message = Request::ListUsers(Some(vec![args.username.clone()]));
|
||||
let message = Request::ListUsers(Some(vec![args.username.to_owned()]));
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(err);
|
||||
@@ -112,38 +76,22 @@ pub async fn passwd_user(
|
||||
}
|
||||
}
|
||||
|
||||
let password: Option<String> = if let Some(password_file) = args.password_file {
|
||||
Some(
|
||||
std::fs::read_to_string(password_file)
|
||||
.context("Failed to read password file")?
|
||||
.trim()
|
||||
.to_string(),
|
||||
)
|
||||
let password = if let Some(password_file) = args.password_file {
|
||||
std::fs::read_to_string(password_file)
|
||||
.context("Failed to read password file")?
|
||||
.trim()
|
||||
.to_string()
|
||||
} else if args.stdin {
|
||||
let mut buffer = String::new();
|
||||
std::io::stdin()
|
||||
.read_line(&mut buffer)
|
||||
.context("Failed to read password from stdin")?;
|
||||
Some(buffer.trim().to_string())
|
||||
} else if args.clear {
|
||||
None
|
||||
buffer.trim().to_string()
|
||||
} else {
|
||||
Some(interactive_password_dialogue_with_double_check(&args.username)?)
|
||||
read_password_from_stdin_with_double_check(&args.username)?
|
||||
};
|
||||
|
||||
let expiry_date = if args.no_expire {
|
||||
None
|
||||
} else if let Some(date) = args.expire_on {
|
||||
Some(date)
|
||||
} else {
|
||||
interactive_password_expiry_dialogue(&args.username)?
|
||||
};
|
||||
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: args.username.clone(),
|
||||
new_password: password,
|
||||
expiry: expiry_date,
|
||||
});
|
||||
let message = Request::PasswdUser((args.username.to_owned(), password));
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
@@ -155,22 +103,9 @@ pub async fn passwd_user(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
print_set_password_output_status(&result, &args.username);
|
||||
|
||||
if matches!(
|
||||
result,
|
||||
Err(SetPasswordError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.is_err() {
|
||||
std::process::exit(1);
|
||||
}
|
||||
print_set_password_output_status(&result, &args.username);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListDatabasesError, Request, Response,
|
||||
print_list_databases_output_status, print_list_databases_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_list_databases_output_status,
|
||||
print_list_databases_output_status_json,
|
||||
},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowDbArgs {
|
||||
/// The `MySQL` database(s) to show
|
||||
/// The MySQL database(s) to show
|
||||
#[arg(num_args = 0.., value_name = "DB_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
@@ -27,9 +26,9 @@ pub struct ShowDbArgs {
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
|
||||
/// Show sizes in bytes instead of human-readable format
|
||||
/// Return a non-zero exit code if any of the results were erroneous
|
||||
#[arg(short, long)]
|
||||
bytes: bool,
|
||||
fail: bool,
|
||||
}
|
||||
|
||||
pub async fn show_databases(
|
||||
@@ -39,7 +38,7 @@ pub async fn show_databases(
|
||||
let message = if args.name.is_empty() {
|
||||
Request::ListDatabases(None)
|
||||
} else {
|
||||
Request::ListDatabases(Some(args.name.clone()))
|
||||
Request::ListDatabases(Some(args.name.to_owned()))
|
||||
};
|
||||
|
||||
server_connection.send(message).await?;
|
||||
@@ -61,26 +60,15 @@ pub async fn show_databases(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_list_databases_output_status_json(&databases);
|
||||
} else {
|
||||
print_list_databases_output_status(&databases, args.bytes);
|
||||
|
||||
if databases.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ListDatabasesError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
print_list_databases_output_status(&databases);
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if databases.values().any(std::result::Result::is_err) {
|
||||
if args.fail && databases.values().any(|res| res.is_err()) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,13 +5,12 @@ use itertools::Itertools;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_database_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListPrivilegesError, Request, Response,
|
||||
print_list_privileges_output_status, print_list_privileges_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_list_privileges_output_status,
|
||||
print_list_privileges_output_status_json,
|
||||
},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
@@ -19,7 +18,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowPrivsArgs {
|
||||
/// The `MySQL` database(s) to show privileges for
|
||||
/// The MySQL database(s) to show privileges for
|
||||
#[arg(num_args = 0.., value_name = "DB_NAME")]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_database_completer)))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
@@ -33,6 +32,10 @@ pub struct ShowPrivsArgs {
|
||||
/// This flag has no effect when used with --json
|
||||
#[arg(short, long)]
|
||||
long: bool,
|
||||
|
||||
/// Return a non-zero exit code if any of the results were erroneous
|
||||
#[arg(short, long)]
|
||||
fail: bool,
|
||||
}
|
||||
|
||||
pub async fn show_database_privileges(
|
||||
@@ -42,7 +45,7 @@ pub async fn show_database_privileges(
|
||||
let message = if args.name.is_empty() {
|
||||
Request::ListPrivileges(None)
|
||||
} else {
|
||||
Request::ListPrivileges(Some(args.name.clone()))
|
||||
Request::ListPrivileges(Some(args.name.to_owned()))
|
||||
};
|
||||
server_connection.send(message).await?;
|
||||
|
||||
@@ -65,26 +68,15 @@ pub async fn show_database_privileges(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_list_privileges_output_status_json(&privilege_data);
|
||||
} else {
|
||||
print_list_privileges_output_status(&privilege_data, args.long);
|
||||
|
||||
if privilege_data.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ListPrivilegesError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if privilege_data.values().any(std::result::Result::is_err) {
|
||||
if args.fail && privilege_data.values().any(|res| res.is_err()) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListUsersError, Request, Response,
|
||||
print_list_users_output_status, print_list_users_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_list_users_output_status,
|
||||
print_list_users_output_status_json,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct ShowUserArgs {
|
||||
/// The `MySQL` user(s) to show
|
||||
/// The MySQL user(s) to show
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
#[arg(num_args = 0.., value_name = "USER_NAME")]
|
||||
username: Vec<MySQLUser>,
|
||||
@@ -26,6 +25,10 @@ pub struct ShowUserArgs {
|
||||
/// Print the information as JSON
|
||||
#[arg(short, long)]
|
||||
json: bool,
|
||||
|
||||
/// Return a non-zero exit code if any of the results were erroneous
|
||||
#[arg(short, long)]
|
||||
fail: bool,
|
||||
}
|
||||
|
||||
pub async fn show_users(
|
||||
@@ -35,7 +38,7 @@ pub async fn show_users(
|
||||
let message = if args.username.is_empty() {
|
||||
Request::ListUsers(None)
|
||||
} else {
|
||||
Request::ListUsers(Some(args.username.clone()))
|
||||
Request::ListUsers(Some(args.username.to_owned()))
|
||||
};
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
@@ -60,26 +63,15 @@ pub async fn show_users(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_list_users_output_status_json(&users);
|
||||
} else {
|
||||
print_list_users_output_status(&users);
|
||||
|
||||
if users.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(ListUsersError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if users.values().any(std::result::Result::is_err) {
|
||||
if args.fail && users.values().any(|result| result.is_err()) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,12 @@ use futures_util::SinkExt;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::{erroneous_server_response, print_authorization_owner_hint},
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, UnlockUserError,
|
||||
print_unlock_users_output_status, print_unlock_users_output_status_json,
|
||||
request_validation::ValidationError,
|
||||
ClientToServerMessageStream, Request, Response, print_unlock_users_output_status,
|
||||
print_unlock_users_output_status_json,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -18,7 +17,7 @@ use crate::{
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct UnlockUserArgs {
|
||||
/// The `MySQL` user(s) to unlock
|
||||
/// The MySQL user(s) to unlock
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(mysql_user_completer)))]
|
||||
#[arg(num_args = 1.., value_name = "USER_NAME")]
|
||||
username: Vec<MySQLUser>,
|
||||
@@ -36,7 +35,7 @@ pub async fn unlock_users(
|
||||
anyhow::bail!("No usernames provided");
|
||||
}
|
||||
|
||||
let message = Request::UnlockUsers(args.username.clone());
|
||||
let message = Request::UnlockUsers(args.username.to_owned());
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
@@ -48,27 +47,12 @@ pub async fn unlock_users(
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if args.json {
|
||||
print_unlock_users_output_status_json(&result);
|
||||
} else {
|
||||
print_unlock_users_output_status(&result);
|
||||
|
||||
if result.iter().any(|(_, res)| {
|
||||
matches!(
|
||||
res,
|
||||
Err(UnlockUserError::ValidationError(
|
||||
ValidationError::AuthorizationError(_)
|
||||
))
|
||||
)
|
||||
}) {
|
||||
print_authorization_owner_hint(&mut server_connection).await?;
|
||||
}
|
||||
}
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
if result.values().any(std::result::Result::is_err) {
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
use crate::core::types::{MySQLDatabase, MySQLUser};
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn trim_db_name_to_32_chars(db_name: &MySQLDatabase) -> MySQLDatabase {
|
||||
db_name.chars().take(32).collect::<String>().into()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn trim_user_name_to_32_chars(user_name: &MySQLUser) -> MySQLUser {
|
||||
user_name.chars().take(32).collect::<String>().into()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::core::{
|
||||
protocol::{
|
||||
CreateDatabaseError, CreateUserError, DropDatabaseError, DropUserError,
|
||||
ListPrivilegesError, ListUsersError, request_validation::ValidationError,
|
||||
GetDatabasesPrivilegeDataError, ListUsersError, request_validation::ValidationError,
|
||||
},
|
||||
types::DbOrUser,
|
||||
};
|
||||
|
||||
pub fn name_validation_error_to_error_message(db_or_user: &DbOrUser) -> String {
|
||||
pub fn name_validation_error_to_error_message(db_or_user: DbOrUser) -> String {
|
||||
let argv0 = std::env::args().next().unwrap_or_else(|| match db_or_user {
|
||||
DbOrUser::Database(_) => "mysql-dbadm".to_string(),
|
||||
DbOrUser::User(_) => "mysql-useradm".to_string(),
|
||||
@@ -23,7 +23,7 @@ pub fn name_validation_error_to_error_message(db_or_user: &DbOrUser) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authorization_error_message(db_or_user: &DbOrUser) -> String {
|
||||
pub fn authorization_error_message(db_or_user: DbOrUser) -> String {
|
||||
format!(
|
||||
"You are not in charge of mysql-{}: '{}'. Skipping.",
|
||||
db_or_user.lowercased_noun(),
|
||||
@@ -31,7 +31,7 @@ pub fn authorization_error_message(db_or_user: &DbOrUser) -> String {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn handle_create_user_error(error: &CreateUserError, name: &str) {
|
||||
pub fn handle_create_user_error(error: CreateUserError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
@@ -39,22 +39,22 @@ pub fn handle_create_user_error(error: &CreateUserError, name: &str) {
|
||||
CreateUserError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(&DbOrUser::User(name.into()))
|
||||
name_validation_error_to_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
CreateUserError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
authorization_error_message(&DbOrUser::User(name.into()))
|
||||
authorization_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
CreateUserError::MySqlError(_) | CreateUserError::UserAlreadyExists => {
|
||||
eprintln!("{argv0}: Failed to create user '{name}'.");
|
||||
eprintln!("{}: Failed to create user '{}'.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_drop_user_error(error: &DropUserError, name: &str) {
|
||||
pub fn handle_drop_user_error(error: DropUserError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
@@ -62,22 +62,22 @@ pub fn handle_drop_user_error(error: &DropUserError, name: &str) {
|
||||
DropUserError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(&DbOrUser::User(name.into()))
|
||||
name_validation_error_to_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
DropUserError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
authorization_error_message(&DbOrUser::User(name.into()))
|
||||
authorization_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
DropUserError::MySqlError(_) | DropUserError::UserDoesNotExist => {
|
||||
eprintln!("{argv0}: Failed to delete user '{name}'.");
|
||||
eprintln!("{}: Failed to delete user '{}'.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_list_users_error(error: &ListUsersError, name: &str) {
|
||||
pub fn handle_list_users_error(error: ListUsersError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-useradm".to_string());
|
||||
@@ -85,27 +85,30 @@ pub fn handle_list_users_error(error: &ListUsersError, name: &str) {
|
||||
ListUsersError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(&DbOrUser::User(name.into()))
|
||||
name_validation_error_to_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
ListUsersError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
authorization_error_message(&DbOrUser::User(name.into()))
|
||||
authorization_error_message(DbOrUser::User(name.into()))
|
||||
);
|
||||
}
|
||||
ListUsersError::UserDoesNotExist => {
|
||||
eprintln!("{argv0}: User '{name}' does not exist. You must create it first.",);
|
||||
eprintln!(
|
||||
"{}: User '{}' does not exist. You must create it first.",
|
||||
argv0, name,
|
||||
);
|
||||
}
|
||||
ListUsersError::MySqlError(_) => {
|
||||
eprintln!("{argv0}: Failed to look up password for user '{name}'");
|
||||
eprintln!("{}: Failed to look up password for user '{}'", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
pub fn handle_create_database_error(error: &CreateDatabaseError, name: &str) {
|
||||
pub fn handle_create_database_error(error: CreateDatabaseError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
@@ -113,26 +116,26 @@ pub fn handle_create_database_error(error: &CreateDatabaseError, name: &str) {
|
||||
CreateDatabaseError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(&DbOrUser::Database(name.into()))
|
||||
name_validation_error_to_error_message(DbOrUser::Database(name.into()))
|
||||
);
|
||||
}
|
||||
|
||||
CreateDatabaseError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
authorization_error_message(&DbOrUser::Database(name.into()))
|
||||
authorization_error_message(DbOrUser::Database(name.into()))
|
||||
);
|
||||
}
|
||||
CreateDatabaseError::MySqlError(_) => {
|
||||
eprintln!("{argv0}: Cannot create database '{name}'.");
|
||||
eprintln!("{}: Cannot create database '{}'.", argv0, name);
|
||||
}
|
||||
CreateDatabaseError::DatabaseAlreadyExists => {
|
||||
eprintln!("{argv0}: Database '{name}' already exists.");
|
||||
eprintln!("{}: Database '{}' already exists.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_drop_database_error(error: &DropDatabaseError, name: &str) {
|
||||
pub fn handle_drop_database_error(error: DropDatabaseError, name: &str) {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
@@ -140,41 +143,47 @@ pub fn handle_drop_database_error(error: &DropDatabaseError, name: &str) {
|
||||
DropDatabaseError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
name_validation_error_to_error_message(&DbOrUser::Database(name.into()))
|
||||
name_validation_error_to_error_message(DbOrUser::Database(name.into()))
|
||||
);
|
||||
}
|
||||
DropDatabaseError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
eprintln!(
|
||||
"{}",
|
||||
authorization_error_message(&DbOrUser::Database(name.into()))
|
||||
authorization_error_message(DbOrUser::Database(name.into()))
|
||||
);
|
||||
}
|
||||
DropDatabaseError::MySqlError(_) => {
|
||||
eprintln!("{argv0}: Cannot drop database '{name}'.");
|
||||
eprintln!("{}: Cannot drop database '{}'.", argv0, name);
|
||||
}
|
||||
DropDatabaseError::DatabaseDoesNotExist => {
|
||||
eprintln!("{argv0}: Database '{name}' doesn't exist.");
|
||||
eprintln!("{}: Database '{}' doesn't exist.", argv0, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_show_database_error_message(error: &ListPrivilegesError, name: &str) -> String {
|
||||
pub fn format_show_database_error_message(
|
||||
error: GetDatabasesPrivilegeDataError,
|
||||
name: &str,
|
||||
) -> String {
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "mysql-dbadm".to_string());
|
||||
|
||||
match error {
|
||||
ListPrivilegesError::ValidationError(ValidationError::NameValidationError(_)) => {
|
||||
name_validation_error_to_error_message(&DbOrUser::Database(name.into()))
|
||||
GetDatabasesPrivilegeDataError::ValidationError(ValidationError::NameValidationError(
|
||||
_,
|
||||
)) => name_validation_error_to_error_message(DbOrUser::Database(name.into())),
|
||||
GetDatabasesPrivilegeDataError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
authorization_error_message(DbOrUser::Database(name.into()))
|
||||
}
|
||||
ListPrivilegesError::ValidationError(ValidationError::AuthorizationError(_)) => {
|
||||
authorization_error_message(&DbOrUser::Database(name.into()))
|
||||
GetDatabasesPrivilegeDataError::MySqlError(err) => {
|
||||
format!(
|
||||
"{}: Failed to look up privileges for database '{}': {}",
|
||||
argv0, name, err
|
||||
)
|
||||
}
|
||||
ListPrivilegesError::MySqlError(err) => {
|
||||
format!("{argv0}: Failed to look up privileges for database '{name}': {err}")
|
||||
}
|
||||
ListPrivilegesError::DatabaseDoesNotExist => {
|
||||
format!("{argv0}: Database '{name}' doesn't exist.")
|
||||
GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => {
|
||||
format!("{}: Database '{}' doesn't exist.", argv0, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_complete::ArgValueCompleter;
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::path::PathBuf;
|
||||
@@ -19,17 +18,17 @@ use crate::{
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
completion::{mysql_database_completer, prefix_completer},
|
||||
completion::mysql_database_completer,
|
||||
database_privileges::DatabasePrivilegeRow,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, ListPrivilegesError, Request, Response,
|
||||
ClientToServerMessageStream, GetDatabasesPrivilegeDataError, Request, Response,
|
||||
create_client_to_server_message_stream,
|
||||
},
|
||||
types::MySQLDatabase,
|
||||
},
|
||||
};
|
||||
|
||||
const HELP_DB_PERM: &str = r"
|
||||
const HELP_DB_PERM: &str = r#"
|
||||
Edit permissions for the DATABASE(s). Running this command will
|
||||
spawn the editor stored in the $EDITOR environment variable.
|
||||
(pico will be used if the variable is unset)
|
||||
@@ -50,7 +49,7 @@ The Y/N-values corresponds to the following mysql privileges:
|
||||
Temp - Enables use of CREATE TEMPORARY TABLE
|
||||
Lock - Enables use of LOCK TABLE
|
||||
References - Enables use of REFERENCES
|
||||
";
|
||||
"#;
|
||||
|
||||
/// Create, drop or edit permissions for the DATABASE(s),
|
||||
/// as determined by the COMMAND.
|
||||
@@ -125,7 +124,6 @@ pub enum Command {
|
||||
pub struct CreateArgs {
|
||||
/// The name of the DATABASE(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(prefix_completer)))]
|
||||
name: Vec<MySQLDatabase>,
|
||||
}
|
||||
|
||||
@@ -157,22 +155,25 @@ pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
if args.help_editperm {
|
||||
println!("{HELP_DB_PERM}");
|
||||
println!("{}", HELP_DB_PERM);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let server_connection = bootstrap_server_connection_and_drop_privileges(
|
||||
args.server_socket_path,
|
||||
args.config,
|
||||
Verbosity::default(),
|
||||
Default::default(),
|
||||
)?;
|
||||
|
||||
let Some(command) = args.command else {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args().next().unwrap_or("mysql-dbadm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args().next().unwrap_or("mysql-dbadm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
tokio_run_command(command, server_connection)?;
|
||||
@@ -192,11 +193,11 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
||||
while let Some(Ok(message)) = message_stream.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{err}");
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {message:?}");
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -207,19 +208,14 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
||||
Command::Show(args) => show_databases(args, message_stream).await,
|
||||
Command::Editperm(args) => {
|
||||
let edit_privileges_args = EditPrivsArgs {
|
||||
single_priv: None,
|
||||
name: Some(args.database),
|
||||
privs: vec![],
|
||||
json: false,
|
||||
editor: None,
|
||||
yes: false,
|
||||
};
|
||||
|
||||
edit_database_privileges(
|
||||
edit_privileges_args,
|
||||
Some(args.database),
|
||||
message_stream,
|
||||
)
|
||||
.await
|
||||
edit_database_privileges(edit_privileges_args, message_stream).await
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -243,8 +239,8 @@ async fn create_databases(
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("Database {name} created."),
|
||||
Err(err) => handle_create_database_error(&err, &name),
|
||||
Ok(()) => println!("Database {} created.", name),
|
||||
Err(err) => handle_create_database_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,8 +265,8 @@ async fn drop_databases(
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("Database {name} dropped."),
|
||||
Err(err) => handle_drop_database_error(&err, &name),
|
||||
Ok(()) => println!("Database {} dropped.", name),
|
||||
Err(err) => handle_drop_database_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,21 +306,24 @@ async fn show_databases(
|
||||
let results: Vec<Result<(MySQLDatabase, Vec<DatabasePrivilegeRow>), String>> = match response {
|
||||
Some(Ok(Response::ListPrivileges(result))) => result
|
||||
.into_iter()
|
||||
.map(|(name, rows)| match rows.map(|rows| (name.clone(), rows)) {
|
||||
Ok(rows) => Ok(rows),
|
||||
Err(ListPrivilegesError::DatabaseDoesNotExist) => Ok((name, vec![])),
|
||||
Err(err) => Err(format_show_database_error_message(&err, &name)),
|
||||
})
|
||||
.map(
|
||||
|(name, rows)| match rows.map(|rows| (name.to_owned(), rows)) {
|
||||
Ok(rows) => Ok(rows),
|
||||
Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist) => Ok((name, vec![])),
|
||||
Err(err) => Err(format_show_database_error_message(err, &name)),
|
||||
},
|
||||
)
|
||||
.collect(),
|
||||
response => return erroneous_server_response(response),
|
||||
};
|
||||
|
||||
for result in results {
|
||||
match result {
|
||||
Ok((name, rows)) => print_db_privs(&name, rows),
|
||||
Err(err) => eprintln!("{err}"),
|
||||
results.into_iter().try_for_each(|result| match result {
|
||||
Ok((name, rows)) => print_db_privs(&name, rows),
|
||||
Err(err) => {
|
||||
eprintln!("{}", err);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -334,7 +333,7 @@ fn yn(value: bool) -> &'static str {
|
||||
if value { "Y" } else { "N" }
|
||||
}
|
||||
|
||||
fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) {
|
||||
fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) -> anyhow::Result<()> {
|
||||
println!(
|
||||
concat!(
|
||||
"Database '{}':\n",
|
||||
@@ -364,4 +363,6 @@ fn print_db_privs(name: &str, rows: Vec<DatabasePrivilegeRow>) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use tokio::net::UnixStream as TokioUnixStream;
|
||||
|
||||
use crate::{
|
||||
client::{
|
||||
commands::{erroneous_server_response, interactive_password_dialogue_with_double_check},
|
||||
commands::{erroneous_server_response, read_password_from_stdin_with_double_check},
|
||||
mysql_admutils_compatibility::{
|
||||
common::trim_user_name_to_32_chars,
|
||||
error_messages::{
|
||||
@@ -18,9 +18,9 @@ use crate::{
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
completion::{mysql_user_completer, prefix_completer},
|
||||
completion::mysql_user_completer,
|
||||
protocol::{
|
||||
ClientToServerMessageStream, Request, Response, SetUserPasswordRequest, create_client_to_server_message_stream
|
||||
ClientToServerMessageStream, Request, Response, create_client_to_server_message_stream,
|
||||
},
|
||||
types::MySQLUser,
|
||||
},
|
||||
@@ -75,7 +75,7 @@ pub enum Command {
|
||||
/// delete the USER(s).
|
||||
Delete(DeleteArgs),
|
||||
|
||||
/// change the `MySQL` password for the USER(s).
|
||||
/// change the MySQL password for the USER(s).
|
||||
Passwd(PasswdArgs),
|
||||
|
||||
/// give information about the USERS(s), or, if
|
||||
@@ -87,7 +87,6 @@ pub enum Command {
|
||||
pub struct CreateArgs {
|
||||
/// The name of the USER(s) to create.
|
||||
#[arg(num_args = 1..)]
|
||||
#[cfg_attr(not(feature = "suid-sgid-mode"), arg(add = ArgValueCompleter::new(prefix_completer)))]
|
||||
name: Vec<MySQLUser>,
|
||||
}
|
||||
|
||||
@@ -119,14 +118,17 @@ pub struct ShowArgs {
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let Some(command) = args.command else {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
let command = match args.command {
|
||||
Some(command) => command,
|
||||
None => {
|
||||
println!(
|
||||
"Try `{} --help' for more information.",
|
||||
std::env::args()
|
||||
.next()
|
||||
.unwrap_or("mysql-useradm".to_string())
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let server_connection = bootstrap_server_connection_and_drop_privileges(
|
||||
@@ -152,11 +154,11 @@ fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyh
|
||||
while let Some(Ok(message)) = message_stream.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{err}");
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {message:?}");
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,8 +190,8 @@ async fn create_user(
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("User '{name}' created."),
|
||||
Err(err) => handle_create_user_error(&err, &name),
|
||||
Ok(()) => println!("User '{}' created.", name),
|
||||
Err(err) => handle_create_user_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,8 +216,8 @@ async fn drop_users(
|
||||
|
||||
for (name, result) in result {
|
||||
match result {
|
||||
Ok(()) => println!("User '{name}' deleted."),
|
||||
Err(err) => handle_drop_user_error(&err, &name),
|
||||
Ok(()) => println!("User '{}' deleted.", name),
|
||||
Err(err) => handle_drop_user_error(err, &name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,19 +247,15 @@ async fn passwd_users(
|
||||
.filter_map(|(name, result)| match result {
|
||||
Ok(user) => Some(user),
|
||||
Err(err) => {
|
||||
handle_list_users_error(&err, &name);
|
||||
handle_list_users_error(err, &name);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
for user in users {
|
||||
let password = interactive_password_dialogue_with_double_check(&user.user)?;
|
||||
let message = Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: user.user.clone(),
|
||||
new_password: Some(password),
|
||||
expiry: None,
|
||||
});
|
||||
let password = read_password_from_stdin_with_double_check(&user.user)?;
|
||||
let message = Request::PasswdUser((user.user.to_owned(), password));
|
||||
server_connection.send(message).await?;
|
||||
match server_connection.next().await {
|
||||
Some(Ok(Response::SetUserPassword(result))) => match result {
|
||||
@@ -293,7 +291,7 @@ async fn show_users(
|
||||
Some(Ok(Response::ListAllUsers(result))) => match result {
|
||||
Ok(users) => users,
|
||||
Err(err) => {
|
||||
eprintln!("Failed to list users: {err:?}");
|
||||
println!("Failed to list users: {:?}", err);
|
||||
return Ok(());
|
||||
}
|
||||
},
|
||||
@@ -302,7 +300,7 @@ async fn show_users(
|
||||
.filter_map(|(name, result)| match result {
|
||||
Ok(user) => Some(user),
|
||||
Err(err) => {
|
||||
handle_list_users_error(&err, &name);
|
||||
handle_list_users_error(err, &name);
|
||||
None
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
use std::{fs, path::PathBuf, sync::Arc, time::Duration};
|
||||
|
||||
use anyhow::{Context, anyhow};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
@@ -14,12 +9,10 @@ use tokio::{net::UnixStream as TokioUnixStream, sync::RwLock};
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
common::{DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executing_in_suid_sgid_mode},
|
||||
protocol::request_validation::GroupDenylist,
|
||||
core::common::{
|
||||
DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executing_in_suid_sgid_mode,
|
||||
},
|
||||
server::{
|
||||
authorization::read_and_parse_group_denylist,
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
landlock::landlock_restrict_server,
|
||||
session_handler,
|
||||
@@ -141,7 +134,7 @@ fn connect_to_external_server(
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
|
||||
std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
|
||||
_ => Err(anyhow::anyhow!("Failed to connect to socket: {e}")),
|
||||
_ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -153,7 +146,7 @@ fn connect_to_external_server(
|
||||
Err(e) => match e.kind() {
|
||||
std::io::ErrorKind::NotFound => Err(anyhow::anyhow!("Socket not found")),
|
||||
std::io::ErrorKind::PermissionDenied => Err(anyhow::anyhow!("Permission denied")),
|
||||
_ => Err(anyhow::anyhow!("Failed to connect to socket: {e}")),
|
||||
_ => Err(anyhow::anyhow!("Failed to connect to socket: {}", e)),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -181,8 +174,6 @@ pub fn drop_privs() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bootstrap an internal server by forking a child process to run the server, giving it
|
||||
/// the other half of a Unix socket pair to communicate with the client process.
|
||||
fn bootstrap_internal_server_and_drop_privs(
|
||||
config_path: Option<PathBuf>,
|
||||
) -> anyhow::Result<StdUnixStream> {
|
||||
@@ -197,10 +188,10 @@ fn bootstrap_internal_server_and_drop_privs(
|
||||
}
|
||||
|
||||
tracing::debug!("Starting server with config at {:?}", config_path);
|
||||
let socket = invoke_server_with_config(&config_path)?;
|
||||
let socket = invoke_server_with_config(config_path)?;
|
||||
drop_privs()?;
|
||||
return Ok(socket);
|
||||
}
|
||||
};
|
||||
|
||||
let config_path = PathBuf::from(DEFAULT_CONFIG_PATH);
|
||||
if fs::metadata(&config_path).is_ok() {
|
||||
@@ -208,10 +199,10 @@ fn bootstrap_internal_server_and_drop_privs(
|
||||
anyhow::bail!("Executable is not SUID/SGID - refusing to start internal sever");
|
||||
}
|
||||
tracing::debug!("Starting server with default config at {:?}", config_path);
|
||||
let socket = invoke_server_with_config(&config_path)?;
|
||||
let socket = invoke_server_with_config(config_path)?;
|
||||
drop_privs()?;
|
||||
return Ok(socket);
|
||||
}
|
||||
};
|
||||
|
||||
anyhow::bail!("No config path provided, and no default config found");
|
||||
}
|
||||
@@ -221,7 +212,7 @@ fn bootstrap_internal_server_and_drop_privs(
|
||||
/// Fork a child process to run the server with the provided config.
|
||||
/// The server will exit silently by itself when it is done, and this function
|
||||
/// will only return for the client with the socket for the server.
|
||||
fn invoke_server_with_config(config_path: &Path) -> anyhow::Result<StdUnixStream> {
|
||||
fn invoke_server_with_config(config_path: PathBuf) -> anyhow::Result<StdUnixStream> {
|
||||
let (server_socket, client_socket) = StdUnixStream::pair()?;
|
||||
let unix_user = UnixUser::from_uid(nix::unistd::getuid().as_raw())?;
|
||||
|
||||
@@ -233,21 +224,17 @@ fn invoke_server_with_config(config_path: &Path) -> anyhow::Result<StdUnixStream
|
||||
nix::unistd::ForkResult::Child => {
|
||||
tracing::debug!("Running server in child process");
|
||||
|
||||
landlock_restrict_server(Some(config_path))
|
||||
landlock_restrict_server(Some(config_path.as_path()))
|
||||
.context("Failed to apply Landlock restrictions to the server process")?;
|
||||
|
||||
match run_forked_server(config_path, server_socket, &unix_user) {
|
||||
match run_forked_server(config_path, server_socket, unix_user) {
|
||||
Err(e) => Err(e),
|
||||
Ok(()) => unreachable!(),
|
||||
Ok(_) => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a `MySQL` connection pool that consists of exactly one connection.
|
||||
///
|
||||
/// This is used for the internal server in SUID/SGID mode, where the server session
|
||||
/// only ever will get a single client.
|
||||
async fn construct_single_connection_mysql_pool(
|
||||
config: &MysqlConfig,
|
||||
) -> anyhow::Result<sqlx::MySqlPool> {
|
||||
@@ -273,29 +260,20 @@ async fn construct_single_connection_mysql_pool(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Run a single server session in the forked process.
|
||||
///
|
||||
/// Run the server in the forked child process.
|
||||
/// This function will not return, but will exit the process with a success code.
|
||||
/// The function assumes that it's caller has already forked the process.
|
||||
fn run_forked_server(
|
||||
config_path: &Path,
|
||||
config_path: PathBuf,
|
||||
server_socket: StdUnixStream,
|
||||
unix_user: &UnixUser,
|
||||
unix_user: UnixUser,
|
||||
) -> anyhow::Result<()> {
|
||||
let config = ServerConfig::read_config_from_path(config_path)
|
||||
let config = ServerConfig::read_config_from_path(&config_path)
|
||||
.context("Failed to read server config in forked process")?;
|
||||
|
||||
let group_denylist = if let Some(denylist_path) = &config.authorization.group_denylist_file {
|
||||
read_and_parse_group_denylist(denylist_path)
|
||||
.context("Failed to read and parse group denylist")?
|
||||
} else {
|
||||
GroupDenylist::new()
|
||||
};
|
||||
|
||||
let result: anyhow::Result<()> = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to start Tokio runtime")?
|
||||
.unwrap()
|
||||
.block_on(async {
|
||||
let socket = TokioUnixStream::from_std(server_socket)?;
|
||||
let db_pool = construct_single_connection_mysql_pool(&config.mysql).await?;
|
||||
@@ -311,10 +289,9 @@ fn run_forked_server(
|
||||
let db_pool = Arc::new(RwLock::new(db_pool));
|
||||
session_handler::session_handler_with_unix_user(
|
||||
socket,
|
||||
unix_user,
|
||||
&unix_user,
|
||||
db_pool,
|
||||
db_is_mariadb,
|
||||
&group_denylist,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -10,13 +10,13 @@ pub const DEFAULT_CONFIG_PATH: &str = "/etc/muscl/config.toml";
|
||||
pub const DEFAULT_SOCKET_PATH: &str = "/run/muscl/muscl.sock";
|
||||
|
||||
pub const ASCII_BANNER: &str = indoc! {
|
||||
r"
|
||||
r#"
|
||||
__
|
||||
____ ___ __ ____________/ /
|
||||
/ __ `__ \/ / / / ___/ ___/ /
|
||||
/ / / / / / /_/ (__ ) /__/ /
|
||||
/_/ /_/ /_/\__,_/____/\___/_/
|
||||
"
|
||||
"#
|
||||
};
|
||||
|
||||
pub const KIND_REGARDS: &str = concat!(
|
||||
@@ -95,14 +95,14 @@ impl UnixUser {
|
||||
|
||||
Ok(UnixUser {
|
||||
username: libc_user.name,
|
||||
groups: groups.iter().map(|g| g.name.clone()).collect(),
|
||||
groups: groups.iter().map(|g| g.name.to_owned()).collect(),
|
||||
})
|
||||
}
|
||||
|
||||
// pub fn from_enviroment() -> anyhow::Result<Self> {
|
||||
// let libc_uid = nix::unistd::getuid();
|
||||
// UnixUser::from_uid(libc_uid.as_raw())
|
||||
// }
|
||||
pub fn from_enviroment() -> anyhow::Result<Self> {
|
||||
let libc_uid = nix::unistd::getuid();
|
||||
UnixUser::from_uid(libc_uid.as_raw())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
mod mysql_database_completer;
|
||||
mod mysql_user_completer;
|
||||
mod prefix_completer;
|
||||
|
||||
pub use mysql_database_completer::*;
|
||||
pub use mysql_user_completer::*;
|
||||
pub use prefix_completer::*;
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn mysql_database_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
@@ -21,18 +20,18 @@ pub fn mysql_database_completer(current: &std::ffi::OsStr) -> Vec<CompletionCand
|
||||
Ok(runtime) => match runtime.block_on(mysql_database_completer_(current)) {
|
||||
Ok(completions) => completions,
|
||||
Err(err) => {
|
||||
eprintln!("Error getting MySQL database completions: {err}");
|
||||
eprintln!("Error getting MySQL database completions: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error starting Tokio runtime: {err}");
|
||||
eprintln!("Error starting Tokio runtime: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server to get `MySQL` database completions.
|
||||
/// Connect to the server to get MySQL database completions.
|
||||
async fn mysql_database_completer_(
|
||||
current: &std::ffi::OsStr,
|
||||
) -> anyhow::Result<Vec<CompletionCandidate>> {
|
||||
@@ -45,11 +44,11 @@ async fn mysql_database_completer_(
|
||||
while let Some(Ok(message)) = server_connection.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{err}");
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {message:?}");
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +62,7 @@ async fn mysql_database_completer_(
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CompleteDatabaseName(suggestions))) => suggestions,
|
||||
response => return erroneous_server_response(response).map(|()| vec![]),
|
||||
response => return erroneous_server_response(response).map(|_| vec![]),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::{
|
||||
},
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn mysql_user_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
@@ -21,18 +20,18 @@ pub fn mysql_user_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidat
|
||||
Ok(runtime) => match runtime.block_on(mysql_user_completer_(current)) {
|
||||
Ok(completions) => completions,
|
||||
Err(err) => {
|
||||
eprintln!("Error getting MySQL user completions: {err}");
|
||||
eprintln!("Error getting MySQL user completions: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error starting Tokio runtime: {err}");
|
||||
eprintln!("Error starting Tokio runtime: {}", err);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server to get `MySQL` user completions.
|
||||
/// Connect to the server to get MySQL user completions.
|
||||
async fn mysql_user_completer_(
|
||||
current: &std::ffi::OsStr,
|
||||
) -> anyhow::Result<Vec<CompletionCandidate>> {
|
||||
@@ -45,11 +44,11 @@ async fn mysql_user_completer_(
|
||||
while let Some(Ok(message)) = server_connection.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{err}");
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {message:?}");
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,7 +62,7 @@ async fn mysql_user_completer_(
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::CompleteUserName(suggestions))) => suggestions,
|
||||
response => return erroneous_server_response(response).map(|()| vec![]),
|
||||
response => return erroneous_server_response(response).map(|_| vec![]),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
use clap_complete::CompletionCandidate;
|
||||
use clap_verbosity_flag::Verbosity;
|
||||
use futures_util::SinkExt;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use crate::{
|
||||
client::commands::erroneous_server_response,
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
protocol::{Request, Response, create_client_to_server_message_stream},
|
||||
},
|
||||
};
|
||||
|
||||
#[must_use]
|
||||
pub fn prefix_completer(current: &std::ffi::OsStr) -> Vec<CompletionCandidate> {
|
||||
match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(runtime) => match runtime.block_on(prefix_completer_(current)) {
|
||||
Ok(completions) => completions,
|
||||
Err(err) => {
|
||||
eprintln!("Error getting prefix completions: {err}");
|
||||
Vec::new()
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
eprintln!("Error starting Tokio runtime: {err}");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Connect to the server to get `MySQL` user completions.
|
||||
async fn prefix_completer_(_current: &std::ffi::OsStr) -> anyhow::Result<Vec<CompletionCandidate>> {
|
||||
let server_connection =
|
||||
bootstrap_server_connection_and_drop_privileges(None, None, Verbosity::new(0, 1))?;
|
||||
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let mut server_connection = create_client_to_server_message_stream(tokio_socket);
|
||||
|
||||
while let Some(Ok(message)) = server_connection.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{err}");
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {message:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let message = Request::ListValidNamePrefixes;
|
||||
|
||||
if let Err(err) = server_connection.send(message).await {
|
||||
server_connection.close().await.ok();
|
||||
anyhow::bail!(anyhow::Error::from(err).context("Failed to communicate with server"));
|
||||
}
|
||||
|
||||
let result = match server_connection.next().await {
|
||||
Some(Ok(Response::ListValidNamePrefixes(prefixes))) => prefixes,
|
||||
response => return erroneous_server_response(response).map(|()| vec![]),
|
||||
};
|
||||
|
||||
server_connection.send(Request::Exit).await?;
|
||||
|
||||
let result = result
|
||||
.into_iter()
|
||||
.map(|prefix| prefix + "_")
|
||||
.map(CompletionCandidate::new)
|
||||
.collect();
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
//! This module contains some base datastructures and functionality for dealing with
|
||||
//! database privileges in `MySQL`.
|
||||
//! database privileges in MySQL.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
@@ -49,7 +49,6 @@ pub struct DatabasePrivilegeRow {
|
||||
|
||||
impl DatabasePrivilegeRow {
|
||||
/// Gets the value of a privilege by its name as a &str.
|
||||
#[must_use]
|
||||
pub fn get_privilege_by_name(&self, name: &str) -> Option<bool> {
|
||||
match name {
|
||||
"select_priv" => Some(self.select_priv),
|
||||
@@ -84,7 +83,6 @@ impl fmt::Display for DatabasePrivilegeRow {
|
||||
}
|
||||
|
||||
/// Converts a database privilege field name to a human-readable name.
|
||||
#[must_use]
|
||||
pub fn db_priv_field_human_readable_name(name: &str) -> String {
|
||||
match name {
|
||||
"Db" => "Database".to_owned(),
|
||||
@@ -100,13 +98,12 @@ pub fn db_priv_field_human_readable_name(name: &str) -> String {
|
||||
"create_tmp_table_priv" => "Temp".to_owned(),
|
||||
"lock_tables_priv" => "Lock".to_owned(),
|
||||
"references_priv" => "References".to_owned(),
|
||||
_ => format!("Unknown({name})"),
|
||||
_ => format!("Unknown({})", name),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a database privilege field name to a single-character name.
|
||||
/// (the characters from the cli privilege editor)
|
||||
#[must_use]
|
||||
pub fn db_priv_field_single_character_name(name: &str) -> &str {
|
||||
match name {
|
||||
"select_priv" => "s",
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
//! This module contains serialization and deserialization logic for
|
||||
//! database privileges related CLI commands.
|
||||
|
||||
use itertools::Itertools;
|
||||
|
||||
use super::diff::{DatabasePrivilegeChange, DatabasePrivilegeRowDiff};
|
||||
use crate::core::types::{MySQLDatabase, MySQLUser};
|
||||
|
||||
const VALID_PRIVILEGE_EDIT_CHARS: &[char] = &[
|
||||
's', 'i', 'u', 'd', 'c', 'D', 'a', 'A', 'I', 't', 'l', 'r', 'A',
|
||||
];
|
||||
|
||||
/// This enum represents a part of a CLI argument for editing database privileges,
|
||||
/// indicating whether privileges are to be added, set, or removed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -19,74 +13,17 @@ pub enum DatabasePrivilegeEditEntryType {
|
||||
Remove,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DatabasePrivilegeEdit {
|
||||
pub type_: DatabasePrivilegeEditEntryType,
|
||||
pub privileges: Vec<char>,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeEdit {
|
||||
pub fn parse_from_str(input: &str) -> anyhow::Result<Self> {
|
||||
let (edit_type, privs_str) = if let Some(privs_str) = input.strip_prefix('+') {
|
||||
(DatabasePrivilegeEditEntryType::Add, privs_str)
|
||||
} else if let Some(privs_str) = input.strip_prefix('-') {
|
||||
(DatabasePrivilegeEditEntryType::Remove, privs_str)
|
||||
} else {
|
||||
(DatabasePrivilegeEditEntryType::Set, input)
|
||||
};
|
||||
|
||||
let privileges: Vec<char> = privs_str.chars().collect();
|
||||
|
||||
if privileges
|
||||
.iter()
|
||||
.any(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
|
||||
{
|
||||
let invalid_chars: String = privileges
|
||||
.iter()
|
||||
.filter(|c| !VALID_PRIVILEGE_EDIT_CHARS.contains(c))
|
||||
.map(|c| format!("'{c}'"))
|
||||
.join(", ");
|
||||
let valid_characters: String = VALID_PRIVILEGE_EDIT_CHARS
|
||||
.iter()
|
||||
.map(|c| format!("'{c}'"))
|
||||
.join(", ");
|
||||
anyhow::bail!(
|
||||
"Invalid character(s) in privilege edit entry: {invalid_chars}\n\nValid characters are: {valid_characters}",
|
||||
);
|
||||
}
|
||||
|
||||
Ok(DatabasePrivilegeEdit {
|
||||
type_: edit_type,
|
||||
privileges,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for DatabasePrivilegeEdit {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
|
||||
DatabasePrivilegeEditEntryType::Set => {}
|
||||
DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
|
||||
}
|
||||
for priv_char in &self.privileges {
|
||||
write!(f, "{priv_char}")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// This struct represents a single CLI argument for editing database privileges.
|
||||
///
|
||||
/// This is typically parsed from a string looking like:
|
||||
///
|
||||
/// `database_name:username:[+|-]privileges`
|
||||
/// `[database_name:]username:[+|-]privileges`
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct DatabasePrivilegeEditEntry {
|
||||
pub database: MySQLDatabase,
|
||||
pub database: Option<MySQLDatabase>,
|
||||
pub user: MySQLUser,
|
||||
pub privilege_edit: DatabasePrivilegeEdit,
|
||||
pub type_: DatabasePrivilegeEditEntryType,
|
||||
pub privileges: Vec<String>,
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeEditEntry {
|
||||
@@ -94,41 +31,80 @@ impl DatabasePrivilegeEditEntry {
|
||||
///
|
||||
/// The expected format is:
|
||||
///
|
||||
/// `database_name:username:[+|-]privileges`
|
||||
/// `[database_name:]username:[+|-]privileges`
|
||||
///
|
||||
/// where:
|
||||
/// - `database_name` is the name of the database to edit privileges for
|
||||
/// - database_name is optional, if omitted the entry applies to all databases
|
||||
/// - username is the name of the user to edit privileges for
|
||||
/// - privileges is a string of characters representing the privileges to add, set or remove
|
||||
/// - the `+` or `-` prefix indicates whether to add or remove the privileges, if omitted the privileges are set directly
|
||||
/// - privileges characters are: siudcDaAItlrA
|
||||
pub fn parse_from_str(arg: &str) -> anyhow::Result<Self> {
|
||||
pub fn parse_from_str(arg: &str) -> anyhow::Result<DatabasePrivilegeEditEntry> {
|
||||
let parts: Vec<&str> = arg.split(':').collect();
|
||||
if parts.len() != 3 {
|
||||
anyhow::bail!("Invalid privilege edit entry format: {arg}");
|
||||
if parts.len() < 2 || parts.len() > 3 {
|
||||
anyhow::bail!("Invalid privilege edit entry format: {}", arg);
|
||||
}
|
||||
|
||||
let (database, user, user_privs) = (parts[0].to_string(), parts[1].to_string(), parts[2]);
|
||||
let (database, user, user_privs) = if parts.len() == 3 {
|
||||
(Some(parts[0].to_string()), parts[1].to_string(), parts[2])
|
||||
} else {
|
||||
(None, parts[0].to_string(), parts[1])
|
||||
};
|
||||
|
||||
if user.is_empty() {
|
||||
anyhow::bail!("Username cannot be empty in privilege edit entry: {arg}");
|
||||
anyhow::bail!("Username cannot be empty in privilege edit entry: {}", arg);
|
||||
}
|
||||
|
||||
let privilege_edit = DatabasePrivilegeEdit::parse_from_str(user_privs)?;
|
||||
let (edit_type, privs_str) = if let Some(privs_str) = user_privs.strip_prefix('+') {
|
||||
(DatabasePrivilegeEditEntryType::Add, privs_str)
|
||||
} else if let Some(privs_str) = user_privs.strip_prefix('-') {
|
||||
(DatabasePrivilegeEditEntryType::Remove, privs_str)
|
||||
} else {
|
||||
(DatabasePrivilegeEditEntryType::Set, user_privs)
|
||||
};
|
||||
|
||||
let privileges: Vec<String> = privs_str.chars().map(|c| c.to_string()).collect();
|
||||
if privileges.iter().any(|c| !"siudcDaAItlrA".contains(c)) {
|
||||
let invalid_chars: String = privileges
|
||||
.iter()
|
||||
.filter(|c| !"siudcDaAItlrA".contains(c.as_str()))
|
||||
.cloned()
|
||||
.collect();
|
||||
anyhow::bail!(
|
||||
"Invalid character(s) in privilege edit entry: {}",
|
||||
invalid_chars
|
||||
);
|
||||
}
|
||||
|
||||
Ok(DatabasePrivilegeEditEntry {
|
||||
database: MySQLDatabase::from(database),
|
||||
database: database.map(MySQLDatabase::from),
|
||||
user: MySQLUser::from(user),
|
||||
privilege_edit,
|
||||
type_: edit_type,
|
||||
privileges,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn as_database_privileges_diff(&self) -> anyhow::Result<DatabasePrivilegeRowDiff> {
|
||||
pub fn as_database_privileges_diff(
|
||||
&self,
|
||||
external_database_name: Option<&MySQLDatabase>,
|
||||
) -> anyhow::Result<DatabasePrivilegeRowDiff> {
|
||||
let database = match self.database.as_ref() {
|
||||
Some(db) => db.clone(),
|
||||
None => {
|
||||
if let Some(external_db) = external_database_name {
|
||||
external_db.clone()
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Database name must be specified either in the privilege edit entry or as an external argument."
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut diff;
|
||||
match self.privilege_edit.type_ {
|
||||
match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Set => {
|
||||
diff = DatabasePrivilegeRowDiff {
|
||||
db: self.database.clone(),
|
||||
db: database,
|
||||
user: self.user.clone(),
|
||||
select_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
insert_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
@@ -142,20 +118,20 @@ impl DatabasePrivilegeEditEntry {
|
||||
lock_tables_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
references_priv: Some(DatabasePrivilegeChange::YesToNo),
|
||||
};
|
||||
for priv_char in &self.privilege_edit.privileges {
|
||||
match priv_char {
|
||||
's' => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'i' => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'u' => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'd' => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'c' => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'D' => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'a' => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'I' => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
't' => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'l' => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'r' => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
'A' => {
|
||||
for priv_char in &self.privileges {
|
||||
match priv_char.as_str() {
|
||||
"s" => diff.select_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"i" => diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"u" => diff.update_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"d" => diff.delete_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"c" => diff.create_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"D" => diff.drop_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"a" => diff.alter_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"I" => diff.index_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"t" => diff.create_tmp_table_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"l" => diff.lock_tables_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"r" => diff.references_priv = Some(DatabasePrivilegeChange::NoToYes),
|
||||
"A" => {
|
||||
diff.select_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.insert_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
diff.update_priv = Some(DatabasePrivilegeChange::NoToYes);
|
||||
@@ -174,7 +150,7 @@ impl DatabasePrivilegeEditEntry {
|
||||
}
|
||||
DatabasePrivilegeEditEntryType::Add | DatabasePrivilegeEditEntryType::Remove => {
|
||||
diff = DatabasePrivilegeRowDiff {
|
||||
db: self.database.clone(),
|
||||
db: database,
|
||||
user: self.user.clone(),
|
||||
select_priv: None,
|
||||
insert_priv: None,
|
||||
@@ -188,25 +164,25 @@ impl DatabasePrivilegeEditEntry {
|
||||
lock_tables_priv: None,
|
||||
references_priv: None,
|
||||
};
|
||||
let value = match self.privilege_edit.type_ {
|
||||
let value = match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Add => DatabasePrivilegeChange::NoToYes,
|
||||
DatabasePrivilegeEditEntryType::Remove => DatabasePrivilegeChange::YesToNo,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
for priv_char in &self.privilege_edit.privileges {
|
||||
match priv_char {
|
||||
's' => diff.select_priv = Some(value),
|
||||
'i' => diff.insert_priv = Some(value),
|
||||
'u' => diff.update_priv = Some(value),
|
||||
'd' => diff.delete_priv = Some(value),
|
||||
'c' => diff.create_priv = Some(value),
|
||||
'D' => diff.drop_priv = Some(value),
|
||||
'a' => diff.alter_priv = Some(value),
|
||||
'I' => diff.index_priv = Some(value),
|
||||
't' => diff.create_tmp_table_priv = Some(value),
|
||||
'l' => diff.lock_tables_priv = Some(value),
|
||||
'r' => diff.references_priv = Some(value),
|
||||
'A' => {
|
||||
for priv_char in &self.privileges {
|
||||
match priv_char.as_str() {
|
||||
"s" => diff.select_priv = Some(value),
|
||||
"i" => diff.insert_priv = Some(value),
|
||||
"u" => diff.update_priv = Some(value),
|
||||
"d" => diff.delete_priv = Some(value),
|
||||
"c" => diff.create_priv = Some(value),
|
||||
"D" => diff.drop_priv = Some(value),
|
||||
"a" => diff.alter_priv = Some(value),
|
||||
"I" => diff.index_priv = Some(value),
|
||||
"t" => diff.create_tmp_table_priv = Some(value),
|
||||
"l" => diff.lock_tables_priv = Some(value),
|
||||
"r" => diff.references_priv = Some(value),
|
||||
"A" => {
|
||||
diff.select_priv = Some(value);
|
||||
diff.insert_priv = Some(value);
|
||||
diff.update_priv = Some(value);
|
||||
@@ -231,9 +207,19 @@ impl DatabasePrivilegeEditEntry {
|
||||
|
||||
impl std::fmt::Display for DatabasePrivilegeEditEntry {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}:, ", self.database)?;
|
||||
if let Some(db) = &self.database {
|
||||
write!(f, "{}:, ", db)?;
|
||||
}
|
||||
write!(f, "{}: ", self.user)?;
|
||||
write!(f, "{}", self.privilege_edit)?;
|
||||
match self.type_ {
|
||||
DatabasePrivilegeEditEntryType::Add => write!(f, "+")?,
|
||||
DatabasePrivilegeEditEntryType::Set => {}
|
||||
DatabasePrivilegeEditEntryType::Remove => write!(f, "-")?,
|
||||
}
|
||||
for priv_char in &self.privileges {
|
||||
write!(f, "{}", priv_char)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -248,12 +234,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: "db".into(),
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
privilege_edit: DatabasePrivilegeEdit {
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!['A'],
|
||||
},
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!["A".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -264,12 +248,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: "db".into(),
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
privilege_edit: DatabasePrivilegeEdit {
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec![],
|
||||
},
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec![],
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -280,16 +262,28 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: "db".into(),
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
privilege_edit: DatabasePrivilegeEdit {
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!['s', 'i', 'u', 'd'],
|
||||
},
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_user_nonexistent_misc() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("user:siud");
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: None,
|
||||
user: "user".into(),
|
||||
type_: DatabasePrivilegeEditEntryType::Set,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cli_arg_parse_set_db_user_nonexistent_privilege() {
|
||||
let result = DatabasePrivilegeEditEntry::parse_from_str("db:user:F");
|
||||
@@ -314,12 +308,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: "db".into(),
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
privilege_edit: DatabasePrivilegeEdit {
|
||||
type_: DatabasePrivilegeEditEntryType::Add,
|
||||
privileges: vec!['s', 'i', 'u', 'd'],
|
||||
},
|
||||
type_: DatabasePrivilegeEditEntryType::Add,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -330,12 +322,10 @@ mod tests {
|
||||
assert_eq!(
|
||||
result.ok(),
|
||||
Some(DatabasePrivilegeEditEntry {
|
||||
database: "db".into(),
|
||||
database: Some("db".into()),
|
||||
user: "user".into(),
|
||||
privilege_edit: DatabasePrivilegeEdit {
|
||||
type_: DatabasePrivilegeEditEntryType::Remove,
|
||||
privileges: vec!['s', 'i', 'u', 'd'],
|
||||
},
|
||||
type_: DatabasePrivilegeEditEntryType::Remove,
|
||||
privileges: vec!["s".into(), "i".into(), "u".into(), "d".into()],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ pub enum DatabasePrivilegeChange {
|
||||
}
|
||||
|
||||
impl DatabasePrivilegeChange {
|
||||
#[must_use]
|
||||
pub fn new(p1: bool, p2: bool) -> Option<DatabasePrivilegeChange> {
|
||||
match (p1, p2) {
|
||||
(true, false) => Some(DatabasePrivilegeChange::YesToNo),
|
||||
@@ -50,7 +49,6 @@ pub struct DatabasePrivilegeRowDiff {
|
||||
|
||||
impl DatabasePrivilegeRowDiff {
|
||||
/// Calculates the difference between two [`DatabasePrivilegeRow`] instances.
|
||||
#[must_use]
|
||||
pub fn from_rows(
|
||||
row1: &DatabasePrivilegeRow,
|
||||
row2: &DatabasePrivilegeRow,
|
||||
@@ -58,8 +56,8 @@ impl DatabasePrivilegeRowDiff {
|
||||
debug_assert!(row1.db == row2.db && row1.user == row2.user);
|
||||
|
||||
DatabasePrivilegeRowDiff {
|
||||
db: row1.db.clone(),
|
||||
user: row1.user.clone(),
|
||||
db: row1.db.to_owned(),
|
||||
user: row1.user.to_owned(),
|
||||
select_priv: DatabasePrivilegeChange::new(row1.select_priv, row2.select_priv),
|
||||
insert_priv: DatabasePrivilegeChange::new(row1.insert_priv, row2.insert_priv),
|
||||
update_priv: DatabasePrivilegeChange::new(row1.update_priv, row2.update_priv),
|
||||
@@ -84,7 +82,6 @@ impl DatabasePrivilegeRowDiff {
|
||||
}
|
||||
|
||||
/// Returns true if there are no changes in this diff.
|
||||
#[must_use]
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.select_priv.is_none()
|
||||
&& self.insert_priv.is_none()
|
||||
@@ -116,7 +113,7 @@ impl DatabasePrivilegeRowDiff {
|
||||
"create_tmp_table_priv" => Ok(self.create_tmp_table_priv),
|
||||
"lock_tables_priv" => Ok(self.lock_tables_priv),
|
||||
"references_priv" => Ok(self.references_priv),
|
||||
_ => anyhow::bail!("Unknown privilege name: {privilege_name}"),
|
||||
_ => anyhow::bail!("Unknown privilege name: {}", privilege_name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,7 +159,7 @@ impl DatabasePrivilegeRowDiff {
|
||||
/// Removes any no-op changes from the diff, based on the original privilege row.
|
||||
fn remove_noops(&mut self, from: &DatabasePrivilegeRow) {
|
||||
fn new_value(
|
||||
change: Option<&DatabasePrivilegeChange>,
|
||||
change: &Option<DatabasePrivilegeChange>,
|
||||
from_value: bool,
|
||||
) -> Option<DatabasePrivilegeChange> {
|
||||
change.as_ref().and_then(|c| match c {
|
||||
@@ -176,24 +173,22 @@ impl DatabasePrivilegeRowDiff {
|
||||
})
|
||||
}
|
||||
|
||||
self.select_priv = new_value(self.select_priv.as_ref(), from.select_priv);
|
||||
self.insert_priv = new_value(self.insert_priv.as_ref(), from.insert_priv);
|
||||
self.update_priv = new_value(self.update_priv.as_ref(), from.update_priv);
|
||||
self.delete_priv = new_value(self.delete_priv.as_ref(), from.delete_priv);
|
||||
self.create_priv = new_value(self.create_priv.as_ref(), from.create_priv);
|
||||
self.drop_priv = new_value(self.drop_priv.as_ref(), from.drop_priv);
|
||||
self.alter_priv = new_value(self.alter_priv.as_ref(), from.alter_priv);
|
||||
self.index_priv = new_value(self.index_priv.as_ref(), from.index_priv);
|
||||
self.create_tmp_table_priv = new_value(
|
||||
self.create_tmp_table_priv.as_ref(),
|
||||
from.create_tmp_table_priv,
|
||||
);
|
||||
self.lock_tables_priv = new_value(self.lock_tables_priv.as_ref(), from.lock_tables_priv);
|
||||
self.references_priv = new_value(self.references_priv.as_ref(), from.references_priv);
|
||||
self.select_priv = new_value(&self.select_priv, from.select_priv);
|
||||
self.insert_priv = new_value(&self.insert_priv, from.insert_priv);
|
||||
self.update_priv = new_value(&self.update_priv, from.update_priv);
|
||||
self.delete_priv = new_value(&self.delete_priv, from.delete_priv);
|
||||
self.create_priv = new_value(&self.create_priv, from.create_priv);
|
||||
self.drop_priv = new_value(&self.drop_priv, from.drop_priv);
|
||||
self.alter_priv = new_value(&self.alter_priv, from.alter_priv);
|
||||
self.index_priv = new_value(&self.index_priv, from.index_priv);
|
||||
self.create_tmp_table_priv =
|
||||
new_value(&self.create_tmp_table_priv, from.create_tmp_table_priv);
|
||||
self.lock_tables_priv = new_value(&self.lock_tables_priv, from.lock_tables_priv);
|
||||
self.references_priv = new_value(&self.references_priv, from.references_priv);
|
||||
}
|
||||
|
||||
fn apply(&self, base: &mut DatabasePrivilegeRow) {
|
||||
fn apply_change(change: Option<&DatabasePrivilegeChange>, target: &mut bool) {
|
||||
fn apply_change(change: &Option<DatabasePrivilegeChange>, target: &mut bool) {
|
||||
match change {
|
||||
Some(DatabasePrivilegeChange::YesToNo) => *target = false,
|
||||
Some(DatabasePrivilegeChange::NoToYes) => *target = true,
|
||||
@@ -201,20 +196,17 @@ impl DatabasePrivilegeRowDiff {
|
||||
}
|
||||
}
|
||||
|
||||
apply_change(self.select_priv.as_ref(), &mut base.select_priv);
|
||||
apply_change(self.insert_priv.as_ref(), &mut base.insert_priv);
|
||||
apply_change(self.update_priv.as_ref(), &mut base.update_priv);
|
||||
apply_change(self.delete_priv.as_ref(), &mut base.delete_priv);
|
||||
apply_change(self.create_priv.as_ref(), &mut base.create_priv);
|
||||
apply_change(self.drop_priv.as_ref(), &mut base.drop_priv);
|
||||
apply_change(self.alter_priv.as_ref(), &mut base.alter_priv);
|
||||
apply_change(self.index_priv.as_ref(), &mut base.index_priv);
|
||||
apply_change(
|
||||
self.create_tmp_table_priv.as_ref(),
|
||||
&mut base.create_tmp_table_priv,
|
||||
);
|
||||
apply_change(self.lock_tables_priv.as_ref(), &mut base.lock_tables_priv);
|
||||
apply_change(self.references_priv.as_ref(), &mut base.references_priv);
|
||||
apply_change(&self.select_priv, &mut base.select_priv);
|
||||
apply_change(&self.insert_priv, &mut base.insert_priv);
|
||||
apply_change(&self.update_priv, &mut base.update_priv);
|
||||
apply_change(&self.delete_priv, &mut base.delete_priv);
|
||||
apply_change(&self.create_priv, &mut base.create_priv);
|
||||
apply_change(&self.drop_priv, &mut base.drop_priv);
|
||||
apply_change(&self.alter_priv, &mut base.alter_priv);
|
||||
apply_change(&self.index_priv, &mut base.index_priv);
|
||||
apply_change(&self.create_tmp_table_priv, &mut base.create_tmp_table_priv);
|
||||
apply_change(&self.lock_tables_priv, &mut base.lock_tables_priv);
|
||||
apply_change(&self.references_priv, &mut base.references_priv);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +214,7 @@ impl fmt::Display for DatabasePrivilegeRowDiff {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fn format_change(
|
||||
f: &mut fmt::Formatter<'_>,
|
||||
change: Option<DatabasePrivilegeChange>,
|
||||
change: &Option<DatabasePrivilegeChange>,
|
||||
field_name: &str,
|
||||
) -> fmt::Result {
|
||||
if let Some(change) = change {
|
||||
@@ -241,17 +233,17 @@ impl fmt::Display for DatabasePrivilegeRowDiff {
|
||||
}
|
||||
}
|
||||
|
||||
format_change(f, self.select_priv, "select_priv")?;
|
||||
format_change(f, self.insert_priv, "insert_priv")?;
|
||||
format_change(f, self.update_priv, "update_priv")?;
|
||||
format_change(f, self.delete_priv, "delete_priv")?;
|
||||
format_change(f, self.create_priv, "create_priv")?;
|
||||
format_change(f, self.drop_priv, "drop_priv")?;
|
||||
format_change(f, self.alter_priv, "alter_priv")?;
|
||||
format_change(f, self.index_priv, "index_priv")?;
|
||||
format_change(f, self.create_tmp_table_priv, "create_tmp_table_priv")?;
|
||||
format_change(f, self.lock_tables_priv, "lock_tables_priv")?;
|
||||
format_change(f, self.references_priv, "references_priv")?;
|
||||
format_change(f, &self.select_priv, "select_priv")?;
|
||||
format_change(f, &self.insert_priv, "insert_priv")?;
|
||||
format_change(f, &self.update_priv, "update_priv")?;
|
||||
format_change(f, &self.delete_priv, "delete_priv")?;
|
||||
format_change(f, &self.create_priv, "create_priv")?;
|
||||
format_change(f, &self.drop_priv, "drop_priv")?;
|
||||
format_change(f, &self.alter_priv, "alter_priv")?;
|
||||
format_change(f, &self.index_priv, "index_priv")?;
|
||||
format_change(f, &self.create_tmp_table_priv, "create_tmp_table_priv")?;
|
||||
format_change(f, &self.lock_tables_priv, "lock_tables_priv")?;
|
||||
format_change(f, &self.references_priv, "references_priv")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -267,7 +259,6 @@ pub enum DatabasePrivilegesDiff {
|
||||
}
|
||||
|
||||
impl DatabasePrivilegesDiff {
|
||||
#[must_use]
|
||||
pub fn get_database_name(&self) -> &MySQLDatabase {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.db,
|
||||
@@ -277,7 +268,6 @@ impl DatabasePrivilegesDiff {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn get_user_name(&self) -> &MySQLUser {
|
||||
match self {
|
||||
DatabasePrivilegesDiff::New(p) => &p.user,
|
||||
@@ -315,7 +305,7 @@ impl DatabasePrivilegesDiff {
|
||||
}
|
||||
|
||||
if matches!(self, DatabasePrivilegesDiff::Noop { .. }) {
|
||||
other.clone_into(self);
|
||||
*self = other.to_owned();
|
||||
return Ok(());
|
||||
} else if matches!(other, DatabasePrivilegesDiff::Noop { .. }) {
|
||||
return Ok(());
|
||||
@@ -337,8 +327,8 @@ impl DatabasePrivilegesDiff {
|
||||
inner_diff.mappend(modified);
|
||||
|
||||
if inner_diff.is_empty() {
|
||||
let db = inner_diff.db.clone();
|
||||
let user = inner_diff.user.clone();
|
||||
let db = inner_diff.db.to_owned();
|
||||
let user = inner_diff.user.to_owned();
|
||||
*self = DatabasePrivilegesDiff::Noop { db, user };
|
||||
}
|
||||
}
|
||||
@@ -362,27 +352,28 @@ pub type DatabasePrivilegeState<'a> = &'a [DatabasePrivilegeRow];
|
||||
/// This function calculates the differences between two sets of database privileges.
|
||||
/// It returns a set of [`DatabasePrivilegesDiff`] that can be used to display or
|
||||
/// apply a set of privilege modifications to the database.
|
||||
#[must_use]
|
||||
pub fn diff_privileges(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: &[DatabasePrivilegeRow],
|
||||
) -> BTreeSet<DatabasePrivilegesDiff> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.clone(), p.user.clone()), p))
|
||||
.collect();
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = to
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.clone(), p.user.clone()), p))
|
||||
.collect();
|
||||
let to_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
to.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result = BTreeSet::new();
|
||||
|
||||
for p in to {
|
||||
if let Some(old_p) = from_lookup_table.get(&(p.db.clone(), p.user.clone())) {
|
||||
if let Some(old_p) = from_lookup_table.get(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
let diff = DatabasePrivilegeRowDiff::from_rows(old_p, p);
|
||||
if !diff.is_empty() {
|
||||
result.insert(DatabasePrivilegesDiff::Modified(diff));
|
||||
@@ -393,7 +384,7 @@ pub fn diff_privileges(
|
||||
}
|
||||
|
||||
for p in from {
|
||||
if !to_lookup_table.contains_key(&(p.db.clone(), p.user.clone())) {
|
||||
if !to_lookup_table.contains_key(&(p.db.to_owned(), p.user.to_owned())) {
|
||||
result.insert(DatabasePrivilegesDiff::Deleted(p.to_owned()));
|
||||
}
|
||||
}
|
||||
@@ -409,16 +400,17 @@ pub fn create_or_modify_privilege_rows(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: &BTreeSet<DatabasePrivilegeRowDiff>,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.clone(), p.user.clone()), p))
|
||||
.collect();
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result = BTreeSet::new();
|
||||
|
||||
for diff in to {
|
||||
if let Some(old_p) = from_lookup_table.get(&(diff.db.clone(), diff.user.clone())) {
|
||||
if let Some(old_p) = from_lookup_table.get(&(diff.db.to_owned(), diff.user.to_owned())) {
|
||||
let mut modified_diff = diff.to_owned();
|
||||
modified_diff.remove_noops(old_p);
|
||||
if !modified_diff.is_empty() {
|
||||
@@ -426,8 +418,8 @@ pub fn create_or_modify_privilege_rows(
|
||||
}
|
||||
} else {
|
||||
let mut new_row = DatabasePrivilegeRow {
|
||||
db: diff.db.clone(),
|
||||
user: diff.user.clone(),
|
||||
db: diff.db.to_owned(),
|
||||
user: diff.user.to_owned(),
|
||||
select_priv: false,
|
||||
insert_priv: false,
|
||||
update_priv: false,
|
||||
@@ -458,11 +450,12 @@ pub fn reduce_privilege_diffs(
|
||||
from: DatabasePrivilegeState<'_>,
|
||||
to: BTreeSet<DatabasePrivilegesDiff>,
|
||||
) -> anyhow::Result<BTreeSet<DatabasePrivilegesDiff>> {
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> = from
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.clone(), p.user.clone()), p))
|
||||
.collect();
|
||||
let from_lookup_table: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegeRow> =
|
||||
HashMap::from_iter(
|
||||
from.iter()
|
||||
.cloned()
|
||||
.map(|p| ((p.db.to_owned(), p.user.to_owned()), p)),
|
||||
);
|
||||
|
||||
let mut result: HashMap<(MySQLDatabase, MySQLUser), DatabasePrivilegesDiff> = from_lookup_table
|
||||
.iter()
|
||||
@@ -488,19 +481,19 @@ pub fn reduce_privilege_diffs(
|
||||
existing_diff.mappend(&diff)?;
|
||||
}
|
||||
Entry::Vacant(vacant_entry) => {
|
||||
vacant_entry.insert(diff.clone());
|
||||
vacant_entry.insert(diff.to_owned());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (key, diff) in &mut result {
|
||||
for (key, diff) in result.iter_mut() {
|
||||
if let Some(from_row) = from_lookup_table.get(key)
|
||||
&& let DatabasePrivilegesDiff::Modified(modified_diff) = diff
|
||||
{
|
||||
modified_diff.remove_noops(from_row);
|
||||
if modified_diff.is_empty() {
|
||||
let db = modified_diff.db.clone();
|
||||
let user = modified_diff.user.clone();
|
||||
let db = modified_diff.db.to_owned();
|
||||
let user = modified_diff.user.to_owned();
|
||||
*diff = DatabasePrivilegesDiff::Noop { db, user };
|
||||
}
|
||||
}
|
||||
@@ -513,7 +506,6 @@ pub fn reduce_privilege_diffs(
|
||||
}
|
||||
|
||||
/// Renders a set of [`DatabasePrivilegesDiff`] into a human-readable formatted table.
|
||||
#[must_use]
|
||||
pub fn display_privilege_diffs(diffs: &BTreeSet<DatabasePrivilegesDiff>) -> String {
|
||||
let mut table = Table::new();
|
||||
table.set_titles(row!["Database", "User", "Privilege diff",]);
|
||||
|
||||
@@ -13,11 +13,10 @@ use itertools::Itertools;
|
||||
use std::cmp::max;
|
||||
|
||||
/// Generates a single row of the privileges table for the editor.
|
||||
#[must_use]
|
||||
pub fn format_privileges_line_for_editor(
|
||||
privs: &DatabasePrivilegeRow,
|
||||
database_name_len: usize,
|
||||
username_len: usize,
|
||||
database_name_len: usize,
|
||||
) -> String {
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.into_iter()
|
||||
@@ -26,7 +25,6 @@ pub fn format_privileges_line_for_editor(
|
||||
"User" => format!("{:width$}", privs.user, width = username_len),
|
||||
privilege => format!(
|
||||
"{:width$}",
|
||||
// SAFETY: unwrap is safe here because the field names are static
|
||||
yn(privs.get_privilege_by_name(privilege).unwrap()),
|
||||
width = db_priv_field_human_readable_name(privilege).len()
|
||||
),
|
||||
@@ -36,14 +34,14 @@ pub fn format_privileges_line_for_editor(
|
||||
.to_string()
|
||||
}
|
||||
|
||||
const EDITOR_COMMENT: &str = r"
|
||||
const EDITOR_COMMENT: &str = r#"
|
||||
# Welcome to the privilege editor.
|
||||
# Each line defines what privileges a single user has on a single database.
|
||||
# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.
|
||||
# If the user should have a certain privilege, write 'Y', otherwise write 'N'.
|
||||
#
|
||||
# Lines starting with '#' are comments and will be ignored.
|
||||
";
|
||||
"#;
|
||||
|
||||
/// Generates the content for the privilege editor.
|
||||
///
|
||||
@@ -54,9 +52,9 @@ pub fn generate_editor_content_from_privilege_data(
|
||||
unix_user: &str,
|
||||
database_name: Option<&MySQLDatabase>,
|
||||
) -> String {
|
||||
let example_user = format!("{unix_user}_user");
|
||||
let example_user = format!("{}_user", unix_user);
|
||||
let example_db = database_name
|
||||
.unwrap_or(&format!("{unix_user}_db").into())
|
||||
.unwrap_or(&format!("{}_db", unix_user).into())
|
||||
.to_string();
|
||||
|
||||
// NOTE: `.max()`` fails when the iterator is empty.
|
||||
@@ -107,8 +105,8 @@ pub fn generate_editor_content_from_privilege_data(
|
||||
lock_tables_priv: false,
|
||||
references_priv: false,
|
||||
},
|
||||
longest_database_name,
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
);
|
||||
|
||||
format!(
|
||||
@@ -116,15 +114,15 @@ pub fn generate_editor_content_from_privilege_data(
|
||||
EDITOR_COMMENT,
|
||||
header.join(" "),
|
||||
if privilege_data.is_empty() {
|
||||
format!("# {example_line}")
|
||||
format!("# {}", example_line)
|
||||
} else {
|
||||
privilege_data
|
||||
.iter()
|
||||
.map(|privs| {
|
||||
format_privileges_line_for_editor(
|
||||
privs,
|
||||
longest_database_name,
|
||||
longest_username,
|
||||
longest_database_name,
|
||||
)
|
||||
})
|
||||
.join("\n")
|
||||
@@ -147,8 +145,11 @@ enum PrivilegeRowParseResult {
|
||||
fn parse_privilege_cell_from_editor(yn: &str, name: &str) -> anyhow::Result<bool> {
|
||||
let human_readable_name = db_priv_field_human_readable_name(name);
|
||||
rev_yn(yn)
|
||||
.ok_or_else(|| anyhow!("Expected Y or N, found {yn}"))
|
||||
.context(format!("Could not parse '{human_readable_name}' privilege"))
|
||||
.ok_or_else(|| anyhow!("Expected Y or N, found {}", yn))
|
||||
.context(format!(
|
||||
"Could not parse '{}' privilege",
|
||||
human_readable_name
|
||||
))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -271,12 +272,12 @@ fn parse_privilege_row_from_editor(row: &str) -> PrivilegeRowParseResult {
|
||||
}
|
||||
|
||||
pub fn parse_privilege_data_from_editor_content(
|
||||
content: &str,
|
||||
content: String,
|
||||
) -> anyhow::Result<Vec<DatabasePrivilegeRow>> {
|
||||
content
|
||||
.trim()
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.split('\n')
|
||||
.map(|line| line.trim())
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
let mut header: Vec<_> = DATABASE_PRIVILEGE_FIELDS
|
||||
@@ -313,7 +314,7 @@ pub fn parse_privilege_data_from_editor_content(
|
||||
PrivilegeRowParseResult::Empty => Ok(None),
|
||||
}
|
||||
})
|
||||
.filter_map(std::result::Result::transpose)
|
||||
.filter_map(|result| result.transpose())
|
||||
.collect::<anyhow::Result<Vec<DatabasePrivilegeRow>>>()
|
||||
}
|
||||
|
||||
@@ -321,64 +322,6 @@ pub fn parse_privilege_data_from_editor_content(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_generate_editor_content_from_privilege_data() {
|
||||
let permissions = vec![
|
||||
DatabasePrivilegeRow {
|
||||
db: "test_abcdef".into(),
|
||||
user: "test_abcdef".into(),
|
||||
select_priv: true,
|
||||
insert_priv: false,
|
||||
update_priv: true,
|
||||
delete_priv: false,
|
||||
create_priv: true,
|
||||
drop_priv: false,
|
||||
alter_priv: true,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: false,
|
||||
references_priv: true,
|
||||
},
|
||||
DatabasePrivilegeRow {
|
||||
db: "test_abcdefghijlkmno".into(),
|
||||
user: "test_abcdef".into(),
|
||||
select_priv: true,
|
||||
insert_priv: false,
|
||||
update_priv: true,
|
||||
delete_priv: false,
|
||||
create_priv: true,
|
||||
drop_priv: false,
|
||||
alter_priv: true,
|
||||
index_priv: false,
|
||||
create_tmp_table_priv: true,
|
||||
lock_tables_priv: false,
|
||||
references_priv: true,
|
||||
},
|
||||
];
|
||||
|
||||
let content = generate_editor_content_from_privilege_data(&permissions, "test", None);
|
||||
|
||||
let expected_lines = vec![
|
||||
"",
|
||||
"# Welcome to the privilege editor.",
|
||||
"# Each line defines what privileges a single user has on a single database.",
|
||||
"# The first two columns respectively represent the database name and the user, and the remaining columns are the privileges.",
|
||||
"# If the user should have a certain privilege, write 'Y', otherwise write 'N'.",
|
||||
"#",
|
||||
"# Lines starting with '#' are comments and will be ignored.",
|
||||
"",
|
||||
"Database User Select Insert Update Delete Create Drop Alter Index Temp Lock References",
|
||||
"test_abcdef test_abcdef Y N Y N Y N Y N Y N Y",
|
||||
"test_abcdefghijlkmno test_abcdef Y N Y N Y N Y N Y N Y",
|
||||
];
|
||||
|
||||
let generated_lines: Vec<&str> = content.lines().collect();
|
||||
|
||||
assert_eq!(generated_lines, expected_lines);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_generated_and_parsed_editor_content_is_equal() {
|
||||
let permissions = vec![
|
||||
@@ -416,7 +359,7 @@ mod tests {
|
||||
|
||||
let content = generate_editor_content_from_privilege_data(&permissions, "user", None);
|
||||
|
||||
let parsed_permissions = parse_privilege_data_from_editor_content(&content).unwrap();
|
||||
let parsed_permissions = parse_privilege_data_from_editor_content(content).unwrap();
|
||||
|
||||
assert_eq!(permissions, parsed_permissions);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ mod list_all_users;
|
||||
mod list_databases;
|
||||
mod list_privileges;
|
||||
mod list_users;
|
||||
mod list_valid_name_prefixes;
|
||||
mod lock_users;
|
||||
mod modify_privileges;
|
||||
mod passwd_user;
|
||||
@@ -30,7 +29,6 @@ pub use list_all_users::*;
|
||||
pub use list_databases::*;
|
||||
pub use list_privileges::*;
|
||||
pub use list_users::*;
|
||||
pub use list_valid_name_prefixes::*;
|
||||
pub use lock_users::*;
|
||||
pub use modify_privileges::*;
|
||||
pub use passwd_user::*;
|
||||
@@ -55,26 +53,13 @@ pub type ClientToServerMessageStream = SerdeFramed<
|
||||
Bincode<Response, Request>,
|
||||
>;
|
||||
|
||||
const MAX_REQUEST_FRAME_LENGTH: usize = 100 * 1024; // 100 KB
|
||||
const MAX_RESPONSE_FRAME_LENGTH: usize = 1024 * 1024; // 1 MB
|
||||
|
||||
pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToServerMessageStream {
|
||||
let codec = {
|
||||
let mut codec = LengthDelimitedCodec::new();
|
||||
codec.set_max_frame_length(MAX_REQUEST_FRAME_LENGTH);
|
||||
codec
|
||||
};
|
||||
let length_delimited = Framed::new(socket, codec);
|
||||
pub fn create_server_to_client_message_stream(socket: UnixStream) -> ServerToClientMessageStream {
|
||||
let length_delimited = Framed::new(socket, LengthDelimitedCodec::new());
|
||||
tokio_serde::Framed::new(length_delimited, Bincode::default())
|
||||
}
|
||||
|
||||
pub fn create_server_to_client_message_stream(socket: UnixStream) -> ServerToClientMessageStream {
|
||||
let codec = {
|
||||
let mut codec = LengthDelimitedCodec::new();
|
||||
codec.set_max_frame_length(MAX_RESPONSE_FRAME_LENGTH);
|
||||
codec
|
||||
};
|
||||
let length_delimited = Framed::new(socket, codec);
|
||||
pub fn create_client_to_server_message_stream(socket: UnixStream) -> ClientToServerMessageStream {
|
||||
let length_delimited = Framed::new(socket, LengthDelimitedCodec::new());
|
||||
tokio_serde::Framed::new(length_delimited, Bincode::default())
|
||||
}
|
||||
|
||||
@@ -83,7 +68,6 @@ pub fn create_server_to_client_message_stream(socket: UnixStream) -> ServerToCli
|
||||
pub enum Request {
|
||||
CheckAuthorization(CheckAuthorizationRequest),
|
||||
|
||||
ListValidNamePrefixes,
|
||||
CompleteDatabaseName(CompleteDatabaseNameRequest),
|
||||
CompleteUserName(CompleteUserNameRequest),
|
||||
|
||||
@@ -111,7 +95,6 @@ pub enum Request {
|
||||
pub enum Response {
|
||||
CheckAuthorization(CheckAuthorizationResponse),
|
||||
|
||||
ListValidNamePrefixes(ListValidNamePrefixesResponse),
|
||||
CompleteDatabaseName(CompleteDatabaseNameResponse),
|
||||
CompleteUserName(CompleteUserNameResponse),
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ pub fn print_check_authorization_output_status(output: &CheckAuthorizationRespon
|
||||
println!("'{}': OK", db_or_user.name());
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
println!(
|
||||
"'{}': {}",
|
||||
db_or_user.name(),
|
||||
err.to_error_message(db_or_user)
|
||||
@@ -57,12 +57,10 @@ pub fn print_check_authorization_output_status_json(output: &CheckAuthorizationR
|
||||
}
|
||||
|
||||
impl CheckAuthorizationError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, db_or_user: &DbOrUser) -> String {
|
||||
self.0.to_error_message(db_or_user)
|
||||
self.0.to_error_message(db_or_user.clone())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
self.0.error_type()
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ pub fn print_create_databases_output_status(output: &CreateDatabasesResponse) {
|
||||
for (database_name, result) in output {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("Database '{database_name}' created successfully.");
|
||||
println!("Database '{}' created successfully.", database_name);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(database_name));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(database_name));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -63,22 +63,20 @@ pub fn print_create_databases_output_status_json(output: &CreateDatabasesRespons
|
||||
}
|
||||
|
||||
impl CreateDatabaseError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String {
|
||||
match self {
|
||||
CreateDatabaseError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::Database(database_name.clone()))
|
||||
err.to_error_message(DbOrUser::Database(database_name.clone()))
|
||||
}
|
||||
CreateDatabaseError::DatabaseAlreadyExists => {
|
||||
format!("Database {database_name} already exists.")
|
||||
format!("Database {} already exists.", database_name)
|
||||
}
|
||||
CreateDatabaseError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
CreateDatabaseError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -29,11 +29,11 @@ pub fn print_create_users_output_status(output: &CreateUsersResponse) {
|
||||
for (username, result) in output {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("User '{username}' created successfully.");
|
||||
println!("User '{}' created successfully.", username);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -63,22 +63,20 @@ pub fn print_create_users_output_status_json(output: &CreateUsersResponse) {
|
||||
}
|
||||
|
||||
impl CreateUserError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
CreateUserError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
CreateUserError::UserAlreadyExists => {
|
||||
format!("User '{username}' already exists.")
|
||||
format!("User '{}' already exists.", username)
|
||||
}
|
||||
CreateUserError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
CreateUserError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -35,8 +35,8 @@ pub fn print_drop_databases_output_status(output: &DropDatabasesResponse) {
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(database_name));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(database_name));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -66,22 +66,20 @@ pub fn print_drop_databases_output_status_json(output: &DropDatabasesResponse) {
|
||||
}
|
||||
|
||||
impl DropDatabaseError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String {
|
||||
match self {
|
||||
DropDatabaseError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::Database(database_name.clone()))
|
||||
err.to_error_message(DbOrUser::Database(database_name.clone()))
|
||||
}
|
||||
DropDatabaseError::DatabaseDoesNotExist => {
|
||||
format!("Database {database_name} does not exist.")
|
||||
format!("Database {} does not exist.", database_name)
|
||||
}
|
||||
DropDatabaseError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
DropDatabaseError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -29,11 +29,11 @@ pub fn print_drop_users_output_status(output: &DropUsersResponse) {
|
||||
for (username, result) in output {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("User '{username}' dropped successfully.");
|
||||
println!("User '{}' dropped successfully.", username);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -63,22 +63,20 @@ pub fn print_drop_users_output_status_json(output: &DropUsersResponse) {
|
||||
}
|
||||
|
||||
impl DropUserError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
DropUserError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
DropUserError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
DropUserError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
DropUserError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -12,15 +12,13 @@ pub enum ListAllDatabasesError {
|
||||
}
|
||||
|
||||
impl ListAllDatabasesError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self) -> String {
|
||||
match self {
|
||||
ListAllDatabasesError::MySqlError(err) => format!("MySQL error: {err}"),
|
||||
ListAllDatabasesError::MySqlError(err) => format!("MySQL error: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListAllDatabasesError::MySqlError(_) => "mysql-error".to_string(),
|
||||
|
||||
@@ -3,27 +3,26 @@ use thiserror::Error;
|
||||
|
||||
use crate::core::database_privileges::DatabasePrivilegeRow;
|
||||
|
||||
pub type ListAllPrivilegesResponse = Result<Vec<DatabasePrivilegeRow>, ListAllPrivilegesError>;
|
||||
pub type ListAllPrivilegesResponse =
|
||||
Result<Vec<DatabasePrivilegeRow>, GetAllDatabasesPrivilegeDataError>;
|
||||
|
||||
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ListAllPrivilegesError {
|
||||
pub enum GetAllDatabasesPrivilegeDataError {
|
||||
#[error("MySQL error: {0}")]
|
||||
MySqlError(String),
|
||||
}
|
||||
|
||||
impl ListAllPrivilegesError {
|
||||
#[must_use]
|
||||
impl GetAllDatabasesPrivilegeDataError {
|
||||
pub fn to_error_message(&self) -> String {
|
||||
match self {
|
||||
ListAllPrivilegesError::MySqlError(err) => format!("MySQL error: {err}"),
|
||||
GetAllDatabasesPrivilegeDataError::MySqlError(err) => format!("MySQL error: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListAllPrivilegesError::MySqlError(_) => "mysql-error".to_string(),
|
||||
GetAllDatabasesPrivilegeDataError::MySqlError(_) => "mysql-error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,15 +12,13 @@ pub enum ListAllUsersError {
|
||||
}
|
||||
|
||||
impl ListAllUsersError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self) -> String {
|
||||
match self {
|
||||
ListAllUsersError::MySqlError(err) => format!("MySQL error: {err}"),
|
||||
ListAllUsersError::MySqlError(err) => format!("MySQL error: {}", err),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListAllUsersError::MySqlError(_) => "mysql-error".to_string(),
|
||||
|
||||
@@ -30,10 +30,7 @@ pub enum ListDatabasesError {
|
||||
MySqlError(String),
|
||||
}
|
||||
|
||||
pub fn print_list_databases_output_status(
|
||||
output: &ListDatabasesResponse,
|
||||
display_size_as_bytes: bool,
|
||||
) {
|
||||
pub fn print_list_databases_output_status(output: &ListDatabasesResponse) {
|
||||
let mut final_database_list: Vec<&DatabaseRow> = Vec::new();
|
||||
for (db_name, db_result) in output {
|
||||
match db_result {
|
||||
@@ -55,11 +52,7 @@ pub fn print_list_databases_output_status(
|
||||
"Users",
|
||||
"Collation",
|
||||
"Character Set",
|
||||
if display_size_as_bytes {
|
||||
"Size (Bytes)"
|
||||
} else {
|
||||
"Size"
|
||||
}
|
||||
"Size (Bytes)"
|
||||
]);
|
||||
for db in final_database_list {
|
||||
table.add_row(row![
|
||||
@@ -68,11 +61,7 @@ pub fn print_list_databases_output_status(
|
||||
db.users.iter().map(|user| user.as_str()).join("\n"),
|
||||
db.collation.as_deref().unwrap_or("N/A"),
|
||||
db.character_set.as_deref().unwrap_or("N/A"),
|
||||
if display_size_as_bytes {
|
||||
db.size_bytes.to_string()
|
||||
} else {
|
||||
humansize::format_size(db.size_bytes, humansize::DECIMAL)
|
||||
}
|
||||
db.size_bytes,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -113,22 +102,20 @@ pub fn print_list_databases_output_status_json(output: &ListDatabasesResponse) {
|
||||
}
|
||||
|
||||
impl ListDatabasesError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String {
|
||||
match self {
|
||||
ListDatabasesError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::Database(database_name.clone()))
|
||||
err.to_error_message(DbOrUser::Database(database_name.clone()))
|
||||
}
|
||||
ListDatabasesError::DatabaseDoesNotExist => {
|
||||
format!("Database '{database_name}' does not exist.")
|
||||
format!("Database '{}' does not exist.", database_name)
|
||||
}
|
||||
ListDatabasesError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListDatabasesError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::core::{
|
||||
pub type ListPrivilegesRequest = Option<Vec<MySQLDatabase>>;
|
||||
|
||||
pub type ListPrivilegesResponse =
|
||||
BTreeMap<MySQLDatabase, Result<Vec<DatabasePrivilegeRow>, ListPrivilegesError>>;
|
||||
BTreeMap<MySQLDatabase, Result<Vec<DatabasePrivilegeRow>, GetDatabasesPrivilegeDataError>>;
|
||||
|
||||
pub fn print_list_privileges_output_status(output: &ListPrivilegesResponse, long_names: bool) {
|
||||
let mut final_privs_map: BTreeMap<MySQLDatabase, Vec<DatabasePrivilegeRow>> = BTreeMap::new();
|
||||
@@ -65,7 +65,7 @@ pub fn print_list_privileges_output_status(output: &ListPrivilegesResponse, long
|
||||
));
|
||||
|
||||
for (_database, rows) in final_privs_map {
|
||||
for row in &rows {
|
||||
for row in rows.iter() {
|
||||
table.add_row(row![
|
||||
row.db,
|
||||
row.user,
|
||||
@@ -117,7 +117,7 @@ pub fn print_list_privileges_output_status_json(output: &ListPrivilegesResponse)
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ListPrivilegesError {
|
||||
pub enum GetDatabasesPrivilegeDataError {
|
||||
#[error("Validation error: {0}")]
|
||||
ValidationError(#[from] ValidationError),
|
||||
|
||||
@@ -128,28 +128,28 @@ pub enum ListPrivilegesError {
|
||||
MySqlError(String),
|
||||
}
|
||||
|
||||
impl ListPrivilegesError {
|
||||
#[must_use]
|
||||
impl GetDatabasesPrivilegeDataError {
|
||||
pub fn to_error_message(&self, database_name: &MySQLDatabase) -> String {
|
||||
match self {
|
||||
ListPrivilegesError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::Database(database_name.clone()))
|
||||
GetDatabasesPrivilegeDataError::ValidationError(err) => {
|
||||
err.to_error_message(DbOrUser::Database(database_name.clone()))
|
||||
}
|
||||
ListPrivilegesError::DatabaseDoesNotExist => {
|
||||
format!("Database '{database_name}' does not exist.")
|
||||
GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => {
|
||||
format!("Database '{}' does not exist.", database_name)
|
||||
}
|
||||
ListPrivilegesError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
GetDatabasesPrivilegeDataError::MySqlError(err) => {
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListPrivilegesError::ValidationError(err) => err.error_type(),
|
||||
ListPrivilegesError::DatabaseDoesNotExist => "database-does-not-exist".to_string(),
|
||||
ListPrivilegesError::MySqlError(_) => "mysql-error".to_string(),
|
||||
GetDatabasesPrivilegeDataError::ValidationError(err) => err.error_type(),
|
||||
GetDatabasesPrivilegeDataError::DatabaseDoesNotExist => {
|
||||
"database-does-not-exist".to_string()
|
||||
}
|
||||
GetDatabasesPrivilegeDataError::MySqlError(_) => "mysql-error".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,22 +97,20 @@ pub fn print_list_users_output_status_json(output: &ListUsersResponse) {
|
||||
}
|
||||
|
||||
impl ListUsersError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
ListUsersError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
ListUsersError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
ListUsersError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ListUsersError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub type ListValidNamePrefixesResponse = Vec<String>;
|
||||
@@ -32,11 +32,11 @@ pub fn print_lock_users_output_status(output: &LockUsersResponse) {
|
||||
for (username, result) in output {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("User '{username}' locked successfully.");
|
||||
println!("User '{}' locked successfully.", username);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -66,25 +66,23 @@ pub fn print_lock_users_output_status_json(output: &LockUsersResponse) {
|
||||
}
|
||||
|
||||
impl LockUserError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
LockUserError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
LockUserError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
LockUserError::UserIsAlreadyLocked => {
|
||||
format!("User '{username}' is already locked.")
|
||||
format!("User '{}' is already locked.", username)
|
||||
}
|
||||
LockUserError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
LockUserError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -53,12 +53,13 @@ pub fn print_modify_database_privileges_output_status(output: &ModifyPrivilegesR
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!(
|
||||
"Privileges for user '{username}' on database '{database_name}' modified successfully."
|
||||
"Privileges for user '{}' on database '{}' modified successfully.",
|
||||
username, database_name
|
||||
);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(database_name, username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(database_name, username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -66,20 +67,19 @@ pub fn print_modify_database_privileges_output_status(output: &ModifyPrivilegesR
|
||||
}
|
||||
|
||||
impl ModifyDatabasePrivilegesError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, database_name: &MySQLDatabase, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
ModifyDatabasePrivilegesError::DatabaseValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::Database(database_name.clone()))
|
||||
err.to_error_message(DbOrUser::Database(database_name.clone()))
|
||||
}
|
||||
ModifyDatabasePrivilegesError::UserValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
ModifyDatabasePrivilegesError::DatabaseDoesNotExist => {
|
||||
format!("Database '{database_name}' does not exist.")
|
||||
format!("Database '{}' does not exist.", database_name)
|
||||
}
|
||||
ModifyDatabasePrivilegesError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
ModifyDatabasePrivilegesError::DiffDoesNotApply(diff) => {
|
||||
format!(
|
||||
@@ -88,19 +88,17 @@ impl ModifyDatabasePrivilegesError {
|
||||
)
|
||||
}
|
||||
ModifyDatabasePrivilegesError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ModifyDatabasePrivilegesError::DatabaseValidationError(err) => {
|
||||
err.error_type() + "/database"
|
||||
}
|
||||
ModifyDatabasePrivilegesError::UserValidationError(err) => err.error_type() + "/user",
|
||||
// TODO: should these be subtyped?
|
||||
ModifyDatabasePrivilegesError::DatabaseValidationError(err) => err.error_type(),
|
||||
ModifyDatabasePrivilegesError::UserValidationError(err) => err.error_type(),
|
||||
ModifyDatabasePrivilegesError::DatabaseDoesNotExist => {
|
||||
"database-does-not-exist".to_string()
|
||||
}
|
||||
@@ -114,26 +112,29 @@ impl ModifyDatabasePrivilegesError {
|
||||
}
|
||||
|
||||
impl DiffDoesNotApplyError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self) -> String {
|
||||
match self {
|
||||
DiffDoesNotApplyError::RowAlreadyExists(database_name, username) => {
|
||||
format!(
|
||||
"Privileges for user '{username}' on database '{database_name}' already exist."
|
||||
"Privileges for user '{}' on database '{}' already exist.",
|
||||
username, database_name
|
||||
)
|
||||
}
|
||||
DiffDoesNotApplyError::RowDoesNotExist(database_name, username) => {
|
||||
format!(
|
||||
"Privileges for user '{username}' on database '{database_name}' do not exist."
|
||||
"Privileges for user '{}' on database '{}' do not exist.",
|
||||
username, database_name
|
||||
)
|
||||
}
|
||||
DiffDoesNotApplyError::RowPrivilegeChangeDoesNotApply(diff, row) => {
|
||||
format!("Could not apply privilege change {diff:?} to row {row:?}")
|
||||
format!(
|
||||
"Could not apply privilege change {:?} to row {:?}",
|
||||
diff, row
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
DiffDoesNotApplyError::RowAlreadyExists(_, _) => "row-already-exists".to_string(),
|
||||
|
||||
@@ -6,12 +6,7 @@ use crate::core::{
|
||||
types::{DbOrUser, MySQLUser},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct SetUserPasswordRequest {
|
||||
pub user: MySQLUser,
|
||||
pub new_password: Option<String>,
|
||||
pub expiry: Option<chrono::NaiveDate>,
|
||||
}
|
||||
pub type SetUserPasswordRequest = (MySQLUser, String);
|
||||
|
||||
pub type SetUserPasswordResponse = Result<(), SetPasswordError>;
|
||||
|
||||
@@ -23,9 +18,6 @@ pub enum SetPasswordError {
|
||||
#[error("User does not exist")]
|
||||
UserDoesNotExist,
|
||||
|
||||
#[error("Cannot clear password with an expiry date set")]
|
||||
ClearPasswordWithExpiry,
|
||||
|
||||
#[error("MySQL error: {0}")]
|
||||
MySqlError(String),
|
||||
}
|
||||
@@ -33,41 +25,35 @@ pub enum SetPasswordError {
|
||||
pub fn print_set_password_output_status(output: &SetUserPasswordResponse, username: &MySQLUser) {
|
||||
match output {
|
||||
Ok(()) => {
|
||||
println!("Password for user '{username}' set successfully.");
|
||||
println!("Password for user '{}' set successfully.", username);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SetPasswordError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
SetPasswordError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
SetPasswordError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
}
|
||||
SetPasswordError::ClearPasswordWithExpiry => {
|
||||
format!("Cannot clear password for user '{username}' when an expiry date is set.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
SetPasswordError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
SetPasswordError::ValidationError(err) => err.error_type(),
|
||||
SetPasswordError::UserDoesNotExist => "user-does-not-exist".to_string(),
|
||||
SetPasswordError::ClearPasswordWithExpiry => "clear-password-with-expiry".to_string(),
|
||||
SetPasswordError::MySqlError(_) => "mysql-error".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,11 +32,11 @@ pub fn print_unlock_users_output_status(output: &UnlockUsersResponse) {
|
||||
for (username, result) in output {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
println!("User '{username}' unlocked successfully.");
|
||||
println!("User '{}' unlocked successfully.", username);
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("{}", err.to_error_message(username));
|
||||
eprintln!("Skipping...");
|
||||
println!("{}", err.to_error_message(username));
|
||||
println!("Skipping...");
|
||||
}
|
||||
}
|
||||
println!();
|
||||
@@ -66,25 +66,23 @@ pub fn print_unlock_users_output_status_json(output: &UnlockUsersResponse) {
|
||||
}
|
||||
|
||||
impl UnlockUserError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, username: &MySQLUser) -> String {
|
||||
match self {
|
||||
UnlockUserError::ValidationError(err) => {
|
||||
err.to_error_message(&DbOrUser::User(username.clone()))
|
||||
err.to_error_message(DbOrUser::User(username.clone()))
|
||||
}
|
||||
UnlockUserError::UserDoesNotExist => {
|
||||
format!("User '{username}' does not exist.")
|
||||
format!("User '{}' does not exist.", username)
|
||||
}
|
||||
UnlockUserError::UserIsAlreadyUnlocked => {
|
||||
format!("User '{username}' is already unlocked.")
|
||||
format!("User '{}' is already unlocked.", username)
|
||||
}
|
||||
UnlockUserError::MySqlError(err) => {
|
||||
format!("MySQL error: {err}")
|
||||
format!("MySQL error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
UnlockUserError::ValidationError(err) => err.error_type(),
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indoc::indoc;
|
||||
use nix::{libc::gid_t, unistd::Group};
|
||||
use itertools::Itertools;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -22,27 +20,29 @@ pub enum NameValidationError {
|
||||
}
|
||||
|
||||
impl NameValidationError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(self, db_or_user: &DbOrUser) -> String {
|
||||
pub fn to_error_message(self, db_or_user: DbOrUser) -> String {
|
||||
match self {
|
||||
NameValidationError::EmptyString => {
|
||||
format!("{} name can not be empty.", db_or_user.capitalized_noun())
|
||||
format!("{} name cannot be empty.", db_or_user.capitalized_noun()).to_owned()
|
||||
}
|
||||
NameValidationError::TooLong => format!(
|
||||
"{} is too long, maximum length is 64 characters.",
|
||||
"{} is too long. Maximum length is 64 characters.",
|
||||
db_or_user.capitalized_noun()
|
||||
),
|
||||
)
|
||||
.to_owned(),
|
||||
NameValidationError::InvalidCharacters => format!(
|
||||
indoc! {r"
|
||||
Invalid characters in {} name: '{}', only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted.
|
||||
"},
|
||||
indoc! {r#"
|
||||
Invalid characters in {} name: '{}'
|
||||
|
||||
Only A-Z, a-z, 0-9, _ (underscore) and - (dash) are permitted.
|
||||
"#},
|
||||
db_or_user.lowercased_noun(),
|
||||
db_or_user.name(),
|
||||
),
|
||||
)
|
||||
.to_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> &'static str {
|
||||
match self {
|
||||
NameValidationError::EmptyString => "empty-string",
|
||||
@@ -54,43 +54,64 @@ impl NameValidationError {
|
||||
|
||||
#[derive(Error, Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||
pub enum AuthorizationError {
|
||||
#[error("Illegal prefix, user is not authorized to manage this resource")]
|
||||
IllegalPrefix,
|
||||
#[error("No matching owner prefix found")]
|
||||
NoMatch,
|
||||
|
||||
// TODO: I don't think this should ever happen?
|
||||
#[error("Name cannot be empty")]
|
||||
StringEmpty,
|
||||
|
||||
#[error("Group was found in denylist")]
|
||||
DenylistError,
|
||||
}
|
||||
|
||||
impl AuthorizationError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(self, db_or_user: &DbOrUser) -> String {
|
||||
pub fn to_error_message(self, db_or_user: DbOrUser) -> String {
|
||||
let user = UnixUser::from_enviroment();
|
||||
|
||||
let UnixUser {
|
||||
username,
|
||||
mut groups,
|
||||
} = user.unwrap_or(UnixUser {
|
||||
username: "???".to_string(),
|
||||
groups: vec![],
|
||||
});
|
||||
|
||||
groups.sort();
|
||||
|
||||
match self {
|
||||
AuthorizationError::IllegalPrefix => format!(
|
||||
"Illegal {} name prefix: you are not allowed to manage databases or users prefixed with '{}'",
|
||||
AuthorizationError::NoMatch => format!(
|
||||
indoc! {r#"
|
||||
Invalid {} name prefix: '{}' does not match your username or any of your groups.
|
||||
Are you sure you are allowed to create {} names with this prefix?
|
||||
The format should be: <prefix>_<{} name>
|
||||
|
||||
Allowed prefixes:
|
||||
- {}
|
||||
{}
|
||||
"#},
|
||||
db_or_user.lowercased_noun(),
|
||||
db_or_user.prefix(),
|
||||
db_or_user.name(),
|
||||
db_or_user.lowercased_noun(),
|
||||
db_or_user.lowercased_noun(),
|
||||
username,
|
||||
groups
|
||||
.into_iter()
|
||||
.filter(|g| g != &username)
|
||||
.map(|g| format!(" - {}", g))
|
||||
.join("\n"),
|
||||
)
|
||||
.to_owned(),
|
||||
// TODO: This error message could be clearer
|
||||
AuthorizationError::StringEmpty => {
|
||||
format!("{} name can not be empty.", db_or_user.capitalized_noun())
|
||||
}
|
||||
AuthorizationError::DenylistError => {
|
||||
format!("'{}' is denied by the group denylist", db_or_user.name())
|
||||
}
|
||||
AuthorizationError::StringEmpty => format!(
|
||||
"'{}' is not a valid {} name.",
|
||||
db_or_user.name(),
|
||||
db_or_user.lowercased_noun()
|
||||
)
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> &'static str {
|
||||
match self {
|
||||
AuthorizationError::IllegalPrefix => "illegal-prefix",
|
||||
AuthorizationError::NoMatch => "no-match",
|
||||
AuthorizationError::StringEmpty => "string-empty",
|
||||
AuthorizationError::DenylistError => "denylist-error",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -106,8 +127,7 @@ pub enum ValidationError {
|
||||
}
|
||||
|
||||
impl ValidationError {
|
||||
#[must_use]
|
||||
pub fn to_error_message(&self, db_or_user: &DbOrUser) -> String {
|
||||
pub fn to_error_message(&self, db_or_user: DbOrUser) -> String {
|
||||
match self {
|
||||
ValidationError::NameValidationError(err) => err.to_error_message(db_or_user),
|
||||
ValidationError::AuthorizationError(err) => err.to_error_message(db_or_user),
|
||||
@@ -121,7 +141,6 @@ impl ValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn error_type(&self) -> String {
|
||||
match self {
|
||||
ValidationError::NameValidationError(err) => {
|
||||
@@ -136,8 +155,6 @@ impl ValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
pub type GroupDenylist = HashSet<gid_t>;
|
||||
|
||||
const MAX_NAME_LENGTH: usize = 64;
|
||||
|
||||
pub fn validate_name(name: &str) -> Result<(), NameValidationError> {
|
||||
@@ -159,7 +176,7 @@ pub fn validate_authorization_by_unix_user(
|
||||
name: &str,
|
||||
user: &UnixUser,
|
||||
) -> Result<(), AuthorizationError> {
|
||||
let prefixes = std::iter::once(user.username.clone())
|
||||
let prefixes = std::iter::once(user.username.to_owned())
|
||||
.chain(user.groups.iter().cloned())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
@@ -180,53 +197,25 @@ pub fn validate_authorization_by_prefixes(
|
||||
|
||||
if prefixes
|
||||
.iter()
|
||||
.filter(|p| name.starts_with(&((*p).clone() + "_")))
|
||||
.filter(|p| name.starts_with(&(p.to_string() + "_")))
|
||||
.collect::<Vec<_>>()
|
||||
.is_empty()
|
||||
{
|
||||
return Err(AuthorizationError::IllegalPrefix);
|
||||
}
|
||||
return Err(AuthorizationError::NoMatch);
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate_authorization_by_group_denylist(
|
||||
name: &str,
|
||||
user: &UnixUser,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> Result<(), AuthorizationError> {
|
||||
// NOTE: if the username matches, we allow it regardless of denylist
|
||||
if user.username == name {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let user_group = Group::from_name(name)
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|g| g.gid.as_raw());
|
||||
|
||||
if let Some(gid) = user_group
|
||||
&& group_denylist.contains(&gid)
|
||||
{
|
||||
Err(AuthorizationError::DenylistError)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate_db_or_user_request(
|
||||
db_or_user: &DbOrUser,
|
||||
unix_user: &UnixUser,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> Result<(), ValidationError> {
|
||||
validate_name(db_or_user.name()).map_err(ValidationError::NameValidationError)?;
|
||||
|
||||
validate_authorization_by_unix_user(db_or_user.name(), unix_user)
|
||||
.map_err(ValidationError::AuthorizationError)?;
|
||||
|
||||
validate_authorization_by_group_denylist(db_or_user.name(), unix_user, group_denylist)
|
||||
.map_err(ValidationError::AuthorizationError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -284,7 +273,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
validate_authorization_by_prefixes("nonexistent_testdb", &prefixes),
|
||||
Err(AuthorizationError::IllegalPrefix)
|
||||
Err(AuthorizationError::NoMatch)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ impl DerefMut for MySQLUser {
|
||||
|
||||
impl fmt::Display for MySQLUser {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:<width$}", self.0, width = f.width().unwrap_or(0))
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ impl DerefMut for MySQLDatabase {
|
||||
|
||||
impl fmt::Display for MySQLDatabase {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:<width$}", self.0, width = f.width().unwrap_or(0))
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,6 @@ pub enum DbOrUser {
|
||||
}
|
||||
|
||||
impl DbOrUser {
|
||||
#[must_use]
|
||||
pub fn lowercased_noun(&self) -> &'static str {
|
||||
match self {
|
||||
DbOrUser::Database(_) => "database",
|
||||
@@ -120,7 +119,6 @@ impl DbOrUser {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn capitalized_noun(&self) -> &'static str {
|
||||
match self {
|
||||
DbOrUser::Database(_) => "Database",
|
||||
@@ -128,19 +126,10 @@ impl DbOrUser {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn name(&self) -> &str {
|
||||
match self {
|
||||
DbOrUser::Database(db) => db.as_str(),
|
||||
DbOrUser::User(user) => user.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn prefix(&self) -> &str {
|
||||
match self {
|
||||
DbOrUser::Database(db) => db.split('_').next().unwrap_or("?"),
|
||||
DbOrUser::User(user) => user.split('_').next().unwrap_or("?"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,392 +0,0 @@
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{CommandFactory, Parser, Subcommand, crate_version};
|
||||
use clap_complete::CompleteEnv;
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
use tokio_stream::StreamExt;
|
||||
|
||||
use muscl_lib::{
|
||||
client::{
|
||||
commands::{
|
||||
CheckAuthArgs, CreateDbArgs, CreateUserArgs, DropDbArgs, DropUserArgs, EditPrivsArgs,
|
||||
LockUserArgs, PasswdUserArgs, ShowDbArgs, ShowPrivsArgs, ShowUserArgs, UnlockUserArgs,
|
||||
check_authorization, create_databases, create_users, drop_databases, drop_users,
|
||||
edit_database_privileges, lock_users, passwd_user, show_database_privileges,
|
||||
show_databases, show_users, unlock_users,
|
||||
},
|
||||
mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm},
|
||||
},
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
common::{ASCII_BANNER, KIND_REGARDS},
|
||||
protocol::{ClientToServerMessageStream, Response, create_client_to_server_message_stream},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
use muscl_lib::core::common::executing_in_suid_sgid_mode;
|
||||
|
||||
const fn long_version() -> &'static str {
|
||||
macro_rules! feature {
|
||||
($title:expr, $flag:expr) => {
|
||||
if cfg!(feature = $flag) {
|
||||
concat!($title, ": enabled")
|
||||
} else {
|
||||
concat!($title, ": disabled")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const_format::concatcp!(
|
||||
crate_version!(),
|
||||
"\n",
|
||||
"build profile: ",
|
||||
env!("BUILD_PROFILE"),
|
||||
"\n",
|
||||
"commit: ",
|
||||
env!("GIT_COMMIT"),
|
||||
"\n\n",
|
||||
"[features]\n",
|
||||
feature!("SUID/SGID mode", "suid-sgid-mode"),
|
||||
"\n",
|
||||
feature!(
|
||||
"mysql-admutils compatibility",
|
||||
"mysql-admutils-compatibility"
|
||||
),
|
||||
"\n",
|
||||
"\n",
|
||||
"[dependencies]\n",
|
||||
const_format::str_replace!(env!("DEPENDENCY_LIST"), ";", "\n")
|
||||
)
|
||||
}
|
||||
|
||||
const LONG_VERSION: &str = long_version();
|
||||
|
||||
const EXAMPLES: &str = const_format::concatcp!(
|
||||
color_print::cstr!("<bold><underline>Examples:</underline></bold>"),
|
||||
r#"
|
||||
# Display help information for any specific command
|
||||
muscl <command> --help
|
||||
|
||||
# Create two users 'alice_user1' and 'alice_user2'
|
||||
muscl create-user alice_user1 alice_user2
|
||||
|
||||
# Create two databases 'alice_db1' and 'alice_db2'
|
||||
muscl create-db alice_db1 alice_db2
|
||||
|
||||
# Grant Select, Update, Insert and Delete privileges on 'alice_db1' to 'alice_user1'
|
||||
muscl edit-privs alice_db1 alice_user1 +suid
|
||||
|
||||
# Show all databases
|
||||
muscl show-db
|
||||
|
||||
# Show which users have privileges on which databases
|
||||
muscl show-privs
|
||||
"#,
|
||||
);
|
||||
|
||||
const BEFORE_LONG_HELP: &str = const_format::concatcp!("\x1b[1m", ASCII_BANNER, "\x1b[0m");
|
||||
const AFTER_LONG_HELP: &str = const_format::concatcp!(EXAMPLES, "\n", KIND_REGARDS,);
|
||||
|
||||
/// Database administration tool for non-admin users to manage their own MySQL databases and users.
|
||||
///
|
||||
/// This tool allows you to manage users and databases in MySQL.
|
||||
///
|
||||
/// You are only allowed to manage databases and users that are prefixed with
|
||||
/// either your username, or a group that you are a member of.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
bin_name = "muscl",
|
||||
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||
version,
|
||||
about,
|
||||
disable_help_subcommand = true,
|
||||
propagate_version = true,
|
||||
before_long_help = BEFORE_LONG_HELP,
|
||||
after_long_help = AFTER_LONG_HELP,
|
||||
long_version = LONG_VERSION,
|
||||
// NOTE: All non-registered "subcommands" are processed before Arg::parse() is called.
|
||||
subcommand_required = true,
|
||||
)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: ClientCommand,
|
||||
|
||||
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
||||
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
||||
// specified here, but in practice clap is being difficult to work with.
|
||||
/// Path to the socket of the server.
|
||||
#[arg(
|
||||
long = "server-socket",
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
server_socket_path: Option<PathBuf>,
|
||||
|
||||
/// Config file to use for the server.
|
||||
///
|
||||
/// This is only useful when running in SUID/SGID mode.
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
#[arg(
|
||||
long = "config",
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
config_path: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
verbose: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
const EDIT_PRIVS_EXAMPLES: &str = color_print::cstr!(
|
||||
r#"
|
||||
<bold><underline>Examples:</underline></bold>
|
||||
# Open interactive editor to edit privileges
|
||||
muscl edit-privs
|
||||
|
||||
# Set privileges `SELECT`, `INSERT`, and `UPDATE` for user `my_user` on database `my_db`
|
||||
muscl edit-privs my_db my_user siu
|
||||
|
||||
# Set all privileges for user `my_other_user` on database `my_other_db`
|
||||
muscl edit-privs my_other_db my_other_user A
|
||||
|
||||
# Add the `DELETE` privilege for user `my_user` on database `my_db`
|
||||
muscl edit-privs my_db my_user +d
|
||||
|
||||
# Set miscellaneous privileges for multiple users on database `my_db`
|
||||
muscl edit-privs -p my_db:my_user:siu -p my_db:my_other_user:+ct -p my_db:yet_another_user:-d
|
||||
"#
|
||||
);
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
#[command(subcommand_required = true)]
|
||||
pub enum ClientCommand {
|
||||
/// Check whether you are authorized to manage the specified databases or users.
|
||||
CheckAuth(CheckAuthArgs),
|
||||
|
||||
/// Create one or more databases
|
||||
CreateDb(CreateDbArgs),
|
||||
|
||||
/// Delete one or more databases
|
||||
DropDb(DropDbArgs),
|
||||
|
||||
/// Print information about one or more databases
|
||||
///
|
||||
/// If no database name is provided, all databases you have access will be shown.
|
||||
ShowDb(ShowDbArgs),
|
||||
|
||||
/// Print user privileges for one or more databases
|
||||
///
|
||||
/// If no database names are provided, all databases you have access to will be shown.
|
||||
ShowPrivs(ShowPrivsArgs),
|
||||
|
||||
/// Change user privileges for one or more databases. See `edit-privs --help` for details.
|
||||
///
|
||||
/// This command has three modes of operation:
|
||||
///
|
||||
/// 1. Interactive mode:
|
||||
///
|
||||
/// If no arguments are provided, the user will be prompted to edit the privileges using a text editor.
|
||||
///
|
||||
/// You can configure your preferred text editor by setting the `VISUAL` or `EDITOR` environment variables.
|
||||
///
|
||||
/// Follow the instructions inside the editor for more information.
|
||||
///
|
||||
/// 2. Non-interactive human-friendly mode:
|
||||
///
|
||||
/// You can provide the command with three positional arguments:
|
||||
///
|
||||
/// - `<DB_NAME>`: The name of the database for which you want to edit privileges.
|
||||
/// - `<USER_NAME>`: The name of the user whose privileges you want to edit.
|
||||
/// - `<[+-]PRIVILEGES>`: A string representing the privileges to set for the user.
|
||||
///
|
||||
/// The `<[+-]PRIVILEGES>` argument is a string of characters, each representing a single privilege.
|
||||
/// The character `A` is an exception - it represents all privileges.
|
||||
/// The optional leading character can be either `+` to grant additional privileges or `-` to revoke privileges.
|
||||
/// If omitted, the privileges will be set exactly as specified, removing any privileges not listed, and adding any that are.
|
||||
///
|
||||
/// The character-to-privilege mapping is defined as follows:
|
||||
///
|
||||
/// - `s` - SELECT
|
||||
/// - `i` - INSERT
|
||||
/// - `u` - UPDATE
|
||||
/// - `d` - DELETE
|
||||
/// - `c` - CREATE
|
||||
/// - `D` - DROP
|
||||
/// - `a` - ALTER
|
||||
/// - `I` - INDEX
|
||||
/// - `t` - CREATE TEMPORARY TABLES
|
||||
/// - `l` - LOCK TABLES
|
||||
/// - `r` - REFERENCES
|
||||
/// - `A` - ALL PRIVILEGES
|
||||
///
|
||||
/// 3. Non-interactive batch mode:
|
||||
///
|
||||
/// By using the `-p` flag, you can provide multiple privilege edits in a single command.
|
||||
///
|
||||
/// The flag value should be formatted as `DB_NAME:USER_NAME:[+-]PRIVILEGES`
|
||||
/// where the privileges are a string of characters, each representing a single privilege.
|
||||
/// (See the character-to-privilege mapping above.)
|
||||
///
|
||||
#[command(
|
||||
verbatim_doc_comment,
|
||||
override_usage = "muscl edit-privs [OPTIONS] [ -p <DB_NAME:USER_NAME:[+-]PRIVILEGES>... | <DB_NAME> <USER_NAME> <[+-]PRIVILEGES> ]",
|
||||
after_long_help = EDIT_PRIVS_EXAMPLES,
|
||||
)]
|
||||
EditPrivs(EditPrivsArgs),
|
||||
|
||||
/// Create one or more users
|
||||
CreateUser(CreateUserArgs),
|
||||
|
||||
/// Delete one or more users
|
||||
DropUser(DropUserArgs),
|
||||
|
||||
/// Change the MySQL password for a user
|
||||
PasswdUser(PasswdUserArgs),
|
||||
|
||||
/// Print information about one or more users
|
||||
///
|
||||
/// If no username is provided, all users you have access will be shown.
|
||||
ShowUser(ShowUserArgs),
|
||||
|
||||
/// Lock account for one or more users
|
||||
LockUser(LockUserArgs),
|
||||
|
||||
/// Unlock account for one or more users
|
||||
UnlockUser(UnlockUserArgs),
|
||||
}
|
||||
|
||||
pub async fn handle_command(
|
||||
command: ClientCommand,
|
||||
server_connection: ClientToServerMessageStream,
|
||||
) -> anyhow::Result<()> {
|
||||
match command {
|
||||
ClientCommand::CheckAuth(args) => check_authorization(args, server_connection).await,
|
||||
ClientCommand::CreateDb(args) => create_databases(args, server_connection).await,
|
||||
ClientCommand::DropDb(args) => drop_databases(args, server_connection).await,
|
||||
ClientCommand::ShowDb(args) => show_databases(args, server_connection).await,
|
||||
ClientCommand::ShowPrivs(args) => show_database_privileges(args, server_connection).await,
|
||||
ClientCommand::EditPrivs(args) => {
|
||||
edit_database_privileges(args, None, server_connection).await
|
||||
}
|
||||
ClientCommand::CreateUser(args) => create_users(args, server_connection).await,
|
||||
ClientCommand::DropUser(args) => drop_users(args, server_connection).await,
|
||||
ClientCommand::PasswdUser(args) => passwd_user(args, server_connection).await,
|
||||
ClientCommand::ShowUser(args) => show_users(args, server_connection).await,
|
||||
ClientCommand::LockUser(args) => lock_users(args, server_connection).await,
|
||||
ClientCommand::UnlockUser(args) => unlock_users(args, server_connection).await,
|
||||
}
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn main() -> anyhow::Result<()> {
|
||||
if handle_dynamic_completion()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||
if handle_mysql_admutils_command()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let args: Args = Args::parse();
|
||||
|
||||
let connection = bootstrap_server_connection_and_drop_privileges(
|
||||
args.server_socket_path,
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
args.config_path,
|
||||
#[cfg(not(feature = "suid-sgid-mode"))]
|
||||
None,
|
||||
args.verbose,
|
||||
)?;
|
||||
|
||||
tokio_run_command(args.command, connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_dynamic_completion() -> anyhow::Result<Option<()>> {
|
||||
if std::env::var_os("COMPLETE").is_some() {
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
if executing_in_suid_sgid_mode()? {
|
||||
use muscl_lib::core::bootstrap::drop_privs;
|
||||
drop_privs()?
|
||||
}
|
||||
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.and_then(|s| {
|
||||
PathBuf::from(s)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
})
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"Could not determine executable name for completion"
|
||||
))?;
|
||||
|
||||
let command = match argv0.as_str() {
|
||||
"muscl" => Args::command(),
|
||||
"mysql-dbadm" => mysql_dbadm::Args::command(),
|
||||
"mysql-useradm" => mysql_useradm::Args::command(),
|
||||
command => anyhow::bail!("Unknown executable name: `{}`", command),
|
||||
};
|
||||
|
||||
CompleteEnv::with_factory(move || command.clone()).complete();
|
||||
|
||||
Ok(Some(()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_mysql_admutils_command() -> anyhow::Result<Option<()>> {
|
||||
let argv0 = std::env::args().next().and_then(|s| {
|
||||
PathBuf::from(s)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
match argv0.as_deref() {
|
||||
Some("mysql-dbadm") => mysql_dbadm::main().map(Some),
|
||||
Some("mysql-useradm") => mysql_useradm::main().map(Some),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Run the given commmand (from the client side) using Tokio.
|
||||
fn tokio_run_command(
|
||||
command: ClientCommand,
|
||||
server_connection: StdUnixStream,
|
||||
) -> anyhow::Result<()> {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to start Tokio runtime")?
|
||||
.block_on(async {
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let mut message_stream = create_client_to_server_message_stream(tokio_socket);
|
||||
|
||||
while let Some(Ok(message)) = message_stream.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handle_command(command, message_stream).await
|
||||
})
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
||||
pub mod client;
|
||||
pub mod core;
|
||||
pub mod server;
|
||||
281
src/main.rs
Normal file
281
src/main.rs
Normal file
@@ -0,0 +1,281 @@
|
||||
#[macro_use]
|
||||
extern crate prettytable;
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{CommandFactory, Parser, crate_version};
|
||||
use clap_complete::CompleteEnv;
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use std::os::unix::net::UnixStream as StdUnixStream;
|
||||
use tokio::net::UnixStream as TokioUnixStream;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
use crate::{
|
||||
core::{
|
||||
bootstrap::bootstrap_server_connection_and_drop_privileges,
|
||||
common::{ASCII_BANNER, KIND_REGARDS, executing_in_suid_sgid_mode},
|
||||
protocol::{Response, create_client_to_server_message_stream},
|
||||
},
|
||||
server::{command::ServerArgs, landlock::landlock_restrict_server},
|
||||
};
|
||||
|
||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||
use crate::client::mysql_admutils_compatibility::{mysql_dbadm, mysql_useradm};
|
||||
|
||||
mod server;
|
||||
|
||||
mod client;
|
||||
mod core;
|
||||
|
||||
const fn long_version() -> &'static str {
|
||||
macro_rules! feature {
|
||||
($title:expr, $flag:expr) => {
|
||||
if cfg!(feature = $flag) {
|
||||
concat!($title, ": enabled")
|
||||
} else {
|
||||
concat!($title, ": disabled")
|
||||
}
|
||||
};
|
||||
}
|
||||
const_format::concatcp!(
|
||||
crate_version!(),
|
||||
"\n",
|
||||
"build profile: ",
|
||||
env!("BUILD_PROFILE"),
|
||||
"\n",
|
||||
"commit: ",
|
||||
env!("GIT_COMMIT"),
|
||||
"\n\n",
|
||||
"[features]\n",
|
||||
feature!("SUID/SGID mode", "suid-sgid-mode"),
|
||||
"\n",
|
||||
feature!(
|
||||
"mysql-admutils compatibility",
|
||||
"mysql-admutils-compatibility"
|
||||
),
|
||||
"\n",
|
||||
)
|
||||
}
|
||||
|
||||
const LONG_VERSION: &str = long_version();
|
||||
|
||||
/// Database administration tool for non-admin users to manage their own MySQL databases and users.
|
||||
///
|
||||
/// This tool allows you to manage users and databases in MySQL.
|
||||
///
|
||||
/// You are only allowed to manage databases and users that are prefixed with
|
||||
/// either your username, or a group that you are a member of.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
bin_name = "muscl",
|
||||
author = "Programvareverkstedet <projects@pvv.ntnu.no>",
|
||||
version,
|
||||
about,
|
||||
disable_help_subcommand = true,
|
||||
propagate_version = true,
|
||||
before_long_help = ASCII_BANNER,
|
||||
after_long_help = KIND_REGARDS,
|
||||
long_version = LONG_VERSION,
|
||||
// NOTE: All non-registered "subcommands" are processed before Arg::parse() is called.
|
||||
subcommand_required = true,
|
||||
)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
|
||||
/// Path to the socket of the server, if it already exists.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
server_socket_path: Option<PathBuf>,
|
||||
|
||||
/// Config file to use for the server.
|
||||
#[arg(
|
||||
short,
|
||||
long,
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
global = true,
|
||||
hide_short_help = true
|
||||
)]
|
||||
config: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
verbose: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
enum Command {
|
||||
#[command(flatten)]
|
||||
Client(client::commands::ClientCommand),
|
||||
|
||||
/// Run the server
|
||||
#[command(hide = true)]
|
||||
Server(server::command::ServerArgs),
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn main() -> anyhow::Result<()> {
|
||||
if handle_dynamic_completion()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(feature = "mysql-admutils-compatibility")]
|
||||
if handle_mysql_admutils_command()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let args: Args = Args::parse();
|
||||
|
||||
if handle_server_command(&args)?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let connection = bootstrap_server_connection_and_drop_privileges(
|
||||
args.server_socket_path,
|
||||
args.config,
|
||||
args.verbose,
|
||||
)?;
|
||||
|
||||
tokio_run_command(args.command, connection)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_dynamic_completion() -> anyhow::Result<Option<()>> {
|
||||
if std::env::var_os("COMPLETE").is_some() {
|
||||
#[cfg(feature = "suid-sgid-mode")]
|
||||
if executing_in_suid_sgid_mode()? {
|
||||
use crate::core::bootstrap::drop_privs;
|
||||
drop_privs()?
|
||||
}
|
||||
|
||||
let argv0 = std::env::args()
|
||||
.next()
|
||||
.and_then(|s| {
|
||||
PathBuf::from(s)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
})
|
||||
.ok_or(anyhow::anyhow!(
|
||||
"Could not determine executable name for completion"
|
||||
))?;
|
||||
|
||||
let command = match argv0.as_str() {
|
||||
"muscl" => Args::command(),
|
||||
"mysql-dbadm" => mysql_dbadm::Args::command(),
|
||||
"mysql-useradm" => mysql_useradm::Args::command(),
|
||||
command => anyhow::bail!("Unknown executable name: `{}`", command),
|
||||
};
|
||||
|
||||
CompleteEnv::with_factory(move || command.clone()).complete();
|
||||
|
||||
Ok(Some(()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_mysql_admutils_command() -> anyhow::Result<Option<()>> {
|
||||
let argv0 = std::env::args().next().and_then(|s| {
|
||||
PathBuf::from(s)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
});
|
||||
|
||||
match argv0.as_deref() {
|
||||
Some("mysql-dbadm") => mysql_dbadm::main().map(Some),
|
||||
Some("mysql-useradm") => mysql_useradm::main().map(Some),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn handle_server_command(args: &Args) -> anyhow::Result<Option<()>> {
|
||||
match args.command {
|
||||
Command::Server(ref command) => {
|
||||
assert!(
|
||||
!executing_in_suid_sgid_mode()?,
|
||||
"The executable should not be SUID or SGID when running the server manually"
|
||||
);
|
||||
|
||||
if !command.disable_landlock {
|
||||
landlock_restrict_server(args.config.as_deref())
|
||||
.context("Failed to apply Landlock restrictions to the server process")?;
|
||||
}
|
||||
|
||||
tokio_start_server(
|
||||
args.config.to_owned(),
|
||||
args.verbose.to_owned(),
|
||||
command.to_owned(),
|
||||
)?;
|
||||
Ok(Some(()))
|
||||
}
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
const MIN_TOKIO_WORKER_THREADS: usize = 4;
|
||||
|
||||
/// Start a long-lived server using Tokio.
|
||||
fn tokio_start_server(
|
||||
config_path: Option<PathBuf>,
|
||||
verbosity: Verbosity<InfoLevel>,
|
||||
args: ServerArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
let worker_thread_count = std::cmp::max(num_cpus::get(), MIN_TOKIO_WORKER_THREADS);
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(worker_thread_count)
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to start Tokio runtime")?
|
||||
.block_on(server::command::handle_command(
|
||||
config_path,
|
||||
verbosity,
|
||||
args,
|
||||
))
|
||||
}
|
||||
|
||||
/// Run the given commmand (from the client side) using Tokio.
|
||||
///
|
||||
/// **WARNING:** This function may be run with elevated privileges.
|
||||
fn tokio_run_command(command: Command, server_connection: StdUnixStream) -> anyhow::Result<()> {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to start Tokio runtime")?
|
||||
.block_on(async {
|
||||
let tokio_socket = TokioUnixStream::from_std(server_connection)?;
|
||||
let mut message_stream = create_client_to_server_message_stream(tokio_socket);
|
||||
|
||||
while let Some(Ok(message)) = message_stream.next().await {
|
||||
match message {
|
||||
Response::Error(err) => {
|
||||
anyhow::bail!("{}", err);
|
||||
}
|
||||
Response::Ready => break,
|
||||
message => {
|
||||
eprintln!("Unexpected message from server: {:?}", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match command {
|
||||
Command::Client(client_args) => {
|
||||
client::commands::handle_command(client_args, message_stream).await
|
||||
}
|
||||
Command::Server(_) => unreachable!(),
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod authorization;
|
||||
mod authorization;
|
||||
pub mod command;
|
||||
mod common;
|
||||
pub mod config;
|
||||
pub mod landlock;
|
||||
|
||||
@@ -1,145 +1,25 @@
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
use anyhow::Context;
|
||||
use nix::unistd::Group;
|
||||
|
||||
use crate::core::{
|
||||
common::UnixUser,
|
||||
protocol::{
|
||||
CheckAuthorizationError,
|
||||
request_validation::{GroupDenylist, validate_db_or_user_request},
|
||||
},
|
||||
protocol::{CheckAuthorizationError, request_validation::validate_db_or_user_request},
|
||||
types::DbOrUser,
|
||||
};
|
||||
|
||||
pub async fn check_authorization(
|
||||
dbs_or_users: Vec<DbOrUser>,
|
||||
unix_user: &UnixUser,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> std::collections::BTreeMap<DbOrUser, Result<(), CheckAuthorizationError>> {
|
||||
let mut results = std::collections::BTreeMap::new();
|
||||
|
||||
for db_or_user in dbs_or_users {
|
||||
if let Err(err) = validate_db_or_user_request(&db_or_user, unix_user, group_denylist)
|
||||
.map_err(CheckAuthorizationError)
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&db_or_user, unix_user).map_err(CheckAuthorizationError)
|
||||
{
|
||||
results.insert(db_or_user.clone(), Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
results.insert(db_or_user.clone(), Ok(()));
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Reads and parses a group denylist file, returning a set of GUIDs
|
||||
///
|
||||
/// The format of the denylist file is expected to be one group name or GID per line.
|
||||
/// Lines starting with '#' are treated as comments and ignored.
|
||||
/// Empty lines are also ignored.
|
||||
///
|
||||
/// Each line looks like one of the following:
|
||||
/// - `gid:1001`
|
||||
/// - `group:admins`
|
||||
pub fn read_and_parse_group_denylist(denylist_path: &Path) -> anyhow::Result<GroupDenylist> {
|
||||
let content = std::fs::read_to_string(denylist_path)
|
||||
.context(format!("Failed to read denylist file at {denylist_path:?}"))?;
|
||||
|
||||
let mut groups = HashSet::with_capacity(content.lines().count());
|
||||
|
||||
for (line_number, line) in content.lines().enumerate() {
|
||||
let trimmed_line = line.trim();
|
||||
|
||||
if trimmed_line.is_empty() || trimmed_line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = trimmed_line.splitn(2, ':').collect();
|
||||
if parts.len() != 2 {
|
||||
tracing::warn!(
|
||||
"Invalid format in denylist file at {:?} on line {}: {}",
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
line
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"gid" => {
|
||||
let gid: u32 = match parts[1].parse() {
|
||||
Ok(gid) => gid,
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Invalid GID '{}' in denylist file at {:?} on line {}: {}",
|
||||
parts[1],
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let group = match Group::from_gid(nix::unistd::Gid::from_raw(gid)) {
|
||||
Ok(Some(g)) => g,
|
||||
Ok(None) => {
|
||||
tracing::warn!(
|
||||
"No group found for GID {} in denylist file at {:?} on line {}",
|
||||
gid,
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to get group for GID {} in denylist file at {:?} on line {}: {}",
|
||||
gid,
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
err
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
groups.insert(group.gid.as_raw());
|
||||
}
|
||||
"group" => match Group::from_name(parts[1]) {
|
||||
Ok(Some(group)) => {
|
||||
groups.insert(group.gid.as_raw());
|
||||
}
|
||||
Ok(None) => {
|
||||
tracing::warn!(
|
||||
"No group found for name '{}' in denylist file at {:?} on line {}",
|
||||
parts[1],
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!(
|
||||
"Failed to get group for name '{}' in denylist file at {:?} on line {}: {}",
|
||||
parts[1],
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
err
|
||||
);
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
tracing::warn!(
|
||||
"Invalid prefix '{}' in denylist file at {:?} on line {}: {}",
|
||||
parts[0],
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
line
|
||||
);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ use std::path::PathBuf;
|
||||
use anyhow::Context;
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap_verbosity_flag::{InfoLevel, Verbosity};
|
||||
use tracing_subscriber::layer::SubscriberExt;
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
use muscl_lib::{
|
||||
use crate::{
|
||||
core::common::{ASCII_BANNER, DEFAULT_CONFIG_PATH, KIND_REGARDS},
|
||||
server::{landlock::landlock_restrict_server, supervisor::Supervisor},
|
||||
server::supervisor::Supervisor,
|
||||
};
|
||||
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
@@ -16,7 +16,6 @@ pub struct ServerArgs {
|
||||
pub subcmd: ServerCommand,
|
||||
|
||||
/// Enable systemd mode
|
||||
#[cfg(target_os = "linux")]
|
||||
#[arg(long)]
|
||||
pub systemd: bool,
|
||||
|
||||
@@ -25,29 +24,6 @@ pub struct ServerArgs {
|
||||
/// This is useful if you are planning to reload the server's configuration.
|
||||
#[arg(long)]
|
||||
pub disable_landlock: bool,
|
||||
|
||||
// NOTE: be careful not to add short options that collide with the `edit-privs` privilege
|
||||
// characters. It should in theory be possible for `edit-privs` to ignore any options
|
||||
// specified here, but in practice clap is being difficult to work with.
|
||||
/// Path to where the server's unix socket should be created. This is only relevant when
|
||||
/// not using systemd socket activation.
|
||||
#[arg(
|
||||
long = "socket",
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
)]
|
||||
socket_path: Option<PathBuf>,
|
||||
|
||||
/// Config file to use for the server.
|
||||
#[arg(
|
||||
long = "config",
|
||||
value_name = "PATH",
|
||||
value_hint = clap::ValueHint::FilePath,
|
||||
)]
|
||||
config_path: Option<PathBuf>,
|
||||
|
||||
#[command(flatten)]
|
||||
verbosity: Verbosity<InfoLevel>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
@@ -71,37 +47,17 @@ const LOG_LEVEL_WARNING: &str = r#"
|
||||
===================================================
|
||||
"#;
|
||||
|
||||
const MIN_TOKIO_WORKER_THREADS: usize = 4;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let args = ServerArgs::parse();
|
||||
|
||||
if !args.disable_landlock {
|
||||
landlock_restrict_server(args.config_path.as_deref())
|
||||
.context("Failed to apply Landlock restrictions to the server process")?;
|
||||
}
|
||||
|
||||
let worker_thread_count = std::cmp::max(num_cpus::get(), MIN_TOKIO_WORKER_THREADS);
|
||||
|
||||
tokio::runtime::Builder::new_multi_thread()
|
||||
.worker_threads(worker_thread_count)
|
||||
.enable_all()
|
||||
.build()
|
||||
.context("Failed to start Tokio runtime")?
|
||||
.block_on(handle_command(args))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn trace_server_prelude() {
|
||||
pub fn trace_server_prelude() {
|
||||
let message = [ASCII_BANNER, "", KIND_REGARDS, ""].join("\n");
|
||||
tracing::info!(message);
|
||||
}
|
||||
|
||||
async fn handle_command(args: ServerArgs) -> anyhow::Result<()> {
|
||||
pub async fn handle_command(
|
||||
config_path: Option<PathBuf>,
|
||||
verbosity: Verbosity<InfoLevel>,
|
||||
args: ServerArgs,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut auto_detected_systemd_mode = false;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let systemd_mode = args.systemd || {
|
||||
if let Ok(true) = sd_notify::booted() {
|
||||
auto_detected_systemd_mode = true;
|
||||
@@ -111,34 +67,28 @@ async fn handle_command(args: ServerArgs) -> anyhow::Result<()> {
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let systemd_mode = false;
|
||||
|
||||
if systemd_mode {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let subscriber = tracing_subscriber::Registry::default()
|
||||
.with(args.verbosity.tracing_level_filter())
|
||||
.with(tracing_journald::layer()?);
|
||||
let subscriber = tracing_subscriber::Registry::default()
|
||||
.with(verbosity.tracing_level_filter())
|
||||
.with(tracing_journald::layer()?);
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("Failed to set global default tracing subscriber")?;
|
||||
tracing::subscriber::set_global_default(subscriber)
|
||||
.context("Failed to set global default tracing subscriber")?;
|
||||
|
||||
trace_server_prelude();
|
||||
trace_server_prelude();
|
||||
|
||||
if args.verbosity.tracing_level_filter() >= tracing::Level::TRACE {
|
||||
tracing::warn!("{}", LOG_LEVEL_WARNING.trim());
|
||||
}
|
||||
if verbosity.tracing_level_filter() >= tracing::Level::TRACE {
|
||||
tracing::warn!("{}", LOG_LEVEL_WARNING.trim());
|
||||
}
|
||||
|
||||
if auto_detected_systemd_mode {
|
||||
tracing::debug!("Running in systemd mode, auto-detected");
|
||||
} else {
|
||||
tracing::debug!("Running in systemd mode");
|
||||
}
|
||||
if auto_detected_systemd_mode {
|
||||
tracing::debug!("Running in systemd mode, auto-detected");
|
||||
} else {
|
||||
tracing::debug!("Running in systemd mode");
|
||||
}
|
||||
} else {
|
||||
let subscriber = tracing_subscriber::Registry::default()
|
||||
.with(args.verbosity.tracing_level_filter())
|
||||
.with(verbosity.tracing_level_filter())
|
||||
.with(
|
||||
tracing_subscriber::fmt::layer()
|
||||
.with_line_number(cfg!(debug_assertions))
|
||||
@@ -155,9 +105,7 @@ async fn handle_command(args: ServerArgs) -> anyhow::Result<()> {
|
||||
tracing::debug!("Running in standalone mode");
|
||||
}
|
||||
|
||||
let config_path = args
|
||||
.config_path
|
||||
.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
|
||||
let config_path = config_path.unwrap_or_else(|| PathBuf::from(DEFAULT_CONFIG_PATH));
|
||||
|
||||
match args.subcmd {
|
||||
ServerCommand::Listen => {
|
||||
@@ -1,43 +1,19 @@
|
||||
use crate::core::{common::UnixUser, protocol::request_validation::GroupDenylist};
|
||||
use nix::unistd::Group;
|
||||
use crate::core::common::UnixUser;
|
||||
use sqlx::prelude::*;
|
||||
|
||||
/// This function retrieves the groups of a user, filtering out any groups
|
||||
/// that are present in the provided denylist.
|
||||
pub fn get_user_filtered_groups(user: &UnixUser, group_denylist: &GroupDenylist) -> Vec<String> {
|
||||
user.groups
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|group_name| {
|
||||
match Group::from_name(&group_name) {
|
||||
Ok(Some(group)) => {
|
||||
if group_denylist.contains(&group.gid.as_raw()) {
|
||||
None
|
||||
} else {
|
||||
Some(group.name)
|
||||
}
|
||||
}
|
||||
// NOTE: allow non-existing groups to pass through the filter
|
||||
_ => Some(group_name),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// This function creates a regex that matches items (users, databases)
|
||||
/// that belong to the user or any of the user's groups.
|
||||
pub fn create_user_group_matching_regex(user: &UnixUser, group_denylist: &GroupDenylist) -> String {
|
||||
let filtered_groups = get_user_filtered_groups(user, group_denylist);
|
||||
if filtered_groups.is_empty() {
|
||||
pub fn create_user_group_matching_regex(user: &UnixUser) -> String {
|
||||
if user.groups.is_empty() {
|
||||
format!("{}_.+", user.username)
|
||||
} else {
|
||||
format!("({}|{})_.+", user.username, filtered_groups.join("|"))
|
||||
format!("({}|{})_.+", user.username, user.groups.join("|"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Some mysql versions with some collations mark some columns as binary fields,
|
||||
/// which in the current version of sqlx is not parsable as string.
|
||||
/// See: <https://github.com/launchbadge/sqlx/issues/3387>
|
||||
/// See: https://github.com/launchbadge/sqlx/issues/3387
|
||||
#[inline]
|
||||
pub fn try_get_with_binary_fallback(
|
||||
row: &sqlx::mysql::MySqlRow,
|
||||
@@ -61,8 +37,7 @@ mod tests {
|
||||
groups: vec!["group1".to_owned(), "group2".to_owned()],
|
||||
};
|
||||
|
||||
let regex = create_user_group_matching_regex(&user, &GroupDenylist::new());
|
||||
println!("Generated regex: {}", regex);
|
||||
let regex = create_user_group_matching_regex(&user);
|
||||
let re = Regex::new(®ex).unwrap();
|
||||
|
||||
assert!(re.is_match("user_something"));
|
||||
|
||||
@@ -44,7 +44,7 @@ impl MysqlConfig {
|
||||
if let Some(password_file) = &self.password_file {
|
||||
let password = fs::read_to_string(password_file)
|
||||
.with_context(|| {
|
||||
format!("Failed to read MySQL password file at {password_file:?}")
|
||||
format!("Failed to read MySQL password file at {:?}", password_file)
|
||||
})?
|
||||
.trim()
|
||||
.to_owned();
|
||||
@@ -78,15 +78,9 @@ impl MysqlConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct AuthorizationConfig {
|
||||
pub group_denylist_file: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub struct ServerConfig {
|
||||
pub socket_path: Option<PathBuf>,
|
||||
pub authorization: AuthorizationConfig,
|
||||
pub mysql: MysqlConfig,
|
||||
}
|
||||
|
||||
@@ -96,8 +90,8 @@ impl ServerConfig {
|
||||
tracing::debug!("Reading config file at {:?}", config_path);
|
||||
|
||||
fs::read_to_string(config_path)
|
||||
.context(format!("Failed to read config file at {config_path:?}"))
|
||||
.context(format!("Failed to read config file at {:?}", config_path))
|
||||
.and_then(|c| toml::from_str(&c).context("Failed to parse config file"))
|
||||
.context(format!("Failed to parse config file at {config_path:?}"))
|
||||
.context(format!("Failed to parse config file at {:?}", config_path))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,11 @@ use crate::{
|
||||
common::UnixUser,
|
||||
protocol::{
|
||||
Request, Response, ServerToClientMessageStream, SetPasswordError,
|
||||
SetUserPasswordRequest, create_server_to_client_message_stream,
|
||||
request_validation::GroupDenylist,
|
||||
create_server_to_client_message_stream,
|
||||
},
|
||||
},
|
||||
server::{
|
||||
authorization::check_authorization,
|
||||
common::get_user_filtered_groups,
|
||||
sql::{
|
||||
database_operations::{
|
||||
complete_database_name, create_databases, drop_databases,
|
||||
@@ -41,7 +39,6 @@ pub async fn session_handler(
|
||||
socket: UnixStream,
|
||||
db_pool: Arc<RwLock<MySqlPool>>,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> anyhow::Result<()> {
|
||||
let uid = match socket.peer_cred() {
|
||||
Ok(cred) => cred.uid(),
|
||||
@@ -79,7 +76,7 @@ pub async fn session_handler(
|
||||
))
|
||||
.await
|
||||
.ok();
|
||||
anyhow::bail!("Failed to get username from uid: {e}");
|
||||
anyhow::bail!("Failed to get username from uid: {}", e);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -88,14 +85,8 @@ pub async fn session_handler(
|
||||
(async move {
|
||||
tracing::info!("Accepted connection from user: {}", unix_user);
|
||||
|
||||
let result = session_handler_with_unix_user(
|
||||
socket,
|
||||
&unix_user,
|
||||
db_pool,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
session_handler_with_unix_user(socket, &unix_user, db_pool, db_is_mariadb).await;
|
||||
|
||||
tracing::info!(
|
||||
"Finished handling requests for connection from user: {}",
|
||||
@@ -113,7 +104,6 @@ pub async fn session_handler_with_unix_user(
|
||||
unix_user: &UnixUser,
|
||||
db_pool: Arc<RwLock<MySqlPool>>,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut message_stream = create_server_to_client_message_stream(socket);
|
||||
|
||||
@@ -141,7 +131,6 @@ pub async fn session_handler_with_unix_user(
|
||||
unix_user,
|
||||
&mut db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -158,7 +147,6 @@ async fn session_handler_with_db_connection(
|
||||
unix_user: &UnixUser,
|
||||
db_connection: &mut MySqlConnection,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> anyhow::Result<()> {
|
||||
stream.send(Response::Ready).await?;
|
||||
loop {
|
||||
@@ -176,235 +164,156 @@ async fn session_handler_with_db_connection(
|
||||
|
||||
// TODO: don't clone the request
|
||||
let request_to_display = match &request {
|
||||
Request::PasswdUser(SetUserPasswordRequest {
|
||||
user,
|
||||
new_password,
|
||||
expiry,
|
||||
}) => Request::PasswdUser(SetUserPasswordRequest {
|
||||
user: user.clone(),
|
||||
new_password: new_password.as_ref().map(|_| "<REDACTED>".to_string()),
|
||||
expiry: *expiry,
|
||||
}),
|
||||
Request::PasswdUser((db_user, _)) => {
|
||||
Request::PasswdUser((db_user.to_owned(), "<REDACTED>".to_string()))
|
||||
}
|
||||
request => request.to_owned(),
|
||||
};
|
||||
|
||||
if request_to_display == Request::Exit {
|
||||
tracing::debug!("Received request: {:#?}", request_to_display);
|
||||
} else {
|
||||
if request_to_display != Request::Exit {
|
||||
tracing::info!("Received request: {:#?}", request_to_display);
|
||||
} else {
|
||||
tracing::debug!("Received request: {:#?}", request_to_display);
|
||||
}
|
||||
|
||||
let response = match request {
|
||||
Request::CheckAuthorization(dbs_or_users) => {
|
||||
let result = check_authorization(dbs_or_users, unix_user, group_denylist).await;
|
||||
let result = check_authorization(dbs_or_users, unix_user).await;
|
||||
Response::CheckAuthorization(result)
|
||||
}
|
||||
Request::ListValidNamePrefixes => {
|
||||
let mut result = Vec::with_capacity(unix_user.groups.len() + 1);
|
||||
result.push(unix_user.username.clone());
|
||||
|
||||
for group in get_user_filtered_groups(unix_user, group_denylist) {
|
||||
result.push(group.clone());
|
||||
}
|
||||
|
||||
Response::ListValidNamePrefixes(result)
|
||||
}
|
||||
Request::CompleteDatabaseName(partial_database_name) => {
|
||||
// TODO: more correct validation here
|
||||
if partial_database_name
|
||||
if !partial_database_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
Response::CompleteDatabaseName(vec![])
|
||||
} else {
|
||||
let result = complete_database_name(
|
||||
partial_database_name,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::CompleteDatabaseName(result)
|
||||
} else {
|
||||
Response::CompleteDatabaseName(vec![])
|
||||
}
|
||||
}
|
||||
Request::CompleteUserName(partial_user_name) => {
|
||||
// TODO: more correct validation here
|
||||
if partial_user_name
|
||||
if !partial_user_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
|
||||
{
|
||||
Response::CompleteUserName(vec![])
|
||||
} else {
|
||||
let result = complete_user_name(
|
||||
partial_user_name,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::CompleteUserName(result)
|
||||
} else {
|
||||
Response::CompleteUserName(vec![])
|
||||
}
|
||||
}
|
||||
Request::CreateDatabases(databases_names) => {
|
||||
let result = create_databases(
|
||||
databases_names,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
create_databases(databases_names, unix_user, db_connection, db_is_mariadb)
|
||||
.await;
|
||||
Response::CreateDatabases(result)
|
||||
}
|
||||
Request::DropDatabases(databases_names) => {
|
||||
let result = drop_databases(
|
||||
databases_names,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
drop_databases(databases_names, unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::DropDatabases(result)
|
||||
}
|
||||
Request::ListDatabases(database_names) => {
|
||||
if let Some(database_names) = database_names {
|
||||
let result = list_databases(
|
||||
database_names,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Request::ListDatabases(database_names) => match database_names {
|
||||
Some(database_names) => {
|
||||
let result =
|
||||
list_databases(database_names, unix_user, db_connection, db_is_mariadb)
|
||||
.await;
|
||||
Response::ListDatabases(result)
|
||||
} else {
|
||||
let result = list_all_databases_for_user(
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
let result =
|
||||
list_all_databases_for_user(unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::ListAllDatabases(result)
|
||||
}
|
||||
}
|
||||
Request::ListPrivileges(database_names) => {
|
||||
if let Some(database_names) = database_names {
|
||||
},
|
||||
Request::ListPrivileges(database_names) => match database_names {
|
||||
Some(database_names) => {
|
||||
let privilege_data = get_databases_privilege_data(
|
||||
database_names,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::ListPrivileges(privilege_data)
|
||||
} else {
|
||||
let privilege_data = get_all_database_privileges(
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
let privilege_data =
|
||||
get_all_database_privileges(unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::ListAllPrivileges(privilege_data)
|
||||
}
|
||||
}
|
||||
},
|
||||
Request::ModifyPrivileges(database_privilege_diffs) => {
|
||||
let result = apply_privilege_diffs(
|
||||
BTreeSet::from_iter(database_privilege_diffs),
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::ModifyPrivileges(result)
|
||||
}
|
||||
Request::CreateUsers(db_users) => {
|
||||
let result = create_database_users(
|
||||
db_users,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
create_database_users(db_users, unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::CreateUsers(result)
|
||||
}
|
||||
Request::DropUsers(db_users) => {
|
||||
let result = drop_database_users(
|
||||
db_users,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
drop_database_users(db_users, unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::DropUsers(result)
|
||||
}
|
||||
Request::PasswdUser(SetUserPasswordRequest {
|
||||
user,
|
||||
new_password,
|
||||
expiry,
|
||||
}) => {
|
||||
Request::PasswdUser((db_user, password)) => {
|
||||
let result = set_password_for_database_user(
|
||||
&user,
|
||||
new_password.as_deref(),
|
||||
expiry,
|
||||
&db_user,
|
||||
&password,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::SetUserPassword(result)
|
||||
}
|
||||
Request::ListUsers(db_users) => {
|
||||
if let Some(db_users) = db_users {
|
||||
let result = list_database_users(
|
||||
db_users,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Request::ListUsers(db_users) => match db_users {
|
||||
Some(db_users) => {
|
||||
let result =
|
||||
list_database_users(db_users, unix_user, db_connection, db_is_mariadb)
|
||||
.await;
|
||||
Response::ListUsers(result)
|
||||
} else {
|
||||
}
|
||||
None => {
|
||||
let result = list_all_database_users_for_unix_user(
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
Response::ListAllUsers(result)
|
||||
}
|
||||
}
|
||||
},
|
||||
Request::LockUsers(db_users) => {
|
||||
let result = lock_database_users(
|
||||
db_users,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
lock_database_users(db_users, unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::LockUsers(result)
|
||||
}
|
||||
Request::UnlockUsers(db_users) => {
|
||||
let result = unlock_database_users(
|
||||
db_users,
|
||||
unix_user,
|
||||
db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
let result =
|
||||
unlock_database_users(db_users, unix_user, db_connection, db_is_mariadb).await;
|
||||
Response::UnlockUsers(result)
|
||||
}
|
||||
Request::Exit => {
|
||||
@@ -412,13 +321,14 @@ async fn session_handler_with_db_connection(
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: don't clone the response
|
||||
let response_to_display = match &response {
|
||||
Response::SetUserPassword(Err(SetPasswordError::MySqlError(_))) => {
|
||||
&Response::SetUserPassword(Err(SetPasswordError::MySqlError(
|
||||
Response::SetUserPassword(Err(SetPasswordError::MySqlError(
|
||||
"<REDACTED>".to_string(),
|
||||
)))
|
||||
}
|
||||
response => response,
|
||||
response => response.to_owned(),
|
||||
};
|
||||
tracing::debug!("Response: {:#?}", response_to_display);
|
||||
|
||||
|
||||
@@ -3,13 +3,11 @@ pub mod database_privilege_operations;
|
||||
pub mod user_operations;
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn quote_literal(s: &str) -> String {
|
||||
format!("'{}'", s.replace('\'', r"\'"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn quote_identifier(s: &str) -> String {
|
||||
format!("`{}`", s.replace('`', r"\`"))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ use sqlx::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::protocol::CompleteDatabaseNameResponse;
|
||||
use crate::core::protocol::request_validation::GroupDenylist;
|
||||
use crate::core::protocol::request_validation::validate_db_or_user_request;
|
||||
use crate::core::types::DbOrUser;
|
||||
use crate::core::types::MySQLDatabase;
|
||||
@@ -50,19 +49,18 @@ pub async fn complete_database_name(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> CompleteDatabaseNameResponse {
|
||||
let result = sqlx::query(
|
||||
r"
|
||||
r#"
|
||||
SELECT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database`
|
||||
FROM `information_schema`.`SCHEMATA`
|
||||
WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
||||
AND `SCHEMA_NAME` REGEXP ?
|
||||
AND `SCHEMA_NAME` LIKE ?
|
||||
",
|
||||
"#,
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
||||
.bind(format!("{database_prefix}%"))
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.bind(format!("{}%", database_prefix))
|
||||
.fetch_all(connection)
|
||||
.await;
|
||||
|
||||
@@ -91,33 +89,29 @@ pub async fn create_databases(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> CreateDatabasesResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for database_name in database_names {
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::Database(database_name.clone()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(CreateDatabaseError::ValidationError)
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::Database(database_name.clone()), unix_user)
|
||||
.map_err(CreateDatabaseError::ValidationError)
|
||||
{
|
||||
results.insert(database_name.clone(), Err(err));
|
||||
results.insert(database_name.to_owned(), Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_database_exists(&database_name, &mut *connection).await {
|
||||
Ok(true) => {
|
||||
results.insert(
|
||||
database_name.clone(),
|
||||
database_name.to_owned(),
|
||||
Err(CreateDatabaseError::DatabaseAlreadyExists),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(
|
||||
database_name.clone(),
|
||||
database_name.to_owned(),
|
||||
Err(CreateDatabaseError::MySqlError(err.to_string())),
|
||||
);
|
||||
continue;
|
||||
@@ -147,33 +141,29 @@ pub async fn drop_databases(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> DropDatabasesResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for database_name in database_names {
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::Database(database_name.clone()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(DropDatabaseError::ValidationError)
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::Database(database_name.clone()), unix_user)
|
||||
.map_err(DropDatabaseError::ValidationError)
|
||||
{
|
||||
results.insert(database_name.clone(), Err(err));
|
||||
results.insert(database_name.to_owned(), Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_database_exists(&database_name, &mut *connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(
|
||||
database_name.clone(),
|
||||
database_name.to_owned(),
|
||||
Err(DropDatabaseError::DatabaseDoesNotExist),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
results.insert(
|
||||
database_name.clone(),
|
||||
database_name.to_owned(),
|
||||
Err(DropDatabaseError::MySqlError(err.to_string())),
|
||||
);
|
||||
continue;
|
||||
@@ -218,7 +208,7 @@ impl FromRow<'_, sqlx::mysql::MySqlRow> for DatabaseRow {
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s.split(',').map(std::borrow::ToOwned::to_owned).collect())
|
||||
Some(s.split(',').map(|s| s.to_owned()).collect())
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
@@ -246,24 +236,20 @@ pub async fn list_databases(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListDatabasesResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for database_name in database_names {
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::Database(database_name.clone()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(ListDatabasesError::ValidationError)
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::Database(database_name.clone()), unix_user)
|
||||
.map_err(ListDatabasesError::ValidationError)
|
||||
{
|
||||
results.insert(database_name.clone(), Err(err));
|
||||
results.insert(database_name.to_owned(), Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = sqlx::query_as::<_, DatabaseRow>(
|
||||
r"
|
||||
r#"
|
||||
SELECT
|
||||
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`,
|
||||
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`,
|
||||
@@ -281,7 +267,7 @@ pub async fn list_databases(
|
||||
ON `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = `mysql`.`db`.`DB`
|
||||
WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` = ?
|
||||
GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME`
|
||||
",
|
||||
"#,
|
||||
|
||||
)
|
||||
.bind(database_name.to_string())
|
||||
@@ -289,7 +275,9 @@ pub async fn list_databases(
|
||||
.await
|
||||
.map_err(|err| ListDatabasesError::MySqlError(err.to_string()))
|
||||
.and_then(|database| {
|
||||
database.map_or_else(|| Err(ListDatabasesError::DatabaseDoesNotExist), Ok)
|
||||
database
|
||||
.map(Ok)
|
||||
.unwrap_or_else(|| Err(ListDatabasesError::DatabaseDoesNotExist))
|
||||
});
|
||||
|
||||
if let Err(err) = &result {
|
||||
@@ -308,10 +296,9 @@ pub async fn list_all_databases_for_user(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListAllDatabasesResponse {
|
||||
let result = sqlx::query_as::<_, DatabaseRow>(
|
||||
r"
|
||||
r#"
|
||||
SELECT
|
||||
CAST(`information_schema`.`SCHEMATA`.`SCHEMA_NAME` AS CHAR(64)) AS `database`,
|
||||
GROUP_CONCAT(DISTINCT CAST(`information_schema`.`TABLES`.`TABLE_NAME` AS CHAR(64)) SEPARATOR ',') AS `tables`,
|
||||
@@ -330,9 +317,9 @@ pub async fn list_all_databases_for_user(
|
||||
WHERE `information_schema`.`SCHEMATA`.`SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
||||
AND `information_schema`.`SCHEMATA`.`SCHEMA_NAME` REGEXP ?
|
||||
GROUP BY `information_schema`.`SCHEMATA`.`SCHEMA_NAME`
|
||||
",
|
||||
"#,
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.fetch_all(connection)
|
||||
.await
|
||||
.map_err(|err| ListAllDatabasesError::MySqlError(err.to_string()));
|
||||
|
||||
@@ -28,10 +28,10 @@ use crate::{
|
||||
DatabasePrivilegesDiff,
|
||||
},
|
||||
protocol::{
|
||||
DiffDoesNotApplyError, ListAllPrivilegesError, ListAllPrivilegesResponse,
|
||||
ListPrivilegesError, ListPrivilegesResponse, ModifyDatabasePrivilegesError,
|
||||
ModifyPrivilegesResponse,
|
||||
request_validation::{GroupDenylist, validate_db_or_user_request},
|
||||
DiffDoesNotApplyError, GetAllDatabasesPrivilegeDataError,
|
||||
GetDatabasesPrivilegeDataError, ListAllPrivilegesResponse, ListPrivilegesResponse,
|
||||
ModifyDatabasePrivilegesError, ModifyPrivilegesResponse,
|
||||
request_validation::validate_db_or_user_request,
|
||||
},
|
||||
types::{DbOrUser, MySQLDatabase, MySQLUser},
|
||||
},
|
||||
@@ -50,11 +50,12 @@ use crate::{
|
||||
fn get_mysql_row_priv_field(row: &MySqlRow, position: usize) -> Result<bool, sqlx::Error> {
|
||||
let field = DATABASE_PRIVILEGE_FIELDS[position];
|
||||
let value = row.try_get(position)?;
|
||||
if let Some(val) = rev_yn(value) {
|
||||
Ok(val)
|
||||
} else {
|
||||
tracing::warn!(r#"Invalid value for privilege "{}": '{}'"#, field, value);
|
||||
Ok(false)
|
||||
match rev_yn(value) {
|
||||
Some(val) => Ok(val),
|
||||
_ => {
|
||||
tracing::warn!(r#"Invalid value for privilege "{}": '{}'"#, field, value);
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,43 +143,32 @@ pub async fn get_databases_privilege_data(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListPrivilegesResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for database_name in &database_names {
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::Database(database_name.clone()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(ListPrivilegesError::ValidationError)
|
||||
for database_name in database_names.iter() {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::Database(database_name.clone()), unix_user)
|
||||
.map_err(GetDatabasesPrivilegeDataError::ValidationError)
|
||||
{
|
||||
results.insert(database_name.to_owned(), Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_database_exists(database_name, connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(
|
||||
database_name.to_owned(),
|
||||
Err(ListPrivilegesError::DatabaseDoesNotExist),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
results.insert(
|
||||
database_name.to_owned(),
|
||||
Err(ListPrivilegesError::MySqlError(e.to_string())),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(true) => {}
|
||||
if !unsafe_database_exists(database_name, connection)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
results.insert(
|
||||
database_name.to_owned(),
|
||||
Err(GetDatabasesPrivilegeDataError::DatabaseDoesNotExist),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let result = unsafe_get_database_privileges(database_name, connection)
|
||||
.await
|
||||
.map_err(|e| ListPrivilegesError::MySqlError(e.to_string()));
|
||||
.map_err(|e| GetDatabasesPrivilegeDataError::MySqlError(e.to_string()));
|
||||
|
||||
results.insert(database_name.to_owned(), result);
|
||||
}
|
||||
@@ -191,13 +181,13 @@ pub async fn get_databases_privilege_data(
|
||||
/// TODO: make this constant
|
||||
fn get_all_db_privs_query() -> String {
|
||||
format!(
|
||||
indoc! {r"
|
||||
indoc! {r#"
|
||||
SELECT {} FROM `db` WHERE `db` IN
|
||||
(SELECT DISTINCT CAST(`SCHEMA_NAME` AS CHAR(64)) AS `database`
|
||||
FROM `information_schema`.`SCHEMATA`
|
||||
WHERE `SCHEMA_NAME` NOT IN ('information_schema', 'performance_schema', 'mysql', 'sys')
|
||||
AND `SCHEMA_NAME` REGEXP ?)
|
||||
"},
|
||||
"#},
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.iter()
|
||||
.map(|field| quote_identifier(field))
|
||||
@@ -210,13 +200,12 @@ pub async fn get_all_database_privileges(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListAllPrivilegesResponse {
|
||||
let result = sqlx::query_as::<_, DatabasePrivilegeRow>(&get_all_db_privs_query())
|
||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.fetch_all(connection)
|
||||
.await
|
||||
.map_err(|e| ListAllPrivilegesError::MySqlError(e.to_string()));
|
||||
.map_err(|e| GetAllDatabasesPrivilegeDataError::MySqlError(e.to_string()));
|
||||
|
||||
if let Err(e) = &result {
|
||||
tracing::error!("Failed to get all database privileges: {:?}", e);
|
||||
@@ -240,23 +229,25 @@ async fn unsafe_apply_privilege_diff(
|
||||
let question_marks =
|
||||
std::iter::repeat_n("?", DATABASE_PRIVILEGE_FIELDS.len()).join(",");
|
||||
|
||||
sqlx::query(format!("INSERT INTO `db` ({tables}) VALUES ({question_marks})").as_str())
|
||||
.bind(p.db.to_string())
|
||||
.bind(p.user.to_string())
|
||||
.bind(yn(p.select_priv))
|
||||
.bind(yn(p.insert_priv))
|
||||
.bind(yn(p.update_priv))
|
||||
.bind(yn(p.delete_priv))
|
||||
.bind(yn(p.create_priv))
|
||||
.bind(yn(p.drop_priv))
|
||||
.bind(yn(p.alter_priv))
|
||||
.bind(yn(p.index_priv))
|
||||
.bind(yn(p.create_tmp_table_priv))
|
||||
.bind(yn(p.lock_tables_priv))
|
||||
.bind(yn(p.references_priv))
|
||||
.execute(connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
sqlx::query(
|
||||
format!("INSERT INTO `db` ({}) VALUES ({})", tables, question_marks).as_str(),
|
||||
)
|
||||
.bind(p.db.to_string())
|
||||
.bind(p.user.to_string())
|
||||
.bind(yn(p.select_priv))
|
||||
.bind(yn(p.insert_priv))
|
||||
.bind(yn(p.update_priv))
|
||||
.bind(yn(p.delete_priv))
|
||||
.bind(yn(p.create_priv))
|
||||
.bind(yn(p.drop_priv))
|
||||
.bind(yn(p.alter_priv))
|
||||
.bind(yn(p.index_priv))
|
||||
.bind(yn(p.create_tmp_table_priv))
|
||||
.bind(yn(p.lock_tables_priv))
|
||||
.bind(yn(p.references_priv))
|
||||
.execute(connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
DatabasePrivilegesDiff::Modified(p) => {
|
||||
let changes = DATABASE_PRIVILEGE_FIELDS
|
||||
@@ -278,23 +269,25 @@ async fn unsafe_apply_privilege_diff(
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query(format!("UPDATE `db` SET {changes} WHERE `Db` = ? AND `User` = ?").as_str())
|
||||
.bind(p.select_priv.map(change_to_yn))
|
||||
.bind(p.insert_priv.map(change_to_yn))
|
||||
.bind(p.update_priv.map(change_to_yn))
|
||||
.bind(p.delete_priv.map(change_to_yn))
|
||||
.bind(p.create_priv.map(change_to_yn))
|
||||
.bind(p.drop_priv.map(change_to_yn))
|
||||
.bind(p.alter_priv.map(change_to_yn))
|
||||
.bind(p.index_priv.map(change_to_yn))
|
||||
.bind(p.create_tmp_table_priv.map(change_to_yn))
|
||||
.bind(p.lock_tables_priv.map(change_to_yn))
|
||||
.bind(p.references_priv.map(change_to_yn))
|
||||
.bind(p.db.to_string())
|
||||
.bind(p.user.to_string())
|
||||
.execute(connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
sqlx::query(
|
||||
format!("UPDATE `db` SET {} WHERE `Db` = ? AND `User` = ?", changes).as_str(),
|
||||
)
|
||||
.bind(p.select_priv.map(change_to_yn))
|
||||
.bind(p.insert_priv.map(change_to_yn))
|
||||
.bind(p.update_priv.map(change_to_yn))
|
||||
.bind(p.delete_priv.map(change_to_yn))
|
||||
.bind(p.create_priv.map(change_to_yn))
|
||||
.bind(p.drop_priv.map(change_to_yn))
|
||||
.bind(p.alter_priv.map(change_to_yn))
|
||||
.bind(p.index_priv.map(change_to_yn))
|
||||
.bind(p.create_tmp_table_priv.map(change_to_yn))
|
||||
.bind(p.lock_tables_priv.map(change_to_yn))
|
||||
.bind(p.references_priv.map(change_to_yn))
|
||||
.bind(p.db.to_string())
|
||||
.bind(p.user.to_string())
|
||||
.execute(connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
}
|
||||
DatabasePrivilegesDiff::Deleted(p) => {
|
||||
sqlx::query("DELETE FROM `db` WHERE `Db` = ? AND `User` = ?")
|
||||
@@ -404,7 +397,6 @@ pub async fn apply_privilege_diffs(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ModifyPrivilegesResponse {
|
||||
let mut results: BTreeMap<(MySQLDatabase, MySQLUser), _> = BTreeMap::new();
|
||||
|
||||
@@ -416,7 +408,6 @@ pub async fn apply_privilege_diffs(
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::Database(diff.get_database_name().to_owned()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(ModifyDatabasePrivilegesError::UserValidationError)
|
||||
{
|
||||
@@ -424,48 +415,31 @@ pub async fn apply_privilege_diffs(
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_db_or_user_request(
|
||||
&DbOrUser::User(diff.get_user_name().to_owned()),
|
||||
unix_user,
|
||||
group_denylist,
|
||||
)
|
||||
.map_err(ModifyDatabasePrivilegesError::UserValidationError)
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(diff.get_user_name().to_owned()), unix_user)
|
||||
.map_err(ModifyDatabasePrivilegesError::UserValidationError)
|
||||
{
|
||||
results.insert(key, Err(err));
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_database_exists(diff.get_database_name(), connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(
|
||||
key,
|
||||
Err(ModifyDatabasePrivilegesError::DatabaseDoesNotExist),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
results.insert(
|
||||
key,
|
||||
Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(true) => {}
|
||||
if !unsafe_database_exists(diff.get_database_name(), connection)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
results.insert(
|
||||
key,
|
||||
Err(ModifyDatabasePrivilegesError::DatabaseDoesNotExist),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
match unsafe_user_exists(diff.get_user_name(), connection).await {
|
||||
Ok(false) => {
|
||||
results.insert(key, Err(ModifyDatabasePrivilegesError::UserDoesNotExist));
|
||||
continue;
|
||||
}
|
||||
Err(e) => {
|
||||
results.insert(
|
||||
key,
|
||||
Err(ModifyDatabasePrivilegesError::MySqlError(e.to_string())),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Ok(true) => {}
|
||||
if !unsafe_user_exists(diff.get_user_name(), connection)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
results.insert(key, Err(ModifyDatabasePrivilegesError::UserDoesNotExist));
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Err(err) = validate_diff(&diff, connection).await {
|
||||
|
||||
@@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::MySqlConnection;
|
||||
use sqlx::prelude::*;
|
||||
|
||||
use crate::core::protocol::request_validation::GroupDenylist;
|
||||
use crate::core::protocol::request_validation::validate_db_or_user_request;
|
||||
use crate::core::types::DbOrUser;
|
||||
use crate::{
|
||||
@@ -34,13 +33,13 @@ pub(super) async fn unsafe_user_exists(
|
||||
connection: &mut MySqlConnection,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
let result = sqlx::query(
|
||||
r"
|
||||
r#"
|
||||
SELECT EXISTS(
|
||||
SELECT 1
|
||||
FROM `mysql`.`user`
|
||||
WHERE `User` = ?
|
||||
)
|
||||
",
|
||||
"#,
|
||||
)
|
||||
.bind(db_user)
|
||||
.fetch_one(connection)
|
||||
@@ -59,18 +58,17 @@ pub async fn complete_user_name(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> Vec<MySQLUser> {
|
||||
let result = sqlx::query(
|
||||
r"
|
||||
r#"
|
||||
SELECT `User` AS `user`
|
||||
FROM `mysql`.`user`
|
||||
WHERE `User` REGEXP ?
|
||||
AND `User` LIKE ?
|
||||
",
|
||||
"#,
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
||||
.bind(format!("{user_prefix}%"))
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.bind(format!("{}%", user_prefix))
|
||||
.fetch_all(connection)
|
||||
.await;
|
||||
|
||||
@@ -99,14 +97,12 @@ pub async fn create_database_users(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> CreateUsersResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(CreateUserError::ValidationError)
|
||||
if let Err(err) = validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(CreateUserError::ValidationError)
|
||||
{
|
||||
results.insert(db_user, Err(err));
|
||||
continue;
|
||||
@@ -145,14 +141,12 @@ pub async fn drop_database_users(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> DropUsersResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(DropUserError::ValidationError)
|
||||
if let Err(err) = validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(DropUserError::ValidationError)
|
||||
{
|
||||
results.insert(db_user, Err(err));
|
||||
continue;
|
||||
@@ -188,54 +182,32 @@ pub async fn drop_database_users(
|
||||
|
||||
pub async fn set_password_for_database_user(
|
||||
db_user: &MySQLUser,
|
||||
password: Option<&str>,
|
||||
expiry: Option<chrono::NaiveDate>,
|
||||
password: &str,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
_db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> SetUserPasswordResponse {
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(SetPasswordError::ValidationError)?;
|
||||
|
||||
if password.is_none() && expiry.is_some() {
|
||||
return Err(SetPasswordError::ClearPasswordWithExpiry);
|
||||
}
|
||||
|
||||
match unsafe_user_exists(db_user, &mut *connection).await {
|
||||
Ok(false) => return Err(SetPasswordError::UserDoesNotExist),
|
||||
Err(err) => return Err(SetPasswordError::MySqlError(err.to_string())),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let result = if let Some(password) = password {
|
||||
let mut query = format!(
|
||||
let result = sqlx::query(
|
||||
format!(
|
||||
"ALTER USER {}@'%' IDENTIFIED BY {}",
|
||||
quote_literal(db_user),
|
||||
quote_literal(password).as_str(),
|
||||
);
|
||||
|
||||
if let Some(expiry_date) = expiry {
|
||||
query.push_str(&format!(" PASSWORD EXPIRE DATE '{}'", expiry_date));
|
||||
}
|
||||
|
||||
sqlx::query(query.as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()))
|
||||
} else {
|
||||
let query = format!(
|
||||
"ALTER USER {}@'%' IDENTIFIED WITH mysql_native_password AS ''",
|
||||
quote_literal(db_user),
|
||||
);
|
||||
|
||||
sqlx::query(query.as_str())
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()))
|
||||
};
|
||||
)
|
||||
.as_str(),
|
||||
)
|
||||
.execute(&mut *connection)
|
||||
.await
|
||||
.map(|_| ())
|
||||
.map_err(|err| SetPasswordError::MySqlError(err.to_string()));
|
||||
|
||||
if result.is_err() {
|
||||
tracing::error!(
|
||||
@@ -257,12 +229,12 @@ const DATABASE_USER_LOCK_STATUS_QUERY_MARIADB: &str = r#"
|
||||
AND `Host` = '%'
|
||||
"#;
|
||||
|
||||
const DATABASE_USER_LOCK_STATUS_QUERY_MYSQL: &str = r"
|
||||
const DATABASE_USER_LOCK_STATUS_QUERY_MYSQL: &str = r#"
|
||||
SELECT `mysql`.`user`.`account_locked` = 'Y'
|
||||
FROM `mysql`.`user`
|
||||
WHERE `User` = ?
|
||||
AND `Host` = '%'
|
||||
";
|
||||
"#;
|
||||
|
||||
// NOTE: this function is unsafe because it does no input validation.
|
||||
async fn database_user_is_locked_unsafe(
|
||||
@@ -297,14 +269,12 @@ pub async fn lock_database_users(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> LockUsersResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(LockUserError::ValidationError)
|
||||
if let Err(err) = validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(LockUserError::ValidationError)
|
||||
{
|
||||
results.insert(db_user, Err(err));
|
||||
continue;
|
||||
@@ -357,14 +327,12 @@ pub async fn unlock_database_users(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> UnlockUsersResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(UnlockUserError::ValidationError)
|
||||
if let Err(err) = validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(UnlockUserError::ValidationError)
|
||||
{
|
||||
results.insert(db_user, Err(err));
|
||||
continue;
|
||||
@@ -451,28 +419,26 @@ JOIN `global_priv` ON
|
||||
AND `user`.`Host` = `global_priv`.`Host`
|
||||
"#;
|
||||
|
||||
const DB_USER_SELECT_STATEMENT_MYSQL: &str = r"
|
||||
const DB_USER_SELECT_STATEMENT_MYSQL: &str = r#"
|
||||
SELECT
|
||||
`user`.`User`,
|
||||
`user`.`Host`,
|
||||
`user`.`authentication_string` != '' AS `has_password`,
|
||||
`user`.`account_locked` = 'Y' AS `account_locked`
|
||||
FROM `user`
|
||||
";
|
||||
"#;
|
||||
|
||||
pub async fn list_database_users(
|
||||
db_users: Vec<MySQLUser>,
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListUsersResponse {
|
||||
let mut results = BTreeMap::new();
|
||||
|
||||
for db_user in db_users {
|
||||
if let Err(err) =
|
||||
validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user, group_denylist)
|
||||
.map_err(ListUsersError::ValidationError)
|
||||
if let Err(err) = validate_db_or_user_request(&DbOrUser::User(db_user.clone()), unix_user)
|
||||
.map_err(ListUsersError::ValidationError)
|
||||
{
|
||||
results.insert(db_user, Err(err));
|
||||
continue;
|
||||
@@ -493,10 +459,8 @@ pub async fn list_database_users(
|
||||
tracing::error!("Failed to list database user '{}': {:?}", &db_user, err);
|
||||
}
|
||||
|
||||
if let Ok(Some(user)) = result.as_mut()
|
||||
&& let Err(err) = set_databases_where_user_has_privileges(user, &mut *connection).await
|
||||
{
|
||||
result = Err(err);
|
||||
if let Ok(Some(user)) = result.as_mut() {
|
||||
append_databases_where_user_has_privileges(user, &mut *connection).await;
|
||||
}
|
||||
|
||||
match result {
|
||||
@@ -513,7 +477,6 @@ pub async fn list_all_database_users_for_unix_user(
|
||||
unix_user: &UnixUser,
|
||||
connection: &mut MySqlConnection,
|
||||
db_is_mariadb: bool,
|
||||
group_denylist: &GroupDenylist,
|
||||
) -> ListAllUsersResponse {
|
||||
let mut result = sqlx::query_as::<_, DatabaseUser>(
|
||||
&(if db_is_mariadb {
|
||||
@@ -522,7 +485,7 @@ pub async fn list_all_database_users_for_unix_user(
|
||||
DB_USER_SELECT_STATEMENT_MYSQL.to_string()
|
||||
} + "WHERE `user`.`User` REGEXP ?"),
|
||||
)
|
||||
.bind(create_user_group_matching_regex(unix_user, group_denylist))
|
||||
.bind(create_user_group_matching_regex(unix_user))
|
||||
.fetch_all(&mut *connection)
|
||||
.await
|
||||
.map_err(|err| ListAllUsersError::MySqlError(err.to_string()));
|
||||
@@ -533,33 +496,27 @@ pub async fn list_all_database_users_for_unix_user(
|
||||
|
||||
if let Ok(users) = result.as_mut() {
|
||||
for user in users {
|
||||
if let Err(mysql_error) =
|
||||
set_databases_where_user_has_privileges(user, &mut *connection).await
|
||||
{
|
||||
return Err(ListAllUsersError::MySqlError(mysql_error.to_string()));
|
||||
}
|
||||
append_databases_where_user_has_privileges(user, &mut *connection).await;
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// This function sets the `databases` field of the given `DatabaseUser`
|
||||
/// where the user has any privileges.
|
||||
pub async fn set_databases_where_user_has_privileges(
|
||||
pub async fn append_databases_where_user_has_privileges(
|
||||
db_user: &mut DatabaseUser,
|
||||
connection: &mut MySqlConnection,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
) {
|
||||
let database_list = sqlx::query(
|
||||
formatdoc!(
|
||||
r"
|
||||
r#"
|
||||
SELECT `Db` AS `database`
|
||||
FROM `db`
|
||||
WHERE `User` = ? AND ({})
|
||||
",
|
||||
"#,
|
||||
DATABASE_PRIVILEGE_FIELDS
|
||||
.iter()
|
||||
.map(|field| format!("`{field}` = 'Y'"))
|
||||
.map(|field| format!("`{}` = 'Y'", field))
|
||||
.join(" OR "),
|
||||
)
|
||||
.as_str(),
|
||||
@@ -576,11 +533,11 @@ pub async fn set_databases_where_user_has_privileges(
|
||||
);
|
||||
}
|
||||
|
||||
db_user.databases = database_list.and_then(|rows| {
|
||||
rows.into_iter()
|
||||
.map(|row| try_get_with_binary_fallback(&row, "database"))
|
||||
.collect::<Result<Vec<String>, sqlx::Error>>()
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
db_user.databases = database_list
|
||||
.map(|rows| {
|
||||
rows.into_iter()
|
||||
.map(|row| try_get_with_binary_fallback(&row, "database").unwrap())
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
@@ -17,13 +17,9 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::{
|
||||
core::protocol::request_validation::GroupDenylist,
|
||||
server::{
|
||||
authorization::read_and_parse_group_denylist,
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
session_handler::session_handler,
|
||||
},
|
||||
use crate::server::{
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
session_handler::session_handler,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -40,7 +36,6 @@ pub struct ReloadEvent;
|
||||
pub struct Supervisor {
|
||||
config_path: PathBuf,
|
||||
config: Arc<Mutex<ServerConfig>>,
|
||||
group_deny_list: Arc<RwLock<GroupDenylist>>,
|
||||
systemd_mode: bool,
|
||||
|
||||
shutdown_cancel_token: CancellationToken,
|
||||
@@ -71,39 +66,20 @@ impl Supervisor {
|
||||
let config = ServerConfig::read_config_from_path(&config_path)
|
||||
.context("Failed to read server configuration")?;
|
||||
|
||||
let group_deny_list = if let Some(denylist_path) = &config.authorization.group_denylist_file
|
||||
{
|
||||
let denylist = read_and_parse_group_denylist(denylist_path)
|
||||
.context("Failed to read group denylist file")?;
|
||||
tracing::debug!(
|
||||
"Loaded group denylist with {} entries from {:?}",
|
||||
denylist.len(),
|
||||
denylist_path
|
||||
);
|
||||
Arc::new(RwLock::new(denylist))
|
||||
} else {
|
||||
tracing::debug!("No group denylist file specified, proceeding without a denylist");
|
||||
Arc::new(RwLock::new(GroupDenylist::new()))
|
||||
};
|
||||
|
||||
let mut watchdog_duration = None;
|
||||
let mut watchdog_micro_seconds = 0;
|
||||
#[cfg(target_os = "linux")]
|
||||
let watchdog_task =
|
||||
if systemd_mode && sd_notify::watchdog_enabled(true, &mut watchdog_micro_seconds) {
|
||||
let watchdog_duration_ = Duration::from_micros(watchdog_micro_seconds);
|
||||
watchdog_duration = Some(Duration::from_micros(watchdog_micro_seconds));
|
||||
tracing::debug!(
|
||||
"Systemd watchdog enabled with {} millisecond interval",
|
||||
watchdog_micro_seconds.div_ceil(1000),
|
||||
);
|
||||
watchdog_duration = Some(watchdog_duration_);
|
||||
Some(spawn_watchdog_task(watchdog_duration_))
|
||||
Some(spawn_watchdog_task(watchdog_duration.unwrap()))
|
||||
} else {
|
||||
tracing::debug!("Systemd watchdog not enabled, skipping watchdog thread");
|
||||
None
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let watchdog_task = None;
|
||||
|
||||
let db_connection_pool =
|
||||
Arc::new(RwLock::new(create_db_connection_pool(&config.mysql).await?));
|
||||
@@ -126,34 +102,19 @@ impl Supervisor {
|
||||
|
||||
let task_tracker = TaskTracker::new();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
let status_notifier_task = if systemd_mode {
|
||||
Some(spawn_status_notifier_task(task_tracker.clone()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let status_notifier_task = None;
|
||||
|
||||
let (tx, rx) = broadcast::channel(1);
|
||||
|
||||
// TODO: try to detech systemd socket before using the provided socket path
|
||||
#[cfg(target_os = "linux")]
|
||||
let listener = Arc::new(RwLock::new(match config.socket_path {
|
||||
Some(ref path) => create_unix_listener_with_socket_path(path.clone()).await?,
|
||||
None => create_unix_listener_with_systemd_socket().await?,
|
||||
}));
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let listener = Arc::new(RwLock::new(
|
||||
create_unix_listener_with_socket_path(
|
||||
config
|
||||
.socket_path
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("Socket path must be set"))?
|
||||
.clone(),
|
||||
)
|
||||
.await?,
|
||||
));
|
||||
|
||||
let (reload_tx, reload_rx) = broadcast::channel(1);
|
||||
let shutdown_cancel_token = CancellationToken::new();
|
||||
@@ -169,14 +130,12 @@ impl Supervisor {
|
||||
db_connection_pool.clone(),
|
||||
rx,
|
||||
db_is_mariadb.clone(),
|
||||
group_deny_list.clone(),
|
||||
))
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
config_path,
|
||||
config: Arc::new(Mutex::new(config)),
|
||||
group_deny_list,
|
||||
systemd_mode,
|
||||
reload_message_receiver: reload_rx,
|
||||
shutdown_cancel_token,
|
||||
@@ -219,24 +178,6 @@ impl Supervisor {
|
||||
.context("Failed to read server configuration")?;
|
||||
let mut config = self.config.clone().lock_owned().await;
|
||||
*config = new_config;
|
||||
|
||||
let group_deny_list = if let Some(denylist_path) = &config.authorization.group_denylist_file
|
||||
{
|
||||
let denylist = read_and_parse_group_denylist(denylist_path)
|
||||
.context("Failed to read group denylist file")?;
|
||||
|
||||
tracing::debug!(
|
||||
"Loaded group denylist with {} entries from {:?}",
|
||||
denylist.len(),
|
||||
denylist_path
|
||||
);
|
||||
denylist
|
||||
} else {
|
||||
tracing::debug!("No group denylist file specified, proceeding without a denylist");
|
||||
GroupDenylist::new()
|
||||
};
|
||||
let mut group_deny_list_lock = self.group_deny_list.write().await;
|
||||
*group_deny_list_lock = group_deny_list;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -270,28 +211,16 @@ impl Supervisor {
|
||||
// first. Make sure to handle that appropriately to avoid a deadlock.
|
||||
async fn reload_listener(&self) -> anyhow::Result<()> {
|
||||
let config = self.config.lock().await;
|
||||
#[cfg(target_os = "linux")]
|
||||
let new_listener = match config.socket_path {
|
||||
Some(ref path) => create_unix_listener_with_socket_path(path.clone()).await?,
|
||||
None => create_unix_listener_with_systemd_socket().await?,
|
||||
};
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let new_listener = create_unix_listener_with_socket_path(
|
||||
config
|
||||
.socket_path
|
||||
.as_ref()
|
||||
.ok_or(anyhow!("Socket path must be set"))?
|
||||
.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut listener = self.listener.write().await;
|
||||
*listener = new_listener;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reload(&self) -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Reloading])?;
|
||||
|
||||
let previous_config = self.config.lock().await.clone();
|
||||
@@ -328,14 +257,12 @@ impl Supervisor {
|
||||
self.resume_receiving_new_connections()?;
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Ready])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn shutdown(&self) -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Stopping])?;
|
||||
|
||||
tracing::debug!("Stop accepting new connections");
|
||||
@@ -384,7 +311,7 @@ impl Supervisor {
|
||||
}
|
||||
}
|
||||
|
||||
() = self.shutdown_cancel_token.cancelled() => {
|
||||
_ = self.shutdown_cancel_token.cancelled() => {
|
||||
tracing::info!("Shutting down server");
|
||||
self.shutdown().await?;
|
||||
break;
|
||||
@@ -396,7 +323,6 @@ impl Supervisor {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn spawn_watchdog_task(duration: Duration) -> JoinHandle<()> {
|
||||
tokio::spawn(async move {
|
||||
let mut interval = interval(duration.div_f32(2.0));
|
||||
@@ -413,7 +339,6 @@ fn spawn_watchdog_task(duration: Duration) -> JoinHandle<()> {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn spawn_status_notifier_task(task_tracker: TaskTracker) -> JoinHandle<()> {
|
||||
const STATUS_UPDATE_INTERVAL_SECS: Duration = Duration::from_secs(1);
|
||||
|
||||
@@ -424,7 +349,7 @@ fn spawn_status_notifier_task(task_tracker: TaskTracker) -> JoinHandle<()> {
|
||||
let count = task_tracker.len();
|
||||
|
||||
let message = if count > 0 {
|
||||
format!("Handling {count} connections")
|
||||
format!("Handling {} connections", count)
|
||||
} else {
|
||||
"Waiting for connections".to_string()
|
||||
};
|
||||
@@ -450,7 +375,7 @@ async fn create_unix_listener_with_socket_path(
|
||||
tracing::info!("Listening on socket {:?}", socket_path);
|
||||
|
||||
match fs::remove_file(socket_path.as_path()) {
|
||||
Ok(()) => {}
|
||||
Ok(_) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e.into()),
|
||||
}
|
||||
@@ -460,14 +385,13 @@ async fn create_unix_listener_with_socket_path(
|
||||
Ok(listener)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
async fn create_unix_listener_with_systemd_socket() -> anyhow::Result<TokioUnixListener> {
|
||||
let fd = sd_notify::listen_fds()
|
||||
.context("Failed to get file descriptors from systemd")?
|
||||
.next()
|
||||
.context("No file descriptors received from systemd")?;
|
||||
|
||||
debug_assert!(fd == 3, "Unexpected file descriptor from systemd: {fd}");
|
||||
debug_assert!(fd == 3, "Unexpected file descriptor from systemd: {}", fd);
|
||||
|
||||
tracing::debug!(
|
||||
"Received file descriptor from systemd with id: '{}', assuming socket",
|
||||
@@ -543,9 +467,7 @@ async fn listener_task(
|
||||
db_pool: Arc<RwLock<MySqlPool>>,
|
||||
mut supervisor_message_receiver: broadcast::Receiver<SupervisorMessage>,
|
||||
db_is_mariadb: Arc<RwLock<bool>>,
|
||||
group_denylist: Arc<RwLock<GroupDenylist>>,
|
||||
) -> anyhow::Result<()> {
|
||||
#[cfg(target_os = "linux")]
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Ready])?;
|
||||
|
||||
loop {
|
||||
@@ -581,14 +503,8 @@ async fn listener_task(
|
||||
|
||||
let db_pool_clone = db_pool.clone();
|
||||
let db_is_mariadb_clone = *db_is_mariadb.read().await;
|
||||
let group_denylist_arc_clone = group_denylist.clone();
|
||||
task_tracker.spawn(async move {
|
||||
match session_handler(
|
||||
conn,
|
||||
db_pool_clone,
|
||||
db_is_mariadb_clone,
|
||||
&*group_denylist_arc_clone.read().await,
|
||||
).await {
|
||||
match session_handler(conn, db_pool_clone, db_is_mariadb_clone).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to run server: {}", e);
|
||||
|
||||
Reference in New Issue
Block a user