Compare commits
1 Commits
auth-daemo
...
denylists
| Author | SHA1 | Date | |
|---|---|---|---|
|
39a2e39ba7
|
@@ -132,6 +132,11 @@ 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/",
|
||||
|
||||
@@ -21,3 +21,6 @@ password_file = "/run/credentials/muscl.service/muscl_mysql_password"
|
||||
|
||||
# Database connection timeout in seconds
|
||||
timeout = 2
|
||||
|
||||
[authorization]
|
||||
group_denylist_file = "/etc/muscl/group_denylist.txt"
|
||||
|
||||
58
assets/debian/group_denylist.txt
Normal file
58
assets/debian/group_denylist.txt
Normal file
@@ -0,0 +1,58 @@
|
||||
# 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:postgres
|
||||
group:proxy
|
||||
group:render
|
||||
group:root
|
||||
group:sasl
|
||||
group:shadow
|
||||
group:src
|
||||
group:staff
|
||||
group:sudo
|
||||
group:sync
|
||||
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,53 +0,0 @@
|
||||
[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
|
||||
@@ -1,8 +0,0 @@
|
||||
[Unit]
|
||||
Description=Authorization daemon for Muscl
|
||||
WantedBy=sockets.target
|
||||
|
||||
[Socket]
|
||||
ListenStream=/run/muscl/muscl-auth-daemon.socket
|
||||
Accept=no
|
||||
SocketMode=0660
|
||||
@@ -1,84 +0,0 @@
|
||||
#!/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)
|
||||
@@ -105,7 +105,6 @@
|
||||
fileset = lib.fileset.unions [
|
||||
(craneLib.fileset.commonCargoSources ./.)
|
||||
./assets
|
||||
./examples
|
||||
];
|
||||
};
|
||||
in {
|
||||
|
||||
@@ -85,9 +85,6 @@ buildFunction ({
|
||||
install -Dm644 assets/systemd/muscl.service -t "$out/lib/systemd/system"
|
||||
substituteInPlace "$out/lib/systemd/system/muscl.service" \
|
||||
--replace-fail '/usr/bin/muscl' "$out/bin/muscl"
|
||||
|
||||
mkdir -p "$out/share/muscl"
|
||||
cp -r examples "$out/share/muscl"
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
|
||||
110
nix/module.nix
110
nix/module.nix
@@ -27,31 +27,6 @@ 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 {
|
||||
@@ -65,6 +40,14 @@ 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;
|
||||
@@ -106,6 +89,12 @@ 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))
|
||||
|
||||
@@ -120,6 +109,10 @@ 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;
|
||||
@@ -174,72 +167,5 @@ 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,25 +56,6 @@ 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 = {
|
||||
|
||||
@@ -9,10 +9,12 @@ 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,
|
||||
core::{
|
||||
common::{DEFAULT_CONFIG_PATH, DEFAULT_SOCKET_PATH, UnixUser, executing_in_suid_sgid_mode},
|
||||
protocol::request_validation::GroupDenylist,
|
||||
},
|
||||
server::{
|
||||
authorization::read_and_parse_group_denylist,
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
landlock::landlock_restrict_server,
|
||||
session_handler,
|
||||
@@ -270,6 +272,13 @@ fn run_forked_server(
|
||||
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()
|
||||
@@ -292,6 +301,7 @@ fn run_forked_server(
|
||||
&unix_user,
|
||||
db_pool,
|
||||
db_is_mariadb,
|
||||
&group_denylist,
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use indoc::indoc;
|
||||
use itertools::Itertools;
|
||||
use nix::{libc::gid_t, unistd::Group};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -60,6 +63,9 @@ pub enum AuthorizationError {
|
||||
// TODO: I don't think this should ever happen?
|
||||
#[error("Name cannot be empty")]
|
||||
StringEmpty,
|
||||
|
||||
#[error("Group was found in denylist")]
|
||||
DenylistError,
|
||||
}
|
||||
|
||||
impl AuthorizationError {
|
||||
@@ -105,6 +111,9 @@ impl AuthorizationError {
|
||||
db_or_user.lowercased_noun()
|
||||
)
|
||||
.to_string(),
|
||||
AuthorizationError::DenylistError => {
|
||||
format!("'{}' is denied by the group denylist", db_or_user.name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +121,7 @@ impl AuthorizationError {
|
||||
match self {
|
||||
AuthorizationError::NoMatch => "no-match",
|
||||
AuthorizationError::StringEmpty => "string-empty",
|
||||
AuthorizationError::DenylistError => "denylist-error",
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,8 +165,25 @@ impl ValidationError {
|
||||
}
|
||||
}
|
||||
|
||||
pub type GroupDenylist = HashSet<gid_t>;
|
||||
|
||||
const MAX_NAME_LENGTH: usize = 64;
|
||||
|
||||
// TODO: use this to render the help message for authorization-error/no-match
|
||||
fn get_user_groups(user: &UnixUser, group_denylist: &GroupDenylist) -> Vec<String> {
|
||||
use nix::unistd::Group;
|
||||
let mut groups = Vec::new();
|
||||
|
||||
for group in user.groups.iter() {
|
||||
if let Some(g) = Group::from_name(group).ok().flatten()
|
||||
&& !group_denylist.contains(&g.gid.as_raw()) {
|
||||
groups.push(group.clone());
|
||||
}
|
||||
}
|
||||
|
||||
groups
|
||||
}
|
||||
|
||||
pub fn validate_name(name: &str) -> Result<(), NameValidationError> {
|
||||
if name.is_empty() {
|
||||
Err(NameValidationError::EmptyString)
|
||||
@@ -207,6 +234,30 @@ pub fn validate_authorization_by_prefixes(
|
||||
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,
|
||||
@@ -216,6 +267,14 @@ pub fn validate_db_or_user_request(
|
||||
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,
|
||||
// TODO: pass actual denylist
|
||||
&GroupDenylist::new(),
|
||||
)
|
||||
.map_err(ValidationError::AuthorizationError)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
mod authorization;
|
||||
pub mod authorization;
|
||||
pub mod command;
|
||||
mod common;
|
||||
pub mod config;
|
||||
|
||||
@@ -1,12 +1,21 @@
|
||||
use std::{collections::HashSet, path::Path};
|
||||
|
||||
use anyhow::Context;
|
||||
use nix::unistd::Group;
|
||||
|
||||
use crate::core::{
|
||||
common::UnixUser,
|
||||
protocol::{CheckAuthorizationError, request_validation::validate_db_or_user_request},
|
||||
protocol::{
|
||||
CheckAuthorizationError,
|
||||
request_validation::{GroupDenylist, 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();
|
||||
|
||||
@@ -17,9 +26,102 @@ pub async fn check_authorization(
|
||||
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 {
|
||||
anyhow::bail!(
|
||||
"Invalid format in denylist file at {:?} on line {}: {}",
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
line
|
||||
);
|
||||
}
|
||||
|
||||
match parts[0] {
|
||||
"gid" => {
|
||||
let gid: u32 = parts[1].parse().with_context(|| {
|
||||
format!(
|
||||
"Invalid GID in denylist file at {:?} on line {}: {}",
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
parts[1]
|
||||
)
|
||||
})?;
|
||||
let group = Group::from_gid(nix::unistd::Gid::from_raw(gid))
|
||||
.context(format!(
|
||||
"Failed to get group for GID {} in denylist file at {:?} on line {}",
|
||||
gid,
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
))?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"No group found for GID {} in denylist file at {:?} on line {}",
|
||||
gid,
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
)
|
||||
})?;
|
||||
groups.insert(group.gid.as_raw());
|
||||
}
|
||||
"group" => {
|
||||
let group = Group::from_name(parts[1])
|
||||
.context(format!(
|
||||
"Failed to get group for name '{}' in denylist file at {:?} on line {}",
|
||||
parts[1],
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
))?
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"No group found for name '{}' in denylist file at {:?} on line {}",
|
||||
parts[1],
|
||||
denylist_path,
|
||||
line_number + 1
|
||||
)
|
||||
})?;
|
||||
groups.insert(group.gid.as_raw());
|
||||
}
|
||||
_ => {
|
||||
anyhow::bail!(
|
||||
"Invalid prefix '{}' in denylist file at {:?} on line {}: {}",
|
||||
parts[0],
|
||||
denylist_path,
|
||||
line_number + 1,
|
||||
line
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(groups)
|
||||
}
|
||||
|
||||
@@ -78,9 +78,15 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use crate::{
|
||||
common::UnixUser,
|
||||
protocol::{
|
||||
Request, Response, ServerToClientMessageStream, SetPasswordError,
|
||||
create_server_to_client_message_stream,
|
||||
create_server_to_client_message_stream, request_validation::GroupDenylist,
|
||||
},
|
||||
},
|
||||
server::{
|
||||
@@ -39,6 +39,7 @@ 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(),
|
||||
@@ -85,8 +86,14 @@ 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).await;
|
||||
let result = session_handler_with_unix_user(
|
||||
socket,
|
||||
&unix_user,
|
||||
db_pool,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
|
||||
tracing::info!(
|
||||
"Finished handling requests for connection from user: {}",
|
||||
@@ -104,6 +111,7 @@ 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);
|
||||
|
||||
@@ -131,6 +139,7 @@ pub async fn session_handler_with_unix_user(
|
||||
unix_user,
|
||||
&mut db_connection,
|
||||
db_is_mariadb,
|
||||
group_denylist,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -147,6 +156,7 @@ 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 {
|
||||
@@ -178,7 +188,7 @@ async fn session_handler_with_db_connection(
|
||||
|
||||
let response = match request {
|
||||
Request::CheckAuthorization(dbs_or_users) => {
|
||||
let result = check_authorization(dbs_or_users, unix_user).await;
|
||||
let result = check_authorization(dbs_or_users, unix_user, group_denylist).await;
|
||||
Response::CheckAuthorization(result)
|
||||
}
|
||||
Request::CompleteDatabaseName(partial_database_name) => {
|
||||
|
||||
@@ -17,9 +17,13 @@ use tokio::{
|
||||
};
|
||||
use tokio_util::{sync::CancellationToken, task::TaskTracker};
|
||||
|
||||
use crate::server::{
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
session_handler::session_handler,
|
||||
use crate::{
|
||||
core::protocol::request_validation::GroupDenylist,
|
||||
server::{
|
||||
authorization::read_and_parse_group_denylist,
|
||||
config::{MysqlConfig, ServerConfig},
|
||||
session_handler::session_handler,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -36,6 +40,7 @@ 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,
|
||||
@@ -66,6 +71,23 @@ impl Supervisor {
|
||||
let config = ServerConfig::read_config_from_path(&config_path)
|
||||
.context("Failed to read server configuration")?;
|
||||
|
||||
let group_deny_list = match &config.authorization.group_denylist_file {
|
||||
Some(denylist_path) => {
|
||||
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))
|
||||
}
|
||||
None => {
|
||||
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;
|
||||
let watchdog_task =
|
||||
@@ -130,12 +152,14 @@ 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,
|
||||
@@ -178,6 +202,26 @@ impl Supervisor {
|
||||
.context("Failed to read server configuration")?;
|
||||
let mut config = self.config.clone().lock_owned().await;
|
||||
*config = new_config;
|
||||
|
||||
let group_deny_list = match &config.authorization.group_denylist_file {
|
||||
Some(denylist_path) => {
|
||||
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
|
||||
}
|
||||
None => {
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -467,6 +511,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<()> {
|
||||
sd_notify::notify(false, &[sd_notify::NotifyState::Ready])?;
|
||||
|
||||
@@ -503,8 +548,14 @@ 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).await {
|
||||
match session_handler(
|
||||
conn,
|
||||
db_pool_clone,
|
||||
db_is_mariadb_clone,
|
||||
&*group_denylist_arc_clone.read().await,
|
||||
).await {
|
||||
Ok(()) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to run server: {}", e);
|
||||
|
||||
Reference in New Issue
Block a user