mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 20:47:01 +01:00
Account valid-from and expiry (#322)
Fixes #59 account policy and lockout. This is achived with a valid_from and expire attribute that are timestamps. Cli tools are added to manage these.
This commit is contained in:
parent
ca71b12b46
commit
018039b0b2
36
Cargo.lock
generated
36
Cargo.lock
generated
|
@ -178,9 +178,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-io"
|
||||
version = "1.1.5"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e727cebd055ab2861a854f79def078c4b99ea722d54c6800a0e274389882d4c"
|
||||
checksum = "b5bfd63f6fc8fd2925473a147d3f4d252c712291efdde0d7057b25146563402c"
|
||||
dependencies = [
|
||||
"concurrent-queue",
|
||||
"fastrand",
|
||||
|
@ -286,9 +286,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.40"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "687c230d85c0a52504709705fc8a53e4a692b83a2184f03dae73e38e1e93a783"
|
||||
checksum = "b246867b8b3b6ae56035f1eb1ed557c1d8eae97f0d53696138a50fa0e3a3b8c0"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -380,9 +380,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce4f9586c9a3151c4b49b19e82ba163dd073614dd057e53c969e1a4db5b52720"
|
||||
checksum = "e9ff35b701f3914bdb8fad3368d822c766ef2858b2583198e41639b936f09d3f"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
|
@ -1561,7 +1561,7 @@ dependencies = [
|
|||
"structopt",
|
||||
"tide",
|
||||
"tide-rustls",
|
||||
"time 0.1.44",
|
||||
"time 0.2.22",
|
||||
"tokio",
|
||||
"tokio-openssl",
|
||||
"tokio-util",
|
||||
|
@ -1617,6 +1617,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"shellexpand",
|
||||
"structopt",
|
||||
"time 0.2.22",
|
||||
"zxcvbn",
|
||||
]
|
||||
|
||||
|
@ -1698,9 +1699,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.77"
|
||||
version = "0.2.78"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2f96b10ec2560088a8e76961b00d47107b3a625fecb76dedb29ee7ccbf98235"
|
||||
checksum = "aa7087f49d294270db4e1928fc110c976cd4b9e5a16348e0a1df09afa99e6c98"
|
||||
|
||||
[[package]]
|
||||
name = "libnss"
|
||||
|
@ -2141,18 +2142,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
|||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "0.4.24"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f48fad7cfbff853437be7cf54d7b993af21f53be7f0988cbfe4a51535aa77205"
|
||||
checksum = "2b9e280448854bd91559252582173b3bd1f8e094a0e644791c0628ca9b1f144f"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "0.4.24"
|
||||
version = "0.4.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24c6d293bdd3ca5a1697997854c6cf7855e43fb6a0ba1c47af57a5bcafd158ae"
|
||||
checksum = "c8c8b352676bc6a4c3d71970560b913cea444a7a921cc2e2d920225e4b91edaa"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2161,9 +2162,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.1.9"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fe74897791e156a0cd8cce0db31b9b2198e67877316bf3086c3acd187f719f0"
|
||||
checksum = "e555d9e657502182ac97b539fb3dae8b79cda19e3e4f8ffb5e8de4f18df93c95"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
|
@ -2256,9 +2257,9 @@ checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
|||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.23"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51ef7cd2518ead700af67bf9d1a658d90b6037d77110fd9c0445429d0ba1c6c9"
|
||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
@ -3056,6 +3057,7 @@ checksum = "55b7151c9065e80917fbf285d9a5d1432f60db41d170ccafc749a136b41a93af"
|
|||
dependencies = [
|
||||
"const_fn",
|
||||
"libc",
|
||||
"serde",
|
||||
"standback",
|
||||
"stdweb",
|
||||
"time-macros",
|
||||
|
|
128
designs/account_policy.rst
Normal file
128
designs/account_policy.rst
Normal file
|
@ -0,0 +1,128 @@
|
|||
Account Policy and Lockouts
|
||||
---------------------------
|
||||
|
||||
For accounts we need to be able to define securite constraints and limits to prevent malicious use
|
||||
or attacks from succeeding. While these attacks may have similar sources or goals, the defences
|
||||
to them may vary.
|
||||
|
||||
A list (not comprehensive) of these include:
|
||||
|
||||
* Credential Stuffing
|
||||
* Phishing (Site Impersonation)
|
||||
* Key Logging / Physical Discovery
|
||||
* Common Password / Spray
|
||||
* Brute Force
|
||||
|
||||
Credential Policies
|
||||
===================
|
||||
|
||||
As the majority of the attacks listed can be prevented with TOTP, and all effectively defeated with
|
||||
Webauthn, it's essential that policies can exist that allow an administrator to set requirements
|
||||
on accounts to what level of authentication they require to protect resources.
|
||||
|
||||
Credential Polcies are inherited from groups, as groups grant rights and claims to other resources.
|
||||
Since it is these resources and privileges we wish to protect, logically the credential policy becomes
|
||||
part of the group that should be protected.
|
||||
|
||||
When multiple credential policies exist that may be conflicting, the "stricter" policy is enforced
|
||||
as a group in the set requires it.
|
||||
|
||||
The strength of credentials is today sorted as:
|
||||
|
||||
* (weakest)
|
||||
* Password
|
||||
* GeneratedPassword
|
||||
* Webauthn (with out verification)
|
||||
* TOTP + Password
|
||||
* Webauthn + Password
|
||||
* WebauthnVerified
|
||||
* WebauthnVerified + Password
|
||||
* (strongest)
|
||||
|
||||
Rate Limiting
|
||||
======================
|
||||
|
||||
Rate Limiting is the process of delaying authentication responses to slow the number of attempts
|
||||
against an account to deter attackers. This is often used to prevent attackers from bruteforcing
|
||||
passwords at a high rate.
|
||||
|
||||
The best defence again these attacks is MFA. Due to the design of Kanidm, the second factor
|
||||
(ie the webauthn token or the otp) is always checked *before* the password, meaning that the
|
||||
attacker is unable to attack the password *unless* they also have the corresponding MFA token.
|
||||
|
||||
However, not all accounts will have MFA enabled, which means that defences are still required to
|
||||
prevent these attacks for password-only accounts. Accounts protected with TOTP must also be rate
|
||||
limited according to NIST sp800 63b. Webauthn does *not* require ratelimiting as a single factor
|
||||
or multi factor device.
|
||||
|
||||
As an account can only have a single proceeding authentication session at a time, this provides
|
||||
serialisation and rate limiting per account of the service. However, as Kanidm will in the future
|
||||
support multiple, distributed replicas, we must consider an architecture that allows eventually
|
||||
consistent behaviour.
|
||||
|
||||
NIST SP800 63b recommends that after 100 failed attempts that the account be locked. Due to the
|
||||
eventually consistent nature, this poses a challenge, namely that:
|
||||
|
||||
* Synchronising this account lock may not be instant, allowing further attempts on parallel servers.
|
||||
* That read only servers may exist in the system which can not write to the entries.
|
||||
* A malicious party may intentionally send incorrect values to force an account to lock.
|
||||
|
||||
To account for this for accounts with TOTP:
|
||||
|
||||
* After an 5 incorrect TOTP's within the time window, the account is locked for 60 seconds. This prevents bruteforce of the TOTP.
|
||||
|
||||
For accounts with password-only:
|
||||
|
||||
* After 5 incorrect attempts the account is rate limited by an increasing time window within the API. This limit delays the response to the auth (regardless of success)
|
||||
* After X attempts, the account is soft locked on the affected server only for a time window of Y increasing up to Z.
|
||||
* If the attempts continue, the account is hard locked and signalled to an external system that this has occured.
|
||||
|
||||
The value of X should be less than 100, so that the NIST guidelines can be met. This is beacuse when there are
|
||||
many replicas, each replica maintains it's own locking state, so "eventually" as each replica is attempted to be
|
||||
bruteforced, then they will all eventually soft lock the account. In larger environments, we require
|
||||
external signalling to coordinate the locking of the account.
|
||||
|
||||
In the future, this can also be informed by:
|
||||
|
||||
* IP/GEOIP from past login's to determine if the behaviour is expected.
|
||||
* HTTP/Browser ID to determine if it's "likely" the person in question.
|
||||
|
||||
These can then assist with choosing to lock or allow an auth to proceed in the face of an attack.
|
||||
|
||||
FUTURE:
|
||||
* Delayed notification about suspect login?
|
||||
|
||||
Ratelimit on unix auth
|
||||
|
||||
Hard Lock + Expiry/Active Time Limits
|
||||
=====================================
|
||||
|
||||
It must be possible to expire an account so it no longer operates (IE temporary contractor) or
|
||||
accounts that can only operate after a known point in time (Student enrollments and their course
|
||||
commencment date).
|
||||
|
||||
This expiry must exist at the account level, but also on issued token/api password levels. This allows revocation of
|
||||
individual tokens, but also the expiry of the account and all tokens as a whole. This expiry may be
|
||||
undone, allowing the credentials to become valid once again.
|
||||
|
||||
On the account, this is represented by two date times. AuthAllowFrom and AuthAllowUntil. These
|
||||
are stored on the server in unix epoch to account for timezones and geographic distribution.
|
||||
|
||||
* Interaction with already issued tokens.
|
||||
* it prevents them from working?
|
||||
|
||||
Must prevent creation of radius auth tokens
|
||||
|
||||
Must prevent login via unix.
|
||||
|
||||
Application Passwords / Issued Oauth Tokens
|
||||
===========================================
|
||||
|
||||
* Relates to claims
|
||||
* Need their own expirys
|
||||
* Need ratelimit as above?
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -31,6 +31,8 @@ strength, random, machine only password.
|
|||
kanidm account credential generate_password --name admin idm_admin
|
||||
Generated password for idm_admin: tqoReZfz....
|
||||
|
||||
## Creating Accounts
|
||||
|
||||
We can now use the idm_admin to create initial groups and accounts.
|
||||
|
||||
kanidm group create demo_group --name idm_admin
|
||||
|
@ -79,6 +81,42 @@ An example can be easily shown with:
|
|||
kanidm group add_members group2 nest_example --name idm_admin
|
||||
kanidm account get nest_example --name anonymous
|
||||
|
||||
## Account Validity
|
||||
|
||||
Kanidm supports accounts that are only able to be authenticated between specific datetime
|
||||
windows. This takes the form of a "valid from" attribute that defines the earliest start
|
||||
date where authentication can succeed, and an expiry date where the account will no longer
|
||||
allow authentication.
|
||||
|
||||
This can be displayed with:
|
||||
|
||||
kanidm account validity show demo_user --name idm_admin
|
||||
valid after: 2020-09-25T21:22:04+10:00
|
||||
expire: 2020-09-25T01:22:04+10:00
|
||||
|
||||
These datetimes are stored in the server as UTC, but presented according to your local system time
|
||||
to aid correct understanding of when the events will occur.
|
||||
|
||||
To set the values, an account with account management permission is required (for example, idm_admin).
|
||||
Again, these values will correctly translated from the entered local timezone to UTC.
|
||||
|
||||
# Set the earliest time the account can start authenticating
|
||||
kanidm account validity begin_from demo_user '2020-09-25T11:22:04+00:00' --name idm_admin
|
||||
# Set the expiry or end date of the account
|
||||
kanidm account validity expire_at demo_user '2020-09-25T11:22:04+00:00' --name idm_admin
|
||||
|
||||
To unset or remove these values the following can be used:
|
||||
|
||||
kanidm account validity begin_from demo_user any|clear --name idm_admin
|
||||
kanidm account validity expire_at demo_user never|clear --name idm_admin
|
||||
|
||||
To "lock" an account, you can set the expire_at value to the past or unix epoch. Even in the situation
|
||||
where the "valid from" is *after* the expire_at, the expire_at will be respected.
|
||||
|
||||
kanidm account validity expire_at demo_user 1970-01-01T00:00:00+00:00 --name idm_admin
|
||||
|
||||
These validity settings impact all authentication functions of the account (kanidm, ldap, radius).
|
||||
|
||||
## Why Can't I Change admin With idm_admin?
|
||||
|
||||
As a security mechanism there is a distinction between "accounts" and "high permission
|
||||
|
|
|
@ -287,6 +287,17 @@ impl KanidmAsyncClient {
|
|||
Ok(Some((r.youare, r.uat)))
|
||||
}
|
||||
|
||||
pub async fn idm_account_set_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
values: &[&str],
|
||||
) -> Result<bool, ClientError> {
|
||||
let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
|
||||
self.perform_put_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), m)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn idm_account_unix_token_get(&self, id: &str) -> Result<UnixUserToken, ClientError> {
|
||||
// Format doesn't work in async
|
||||
// format!("/v1/account/{}/_unix/_token", id).as_str()
|
||||
|
|
|
@ -342,7 +342,7 @@ impl KanidmClient {
|
|||
}
|
||||
|
||||
pub fn get_token(&self) -> Option<&str> {
|
||||
self.bearer_token.as_ref().map(|s| s.as_str())
|
||||
self.bearer_token.as_deref()
|
||||
}
|
||||
|
||||
pub fn logout(&mut self) -> Result<(), reqwest::Error> {
|
||||
|
@ -767,6 +767,28 @@ impl KanidmClient {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn idm_account_get_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
) -> Result<Option<Vec<String>>, ClientError> {
|
||||
self.perform_get_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_purge_attr(&self, id: &str, attr: &str) -> Result<bool, ClientError> {
|
||||
self.perform_delete_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str())
|
||||
}
|
||||
|
||||
pub fn idm_account_set_attr(
|
||||
&self,
|
||||
id: &str,
|
||||
attr: &str,
|
||||
values: &[&str],
|
||||
) -> Result<bool, ClientError> {
|
||||
let m: Vec<_> = values.iter().map(|v| (*v).to_string()).collect();
|
||||
self.perform_put_request(format!("/v1/account/{}/_attr/{}", id, attr).as_str(), m)
|
||||
}
|
||||
|
||||
pub fn idm_account_primary_credential_import_password(
|
||||
&self,
|
||||
id: &str,
|
||||
|
|
|
@ -772,6 +772,8 @@ fn test_server_rest_totp_auth_lifecycle() {
|
|||
});
|
||||
}
|
||||
|
||||
// Test setting account expiry
|
||||
|
||||
// Test the self version of the radius path.
|
||||
|
||||
// Test hitting all auth-required endpoints and assert they give unauthorized.
|
||||
|
|
|
@ -251,6 +251,9 @@ pub struct UnixUserToken {
|
|||
pub shell: Option<String>,
|
||||
pub groups: Vec<UnixGroupToken>,
|
||||
pub sshkeys: Vec<String>,
|
||||
// The default value of bool is false.
|
||||
#[serde(default)]
|
||||
pub valid: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for UnixUserToken {
|
||||
|
|
|
@ -37,5 +37,6 @@ serde = "1.0"
|
|||
serde_json = "1.0"
|
||||
shellexpand = "2.0"
|
||||
rayon = "1.2"
|
||||
time = "0.2"
|
||||
|
||||
zxcvbn = "2.0"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::common::CommonOpt;
|
||||
use crate::password_prompt;
|
||||
use structopt::StructOpt;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AccountCommonOpt {
|
||||
|
@ -24,6 +25,30 @@ pub struct AccountNamedOpt {
|
|||
copt: CommonOpt,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AccountNamedExpireDateTimeOpt {
|
||||
#[structopt(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[structopt(flatten)]
|
||||
copt: CommonOpt,
|
||||
#[structopt(name = "datetime")]
|
||||
/// An rfc3339 time of the format "YYYY-MM-DDTHH:MM:SS+TZ", "2020-09-25T11:22:02+10:00"
|
||||
/// or the word "never", "clear" to remove account expiry.
|
||||
datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AccountNamedValidDateTimeOpt {
|
||||
#[structopt(flatten)]
|
||||
aopts: AccountCommonOpt,
|
||||
#[structopt(flatten)]
|
||||
copt: CommonOpt,
|
||||
#[structopt(name = "datetime")]
|
||||
/// An rfc3339 time of the format "YYYY-MM-DDTHH:MM:SS+TZ", "2020-09-25T11:22:02+10:00"
|
||||
/// or the word "any", "clear" to remove valid from enforcement.
|
||||
datetime: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub struct AccountNamedTagOpt {
|
||||
#[structopt(flatten)]
|
||||
|
@ -106,6 +131,16 @@ pub enum AccountSsh {
|
|||
Delete(AccountNamedTagOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum AccountValidity {
|
||||
#[structopt(name = "show")]
|
||||
Show(AccountNamedOpt),
|
||||
#[structopt(name = "expire_at")]
|
||||
ExpireAt(AccountNamedExpireDateTimeOpt),
|
||||
#[structopt(name = "begin_from")]
|
||||
BeginFrom(AccountNamedValidDateTimeOpt),
|
||||
}
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
pub enum AccountOpt {
|
||||
#[structopt(name = "credential")]
|
||||
|
@ -124,6 +159,8 @@ pub enum AccountOpt {
|
|||
Create(AccountCreateOpt),
|
||||
#[structopt(name = "delete")]
|
||||
Delete(AccountNamedOpt),
|
||||
#[structopt(name = "validity")]
|
||||
Validity(AccountValidity),
|
||||
}
|
||||
|
||||
impl AccountOpt {
|
||||
|
@ -152,6 +189,11 @@ impl AccountOpt {
|
|||
AccountOpt::Get(aopt) => aopt.copt.debug,
|
||||
AccountOpt::Delete(aopt) => aopt.copt.debug,
|
||||
AccountOpt::Create(aopt) => aopt.copt.debug,
|
||||
AccountOpt::Validity(avopt) => match avopt {
|
||||
AccountValidity::Show(ano) => ano.copt.debug,
|
||||
AccountValidity::ExpireAt(ano) => ano.copt.debug,
|
||||
AccountValidity::BeginFrom(ano) => ano.copt.debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -327,6 +369,118 @@ impl AccountOpt {
|
|||
eprintln!("Error -> {:?}", e)
|
||||
}
|
||||
}
|
||||
AccountOpt::Validity(avopt) => match avopt {
|
||||
AccountValidity::Show(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
|
||||
let r = client
|
||||
.idm_account_get_attr(ano.aopts.account_id.as_str(), "account_expire")
|
||||
.and_then(|v1| {
|
||||
client
|
||||
.idm_account_get_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
)
|
||||
.map(|v2| (v1, v2))
|
||||
});
|
||||
|
||||
match r {
|
||||
Ok((ex, vf)) => {
|
||||
if let Some(t) = vf {
|
||||
// Convert the time to local timezone.
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(time::UtcOffset::current_local_offset())
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
|
||||
println!("valid after: {}", t);
|
||||
} else {
|
||||
println!("valid after: any time");
|
||||
}
|
||||
|
||||
if let Some(t) = ex {
|
||||
let t = OffsetDateTime::parse(&t[0], time::Format::Rfc3339)
|
||||
.map(|odt| {
|
||||
odt.to_offset(time::UtcOffset::current_local_offset())
|
||||
.format(time::Format::Rfc3339)
|
||||
})
|
||||
.unwrap_or_else(|_| "invalid timestamp".to_string());
|
||||
println!("expire: {}", t);
|
||||
} else {
|
||||
println!("expire: never");
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("Error -> {:?}", e),
|
||||
}
|
||||
}
|
||||
AccountValidity::ExpireAt(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
if ano.datetime == "never" || ano.datetime == "clear" {
|
||||
// Unset the value
|
||||
if let Err(e) = client
|
||||
.idm_account_purge_attr(ano.aopts.account_id.as_str(), "account_expire")
|
||||
{
|
||||
eprintln!("Error -> {:?}", e)
|
||||
} else {
|
||||
println!("Success")
|
||||
}
|
||||
} else {
|
||||
if let Err(e) =
|
||||
OffsetDateTime::parse(ano.datetime.as_str(), time::Format::Rfc3339)
|
||||
{
|
||||
eprintln!("Error -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = client.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_expire",
|
||||
&[ano.datetime.as_str()],
|
||||
) {
|
||||
eprintln!("Error -> {:?}", e);
|
||||
} else {
|
||||
println!("Success")
|
||||
}
|
||||
}
|
||||
}
|
||||
AccountValidity::BeginFrom(ano) => {
|
||||
let client = ano.copt.to_client();
|
||||
if ano.datetime == "any"
|
||||
|| ano.datetime == "clear"
|
||||
|| ano.datetime == "whenever"
|
||||
{
|
||||
// Unset the value
|
||||
if let Err(e) = client.idm_account_purge_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
) {
|
||||
eprintln!("Error -> {:?}", e)
|
||||
} else {
|
||||
println!("Success")
|
||||
}
|
||||
} else {
|
||||
// Attempt to parse and set
|
||||
if let Err(e) =
|
||||
OffsetDateTime::parse(ano.datetime.as_str(), time::Format::Rfc3339)
|
||||
{
|
||||
eprintln!("Error -> {:?}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = client.idm_account_set_attr(
|
||||
ano.aopts.account_id.as_str(),
|
||||
"account_valid_from",
|
||||
&[ano.datetime.as_str()],
|
||||
) {
|
||||
eprintln!("Error -> {:?}", e);
|
||||
} else {
|
||||
println!("Success")
|
||||
}
|
||||
}
|
||||
}
|
||||
}, // end AccountOpt::Validity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,14 +58,13 @@ impl CommonOpt {
|
|||
None => client_builder,
|
||||
};
|
||||
|
||||
let client = match client_builder.build() {
|
||||
match client_builder.build() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to build client instance -- {:?}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
client
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_client(&self) -> KanidmClient {
|
||||
|
@ -79,7 +78,7 @@ impl CommonOpt {
|
|||
}
|
||||
};
|
||||
|
||||
if tokens.len() == 0 {
|
||||
if tokens.is_empty() {
|
||||
error!(
|
||||
"No valid authentication tokens found. Please login with the 'login' subcommand."
|
||||
);
|
||||
|
@ -100,6 +99,7 @@ impl CommonOpt {
|
|||
}
|
||||
None => {
|
||||
if tokens.len() == 1 {
|
||||
#[allow(clippy::expect_used)]
|
||||
let (f_uname, f_token) = tokens.iter().next().expect("Memory Corruption");
|
||||
// else pick the first token
|
||||
info!("Authenticated as {}", f_uname);
|
||||
|
|
|
@ -4,7 +4,7 @@ use std::fs::File;
|
|||
use std::io::{BufReader, BufWriter};
|
||||
use structopt::StructOpt;
|
||||
|
||||
static TOKEN_PATH: &'static str = "~/.cache/kanidm_tokens";
|
||||
static TOKEN_PATH: &str = "~/.cache/kanidm_tokens";
|
||||
|
||||
pub fn read_tokens() -> Result<BTreeMap<String, String>, ()> {
|
||||
let token_path: String = shellexpand::tilde(TOKEN_PATH).into_owned();
|
||||
|
@ -50,7 +50,7 @@ impl LoginOpt {
|
|||
pub fn exec(&self) {
|
||||
let mut client = self.copt.to_unauth_client();
|
||||
|
||||
let (r, username) = match self.copt.username.as_ref().map(|s| s.as_str()) {
|
||||
let (r, username) = match self.copt.username.as_deref() {
|
||||
None | Some("anonymous") => (client.auth_anonymous(), "anonymous".to_string()),
|
||||
Some(username) => {
|
||||
let password = match rpassword::prompt_password_stderr("Enter password: ") {
|
||||
|
|
|
@ -443,6 +443,10 @@ impl CacheLayer {
|
|||
self.refresh_usertoken(&account_id, item).await
|
||||
}
|
||||
}
|
||||
.map(|t| {
|
||||
debug!("token -> {:?}", t);
|
||||
t
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_grouptoken(&self, grp_id: Id) -> Result<Option<UnixGroupToken>, ()> {
|
||||
|
@ -506,7 +510,16 @@ impl CacheLayer {
|
|||
// Get ssh keys for an account id
|
||||
pub async fn get_sshkeys(&self, account_id: &str) -> Result<Vec<String>, ()> {
|
||||
let token = self.get_usertoken(Id::Name(account_id.to_string())).await?;
|
||||
Ok(token.map(|t| t.sshkeys).unwrap_or_else(Vec::new))
|
||||
Ok(token
|
||||
.map(|t| {
|
||||
// Only return keys if the account is valid
|
||||
if t.valid {
|
||||
t.sshkeys
|
||||
} else {
|
||||
Vec::with_capacity(0)
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| Vec::with_capacity(0)))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
|
@ -704,7 +717,13 @@ impl CacheLayer {
|
|||
) -> Result<Option<bool>, ()> {
|
||||
debug!("Attempt offline password check");
|
||||
match token.as_ref() {
|
||||
Some(t) => self.check_cache_userpassword(&t.uuid, cred).await.map(Some),
|
||||
Some(t) => {
|
||||
if t.valid {
|
||||
self.check_cache_userpassword(&t.uuid, cred).await.map(Some)
|
||||
} else {
|
||||
Ok(Some(false))
|
||||
}
|
||||
}
|
||||
None => Ok(None),
|
||||
}
|
||||
/*
|
||||
|
@ -731,7 +750,7 @@ impl CacheLayer {
|
|||
user_set, self.pam_allow_groups
|
||||
);
|
||||
|
||||
user_set.intersection(&self.pam_allow_groups).count() > 0
|
||||
user_set.intersection(&self.pam_allow_groups).count() > 0 && tok.valid
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
|
@ -718,6 +718,7 @@ mod tests {
|
|||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
valid: true,
|
||||
};
|
||||
|
||||
let id_name = Id::Name("testuser".to_string());
|
||||
|
@ -890,6 +891,7 @@ mod tests {
|
|||
shell: None,
|
||||
groups: vec![gt1.clone(), gt2],
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
valid: true,
|
||||
};
|
||||
|
||||
// First, add the groups.
|
||||
|
@ -944,6 +946,7 @@ mod tests {
|
|||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
valid: true,
|
||||
};
|
||||
|
||||
// Test that with no account, is false
|
||||
|
@ -1046,6 +1049,7 @@ mod tests {
|
|||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
valid: true,
|
||||
};
|
||||
|
||||
let ut2 = UnixUserToken {
|
||||
|
@ -1057,6 +1061,7 @@ mod tests {
|
|||
shell: None,
|
||||
groups: Vec::new(),
|
||||
sshkeys: vec!["key-a".to_string()],
|
||||
valid: true,
|
||||
};
|
||||
|
||||
let id_name = Id::Name("testuser".to_string());
|
||||
|
|
|
@ -23,9 +23,10 @@ const ADMIN_TEST_PASSWORD: &str = "integration test admin password";
|
|||
const TESTACCOUNT1_PASSWORD_A: &str = "password a for account1 test";
|
||||
const TESTACCOUNT1_PASSWORD_B: &str = "password b for account1 test";
|
||||
const TESTACCOUNT1_PASSWORD_INC: &str = "never going to work";
|
||||
const ACCOUNT_EXPIRE: &str = "1970-01-01T00:00:00+00:00";
|
||||
|
||||
fn run_test(fix_fn: fn(&mut KanidmClient) -> (), test_fn: fn(CacheLayer, KanidmAsyncClient) -> ()) {
|
||||
// ::std::env::set_var("RUST_LOG", "actix_web=warn,kanidm=error");
|
||||
// ::std::env::set_var("RUST_LOG", "kanidm=debug");
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
|
||||
let (mut ready_tx, mut ready_rx) = mpsc::channel(1);
|
||||
|
@ -42,8 +43,8 @@ fn run_test(fix_fn: fn(&mut KanidmClient) -> (), test_fn: fn(CacheLayer, KanidmA
|
|||
config.address = format!("127.0.0.1:{}", port);
|
||||
config.secure_cookies = false;
|
||||
config.integration_test_config = Some(int_config);
|
||||
// config.log_level = Some(LogLevel::Verbose as u32);
|
||||
config.log_level = Some(LogLevel::Quiet as u32);
|
||||
// config.log_level = Some(LogLevel::Verbose as u32);
|
||||
config.threads = 1;
|
||||
|
||||
let t_handle = thread::spawn(move || {
|
||||
|
@ -551,3 +552,78 @@ fn test_cache_account_pam_nonexist() {
|
|||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cache_account_expiry() {
|
||||
run_test(test_fixture, |cachelayer, mut adminclient| {
|
||||
let mut rt = Runtime::new().expect("Failed to start tokio");
|
||||
let fut = async move {
|
||||
cachelayer.attempt_online().await;
|
||||
assert!(cachelayer.test_connection().await);
|
||||
|
||||
// We need one good auth first to prime the cache with a hash.
|
||||
let a1 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a1 == Some(true));
|
||||
// Invalidate to make sure we go online next checks.
|
||||
assert!(cachelayer.invalidate().await.is_ok());
|
||||
|
||||
// expire the account
|
||||
adminclient
|
||||
.auth_simple_password("admin", ADMIN_TEST_PASSWORD)
|
||||
.await
|
||||
.expect("failed to auth as admin");
|
||||
adminclient
|
||||
.idm_account_set_attr("testaccount1", "account_expire", &[ACCOUNT_EXPIRE])
|
||||
.await
|
||||
.unwrap();
|
||||
// auth will fail
|
||||
let a2 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a2 == Some(false));
|
||||
|
||||
// ssh keys should be empty
|
||||
let sk = cachelayer
|
||||
.get_sshkeys("testaccount1")
|
||||
.await
|
||||
.expect("Failed to get from cache.");
|
||||
assert!(sk.len() == 0);
|
||||
|
||||
// Pam account allowed should be denied.
|
||||
let a3 = cachelayer
|
||||
.pam_account_allowed("testaccount1")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a3 == Some(false));
|
||||
|
||||
// go offline
|
||||
cachelayer.mark_offline().await;
|
||||
|
||||
// Now, check again ...
|
||||
let a4 = cachelayer
|
||||
.pam_account_authenticate("testaccount1", TESTACCOUNT1_PASSWORD_A)
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a4 == Some(false));
|
||||
|
||||
// ssh keys should be empty
|
||||
let sk = cachelayer
|
||||
.get_sshkeys("testaccount1")
|
||||
.await
|
||||
.expect("Failed to get from cache.");
|
||||
assert!(sk.len() == 0);
|
||||
|
||||
// Pam account allowed should be denied.
|
||||
let a5 = cachelayer
|
||||
.pam_account_allowed("testaccount1")
|
||||
.await
|
||||
.expect("failed to authenticate");
|
||||
assert!(a5 == Some(false));
|
||||
};
|
||||
rt.block_on(fut);
|
||||
})
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ r2d2 = "0.8"
|
|||
r2d2_sqlite = "0.16"
|
||||
|
||||
structopt = { version = "0.3", default-features = false }
|
||||
time = "0.1"
|
||||
time = { version = "0.2", features = ["serde", "std"] }
|
||||
|
||||
hashbrown = "0.8"
|
||||
concread = "^0.2.3"
|
||||
|
|
|
@ -500,9 +500,16 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
};
|
||||
|
||||
let ct = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(audit, "Clock Error -> {:?}", e);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
ltrace!(audit, "Begin event {:?}", rate);
|
||||
|
||||
idm_read.get_radiusauthtoken(&mut audit, &rate)
|
||||
idm_read.get_radiusauthtoken(&mut audit, &rate, ct)
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
|
@ -554,9 +561,16 @@ impl QueryServerReadV1 {
|
|||
}
|
||||
};
|
||||
|
||||
let ct = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_err(|e| {
|
||||
ladmin_error!(audit, "Clock Error -> {:?}", e);
|
||||
OperationError::InvalidState
|
||||
})?;
|
||||
|
||||
ltrace!(audit, "Begin event {:?}", rate);
|
||||
|
||||
idm_read.get_unixusertoken(&mut audit, &rate)
|
||||
idm_read.get_unixusertoken(&mut audit, &rate, ct)
|
||||
}
|
||||
);
|
||||
self.log.send(audit).map_err(|_| {
|
||||
|
|
|
@ -67,6 +67,7 @@ pub enum DbValueV1 {
|
|||
UI(u32),
|
||||
CI(DbCidV1),
|
||||
NU(String),
|
||||
DT(String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -97,7 +97,9 @@ pub const JSON_IDM_SELF_ACP_READ_V1: &str = r#"{
|
|||
"radius_secret",
|
||||
"gidnumber",
|
||||
"loginshell",
|
||||
"uuid"
|
||||
"uuid",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -331,7 +333,7 @@ pub const JSON_IDM_ACP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "mail", "gidnumber", "account_expire", "account_valid_from"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -353,10 +355,10 @@ pub const JSON_IDM_ACP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "mail", "account_expire", "account_valid_from"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -385,7 +387,9 @@ pub const JSON_IDM_ACP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
|
|||
"description",
|
||||
"primary_credential",
|
||||
"ssh_publickey",
|
||||
"mail"
|
||||
"mail",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "account"
|
||||
|
@ -434,7 +438,7 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_READ_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_search_attr": [
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof"
|
||||
"class", "name", "spn", "uuid", "displayname", "ssh_publickey", "primary_credential", "memberof", "account_expire", "account_valid_from"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -456,10 +460,10 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_WRITE_PRIV_V1: &str = r#"{
|
|||
"{\"and\": [{\"eq\": [\"class\",\"account\"]}, {\"eq\": [\"memberof\",\"00000000-0000-0000-0000-000000001000\"]}, {\"andnot\": {\"or\": [{\"eq\": [\"class\", \"tombstone\"]}, {\"eq\": [\"class\", \"recycled\"]}]}}]}"
|
||||
],
|
||||
"acp_modify_removedattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from"
|
||||
],
|
||||
"acp_modify_presentattr": [
|
||||
"name", "displayname", "ssh_publickey", "primary_credential"
|
||||
"name", "displayname", "ssh_publickey", "primary_credential", "account_expire", "account_valid_from"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
@ -758,7 +762,9 @@ pub const JSON_IDM_ACP_HP_ACCOUNT_MANAGE_PRIV_V1: &str = r#"{
|
|||
"displayname",
|
||||
"description",
|
||||
"primary_credential",
|
||||
"ssh_publickey"
|
||||
"ssh_publickey",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
],
|
||||
"acp_create_class": [
|
||||
"object", "account"
|
||||
|
|
|
@ -13,7 +13,7 @@ pub use crate::constants::system_config::*;
|
|||
pub use crate::constants::uuids::*;
|
||||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 11;
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 12;
|
||||
// On test builds, define to 60 seconds
|
||||
#[cfg(test)]
|
||||
pub const PURGE_FREQUENCY: u64 = 60;
|
||||
|
|
|
@ -426,6 +426,66 @@ pub const JSON_SCHEMA_ATTR_NSUNIQUEID: &str = r#"{
|
|||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_ACCOUNT_EXPIRE: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"The datetime after which this accounnt no longer may authenticate."
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"account_expire"
|
||||
],
|
||||
"syntax": [
|
||||
"DATETIME"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000072"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
pub const JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM: &str = r#"{
|
||||
"attrs": {
|
||||
"class": [
|
||||
"object",
|
||||
"system",
|
||||
"attributetype"
|
||||
],
|
||||
"description": [
|
||||
"The datetime after which this account may commence authenticating."
|
||||
],
|
||||
"index": [],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
"multivalue": [
|
||||
"false"
|
||||
],
|
||||
"attributename": [
|
||||
"account_valid_from"
|
||||
],
|
||||
"syntax": [
|
||||
"DATETIME"
|
||||
],
|
||||
"uuid": [
|
||||
"00000000-0000-0000-0000-ffff00000073"
|
||||
]
|
||||
}
|
||||
}"#;
|
||||
|
||||
// === classes ===
|
||||
|
||||
pub const JSON_SCHEMA_CLASS_PERSON: &str = r#"
|
||||
{
|
||||
"attrs": {
|
||||
|
@ -499,7 +559,9 @@ pub const JSON_SCHEMA_CLASS_ACCOUNT: &str = r#"
|
|||
"systemmay": [
|
||||
"primary_credential",
|
||||
"ssh_publickey",
|
||||
"radius_secret"
|
||||
"radius_secret",
|
||||
"account_expire",
|
||||
"account_valid_from"
|
||||
],
|
||||
"systemmust": [
|
||||
"displayname",
|
||||
|
|
|
@ -116,6 +116,9 @@ pub const STR_UUID_SCHEMA_ATTR_NICE: &str = "00000000-0000-0000-0000-ffff0000006
|
|||
pub const STR_UUID_SCHEMA_ATTR_ENTRYUUID: &str = "00000000-0000-0000-0000-ffff00000070";
|
||||
pub const STR_UUID_SCHEMA_ATTR_OBJECTCLASS: &str = "00000000-0000-0000-0000-ffff00000071";
|
||||
|
||||
pub const _STR_UUID_SCHEMA_ATTR_ACCOUNT_EXPIRE: &str = "00000000-0000-0000-0000-ffff00000072";
|
||||
pub const _STR_UUID_SCHEMA_ATTR_ACCOUNT_VALID_FROM: &str = "00000000-0000-0000-0000-ffff00000073";
|
||||
|
||||
// System and domain infos
|
||||
// I'd like to strongly criticise william of the past for fucking up these allocations.
|
||||
pub const STR_UUID_SYSTEM_INFO: &str = "00000000-0000-0000-0000-ffffff000001";
|
||||
|
|
|
@ -51,6 +51,7 @@ use std::collections::BTreeSet;
|
|||
// use std::collections::BTreeMap as Map;
|
||||
use hashbrown::HashMap as Map;
|
||||
use hashbrown::HashSet;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
// use std::convert::TryFrom;
|
||||
|
@ -1583,6 +1584,11 @@ impl<VALID, STATE> Entry<VALID, STATE> {
|
|||
.and_then(|a| a.get_radius_secret())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_single_datetime(&self, attr: &str) -> Option<OffsetDateTime> {
|
||||
self.get_ava_single(attr).and_then(|a| a.to_datetime())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get_ava_single_str(&self, attr: &str) -> Option<&str> {
|
||||
self.get_ava_single(attr).and_then(|v| v.to_str())
|
||||
|
|
|
@ -14,6 +14,8 @@ use crate::modify::{ModifyInvalid, ModifyList};
|
|||
use crate::server::{QueryServerReadTransaction, QueryServerWriteTransaction};
|
||||
use crate::value::{PartialValue, Value};
|
||||
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -58,6 +60,10 @@ macro_rules! try_from_entry {
|
|||
"Missing attribute: spn".to_string(),
|
||||
))?;
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
||||
// Resolved by the caller
|
||||
let groups = $groups;
|
||||
|
||||
|
@ -69,6 +75,8 @@ macro_rules! try_from_entry {
|
|||
displayname,
|
||||
groups,
|
||||
primary,
|
||||
valid_from,
|
||||
expire,
|
||||
spn,
|
||||
})
|
||||
}};
|
||||
|
@ -87,6 +95,8 @@ pub(crate) struct Account {
|
|||
pub uuid: Uuid,
|
||||
pub groups: Vec<Group>,
|
||||
pub primary: Option<Credential>,
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
// primary: Credential
|
||||
// app_creds: Vec<Credential>
|
||||
// account expiry? (as opposed to cred expiry)
|
||||
|
@ -147,6 +157,27 @@ impl Account {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||
let cot = OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
let vmin = if let Some(vft) = &self.valid_from {
|
||||
// If current time greater than strat time window
|
||||
vft < &cot
|
||||
} else {
|
||||
// We have no time, not expired.
|
||||
true
|
||||
};
|
||||
let vmax = if let Some(ext) = &self.expire {
|
||||
// If exp greater than ct then expired.
|
||||
&cot < ext
|
||||
} else {
|
||||
// If not present, we are not expired
|
||||
true
|
||||
};
|
||||
// Mix the results
|
||||
vmin && vmax
|
||||
}
|
||||
|
||||
pub fn is_anonymous(&self) -> bool {
|
||||
self.uuid == *UUID_ANONYMOUS
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ const BAD_PASSWORD_MSG: &str = "incorrect password";
|
|||
const BAD_TOTP_MSG: &str = "incorrect totp";
|
||||
const BAD_AUTH_TYPE_MSG: &str = "invalid authentication method in this context";
|
||||
const BAD_CREDENTIALS: &str = "invalid credential message";
|
||||
const ACCOUNT_EXPIRED: &str = "account expired";
|
||||
|
||||
enum CredState {
|
||||
Success(Vec<Claim>),
|
||||
|
@ -48,7 +49,7 @@ struct CredTotpPw {
|
|||
|
||||
#[derive(Clone, Debug)]
|
||||
enum CredHandler {
|
||||
Denied,
|
||||
Denied(&'static str),
|
||||
// The bool is a flag if the cred has been authed against.
|
||||
Anonymous,
|
||||
// AppPassword
|
||||
|
@ -286,10 +287,10 @@ impl CredHandler {
|
|||
async_tx: &Sender<DelayedAction>,
|
||||
) -> CredState {
|
||||
match self {
|
||||
CredHandler::Denied => {
|
||||
CredHandler::Denied(reason) => {
|
||||
// Sad trombone.
|
||||
lsecurity!(au, "Handler::Denied -> Result::Denied");
|
||||
CredState::Denied("authentication denied")
|
||||
CredState::Denied(reason)
|
||||
}
|
||||
CredHandler::Anonymous => Self::validate_anonymous(au, creds),
|
||||
CredHandler::Password(ref mut pw) => {
|
||||
|
@ -303,7 +304,7 @@ impl CredHandler {
|
|||
|
||||
pub fn valid_auth_mechs(&self) -> Vec<AuthAllowed> {
|
||||
match &self {
|
||||
CredHandler::Denied => Vec::new(),
|
||||
CredHandler::Denied(_) => Vec::new(),
|
||||
CredHandler::Anonymous => vec![AuthAllowed::Anonymous],
|
||||
CredHandler::Password(_) => vec![AuthAllowed::Password],
|
||||
// webauth
|
||||
|
@ -312,10 +313,10 @@ impl CredHandler {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_denied(&self) -> bool {
|
||||
pub(crate) fn is_denied(&self) -> Option<&'static str> {
|
||||
match &self {
|
||||
CredHandler::Denied => true,
|
||||
_ => false,
|
||||
CredHandler::Denied(x) => Some(x),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -339,51 +340,69 @@ pub(crate) struct AuthSession {
|
|||
}
|
||||
|
||||
impl AuthSession {
|
||||
pub fn new(au: &mut AuditScope, account: Account, appid: Option<String>) -> Self {
|
||||
pub fn new(
|
||||
au: &mut AuditScope,
|
||||
account: Account,
|
||||
appid: Option<String>,
|
||||
ct: Duration,
|
||||
) -> (Option<Self>, AuthState) {
|
||||
// During this setup, determine the credential handler that we'll be using
|
||||
// for this session. This is currently based on presentation of an application
|
||||
// id.
|
||||
let handler = match appid {
|
||||
Some(_) => CredHandler::Denied,
|
||||
None => {
|
||||
// We want the primary handler - this is where we make a decision
|
||||
// based on the anonymous ... in theory this could be cleaner
|
||||
// and interact with the account more?
|
||||
if account.is_anonymous() {
|
||||
CredHandler::Anonymous
|
||||
} else {
|
||||
// Now we see if they have one ...
|
||||
match &account.primary {
|
||||
Some(cred) => {
|
||||
// Probably means new authsession has to be failable
|
||||
CredHandler::try_from(cred).unwrap_or_else(|_| {
|
||||
lsecurity_critical!(
|
||||
au,
|
||||
"corrupt credentials, unable to start credhandler"
|
||||
);
|
||||
CredHandler::Denied
|
||||
})
|
||||
let handler = if account.is_within_valid_time(ct) {
|
||||
match appid {
|
||||
Some(_) => CredHandler::Denied("authentication denied"),
|
||||
None => {
|
||||
// We want the primary handler - this is where we make a decision
|
||||
// based on the anonymous ... in theory this could be cleaner
|
||||
// and interact with the account more?
|
||||
if account.is_anonymous() {
|
||||
CredHandler::Anonymous
|
||||
} else {
|
||||
// Now we see if they have one ...
|
||||
match &account.primary {
|
||||
Some(cred) => {
|
||||
// Probably means new authsession has to be failable
|
||||
CredHandler::try_from(cred).unwrap_or_else(|_| {
|
||||
lsecurity_critical!(
|
||||
au,
|
||||
"corrupt credentials, unable to start credhandler"
|
||||
);
|
||||
CredHandler::Denied("invalid credential state")
|
||||
})
|
||||
}
|
||||
None => {
|
||||
lsecurity!(au, "account has no primary credentials");
|
||||
CredHandler::Denied("invalid credential state")
|
||||
}
|
||||
}
|
||||
None => CredHandler::Denied,
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lsecurity!(au, "account expired");
|
||||
CredHandler::Denied(ACCOUNT_EXPIRED)
|
||||
};
|
||||
|
||||
// Is this handler locked?
|
||||
// Is the whole account locked?
|
||||
// What about in memory account locking? Is that something
|
||||
// we store in the account somehow?
|
||||
// TODO #59: Implement handler locking!
|
||||
|
||||
// if credhandler == deny, finish = true.
|
||||
let finished: bool = handler.is_denied();
|
||||
if let Some(reason) = handler.is_denied() {
|
||||
// Already denied, lets send that result
|
||||
(None, AuthState::Denied(reason.to_string()))
|
||||
} else {
|
||||
// We can proceed
|
||||
let auth_session = AuthSession {
|
||||
account,
|
||||
handler,
|
||||
appid,
|
||||
finished: false,
|
||||
};
|
||||
// Get the set of mechanisms that can proceed. This is tied
|
||||
// to the session so that it can mutate state and have progression
|
||||
// of what's next, or ordering.
|
||||
let next_mech = auth_session.valid_auth_mechs();
|
||||
|
||||
AuthSession {
|
||||
account,
|
||||
handler,
|
||||
appid,
|
||||
finished,
|
||||
let state = AuthState::Continue(next_mech);
|
||||
(Some(auth_session), state)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -407,6 +426,7 @@ impl AuthSession {
|
|||
"Credentials denied: potential flood/dos/bruteforce attempt. {} creds were sent.",
|
||||
creds.len()
|
||||
);
|
||||
self.finished = true;
|
||||
return Ok(AuthState::Denied(BAD_CREDENTIALS.to_string()));
|
||||
}
|
||||
|
||||
|
@ -448,7 +468,7 @@ impl AuthSession {
|
|||
// If success, to authtoken?
|
||||
}
|
||||
|
||||
pub fn valid_auth_mechs(&self) -> Vec<AuthAllowed> {
|
||||
fn valid_auth_mechs(&self) -> Vec<AuthAllowed> {
|
||||
if self.finished {
|
||||
Vec::new()
|
||||
} else {
|
||||
|
@ -468,6 +488,7 @@ mod tests {
|
|||
AuthSession, BAD_AUTH_TYPE_MSG, BAD_CREDENTIALS, BAD_PASSWORD_MSG, BAD_TOTP_MSG,
|
||||
};
|
||||
use crate::idm::AuthState;
|
||||
use crate::utils::duration_from_epoch_now;
|
||||
use kanidm_proto::v1::{AuthAllowed, AuthCredential};
|
||||
use std::time::Duration;
|
||||
// use async_std::task;
|
||||
|
@ -485,16 +506,19 @@ mod tests {
|
|||
|
||||
let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1);
|
||||
|
||||
let session = AuthSession::new(&mut audit, anon_account, None);
|
||||
let (_session, state) =
|
||||
AuthSession::new(&mut audit, anon_account, None, duration_from_epoch_now());
|
||||
|
||||
let auth_mechs = session.valid_auth_mechs();
|
||||
|
||||
assert!(
|
||||
true == auth_mechs.iter().fold(false, |acc, x| match x {
|
||||
AuthAllowed::Anonymous => true,
|
||||
_ => acc,
|
||||
})
|
||||
);
|
||||
if let AuthState::Continue(auth_mechs) = state {
|
||||
assert!(
|
||||
true == auth_mechs.iter().fold(false, |acc, x| match x {
|
||||
AuthAllowed::Anonymous => true,
|
||||
_ => acc,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
panic!("Invalid auth state")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -505,9 +529,13 @@ mod tests {
|
|||
None,
|
||||
);
|
||||
let anon_account = entry_str_to_account!(JSON_ANONYMOUS_V1);
|
||||
let mut session = AuthSession::new(&mut audit, anon_account, None);
|
||||
let (session, _) =
|
||||
AuthSession::new(&mut audit, anon_account, None, duration_from_epoch_now());
|
||||
let (async_tx, mut async_rx) = unbounded();
|
||||
|
||||
// Will be some.
|
||||
let mut session = session.unwrap();
|
||||
|
||||
let attempt = vec![
|
||||
AuthCredential::Anonymous,
|
||||
AuthCredential::Anonymous,
|
||||
|
@ -534,16 +562,20 @@ mod tests {
|
|||
None,
|
||||
);
|
||||
|
||||
let session = AuthSession::new(
|
||||
let (session, state) = AuthSession::new(
|
||||
&mut audit,
|
||||
anon_account,
|
||||
Some("NonExistantAppID".to_string()),
|
||||
duration_from_epoch_now(),
|
||||
);
|
||||
|
||||
let auth_mechs = session.valid_auth_mechs();
|
||||
assert!(session.is_none());
|
||||
|
||||
// Will always move to denied.
|
||||
assert!(auth_mechs == Vec::new());
|
||||
if let AuthState::Denied(_) = state {
|
||||
// Pass
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -561,16 +593,20 @@ mod tests {
|
|||
account.primary = Some(cred);
|
||||
|
||||
// now check
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
let (async_tx, mut async_rx) = unbounded();
|
||||
let auth_mechs = session.valid_auth_mechs();
|
||||
|
||||
assert!(
|
||||
true == auth_mechs.iter().fold(false, |acc, x| match x {
|
||||
AuthAllowed::Password => true,
|
||||
_ => acc,
|
||||
})
|
||||
);
|
||||
if let AuthState::Continue(auth_mechs) = state {
|
||||
assert!(
|
||||
true == auth_mechs.iter().fold(false, |acc, x| match x {
|
||||
AuthAllowed::Password => true,
|
||||
_ => acc,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
|
||||
let attempt = vec![AuthCredential::Password("bad_password".to_string())];
|
||||
match session.validate_creds(&mut audit, &attempt, &Duration::from_secs(0), &async_tx) {
|
||||
|
@ -578,7 +614,9 @@ mod tests {
|
|||
_ => panic!(),
|
||||
};
|
||||
|
||||
let mut session = AuthSession::new(&mut audit, account, None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account, None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
let attempt = vec![AuthCredential::Password("test_password".to_string())];
|
||||
match session.validate_creds(&mut audit, &attempt, &Duration::from_secs(0), &async_tx) {
|
||||
Ok(AuthState::Success(_)) => {}
|
||||
|
@ -624,20 +662,26 @@ mod tests {
|
|||
account.primary = Some(cred);
|
||||
|
||||
// now check
|
||||
let session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (_session, state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let (async_tx, mut async_rx) = unbounded();
|
||||
let auth_mechs = session.valid_auth_mechs();
|
||||
assert!(auth_mechs.iter().fold(true, |acc, x| match x {
|
||||
AuthAllowed::Password => acc,
|
||||
AuthAllowed::TOTP => acc,
|
||||
_ => false,
|
||||
}));
|
||||
if let AuthState::Continue(auth_mechs) = state {
|
||||
assert!(auth_mechs.iter().fold(true, |acc, x| match x {
|
||||
AuthAllowed::Password => acc,
|
||||
AuthAllowed::TOTP => acc,
|
||||
_ => false,
|
||||
}));
|
||||
} else {
|
||||
panic!();
|
||||
}
|
||||
|
||||
// Rest of test go here
|
||||
|
||||
// check send anon (fail)
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::Anonymous],
|
||||
|
@ -654,7 +698,9 @@ mod tests {
|
|||
// check send bad pw, should get continue (even though denied set)
|
||||
// then send good totp, should fail.
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::Password(pw_bad.to_string())],
|
||||
|
@ -677,7 +723,9 @@ mod tests {
|
|||
// check send bad pw, should get continue (even though denied set)
|
||||
// then send bad totp, should fail TOTP
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::Password(pw_bad.to_string())],
|
||||
|
@ -701,7 +749,9 @@ mod tests {
|
|||
// check send good pw, should get continue
|
||||
// then send good totp, success
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::Password(pw_good.to_string())],
|
||||
|
@ -725,7 +775,9 @@ mod tests {
|
|||
// check send good pw, should get continue
|
||||
// then send bad totp, fail otp
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::Password(pw_good.to_string())],
|
||||
|
@ -748,7 +800,9 @@ mod tests {
|
|||
|
||||
// check send bad totp, should fail immediate
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::TOTP(totp_bad)],
|
||||
|
@ -763,7 +817,9 @@ mod tests {
|
|||
// check send good totp, should continue
|
||||
// then bad pw, fail pw
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::TOTP(totp_good)],
|
||||
|
@ -787,7 +843,9 @@ mod tests {
|
|||
// check send good totp, should continue
|
||||
// then good pw, success
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![AuthCredential::TOTP(totp_good)],
|
||||
|
@ -812,7 +870,9 @@ mod tests {
|
|||
|
||||
// check bad totp, bad pw, fail totp.
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![
|
||||
|
@ -828,7 +888,9 @@ mod tests {
|
|||
}
|
||||
// check send bad pw, good totp fail password
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![
|
||||
|
@ -844,7 +906,9 @@ mod tests {
|
|||
}
|
||||
// check send good pw, bad totp fail totp.
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![
|
||||
|
@ -860,7 +924,9 @@ mod tests {
|
|||
}
|
||||
// check good pw, good totp, success
|
||||
{
|
||||
let mut session = AuthSession::new(&mut audit, account.clone(), None);
|
||||
let (session, _state) =
|
||||
AuthSession::new(&mut audit, account.clone(), None, duration_from_epoch_now());
|
||||
let mut session = session.unwrap();
|
||||
match session.validate_creds(
|
||||
&mut audit,
|
||||
&vec![
|
||||
|
|
|
@ -7,6 +7,8 @@ use crate::server::QueryServerReadTransaction;
|
|||
use crate::value::PartialValue;
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::RadiusAuthToken;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
lazy_static! {
|
||||
static ref PVCLASS_ACCOUNT: PartialValue = PartialValue::new_class("account");
|
||||
|
@ -19,6 +21,8 @@ pub(crate) struct RadiusAccount {
|
|||
pub uuid: Uuid,
|
||||
pub groups: Vec<Group>,
|
||||
pub radius_secret: String,
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl RadiusAccount {
|
||||
|
@ -58,16 +62,52 @@ impl RadiusAccount {
|
|||
|
||||
let groups = Group::try_from_account_entry_red_ro(au, &value, qs)?;
|
||||
|
||||
let valid_from = value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = value.get_ava_single_datetime("account_expire");
|
||||
|
||||
Ok(RadiusAccount {
|
||||
name,
|
||||
uuid,
|
||||
displayname,
|
||||
groups,
|
||||
radius_secret,
|
||||
valid_from,
|
||||
expire,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn to_radiusauthtoken(&self) -> Result<RadiusAuthToken, OperationError> {
|
||||
fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||
let cot = OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
let vmin = if let Some(vft) = &self.valid_from {
|
||||
// If current time greater than strat time window
|
||||
vft < &cot
|
||||
} else {
|
||||
// We have no time, not expired.
|
||||
true
|
||||
};
|
||||
let vmax = if let Some(ext) = &self.expire {
|
||||
// If exp greater than ct then expired.
|
||||
&cot < ext
|
||||
} else {
|
||||
// If not present, we are not expired
|
||||
true
|
||||
};
|
||||
// Mix the results
|
||||
vmin && vmax
|
||||
}
|
||||
|
||||
pub(crate) fn to_radiusauthtoken(
|
||||
&self,
|
||||
ct: Duration,
|
||||
) -> Result<RadiusAuthToken, OperationError> {
|
||||
if !self.is_within_valid_time(ct) {
|
||||
return Err(OperationError::InvalidAccountState(
|
||||
"Account Expired".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
// If we don't have access/permission, then just error instead.
|
||||
// This includes if we don't have the secret.
|
||||
Ok(RadiusAuthToken {
|
||||
|
|
|
@ -22,8 +22,6 @@ use crate::value::PartialValue;
|
|||
use crate::actors::v1_write::QueryServerWriteV1;
|
||||
use crate::idm::delayed::{DelayedAction, PasswordUpgrade, UnixPasswordUpgrade};
|
||||
|
||||
use crate::idm::AuthState;
|
||||
|
||||
use kanidm_proto::v1::OperationError;
|
||||
use kanidm_proto::v1::RadiusAuthToken;
|
||||
// use kanidm_proto::v1::TOTPSecret as ProtoTOTPSecret;
|
||||
|
@ -239,7 +237,6 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
ct: Duration,
|
||||
) -> Result<AuthResult, OperationError> {
|
||||
ltrace!(au, "Received -> {:?}", ae);
|
||||
|
||||
// Match on the auth event, to see what we need to do.
|
||||
|
||||
match &ae.step {
|
||||
|
@ -280,30 +277,30 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
// continue, and helps to keep non-needed entry specific data
|
||||
// out of the LRU.
|
||||
let account = Account::try_from_entry_ro(au, &entry, &mut self.qs_read)?;
|
||||
let auth_session = AuthSession::new(au, account, init.appid.clone());
|
||||
|
||||
// Get the set of mechanisms that can proceed. This is tied
|
||||
// to the session so that it can mutate state and have progression
|
||||
// of what's next, or ordering.
|
||||
let next_mech = auth_session.valid_auth_mechs();
|
||||
let (auth_session, state) =
|
||||
AuthSession::new(au, account, init.appid.clone(), ct);
|
||||
|
||||
match auth_session {
|
||||
Some(auth_session) => {
|
||||
lperf_segment!(au, "idm::server::auth<Init> -> sessions", || {
|
||||
if self.sessions.contains_key(&sessionid) {
|
||||
Err(OperationError::InvalidSessionState)
|
||||
} else {
|
||||
self.sessions.insert(sessionid, auth_session);
|
||||
// Debugging: ensure we really inserted ...
|
||||
debug_assert!(self.sessions.get(&sessionid).is_some());
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
}
|
||||
None => {
|
||||
lsecurity!(au, "Authentication Session Unable to begin");
|
||||
}
|
||||
};
|
||||
// If we have a session of the same id, return an error (despite how
|
||||
// unlikely this is ...
|
||||
lperf_segment!(au, "idm::server::auth<Init> -> sessions", || {
|
||||
if self.sessions.contains_key(&sessionid) {
|
||||
Err(OperationError::InvalidSessionState)
|
||||
} else {
|
||||
self.sessions.insert(sessionid, auth_session);
|
||||
// Debugging: ensure we really inserted ...
|
||||
debug_assert!(self.sessions.get(&sessionid).is_some());
|
||||
Ok(())
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(AuthResult {
|
||||
sessionid,
|
||||
state: AuthState::Continue(next_mech),
|
||||
})
|
||||
Ok(AuthResult { sessionid, state })
|
||||
})
|
||||
}
|
||||
AuthEventStep::Creds(creds) => {
|
||||
|
@ -338,7 +335,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
uae: &UnixUserAuthEvent,
|
||||
_ct: Duration,
|
||||
ct: Duration,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// TODO #59: Implement soft lock checking for unix creds here!
|
||||
|
||||
|
@ -355,14 +352,14 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
})?;
|
||||
|
||||
// Validate the unix_pw - this checks the account/cred lock states.
|
||||
account.verify_unix_credential(au, uae.cleartext.as_str(), &self.async_tx)
|
||||
account.verify_unix_credential(au, uae.cleartext.as_str(), &self.async_tx, ct)
|
||||
}
|
||||
|
||||
pub fn auth_ldap(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
lae: &LdapAuthEvent,
|
||||
_ct: Duration,
|
||||
ct: Duration,
|
||||
) -> Result<Option<LdapBoundToken>, OperationError> {
|
||||
// TODO #59: Implement soft lock checking for unix creds here!
|
||||
|
||||
|
@ -395,7 +392,7 @@ impl<'a> IdmServerWriteTransaction<'a> {
|
|||
let account =
|
||||
UnixUserAccount::try_from_entry_ro(au, &account_entry, &mut self.qs_read)?;
|
||||
if account
|
||||
.verify_unix_credential(au, lae.cleartext.as_str(), &self.async_tx)?
|
||||
.verify_unix_credential(au, lae.cleartext.as_str(), &self.async_tx, ct)?
|
||||
.is_some()
|
||||
{
|
||||
// Get the anon uat
|
||||
|
@ -438,6 +435,7 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
rate: &RadiusAuthTokenEvent,
|
||||
ct: Duration,
|
||||
) -> Result<RadiusAuthToken, OperationError> {
|
||||
let account = self
|
||||
.qs_read
|
||||
|
@ -450,26 +448,27 @@ impl<'a> IdmServerProxyReadTransaction<'a> {
|
|||
e
|
||||
})?;
|
||||
|
||||
account.to_radiusauthtoken()
|
||||
account.to_radiusauthtoken(ct)
|
||||
}
|
||||
|
||||
pub fn get_unixusertoken(
|
||||
&mut self,
|
||||
au: &mut AuditScope,
|
||||
uute: &UnixUserTokenEvent,
|
||||
ct: Duration,
|
||||
) -> Result<UnixUserToken, OperationError> {
|
||||
let account = self
|
||||
.qs_read
|
||||
.impersonate_search_ext_uuid(au, &uute.target, &uute.event)
|
||||
.impersonate_search_uuid(au, &uute.target, &uute.event)
|
||||
.and_then(|account_entry| {
|
||||
UnixUserAccount::try_from_entry_reduced(au, &account_entry, &mut self.qs_read)
|
||||
UnixUserAccount::try_from_entry_ro(au, &account_entry, &mut self.qs_read)
|
||||
})
|
||||
.map_err(|e| {
|
||||
ladmin_error!(au, "Failed to start unix user token -> {:?}", e);
|
||||
e
|
||||
})?;
|
||||
|
||||
account.to_unixusertoken()
|
||||
account.to_unixusertoken(ct)
|
||||
}
|
||||
|
||||
pub fn get_unixgrouptoken(
|
||||
|
@ -1423,7 +1422,7 @@ mod tests {
|
|||
let mut idms_prox_read = idms.proxy_read();
|
||||
let rate = RadiusAuthTokenEvent::new_internal(UUID_ADMIN.clone());
|
||||
let tok_r = idms_prox_read
|
||||
.get_radiusauthtoken(au, &rate)
|
||||
.get_radiusauthtoken(au, &rate, duration_from_epoch_now())
|
||||
.expect("Failed to generate radius auth token");
|
||||
|
||||
// view the token?
|
||||
|
@ -1519,7 +1518,7 @@ mod tests {
|
|||
|
||||
let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN.clone());
|
||||
let tok_r = idms_prox_read
|
||||
.get_unixusertoken(au, &uute)
|
||||
.get_unixusertoken(au, &uute, duration_from_epoch_now())
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
|
@ -1527,6 +1526,7 @@ mod tests {
|
|||
assert!(tok_r.groups.len() == 2);
|
||||
assert!(tok_r.groups[0].name == "admin");
|
||||
assert!(tok_r.groups[1].name == "testgroup");
|
||||
assert!(tok_r.valid == true);
|
||||
|
||||
// Show we can get the admin as a unix group token too
|
||||
let ugte = UnixGroupTokenEvent::new_internal(
|
||||
|
@ -1563,7 +1563,6 @@ mod tests {
|
|||
|
||||
let pce = UnixPasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
|
||||
assert!(idms_prox_write.set_unix_account_password(au, &pce).is_ok());
|
||||
assert!(idms_prox_write.set_unix_account_password(au, &pce).is_ok());
|
||||
assert!(idms_prox_write.commit(au).is_ok());
|
||||
|
||||
|
@ -1835,4 +1834,202 @@ mod tests {
|
|||
idms_delayed.is_empty_or_panic();
|
||||
})
|
||||
}
|
||||
|
||||
// For testing the timeouts
|
||||
// We need times on this scale
|
||||
// not yet valid <-> valid from time <-> current_time <-> expire time <-> expired
|
||||
const TEST_NOT_YET_VALID_TIME: u64 = TEST_CURRENT_TIME - 240;
|
||||
const TEST_VALID_FROM_TIME: u64 = TEST_CURRENT_TIME - 120;
|
||||
const TEST_EXPIRE_TIME: u64 = TEST_CURRENT_TIME + 120;
|
||||
const TEST_AFTER_EXPIRY: u64 = TEST_CURRENT_TIME + 240;
|
||||
|
||||
fn set_admin_valid_time(au: &mut AuditScope, qs: &QueryServer) {
|
||||
let qs_write = qs.write(duration_from_epoch_now());
|
||||
|
||||
let v_valid_from = Value::new_datetime_epoch(Duration::from_secs(TEST_VALID_FROM_TIME));
|
||||
let v_expire = Value::new_datetime_epoch(Duration::from_secs(TEST_EXPIRE_TIME));
|
||||
|
||||
// now modify and provide a primary credential.
|
||||
let me_inv_m = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq("name", PartialValue::new_iname("admin"))),
|
||||
ModifyList::new_list(vec![
|
||||
Modify::Present("account_expire".to_string(), v_expire),
|
||||
Modify::Present("account_valid_from".to_string(), v_valid_from),
|
||||
]),
|
||||
)
|
||||
};
|
||||
// go!
|
||||
assert!(qs_write.modify(au, &me_inv_m).is_ok());
|
||||
|
||||
qs_write.commit(au).expect("Must not fail");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_account_valid_from_expire() {
|
||||
run_idm_test!(|qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
au: &mut AuditScope| {
|
||||
// Any account taht is not yet valrid / expired can't auth.
|
||||
|
||||
init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
// Set the valid bounds high/low
|
||||
// TEST_VALID_FROM_TIME/TEST_EXPIRE_TIME
|
||||
set_admin_valid_time(au, qs);
|
||||
|
||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
||||
let mut idms_write = idms.write();
|
||||
let admin_init = AuthEvent::named_init("admin");
|
||||
let r1 = idms_write.auth(au, &admin_init, time_low);
|
||||
|
||||
let ar = r1.unwrap();
|
||||
let AuthResult {
|
||||
sessionid: _,
|
||||
state,
|
||||
} = ar;
|
||||
|
||||
match state {
|
||||
AuthState::Denied(_) => {}
|
||||
_ => {
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
|
||||
idms_write.commit(au).expect("Must not fail");
|
||||
|
||||
// And here!
|
||||
let mut idms_write = idms.write();
|
||||
let admin_init = AuthEvent::named_init("admin");
|
||||
let r1 = idms_write.auth(au, &admin_init, time_high);
|
||||
|
||||
let ar = r1.unwrap();
|
||||
let AuthResult {
|
||||
sessionid: _,
|
||||
state,
|
||||
} = ar;
|
||||
|
||||
match state {
|
||||
AuthState::Denied(_) => {}
|
||||
_ => {
|
||||
panic!();
|
||||
}
|
||||
};
|
||||
|
||||
idms_write.commit(au).expect("Must not fail");
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_unix_valid_from_expire() {
|
||||
run_idm_test!(|qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
au: &mut AuditScope| {
|
||||
// Any account that is expired can't unix auth.
|
||||
init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
set_admin_valid_time(au, qs);
|
||||
|
||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
||||
// make the admin a valid posix account
|
||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now());
|
||||
let me_posix = unsafe {
|
||||
ModifyEvent::new_internal_invalid(
|
||||
filter!(f_eq("name", PartialValue::new_iname("admin"))),
|
||||
ModifyList::new_list(vec![
|
||||
Modify::Present("class".to_string(), Value::new_class("posixaccount")),
|
||||
Modify::Present("gidnumber".to_string(), Value::new_uint32(2001)),
|
||||
]),
|
||||
)
|
||||
};
|
||||
assert!(idms_prox_write.qs_write.modify(au, &me_posix).is_ok());
|
||||
|
||||
let pce = UnixPasswordChangeEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
|
||||
assert!(idms_prox_write.set_unix_account_password(au, &pce).is_ok());
|
||||
assert!(idms_prox_write.commit(au).is_ok());
|
||||
|
||||
// Now check auth when the time is too high or too low.
|
||||
let mut idms_write = idms.write();
|
||||
let uuae_good = UnixUserAuthEvent::new_internal(&UUID_ADMIN, TEST_PASSWORD);
|
||||
|
||||
let a1 = idms_write.auth_unix(au, &uuae_good, time_low);
|
||||
// Should this actually send an error with the details? Or just silently act as
|
||||
// badpw?
|
||||
match a1 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
let a2 = idms_write.auth_unix(au, &uuae_good, time_high);
|
||||
match a2 {
|
||||
Ok(None) => {}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
idms_write.commit(au).expect("Must not fail");
|
||||
// Also check the generated unix tokens are invalid.
|
||||
let mut idms_prox_read = idms.proxy_read();
|
||||
let uute = UnixUserTokenEvent::new_internal(UUID_ADMIN.clone());
|
||||
|
||||
let tok_r = idms_prox_read
|
||||
.get_unixusertoken(au, &uute, time_low)
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
assert!(tok_r.valid == false);
|
||||
|
||||
let tok_r = idms_prox_read
|
||||
.get_unixusertoken(au, &uute, time_high)
|
||||
.expect("Failed to generate unix user token");
|
||||
|
||||
assert!(tok_r.name == "admin");
|
||||
assert!(tok_r.valid == false);
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_radius_valid_from_expire() {
|
||||
run_idm_test!(|qs: &QueryServer,
|
||||
idms: &IdmServer,
|
||||
_idms_delayed: &mut IdmServerDelayed,
|
||||
au: &mut AuditScope| {
|
||||
// Any account not valid/expiry should not return
|
||||
// a radius packet.
|
||||
init_admin_w_password(au, qs, TEST_PASSWORD).expect("Failed to setup admin account");
|
||||
set_admin_valid_time(au, qs);
|
||||
|
||||
let time_low = Duration::from_secs(TEST_NOT_YET_VALID_TIME);
|
||||
let time_high = Duration::from_secs(TEST_AFTER_EXPIRY);
|
||||
|
||||
let mut idms_prox_write = idms.proxy_write(duration_from_epoch_now());
|
||||
let rrse = RegenerateRadiusSecretEvent::new_internal(UUID_ADMIN.clone());
|
||||
let _r1 = idms_prox_write
|
||||
.regenerate_radius_secret(au, &rrse)
|
||||
.expect("Failed to reset radius credential 1");
|
||||
idms_prox_write.commit(au).expect("failed to commit");
|
||||
|
||||
let mut idms_prox_read = idms.proxy_read();
|
||||
let rate = RadiusAuthTokenEvent::new_internal(UUID_ADMIN.clone());
|
||||
let tok_r = idms_prox_read.get_radiusauthtoken(au, &rate, time_low);
|
||||
|
||||
if let Err(_) = tok_r {
|
||||
// Ok?
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
|
||||
let tok_r = idms_prox_read.get_radiusauthtoken(au, &rate, time_high);
|
||||
|
||||
if let Err(_) = tok_r {
|
||||
// Ok?
|
||||
} else {
|
||||
assert!(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,8 @@ use kanidm_proto::v1::{UnixGroupToken, UnixUserToken};
|
|||
use crate::idm::delayed::{DelayedAction, UnixPasswordUpgrade};
|
||||
|
||||
// use crossbeam::channel::Sender;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::UnboundedSender as Sender;
|
||||
|
||||
use std::iter;
|
||||
|
@ -31,6 +33,8 @@ pub(crate) struct UnixUserAccount {
|
|||
pub sshkeys: Vec<String>,
|
||||
pub groups: Vec<UnixGroup>,
|
||||
cred: Option<Credential>,
|
||||
pub valid_from: Option<OffsetDateTime>,
|
||||
pub expire: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
@ -94,6 +98,10 @@ macro_rules! try_from_entry {
|
|||
.get_ava_single_credential("unix_password")
|
||||
.map(|v| v.clone());
|
||||
|
||||
let valid_from = $value.get_ava_single_datetime("account_valid_from");
|
||||
|
||||
let expire = $value.get_ava_single_datetime("account_expire");
|
||||
|
||||
Ok(UnixUserAccount {
|
||||
name,
|
||||
spn,
|
||||
|
@ -104,6 +112,8 @@ macro_rules! try_from_entry {
|
|||
sshkeys,
|
||||
groups: $groups,
|
||||
cred,
|
||||
valid_from,
|
||||
expire,
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
@ -127,6 +137,7 @@ impl UnixUserAccount {
|
|||
try_from_entry!(value, groups)
|
||||
}
|
||||
|
||||
/*
|
||||
pub(crate) fn try_from_entry_reduced(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
|
@ -135,8 +146,9 @@ impl UnixUserAccount {
|
|||
let groups = UnixGroup::try_from_account_entry_red_ro(au, value, qs)?;
|
||||
try_from_entry!(value, groups)
|
||||
}
|
||||
*/
|
||||
|
||||
pub(crate) fn to_unixusertoken(&self) -> Result<UnixUserToken, OperationError> {
|
||||
pub(crate) fn to_unixusertoken(&self, ct: Duration) -> Result<UnixUserToken, OperationError> {
|
||||
let groups: Result<Vec<_>, _> = self.groups.iter().map(|g| g.to_unixgrouptoken()).collect();
|
||||
let groups = groups?;
|
||||
|
||||
|
@ -149,6 +161,7 @@ impl UnixUserAccount {
|
|||
shell: self.shell.clone(),
|
||||
groups,
|
||||
sshkeys: self.sshkeys.clone(),
|
||||
valid: self.is_within_valid_time(ct),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -166,13 +179,41 @@ impl UnixUserAccount {
|
|||
Ok(ModifyList::new_purge_and_set("unix_password", vcred))
|
||||
}
|
||||
|
||||
fn is_within_valid_time(&self, ct: Duration) -> bool {
|
||||
let cot = OffsetDateTime::unix_epoch() + ct;
|
||||
|
||||
let vmin = if let Some(vft) = &self.valid_from {
|
||||
// If current time greater than start time window
|
||||
vft < &cot
|
||||
} else {
|
||||
// We have no time, not expired.
|
||||
true
|
||||
};
|
||||
let vmax = if let Some(ext) = &self.expire {
|
||||
// If exp greater than ct then expired.
|
||||
&cot < ext
|
||||
} else {
|
||||
// If not present, we are not expired
|
||||
true
|
||||
};
|
||||
// Mix the results
|
||||
vmin && vmax
|
||||
}
|
||||
|
||||
pub(crate) fn verify_unix_credential(
|
||||
&self,
|
||||
au: &mut AuditScope,
|
||||
cleartext: &str,
|
||||
async_tx: &Sender<DelayedAction>,
|
||||
ct: Duration,
|
||||
) -> Result<Option<UnixUserToken>, OperationError> {
|
||||
// TODO #59: Is the cred locked?
|
||||
// Is the cred locked?
|
||||
|
||||
if !self.is_within_valid_time(ct) {
|
||||
lsecurity!(au, "Account is not within valid time period");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// is the cred some or none?
|
||||
match &self.cred {
|
||||
Some(cred) => {
|
||||
|
@ -192,7 +233,9 @@ impl UnixUserAccount {
|
|||
})?;
|
||||
}
|
||||
|
||||
Some(self.to_unixusertoken()).transpose()
|
||||
// Technically this means we check the times twice, but that doesn't
|
||||
// seem like a big deal when we want to short cut return on invalid.
|
||||
Some(self.to_unixusertoken(ct)).transpose()
|
||||
} else {
|
||||
// Failed to auth
|
||||
lsecurity!(au, "Failed unix cred handling (denied)");
|
||||
|
@ -365,6 +408,7 @@ impl UnixGroup {
|
|||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
|
||||
/*
|
||||
pub fn try_from_account_entry_red_ro(
|
||||
au: &mut AuditScope,
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
|
@ -372,6 +416,7 @@ impl UnixGroup {
|
|||
) -> Result<Vec<Self>, OperationError> {
|
||||
try_from_account_group_e!(au, value, qs)
|
||||
}
|
||||
*/
|
||||
|
||||
pub fn try_from_entry_reduced(
|
||||
value: &Entry<EntryReduced, EntryCommitted>,
|
||||
|
|
|
@ -200,6 +200,7 @@ impl SchemaAttribute {
|
|||
SyntaxType::UINT32 => v.is_uint32(),
|
||||
SyntaxType::CID => v.is_cid(),
|
||||
SyntaxType::NSUNIQUEID => v.is_nsuniqueid(),
|
||||
SyntaxType::DATETIME => v.is_datetime(),
|
||||
};
|
||||
if r {
|
||||
Ok(())
|
||||
|
@ -375,6 +376,15 @@ impl SchemaAttribute {
|
|||
}
|
||||
})
|
||||
}),
|
||||
SyntaxType::DATETIME => ava.iter().fold(Ok(()), |acc, v| {
|
||||
acc.and_then(|_| {
|
||||
if v.is_datetime() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SchemaError::InvalidAttributeSyntax(a.to_string()))
|
||||
}
|
||||
})
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -335,9 +335,8 @@ pub trait QueryServerTransaction {
|
|||
}
|
||||
|
||||
// Who they are will go here
|
||||
/*
|
||||
fn impersonate_search(
|
||||
&mut self,
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
filter: Filter<FilterInvalid>,
|
||||
filter_intent: Filter<FilterInvalid>,
|
||||
|
@ -351,7 +350,6 @@ pub trait QueryServerTransaction {
|
|||
.map_err(OperationError::SchemaViolation)?;
|
||||
self.impersonate_search_valid(audit, f_valid, f_intent_valid, event)
|
||||
}
|
||||
*/
|
||||
|
||||
fn impersonate_search_ext(
|
||||
&self,
|
||||
|
@ -423,6 +421,30 @@ pub trait QueryServerTransaction {
|
|||
})
|
||||
}
|
||||
|
||||
fn impersonate_search_uuid(
|
||||
&self,
|
||||
audit: &mut AuditScope,
|
||||
uuid: &Uuid,
|
||||
event: &Event,
|
||||
) -> Result<Entry<EntrySealed, EntryCommitted>, OperationError> {
|
||||
lperf_segment!(audit, "server::internal_search_uuid", || {
|
||||
let filter_intent = filter_all!(f_eq("uuid", PartialValue::new_uuid(*uuid)));
|
||||
let filter = filter!(f_eq("uuid", PartialValue::new_uuid(*uuid)));
|
||||
let res = self.impersonate_search(audit, filter, filter_intent, event);
|
||||
match res {
|
||||
Ok(vs) => {
|
||||
if vs.len() > 1 {
|
||||
return Err(OperationError::NoMatchingEntries);
|
||||
}
|
||||
vs.into_iter()
|
||||
.next()
|
||||
.ok_or(OperationError::NoMatchingEntries)
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Do a schema aware conversion from a String:String to String:Value for modification
|
||||
/// present.
|
||||
fn clone_value(
|
||||
|
@ -490,6 +512,8 @@ pub trait QueryServerTransaction {
|
|||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid uint32 syntax".to_string())),
|
||||
SyntaxType::CID => Err(OperationError::InvalidAttribute("CIDs are generated and not able to be set.".to_string())),
|
||||
SyntaxType::NSUNIQUEID => Ok(Value::new_nsuniqueid_s(value)),
|
||||
SyntaxType::DATETIME => Value::new_datetime_s(value)
|
||||
.ok_or_else(|| OperationError::InvalidAttribute("Invalid DateTime (rfc3339) syntax".to_string())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -576,6 +600,11 @@ pub trait QueryServerTransaction {
|
|||
OperationError::InvalidAttribute("Invalid cid syntax".to_string())
|
||||
}),
|
||||
SyntaxType::NSUNIQUEID => Ok(PartialValue::new_nsuniqueid_s(value)),
|
||||
SyntaxType::DATETIME => PartialValue::new_datetime_s(value).ok_or_else(|| {
|
||||
OperationError::InvalidAttribute(
|
||||
"Invalid DateTime (rfc3339) syntax".to_string(),
|
||||
)
|
||||
}),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -1964,6 +1993,8 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
JSON_SCHEMA_ATTR_BADLIST_PASSWORD,
|
||||
JSON_SCHEMA_ATTR_LOGINSHELL,
|
||||
JSON_SCHEMA_ATTR_UNIX_PASSWORD,
|
||||
JSON_SCHEMA_ATTR_ACCOUNT_EXPIRE,
|
||||
JSON_SCHEMA_ATTR_ACCOUNT_VALID_FROM,
|
||||
JSON_SCHEMA_CLASS_PERSON,
|
||||
JSON_SCHEMA_CLASS_GROUP,
|
||||
JSON_SCHEMA_CLASS_ACCOUNT,
|
||||
|
|
|
@ -7,6 +7,8 @@ use std::borrow::Borrow;
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use sshkeys::PublicKey as SshPublicKey;
|
||||
|
@ -124,6 +126,7 @@ pub enum SyntaxType {
|
|||
UINT32,
|
||||
CID,
|
||||
NSUNIQUEID,
|
||||
DATETIME,
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for SyntaxType {
|
||||
|
@ -148,6 +151,7 @@ impl TryFrom<&str> for SyntaxType {
|
|||
"UINT32" => Ok(SyntaxType::UINT32),
|
||||
"CID" => Ok(SyntaxType::CID),
|
||||
"NSUNIQUEID" => Ok(SyntaxType::NSUNIQUEID),
|
||||
"DATETIME" => Ok(SyntaxType::DATETIME),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +178,7 @@ impl TryFrom<usize> for SyntaxType {
|
|||
13 => Ok(SyntaxType::CID),
|
||||
14 => Ok(SyntaxType::UTF8STRING_INAME),
|
||||
15 => Ok(SyntaxType::NSUNIQUEID),
|
||||
16 => Ok(SyntaxType::DATETIME),
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
@ -198,6 +203,7 @@ impl SyntaxType {
|
|||
SyntaxType::CID => 13,
|
||||
SyntaxType::UTF8STRING_INAME => 14,
|
||||
SyntaxType::NSUNIQUEID => 15,
|
||||
SyntaxType::DATETIME => 16,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -224,6 +230,7 @@ impl fmt::Display for SyntaxType {
|
|||
SyntaxType::UINT32 => "UINT32",
|
||||
SyntaxType::CID => "CID",
|
||||
SyntaxType::NSUNIQUEID => "NSUNIQUEID",
|
||||
SyntaxType::DATETIME => "DATETIME",
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -267,6 +274,7 @@ pub enum PartialValue {
|
|||
Uint32(u32),
|
||||
Cid(Cid),
|
||||
Nsuniqueid(String),
|
||||
DateTime(OffsetDateTime),
|
||||
}
|
||||
|
||||
impl PartialValue {
|
||||
|
@ -529,6 +537,24 @@ impl PartialValue {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn new_datetime_epoch(ts: Duration) -> Self {
|
||||
PartialValue::DateTime(OffsetDateTime::unix_epoch() + ts)
|
||||
}
|
||||
|
||||
pub fn new_datetime_s(s: &str) -> Option<Self> {
|
||||
OffsetDateTime::parse(s, time::Format::Rfc3339)
|
||||
.ok()
|
||||
.map(|odt| odt.to_offset(time::UtcOffset::UTC))
|
||||
.map(PartialValue::DateTime)
|
||||
}
|
||||
|
||||
pub fn is_datetime(&self) -> bool {
|
||||
match self {
|
||||
PartialValue::DateTime(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
PartialValue::Utf8(s) => Some(s.as_str()),
|
||||
|
@ -578,6 +604,10 @@ impl PartialValue {
|
|||
PartialValue::Uint32(u) => u.to_string(),
|
||||
// This will never work, we don't allow equality searching on Cid's
|
||||
PartialValue::Cid(_) => "_".to_string(),
|
||||
PartialValue::DateTime(odt) => {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1041,6 +1071,31 @@ impl Value {
|
|||
self.pv.is_nsuniqueid()
|
||||
}
|
||||
|
||||
pub fn new_datetime_epoch(ts: Duration) -> Self {
|
||||
Value {
|
||||
pv: PartialValue::new_datetime_epoch(ts),
|
||||
data: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_datetime_s(s: &str) -> Option<Self> {
|
||||
PartialValue::new_datetime_s(s).map(|pv| Value { pv, data: None })
|
||||
}
|
||||
|
||||
pub fn to_datetime(&self) -> Option<OffsetDateTime> {
|
||||
match &self.pv {
|
||||
PartialValue::DateTime(odt) => {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
Some(*odt)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_datetime(&self) -> bool {
|
||||
self.pv.is_datetime()
|
||||
}
|
||||
|
||||
pub fn contains(&self, s: &PartialValue) -> bool {
|
||||
self.pv.contains(s)
|
||||
}
|
||||
|
@ -1133,6 +1188,9 @@ impl Value {
|
|||
pv: PartialValue::Nsuniqueid(s),
|
||||
data: None,
|
||||
}),
|
||||
DbValueV1::DT(s) => PartialValue::new_datetime_s(&s)
|
||||
.ok_or(())
|
||||
.map(|pv| Value { pv, data: None }),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1200,6 +1258,10 @@ impl Value {
|
|||
t: c.ts,
|
||||
}),
|
||||
PartialValue::Nsuniqueid(s) => DbValueV1::NU(s.clone()),
|
||||
PartialValue::DateTime(odt) => {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
DbValueV1::DT(odt.format(time::Format::Rfc3339))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1328,6 +1390,10 @@ impl Value {
|
|||
PartialValue::Spn(n, r) => format!("{}@{}", n, r),
|
||||
PartialValue::Uint32(u) => u.to_string(),
|
||||
PartialValue::Cid(c) => format!("{:?}_{}_{}", c.ts, c.d_uuid, c.s_uuid),
|
||||
PartialValue::DateTime(odt) => {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
odt.format(time::Format::Rfc3339)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1368,6 +1434,7 @@ impl Value {
|
|||
None => false,
|
||||
},
|
||||
PartialValue::Nsuniqueid(s) => NSUNIQUEID_RE.is_match(s),
|
||||
PartialValue::DateTime(odt) => odt.offset() == time::UtcOffset::UTC,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
@ -1396,6 +1463,10 @@ impl Value {
|
|||
PartialValue::Spn(n, r) => vec![format!("{}@{}", n, r)],
|
||||
PartialValue::Uint32(u) => vec![u.to_string()],
|
||||
PartialValue::Cid(_) => vec![],
|
||||
PartialValue::DateTime(odt) => {
|
||||
debug_assert!(odt.offset() == time::UtcOffset::UTC);
|
||||
vec![odt.format(time::Format::Rfc3339)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1586,6 +1657,34 @@ mod tests {
|
|||
assert!(val2.validate());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_value_datetime() {
|
||||
// Datetimes must always convert to UTC, and must always be rfc3339
|
||||
let val1 = Value::new_datetime_s("2020-09-25T11:22:02+10:00").expect("Must be valid");
|
||||
assert!(val1.validate());
|
||||
let val2 = Value::new_datetime_s("2020-09-25T01:22:02+00:00").expect("Must be valid");
|
||||
assert!(val2.validate());
|
||||
assert!(Value::new_datetime_s("2020-09-25T01:22:02").is_none());
|
||||
assert!(Value::new_datetime_s("2020-09-25").is_none());
|
||||
assert!(Value::new_datetime_s("2020-09-25T01:22:02+10").is_none());
|
||||
assert!(Value::new_datetime_s("2020-09-25 01:22:02+00:00").is_none());
|
||||
|
||||
// Manually craft
|
||||
let inv1 = Value {
|
||||
pv: PartialValue::DateTime(
|
||||
OffsetDateTime::now_utc().to_offset(time::UtcOffset::east_hours(10)),
|
||||
),
|
||||
data: None,
|
||||
};
|
||||
assert!(!inv1.validate());
|
||||
|
||||
let val3 = Value {
|
||||
pv: PartialValue::DateTime(OffsetDateTime::now_utc()),
|
||||
data: None,
|
||||
};
|
||||
assert!(val3.validate());
|
||||
}
|
||||
|
||||
/*
|
||||
#[test]
|
||||
fn test_schema_syntax_json_filter() {
|
||||
|
|
Loading…
Reference in a new issue