1115 priv (reauth, sudo) mode ()

This commit is contained in:
Firstyear 2023-03-27 11:38:09 +10:00 committed by GitHub
parent 2c2c3a6ddb
commit 4718f2dc6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2241 additions and 1046 deletions

2
Cargo.lock generated
View file

@ -2489,6 +2489,7 @@ dependencies = [
"serde_json",
"sketching",
"testkit-macros",
"time 0.2.27",
"tokio",
"tracing",
"url",
@ -2508,6 +2509,7 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"time 0.2.27",
"url",
"uuid",
"wasm-bindgen",

View file

@ -14,6 +14,7 @@
- [Administration](administrivia.md)
- [Accounts and Groups](accounts_and_groups.md)
- [Authentication and Credentials](authentication.md)
- [POSIX Accounts and Groups](posix_accounts.md)
- [Backup and Restore](backup_restore.md)
- [Database Maintenance](database_maint.md)

View file

@ -119,38 +119,6 @@ text=Persons may change their own displayname, name, and legal name at any time.
<!-- deno-fmt-ignore-end -->
## Resetting Person Account Credentials
Members of the `idm_account_manage_priv` group have the rights to manage person and service accounts
security and login aspects. This includes resetting account credentials.
You can perform a password reset on the demo\_user, for example as the idm\_admin user, who is a
default member of this group. The lines below prefixed with `#` are the interactive credential
update interface.
```bash
kanidm person credential update demo_user --name idm_admin
# spn: demo_user@idm.example.com
# Name: Demonstration User
# Primary Credential:
# uuid: 0e19cd08-f943-489e-8ff2-69f9eacb1f31
# generated password: set
# Can Commit: true
#
# cred update (? for help) # : pass
# New password:
# New password: [hidden]
# Confirm password:
# Confirm password: [hidden]
# success
#
# cred update (? for help) # : commit
# Do you want to commit your changes? yes
# success
kanidm login --name demo_user
kanidm self whoami --name demo_user
```
## Creating Service Accounts
The `admin` service account can be used to create service accounts.

137
book/src/authentication.md Normal file
View file

@ -0,0 +1,137 @@
# Authentication and Credentials
A primary job of a system like Kanidm is to manage credentials for persons. This can involve a range
of operations from new user onboarding, credential resets, and self service.
## Types of Credentials
### Passkeys
This is the preferred method of authentication in Kanidm. Passkeys represent "all possible
cryptographic" authenticators that support Webauthn. Examples of this include Yubikeys, TouchID,
Windows Hello, TPM's and more.
These devices are unphishable, self contained multifactor authenticators and are considered the most
secure method of authentication in Kanidm.
<!-- deno-fmt-ignore-start -->
{{#template templates/kani-warning.md
imagepath=images
title=Warning!
text=Kanidm's definition of Passkeys differs to other systems. This is because we adopted the term very early before it has changed and evolved.
}}
<!-- deno-fmt-ignore-end -->
### Password + TOTP
This is a classic Time-based One Time Password combined with a password. Different to other systems
Kanidm will prompt for the TOTP _first_ before the password. This is to prevent drive by bruteforce
against the password of the account and testing if the password is vulnerable.
While this authentication method is mostly secure, we do not advise it for high security
environments due to the fact it is still possible to perform realtime phishing attacks.
## Resetting Person Account Credentials
Members of the `idm_account_manage_priv` group have the rights to manage person and service accounts
security and login aspects. This includes resetting account credentials.
### Onboarding a New Person / Resetting Credentials
These processes are very similar. You can send a credential reset link to a user so that they can
directly enroll their own credentials. To generate this link or qrcode:
```bash
kanidm person credential create-reset-token demo_user --name idm_admin
# The person can use one of the following to allow the credential reset
#
# Scan this QR Code:
#
# █████████████████████████████████████████████
# █████████████████████████████████████████████
# ████ ▄▄▄▄▄ █▄██ ▀▀▀▄▀▀█ ▄▀▀▀▀▄▀▀▄█ ▄▄▄▄▄ ████
# ████ █ █ █▀ ▄▄▄▀█ █▀ ██ ▀ ▀▄█ █ █ ████
# ████ █▄▄▄█ █ █▄█ ▀ ▄███▄ ▀▄▀▄ █ █▄▄▄█ ████
# ████▄▄▄▄▄▄▄█ █▄▀▄█▄█ █▄▀▄▀▄█▄█ █▄█▄▄▄▄▄▄▄████
# ████ ▀█▀ ▀▄▄▄ ▄▄▄▄▄▄▄█▀ ▄█▀█▀ ▄▀ ▄ █▀▄████
# ████▄ █ ▀ ▄█▀█ ▀█ ▀█▄ ▀█▀ ▄█▄ █▀▄▀██▄▀█████
# ████ ▀▀▀█▀▄██▄▀█ ▄▀█▄▄█▀▄▀▀▀▀▀▄▀▀▄▄▄▀ ▄▄ ████
# ████ █▄▀ ▄▄ ▄▀▀ ▀ █▄█ ▀▀ █▀▄▄█▄ ▀ ▄ ▀▀████
# ████ █▀▄ █▄▄ █ █▀▀█▀█▄ ▀█▄█▄█▀▄▄ ▀▀ ▄▄ ▄████
# █████ ▀█▄▀▄▄▀▀ ██▀▀█▄█▄█▄█ █▀▄█ ▄█ ▄▄▀▀█████
# ████▄▄▀ ▄▄ ▀▀▄▀▀ ▄▄█ ▄ █▄ ▄▄ ▀▀▀▄▄ ▀▄▄██████
# ████▄▄▀ ▀▀▄▀▄ ▀▀▀▀█▀█▄▀▀ ▄▄▄ ▄ ▄█▀ ▄ ▄ ████
# ████▀▄ ▀▄▄█▀█▀▄ ▄██▄█▀ ▄█▀█ ▀▄ ███▄█ ▄█▄████
# ██████ ▀▄█▄██▀ ▀█▄▀ ▀▀▄ ▀▀█ ██▀█▄▄▀██ ▀▀████
# ████▄▄██▄▄▄▄ ▀▄██▀█ ███▀ ██▄▀▀█ ▄▄▄ ███ ████
# ████ ▄▄▄▄▄ █▄ ▄▄ ▀█▀ ▀▀ █▀▄▄▄▄█ █▄█ ▀▀ ▀████
# ████ █ █ █▄█▄▀ ██▀█▄ ▀█▄▀▄ ▀▀▄ ▄▄▄▀ ████
# ████ █▄▄▄█ ██▀█ ▀▄▀█▄█▄█▄▀▀▄▄ ▀ ▄▄▄█▀█ █████
# ████▄▄▄▄▄▄▄█▄█▄▄▄▄▄▄█▄█▄██▄█▄▄▄█▄██▄███▄▄████
# █████████████████████████████████████████████
# ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
#
# This link: https://localhost:8443/ui/reset?token=8qDRG-AE1qC-zjjAT-0Fkd6
# Or run this command: kanidm person credential use_reset_token 8qDRG-AE1qC-zjjAT-0Fkd6
```
If the user wishes you can direct them to `https://idm.mydomain.name/ui/reset` where they can
manually enter their token value.
Each token can be used only once within a 24 hour period. Once the credentials have been set the
token is immediately invalidated.
### Resetting Credentials Directly
You can perform a password reset on the demo\_user, for example as the idm\_admin user, who is a
default member of this group. The lines below prefixed with `#` are the interactive credential
update interface. This allows the user to directly manage the credentials of another account.
```bash
kanidm person credential update demo_user --name idm_admin
# spn: demo_user@idm.example.com
# Name: Demonstration User
# Primary Credential:
# uuid: 0e19cd08-f943-489e-8ff2-69f9eacb1f31
# generated password: set
# Can Commit: true
#
# cred update (? for help) # : pass
# New password:
# New password: [hidden]
# Confirm password:
# Confirm password: [hidden]
# success
#
# cred update (? for help) # : commit
# Do you want to commit your changes? yes
# success
kanidm login --name demo_user
kanidm self whoami --name demo_user
```
## Reauthentication / Privilege Access Mode
To allow for longer lived sessions in Kanidm, by default sessions are issued in a "privilege
capable" but read-only mode. In order to access privileges for a short time, you must
re-authenticate. This re-issues your session with a small time limited read-write session
internally. You can consider this to be like `sudo` on a unix system or `UAC` on windows where you
reauthenticate for short periods to access higher levels of privilege.
When using a user command that requires these privileges you will be warned:
```
kanidm person credential update william
# Privileges have expired for william@idm.example.com - you need to re-authenticate again.
```
To reauthenticate
```
kanidm reauth -D william
```
> **NOTE** During reauthentication can only use the same credential that was used to initially
> authenticate to the session. The reauth flow will not allow any other credentials to be used!

View file

@ -540,13 +540,15 @@ impl KanidmClient {
.body(req_string)
.header(CONTENT_TYPE, APPLICATION_JSON);
/*
let response = if let Some(token) = &self.bearer_token {
response.bearer_auth(token)
} else {
response
// If we have a bearer token, set it now.
let response = {
let tguard = self.bearer_token.read().await;
if let Some(token) = &(*tguard) {
response.bearer_auth(token)
} else {
response
}
};
*/
// If we have a session header, set it now.
let response = {
@ -900,7 +902,10 @@ impl KanidmClient {
pub async fn auth_step_init(&self, ident: &str) -> Result<Set<AuthMech>, ClientError> {
let auth_init = AuthRequest {
step: AuthStep::Init(ident.to_string()),
step: AuthStep::Init2 {
username: ident.to_string(),
issue: AuthIssueSession::Token,
},
};
let r: Result<AuthResponse, _> =
@ -1222,6 +1227,100 @@ impl KanidmClient {
}
}
pub async fn reauth_begin(&self) -> Result<Vec<AuthAllowed>, ClientError> {
let issue = AuthIssueSession::Token;
let r: Result<AuthResponse, _> = self.perform_auth_post_request("/v1/reauth", issue).await;
r.map(|v| {
debug!("Authentication Session ID -> {:?}", v.sessionid);
v.state
})
.and_then(|state| match state {
AuthState::Continue(allowed) => Ok(allowed),
_ => Err(ClientError::AuthenticationFailed),
})
}
pub async fn reauth_simple_password(&self, password: &str) -> Result<(), ClientError> {
let state = match self.reauth_begin().await {
Ok(mut s) => s.pop(),
Err(e) => return Err(e),
};
match state {
Some(AuthAllowed::Password) => {}
_ => {
return Err(ClientError::AuthenticationFailed);
}
};
let r = self.auth_step_password(password).await?;
match r.state {
AuthState::Success(_) => Ok(()),
_ => Err(ClientError::AuthenticationFailed),
}
}
pub async fn reauth_password_totp(&self, password: &str, totp: u32) -> Result<(), ClientError> {
let state = match self.reauth_begin().await {
Ok(s) => s,
Err(e) => return Err(e),
};
if !state.contains(&AuthAllowed::Totp) {
debug!("TOTP step not offered.");
return Err(ClientError::AuthenticationFailed);
}
let r = self.auth_step_totp(totp).await?;
// Should need to continue.
match r.state {
AuthState::Continue(allowed) => {
if !allowed.contains(&AuthAllowed::Password) {
debug!("Password step not offered.");
return Err(ClientError::AuthenticationFailed);
}
}
_ => {
debug!("Invalid AuthState presented.");
return Err(ClientError::AuthenticationFailed);
}
};
let r = self.auth_step_password(password).await?;
match r.state {
AuthState::Success(_token) => Ok(()),
_ => Err(ClientError::AuthenticationFailed),
}
}
pub async fn reauth_passkey_begin(&self) -> Result<RequestChallengeResponse, ClientError> {
let state = match self.reauth_begin().await {
Ok(mut s) => s.pop(),
Err(e) => return Err(e),
};
// State is now a set of auth continues.
match state {
Some(AuthAllowed::Passkey(r)) => Ok(r),
_ => Err(ClientError::AuthenticationFailed),
}
}
pub async fn reauth_passkey_complete(
&self,
pkc: Box<PublicKeyCredential>,
) -> Result<(), ClientError> {
let r = self.auth_step_passkey_complete(pkc).await?;
match r.state {
AuthState::Success(_token) => Ok(()),
_ => Err(ClientError::AuthenticationFailed),
}
}
pub async fn auth_valid(&self) -> Result<(), ClientError> {
self.perform_get_request("/v1/auth/valid").await
}

View file

@ -298,30 +298,6 @@ pub struct Application {
}
*/
#[derive(Debug, Serialize, Deserialize, Clone, Ord, PartialOrd, Eq, PartialEq)]
#[serde(rename_all = "lowercase")]
pub enum AuthType {
Anonymous,
UnixPassword,
Password,
GeneratedPassword,
PasswordMfa,
Passkey,
}
impl fmt::Display for AuthType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthType::Anonymous => write!(f, "anonymous"),
AuthType::UnixPassword => write!(f, "unixpassword"),
AuthType::Password => write!(f, "password"),
AuthType::GeneratedPassword => write!(f, "generatedpassword"),
AuthType::PasswordMfa => write!(f, "passwordmfa"),
AuthType::Passkey => write!(f, "passkey"),
}
}
}
#[derive(Debug, Serialize, Deserialize, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
#[serde(rename_all = "lowercase")]
#[derive(TryFromPrimitive)]
@ -418,7 +394,6 @@ pub enum UatPurpose {
#[serde(rename_all = "lowercase")]
pub struct UserAuthToken {
pub session_id: Uuid,
pub auth_type: AuthType,
#[serde(with = "time::serde::timestamp")]
pub issued_at: time::OffsetDateTime,
/// If none, there is no expiry, and this is always valid. If there is
@ -474,6 +449,15 @@ impl UserAuthToken {
pub fn name(&self) -> &str {
self.spn.split_once('@').map(|x| x.0).unwrap_or(&self.spn)
}
/// Show if the uat at a current point in time has active read-write
/// capabilities.
pub fn purpose_readwrite_active(&self, ct: time::OffsetDateTime) -> bool {
match self.purpose {
UatPurpose::ReadWrite { expiry: Some(exp) } => ct < exp,
_ => false,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]

View file

@ -3,8 +3,9 @@ ARG BASE_IMAGE=opensuse/tumbleweed:latest
FROM ${BASE_IMAGE} AS repos
RUN \
--mount=type=cache,id=zypp,target=/var/cache/zypp \
zypper mr -k repo-oss && \
zypper mr -k repo-update && \
zypper mr -k repo-oss; \
zypper mr -k repo-non-oss; \
zypper mr -k repo-update; \
zypper dup -y
# ======================

View file

@ -5,9 +5,9 @@ use std::sync::Arc;
use kanidm_proto::internal::AppLink;
use kanidm_proto::v1::{
ApiToken, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus, CredentialStatus,
Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest, SearchResponse, UatStatus,
UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse,
ApiToken, AuthIssueSession, AuthRequest, BackupCodesView, CURequest, CUSessionToken, CUStatus,
CredentialStatus, Entry as ProtoEntry, OperationError, RadiusAuthToken, SearchRequest,
SearchResponse, UatStatus, UnixGroupToken, UnixUserToken, UserAuthToken, WhoamiResponse,
};
use ldap3_proto::simple::*;
use regex::Regex;
@ -103,7 +103,7 @@ impl QueryServerReadV1 {
#[instrument(
level = "info",
name = "auth",
skip(self, sessionid, req, eventid)
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_auth(
@ -145,6 +145,46 @@ impl QueryServerReadV1 {
res
}
#[instrument(
level = "info",
name = "reauth",
skip_all,
fields(uuid = ?eventid)
)]
pub async fn handle_reauth(
&self,
uat: Option<String>,
issue: AuthIssueSession,
eventid: Uuid,
) -> Result<AuthResult, OperationError> {
let ct = duration_from_epoch_now();
let mut idm_auth = self.idms.auth().await;
security_info!("Begin reauth event");
let ident = idm_auth
.validate_and_parse_token_to_ident(uat.as_deref(), ct)
.map_err(|e| {
admin_error!(?e, "Invalid identity");
e
})?;
// Trigger a session clean *before* we take any auth steps.
// It's important to do this before to ensure that timeouts on
// the session are enforced.
idm_auth.expire_auth_sessions(ct).await;
// Generally things like auth denied are in Ok() msgs
// so true errors should always trigger a rollback.
let res = idm_auth
.reauth_init(ident, issue, ct)
.await
.and_then(|r| idm_auth.commit().map(|_| r));
security_info!(?res, "Sending reauth result");
res
}
#[instrument(
level = "info",
name = "online_backup",

View file

@ -3,9 +3,9 @@ use std::sync::Arc;
use std::time::Duration;
use kanidm_proto::v1::{
AccountUnixExtend, AuthType, CUIntentToken, CUSessionToken, CUStatus, CreateRequest,
DeleteRequest, Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify,
ModifyList as ProtoModifyList, ModifyRequest, OperationError,
AccountUnixExtend, CUIntentToken, CUSessionToken, CUStatus, CreateRequest, DeleteRequest,
Entry as ProtoEntry, GroupUnixExtend, Modify as ProtoModify, ModifyList as ProtoModifyList,
ModifyRequest, OperationError,
};
use time::OffsetDateTime;
use tracing::{info, instrument, span, trace, Level};
@ -567,7 +567,7 @@ impl QueryServerWriteV1 {
.map(|ident| (ident, uat))
})?;
if uat.auth_type == AuthType::Anonymous {
if uat.uuid == UUID_ANONYMOUS {
info!("Ignoring request to logout anonymous session - these sessions are not recorded");
return Ok(());
}

View file

@ -535,6 +535,9 @@ pub fn create_https_server(
appserver
.at("/v1/auth/valid")
.mapped_get(&mut routemap, auth_valid);
appserver
.at("/v1/reauth")
.mapped_post(&mut routemap, reauth);
appserver.at("/v1/logout").mapped_get(&mut routemap, logout);

View file

@ -1064,6 +1064,25 @@ pub async fn do_nothing(_req: tide::Request<AppState>) -> tide::Result {
Ok(res)
}
pub async fn reauth(mut req: tide::Request<AppState>) -> tide::Result {
let uat = req.get_current_uat();
let (eventid, hvalue) = req.new_eventid();
let obj: AuthIssueSession = req.body_json().await.map_err(|e| {
debug!("Failed get body JSON? {:?}", e);
e
})?;
let inter = req
.state()
// This may change in the future ...
.qe_r_ref
.handle_reauth(uat, obj, eventid)
.await;
auth_session_state_management(req, inter, hvalue)
}
pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
// First, deal with some state management.
// Do anything here first that's needed like getting the session details
@ -1077,25 +1096,28 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
e
})?;
let mut auth_session_id_tok = None;
// We probably need to know if we allocate the cookie, that this is a
// new session, and in that case, anything *except* authrequest init is
// invalid.
let res: Result<AuthResponse, _> = match req
let inter = req
.state()
// This may change in the future ...
.qe_r_ref
.handle_auth(maybe_sessionid, obj, eventid)
.await
{
// .and_then(|ar| {
Ok(ar) => {
let AuthResult {
state,
sessionid,
delay: _,
} = ar;
.await;
auth_session_state_management(req, inter, hvalue)
}
fn auth_session_state_management(
mut req: tide::Request<AppState>,
inter: Result<AuthResult, OperationError>,
hvalue: String,
) -> tide::Result {
let mut auth_session_id_tok = None;
let res: Result<AuthResponse, _> = match inter {
Ok(AuthResult { state, sessionid }) => {
// Do some response/state management.
match state {
AuthState::Choose(allowed) => {
@ -1131,6 +1153,7 @@ pub async fn auth(mut req: tide::Request<AppState>) -> tide::Result {
let msession = req.session_mut();
// Ensure the auth-session-id is set
msession.remove("auth-session-id");
trace!(?sessionid, "🔥 🔥 ");
msession
.insert("auth-session-id", sessionid)
.map_err(|e| {

View file

@ -55,7 +55,7 @@ smolset.workspace = true
sshkeys.workspace = true
tide.workspace = true
time = { workspace = true, features = ["serde", "std"] }
tokio = { workspace = true, features = ["net", "sync", "time"] }
tokio = { workspace = true, features = ["net", "sync", "time", "rt"] }
tokio-util = { workspace = true, features = ["codec"] }
toml.workspace = true
touch.workspace = true

View file

@ -78,6 +78,8 @@ pub const PW_MIN_LENGTH: usize = 10;
// Default
pub const AUTH_SESSION_EXPIRY: u64 = 3600;
// Ten minutes by default;
pub const AUTH_PRIVILEGE_EXPIRY: u64 = 600;
// The time that a token can be used before session
// status is enforced. This needs to be longer than

View file

@ -240,7 +240,7 @@ impl Totp {
self.do_totp_duration_from_epoch(&dur)
}
pub fn verify(&self, chal: u32, time: &Duration) -> bool {
pub fn verify(&self, chal: u32, time: Duration) -> bool {
let secs = time.as_secs();
let counter = secs / self.step;
// Any error becomes a failure.
@ -405,12 +405,12 @@ mod tests {
let otp = Totp::new(key, TOTP_DEFAULT_STEP, TotpAlgo::Sha512, TotpDigits::Six);
let d = Duration::from_secs(secs);
// Step
assert!(otp.verify(952181, &d));
assert!(otp.verify(952181, d));
// Step - 1
assert!(otp.verify(685469, &d));
assert!(otp.verify(685469, d));
// This is step - 2
assert!(!otp.verify(217213, &d));
assert!(!otp.verify(217213, d));
// This is step + 1
assert!(!otp.verify(972806, &d));
assert!(!otp.verify(972806, d));
}
}

View file

@ -2,8 +2,7 @@ use std::collections::{BTreeMap, BTreeSet};
use std::time::Duration;
use kanidm_proto::v1::{
AuthType, BackupCodesView, CredentialStatus, OperationError, UatPurpose, UatStatus, UiHint,
UserAuthToken,
BackupCodesView, CredentialStatus, OperationError, UatPurpose, UatStatus, UiHint, UserAuthToken,
};
use time::OffsetDateTime;
use uuid::Uuid;
@ -197,27 +196,97 @@ impl Account {
pub(crate) fn to_userauthtoken(
&self,
session_id: Uuid,
scope: SessionScope,
ct: Duration,
auth_type: AuthType,
expiry_secs: Option<u64>,
) -> Option<UserAuthToken> {
// This could consume self?
// The cred handler provided is what authenticated this user, so we can use it to
// process what the proper claims should be.
// Get the claims from the cred_h
// TODO: Apply policy to this expiry time.
let expiry = expiry_secs
.map(|offset| OffsetDateTime::unix_epoch() + ct + Duration::from_secs(offset));
// We have to remove the nanoseconds because when we transmit this / serialise it we drop
// the nanoseconds, but if we haven't done a serialise on the server our db cache has the
// ns value which breaks some checks.
let ct = ct - Duration::from_nanos(ct.subsec_nanos() as u64);
let issued_at = OffsetDateTime::unix_epoch() + ct;
// TODO: Apply priv expiry, and what type of token this is (ident, ro, rw).
let purpose = UatPurpose::ReadWrite { expiry };
let expiry =
Some(OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_SESSION_EXPIRY));
let (purpose, expiry) = match scope {
// Issue an invalid/expired session.
SessionScope::Synchronise => {
warn!(
"Should be impossible to issue sync sessions with a uat. Refusing to proceed."
);
return None;
}
SessionScope::ReadOnly => (UatPurpose::ReadOnly, expiry),
SessionScope::ReadWrite => {
// These sessions are always rw, and so have limited life.
(UatPurpose::ReadWrite { expiry }, expiry)
}
SessionScope::PrivilegeCapable =>
// Return a rw capable session with the expiry currently invalid.
// These sessions COULD live forever since they can re-auth properly.
// Today I'm setting this to 24hr though.
{
(
UatPurpose::ReadWrite { expiry: None },
Some(OffsetDateTime::unix_epoch() + ct + Duration::from_secs(86400)),
)
}
};
Some(UserAuthToken {
session_id,
expiry,
issued_at,
purpose,
uuid: self.uuid,
displayname: self.displayname.clone(),
spn: self.spn.clone(),
mail_primary: self.mail_primary.clone(),
ui_hints: self.ui_hints.clone(),
// application: None,
// groups: self.groups.iter().map(|g| g.to_proto()).collect(),
})
}
/// Given the session_id and other metadata, reissue a user authentication token
/// that has elevated privileges. In the future we may adapt this to change what
/// scopes are granted per-reauth.
pub(crate) fn to_reissue_userauthtoken(
&self,
session_id: Uuid,
session_expiry: Option<OffsetDateTime>,
scope: SessionScope,
ct: Duration,
) -> Option<UserAuthToken> {
let issued_at = OffsetDateTime::unix_epoch() + ct;
let (purpose, expiry) = match scope {
SessionScope::Synchronise | SessionScope::ReadOnly | SessionScope::ReadWrite => {
warn!(
"Impossible state, should not be re-issuing for session scope {:?}",
scope
);
return None;
}
SessionScope::PrivilegeCapable =>
// Return a ReadWrite session with an inner expiry for the privileges
{
let expiry = Some(
OffsetDateTime::unix_epoch() + ct + Duration::from_secs(AUTH_PRIVILEGE_EXPIRY),
);
(
UatPurpose::ReadWrite { expiry },
// Needs to come from the actual original session. If we don't do this we have
// to re-update the expiry in the DB. We don't want a re-auth to extend a time
// bound session.
session_expiry,
)
}
};
Some(UserAuthToken {
session_id,
auth_type,
expiry,
issued_at,
purpose,
@ -457,19 +526,25 @@ impl Account {
// Anonymous does NOT record it's sessions, so we simply check the expiry time
// of the token. This is already done for us as noted above.
if uat.auth_type == AuthType::Anonymous {
if uat.uuid == UUID_ANONYMOUS {
security_info!("Anonymous sessions do not have session records, session is valid.");
true
} else {
// Get the sessions.
let session_present = entry
.get_ava_as_session_map("user_auth_token_session")
.map(|session_map| session_map.get(&uat.session_id).is_some())
.unwrap_or(false);
.and_then(|session_map| session_map.get(&uat.session_id));
if session_present {
if let Some(session) = session_present {
security_info!("A valid session value exists for this token");
true
if session.expiry == uat.expiry {
true
} else {
security_info!("Session and uat expiry are not consistent, rejecting.");
debug!(ses_exp = ?session.expiry, uat_exp = ?uat.expiry);
false
}
} else {
let grace = uat.issued_at + GRACE_WINDOW;
let current = time::OffsetDateTime::unix_epoch() + ct;
@ -536,8 +611,10 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
f_eq("user_auth_token_session", PartialValue::Refer(dte.token_id))
])),
&modlist,
// Provide the event to impersonate
&dte.ident,
// Provide the event to impersonate. Notice how we project this with readwrite
// capability? This is because without this we'd force re-auths to end
// a session and we don't want that! you should always be able to logout!
&dte.ident.project_with_scope(AccessScope::ReadWrite),
)
.map_err(|e| {
admin_error!("Failed to destroy user auth token {:?}", e);
@ -683,7 +760,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
#[cfg(test)]
mod tests {
use crate::prelude::*;
use kanidm_proto::v1::{AuthType, UiHint};
use kanidm_proto::v1::UiHint;
#[test]
fn test_idm_account_from_anonymous() {
@ -719,7 +796,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Passkey, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
// Check the ui hints are as expected.
@ -744,7 +821,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Passkey, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
assert!(uat.ui_hints.len() == 2);
@ -769,7 +846,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Passkey, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
assert!(uat.ui_hints.len() == 3);

View file

@ -5,13 +5,14 @@
use std::collections::BTreeMap;
pub use std::collections::BTreeSet as Set;
use std::convert::TryFrom;
use std::fmt;
use std::time::Duration;
// use webauthn_rs::proto::Credential as WebauthnCredential;
use compact_jwt::{Jws, JwsSigner};
use hashbrown::HashSet;
use kanidm_proto::v1::{
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, AuthType, OperationError,
AuthAllowed, AuthCredential, AuthIssueSession, AuthMech, OperationError, UserAuthToken,
};
// use crossbeam::channel::Sender;
use tokio::sync::mpsc::UnboundedSender as Sender;
@ -31,6 +32,8 @@ use crate::idm::delayed::{
};
use crate::idm::AuthState;
use crate::prelude::*;
use crate::value::Session;
use time::OffsetDateTime;
// Each CredHandler takes one or more credentials and determines if the
// handlers requirements can be 100% fulfilled. This is where MFA or other
@ -46,6 +49,38 @@ const BAD_CREDENTIALS: &str = "invalid credential message";
const ACCOUNT_EXPIRED: &str = "account expired";
const PW_BADLIST_MSG: &str = "password is in badlist";
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum AuthType {
Anonymous,
UnixPassword,
Password,
GeneratedPassword,
PasswordMfa,
Passkey,
}
impl fmt::Display for AuthType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AuthType::Anonymous => write!(f, "anonymous"),
AuthType::UnixPassword => write!(f, "unixpassword"),
AuthType::Password => write!(f, "password"),
AuthType::GeneratedPassword => write!(f, "generatedpassword"),
AuthType::PasswordMfa => write!(f, "passwordmfa"),
AuthType::Passkey => write!(f, "passkey"),
}
}
}
#[derive(Debug, Clone)]
enum AuthIntent {
InitialAuth,
Reauth {
session_id: Uuid,
session_expiry: Option<OffsetDateTime>,
},
}
/// A response type to indicate the progress and potential result of an authentication attempt.
enum CredState {
Success { auth_type: AuthType, cred_id: Uuid },
@ -227,6 +262,34 @@ impl TryFrom<(&BTreeMap<Uuid, (String, PasskeyV4)>, &Webauthn)> for CredHandler
}
}
impl TryFrom<(Uuid, &PasskeyV4, &Webauthn)> for CredHandler {
type Error = ();
fn try_from(
(cred_id, pk, webauthn): (Uuid, &PasskeyV4, &Webauthn),
) -> Result<Self, Self::Error> {
let cred_ids = btreemap!((pk.cred_id().clone(), cred_id));
let pks = vec![pk.clone()];
webauthn
.start_passkey_authentication(pks.as_slice())
.map(|(chal, wan_state)| CredHandler::Passkey {
c_wan: CredWebauthn {
chal,
wan_state,
state: CredVerifyState::Init,
},
cred_ids,
})
.map_err(|e| {
security_info!(
?e,
"Unable to create passkey webauthn authentication challenge"
);
// maps to unit.
})
}
}
impl CredHandler {
/// Determine if this password factor requires an upgrade of it's cryptographic type. If
/// so, send an asynchronous event into the queue that will allow the password to have it's
@ -322,7 +385,7 @@ impl CredHandler {
fn validate_password_mfa(
cred: &AuthCredential,
cred_id: Uuid,
ts: &Duration,
ts: Duration,
pw_mfa: &mut CredMfa,
webauthn: &Webauthn,
who: Uuid,
@ -539,7 +602,7 @@ impl CredHandler {
pub fn validate(
&mut self,
cred: &AuthCredential,
ts: &Duration,
ts: Duration,
who: Uuid,
async_tx: &Sender<DelayedAction>,
webauthn: &Webauthn,
@ -669,6 +732,10 @@ pub(crate) struct AuthSession {
// The type of session we will issue if successful
issue: AuthIssueSession,
// What is the "intent" behind this auth session? Are we doing an initial auth? Or a re-auth
// for a privilege grant?
intent: AuthIntent,
}
impl AuthSession {
@ -713,7 +780,7 @@ impl AuthSession {
};
if handlers.is_empty() {
security_info!("account has no primary credentials");
security_info!("account has no available credentials");
AuthSessionState::Denied("invalid credential state")
} else {
AuthSessionState::Init(handlers)
@ -734,6 +801,7 @@ impl AuthSession {
account,
state,
issue,
intent: AuthIntent::InitialAuth,
};
// Get the set of mechanisms that can proceed. This is tied
// to the session so that it can mutate state and have progression
@ -746,6 +814,100 @@ impl AuthSession {
}
}
/// Build a new auth session which has been preconfigured for re-authentication.
/// This differs from [`AuthSession::new`] as we preselect the credential that
/// will be used in this operation based on the credential id that was used in the
/// initial authentication.
pub(crate) fn new_reauth(
account: Account,
session_id: Uuid,
session: &Session,
cred_id: Uuid,
issue: AuthIssueSession,
webauthn: &Webauthn,
ct: Duration,
) -> (Option<Self>, AuthState) {
/// An inner enum to allow us to more easily define state within this fn
enum State {
Expired,
NoMatchingCred,
Proceed(CredHandler),
}
let state = if account.is_within_valid_time(ct) {
// Get the credential that matches this cred_id.
//
// To make this work "cleanly" we can't really nest a bunch of if
// statements like if primary and if primary.cred_id because the logic will
// just be chaos. So for now we build this as a mut option.
//
// Do we need to double check for anon here? I don't think so since the
// anon cred_id won't ever exist on an account.
let mut cred_handler = None;
if let Some(primary) = account.primary.as_ref() {
if primary.uuid == cred_id {
if let Ok(ch) = CredHandler::try_from((primary, webauthn)) {
// Update it.
debug_assert!(cred_handler.is_none());
cred_handler = Some(ch);
} else {
security_critical!(
"corrupt credentials, unable to start primary credhandler"
);
}
}
}
if let Some(pk) = account.passkeys.get(&cred_id).map(|(_, pk)| pk) {
if let Ok(ch) = CredHandler::try_from((cred_id, pk, webauthn)) {
// Update it.
debug_assert!(cred_handler.is_none());
cred_handler = Some(ch);
} else {
security_critical!("corrupt credentials, unable to start primary credhandler");
}
}
// Did anything get set-up?
if let Some(cred_handler) = cred_handler {
State::Proceed(cred_handler)
} else {
State::NoMatchingCred
}
} else {
State::Expired
};
match state {
State::Proceed(handler) => {
let allow = handler.next_auth_allowed();
let auth_session = AuthSession {
account,
state: AuthSessionState::InProgress(handler),
issue,
intent: AuthIntent::Reauth {
session_id,
session_expiry: session.expiry.clone(),
},
};
let as_state = AuthState::Continue(allow);
(Some(auth_session), as_state)
}
State::Expired => {
security_info!("account expired");
(None, AuthState::Denied(ACCOUNT_EXPIRED.to_string()))
}
State::NoMatchingCred => {
security_error!("Unable to select a credential for authentication");
(None, AuthState::Denied(BAD_CREDENTIALS.to_string()))
}
}
}
// This is used for softlock identification only.
pub fn get_credential_uuid(&self) -> Result<Option<Uuid>, OperationError> {
match &self.state {
@ -830,7 +992,7 @@ impl AuthSession {
pub fn validate_creds(
&mut self,
cred: &AuthCredential,
time: &Duration,
time: Duration,
async_tx: &Sender<DelayedAction>,
webauthn: &Webauthn,
pw_badlist_set: Option<&HashSet<String>>,
@ -852,68 +1014,8 @@ impl AuthSession {
pw_badlist_set,
) {
CredState::Success { auth_type, cred_id } => {
security_info!("Successful cred handling");
let session_id = Uuid::new_v4();
let issue = self.issue;
// We need to actually work this out better, and then
// pass it to to_userauthtoken
let scope = SessionScope::ReadWrite;
security_info!(
"Issuing {:?} session {} for {} {}",
issue,
session_id,
self.account.spn,
self.account.uuid
);
let uat = self
.account
.to_userauthtoken(
session_id,
*time,
auth_type.clone(),
Some(AUTH_SESSION_EXPIRY),
)
.ok_or(OperationError::InvalidState)?;
// Queue the session info write.
// This is dependent on the type of authentication factors
// used. Generally we won't submit for Anonymous. Add an extra
// safety barrier for auth types that shouldn't be here. Generally we
// submit session info for everything else.
match auth_type {
AuthType::Anonymous => {
// Skip - these sessions are not validated by session id.
}
AuthType::UnixPassword => {
// Impossibru!
admin_error!("Impossible auth type (UnixPassword) found");
return Err(OperationError::InvalidState);
}
AuthType::Password
| AuthType::GeneratedPassword
| AuthType::PasswordMfa
| AuthType::Passkey => {
trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
target_uuid: self.account.uuid,
session_id,
cred_id,
label: "Auth Session".to_string(),
expiry: uat.expiry,
issued_at: uat.issued_at,
issued_by: IdentityId::User(self.account.uuid),
scope,
}))
.map_err(|_| {
admin_error!("unable to queue failing authentication as the session will not validate ... ");
OperationError::InvalidState
})?;
}
};
// Issue the uat based on a set of factors.
let uat = self.issue_uat(auth_type, time, async_tx, cred_id)?;
let jwt = Jws::new(uat);
// Now encrypt and prepare the token for return to the client.
@ -930,7 +1032,7 @@ impl AuthSession {
(
Some(AuthSessionState::Success),
Ok(AuthState::Success(token, issue)),
Ok(AuthState::Success(token, self.issue)),
)
}
CredState::Continue(allowed) => {
@ -967,6 +1069,104 @@ impl AuthSession {
response
}
fn issue_uat(
&mut self,
auth_type: AuthType,
time: Duration,
async_tx: &Sender<DelayedAction>,
cred_id: Uuid,
) -> Result<UserAuthToken, OperationError> {
security_info!("Successful cred handling");
match self.intent {
AuthIntent::InitialAuth => {
let session_id = Uuid::new_v4();
// We need to actually work this out better, and then
// pass it to to_userauthtoken
let scope = match auth_type {
AuthType::UnixPassword | AuthType::Anonymous => SessionScope::ReadOnly,
AuthType::GeneratedPassword => SessionScope::ReadWrite,
AuthType::Password | AuthType::PasswordMfa | AuthType::Passkey => {
SessionScope::PrivilegeCapable
}
};
security_info!(
"Issuing {:?} session {} for {} {}",
self.issue,
session_id,
self.account.spn,
self.account.uuid
);
let uat = self
.account
.to_userauthtoken(session_id, scope, time)
.ok_or(OperationError::InvalidState)?;
// Queue the session info write.
// This is dependent on the type of authentication factors
// used. Generally we won't submit for Anonymous. Add an extra
// safety barrier for auth types that shouldn't be here. Generally we
// submit session info for everything else.
match auth_type {
AuthType::Anonymous => {
// Skip - these sessions are not validated by session id.
}
AuthType::UnixPassword => {
// Impossibru!
admin_error!("Impossible auth type (UnixPassword) found");
return Err(OperationError::InvalidState);
}
AuthType::Password
| AuthType::GeneratedPassword
| AuthType::PasswordMfa
| AuthType::Passkey => {
trace!("⚠️ Queued AuthSessionRecord for {}", self.account.uuid);
async_tx.send(DelayedAction::AuthSessionRecord(AuthSessionRecord {
target_uuid: self.account.uuid,
session_id,
cred_id,
label: "Auth Session".to_string(),
expiry: uat.expiry,
issued_at: uat.issued_at,
issued_by: IdentityId::User(self.account.uuid),
scope,
}))
.map_err(|_| {
admin_error!("unable to queue failing authentication as the session will not validate ... ");
OperationError::InvalidState
})?;
}
};
Ok(uat)
}
AuthIntent::Reauth {
session_id,
session_expiry,
} => {
// Sanity check - We have already been really strict about what session types
// can actually trigger a re-auth, but we recheck here for paranoia!
let scope = match auth_type {
AuthType::UnixPassword | AuthType::Anonymous | AuthType::GeneratedPassword => {
error!("AuthType used in Reauth is not valid for session re-issuance. Rejecting");
return Err(OperationError::InvalidState);
}
AuthType::Password | AuthType::PasswordMfa | AuthType::Passkey => {
SessionScope::PrivilegeCapable
}
};
let uat = self
.account
.to_reissue_userauthtoken(session_id, session_expiry, scope, time)
.ok_or(OperationError::InvalidState)?;
Ok(uat)
}
}
}
/// End the session, defaulting to a denied.
pub fn end_session(&mut self, reason: &'static str) -> Result<AuthState, OperationError> {
let mut next_state = AuthSessionState::Denied(reason);
@ -1123,7 +1323,7 @@ mod tests {
let jws_signer = create_jwt_signer();
match session.validate_creds(
&attempt,
&Duration::from_secs(0),
Duration::from_secs(0),
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1141,7 +1341,7 @@ mod tests {
let attempt = AuthCredential::Password("test_password".to_string());
match session.validate_creds(
&attempt,
&Duration::from_secs(0),
Duration::from_secs(0),
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1181,7 +1381,7 @@ mod tests {
let attempt = AuthCredential::Password("list@no3IBTyqHu$bad".to_string());
match session.validate_creds(
&attempt,
&Duration::from_secs(0),
Duration::from_secs(0),
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1295,7 +1495,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Anonymous,
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1315,7 +1515,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1332,7 +1532,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_bad),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1351,7 +1551,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1362,7 +1562,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1381,7 +1581,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1392,7 +1592,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1453,7 +1653,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1464,7 +1664,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_badlist.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1599,7 +1799,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Anonymous,
&ts,
ts,
&async_tx,
&webauthn,
None,
@ -1621,7 +1821,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Passkey(resp),
&ts,
ts,
&async_tx,
&webauthn,
None,
@ -1655,7 +1855,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Passkey(resp),
&ts,
ts,
&async_tx,
&webauthn,
None,
@ -1698,7 +1898,7 @@ mod tests {
// not inline.
match session.validate_creds(
&AuthCredential::Passkey(resp),
&ts,
ts,
&async_tx,
&webauthn,
None,
@ -1742,7 +1942,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1760,7 +1960,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(0),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1789,7 +1989,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1813,7 +2013,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1824,7 +2024,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1854,7 +2054,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1865,7 +2065,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1930,7 +2130,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1948,7 +2148,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_bad),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1975,7 +2175,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -1999,7 +2199,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2010,7 +2210,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2034,7 +2234,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2045,7 +2245,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2063,7 +2263,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2074,7 +2274,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2103,7 +2303,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::SecurityKey(resp),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2114,7 +2314,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2190,7 +2390,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2207,7 +2407,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_bad),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2225,7 +2425,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_good.clone()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2236,7 +2436,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_bad.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2260,7 +2460,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::BackupCode(backup_code_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2271,7 +2471,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2302,7 +2502,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2313,7 +2513,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2380,7 +2580,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good_a),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2391,7 +2591,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2414,7 +2614,7 @@ mod tests {
match session.validate_creds(
&AuthCredential::Totp(totp_good_b),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),
@ -2425,7 +2625,7 @@ mod tests {
};
match session.validate_creds(
&AuthCredential::Password(pw_good.to_string()),
&ts,
ts,
&async_tx,
&webauthn,
Some(&pw_badlist_cache),

View file

@ -1228,7 +1228,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
MfaRegState::TotpInit(totp_token)
| MfaRegState::TotpTryAgain(totp_token)
| MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
if totp_token.verify(totp_chal, &ct) {
if totp_token.verify(totp_chal, ct) {
// It was valid. Update the credential.
let ncred = session
.primary
@ -1250,7 +1250,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
// check that just in case.
let token_sha1 = totp_token.clone().downgrade_to_legacy();
if token_sha1.verify(totp_chal, &ct) {
if token_sha1.verify(totp_chal, ct) {
// Greeeaaaaaatttt. It's a broken app. Let's check the user
// knows this is broken, before we proceed.
session.mfaregstate = MfaRegState::TotpInvalidSha1(
@ -1747,11 +1747,7 @@ mod tests {
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
@ -1762,11 +1758,7 @@ mod tests {
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
@ -1781,7 +1773,6 @@ mod tests {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the auth session
let da = idms_delayed.try_recv().expect("invalid");
@ -1806,11 +1797,7 @@ mod tests {
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
@ -1821,11 +1808,7 @@ mod tests {
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
@ -1836,11 +1819,7 @@ mod tests {
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth.auth(&totp_step, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
@ -1855,7 +1834,6 @@ mod tests {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the auth session
let da = idms_delayed.try_recv().expect("invalid");
@ -1879,11 +1857,7 @@ mod tests {
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
@ -1894,22 +1868,14 @@ mod tests {
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let code_step = AuthEvent::cred_step_backup_code(sessionid, code);
let r2 = idms_auth.auth(&code_step, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
@ -1924,7 +1890,6 @@ mod tests {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// There now should be a backup code invalidation present
let da = idms_delayed.try_recv().expect("invalid");
@ -1954,11 +1919,7 @@ mod tests {
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
@ -1969,11 +1930,7 @@ mod tests {
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
trace!(?state);
@ -2001,7 +1958,6 @@ mod tests {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the webauthn update
let da = idms_delayed.try_recv().expect("invalid");

View file

@ -1,5 +1,3 @@
use std::time::Duration;
use crate::idm::AuthState;
use crate::prelude::*;
use kanidm_proto::v1::OperationError;
@ -480,7 +478,6 @@ impl AuthEvent {
pub struct AuthResult {
pub sessionid: Uuid,
pub state: AuthState,
pub delay: Option<Duration>,
}
/*

View file

@ -27,7 +27,7 @@ use kanidm_proto::oauth2::{
ClaimType, DisplayValue, GrantType, IdTokenSignAlg, ResponseMode, ResponseType, SubjectType,
TokenEndpointAuthMethod,
};
use kanidm_proto::v1::{AuthType, UserAuthToken};
use kanidm_proto::v1::UserAuthToken;
use openssl::sha;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
@ -126,7 +126,6 @@ enum Oauth2TokenType {
scopes: Vec<String>,
parent_session_id: Uuid,
session_id: Uuid,
auth_type: AuthType,
expiry: time::OffsetDateTime,
uuid: Uuid,
iat: i64,
@ -641,8 +640,8 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// NOTE: login_hint is handled in the UI code, not here.
// Deny any uat with an auth method of anonymous
if uat.auth_type == AuthType::Anonymous {
// Deny anonymous access to oauth2
if uat.uuid == UUID_ANONYMOUS {
admin_error!(
"Invalid oauth2 request - refusing to allow user that authenticated with anonymous"
);
@ -1130,7 +1129,9 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
// TODO: If max_age was requested in the request, we MUST provide auth_time.
// amr == auth method
let amr = Some(vec![code_xchg.uat.auth_type.to_string()]);
// We removed this from uat, and I think that it's okay here. AMR is a bit useless anyway
// since there is no standard for what it should look like wrt to cred strength.
let amr = None;
let iss = o2rs.iss.clone();
@ -1188,7 +1189,6 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
scopes: code_xchg.scopes,
parent_session_id,
session_id,
auth_type: code_xchg.uat.auth_type,
expiry,
uuid: code_xchg.uat.uuid,
iat,
@ -1272,7 +1272,6 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
scopes,
parent_session_id,
session_id,
auth_type: _,
expiry,
uuid,
iat,
@ -1377,7 +1376,6 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
scopes,
parent_session_id,
session_id,
auth_type,
expiry,
uuid,
iat,
@ -1413,7 +1411,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
Err(err) => return Err(Oauth2Error::ServerError(err)),
};
let amr = Some(vec![auth_type.to_string()]);
let amr = None;
let iss = o2rs.iss.clone();
@ -1628,7 +1626,7 @@ mod tests {
use base64urlsafedata::Base64UrlSafeData;
use compact_jwt::{JwaAlg, Jwk, JwkUse, JwsValidator, OidcSubject, OidcUnverified};
use kanidm_proto::oauth2::*;
use kanidm_proto::v1::{AuthType, UserAuthToken};
use kanidm_proto::v1::UserAuthToken;
use openssl::sha;
use crate::idm::delayed::DelayedAction;
@ -1753,12 +1751,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(
session_id,
ct,
AuthType::PasswordMfa,
Some(AUTH_SESSION_EXPIRY),
)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -1769,18 +1762,14 @@ mod tests {
(secret, uat, ident, uuid)
}
async fn setup_idm_admin(
idms: &IdmServer,
ct: Duration,
authtype: AuthType,
) -> (UserAuthToken, Identity) {
async fn setup_idm_admin(idms: &IdmServer, ct: Duration) -> (UserAuthToken, Identity) {
let mut idms_prox_write = idms.proxy_write(ct).await;
let account = idms_prox_write
.target_to_account(UUID_IDM_ADMIN)
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat = account
.to_userauthtoken(session_id, ct, authtype, Some(AUTH_SESSION_EXPIRY))
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -1871,9 +1860,8 @@ mod tests {
let (_secret, uat, ident, _) =
setup_oauth2_resource_server(idms, ct, true, false, false).await;
let (anon_uat, anon_ident) = setup_idm_admin(idms, ct, AuthType::Anonymous).await;
let (idm_admin_uat, idm_admin_ident) =
setup_idm_admin(idms, ct, AuthType::PasswordMfa).await;
let (anon_uat, anon_ident) = setup_idm_admin(idms, ct).await;
let (idm_admin_uat, idm_admin_ident) = setup_idm_admin(idms, ct).await;
// Need a uat from a user not in the group. Probs anonymous.
let idms_prox_read = idms.proxy_read().await;
@ -2043,12 +2031,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat2 = account
.to_userauthtoken(
session_id,
ct,
AuthType::PasswordMfa,
Some(AUTH_SESSION_EXPIRY),
)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident2 = idms_prox_write
.process_uat_to_identity(&uat2, ct)
@ -2682,12 +2665,7 @@ mod tests {
.expect("account must exist");
let session_id = uuid::Uuid::new_v4();
let uat2 = account
.to_userauthtoken(
session_id,
ct,
AuthType::PasswordMfa,
Some(AUTH_SESSION_EXPIRY),
)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident2 = idms_prox_write
.process_uat_to_identity(&uat2, ct)
@ -2996,7 +2974,7 @@ mod tests {
assert!(oidc.nonce == Some("abcdef".to_string()));
assert!(oidc.at_hash.is_none());
assert!(oidc.acr.is_none());
assert!(oidc.amr == Some(vec!["passwordmfa".to_string()]));
assert!(oidc.amr == None);
assert!(oidc.azp == Some("test_resource_server".to_string()));
assert!(oidc.jti.is_none());
assert!(oidc.s_claims.name == Some("System Administrator".to_string()));

View file

@ -1,27 +1,173 @@
use crate::prelude::*;
use crate::credential::softlock::CredSoftLock;
use crate::idm::account::Account;
use crate::idm::authsession::AuthSession;
use crate::idm::event::AuthResult;
use crate::idm::server::IdmServerAuthTransaction;
use crate::idm::AuthState;
use crate::utils::uuid_from_duration;
#[derive(Debug)]
pub struct ReauthEvent {
// pub ident: Option<Identity>,
// pub step: AuthEventStep,
// pub sessionid: Option<Uuid>,
}
// use webauthn_rs::prelude::Webauthn;
use std::sync::Arc;
use tokio::sync::Mutex;
use kanidm_proto::v1::AuthIssueSession;
use super::server::CredSoftLockMutex;
impl<'a> IdmServerAuthTransaction<'a> {
pub async fn reauth(
pub async fn reauth_init(
&mut self,
_ae: &ReauthEvent,
_ct: Duration,
ident: Identity,
issue: AuthIssueSession,
ct: Duration,
) -> Result<AuthResult, OperationError> {
todo!();
// re-auth only works on users, so lets get the user account.
// hint - it's in the ident!
let entry = match ident.get_user_entry() {
Some(entry) => entry,
None => {
error!("Ident is not a user and has no entry associated. Unable to proceed.");
return Err(OperationError::InvalidState);
}
};
// Setup the account record.
let account = Account::try_from_entry_ro(entry.as_ref(), &mut self.qs_read)?;
security_info!(
username = %account.name,
issue = ?issue,
uuid = %account.uuid,
"Initiating Re-Authentication Session",
);
// Check that the entry/session can be re-authed.
let session = entry
.get_ava_as_session_map("user_auth_token_session")
.and_then(|sessions| sessions.get(&ident.session_id))
.ok_or_else(|| {
error!("Ident session is not present in entry. Perhaps replication is delayed?");
OperationError::InvalidState
})?;
match session.scope {
SessionScope::PrivilegeCapable => {
// Yes! This session can re-auth!
}
SessionScope::ReadOnly | SessionScope::ReadWrite | SessionScope::Synchronise => {
// These can not!
error!("Session scope is not PrivilegeCapable and can not be used in re-auth.");
return Err(OperationError::InvalidState);
}
};
// Get the credential id.
let session_cred_id = session.cred_id;
// == Everything Checked Out! ==
// Let's setup to proceed with the re-auth.
// Allocate the *authentication* session id based on current time / sid.
let sessionid = uuid_from_duration(ct, self.sid);
// Start getting things.
let _session_ticket = self.session_ticket.acquire().await;
// Setup soft locks here if required.
let maybe_slock = account
.primary_cred_uuid_and_policy()
.and_then(|(cred_uuid, policy)| {
// Acquire the softlock map
//
// We have no issue calling this with .write here, since we
// already hold the session_ticket above.
//
// We only do this if the primary credential being used here is for
// this re-auth session. Else passkeys/devicekeys are not bounded by this
// problem.
if cred_uuid == session_cred_id {
let mut softlock_write = self.softlocks.write();
let slock_ref: CredSoftLockMutex =
if let Some(slock_ref) = softlock_write.get(&cred_uuid) {
slock_ref.clone()
} else {
// Create if not exist, and the cred type supports softlocking.
let slock = Arc::new(Mutex::new(CredSoftLock::new(policy)));
softlock_write.insert(cred_uuid, slock.clone());
slock
};
softlock_write.commit();
Some(slock_ref)
} else {
None
}
});
// Check if the cred is locked! We want to fail fast here! Unlike the auth flow we have
// already selected our credential so we can test it's slock, else we could be allowing
// 1-attempt per-reauth.
let is_valid = if let Some(slock_ref) = maybe_slock {
let mut slock = slock_ref.lock().await;
slock.apply_time_step(ct);
slock.is_valid()
} else {
true
};
if !is_valid {
warn!(
"Credential {:?} is currently softlocked, unable to proceed",
session_cred_id
);
return Ok(AuthResult {
sessionid: ident.get_session_id(),
state: AuthState::Denied("Credential is temporarily locked".to_string()),
});
}
// Create a re-auth session
let (auth_session, state) = AuthSession::new_reauth(
account,
ident.session_id,
session,
session_cred_id,
issue,
self.webauthn,
ct,
);
// Push the re-auth session to the session maps.
match auth_session {
Some(auth_session) => {
let mut session_write = self.sessions.write();
if session_write.contains_key(&sessionid) {
// If we have a session of the same id, return an error (despite how
// unlikely this is ...
Err(OperationError::InvalidSessionState)
} else {
session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
// Debugging: ensure we really inserted ...
debug_assert!(session_write.get(&sessionid).is_some());
Ok(())
}?;
session_write.commit();
}
None => {
security_info!("Authentication Session Unable to begin");
}
};
Ok(AuthResult { sessionid, state })
}
}
#[cfg(test)]
mod tests {
use crate::credential::totp::Totp;
use crate::idm::credupdatesession::{InitCredentialUpdateEvent, MfaRegStateStatus};
use crate::idm::delayed::DelayedAction;
use crate::idm::event::{AuthEvent, AuthResult};
@ -114,6 +260,66 @@ mod tests {
wa
}
async fn setup_testaccount_password_totp(idms: &IdmServer, ct: Duration) -> (String, Totp) {
let mut idms_prox_write = idms.proxy_write(ct).await;
let testperson = idms_prox_write
.qs_write
.internal_search_uuid(TESTPERSON_UUID)
.expect("failed");
let (cust, _c_status) = idms_prox_write
.init_credential_update(
&InitCredentialUpdateEvent::new_impersonate_entry(testperson),
ct,
)
.expect("Failed to begin credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
let cutxn = idms.cred_update_transaction().await;
let pw = crate::utils::password_from_random();
let c_status = cutxn
.credential_primary_set_password(&cust, ct, &pw)
.expect("Failed to update the primary cred password");
assert!(c_status.can_commit());
let c_status = cutxn
.credential_primary_init_totp(&cust, ct)
.expect("Failed to update the primary cred password");
// Check the status has the token.
let totp_token: Totp = match c_status.mfaregstate() {
MfaRegStateStatus::TotpCheck(secret) => Some(secret.clone().try_into().unwrap()),
_ => None,
}
.expect("Unable to retrieve totp token, invalid state.");
trace!(?totp_token);
let chal = totp_token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let c_status = cutxn
.credential_primary_check_totp(&cust, ct, chal, "totp")
.expect("Failed to update the primary cred password");
assert!(matches!(c_status.mfaregstate(), MfaRegStateStatus::None));
assert!(c_status.can_commit());
drop(cutxn);
let mut idms_prox_write = idms.proxy_write(ct).await;
idms_prox_write
.commit_credential_update(&cust, ct)
.expect("Failed to commit credential update.");
idms_prox_write.commit().expect("Failed to commit txn");
(pw, totp_token)
}
async fn auth_passkey(
idms: &IdmServer,
ct: Duration,
@ -127,11 +333,7 @@ mod tests {
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
@ -142,11 +344,7 @@ mod tests {
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
trace!(?state);
@ -174,7 +372,6 @@ mod tests {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
delay: _,
}) => {
// Process the webauthn update
let da = idms_delayed.try_recv().expect("invalid");
@ -196,6 +393,71 @@ mod tests {
}
}
async fn auth_password_totp(
idms: &IdmServer,
ct: Duration,
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
let mut idms_auth = idms.auth().await;
let auth_init = AuthEvent::named_init("testperson");
let r1 = idms_auth.auth(&auth_init, ct).await;
let ar = r1.unwrap();
let AuthResult { sessionid, state } = ar;
if !matches!(state, AuthState::Choose(_)) {
debug!("Can't proceed - {:?}", state);
return None;
};
let auth_begin = AuthEvent::begin_mech(sessionid, AuthMech::PasswordMfa);
let r2 = idms_auth.auth(&auth_begin, ct).await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let totp = token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth.auth(&totp_step, ct).await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success
let r3 = idms_auth.auth(&pw_step, ct).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
// Process the auth session
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
// We have to actually write this one else the following tests
// won't work!
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
Some(token)
}
_ => None,
}
}
async fn token_to_ident(idms: &IdmServer, ct: Duration, token: Option<&str>) -> Identity {
let mut idms_prox_read = idms.proxy_read().await;
@ -204,6 +466,128 @@ mod tests {
.expect("Invalid UAT")
}
async fn reauth_passkey(
idms: &IdmServer,
ct: Duration,
ident: &Identity,
wa: &mut WebauthnAuthenticator<SoftPasskey>,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
let mut idms_auth = idms.auth().await;
let origin = idms_auth.get_origin().clone();
let auth_allowed = idms_auth
.reauth_init(ident.clone(), AuthIssueSession::Token, ct)
.await
.expect("Failed to start reauth.");
let AuthResult { sessionid, state } = auth_allowed;
trace!(?state);
let rcr = match state {
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Passkey(rcr)) => rcr,
_ => return None,
},
_ => unreachable!(),
};
trace!(?rcr);
let resp = wa
.do_authentication(origin, rcr)
.expect("failed to use softtoken to authenticate");
let passkey_step = AuthEvent::cred_step_passkey(sessionid, resp);
let r3 = idms_auth.auth(&passkey_step, ct).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
// Process the webauthn update
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::WebauthnCounterIncrement(_)));
let r = idms.delayed_action(ct, da).await;
assert!(r.is_ok());
// NOTE: Unlike initial auth we don't need to check the auth session in the queue
// since we don't re-issue it.
Some(token)
}
_ => unreachable!(),
}
}
async fn reauth_password_totp(
idms: &IdmServer,
ct: Duration,
ident: &Identity,
pw: &str,
token: &Totp,
idms_delayed: &mut IdmServerDelayed,
) -> Option<String> {
let mut idms_auth = idms.auth().await;
let auth_allowed = idms_auth
.reauth_init(ident.clone(), AuthIssueSession::Token, ct)
.await
.expect("Failed to start reauth.");
let AuthResult { sessionid, state } = auth_allowed;
trace!(?state);
match state {
AuthState::Denied(reason) => {
trace!("{}", reason);
return None;
}
AuthState::Continue(mut allowed) => match allowed.pop() {
Some(AuthAllowed::Totp) => {}
_ => return None,
},
_ => unreachable!(),
};
let totp = token
.do_totp_duration_from_epoch(&ct)
.expect("Failed to perform totp step");
let totp_step = AuthEvent::cred_step_totp(sessionid, totp);
let r2 = idms_auth.auth(&totp_step, ct).await;
let ar = r2.unwrap();
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Continue(_)));
let pw_step = AuthEvent::cred_step_password(sessionid, pw);
// Expect success
let r3 = idms_auth.auth(&pw_step, ct).await;
debug!("r3 ==> {:?}", r3);
idms_auth.commit().expect("Must not fail");
match r3 {
Ok(AuthResult {
sessionid: _,
state: AuthState::Success(token, AuthIssueSession::Token),
}) => {
// Process the auth session
let da = idms_delayed.try_recv().expect("invalid");
assert!(matches!(da, DelayedAction::AuthSessionRecord(_)));
Some(token)
}
_ => None,
}
}
#[idm_test]
async fn test_idm_reauth_passkey(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
let ct = duration_from_epoch_now();
@ -220,14 +604,75 @@ mod tests {
// Token_str to uat
let ident = token_to_ident(idms, ct, Some(token.as_str())).await;
// Check that the rw entitlement is not present, and that re-auth is allowed.
// assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
// Check that the rw entitlement is not present
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
// Assert the session is rw capable though.
// Assert the session is rw capable though which is what will allow the re-auth
// to proceed.
// Do a re-auth
let session = ident.get_session().expect("Unable to access sessions");
assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
// Start the re-auth
let token = reauth_passkey(idms, ct, &ident, &mut passkey, idms_delayed)
.await
.expect("Failed to get new session token");
// Token_str to uat
let ident = token_to_ident(idms, ct, Some(token.as_str())).await;
// They now have the entitlement.
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadWrite));
}
#[idm_test]
async fn test_idm_reauth_softlocked_pw(idms: &IdmServer, idms_delayed: &mut IdmServerDelayed) {
// This test is to enforce that an account in a soft lock state can't proceed
// we a re-auth.
let ct = duration_from_epoch_now();
// Setup the test account
setup_testaccount(idms, ct).await;
let (pw, totp) = setup_testaccount_password_totp(idms, ct).await;
// Do an initial auth.
let token = auth_password_totp(idms, ct, &pw, &totp, idms_delayed)
.await
.expect("failed to authenticate with passkey");
// Token_str to uat
let ident = token_to_ident(idms, ct, Some(token.as_str())).await;
// Check that the rw entitlement is not present
debug!(?ident);
assert!(matches!(ident.access_scope(), AccessScope::ReadOnly));
// Assert the session is rw capable though which is what will allow the re-auth
// to proceed.
let session = ident.get_session().expect("Unable to access sessions");
assert!(matches!(session.scope, SessionScope::PrivilegeCapable));
// Softlock the account now.
assert!(reauth_password_totp(
idms,
ct,
&ident,
"absolutely-wrong-password",
&totp,
idms_delayed
)
.await
.is_none());
// Start the re-auth - MUST FAIL!
assert!(
reauth_password_totp(idms, ct, &ident, &pw, &totp, idms_delayed)
.await
.is_none()
);
}
}

View file

@ -56,8 +56,8 @@ use crate::prelude::*;
use crate::utils::{password_from_random, readable_password_from_random, uuid_from_duration, Sid};
use crate::value::{Oauth2Session, Session};
type AuthSessionMutex = Arc<Mutex<AuthSession>>;
type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
pub(crate) type AuthSessionMutex = Arc<Mutex<AuthSession>>;
pub(crate) type CredSoftLockMutex = Arc<Mutex<CredSoftLock>>;
#[derive(Clone)]
pub struct DomainKeys {
@ -91,18 +91,18 @@ pub struct IdmServer {
/// Contains methods that require writes, but in the context of writing to the idm in memory structures (maybe the query server too). This is things like authentication.
pub struct IdmServerAuthTransaction<'a> {
session_ticket: &'a Semaphore,
sessions: &'a BptreeMap<Uuid, AuthSessionMutex>,
softlocks: &'a HashMap<Uuid, CredSoftLockMutex>,
pub(crate) session_ticket: &'a Semaphore,
pub(crate) sessions: &'a BptreeMap<Uuid, AuthSessionMutex>,
pub(crate) softlocks: &'a HashMap<Uuid, CredSoftLockMutex>,
pub qs_read: QueryServerReadTransaction<'a>,
/// Thread/Server ID
sid: Sid,
pub(crate) sid: Sid,
// For flagging eventual actions.
async_tx: Sender<DelayedAction>,
webauthn: &'a Webauthn,
pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
domain_keys: CowCellReadTxn<DomainKeys>,
pub(crate) async_tx: Sender<DelayedAction>,
pub(crate) webauthn: &'a Webauthn,
pub(crate) pw_badlist_cache: CowCellReadTxn<HashSet<String>>,
pub(crate) domain_keys: CowCellReadTxn<DomainKeys>,
}
pub struct IdmServerCredUpdateTransaction<'a> {
@ -661,7 +661,7 @@ pub trait IdmServerTransaction<'a> {
let scope = match uat.purpose {
UatPurpose::ReadOnly => AccessScope::ReadOnly,
UatPurpose::ReadWrite { expiry: None } => AccessScope::ReadWrite,
UatPurpose::ReadWrite { expiry: None } => AccessScope::ReadOnly,
UatPurpose::ReadWrite {
expiry: Some(expiry),
} => {
@ -910,9 +910,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
ae: &AuthEvent,
ct: Duration,
) -> Result<AuthResult, OperationError> {
trace!(?ae, "Received");
// Match on the auth event, to see what we need to do.
match &ae.step {
AuthEventStep::Init(init) => {
// lperf_segment!("idm::server::auth<Init>", || {
@ -984,35 +982,6 @@ impl<'a> IdmServerAuthTransaction<'a> {
slock_ref
});
/*
let mut maybe_slock = if let Some(slock_ref) = maybe_slock_ref.as_ref() {
Some(slock_ref.lock().await)
} else {
None
};
// Need to as_mut here so that we hold the slock for the whole operation.
let is_valid = if let Some(slock) = maybe_slock.as_mut() {
slock.apply_time_step(ct);
slock.is_valid()
} else {
false
};
*/
/*
let (auth_session, state) = if is_valid {
AuthSession::new(account, self.webauthn, ct)
} else {
// it's softlocked, don't even bother.
security_info!("Account is softlocked, or has no credentials associated.");
(
None,
AuthState::Denied("Account is temporarily locked".to_string()),
)
};
*/
let (auth_session, state) =
AuthSession::new(account, init.issue, self.webauthn, ct);
@ -1020,6 +989,8 @@ impl<'a> IdmServerAuthTransaction<'a> {
Some(auth_session) => {
let mut session_write = self.sessions.write();
if session_write.contains_key(&sessionid) {
// If we have a session of the same id, return an error (despite how
// unlikely this is ...
Err(OperationError::InvalidSessionState)
} else {
session_write.insert(sessionid, Arc::new(Mutex::new(auth_session)));
@ -1034,23 +1005,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
}
};
// TODO: Change this william!
// For now ...
let delay = None;
// If we have a session of the same id, return an error (despite how
// unlikely this is ...
Ok(AuthResult {
sessionid,
state,
delay,
})
Ok(AuthResult { sessionid, state })
} // AuthEventStep::Init
AuthEventStep::Begin(mech) => {
// lperf_segment!("idm::server::auth<Begin>", || {
// let _session_ticket = self.session_ticket.acquire().await;
let session_read = self.sessions.read();
// Do we have a session?
let auth_session_ref = session_read
@ -1091,13 +1048,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
// Fail the session
auth_session.end_session("Account is temporarily locked")
}
.map(|aus| {
let delay = None;
AuthResult {
sessionid: mech.sessionid,
state: aus,
delay,
}
.map(|aus| AuthResult {
sessionid: mech.sessionid,
state: aus,
})
} // End AuthEventStep::Mech
AuthEventStep::Cred(creds) => {
@ -1151,7 +1104,7 @@ impl<'a> IdmServerAuthTransaction<'a> {
auth_session
.validate_creds(
&creds.cred,
&ct,
ct,
&self.async_tx,
self.webauthn,
pw_badlist_cache,
@ -1172,16 +1125,9 @@ impl<'a> IdmServerAuthTransaction<'a> {
// Fail the session
auth_session.end_session("Account is temporarily locked")
}
.map(|aus| {
// TODO: Change this william!
// For now ...
let delay = None;
AuthResult {
// Is this right?
sessionid: creds.sessionid,
state: aus,
delay,
}
.map(|aus| AuthResult {
sessionid: creds.sessionid,
state: aus,
})
} // End AuthEventStep::Cred
}
@ -2246,7 +2192,7 @@ mod tests {
use std::convert::TryFrom;
use std::time::Duration;
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, AuthType, OperationError};
use kanidm_proto::v1::{AuthAllowed, AuthIssueSession, AuthMech, OperationError};
use smartstring::alias::String as AttrString;
use time::OffsetDateTime;
use uuid::Uuid;
@ -2284,12 +2230,7 @@ mod tests {
let sid = match r1 {
Ok(ar) => {
let AuthResult {
sessionid,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
let AuthResult { sessionid, state } = ar;
match state {
AuthState::Choose(mut conts) => {
// Should only be one auth mech
@ -2330,10 +2271,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Continue(allowed) => {
// Check the uat.
@ -2370,10 +2309,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
@ -2453,13 +2390,8 @@ mod tests {
let r1 = idms_auth.auth(&admin_init, ct).await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay,
} = ar;
let AuthResult { sessionid, state } = ar;
debug_assert!(delay.is_none());
assert!(matches!(state, AuthState::Choose(_)));
// Now push that we want the Password Mech.
@ -2467,13 +2399,7 @@ mod tests {
let r2 = idms_auth.auth(&admin_begin, ct).await;
let ar = r2.unwrap();
let AuthResult {
sessionid,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
let AuthResult { sessionid, state } = ar;
match state {
AuthState::Continue(_) => {}
@ -2506,11 +2432,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(token, AuthIssueSession::Token) => {
// Check the uat.
@ -2577,9 +2500,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
@ -2626,9 +2547,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(_reason) => {
// Check the uat.
@ -3074,10 +2993,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(_) => {}
_ => {
@ -3096,10 +3013,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(_) => {}
_ => {
@ -3243,9 +3158,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(reason) => {
assert!(reason != "Account is temporarily locked");
@ -3273,11 +3186,7 @@ mod tests {
.auth(&admin_init, Duration::from_secs(TEST_CURRENT_TIME))
.await;
let ar = r1.unwrap();
let AuthResult {
sessionid,
state,
delay: _,
} = ar;
let AuthResult { sessionid, state } = ar;
assert!(matches!(state, AuthState::Choose(_)));
// Soft locks only apply once a mechanism is chosen
@ -3290,10 +3199,8 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(reason) => {
assert!(reason == "Account is temporarily locked");
@ -3328,9 +3235,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Success(_uat, AuthIssueSession::Token) => {
// Check the uat.
@ -3393,9 +3298,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(reason) => {
assert!(reason != "Account is temporarily locked");
@ -3427,9 +3330,7 @@ mod tests {
let AuthResult {
sessionid: _,
state,
delay,
} = ar;
debug_assert!(delay.is_none());
match state {
AuthState::Denied(reason) => {
assert!(reason == "Account is temporarily locked");
@ -3722,7 +3623,7 @@ mod tests {
// == anonymous
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Anonymous, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -3736,7 +3637,7 @@ mod tests {
// == unixpassword
let uat = account
.to_userauthtoken(session_id, ct, AuthType::UnixPassword, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -3750,7 +3651,7 @@ mod tests {
// == password
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Password, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -3764,7 +3665,7 @@ mod tests {
// == generatedpassword
let uat = account
.to_userauthtoken(session_id, ct, AuthType::GeneratedPassword, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -3778,7 +3679,7 @@ mod tests {
// == webauthn
let uat = account
.to_userauthtoken(session_id, ct, AuthType::Passkey, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)
@ -3792,7 +3693,7 @@ mod tests {
// == passwordmfa
let uat = account
.to_userauthtoken(session_id, ct, AuthType::PasswordMfa, None)
.to_userauthtoken(session_id, SessionScope::ReadWrite, ct)
.expect("Unable to create uat");
let ident = idms_prox_write
.process_uat_to_identity(&uat, ct)

View file

@ -13,7 +13,9 @@ macro_rules! setup_test {
.expect("Failed to init BE");
let qs = QueryServer::new(be, schema_outer, "example.com".to_string());
tokio::runtime::Runtime::new()
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.initialise_helper(duration_from_epoch_now()))
.expect("init failed!");
@ -36,13 +38,17 @@ macro_rules! setup_test {
.expect("Failed to init BE");
let qs = QueryServer::new(be, schema_outer, "example.com".to_string());
tokio::runtime::Runtime::new()
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.initialise_helper(duration_from_epoch_now()))
.expect("init failed!");
if !$preload_entries.is_empty() {
let mut qs_write = tokio::runtime::Runtime::new()
let mut qs_write = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.write(duration_from_epoch_now()));
qs_write
@ -105,7 +111,9 @@ macro_rules! run_create_test {
};
{
let mut qs_write = tokio::runtime::Runtime::new()
let mut qs_write = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.write(duration_from_epoch_now()));
let r = qs_write.create(&ce);
@ -123,7 +131,9 @@ macro_rules! run_create_test {
}
// Make sure there are no errors.
trace!("starting verification");
let ver = tokio::runtime::Runtime::new()
let ver = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.verify());
trace!("verification -> {:?}", ver);
@ -151,7 +161,9 @@ macro_rules! run_modify_test {
let qs = setup_test!($preload_entries);
{
let mut qs_write = tokio::runtime::Runtime::new()
let mut qs_write = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.write(duration_from_epoch_now()));
$pre_hook(&mut qs_write);
@ -166,7 +178,9 @@ macro_rules! run_modify_test {
};
{
let mut qs_write = tokio::runtime::Runtime::new()
let mut qs_write = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.write(duration_from_epoch_now()));
let r = qs_write.modify(&me);
@ -184,7 +198,9 @@ macro_rules! run_modify_test {
}
// Make sure there are no errors.
trace!("starting verification");
let ver = tokio::runtime::Runtime::new()
let ver = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.verify());
trace!("verification -> {:?}", ver);
@ -216,7 +232,9 @@ macro_rules! run_delete_test {
};
{
let mut qs_write = tokio::runtime::Runtime::new()
let mut qs_write = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.write(duration_from_epoch_now()));
let r = qs_write.delete(&de);
@ -234,7 +252,9 @@ macro_rules! run_delete_test {
}
// Make sure there are no errors.
trace!("starting verification");
let ver = tokio::runtime::Runtime::new()
let ver = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(qs.verify());
trace!("verification -> {:?}", ver);

View file

@ -14,6 +14,7 @@ use kanidm_proto::v1::{ApiTokenPurpose, UatPurpose};
use serde::{Deserialize, Serialize};
use crate::prelude::*;
use crate::value::Session;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AccessScope {
@ -54,7 +55,7 @@ impl From<&UatPurpose> for AccessScope {
#[derive(Debug, Clone)]
/// Metadata and the entry of the current Identity which is an external account/user.
pub struct IdentUser {
pub entry: Arc<Entry<EntrySealed, EntryCommitted>>,
pub entry: Arc<EntrySealedCommitted>,
// IpAddr?
// Other metadata?
}
@ -157,10 +158,33 @@ impl Identity {
self.scope
}
pub fn project_with_scope(&self, scope: AccessScope) -> Self {
let mut new = self.clone();
new.scope = scope;
new
}
pub fn get_session_id(&self) -> Uuid {
self.session_id
}
pub fn get_session(&self) -> Option<&Session> {
match &self.origin {
IdentType::Internal | IdentType::Synch(_) => None,
IdentType::User(u) => u
.entry
.get_ava_as_session_map("user_auth_token_session")
.and_then(|sessions| sessions.get(&self.session_id)),
}
}
pub fn get_user_entry(&self) -> Option<Arc<EntrySealedCommitted>> {
match &self.origin {
IdentType::Internal | IdentType::Synch(_) => None,
IdentType::User(u) => Some(u.entry.clone()),
}
}
pub fn from_impersonate(ident: &Self) -> Self {
// TODO #64 ?: In the future, we could change some of this data
// to reflect the fact we are in fact impersonating the action

View file

@ -39,3 +39,4 @@ serde_json.workspace = true
webauthn-authenticator-rs.workspace = true
oauth2_ext = { workspace = true, default-features = false }
futures.workspace = true
time.workspace = true

View file

@ -203,8 +203,16 @@ async fn login_account(rsclient: &KanidmClient, id: &str) {
let res = rsclient
.auth_simple_password(id, "eicieY7ahchaoCh0eeTa")
.await;
// Setup privs
println!("{} logged in", id);
assert!(res.is_ok());
let res = rsclient
.reauth_simple_password("eicieY7ahchaoCh0eeTa")
.await;
println!("{} priv granted for", id);
assert!(res.is_ok());
}
// Login to the given account, but first login with default admin credentials.

View file

@ -1052,7 +1052,20 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
.await;
assert!(res.is_ok());
// We are now authed as the demo_account
// We are now authed as the demo_account, however we need to priv auth to get write
// access to self for credential updates.
let totp_chal = totp
.do_totp_duration_from_epoch(
&SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap(),
)
.expect("Failed to do totp?");
let res = rsclient
.reauth_password_totp("sohdi3iuHo6mai7noh0a", totp_chal)
.await;
assert!(res.is_ok());
// Self create the session and remove the totp now.
let (session_token, _status) = rsclient
@ -1079,8 +1092,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
assert!(res.is_ok());
}
#[kanidmd_testkit::test]
async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
async fn setup_demo_account_passkey(rsclient: &KanidmClient) -> WebauthnAuthenticator<SoftPasskey> {
let res = rsclient
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
.await;
@ -1154,6 +1166,14 @@ async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
// Assert it now works.
let _ = rsclient.logout();
wa
}
#[kanidmd_testkit::test]
async fn test_server_credential_update_session_passkey(rsclient: KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await;
let res = rsclient
.auth_passkey_begin("demo_account")
.await
@ -1320,3 +1340,72 @@ async fn test_server_user_auth_token_lifecycle(rsclient: KanidmClient) {
// No need to test expiry, that's validated in the server internal tests.
}
#[kanidmd_testkit::test]
async fn test_server_user_auth_reauthentication(rsclient: KanidmClient) {
let mut wa = setup_demo_account_passkey(&rsclient).await;
let res = rsclient
.auth_passkey_begin("demo_account")
.await
.expect("Failed to start passkey auth");
let pkc = wa
.do_authentication(rsclient.get_origin().clone(), res)
.map(Box::new)
.expect("Failed to authentication with soft passkey");
let res = rsclient.auth_passkey_complete(pkc).await;
assert!(res.is_ok());
// Assert we are still readonly
let token = rsclient
.get_token()
.await
.expect("Must have a bearer token");
let jwtu = JwsUnverified::from_str(&token).expect("Failed to parse jwsu");
let uat: UserAuthToken = jwtu
.validate_embeded()
.map(|jws| jws.into_inner())
.expect("Unable to open up token.");
let now = time::OffsetDateTime::now_utc();
assert!(uat.purpose_readwrite_active(now) == false);
// The auth is done, now we have to setup to re-auth for our session.
// Should we bother looking at the internals of the token here to assert
// it all worked? I don't think we have to because the server tests have
// already checked all those bits.
let res = rsclient
// TODO! Should we actually be able to track what was used here? Or
// do we just assume?
.reauth_passkey_begin()
.await
.expect("Failed to start passkey re-authentication");
let pkc = wa
.do_authentication(rsclient.get_origin().clone(), res)
.map(Box::new)
.expect("Failed to re-authenticate with soft passkey");
let res = rsclient.reauth_passkey_complete(pkc).await;
assert!(res.is_ok());
// assert we are elevated now
let token = rsclient
.get_token()
.await
.expect("Must have a bearer token");
let jwtu = JwsUnverified::from_str(&token).expect("Failed to parse jwsu");
let uat: UserAuthToken = jwtu
.validate_embeded()
.map(|jws| jws.into_inner())
.expect("Unable to open up token.");
let now = time::OffsetDateTime::now_utc();
eprintln!("{:?} {:?}", now, uat.purpose);
assert!(uat.purpose_readwrite_active(now) == true);
}

View file

@ -47,6 +47,7 @@ url = { workspace = true }
uuid = { workspace = true }
yew = { workspace = true, features = ["csr"] }
yew-router = { workspace = true }
time.workspace = true
[dependencies.web-sys]

View file

@ -233,19 +233,19 @@ function addBorrowedObject(obj) {
}
function __wbg_adapter_48(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hf55006aea4c68504(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hac369e4ee86f495a(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
}
function __wbg_adapter_51(arg0, arg1, arg2) {
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h952401ac26470b7a(arg0, arg1, addHeapObject(arg2));
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__he1d6c0533ad33db4(arg0, arg1, addHeapObject(arg2));
}
function __wbg_adapter_54(arg0, arg1, arg2) {
try {
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h789a13d164f258bd(arg0, arg1, addBorrowedObject(arg2));
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0f629f079cf8b186(arg0, arg1, addBorrowedObject(arg2));
} finally {
heap[stack_pointer++] = undefined;
}
@ -350,6 +350,10 @@ function getImports() {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) {
const ret = getObject(arg0) === getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
const ret = BigInt.asUintN(64, arg0);
return addHeapObject(ret);
@ -365,10 +369,6 @@ function getImports() {
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_jsval_eq = function(arg0, arg1) {
const ret = getObject(arg0) === getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_object_clone_ref = function(arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
@ -407,6 +407,14 @@ function getImports() {
imports.wbg.__wbg_modalhidebyid_6dd8ae230b194210 = function(arg0, arg1) {
modal_hide_by_id(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_cachekey_b61393159c57fd7b = function(arg0, arg1) {
const ret = getObject(arg1).__yew_subtree_cache_key;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
@ -423,14 +431,6 @@ function getImports() {
imports.wbg.__wbg_setcachekey_80183b7cfc421143 = function(arg0, arg1) {
getObject(arg0).__yew_subtree_cache_key = arg1 >>> 0;
};
imports.wbg.__wbg_setlistenerid_3183aae8fa5840fb = function(arg0, arg1) {
getObject(arg0).__yew_listener_id = arg1 >>> 0;
};
imports.wbg.__wbg_listenerid_12315eee21527820 = function(arg0, arg1) {
const ret = getObject(arg1).__yew_listener_id;
getInt32Memory0()[arg0 / 4 + 1] = isLikeNone(ret) ? 0 : ret;
getInt32Memory0()[arg0 / 4 + 0] = !isLikeNone(ret);
};
imports.wbg.__wbg_new_abda76e883ba8a5f = function() {
const ret = new Error();
return addHeapObject(ret);
@ -567,31 +567,36 @@ function getImports() {
imports.wbg.__wbg_pushState_429f091d389407b4 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
}, arguments) };
imports.wbg.__wbg_instanceof_Response_fb3a4df648c1859b = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Response;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_status_d483a4ac847f380a = function(arg0) {
const ret = getObject(arg0).status;
return ret;
};
imports.wbg.__wbg_headers_6093927dc359903e = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_json_b9414eb18cb751d0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).json();
return addHeapObject(ret);
imports.wbg.__wbg_href_bb86bb94d1c6861b = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_text_f61464d781b099f0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).text();
return addHeapObject(ret);
imports.wbg.__wbg_pathname_7b2f7ba43a0fdd6e = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_search_23418f9752ba7ba6 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_hash_03f283be75af7a56 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_replace_eacc9fc818c999c1 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_log_7bb108d119bafbc1 = function(arg0) {
console.log(getObject(arg0));
@ -657,6 +662,50 @@ function getImports() {
imports.wbg.__wbg_set_a5d34c36a1a4ebd1 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_new_13cda049130ea17b = function() { return handleError(function () {
const ret = new URLSearchParams();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_73f794d491a0e44f = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_f021903057d23f5e = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_href_e7e4e286ccd6b390 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_instanceof_HtmlInputElement_5c9d54338207f061 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_checked_44c09d0c819e33ad = function(arg0) {
const ret = getObject(arg0).checked;
return ret;
};
imports.wbg.__wbg_setchecked_cbd6f423c4deba69 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_1f2c9e357d18d3ea = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_a706abe70dab1b65 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_href_337141180d3d9dc0 = function(arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
@ -699,62 +748,6 @@ function getImports() {
const ret = new URL(getStringFromWasm0(arg0, arg1), getStringFromWasm0(arg2, arg3));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_new_13cda049130ea17b = function() { return handleError(function () {
const ret = new URLSearchParams();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_add_73f794d491a0e44f = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).add(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_remove_f021903057d23f5e = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).remove(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_instanceof_HtmlInputElement_5c9d54338207f061 = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof HTMLInputElement;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_checked_44c09d0c819e33ad = function(arg0) {
const ret = getObject(arg0).checked;
return ret;
};
imports.wbg.__wbg_setchecked_cbd6f423c4deba69 = function(arg0, arg1) {
getObject(arg0).checked = arg1 !== 0;
};
imports.wbg.__wbg_value_1f2c9e357d18d3ea = function(arg0, arg1) {
const ret = getObject(arg1).value;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_setvalue_a706abe70dab1b65 = function(arg0, arg1, arg2) {
getObject(arg0).value = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_url_bd2775644ef804ec = function(arg0, arg1) {
const ret = getObject(arg1).url;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_headers_ab5251d2727ac41e = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithstr_533a2b691cd87b92 = function() { return handleError(function (arg0, arg1) {
const ret = new Request(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newwithstrandinit_c45f0dc6da26fd03 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Element_cb847a3fc7b1b1a4 = function(arg0) {
let result;
try {
@ -817,13 +810,33 @@ function getImports() {
const ret = getObject(arg0).get(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_href_e7e4e286ccd6b390 = function(arg0, arg1) {
const ret = getObject(arg1).href;
imports.wbg.__wbg_url_bd2775644ef804ec = function(arg0, arg1) {
const ret = getObject(arg1).url;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
};
imports.wbg.__wbg_headers_ab5251d2727ac41e = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_newwithstr_533a2b691cd87b92 = function() { return handleError(function (arg0, arg1) {
const ret = new Request(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_newwithstrandinit_c45f0dc6da26fd03 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), getObject(arg2));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_credentials_09ff63679d98fa1a = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_getClientExtensionResults_cfd034586dac1bf4 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_instanceof_Event_4637acc4ed1080b8 = function(arg0) {
let result;
try {
@ -853,6 +866,12 @@ function getImports() {
imports.wbg.__wbg_preventDefault_16b2170b12f56317 = function(arg0) {
getObject(arg0).preventDefault();
};
imports.wbg.__wbg_addEventListener_cf5b03cd29763277 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_b25f5db74f767386 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_newwithform_3bddcbb6564115e4 = function() { return handleError(function (arg0) {
const ret = new FormData(getObject(arg0));
return addHeapObject(ret);
@ -861,51 +880,6 @@ function getImports() {
const ret = getObject(arg0).get(getStringFromWasm0(arg1, arg2));
return addHeapObject(ret);
};
imports.wbg.__wbg_credentials_09ff63679d98fa1a = function(arg0) {
const ret = getObject(arg0).credentials;
return addHeapObject(ret);
};
imports.wbg.__wbg_getClientExtensionResults_cfd034586dac1bf4 = function(arg0) {
const ret = getObject(arg0).getClientExtensionResults();
return addHeapObject(ret);
};
imports.wbg.__wbg_addEventListener_cf5b03cd29763277 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).addEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), getObject(arg4));
}, arguments) };
imports.wbg.__wbg_removeEventListener_b25f5db74f767386 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
getObject(arg0).removeEventListener(getStringFromWasm0(arg1, arg2), getObject(arg3), arg4 !== 0);
}, arguments) };
imports.wbg.__wbg_href_bb86bb94d1c6861b = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).href;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_pathname_7b2f7ba43a0fdd6e = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).pathname;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_search_23418f9752ba7ba6 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).search;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_hash_03f283be75af7a56 = function() { return handleError(function (arg0, arg1) {
const ret = getObject(arg1).hash;
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len0;
getInt32Memory0()[arg0 / 4 + 0] = ptr0;
}, arguments) };
imports.wbg.__wbg_replace_eacc9fc818c999c1 = function() { return handleError(function (arg0, arg1, arg2) {
getObject(arg0).replace(getStringFromWasm0(arg1, arg2));
}, arguments) };
imports.wbg.__wbg_parentNode_e81e6d5dc2fc35b0 = function(arg0) {
const ret = getObject(arg0).parentNode;
return isLikeNone(ret) ? 0 : addHeapObject(ret);
@ -944,6 +918,32 @@ function getImports() {
const ret = getObject(arg0).removeChild(getObject(arg1));
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_instanceof_Response_fb3a4df648c1859b = function(arg0) {
let result;
try {
result = getObject(arg0) instanceof Response;
} catch {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_status_d483a4ac847f380a = function(arg0) {
const ret = getObject(arg0).status;
return ret;
};
imports.wbg.__wbg_headers_6093927dc359903e = function(arg0) {
const ret = getObject(arg0).headers;
return addHeapObject(ret);
};
imports.wbg.__wbg_json_b9414eb18cb751d0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).json();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_text_f61464d781b099f0 = function() { return handleError(function (arg0) {
const ret = getObject(arg0).text();
return addHeapObject(ret);
}, arguments) };
imports.wbg.__wbg_get_27fe3dac1c4d0224 = function(arg0, arg1) {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
@ -1071,6 +1071,14 @@ function getImports() {
const ret = Number.isSafeInteger(getObject(arg0));
return ret;
};
imports.wbg.__wbg_new0_25059e40b1c02766 = function() {
const ret = new Date();
return addHeapObject(ret);
};
imports.wbg.__wbg_toISOString_8e31986cf23150ba = function(arg0) {
const ret = getObject(arg0).toISOString();
return addHeapObject(ret);
};
imports.wbg.__wbg_entries_4e1315b774245952 = function(arg0) {
const ret = Object.entries(getObject(arg0));
return addHeapObject(ret);
@ -1148,16 +1156,16 @@ function getImports() {
const ret = wasm.memory;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper4748 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1092, __wbg_adapter_48);
imports.wbg.__wbindgen_closure_wrapper4757 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1106, __wbg_adapter_48);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5602 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1422, __wbg_adapter_51);
imports.wbg.__wbindgen_closure_wrapper5652 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1432, __wbg_adapter_51);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_closure_wrapper5680 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1452, __wbg_adapter_54);
imports.wbg.__wbindgen_closure_wrapper5715 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 1458, __wbg_adapter_54);
return addHeapObject(ret);
};

View file

@ -63,6 +63,7 @@ pub enum State {
#[derive(PartialEq, Eq, Properties)]
pub struct ChangeUnixPasswordProps {
pub uat: UserAuthToken,
pub enabled: bool,
}
impl Component for ChangeUnixPassword {
@ -139,6 +140,7 @@ impl Component for ChangeUnixPassword {
};
let submit_enabled = self.pw_check == PwCheck::Valid;
let button_enabled = ctx.props().enabled;
let pw_val = self.pw_val.clone();
let pw_check_val = self.pw_check_val.clone();
@ -150,8 +152,9 @@ impl Component for ChangeUnixPassword {
html! {
<>
<button type="button" class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target={format!("#{}", crate::constants::ID_UNIX_PASSWORDCHANGE)}
disabled={ !button_enabled }
data-bs-toggle="modal"
data-bs-target={format!("#{}", crate::constants::ID_UNIX_PASSWORDCHANGE)}
>
{ "Update your Unix Password" }
</button>

View file

@ -21,10 +21,20 @@ use crate::{models, utils};
pub struct LoginApp {
inputvalue: String,
session_id: String,
state: LoginState,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum LoginWorkflow {
Login,
Reauth,
}
#[derive(PartialEq, Properties)]
pub struct LoginAppProps {
pub workflow: LoginWorkflow,
}
#[derive(PartialEq)]
enum TotpState {
Enabled,
@ -33,7 +43,8 @@ enum TotpState {
}
enum LoginState {
Init { enable: bool, remember_me: bool },
InitLogin { enable: bool, remember_me: bool },
InitReauth { enable: bool },
// Select between different cred types, either password (and MFA) or Passkey
Select(Vec<AuthMech>),
// The choices of authentication mechanism.
@ -60,7 +71,7 @@ pub enum LoginAppMsg {
TotpSubmit,
PasskeySubmit(PublicKeyCredential),
SecurityKeySubmit(PublicKeyCredential),
Start(String, AuthResponse),
Start(AuthResponse),
Next(AuthResponse),
Continue(usize),
Select(usize),
@ -112,15 +123,10 @@ impl LoginApp {
let headers = resp.headers();
if status == 200 {
let session_id = headers
.get("x-kanidm-auth-session-id")
.ok()
.flatten()
.unwrap_or_default();
let jsval = JsFuture::from(resp.json()?).await?;
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
.expect_throw("Invalid response type - auth_init::AuthResponse");
Ok(LoginAppMsg::Start(session_id, state))
Ok(LoginAppMsg::Start(state))
} else if status == 404 {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
@ -137,10 +143,55 @@ impl LoginApp {
}
}
async fn auth_step(
authreq: AuthRequest,
session_id: String,
) -> Result<LoginAppMsg, FetchError> {
async fn reauth_init() -> Result<LoginAppMsg, FetchError> {
let issue = AuthIssueSession::Cookie;
let authreq_jsvalue = serde_json::to_string(&issue)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
let mut opts = RequestInit::new();
opts.method("POST");
opts.mode(RequestMode::SameOrigin);
opts.credentials(RequestCredentials::SameOrigin);
opts.body(Some(&authreq_jsvalue));
let request = Request::new_with_str_and_init("/v1/reauth", &opts)?;
request
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
let resp: Response = resp_value
.dyn_into()
.expect_throw("Invalid response type - reauth_init::Response");
let status = resp.status();
let headers = resp.headers();
if status == 200 {
let jsval = JsFuture::from(resp.json()?).await?;
let state: AuthResponse = serde_wasm_bindgen::from_value(jsval)
.expect_throw("Invalid response type - auth_init::AuthResponse");
Ok(LoginAppMsg::Next(state))
} else if status == 404 {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
console::error!(format!(
"User not found: {:?}. Operation ID: {:?}",
text, kopid
));
Ok(LoginAppMsg::UnknownUser)
} else {
let kopid = headers.get("x-kanidm-opid").ok().flatten();
let text = JsFuture::from(resp.text()?).await?;
let emsg = text.as_string().unwrap_or_default();
Ok(LoginAppMsg::Error { emsg, kopid })
}
}
async fn auth_step(authreq: AuthRequest) -> Result<LoginAppMsg, FetchError> {
let authreq_jsvalue = serde_json::to_string(&authreq)
.map(|s| JsValue::from(&s))
.expect_throw("Failed to serialise authreq");
@ -157,10 +208,6 @@ impl LoginApp {
.headers()
.set("content-type", "application/json")
.expect_throw("failed to set content-type header");
request
.headers()
.set("x-kanidm-auth-session-id", session_id.as_str())
.expect_throw("failed to set x-kanidm-auth-session-id header");
let window = utils::window();
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
@ -246,7 +293,7 @@ impl LoginApp {
fn view_state(&self, ctx: &Context<Self>) -> Html {
let inputvalue = self.inputvalue.clone();
match &self.state {
LoginState::Init {
LoginState::InitLogin {
enable,
remember_me,
} => {
@ -300,6 +347,31 @@ impl LoginApp {
</>
}
}
LoginState::InitReauth { enable } => {
html! {
<>
<div class="container">
<p>{ "Reauthenticate to continue" }</p>
<form
onsubmit={ ctx.link().callback(|e: SubmitEvent| {
#[cfg(debug_assertions)]
console::debug!("login::view_state -> Init - prevent_default()".to_string());
e.prevent_default();
LoginAppMsg::Begin
} ) }
>
<div class={CLASS_DIV_LOGIN_BUTTON}>
<button
type="submit"
class={CLASS_BUTTON_DARK}
disabled={ !enable }
>{" Begin "}</button>
</div>
</form>
</div>
</>
}
}
// Selecting between password (and MFA) or Passkey
LoginState::Select(mechs) => {
html! {
@ -570,47 +642,57 @@ impl LoginApp {
impl Component for LoginApp {
type Message = LoginAppMsg;
type Properties = ();
type Properties = LoginAppProps;
fn create(_ctx: &Context<Self>) -> Self {
fn create(ctx: &Context<Self>) -> Self {
#[cfg(debug_assertions)]
console::debug!("create".to_string());
// Assume we are here for a good reason.
// -- clear the bearer to prevent conflict
models::clear_bearer_token();
// Do we have a login hint?
let (inputvalue, remember_me) = models::pop_login_hint()
.map(|user| (user, false))
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default();
#[cfg(debug_assertions)]
{
let document = utils::document();
let html_document = document
.dyn_into::<web_sys::HtmlDocument>()
.expect_throw("failed to dyn cast to htmldocument");
let cookie = html_document
.cookie()
.expect_throw("failed to access page cookies");
console::debug!("cookies".to_string());
console::debug!(cookie);
}
// Clean any cookies.
// TODO: actually check that it's cleaning the cookies.
let mut inputvalue = "".to_string();
let workflow = ctx.props().workflow;
let state = match workflow {
LoginWorkflow::Login => {
// Assume we are here for a good reason.
// -- clear the bearer to prevent conflict
models::clear_bearer_token();
// Do we have a login hint?
let (model_iv, remember_me) = models::pop_login_hint()
.map(|user| (user, false))
.or_else(|| models::get_login_remember_me().map(|user| (user, true)))
.unwrap_or_default();
let state = LoginState::Init {
enable: true,
remember_me,
inputvalue = model_iv;
#[cfg(debug_assertions)]
{
let document = utils::document();
let html_document = document
.dyn_into::<web_sys::HtmlDocument>()
.expect_throw("failed to dyn cast to htmldocument");
let cookie = html_document
.cookie()
.expect_throw("failed to access page cookies");
console::debug!("cookies".to_string());
console::debug!(cookie);
}
// Clean any cookies.
// TODO: actually check that it's cleaning the cookies.
LoginState::InitLogin {
enable: true,
remember_me,
}
}
LoginWorkflow::Reauth => {
// Unlike login, don't clear tokens or cookies - these are needed during the operation
// to actually start the reauth as the same user.
LoginState::InitReauth { enable: true }
}
};
add_body_form_classes!();
LoginApp {
inputvalue,
session_id: "".to_string(),
state,
}
LoginApp { inputvalue, state }
}
fn changed(&mut self, _ctx: &Context<Self>, _props: &Self::Properties) -> bool {
@ -625,52 +707,73 @@ impl Component for LoginApp {
}
LoginAppMsg::Restart => {
// Clear any leftover input. Reset to the remembered username if any.
let (inputvalue, remember_me) = models::get_login_remember_me()
.map(|user| (user, true))
.unwrap_or_default();
match ctx.props().workflow {
LoginWorkflow::Login => {
let (inputvalue, remember_me) = models::get_login_remember_me()
.map(|user| (user, true))
.unwrap_or_default();
self.inputvalue = inputvalue;
self.session_id = "".to_string();
self.state = LoginState::Init {
enable: true,
remember_me,
};
self.inputvalue = inputvalue;
self.state = LoginState::InitLogin {
enable: true,
remember_me,
};
}
LoginWorkflow::Reauth => {
self.inputvalue = "".to_string();
self.state = LoginState::InitReauth { enable: true };
}
}
true
}
LoginAppMsg::Begin => {
#[cfg(debug_assertions)]
console::debug!(format!("begin -> {:?}", self.inputvalue));
// Disable the button?
let username = self.inputvalue.clone();
// If the remember-me was checked, stash it here.
// If it was false, clear existing data.
match ctx.props().workflow {
LoginWorkflow::Login => {
#[cfg(debug_assertions)]
console::debug!(format!("begin -> {:?}", self.inputvalue));
// Disable the button?
let username = self.inputvalue.clone();
// If the remember-me was checked, stash it here.
// If it was false, clear existing data.
let remember_me = if utils::get_inputelement_by_id("remember_me_check")
.map(|element| element.checked())
.unwrap_or(false)
{
models::push_login_remember_me(username.clone());
true
} else {
models::pop_login_remember_me();
false
};
let remember_me = if utils::get_inputelement_by_id("remember_me_check")
.map(|element| element.checked())
.unwrap_or(false)
{
models::push_login_remember_me(username.clone());
true
} else {
models::pop_login_remember_me();
false
};
#[cfg(debug_assertions)]
console::debug!(format!("begin remember_me -> {:?}", remember_me));
#[cfg(debug_assertions)]
console::debug!(format!("begin remember_me -> {:?}", remember_me));
ctx.link().send_future(async {
match Self::auth_init(username).await {
Ok(v) => v,
Err(v) => v.into(),
ctx.link().send_future(async {
match Self::auth_init(username).await {
Ok(v) => v,
Err(v) => v.into(),
}
});
self.state = LoginState::InitLogin {
enable: false,
remember_me,
};
}
});
self.state = LoginState::Init {
enable: false,
remember_me,
};
LoginWorkflow::Reauth => {
ctx.link().send_future(async {
match Self::reauth_init().await {
Ok(v) => v,
Err(v) => v.into(),
}
});
self.inputvalue = "".to_string();
self.state = LoginState::InitReauth { enable: false };
}
}
true
}
LoginAppMsg::PasswordSubmit => {
@ -681,9 +784,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Password(self.inputvalue.clone())),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -700,9 +802,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::BackupCode(self.inputvalue.clone())),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -721,9 +822,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Totp(totp)),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -745,9 +845,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::SecurityKey(Box::new(resp))),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -761,9 +860,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Cred(AuthCredential::Passkey(Box::new(resp))),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -771,23 +869,21 @@ impl Component for LoginApp {
// Do not submit here, we need to wait for the next ui transition.
false
}
LoginAppMsg::Start(session_id, resp) => {
LoginAppMsg::Start(resp) => {
// Clear any leftover input
self.inputvalue = "".to_string();
#[cfg(debug_assertions)]
console::debug!(format!("start -> {:?} : {:?}", resp, session_id));
console::debug!(format!("start -> {:?}", resp));
match resp.state {
AuthState::Choose(mut mechs) => {
self.session_id = session_id;
if mechs.len() == 1 {
// If it's only one mech, just submit that.
let mech = mechs.pop().expect_throw("Memory corruption occurred");
let authreq = AuthRequest {
step: AuthStep::Begin(mech),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -826,9 +922,8 @@ impl Component for LoginApp {
let authreq = AuthRequest {
step: AuthStep::Begin(mech.clone()),
};
let session_id = self.session_id.clone();
ctx.link().send_future(async {
match Self::auth_step(authreq, session_id).await {
match Self::auth_step(authreq).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -1016,6 +1111,7 @@ impl Component for LoginApp {
<center>
<img src="/pkg/img/logo-square.svg" alt="Kanidm" class="kanidm_logo"/>
// TODO: replace this with a call to domain info
// More likely we should have this passed in from the props when we start.
<h3>{ "Kanidm" }</h3>
</center>
{ self.view_state(ctx) }

View file

@ -12,7 +12,7 @@ use yew::prelude::*;
use yew_router::prelude::*;
use crate::credential::reset::CredentialResetApp;
use crate::login::LoginApp;
use crate::login::{LoginApp, LoginWorkflow};
use crate::oauth2::Oauth2App;
use crate::views::{ViewRoute, ViewsApp};
@ -28,6 +28,9 @@ pub enum Route {
#[at("/ui/login")]
Login,
#[at("/ui/reauth")]
Reauth,
#[at("/ui/oauth2")]
Oauth2,
@ -57,7 +60,9 @@ fn switch(route: Route) -> Html {
#[allow(clippy::let_unit_value)]
Route::Landing => html! { <Landing /> },
#[allow(clippy::let_unit_value)]
Route::Login => html! { <LoginApp /> },
Route::Login => html! { <LoginApp workflow={ LoginWorkflow::Login } /> },
#[allow(clippy::let_unit_value)]
Route::Reauth => html! { <LoginApp workflow={ LoginWorkflow::Reauth } /> },
#[allow(clippy::let_unit_value)]
Route::Oauth2 => html! { <Oauth2App /> },
#[allow(clippy::let_unit_value)]

View file

@ -27,6 +27,7 @@ pub enum Msg {
emsg: String,
kopid: Option<String>,
},
RequestReauth,
}
impl From<FetchError> for Msg {
@ -76,7 +77,7 @@ impl Component for SecurityApp {
let id = uat.uuid.to_string();
ctx.link().send_future(async {
match Self::fetch_token_valid(id).await {
match Self::request_credential_update(id).await {
Ok(v) => v,
Err(v) => v.into(),
}
@ -98,6 +99,18 @@ impl Component for SecurityApp {
// the state.
false
}
Msg::RequestReauth => {
models::push_return_location(models::Location::Views(ViewRoute::Security));
ctx.link()
.navigator()
.expect_throw("failed to read history")
.push(&Route::Reauth);
// No need to redraw, or reset state, since this redirect will destroy
// the state.
false
}
Msg::Error { emsg, kopid } => {
self.state = State::Error { emsg, kopid };
true
@ -111,8 +124,20 @@ impl Component for SecurityApp {
}
fn view(&self, ctx: &Context<Self>) -> Html {
let uat = ctx.props().current_user_uat.clone();
let jsdate = js_sys::Date::new_0();
let isotime: String = jsdate.to_iso_string().into();
// TODO: Actually check the time of expiry on the uat and have a timer set that
// re-locks things nicely.
let time = time::OffsetDateTime::parse(&isotime, time::Format::Rfc3339)
.map(|odt| odt + time::Duration::new(60, 0))
.expect_throw("Unable to process time stamp");
let is_priv_able = uat.purpose_readwrite_active(time);
let submit_enabled = match self.state {
State::Init | State::Error { .. } => true,
State::Init | State::Error { .. } => is_priv_able,
State::Waiting => false,
};
@ -132,7 +157,31 @@ impl Component for SecurityApp {
_ => html! { <></> },
};
let uat = ctx.props().current_user_uat.clone();
let unlock = if is_priv_able {
html! {
<div>
<button type="button" class="btn btn-primary"
disabled=true
>
{ "Security Settings Unlocked 🔓" }
</button>
</div>
}
} else {
html! {
<div>
<button type="button" class="btn btn-primary"
onclick={
ctx.link().callback(|_e| {
Msg::RequestReauth
})
}
>
{ "Unlock Security Settings 🔒" }
</button>
</div>
}
};
html! {
<>
@ -140,12 +189,13 @@ impl Component for SecurityApp {
<h2>{ "Security" }</h2>
</div>
{ flash }
{ unlock }
<hr/>
<div>
<p>
<button type="button" class="btn btn-primary"
disabled={ !submit_enabled }
onclick={
// TODO: figure out if we need the e here? :)
ctx.link().callback(|_e| {
Msg::RequestCredentialUpdate
})
@ -159,7 +209,7 @@ impl Component for SecurityApp {
if uat.ui_hints.contains(&UiHint::PosixAccount) {
<div>
<p>
<ChangeUnixPassword uat={ uat }/>
<ChangeUnixPassword uat={ uat } enabled={ is_priv_able } />
</p>
</div>
}
@ -169,7 +219,7 @@ impl Component for SecurityApp {
}
impl SecurityApp {
async fn fetch_token_valid(id: String) -> Result<Msg, FetchError> {
async fn request_credential_update(id: String) -> Result<Msg, FetchError> {
let mut opts = RequestInit::new();
opts.method("GET");
opts.mode(RequestMode::SameOrigin);

View file

@ -3,9 +3,9 @@ ARG BASE_IMAGE=opensuse/tumbleweed:latest
FROM ${BASE_IMAGE} AS repos
RUN \
--mount=type=cache,id=zypp,target=/var/cache/zypp \
zypper mr -k repo-oss && \
zypper mr -k repo-non-oss && \
zypper mr -k repo-update && \
zypper mr -k repo-oss; \
zypper mr -k repo-non-oss; \
zypper mr -k repo-update; \
zypper dup -y
FROM repos AS builder

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::PwBadlistOpt;
use futures_concurrency::prelude::*;
// use std::thread;
@ -19,7 +20,7 @@ impl PwBadlistOpt {
pub async fn exec(&self) {
match self {
PwBadlistOpt::Show(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.system_password_badlist_get().await {
Ok(list) => {
for i in list {
@ -121,7 +122,7 @@ impl PwBadlistOpt {
println!("{}", pw);
}
} else {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.system_password_badlist_append(filt_pwset).await {
Ok(_) => println!("Success"),
Err(e) => eprintln!("{:?}", e),
@ -129,7 +130,7 @@ impl PwBadlistOpt {
}
} // End Upload
PwBadlistOpt::Remove { copt, paths } => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
let mut pwset: Vec<String> = Vec::new();

View file

@ -10,6 +10,11 @@ use kanidm_proto::v1::UserAuthToken;
use crate::session::read_tokens;
use crate::CommonOpt;
pub enum OpType {
Read,
Write,
}
impl CommonOpt {
pub fn to_unauth_client(&self) -> KanidmClient {
let config_path: String = shellexpand::tilde(DEFAULT_CLIENT_CONFIG_PATH_HOME).into_owned();
@ -68,7 +73,7 @@ impl CommonOpt {
})
}
pub async fn to_client(&self) -> KanidmClient {
pub async fn to_client(&self, optype: OpType) -> KanidmClient {
let client = self.to_unauth_client();
// Read the token file.
let tokens = match read_tokens() {
@ -133,8 +138,9 @@ impl CommonOpt {
.map(|jws: Jws<UserAuthToken>| jws.into_inner())
{
Ok(uat) => {
let now_utc = time::OffsetDateTime::now_utc();
if let Some(exp) = uat.expiry {
if time::OffsetDateTime::now_utc() >= exp {
if now_utc >= exp {
error!(
"Session has expired for {} - you may need to login again.",
uat.spn
@ -142,6 +148,20 @@ impl CommonOpt {
std::process::exit(1);
}
}
// Check what we are doing based on op.
match optype {
OpType::Read => {}
OpType::Write => {
if !uat.purpose_readwrite_active(now_utc + time::Duration::new(20, 0)) {
error!(
"Privileges have expired for {} - you need to re-authenticate again.",
uat.spn
);
std::process::exit(1);
}
}
}
}
Err(e) => {
error!("Unable to read token for requested user - you may need to login again.");

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::DomainOpt;
impl DomainOpt {
@ -15,7 +16,7 @@ impl DomainOpt {
"Attempting to set the domain's display name to: {:?}",
opt.new_display_name
);
let client = opt.copt.to_client().await;
let client = opt.copt.to_client(OpType::Write).await;
match client
.idm_domain_set_display_name(&opt.new_display_name)
.await
@ -25,14 +26,14 @@ impl DomainOpt {
}
}
DomainOpt::Show(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_domain_get().await {
Ok(e) => println!("{}", e),
Err(e) => error!("Error -> {:?}", e),
}
}
DomainOpt::ResetTokenKey(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.idm_domain_reset_token_key().await {
Ok(_) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::{GroupOpt, GroupPosix};
impl GroupOpt {
@ -22,7 +23,7 @@ impl GroupOpt {
pub async fn exec(&self) {
match self {
GroupOpt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_group_list().await {
Ok(r) => r.iter().for_each(|ent| match copt.output_mode.as_str() {
"json" => {
@ -34,7 +35,7 @@ impl GroupOpt {
}
}
GroupOpt::Get(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Read).await;
// idm_group_get
match client.idm_group_get(gcopt.name.as_str()).await {
Ok(Some(e)) => match gcopt.copt.output_mode.as_str() {
@ -48,7 +49,7 @@ impl GroupOpt {
}
}
GroupOpt::Create(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
match client.idm_group_create(gcopt.name.as_str()).await {
Err(err) => {
error!("Error -> {:?}", err)
@ -57,14 +58,14 @@ impl GroupOpt {
}
}
GroupOpt::Delete(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
match client.idm_group_delete(gcopt.name.as_str()).await {
Err(e) => error!("Error -> {:?}", e),
Ok(_) => println!("Successfully deleted group {}", gcopt.name.as_str()),
}
}
GroupOpt::PurgeMembers(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
match client.idm_group_purge_members(gcopt.name.as_str()).await {
Err(e) => error!("Error -> {:?}", e),
Ok(_) => println!(
@ -74,7 +75,7 @@ impl GroupOpt {
}
}
GroupOpt::ListMembers(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Read).await;
match client.idm_group_get_members(gcopt.name.as_str()).await {
Ok(Some(groups)) => groups.iter().for_each(|m| println!("{:?}", m)),
Ok(None) => warn!("No members in group {}", gcopt.name.as_str()),
@ -82,7 +83,7 @@ impl GroupOpt {
}
}
GroupOpt::AddMembers(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
let new_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
match client
@ -99,7 +100,7 @@ impl GroupOpt {
}
GroupOpt::RemoveMembers(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
let remove_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
match client
@ -112,7 +113,7 @@ impl GroupOpt {
}
GroupOpt::SetMembers(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
let new_members: Vec<&str> = gcopt.members.iter().map(String::as_str).collect();
match client
@ -125,14 +126,14 @@ impl GroupOpt {
}
GroupOpt::Posix { commands } => match commands {
GroupPosix::Show(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Read).await;
match client.idm_group_unix_token_get(gcopt.name.as_str()).await {
Ok(token) => println!("{}", token),
Err(e) => error!("Error -> {:?}", e),
}
}
GroupPosix::Set(gcopt) => {
let client = gcopt.copt.to_client().await;
let client = gcopt.copt.to_client(OpType::Write).await;
match client
.idm_group_unix_extend(gcopt.name.as_str(), gcopt.gidnumber)
.await

View file

@ -14,8 +14,8 @@
#[macro_use]
extern crate tracing;
use crate::common::OpType;
use std::path::PathBuf;
use uuid::Uuid;
include!("../opt/kanidm.rs");
@ -43,7 +43,7 @@ impl SelfOpt {
pub async fn exec(&self) {
match self {
SelfOpt::Whoami(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.whoami().await {
Ok(o_ent) => {
@ -89,6 +89,7 @@ impl KanidmClientOpt {
match self {
KanidmClientOpt::Raw { commands } => commands.debug(),
KanidmClientOpt::Login(lopt) => lopt.debug(),
KanidmClientOpt::Reauth(lopt) => lopt.debug(),
KanidmClientOpt::Logout(lopt) => lopt.debug(),
KanidmClientOpt::Session { commands } => commands.debug(),
KanidmClientOpt::CSelf { commands } => commands.debug(),
@ -108,6 +109,7 @@ impl KanidmClientOpt {
match self {
KanidmClientOpt::Raw { commands } => commands.exec().await,
KanidmClientOpt::Login(lopt) => lopt.exec().await,
KanidmClientOpt::Reauth(lopt) => lopt.exec().await,
KanidmClientOpt::Logout(lopt) => lopt.exec().await,
KanidmClientOpt::Session { commands } => commands.exec().await,
KanidmClientOpt::CSelf { commands } => commands.exec().await,

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::Oauth2Opt;
impl Oauth2Opt {
@ -28,14 +29,14 @@ impl Oauth2Opt {
pub async fn exec(&self) {
match self {
Oauth2Opt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_oauth2_rs_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
Oauth2Opt::Get(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Read).await;
match client.idm_oauth2_rs_get(nopt.name.as_str()).await {
Ok(Some(e)) => println!("{}", e),
Ok(None) => println!("No matching entries"),
@ -43,7 +44,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::CreateBasic(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Read).await;
match client
.idm_oauth2_rs_basic_create(
cbopt.nopt.name.as_str(),
@ -57,7 +58,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::UpdateScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update_scope_map(
cbopt.nopt.name.as_str(),
@ -71,7 +72,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::DeleteScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_delete_scope_map(cbopt.nopt.name.as_str(), cbopt.group.as_str())
.await
@ -81,7 +82,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::UpdateSupScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update_sup_scope_map(
cbopt.nopt.name.as_str(),
@ -95,7 +96,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::DeleteSupScopeMap(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_delete_sup_scope_map(
cbopt.nopt.name.as_str(),
@ -108,7 +109,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::ResetSecrets(cbopt) => {
let client = cbopt.copt.to_client().await;
let client = cbopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update(
cbopt.name.as_str(),
@ -127,7 +128,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::ShowBasicSecret(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Read).await;
match client
.idm_oauth2_rs_get_basic_secret(nopt.name.as_str())
.await
@ -143,14 +144,14 @@ impl Oauth2Opt {
}
}
Oauth2Opt::Delete(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client.idm_oauth2_rs_delete(nopt.name.as_str()).await {
Ok(_) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
Oauth2Opt::SetDisplayname(cbopt) => {
let client = cbopt.nopt.copt.to_client().await;
let client = cbopt.nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update(
cbopt.nopt.name.as_str(),
@ -169,7 +170,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::SetName { nopt, name } => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update(
nopt.name.as_str(),
@ -188,7 +189,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::SetLandingUrl { nopt, url } => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_update(
nopt.name.as_str(),
@ -207,21 +208,21 @@ impl Oauth2Opt {
}
}
Oauth2Opt::EnablePkce(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client.idm_oauth2_rs_enable_pkce(nopt.name.as_str()).await {
Ok(_) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
Oauth2Opt::DisablePkce(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client.idm_oauth2_rs_disable_pkce(nopt.name.as_str()).await {
Ok(_) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
Oauth2Opt::EnableLegacyCrypto(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_enable_legacy_crypto(nopt.name.as_str())
.await
@ -231,7 +232,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::DisableLegacyCrypto(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_disable_legacy_crypto(nopt.name.as_str())
.await
@ -241,7 +242,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::PreferShortUsername(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_prefer_short_username(nopt.name.as_str())
.await
@ -251,7 +252,7 @@ impl Oauth2Opt {
}
}
Oauth2Opt::PreferSPNUsername(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client
.idm_oauth2_rs_prefer_spn_username(nopt.name.as_str())
.await

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use std::fmt::{self, Debug};
use std::str::FromStr;
@ -63,7 +64,7 @@ impl PersonOpt {
PersonOpt::Credential { commands } => commands.exec().await,
PersonOpt::Radius { commands } => match commands {
AccountRadius::Show(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
let rcred = client
.idm_account_radius_credential_get(aopt.aopts.account_id.as_str())
@ -85,7 +86,7 @@ impl PersonOpt {
}
}
AccountRadius::Generate(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_account_radius_credential_regenerate(aopt.aopts.account_id.as_str())
.await
@ -94,7 +95,7 @@ impl PersonOpt {
}
}
AccountRadius::DeleteSecret(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
let mut modmessage = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: "radius account_delete".to_string(),
@ -125,7 +126,7 @@ impl PersonOpt {
}, // end PersonOpt::Radius
PersonOpt::Posix { commands } => match commands {
PersonPosix::Show(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
.await
@ -137,7 +138,7 @@ impl PersonOpt {
}
}
PersonPosix::Set(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_person_account_unix_extend(
aopt.aopts.account_id.as_str(),
@ -150,7 +151,7 @@ impl PersonOpt {
}
}
PersonPosix::SetPassword(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
let password = match password_prompt("Enter new posix (sudo) password: ") {
Some(v) => v,
None => {
@ -172,7 +173,7 @@ impl PersonOpt {
}, // end PersonOpt::Posix
PersonOpt::Session { commands } => match commands {
AccountUserAuthToken::Status(apo) => {
let client = apo.copt.to_client().await;
let client = apo.copt.to_client(OpType::Read).await;
match client
.idm_account_list_user_auth_token(apo.aopts.account_id.as_str())
.await
@ -196,7 +197,7 @@ impl PersonOpt {
copt,
session_id,
} => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_account_destroy_user_auth_token(aopts.account_id.as_str(), *session_id)
.await
@ -212,7 +213,7 @@ impl PersonOpt {
}, // End PersonOpt::Session
PersonOpt::Ssh { commands } => match commands {
AccountSsh::List(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str())
@ -225,7 +226,7 @@ impl PersonOpt {
}
}
AccountSsh::Add(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_person_account_post_ssh_pubkey(
aopt.aopts.account_id.as_str(),
@ -238,7 +239,7 @@ impl PersonOpt {
}
}
AccountSsh::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_person_account_delete_ssh_pubkey(
aopt.aopts.account_id.as_str(),
@ -251,14 +252,14 @@ impl PersonOpt {
}
}, // end PersonOpt::Ssh
PersonOpt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_person_account_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
PersonOpt::Update(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
match client
.idm_person_account_update(
aopt.aopts.account_id.as_str(),
@ -274,7 +275,7 @@ impl PersonOpt {
}
}
PersonOpt::Get(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
match client
.idm_person_account_get(aopt.aopts.account_id.as_str())
.await
@ -285,7 +286,7 @@ impl PersonOpt {
}
}
PersonOpt::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
let mut modmessage = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: "account delete".to_string(),
@ -314,7 +315,7 @@ impl PersonOpt {
};
}
PersonOpt::Create(acopt) => {
let client = acopt.copt.to_client().await;
let client = acopt.copt.to_client(OpType::Write).await;
match client
.idm_person_account_create(
acopt.aopts.account_id.as_str(),
@ -324,7 +325,7 @@ impl PersonOpt {
{
Ok(_) => {
println!(
"Successfully created display_name=\"{}\" username={}>",
"Successfully created display_name=\"{}\" username={}",
acopt.display_name.as_str(),
acopt.aopts.account_id.as_str(),
)
@ -336,7 +337,7 @@ impl PersonOpt {
}
PersonOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Read).await;
println!("user: {}", ano.aopts.account_id.as_str());
let ex = match client
@ -400,7 +401,7 @@ impl PersonOpt {
}
}
AccountValidity::ExpireAt(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Write).await;
if matches!(ano.datetime.as_str(), "never" | "clear") {
// Unset the value
match client
@ -435,7 +436,7 @@ impl PersonOpt {
}
}
AccountValidity::BeginFrom(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Write).await;
if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
// Unset the value
match client
@ -488,7 +489,7 @@ impl AccountCredential {
pub async fn exec(&self) {
match self {
AccountCredential::Status(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_person_account_get_credential_status(aopt.aopts.account_id.as_str())
.await
@ -502,7 +503,7 @@ impl AccountCredential {
}
}
AccountCredential::Update(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
match client
.idm_account_credential_update_begin(aopt.aopts.account_id.as_str())
.await
@ -543,7 +544,7 @@ impl AccountCredential {
}
}
AccountCredential::CreateResetToken(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
// What's the client url?
match client

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use std::collections::BTreeMap;
use std::error::Error;
use std::fs::File;
@ -29,7 +30,7 @@ impl RawOpt {
pub async fn exec(&self) {
match self {
RawOpt::Search(sopt) => {
let client = sopt.commonopts.to_client().await;
let client = sopt.commonopts.to_client(OpType::Read).await;
let filter: Filter = match serde_json::from_str(sopt.filter.as_str()) {
Ok(f) => f,
@ -45,7 +46,7 @@ impl RawOpt {
}
}
RawOpt::Create(copt) => {
let client = copt.commonopts.to_client().await;
let client = copt.commonopts.to_client(OpType::Write).await;
// Read the file?
let r_entries: Vec<BTreeMap<String, Vec<String>>> = match read_file(&copt.file) {
Ok(r) => r,
@ -62,7 +63,7 @@ impl RawOpt {
}
}
RawOpt::Modify(mopt) => {
let client = mopt.commonopts.to_client().await;
let client = mopt.commonopts.to_client(OpType::Write).await;
// Read the file?
let filter: Filter = match serde_json::from_str(mopt.filter.as_str()) {
Ok(f) => f,
@ -86,7 +87,7 @@ impl RawOpt {
}
}
RawOpt::Delete(dopt) => {
let client = dopt.commonopts.to_client().await;
let client = dopt.commonopts.to_client(OpType::Write).await;
let filter: Filter = match serde_json::from_str(dopt.filter.as_str()) {
Ok(f) => f,
Err(e) => {

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::RecycleOpt;
impl RecycleOpt {
@ -12,7 +13,7 @@ impl RecycleOpt {
pub async fn exec(&self) {
match self {
RecycleOpt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.recycle_bin_list().await {
Ok(r) => r.iter().for_each(|e| println!("{}", e)),
Err(e) => {
@ -21,7 +22,7 @@ impl RecycleOpt {
}
}
RecycleOpt::Get(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
match client.recycle_bin_get(nopt.name.as_str()).await {
Ok(Some(e)) => println!("{}", e),
Ok(None) => println!("No matching entries"),
@ -31,7 +32,7 @@ impl RecycleOpt {
}
}
RecycleOpt::Revive(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Write).await;
if let Err(e) = client.recycle_bin_revive(nopt.name.as_str()).await {
error!("Error -> {:?}", e);
}

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
use time::OffsetDateTime;
@ -49,7 +50,7 @@ impl ServiceAccountOpt {
match self {
ServiceAccountOpt::Credential { commands } => match commands {
ServiceAccountCredential::Status(apo) => {
let client = apo.copt.to_client().await;
let client = apo.copt.to_client(OpType::Read).await;
match client
.idm_service_account_get_credential_status(apo.aopts.account_id.as_str())
.await
@ -63,7 +64,7 @@ impl ServiceAccountOpt {
}
}
ServiceAccountCredential::GeneratePw(apo) => {
let client = apo.copt.to_client().await;
let client = apo.copt.to_client(OpType::Write).await;
match client
.idm_service_account_generate_password(apo.aopts.account_id.as_str())
.await
@ -79,7 +80,7 @@ impl ServiceAccountOpt {
}, // End ServiceAccountOpt::Credential
ServiceAccountOpt::ApiToken { commands } => match commands {
ServiceAccountApiToken::Status(apo) => {
let client = apo.copt.to_client().await;
let client = apo.copt.to_client(OpType::Read).await;
match client
.idm_service_account_list_api_token(apo.aopts.account_id.as_str())
.await
@ -126,7 +127,7 @@ impl ServiceAccountOpt {
None
};
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_service_account_generate_api_token(
@ -164,7 +165,7 @@ impl ServiceAccountOpt {
copt,
token_id,
} => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_service_account_destroy_api_token(aopts.account_id.as_str(), *token_id)
.await
@ -180,7 +181,7 @@ impl ServiceAccountOpt {
}, // End ServiceAccountOpt::ApiToken
ServiceAccountOpt::Posix { commands } => match commands {
ServiceAccountPosix::Show(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_account_unix_token_get(aopt.aopts.account_id.as_str())
.await
@ -192,7 +193,7 @@ impl ServiceAccountOpt {
}
}
ServiceAccountPosix::Set(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_service_account_unix_extend(
aopt.aopts.account_id.as_str(),
@ -207,7 +208,7 @@ impl ServiceAccountOpt {
}, // end ServiceAccountOpt::Posix
ServiceAccountOpt::Session { commands } => match commands {
AccountUserAuthToken::Status(apo) => {
let client = apo.copt.to_client().await;
let client = apo.copt.to_client(OpType::Read).await;
match client
.idm_account_list_user_auth_token(apo.aopts.account_id.as_str())
.await
@ -231,7 +232,7 @@ impl ServiceAccountOpt {
copt,
session_id,
} => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_account_destroy_user_auth_token(aopts.account_id.as_str(), *session_id)
.await
@ -247,7 +248,7 @@ impl ServiceAccountOpt {
}, // End ServiceAccountOpt::Session
ServiceAccountOpt::Ssh { commands } => match commands {
AccountSsh::List(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_account_get_ssh_pubkeys(aopt.aopts.account_id.as_str())
@ -260,7 +261,7 @@ impl ServiceAccountOpt {
}
}
AccountSsh::Add(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_service_account_post_ssh_pubkey(
aopt.aopts.account_id.as_str(),
@ -273,7 +274,7 @@ impl ServiceAccountOpt {
}
}
AccountSsh::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_service_account_delete_ssh_pubkey(
aopt.aopts.account_id.as_str(),
@ -286,14 +287,14 @@ impl ServiceAccountOpt {
}
}, // end ServiceAccountOpt::Ssh
ServiceAccountOpt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_service_account_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
ServiceAccountOpt::Update(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
match client
.idm_service_account_update(
aopt.aopts.account_id.as_str(),
@ -308,7 +309,7 @@ impl ServiceAccountOpt {
}
}
ServiceAccountOpt::Get(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Read).await;
match client
.idm_service_account_get(aopt.aopts.account_id.as_str())
.await
@ -319,7 +320,7 @@ impl ServiceAccountOpt {
}
}
ServiceAccountOpt::Delete(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
let mut modmessage = AccountChangeMessage {
output_mode: ConsoleOutputMode::Text,
action: "account delete".to_string(),
@ -348,7 +349,7 @@ impl ServiceAccountOpt {
};
}
ServiceAccountOpt::Create(acopt) => {
let client = acopt.copt.to_client().await;
let client = acopt.copt.to_client(OpType::Write).await;
if let Err(e) = client
.idm_service_account_create(
acopt.aopts.account_id.as_str(),
@ -361,7 +362,7 @@ impl ServiceAccountOpt {
}
ServiceAccountOpt::Validity { commands } => match commands {
AccountValidity::Show(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Read).await;
println!("user: {}", ano.aopts.account_id.as_str());
let ex = match client
@ -425,7 +426,7 @@ impl ServiceAccountOpt {
}
}
AccountValidity::ExpireAt(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Write).await;
if matches!(ano.datetime.as_str(), "never" | "clear") {
// Unset the value
match client
@ -460,7 +461,7 @@ impl ServiceAccountOpt {
}
}
AccountValidity::BeginFrom(ano) => {
let client = ano.copt.to_client().await;
let client = ano.copt.to_client(OpType::Write).await;
if matches!(ano.datetime.as_str(), "any" | "clear" | "whenever") {
// Unset the value
match client
@ -497,7 +498,7 @@ impl ServiceAccountOpt {
}
}, // end ServiceAccountOpt::Validity
ServiceAccountOpt::IntoPerson(aopt) => {
let client = aopt.copt.to_client().await;
let client = aopt.copt.to_client(OpType::Write).await;
match client
.idm_service_account_into_person(aopt.aopts.account_id.as_str())
.await

View file

@ -1,10 +1,11 @@
use crate::common::OpType;
use std::collections::BTreeMap;
use std::fs::{create_dir, File};
use std::io::{self, BufReader, BufWriter, ErrorKind, Write};
use std::path::PathBuf;
use std::str::FromStr;
use compact_jwt::JwsUnverified;
use compact_jwt::{Jws, JwsUnverified};
use dialoguer::theme::ColorfulTheme;
use dialoguer::Select;
use kanidm_client::{ClientError, KanidmClient};
@ -15,7 +16,7 @@ use webauthn_authenticator_rs::prelude::RequestChallengeResponse;
use crate::common::prompt_for_username_get_username;
use crate::webauthn::get_authenticator;
use crate::{LoginOpt, LogoutOpt, SessionOpt};
use crate::{LoginOpt, LogoutOpt, ReauthOpt, SessionOpt};
static TOKEN_DIR: &str = "~/.cache";
static TOKEN_PATH: &str = "~/.cache/kanidm_tokens";
@ -137,107 +138,214 @@ fn get_index_choice_dialoguer(msg: &str, options: &[String]) -> usize {
selection
}
async fn do_password(
client: &mut KanidmClient,
password: &Option<String>,
) -> Result<AuthResponse, ClientError> {
let password = match password {
Some(password) => {
trace!("User provided password directly, don't need to prompt.");
password.to_owned()
}
None => rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
error!("Failed to create password prompt -- {:?}", e);
std::process::exit(1);
}),
};
client.auth_step_password(password.as_str()).await
}
async fn do_backup_code(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
print!("Enter Backup Code: ");
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
#[allow(clippy::unwrap_used)]
io::stdout().flush().unwrap();
let mut backup_code = String::new();
loop {
if let Err(e) = io::stdin().read_line(&mut backup_code) {
error!("Failed to read from stdin -> {:?}", e);
return Err(ClientError::SystemError);
};
if !backup_code.trim().is_empty() {
break;
};
}
client.auth_step_backup_code(backup_code.trim()).await
}
async fn do_totp(client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
let totp = loop {
print!("Enter TOTP: ");
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
if let Err(e) = io::stdout().flush() {
error!("Somehow we failed to flush stdout: {:?}", e);
};
let mut buffer = String::new();
if let Err(e) = io::stdin().read_line(&mut buffer) {
error!("Failed to read from stdin -> {:?}", e);
return Err(ClientError::SystemError);
};
let response = buffer.trim();
match response.parse::<u32>() {
Ok(i) => break i,
Err(_) => eprintln!("Invalid Number"),
};
};
client.auth_step_totp(totp).await
}
async fn do_passkey(
client: &mut KanidmClient,
pkr: RequestChallengeResponse,
) -> Result<AuthResponse, ClientError> {
let mut wa = get_authenticator();
println!("Your authenticator will now flash for you to interact with it.");
let auth = wa
.do_authentication(client.get_origin().clone(), pkr)
.map(Box::new)
.unwrap_or_else(|e| {
error!("Failed to interact with webauthn device. -- {:?}", e);
std::process::exit(1);
});
client.auth_step_passkey_complete(auth).await
}
async fn do_securitykey(
client: &mut KanidmClient,
pkr: RequestChallengeResponse,
) -> Result<AuthResponse, ClientError> {
let mut wa = get_authenticator();
println!("Your authenticator will now flash for you to interact with it.");
let auth = wa
.do_authentication(client.get_origin().clone(), pkr)
.map(Box::new)
.unwrap_or_else(|e| {
error!("Failed to interact with webauthn device. -- {:?}", e);
std::process::exit(1);
});
client.auth_step_securitykey_complete(auth).await
}
async fn process_auth_state(
mut allowed: Vec<AuthAllowed>,
mut client: KanidmClient,
maybe_password: &Option<String>,
) {
loop {
debug!("Allowed mechanisms -> {:?}", allowed);
// What auth can proceed?
let choice = match allowed.len() {
0 => {
error!("Error during authentication phase: Server offered no method to proceed");
std::process::exit(1);
}
1 =>
{
#[allow(clippy::expect_used)]
allowed
.get(0)
.expect("can not fail - bounds already checked.")
}
_ => {
let mut options = Vec::new();
for val in allowed.iter() {
options.push(val.to_string());
}
let msg = "Please choose what credential to provide:";
let selection = get_index_choice_dialoguer(msg, &options);
#[allow(clippy::expect_used)]
allowed
.get(selection)
.expect("can not fail - bounds already checked.")
}
};
let res = match choice {
AuthAllowed::Anonymous => client.auth_step_anonymous().await,
AuthAllowed::Password => do_password(&mut client, maybe_password).await,
AuthAllowed::BackupCode => do_backup_code(&mut client).await,
AuthAllowed::Totp => do_totp(&mut client).await,
AuthAllowed::Passkey(chal) => do_passkey(&mut client, chal.clone()).await,
AuthAllowed::SecurityKey(chal) => do_securitykey(&mut client, chal.clone()).await,
};
// Now update state.
let state = res
.unwrap_or_else(|e| {
error!("Error in authentication phase: {:?}", e);
std::process::exit(1);
})
.state;
// What auth state are we in?
allowed = match &state {
AuthState::Continue(allowed) => allowed.to_vec(),
AuthState::Success(_token) => break,
AuthState::Denied(reason) => {
error!("Authentication Denied: {:?}", reason);
std::process::exit(1);
}
_ => {
error!("Error in authentication phase: invalid authstate");
std::process::exit(1);
}
};
// Loop again.
}
// Read the current tokens
let mut tokens = read_tokens().unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
std::process::exit(1);
});
// Add our new one
let (username, tonk) = match client.get_token().await {
Some(t) => {
let tonk = match JwsUnverified::from_str(&t).and_then(|jwtu| {
jwtu.validate_embeded()
.map(|jws: Jws<UserAuthToken>| jws.into_inner())
}) {
Ok(uat) => uat,
Err(e) => {
error!("Unable to parse token - {:?}", e);
std::process::exit(1);
}
};
let username = tonk.name().to_string();
// Return the un-parsed token
(username, t)
}
None => {
error!("Error retrieving client session");
std::process::exit(1);
}
};
tokens.insert(username.clone(), tonk);
// write them out.
if write_tokens(&tokens).is_err() {
error!("Error persisting authentication token store");
std::process::exit(1);
};
// Success!
println!("Login Success for {}", username);
}
impl LoginOpt {
pub fn debug(&self) -> bool {
self.copt.debug
}
async fn do_password(
&self,
client: &mut KanidmClient,
password: &Option<String>,
) -> Result<AuthResponse, ClientError> {
let password = match password {
Some(password) => {
trace!("User provided password directly, don't need to prompt.");
password.to_owned()
}
None => rpassword::prompt_password("Enter password: ").unwrap_or_else(|e| {
error!("Failed to create password prompt -- {:?}", e);
std::process::exit(1);
}),
};
client.auth_step_password(password.as_str()).await
}
async fn do_backup_code(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
print!("Enter Backup Code: ");
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
#[allow(clippy::unwrap_used)]
io::stdout().flush().unwrap();
let mut backup_code = String::new();
loop {
if let Err(e) = io::stdin().read_line(&mut backup_code) {
error!("Failed to read from stdin -> {:?}", e);
return Err(ClientError::SystemError);
};
if !backup_code.trim().is_empty() {
break;
};
}
client.auth_step_backup_code(backup_code.trim()).await
}
async fn do_totp(&self, client: &mut KanidmClient) -> Result<AuthResponse, ClientError> {
let totp = loop {
print!("Enter TOTP: ");
// We flush stdout so it'll write the buffer to screen, continuing operation. Without it, the application halts.
if let Err(e) = io::stdout().flush() {
error!("Somehow we failed to flush stdout: {:?}", e);
};
let mut buffer = String::new();
if let Err(e) = io::stdin().read_line(&mut buffer) {
error!("Failed to read from stdin -> {:?}", e);
return Err(ClientError::SystemError);
};
let response = buffer.trim();
match response.parse::<u32>() {
Ok(i) => break i,
Err(_) => eprintln!("Invalid Number"),
};
};
client.auth_step_totp(totp).await
}
async fn do_passkey(
&self,
client: &mut KanidmClient,
pkr: RequestChallengeResponse,
) -> Result<AuthResponse, ClientError> {
let mut wa = get_authenticator();
println!("Your authenticator will now flash for you to interact with it.");
let auth = wa
.do_authentication(client.get_origin().clone(), pkr)
.map(Box::new)
.unwrap_or_else(|e| {
error!("Failed to interact with webauthn device. -- {:?}", e);
std::process::exit(1);
});
client.auth_step_passkey_complete(auth).await
}
async fn do_securitykey(
&self,
client: &mut KanidmClient,
pkr: RequestChallengeResponse,
) -> Result<AuthResponse, ClientError> {
let mut wa = get_authenticator();
println!("Your authenticator will now flash for you to interact with it.");
let auth = wa
.do_authentication(client.get_origin().clone(), pkr)
.map(Box::new)
.unwrap_or_else(|e| {
error!("Failed to interact with webauthn device. -- {:?}", e);
std::process::exit(1);
});
client.auth_step_securitykey_complete(auth).await
}
pub async fn exec(&self) {
let mut client = self.copt.to_unauth_client();
let client = self.copt.to_unauth_client();
let username = match self.copt.username.as_deref() {
Some(val) => val,
None => {
@ -284,7 +392,7 @@ impl LoginOpt {
}
};
let mut allowed = client
let allowed = client
.auth_step_begin((*mech).clone())
.await
.unwrap_or_else(|e| {
@ -293,95 +401,24 @@ impl LoginOpt {
});
// We now have the first auth state, so we can proceed until complete.
loop {
debug!("Allowed mechanisms -> {:?}", allowed);
// What auth can proceed?
let choice = match allowed.len() {
0 => {
error!(
"Error during authentication phase: Server offered no method to proceed"
);
std::process::exit(1);
}
1 =>
{
#[allow(clippy::expect_used)]
allowed
.get(0)
.expect("can not fail - bounds already checked.")
}
_ => {
let mut options = Vec::new();
for val in allowed.iter() {
options.push(val.to_string());
}
let msg = "Please choose what credential to provide:";
let selection = get_index_choice_dialoguer(msg, &options);
process_auth_state(allowed, client, &self.password).await;
}
}
#[allow(clippy::expect_used)]
allowed
.get(selection)
.expect("can not fail - bounds already checked.")
}
};
impl ReauthOpt {
pub fn debug(&self) -> bool {
self.copt.debug
}
let res = match choice {
AuthAllowed::Anonymous => client.auth_step_anonymous().await,
AuthAllowed::Password => self.do_password(&mut client, &self.password).await,
AuthAllowed::BackupCode => self.do_backup_code(&mut client).await,
AuthAllowed::Totp => self.do_totp(&mut client).await,
AuthAllowed::Passkey(chal) => self.do_passkey(&mut client, chal.clone()).await,
AuthAllowed::SecurityKey(chal) => {
self.do_securitykey(&mut client, chal.clone()).await
}
};
pub async fn exec(&self) {
let client = self.copt.to_client(OpType::Read).await;
// Now update state.
let state = res
.unwrap_or_else(|e| {
error!("Error in authentication phase: {:?}", e);
std::process::exit(1);
})
.state;
// What auth state are we in?
allowed = match &state {
AuthState::Continue(allowed) => allowed.to_vec(),
AuthState::Success(_token) => break,
AuthState::Denied(reason) => {
error!("Authentication Denied: {:?}", reason);
std::process::exit(1);
}
_ => {
error!("Error in authentication phase: invalid authstate");
std::process::exit(1);
}
};
// Loop again.
}
// Read the current tokens
let mut tokens = read_tokens().unwrap_or_else(|_| {
error!("Error retrieving authentication token store");
let allowed = client.reauth_begin().await.unwrap_or_else(|e| {
error!("Error during reauthentication begin phase: {:?}", e);
std::process::exit(1);
});
// Add our new one
match client.get_token().await {
Some(t) => tokens.insert(username.to_string(), t),
None => {
error!("Error retrieving client session");
std::process::exit(1);
}
};
// write them out.
if write_tokens(&tokens).is_err() {
error!("Error persisting authentication token store");
std::process::exit(1);
};
// Success!
println!("Login Success for {}", username);
process_auth_state(allowed, client, &None).await;
}
}
@ -405,7 +442,7 @@ impl LogoutOpt {
},
}
} else {
let client = self.copt.to_client().await;
let client = self.copt.to_client(OpType::Read).await;
let token = match client.get_token().await {
Some(t) => t,
None => {

View file

@ -1,3 +1,4 @@
use crate::common::OpType;
use crate::SynchOpt;
use dialoguer::Confirm;
@ -18,14 +19,14 @@ impl SynchOpt {
pub async fn exec(&self) {
match self {
SynchOpt::List(copt) => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Read).await;
match client.idm_sync_account_list().await {
Ok(r) => r.iter().for_each(|ent| println!("{}", ent)),
Err(e) => error!("Error -> {:?}", e),
}
}
SynchOpt::Get(nopt) => {
let client = nopt.copt.to_client().await;
let client = nopt.copt.to_client(OpType::Read).await;
match client.idm_sync_account_get(nopt.name.as_str()).await {
Ok(Some(e)) => println!("{}", e),
Ok(None) => println!("No matching entries"),
@ -37,7 +38,7 @@ impl SynchOpt {
copt,
description,
} => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_sync_account_create(account_id, description.as_deref())
.await
@ -51,7 +52,7 @@ impl SynchOpt {
label,
copt,
} => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client
.idm_sync_account_generate_token(account_id, label)
.await
@ -61,14 +62,14 @@ impl SynchOpt {
}
}
SynchOpt::DestroyToken { account_id, copt } => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.idm_sync_account_destroy_token(account_id).await {
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
}
}
SynchOpt::ForceRefresh { account_id, copt } => {
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.idm_sync_account_force_refresh(account_id).await {
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
@ -85,7 +86,7 @@ impl SynchOpt {
return;
}
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.idm_sync_account_finalise(account_id).await {
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),
@ -102,7 +103,7 @@ impl SynchOpt {
return;
}
let client = copt.to_client().await;
let client = copt.to_client(OpType::Write).await;
match client.idm_sync_account_terminate(account_id).await {
Ok(()) => println!("Success"),
Err(e) => error!("Error -> {:?}", e),

View file

@ -508,6 +508,12 @@ pub struct LoginOpt {
password: Option<String>,
}
#[derive(Debug, Args)]
pub struct ReauthOpt {
#[clap(flatten)]
copt: CommonOpt,
}
#[derive(Debug, Args)]
pub struct LogoutOpt {
#[clap(flatten)]
@ -854,6 +860,8 @@ pub enum SystemOpt {
pub enum KanidmClientOpt {
/// Login to an account to use with future cli operations
Login(LoginOpt),
/// Reauthenticate to access privileged functions of this account for a short period.
Reauth(ReauthOpt),
/// Logout of an active cli session
Logout(LogoutOpt),
/// Manage active cli sessions