feat!: implement complete password auth chain (+)
This is a final implementation, it completely works, is safe and audited. There are several changes overall (most explained in the README): - Added `debuggable` cargo feature to disable debug ability, reducing a bit library size. - Completed README with setup, concernsm, features and more. - Add `asdeny` argument to behave as a PAM barrier. Updated nixos configuration accordingly. - Modularity, separated config from API client and CURL client, each can take a `debug` option for a clean function call interface. - Unified logging methods, there is a current `e/println!` call in all the codebase, behind a macro, to allow for consistent log formatting. - Readability, separated and broke down cognitive complicated structures for ease of readability, methods are as simple as I could get them to be and reusable across `authentication` and `password` flows. - HTTP headers now are macro'ed to prevent typos and have unified casing and representations. - Simplified CURL interface so it integrates perfectly with rust type system for just what's necessary, it's more readable and loggable. - Checked PAM returned errors to see if they are sensible enough. - Checked this interacts with `pam_unix.so` as expected.
This commit is contained in:
@@ -11,6 +11,10 @@ categories = ["authentication", "network-programming"]
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[features]
|
||||
default = ["debuggable"]
|
||||
debuggable = []
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
opt-level = 3
|
||||
|
||||
@@ -4,9 +4,35 @@ Tiny PAM module to perform authentication against a keycloak instance.
|
||||
|
||||
Testing and development are done within a nix environment, if you don't plan to use nix extract the pieces you care about, but we only support nix configuration.
|
||||
|
||||
# Features
|
||||
|
||||
- Supports authentication and setting/changing one's password.
|
||||
- Can be restrained to only a group of users, so it won't interfere with system users.
|
||||
- Debuggable, if the option is set, you will get useful logs when using the module.
|
||||
- Error ready, extensive logging and descriptive errors to discover what and where went wrong.
|
||||
- Minimal, made in rust with a tiny dependency tree, avoiding unnecessary dynamic memory use, all methods are straight-forward and lightweight.
|
||||
- Reliable, made in rust without the use of `unwrap`, `expect`, slice indexing, etc; and graceful error handling.
|
||||
|
||||
> [!INFO]
|
||||
> Debuggability can be turned off with a rust feature flag to slightly decrease library size.
|
||||
|
||||
# System Configuration
|
||||
|
||||
This is being worked on, refer to the test configuration at [`tests/nixos/configuration.nix`](./tests/nixos/configuration.nix).
|
||||
For examples, refer to the test nix configuration at [`tests/nixos/configuration.nix`](./tests/nixos/configuration.nix).
|
||||
|
||||
To explain the basic PAM flow, I will assume some PAM knowledge, but still explain most details, refer to PAM documentation if you have doubts.
|
||||
|
||||
All parts of the module should have a PAM priority slightly prior to the `pam_unix.so` counterpart.
|
||||
|
||||
The PAM module can work as two rule types: `auth` and `password`; but both behave in a similar way. Both do different stuff, but I will call it their "action" as they match for most of the flow.
|
||||
|
||||
For correct behavior, the module has to be staged:
|
||||
|
||||
- The first stage is `sufficient`, if the user is meant to be handled (refer to the configuration) the PAM module will perform its action or fail.
|
||||
One issue with `sufficient` is that failure will fall through the next module (`pam_unix.so` if configured as intended).
|
||||
- To fix this, the module has to be loaded again as `requisite` (so failures don't trigger the rest of the `required` chain) with an argument that will make it behave differently, this will behave analogous to `pam_deny.so`, but only for users meant to be handled. Essentially acts as a barrier preventing keycloak users from falling to additional authentication methods.
|
||||
|
||||
This means that normal users exempt from keycloak authentication will use PAM as if keycloak wasn't there. But users meant to be handled won't be able to pass onto other authentication methods.
|
||||
|
||||
# Arguments
|
||||
|
||||
@@ -18,11 +44,21 @@ You can `cargo doc` to find the documentation for this at `keycloak_pam > args >
|
||||
|
||||
With the purpose of being tiny and minimal, the module uses libcurl for the few requests it makes, so HTTP/S and all networking behavior is offloaded to it.
|
||||
|
||||
This is also made in rust, which means default `String` and `&str` are guaranteed to be UTF-8 encoded and invalid UTF-8 input might fail. I did try to use OS and C strings for all input, but I recommend still avoiding non-UTF8 input. Password change needs to convert to `String` at some point and UTF-8 input is required.
|
||||
This is also made in rust, which means default `String` and `&str` are guaranteed to be UTF-8 encoded, and invalid UTF-8 input might fail. I did try to use OS and C strings for all input, but I recommend still avoiding non-UTF8 input. For instance, currently, password change needs to convert to `String` at some point and UTF-8 input is required.
|
||||
|
||||
The code should **NEVER** panic (aside from memory management issues), no `unwrap`, `expect` or such methods should ever be used, slice indexing is avoided and errors are gracefully managed.
|
||||
|
||||
To check the group, keycloak-pam uses libc's `getgrnam_r`, this takes a buffer we hardcoded to 4 KiB, if there happens to be a group bigger than this, keycloak-pam won't be able to handle and will fail explaining this.
|
||||
To check the group, keycloak-pam uses libc's `getgrnam_r`, this takes a buffer we hardcoded to 4 KiB, if there happens to be a group bigger than this, keycloak-pam won't be able to handle it and will fail gracefully.
|
||||
|
||||
# Security
|
||||
|
||||
Important security concerns or questions might be:
|
||||
|
||||
- `faillock`: Faillock is a `required` PAM module that runs well before unix and this module. This service does not implement rate-limiting or similar mechanisms, but faillock should be there to handle this for us.
|
||||
- Setting a password as another user. This module doesn't check the user calling it, and it can be used to set password of uninitialized users, so if things were passed stright to the module, `passwd test` as `anotheruser` would just ask for a new password for `test`. Tested with `passwd` and **this is not the case**, PAM or `passwd` check the user you are trying to change the password to and your current one to prevent things like this.
|
||||
- Falling back to `pam_unix`, as explained in [configuration](#system-configuration), the module can be set up as a barrier to itself so failures of it in `sufficient` mode won't fall back to unix. If this were the case, it could create inconsistencies with `/etc/passwd` and keycloak, allowing arbitrary login with either module.
|
||||
|
||||
As the project evolves, it's important to revisit these concerns as it's a bit harder to guarantee them and could be potential breach points.
|
||||
|
||||
# Policy
|
||||
|
||||
@@ -34,20 +70,22 @@ Must check the subject user against the configured group. If it's not a member,
|
||||
|
||||
Here things differ a bit from login to password change:
|
||||
|
||||
## Login
|
||||
## Authentication
|
||||
|
||||
This is simple, will only allow login if the password used to log in is not temporary, any error will fail with an error.
|
||||
|
||||
The client used for this is still not definitive, currently it uses a client ID and client secret for this, `account` by default, but this might change.
|
||||
|
||||
## Registration
|
||||
## Password
|
||||
|
||||
Even then, a user might and should be able to login if their account is not set up (e.g. through ssh).
|
||||
Even then, a user might and should be able to login ito their unix account if their keycloak account is not set up (e.g. through ssh).
|
||||
|
||||
If they try to change their password without having one set up, it won't ask for their current one and will use the admin REST API to set the first one, this one won't be a temporary password and will fully set up their accounts.
|
||||
|
||||
# Behavior
|
||||
|
||||
## User Authentication
|
||||
|
||||
To authenticate users, the module uses the endpoint `{BASE_URL}/realms/{realm}/protocol/openid-connect/token` as a configured client. This needs a client ID and its secret that you need to get and configure per realm.
|
||||
|
||||
A curl request you can use anywhere to test this is:
|
||||
@@ -65,6 +103,16 @@ The secret and id there are just an example (`account` is a client configurd by
|
||||
|
||||
This is the simplest way to check credentials but also has the consequence of leaking unhandled oauth2/OIDC user tokens. I'm unsure if this could have further consequences but it's a risk worth considering.
|
||||
|
||||
## Password Change
|
||||
|
||||
This is a bit more convoluted and frail.
|
||||
|
||||
First of all, when a user wants to change their password, this module fetches their current passwords to see if the user has any.
|
||||
|
||||
If it has some, it will authenticate the user in the same way that [`User Authentication`](#user-authentication) does, and if it succeeds, change the password in the same way a user with none would.
|
||||
|
||||
If the user has no password it will ask for one and use the admin API to `PUT` a new one with the [`reset-password`](https://www.keycloak.org/docs-api/latest/rest-api/index.html#_put_adminrealmsrealmusersuser_idreset_password) endpoint.
|
||||
|
||||
# Development
|
||||
|
||||
The nix test configuration relies on bridging the VM IP, for this it relies on the default `libvirt`'s interface and needs the `qemu-bridge-helper`. This is found in a nix shell hook.
|
||||
|
||||
+10
@@ -8,9 +8,11 @@ pub const DEFAULT_CONFIGURATION_PATH: &CStr = c"/etc/keycloak-pam.json";
|
||||
///
|
||||
/// - `config=`, optional, accepts a path to load json configuration from, defaults to [`DEFAULT_CONFIGURATION_PATH`]
|
||||
/// - `debug`, optional, flag, ignores the PAM policy about silence and prints regardless
|
||||
/// - `asdeny`, will behave analogously to `pam_deny.so` for keycloak users as described in README.
|
||||
pub struct Args<'a> {
|
||||
pub config: &'a CStr,
|
||||
pub debug: bool,
|
||||
pub asdeny: bool,
|
||||
}
|
||||
|
||||
impl<'a> Args<'a> {
|
||||
@@ -19,11 +21,19 @@ impl<'a> Args<'a> {
|
||||
let mut parsed_args = Args {
|
||||
config: DEFAULT_CONFIGURATION_PATH,
|
||||
debug: false,
|
||||
asdeny: false,
|
||||
};
|
||||
|
||||
for arg in args {
|
||||
if arg == &c"debug" {
|
||||
#[cfg(not(feature = "debuggable"))]
|
||||
eprintln!(
|
||||
"WARN: Compile feature `debuggable` has been turned off, \
|
||||
so no debug info will be shown."
|
||||
);
|
||||
parsed_args.debug = true;
|
||||
} else if arg == &c"asdeny" {
|
||||
parsed_args.asdeny = true;
|
||||
}
|
||||
|
||||
if let Some((b"config", path)) = arg.split_ch(b'=') {
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
use std::{
|
||||
ffi::{CStr, CString, OsStr},
|
||||
fs::File,
|
||||
io,
|
||||
};
|
||||
|
||||
use crate::{ext::CStrExt as _, libc as mylibc};
|
||||
|
||||
#[allow(clippy::unsafe_derive_deserialize)] // seriously? -_-
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Configuration {
|
||||
/// Base URL to send requests to **without trailing slash**. (e.g. `"http://127.0.0.1:3000"`)
|
||||
pub base_url: String,
|
||||
/// Default realm name to use. (e.g. `"master"`)
|
||||
pub default_realm: String,
|
||||
/// Client ID to use for unprivileged user auths (e.g. `"account"`).
|
||||
///
|
||||
/// Must be allowed to authenticate `Direct access grants` (which `account` doesn't by default).
|
||||
pub client_id: String,
|
||||
/// Client secret for `client_id`.
|
||||
pub client_secret: String,
|
||||
/// Unix group name of users to check against keycloak.
|
||||
pub unix_group: CString,
|
||||
/// Username of the admin account.
|
||||
pub admin_username: String,
|
||||
/// Password of the admin account.
|
||||
pub admin_password: String,
|
||||
/// Client ID of the client to use for admin REST API authorization (e.g. `"admin-cli"`)
|
||||
pub admincli_client: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LoadError {
|
||||
#[error("opening configuration: {0}")]
|
||||
OpenConfig(#[from] io::Error),
|
||||
#[error("parsing configuration: {0}")]
|
||||
ParseConfig(#[from] serde_json::Error),
|
||||
}
|
||||
|
||||
impl Configuration {
|
||||
pub fn load_from_configuration(cfg: &OsStr) -> Result<Self, LoadError> {
|
||||
let f = File::open(cfg)?;
|
||||
Ok(serde_json::from_reader(f)?)
|
||||
}
|
||||
|
||||
pub fn should_skip(&self, username: &OsStr) -> Result<bool, mylibc::GetgrnamError> {
|
||||
mylibc::getgrnam_use(&self.unix_group, |group| {
|
||||
let mut members_ptr = group.gr_mem;
|
||||
|
||||
// SAFETY: The array pointer CANNOT be null, even if incremented.
|
||||
while let member_cstr_ptr = unsafe { *members_ptr }
|
||||
&& !member_cstr_ptr.is_null()
|
||||
{
|
||||
// SAFETY: its guaranteed to be a CStr and its non-null
|
||||
let member_osstr = unsafe { CStr::from_ptr(member_cstr_ptr) }.to_os_str();
|
||||
if member_osstr == username {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: if current pointer wasn't NULL, we can increase to the next one
|
||||
members_ptr = unsafe { members_ptr.add(1) };
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
//! Named macro literal headers to prevent typos and have consistent casing.
|
||||
//!
|
||||
//! Also type safe as in, content types must exist in an enum.
|
||||
|
||||
/// Fancy trick to get completion on an argument for [`mime_type`]
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! mime_completion {
|
||||
($ty:ident) => {{
|
||||
#![allow(
|
||||
dead_code,
|
||||
reason = "Instantiated for each use, will use just one variant"
|
||||
)]
|
||||
pub enum MimeType {
|
||||
UrlEncoded,
|
||||
Json,
|
||||
}
|
||||
_ = MimeType::$ty;
|
||||
}};
|
||||
}
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! mime_type {
|
||||
(Json) => {
|
||||
"application/json"
|
||||
};
|
||||
|
||||
(UrlEncoded) => {
|
||||
"application/x-www-form-urlencoded"
|
||||
};
|
||||
}
|
||||
#[cfg(doc)]
|
||||
pub(crate) use mime_type;
|
||||
|
||||
// --- exported macros here on ---
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! content_type {
|
||||
($ty:ident) => {{
|
||||
mime_completion!($ty);
|
||||
::std::concat!("Content-Type: ", mime_type!($ty))
|
||||
}};
|
||||
}
|
||||
pub(crate) use content_type;
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! accept {
|
||||
($ty:ident) => {{
|
||||
mime_completion!($ty);
|
||||
::std::concat!("Accept: ", mime_type!($ty))
|
||||
}};
|
||||
}
|
||||
pub(crate) use accept;
|
||||
|
||||
#[macro_export(local_inner_macros)]
|
||||
macro_rules! authorization_bearer {
|
||||
($tok:expr) => {
|
||||
::std::format!("Authorization: Bearer {}", $tok)
|
||||
};
|
||||
}
|
||||
pub(crate) use authorization_bearer;
|
||||
+81
-201
@@ -1,40 +1,22 @@
|
||||
use std::{
|
||||
ffi::{CStr, CString, OsStr},
|
||||
fmt,
|
||||
fs::File,
|
||||
io::{self, Cursor, Read as _},
|
||||
os::unix::ffi::OsStrExt as _,
|
||||
};
|
||||
|
||||
use curl::easy::{Easy, List};
|
||||
use serde::Deserialize;
|
||||
use std::{ffi::OsStr, os::unix::ffi::OsStrExt as _};
|
||||
|
||||
use crate::{
|
||||
ext::{CStrExt as _, StrExt},
|
||||
keycloak::api::TokenResponse,
|
||||
libc as mylibc,
|
||||
config::Configuration,
|
||||
ext::StrExt as _,
|
||||
keycloak::{
|
||||
api::TokenResponse,
|
||||
mycurl::{Method, Response},
|
||||
},
|
||||
};
|
||||
|
||||
pub mod api;
|
||||
pub mod headers;
|
||||
pub mod mycurl;
|
||||
|
||||
#[allow(clippy::unsafe_derive_deserialize)] // seriously? -_-
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct KeycloakAPI {
|
||||
base_url: String,
|
||||
default_realm: String,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
unix_group: CString,
|
||||
admin_username: String,
|
||||
admin_password: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum LoadError {
|
||||
#[error("opening configuration: {0}")]
|
||||
OpenConfig(#[from] io::Error),
|
||||
#[error("parsing configuration: {0}")]
|
||||
ParseConfig(#[from] serde_json::Error),
|
||||
pub struct KeycloakAPI<'a> {
|
||||
config: &'a Configuration,
|
||||
curl: mycurl::Client,
|
||||
debug: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
@@ -70,122 +52,39 @@ pub enum LoginError {
|
||||
InvalidLogin(#[from] api::AuthError),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Method<'a> {
|
||||
GET,
|
||||
POST(Option<&'a [u8]>),
|
||||
PUT(Option<Box<[u8]>>),
|
||||
macro_rules! err_http_body {
|
||||
(if $debug:expr => $body:expr) => {
|
||||
$crate::macros::debug_print!(if $debug =>
|
||||
"keycloak-pam: got HTTP error: {}",
|
||||
String::from_utf8_lossy($body)
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
impl fmt::Debug for Method<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::GET => write!(f, "GET"),
|
||||
Self::POST(None) => write!(f, "POST"),
|
||||
Self::POST(Some(_)) => write!(f, "POST<body>"),
|
||||
Self::PUT(None) => write!(f, "PUT"),
|
||||
Self::PUT(Some(_)) => write!(f, "PUT<body>"),
|
||||
}
|
||||
}
|
||||
// if self.debug => "matched users" = found_users
|
||||
// -> "keycloak-pam: matched users = {found_users:#?}"
|
||||
macro_rules! debug_info {
|
||||
(if $debug:expr => $name:literal = $value:expr) => {
|
||||
$crate::macros::debug_print!(if $debug =>
|
||||
::std::concat!($name, " = {:#?}"), $value
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct CurlStatus(u32);
|
||||
|
||||
impl CurlStatus {
|
||||
const fn is_ok(self) -> bool {
|
||||
matches!(self.0, 200..300)
|
||||
}
|
||||
}
|
||||
|
||||
fn curl_request(
|
||||
method: Method,
|
||||
uri: &str,
|
||||
headers: &[&str],
|
||||
debug: bool,
|
||||
) -> Result<(CurlStatus, Vec<u8>), curl::Error> {
|
||||
if debug {
|
||||
eprintln!("{method:?} {uri} -H \"{headers:?}\"");
|
||||
}
|
||||
|
||||
let mut easy = Easy::new();
|
||||
|
||||
let mut curl_headers = List::new();
|
||||
for header in headers {
|
||||
curl_headers.append(header)?;
|
||||
}
|
||||
easy.http_headers(curl_headers)?;
|
||||
|
||||
easy.url(uri)?;
|
||||
|
||||
match method {
|
||||
Method::GET => {
|
||||
easy.get(true)?;
|
||||
}
|
||||
Method::POST(body) => {
|
||||
easy.post(true)?;
|
||||
if let Some(body) = body {
|
||||
easy.post_fields_copy(body)?;
|
||||
}
|
||||
}
|
||||
Method::PUT(body) => {
|
||||
easy.put(true)?;
|
||||
if let Some(body) = body {
|
||||
easy.upload(true)?;
|
||||
easy.in_filesize(body.len() as u64)?;
|
||||
let mut cur = Cursor::new(body);
|
||||
easy.read_function(move |buf| Ok(cur.read(buf).unwrap_or(0)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut response_data = Vec::new();
|
||||
{
|
||||
let mut transfer = easy.transfer();
|
||||
transfer.write_function(|data| {
|
||||
response_data.extend_from_slice(data);
|
||||
Ok(data.len())
|
||||
})?;
|
||||
|
||||
transfer.perform()?;
|
||||
}
|
||||
|
||||
Ok((CurlStatus(easy.response_code()?), response_data))
|
||||
}
|
||||
|
||||
impl KeycloakAPI {
|
||||
pub fn load_from_configuration(cfg: &OsStr) -> Result<Self, LoadError> {
|
||||
let f = File::open(cfg)?;
|
||||
Ok(serde_json::from_reader(f)?)
|
||||
}
|
||||
|
||||
pub fn should_skip(&self, username: &OsStr) -> Result<bool, mylibc::GetgrnamError> {
|
||||
mylibc::getgrnam_use(&self.unix_group, |group| {
|
||||
let mut members_ptr = group.gr_mem;
|
||||
|
||||
// SAFETY: The array pointer CANNOT be null, even if incremented.
|
||||
while let member_cstr_ptr = unsafe { *members_ptr }
|
||||
&& !member_cstr_ptr.is_null()
|
||||
{
|
||||
// SAFETY: its guaranteed to be a CStr and its non-null
|
||||
let member_osstr = unsafe { CStr::from_ptr(member_cstr_ptr) }.to_os_str();
|
||||
if member_osstr == username {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SAFETY: if current pointer wasn't NULL, we can increase to the next one
|
||||
members_ptr = unsafe { members_ptr.add(1) };
|
||||
}
|
||||
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_admin_token(&self, debug: bool) -> Result<TokenResponse, LoginError> {
|
||||
self.get_admincli_oidc_token(
|
||||
self.admin_username.as_os_str(),
|
||||
self.admin_password.as_os_str(),
|
||||
impl<'a> KeycloakAPI<'a> {
|
||||
#[must_use]
|
||||
pub const fn new(config: &'a Configuration, debug: bool) -> Self {
|
||||
Self {
|
||||
config,
|
||||
curl: mycurl::Client { debug },
|
||||
debug,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_admin_token(&self) -> Result<TokenResponse, LoginError> {
|
||||
self.get_admincli_oidc_token(
|
||||
self.config.admin_username.as_os_str(),
|
||||
self.config.admin_password.as_os_str(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -193,53 +92,51 @@ impl KeycloakAPI {
|
||||
&self,
|
||||
admin_username: &OsStr,
|
||||
admin_password: &OsStr,
|
||||
debug: bool,
|
||||
) -> Result<TokenResponse, LoginError> {
|
||||
let body = format!(
|
||||
"grant_type=password&client_id=admin-cli&username={USERNAME}&password={PASSWORD}",
|
||||
"grant_type=password&\
|
||||
client_id={CLIENT_ID}&\
|
||||
username={USERNAME}&\
|
||||
password={PASSWORD}",
|
||||
CLIENT_ID = urlencoding::encode(&self.config.admincli_client),
|
||||
USERNAME = urlencoding::encode_binary(admin_username.as_bytes()),
|
||||
PASSWORD = urlencoding::encode_binary(admin_password.as_bytes()),
|
||||
);
|
||||
|
||||
self.get_token_from_body(&body, debug)
|
||||
self.get_token_from_body(&body)
|
||||
}
|
||||
|
||||
pub fn get_oidc_token(
|
||||
&self,
|
||||
username: &OsStr,
|
||||
password: &OsStr,
|
||||
debug: bool,
|
||||
) -> Result<TokenResponse, LoginError> {
|
||||
let body = format!(
|
||||
"grant_type=password&client_id={CLIENT_ID}&client_secret={CLIENT_SECRET}&username={USERNAME}&password={PASSWORD}",
|
||||
CLIENT_ID = urlencoding::encode(&self.client_id),
|
||||
CLIENT_SECRET = urlencoding::encode(&self.client_secret),
|
||||
"grant_type=password&\
|
||||
client_id={CLIENT_ID}&\
|
||||
client_secret={CLIENT_SECRET}&\
|
||||
username={USERNAME}&\
|
||||
password={PASSWORD}",
|
||||
CLIENT_ID = urlencoding::encode(&self.config.client_id),
|
||||
CLIENT_SECRET = urlencoding::encode(&self.config.client_secret),
|
||||
USERNAME = urlencoding::encode_binary(username.as_bytes()),
|
||||
PASSWORD = urlencoding::encode_binary(password.as_bytes()),
|
||||
);
|
||||
|
||||
self.get_token_from_body(&body, debug)
|
||||
self.get_token_from_body(&body)
|
||||
}
|
||||
|
||||
pub fn get_token_from_body(
|
||||
&self,
|
||||
body: &str,
|
||||
debug: bool,
|
||||
) -> Result<TokenResponse, LoginError> {
|
||||
let (status_code, body) = curl_request(
|
||||
pub fn get_token_from_body(&self, body: &str) -> Result<TokenResponse, LoginError> {
|
||||
let Response { status, data: body } = self.curl.request(
|
||||
Method::POST(Some(body.as_bytes())),
|
||||
&format!(
|
||||
"{}/realms/{}/protocol/openid-connect/token",
|
||||
self.base_url, self.default_realm,
|
||||
self.config.base_url, self.config.default_realm,
|
||||
),
|
||||
&[
|
||||
"Content-Type: application/x-www-form-urlencoded",
|
||||
"Accept: application/json",
|
||||
],
|
||||
debug,
|
||||
&[headers::content_type!(UrlEncoded), headers::accept!(Json)],
|
||||
)?;
|
||||
|
||||
if status_code.is_ok() {
|
||||
if status.is_ok() {
|
||||
Ok(serde_json::from_slice(&body)?)
|
||||
} else {
|
||||
Err(LoginError::InvalidLogin(serde_json::from_slice(&body)?))
|
||||
@@ -250,55 +147,43 @@ impl KeycloakAPI {
|
||||
&self,
|
||||
username: &str,
|
||||
admin_token: &str,
|
||||
debug: bool,
|
||||
) -> Result<(String, Vec<api::Password>), GetCredentialsError> {
|
||||
let (status_code, body) = curl_request(
|
||||
let Response { status, data: body } = self.curl.request(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"{}/admin/realms/{}/users?username={username}",
|
||||
self.base_url, self.default_realm
|
||||
self.config.base_url, self.config.default_realm
|
||||
),
|
||||
&[&format!("Authorization: Bearer {admin_token}")],
|
||||
debug,
|
||||
&[&headers::authorization_bearer!(admin_token)],
|
||||
)?;
|
||||
if !status_code.is_ok() {
|
||||
if debug {
|
||||
eprintln!("got error: {}", String::from_utf8_lossy(&body));
|
||||
}
|
||||
if !status.is_ok() {
|
||||
err_http_body!(if self.debug => &body);
|
||||
return Err(GetCredentialsError::ErrorResponse);
|
||||
}
|
||||
|
||||
let mut found_users: Vec<api::User> = serde_json::from_slice(&body)?;
|
||||
if debug {
|
||||
eprintln!("matched users = {found_users:#?}");
|
||||
}
|
||||
let [first_user] = &mut found_users[..] else {
|
||||
let found_users: Vec<api::User> = serde_json::from_slice(&body)?;
|
||||
debug_info!(if self.debug => "matched users" = found_users);
|
||||
let Ok([first_user]) = TryInto::<[api::User; _]>::try_into(found_users) else {
|
||||
return Err(GetCredentialsError::NonUnitResults);
|
||||
};
|
||||
|
||||
let (status_code, body) = curl_request(
|
||||
let Response { status, data: body } = self.curl.request(
|
||||
Method::GET,
|
||||
&format!(
|
||||
"{}/admin/realms/{}/users/{}/credentials",
|
||||
self.base_url, self.default_realm, first_user.id
|
||||
self.config.base_url, self.config.default_realm, first_user.id
|
||||
),
|
||||
&[
|
||||
&format!("Authorization: Bearer {admin_token}"),
|
||||
"Accept: application/json",
|
||||
&headers::authorization_bearer!(admin_token),
|
||||
headers::accept!(Json),
|
||||
],
|
||||
debug,
|
||||
)?;
|
||||
if !status_code.is_ok() {
|
||||
if debug {
|
||||
eprintln!("got error: {}", String::from_utf8_lossy(&body));
|
||||
}
|
||||
if !status.is_ok() {
|
||||
err_http_body!(if self.debug => &body);
|
||||
return Err(GetCredentialsError::ErrorResponse);
|
||||
}
|
||||
|
||||
Ok((
|
||||
std::mem::take(&mut first_user.id),
|
||||
serde_json::from_slice(&body)?,
|
||||
))
|
||||
Ok((first_user.id, serde_json::from_slice(&body)?))
|
||||
}
|
||||
|
||||
pub fn set_user_credentials(
|
||||
@@ -306,7 +191,6 @@ impl KeycloakAPI {
|
||||
user_id: &str,
|
||||
new_password: &str,
|
||||
admin_token: &str,
|
||||
debug: bool,
|
||||
) -> Result<(), SetCredentialsError> {
|
||||
let request_body = serde_json::to_vec(&api::ResetPasswordRequest {
|
||||
temporary: false,
|
||||
@@ -315,23 +199,19 @@ impl KeycloakAPI {
|
||||
})
|
||||
.map_err(SetCredentialsError::SerializeError)?;
|
||||
|
||||
let (status_code, body) = curl_request(
|
||||
let Response { status, data: body } = self.curl.request(
|
||||
Method::PUT(Some(request_body.into_boxed_slice())),
|
||||
&format!(
|
||||
// "http://127.0.0.1:3000/admin/realms/master/users/79149d47-17e6-42be-9fd8-47b04127f162/reset-password",
|
||||
"{}/admin/realms/{}/users/{user_id}/reset-password",
|
||||
self.base_url, self.default_realm
|
||||
self.config.base_url, self.config.default_realm
|
||||
),
|
||||
&[
|
||||
&format!("Authorization: Bearer {admin_token}"),
|
||||
"Content-Type: application/json",
|
||||
&headers::authorization_bearer!(admin_token),
|
||||
headers::content_type!(Json),
|
||||
],
|
||||
debug,
|
||||
)?;
|
||||
if !status_code.is_ok() {
|
||||
if debug {
|
||||
eprintln!("got error: {}", String::from_utf8_lossy(&body));
|
||||
}
|
||||
if !status.is_ok() {
|
||||
err_http_body!(if self.debug => &body);
|
||||
return Err(SetCredentialsError::ErrorResponse);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
//! Tiny abstractions for `libcurl`
|
||||
//!
|
||||
//! Lacks traits that could be implemented to keep the minimal intent.
|
||||
|
||||
use std::{
|
||||
fmt,
|
||||
io::{Cursor, Read as _},
|
||||
};
|
||||
|
||||
use curl::easy::{Easy, List};
|
||||
|
||||
use crate::macros::debug_print;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Status(u32);
|
||||
|
||||
impl Status {
|
||||
#[must_use]
|
||||
pub const fn is_ok(self) -> bool {
|
||||
matches!(self.0, 200..300)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Method<'a> {
|
||||
GET,
|
||||
POST(Option<&'a [u8]>),
|
||||
PUT(Option<Box<[u8]>>),
|
||||
}
|
||||
|
||||
impl fmt::Debug for Method<'_> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::GET => write!(f, "GET"),
|
||||
Self::POST(None) => write!(f, "POST"),
|
||||
Self::POST(Some(b)) => write!(f, "POST<{} B>", b.len()),
|
||||
Self::PUT(None) => write!(f, "PUT"),
|
||||
Self::PUT(Some(b)) => write!(f, "PUT<{} B>", b.len()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
pub debug: bool,
|
||||
}
|
||||
|
||||
pub struct Response {
|
||||
pub status: Status,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
pub fn add_headers(easy: &mut Easy, headers: &[&str]) -> Result<(), curl::Error> {
|
||||
let mut curl_headers = List::new();
|
||||
|
||||
for header in headers {
|
||||
curl_headers.append(header)?;
|
||||
}
|
||||
easy.http_headers(curl_headers)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_method_and_body(easy: &mut Easy, method: Method) -> Result<(), curl::Error> {
|
||||
match method {
|
||||
Method::GET => {
|
||||
easy.get(true)?;
|
||||
}
|
||||
Method::POST(body) => {
|
||||
easy.post(true)?;
|
||||
if let Some(body) = body {
|
||||
easy.post_fields_copy(body)?;
|
||||
}
|
||||
}
|
||||
Method::PUT(body) => {
|
||||
easy.put(true)?;
|
||||
if let Some(body) = body {
|
||||
easy.upload(true)?;
|
||||
easy.in_filesize(body.len() as u64)?;
|
||||
let mut cur = Cursor::new(body);
|
||||
easy.read_function(move |buf| Ok(cur.read(buf).unwrap_or(0)))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn transfer_data_into<'easy, 'data>(
|
||||
easy: &'easy mut Easy,
|
||||
into: &'data mut Vec<u8>,
|
||||
) -> Result<curl::easy::Transfer<'easy, 'data>, curl::Error> {
|
||||
let mut transfer = easy.transfer();
|
||||
transfer.write_function(|recv_data| {
|
||||
into.extend_from_slice(recv_data);
|
||||
Ok(recv_data.len())
|
||||
})?;
|
||||
|
||||
Ok(transfer)
|
||||
}
|
||||
|
||||
pub fn recv_data(easy: &mut Easy) -> Result<Vec<u8>, curl::Error> {
|
||||
let mut data = Vec::new();
|
||||
{
|
||||
let transfer = Self::transfer_data_into(easy, &mut data)?;
|
||||
transfer.perform()?;
|
||||
}
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub fn request(
|
||||
&self,
|
||||
method: Method,
|
||||
uri: &str,
|
||||
headers: &[&str],
|
||||
) -> Result<Response, curl::Error> {
|
||||
debug_print!(if self.debug =>
|
||||
"{method:?} {uri} ({} headers)",
|
||||
headers.len(), method = method, uri = uri
|
||||
);
|
||||
|
||||
let mut easy = Easy::new();
|
||||
let client = &mut easy;
|
||||
|
||||
Self::add_headers(client, headers)?;
|
||||
client.url(uri)?;
|
||||
Self::set_method_and_body(client, method)?;
|
||||
|
||||
let data = Self::recv_data(client)?;
|
||||
|
||||
Ok(Response {
|
||||
status: Status(easy.response_code()?),
|
||||
data,
|
||||
})
|
||||
}
|
||||
}
|
||||
+119
-105
@@ -1,164 +1,126 @@
|
||||
//! Privileged PAM module.
|
||||
|
||||
use std::ffi::{CStr, OsStr};
|
||||
use std::ffi::{CStr, OsStr, OsString};
|
||||
use std::os::unix::ffi::OsStrExt as _;
|
||||
|
||||
use nonstick::{ErrorCode, ModuleClient, PamModule, pam_export};
|
||||
use nonstick::{ErrorCode as E, ModuleClient, PamModule, pam_export};
|
||||
|
||||
use keycloak::KeycloakAPI;
|
||||
|
||||
use crate::args::Args;
|
||||
use crate::config::Configuration;
|
||||
use crate::ext::CStrExt as _;
|
||||
use crate::macros::{debug_print, error_print};
|
||||
|
||||
pub mod args;
|
||||
pub mod config;
|
||||
pub mod ext;
|
||||
pub mod keycloak;
|
||||
pub mod libc;
|
||||
pub mod macros;
|
||||
|
||||
pub struct KeycloakPAM;
|
||||
pam_export!(KeycloakPAM);
|
||||
|
||||
/// Conditionally prints the error and returns the value intouched.
|
||||
///
|
||||
/// All prints should use this macro to also format output.
|
||||
macro_rules! error_print {
|
||||
($expr:expr, $if_print:ident) => {{
|
||||
let expr = $expr;
|
||||
if let Err(ref error) = expr
|
||||
&& $if_print
|
||||
{
|
||||
eprintln!("keycloak-pam ({}): {error}", line!());
|
||||
};
|
||||
expr
|
||||
}};
|
||||
}
|
||||
|
||||
// TODO: turns out nonstick has great resources explaining sensible return errors, double check
|
||||
/// Errors:
|
||||
///
|
||||
/// - [`ErrorCode::ConversationError`]: Something wrong converting types (e.g. UTF-8)
|
||||
/// - [`ErrorCode::ServiceError`]: Something misconfigured in the service (e.g. bad configuration)
|
||||
/// - [`ErrorCode::PermissionDenied`]: User not meant to be handled (e.g. user not member of
|
||||
/// configured group)
|
||||
/// - [`ErrorCode::AuthInfoUnavailable`]: Error fetch auth info (e.g. keycloak backend down)
|
||||
/// - [`ErrorCode::AuthenticationError`]: Failed to authenticate (e.g. wrong credentials)
|
||||
impl<M: ModuleClient> PamModule<M> for KeycloakPAM {
|
||||
// Double check sensible error codes with
|
||||
// <https://docs.rs/nonstick/latest/nonstick/trait.PamModule.html#method.authenticate>.
|
||||
//
|
||||
/// # Errors
|
||||
///
|
||||
/// Just a combination of [`Self::init`] and [`Self::get_token_for`].
|
||||
fn authenticate(
|
||||
handle: &mut M,
|
||||
args: Vec<&CStr>,
|
||||
flags: nonstick::AuthnFlags,
|
||||
) -> nonstick::Result<()> {
|
||||
use ErrorCode as E;
|
||||
|
||||
let args = Args::parse(&args);
|
||||
if args.debug {
|
||||
eprintln!("starting authentication...");
|
||||
}
|
||||
|
||||
let no_silent_flag = (flags & nonstick::AuthnFlags::SILENT).is_empty();
|
||||
let can_print = no_silent_flag | args.debug;
|
||||
let (args, can_print, config, username) = Self::init(handle, &args, !no_silent_flag)?;
|
||||
|
||||
debug_print!(if args.debug => "authenticating {}", username.to_string_lossy());
|
||||
|
||||
{
|
||||
let api_client = KeycloakAPI::load_from_configuration(args.config.to_os_str());
|
||||
let api_client = error_print!(api_client, can_print).map_err(|_| E::ServiceError)?;
|
||||
|
||||
let username = handle.username(None)?;
|
||||
if args.debug {
|
||||
eprintln!("authenticating {}", username.to_string_lossy());
|
||||
}
|
||||
|
||||
let should_skip = error_print!(api_client.should_skip(&username), can_print)
|
||||
.map_err(|_| E::ServiceError)?;
|
||||
if should_skip {
|
||||
return Err(E::PermissionDenied);
|
||||
}
|
||||
|
||||
let password = handle.authtok(None)?;
|
||||
|
||||
Self::get_token_for(
|
||||
&api_client,
|
||||
username.as_os_str(),
|
||||
&password,
|
||||
can_print,
|
||||
args.debug,
|
||||
)
|
||||
.map(|_| ())
|
||||
let api_client = KeycloakAPI::new(&config, args.debug);
|
||||
|
||||
Self::get_token_for(&api_client, username.as_os_str(), &password, can_print).map(|_| ())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check if another user can call passwd on another user without passwd and skip auth
|
||||
// Double check sensible error codes with
|
||||
// <https://docs.rs/nonstick/latest/nonstick/trait.PamModule.html#method.change_authtok>
|
||||
//
|
||||
/// # Warning
|
||||
///
|
||||
/// Preliminary checks ([`nonstick::AuthtokAction::Validate`]) are not supported, it will
|
||||
/// succeed always (if this user should be handled and gets that far).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// As a baseline, the errors of [`Self::init`], [`Self::get_token_for`] and
|
||||
/// [`Self::get_creds_for`].
|
||||
///
|
||||
/// Extra:
|
||||
/// - [`E::BufferError`]: If some data could not be converted to UTF-8 or anything went wrong
|
||||
/// serializing data.
|
||||
/// - [`E::AuthTokDisableAging`]: If a [`nonstick::AuthtokFlags::CHANGE_EXPIRED_AUTHTOK`] is
|
||||
/// attempted.
|
||||
/// - [`E::AuthInfoUnavailable`]: If correct keycloak data could not be accessed.
|
||||
/// - [`E::ServiceError`]: If there's a potential misconfiguration or unexpected error response.
|
||||
/// - [`E::CredentialsUnavailable`]: Any networking failure when setting the passowrd, after
|
||||
/// there have already been communications with Keycloak.
|
||||
fn change_authtok(
|
||||
handle: &mut M,
|
||||
args: Vec<&std::ffi::CStr>,
|
||||
action: nonstick::AuthtokAction,
|
||||
flags: nonstick::AuthtokFlags,
|
||||
) -> nonstick::Result<()> {
|
||||
use ErrorCode as E;
|
||||
let no_silent_flag = (flags & nonstick::AuthtokFlags::SILENT).is_empty();
|
||||
let (args, can_print, config, username) = Self::init(handle, &args, !no_silent_flag)?;
|
||||
let username_str = str::from_utf8(username.as_bytes()).map_err(|_| E::BufferError)?;
|
||||
|
||||
let args = Args::parse(&args);
|
||||
if args.debug {
|
||||
eprintln!("starting authtok change...");
|
||||
if !(flags & nonstick::AuthtokFlags::CHANGE_EXPIRED_AUTHTOK).is_empty() {
|
||||
return Err(E::AuthTokDisableAging);
|
||||
}
|
||||
|
||||
let no_silent_flag = (flags & nonstick::AuthtokFlags::SILENT).is_empty();
|
||||
let can_print = no_silent_flag | args.debug;
|
||||
debug_print!(if args.debug => "changing authtok for {}", username_str);
|
||||
|
||||
{
|
||||
let api_client = KeycloakAPI::load_from_configuration(args.config.to_os_str());
|
||||
let api_client = error_print!(api_client, can_print).map_err(|_| E::ServiceError)?;
|
||||
|
||||
let username = handle.username(None)?;
|
||||
let username_str = str::from_utf8(username.as_bytes()).map_err(|_| E::BufferError)?;
|
||||
|
||||
let should_skip = error_print!(api_client.should_skip(&username), can_print)
|
||||
.map_err(|_| E::ServiceError)?;
|
||||
if should_skip {
|
||||
return Err(E::Ignore);
|
||||
}
|
||||
|
||||
if action != nonstick::AuthtokAction::Update {
|
||||
if args.debug {
|
||||
eprintln!("not update, skipping...");
|
||||
}
|
||||
debug_print!(if args.debug => "not update, skippnig...");
|
||||
// *should* be valid
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let admin_token = error_print!(api_client.get_admin_token(args.debug), can_print)
|
||||
.map_err(|error| match error {
|
||||
let api_client = KeycloakAPI::new(&config, args.debug);
|
||||
|
||||
let admin_token = error_print!(api_client.get_admin_token(), can_print).map_err(
|
||||
|error| match error {
|
||||
keycloak::LoginError::Fetch(_)
|
||||
| keycloak::LoginError::InvalidAPIResponse(_) => E::AuthInfoUnavailable,
|
||||
keycloak::LoginError::InvalidLogin(_) => E::ServiceError,
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
|
||||
if args.debug {
|
||||
eprintln!("verifying credentials for {username_str}");
|
||||
}
|
||||
debug_print!(if args.debug => "verifying credentials");
|
||||
let (user_id, user_creds) = Self::get_creds_for(
|
||||
&api_client,
|
||||
username_str,
|
||||
&admin_token.access_token,
|
||||
can_print,
|
||||
args.debug,
|
||||
)?;
|
||||
|
||||
if !user_creds.is_empty() {
|
||||
let old_authtok = handle.old_authtok(None)?;
|
||||
Self::get_token_for(&api_client, &username, &old_authtok, can_print, args.debug)?;
|
||||
if args.debug {
|
||||
eprintln!("identity verified");
|
||||
}
|
||||
Self::get_token_for(&api_client, &username, &old_authtok, can_print)?;
|
||||
debug_print!(if args.debug => "identity verified");
|
||||
}
|
||||
|
||||
let authtok = handle.authtok(None)?;
|
||||
let authtok_str = str::from_utf8(authtok.as_bytes()).map_err(|_| E::BufferError)?;
|
||||
error_print!(
|
||||
api_client.set_user_credentials(
|
||||
&user_id,
|
||||
authtok_str,
|
||||
&admin_token.access_token,
|
||||
args.debug
|
||||
),
|
||||
api_client.set_user_credentials(&user_id, authtok_str, &admin_token.access_token,),
|
||||
can_print
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
@@ -179,17 +141,66 @@ impl<M: ModuleClient> PamModule<M> for KeycloakPAM {
|
||||
}
|
||||
|
||||
impl KeycloakPAM {
|
||||
/// Can also be used to check the credentials
|
||||
fn get_token_for(
|
||||
/// Does basic initialization tasks, as well as handling `asdeny`.
|
||||
///
|
||||
/// The errors are:
|
||||
/// - [`E::ServiceError`]: Service misconfigurations (e.g. bad config parsing)
|
||||
/// - [`E::UserUnknown`]: If user is not meant to be handled (e.g. system users in `sufficient` mode)
|
||||
/// - [`E::Ignore`]: If the user is not meant to be handled in `requisite` mode
|
||||
/// ([`E::UserUnknown`] breaks requisite chain, [`E::Ignore`] doesn't).
|
||||
/// - [`E::AuthenticationError`]: If user should've been handled and on deny mode.
|
||||
pub fn init<'a, M: ModuleClient>(
|
||||
handle: &mut M,
|
||||
args: &'a [&'a std::ffi::CStr],
|
||||
silent_flag: bool,
|
||||
) -> nonstick::Result<(Args<'a>, bool, Configuration, OsString)> {
|
||||
let args = Args::parse(args);
|
||||
|
||||
let can_print = !silent_flag | args.debug;
|
||||
|
||||
let configuration = Configuration::load_from_configuration(args.config.to_os_str());
|
||||
let configuration = error_print!(configuration, can_print).map_err(|_| E::ServiceError)?;
|
||||
|
||||
let username = handle.username(None)?;
|
||||
|
||||
let should_skip = error_print!(configuration.should_skip(&username), can_print)
|
||||
.map_err(|_| E::ServiceError)?;
|
||||
|
||||
debug_print!(
|
||||
if args.debug => "[init] asdeny? {} should_skip? {}",
|
||||
args.asdeny, should_skip
|
||||
);
|
||||
#[allow(clippy::redundant_else, reason = "More readable IMO")]
|
||||
if args.asdeny {
|
||||
// Deny policy
|
||||
return Err(if should_skip {
|
||||
E::Ignore
|
||||
} else {
|
||||
E::AuthenticationError
|
||||
});
|
||||
} else {
|
||||
// Normal policy
|
||||
if should_skip {
|
||||
return Err(E::UserUnknown);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((args, can_print, configuration, username))
|
||||
}
|
||||
|
||||
/// Can also be used to check the validity of credentials.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`E::AuthInfoUnavailable`]: Anything wrong with keycloak, we assume it's just down now.
|
||||
/// - [`E::AuthenticationError`]: If everything went right but checking credentials.
|
||||
pub fn get_token_for(
|
||||
api_client: &keycloak::KeycloakAPI,
|
||||
username: &OsStr,
|
||||
password: &OsStr,
|
||||
can_print: bool,
|
||||
debug: bool,
|
||||
) -> nonstick::Result<keycloak::api::TokenResponse> {
|
||||
use ErrorCode as E;
|
||||
|
||||
let checked_credentials = api_client.get_oidc_token(username, password, debug);
|
||||
let checked_credentials = api_client.get_oidc_token(username, password);
|
||||
error_print!(checked_credentials, can_print).map_err(|error| match error {
|
||||
keycloak::LoginError::Fetch(_) | keycloak::LoginError::InvalidAPIResponse(_) => {
|
||||
E::AuthInfoUnavailable
|
||||
@@ -198,17 +209,20 @@ impl KeycloakPAM {
|
||||
})
|
||||
}
|
||||
|
||||
/// Can be used to see if user has configured passwords.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`E::AuthInfoUnavailable`]: Anything wrong with keycloak, we assume it's just down now.
|
||||
/// - [`E::AuthenticationError`]: If everything went right but checking credentials.
|
||||
fn get_creds_for(
|
||||
api_client: &keycloak::KeycloakAPI,
|
||||
username: &str,
|
||||
admin_token: &str,
|
||||
can_print: bool,
|
||||
debug: bool,
|
||||
) -> nonstick::Result<(String, Vec<keycloak::api::Password>)> {
|
||||
use ErrorCode as E;
|
||||
|
||||
error_print!(
|
||||
api_client.get_user_credentials(username, admin_token, debug),
|
||||
api_client.get_user_credentials(username, admin_token),
|
||||
can_print
|
||||
)
|
||||
.map_err(|error| match error {
|
||||
|
||||
+2
-4
@@ -13,14 +13,12 @@ pub enum GetgrnamError {
|
||||
Unexpected,
|
||||
}
|
||||
|
||||
// TODO: Reorganise this function, error handing is shallow
|
||||
|
||||
/// # Safety
|
||||
///
|
||||
/// The function is completely safe to use but the use of `cb` requires unsafe:
|
||||
/// The function is completely safe to call but the use of `cb` requires unsafe:
|
||||
///
|
||||
/// - All raw pointers in there are only valid for the duration of the function as the buffer they
|
||||
/// point to is local to this function.
|
||||
/// point to is local to this ([`getgrnam_use`]) function.
|
||||
pub fn getgrnam_use<T>(
|
||||
cname: &CStr,
|
||||
mut cb: impl FnMut(libc::group) -> T,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#[cfg(feature = "debuggable")]
|
||||
macro_rules! keycloak_print {
|
||||
($fmt:expr $(, $($args:tt)*)?) => {
|
||||
::std::eprintln!(
|
||||
::std::concat!("keycloak-pam ({}:{}): ", $fmt),
|
||||
::std::file!(), ::std::line!() $(, $($args)*)?
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "debuggable"))]
|
||||
macro_rules! keycloak_print {
|
||||
($fmt:expr $(, $($args:tt)*)?) => {
|
||||
#[allow(clippy::double_parens, reason = "The lazy way of this might have just one content")]
|
||||
let _ = (&$fmt $(, &$($args)*)?);
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) use keycloak_print;
|
||||
|
||||
/// Conditionally prints the error and returns the value untouched.
|
||||
///
|
||||
/// All prints should use this macro to also format output.
|
||||
macro_rules! error_print {
|
||||
($expr:expr, $if_print:ident) => {{
|
||||
let expr = $expr;
|
||||
if let Err(ref error) = expr
|
||||
&& $if_print
|
||||
{
|
||||
$crate::macros::keycloak_print!("{}", error);
|
||||
};
|
||||
expr
|
||||
}};
|
||||
}
|
||||
pub(crate) use error_print;
|
||||
|
||||
macro_rules! debug_print {
|
||||
(if $debug:expr => $($args:tt)*) => {
|
||||
if $debug {
|
||||
$crate::macros::keycloak_print!($($args)*);
|
||||
}
|
||||
};
|
||||
}
|
||||
pub(crate) use debug_print;
|
||||
@@ -16,11 +16,10 @@ let
|
||||
|
||||
inherit (self') packages;
|
||||
|
||||
debug = true;
|
||||
fqdn = "keycloak-pam.test";
|
||||
|
||||
passwordFile = builtins.toFile "super-secure-password" "nuhuh";
|
||||
|
||||
theme-name = "default";
|
||||
in
|
||||
{
|
||||
virtualisation.vmVariant.virtualisation = {
|
||||
@@ -149,42 +148,41 @@ in
|
||||
unix_group = "keycloak-login";
|
||||
admin_username = "admin";
|
||||
admin_password = "dontchangeme";
|
||||
admincli_client = "admin-cli";
|
||||
}
|
||||
);
|
||||
|
||||
type = {
|
||||
auth = {
|
||||
type = "auth";
|
||||
control = "sufficient";
|
||||
};
|
||||
password = {
|
||||
type = "password";
|
||||
control = "sufficient";
|
||||
};
|
||||
};
|
||||
|
||||
settings = {
|
||||
debug = true;
|
||||
inherit debug;
|
||||
config = keycloakPamConfig;
|
||||
};
|
||||
|
||||
mkRuleBeforeUnix =
|
||||
{ type, control }:
|
||||
service: {
|
||||
"${type}".keycloak-pam = {
|
||||
mkRuleBeforeUnix = type: service: {
|
||||
"${type}" = {
|
||||
keycloak-pam = {
|
||||
order = config.security.pam.services."${service}".rules."${type}".unix.order - 10;
|
||||
inherit modulePath settings control;
|
||||
control = "sufficient";
|
||||
inherit modulePath settings;
|
||||
};
|
||||
keycloak-deny = {
|
||||
order = config.security.pam.services."${service}".rules."${type}".unix.order - 9;
|
||||
control = "requisite";
|
||||
settings = settings // {
|
||||
asdeny = true;
|
||||
};
|
||||
inherit modulePath;
|
||||
};
|
||||
};
|
||||
};
|
||||
mkRulesBeforeUnix =
|
||||
types: service: builtins.foldl' (rules: type: rules // mkRuleBeforeUnix type service) { } types;
|
||||
|
||||
mkServicesWith =
|
||||
withfn: builtins.foldl' (attrs: service: attrs // { "${service}".rules = withfn service; }) { };
|
||||
mkAuths = mkServicesWith (mkRuleBeforeUnix type.auth);
|
||||
mkAuths = mkServicesWith (mkRuleBeforeUnix "auth");
|
||||
mkPasswds = mkServicesWith (mkRulesBeforeUnix [
|
||||
type.auth
|
||||
type.password
|
||||
"auth"
|
||||
"password"
|
||||
]);
|
||||
in
|
||||
mkAuths [
|
||||
|
||||
Reference in New Issue
Block a user