mirror of
https://github.com/kanidm/kanidm.git
synced 2025-05-19 15:33:54 +02:00
1115 priv (reauth, sudo) mode (#1479)
This commit is contained in:
parent
2c2c3a6ddb
commit
4718f2dc6b
Cargo.lock
book/src
libs/client/src
proto/src
server
Dockerfile
core/src
lib
testkit
web_ui
tools
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
137
book/src/authentication.md
Normal 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!
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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
|
||||
|
||||
# ======================
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(());
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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| {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
|
@ -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>
|
||||
|
|
|
@ -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) }
|
|
@ -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)]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue