Compare commits

...

6 Commits

Author SHA1 Message Date
92d04f3312 Fix bad comment
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 19:14:46 +02:00
abd86ff22d Can set authorized authentication providers for a given account 2023-04-24 19:13:36 +02:00
f64f01a958 Can block local login for an account
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 18:46:21 +02:00
96ffc669d7 Add logo of popular brands
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 16:07:14 +02:00
d9f659ce98 Add basic providers configuration
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 15:43:49 +02:00
e73b5b8e5b Update dependencies
All checks were successful
continuous-integration/drone/push Build is passing
2023-04-24 15:14:10 +02:00
20 changed files with 392 additions and 43 deletions

52
Cargo.lock generated
View File

@@ -317,9 +317,9 @@ dependencies = [
[[package]]
name = "aho-corasick"
version = "0.7.20"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
dependencies = [
"memchr",
]
@@ -664,9 +664,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.12.0"
version = "3.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535"
checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
[[package]]
name = "bytemuck"
@@ -737,9 +737,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.2.2"
version = "4.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b802d85aaf3a1cdb02b224ba472ebdea62014fccfcb269b95a4d76443b5ee5a"
checksum = "956ac1f6381d8d82ab4684768f89c0ea3afe66925ceadb4eeb3fc452ffc55d62"
dependencies = [
"clap_builder",
"clap_derive",
@@ -748,9 +748,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.2.2"
version = "4.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14a1a858f532119338887a4b8e1af9c60de8249cd7bafd68036a489e261e37b6"
checksum = "84080e799e54cff944f4b4a4b0e71630b0e0443b25b985175c7dddc1a859b749"
dependencies = [
"anstream",
"anstyle",
@@ -866,9 +866,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cpufeatures"
version = "0.2.6"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181"
checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58"
dependencies = [
"libc",
]
@@ -1283,9 +1283,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.3.17"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66b91535aa35fea1523ad1b86cb6b53c28e0ae566ba4a460f4457e936cad7c6f"
checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21"
dependencies = [
"bytes",
"fnv",
@@ -1641,9 +1641,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.141"
version = "0.2.142"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
[[package]]
name = "libm"
@@ -1662,9 +1662,9 @@ dependencies = [
[[package]]
name = "linux-raw-sys"
version = "0.3.1"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f"
checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf"
[[package]]
name = "local-channel"
@@ -1875,9 +1875,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.50"
version = "0.10.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1"
checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3"
dependencies = [
"bitflags",
"cfg-if",
@@ -1901,9 +1901,9 @@ dependencies = [
[[package]]
name = "openssl-sys"
version = "0.9.85"
version = "0.9.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0"
checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69"
dependencies = [
"cc",
"libc",
@@ -2135,9 +2135,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.7.3"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d"
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
dependencies = [
"aho-corasick",
"memchr",
@@ -2146,9 +2146,9 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.6.29"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
[[package]]
name = "rfc6979"
@@ -2217,9 +2217,9 @@ dependencies = [
[[package]]
name = "rustix"
version = "0.37.11"
version = "0.37.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77"
checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f"
dependencies = [
"bitflags",
"errno",

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg version="1.1" id="main_outline" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 640 640" style="enable-background:new 0 0 640 640;" xml:space="preserve"><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-custom-link"/><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-general-link"/><style xmlns="" lang="en" type="text/css" id="dark-mode-custom-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-sheet"/>
<g>
<path id="teabag" style="fill:#FFFFFF" d="M395.9,484.2l-126.9-61c-12.5-6-17.9-21.2-11.8-33.8l61-126.9c6-12.5,21.2-17.9,33.8-11.8 c17.2,8.3,27.1,13,27.1,13l-0.1-109.2l16.7-0.1l0.1,117.1c0,0,57.4,24.2,83.1,40.1c3.7,2.3,10.2,6.8,12.9,14.4 c2.1,6.1,2,13.1-1,19.3l-61,126.9C423.6,484.9,408.4,490.3,395.9,484.2z"/>
<g>
<g>
<path style="fill:#609926" d="M622.7,149.8c-4.1-4.1-9.6-4-9.6-4s-117.2,6.6-177.9,8c-13.3,0.3-26.5,0.6-39.6,0.7c0,39.1,0,78.2,0,117.2 c-5.5-2.6-11.1-5.3-16.6-7.9c0-36.4-0.1-109.2-0.1-109.2c-29,0.4-89.2-2.2-89.2-2.2s-141.4-7.1-156.8-8.5 c-9.8-0.6-22.5-2.1-39,1.5c-8.7,1.8-33.5,7.4-53.8,26.9C-4.9,212.4,6.6,276.2,8,285.8c1.7,11.7,6.9,44.2,31.7,72.5 c45.8,56.1,144.4,54.8,144.4,54.8s12.1,28.9,30.6,55.5c25,33.1,50.7,58.9,75.7,62c63,0,188.9-0.1,188.9-0.1s12,0.1,28.3-10.3 c14-8.5,26.5-23.4,26.5-23.4s12.9-13.8,30.9-45.3c5.5-9.7,10.1-19.1,14.1-28c0,0,55.2-117.1,55.2-231.1 C633.2,157.9,624.7,151.8,622.7,149.8z M125.6,353.9c-25.9-8.5-36.9-18.7-36.9-18.7S69.6,321.8,60,295.4 c-16.5-44.2-1.4-71.2-1.4-71.2s8.4-22.5,38.5-30c13.8-3.7,31-3.1,31-3.1s7.1,59.4,15.7,94.2c7.2,29.2,24.8,77.7,24.8,77.7 S142.5,359.9,125.6,353.9z M425.9,461.5c0,0-6.1,14.5-19.6,15.4c-5.8,0.4-10.3-1.2-10.3-1.2s-0.3-0.1-5.3-2.1l-112.9-55 c0,0-10.9-5.7-12.8-15.6c-2.2-8.1,2.7-18.1,2.7-18.1L322,273c0,0,4.8-9.7,12.2-13c0.6-0.3,2.3-1,4.5-1.5c8.1-2.1,18,2.8,18,2.8 l110.7,53.7c0,0,12.6,5.7,15.3,16.2c1.9,7.4-0.5,14-1.8,17.2C474.6,363.8,425.9,461.5,425.9,461.5z"/>
<path style="fill:#609926" d="M326.8,380.1c-8.2,0.1-15.4,5.8-17.3,13.8c-1.9,8,2,16.3,9.1,20c7.7,4,17.5,1.8,22.7-5.4 c5.1-7.1,4.3-16.9-1.8-23.1l24-49.1c1.5,0.1,3.7,0.2,6.2-0.5c4.1-0.9,7.1-3.6,7.1-3.6c4.2,1.8,8.6,3.8,13.2,6.1 c4.8,2.4,9.3,4.9,13.4,7.3c0.9,0.5,1.8,1.1,2.8,1.9c1.6,1.3,3.4,3.1,4.7,5.5c1.9,5.5-1.9,14.9-1.9,14.9 c-2.3,7.6-18.4,40.6-18.4,40.6c-8.1-0.2-15.3,5-17.7,12.5c-2.6,8.1,1.1,17.3,8.9,21.3c7.8,4,17.4,1.7,22.5-5.3 c5-6.8,4.6-16.3-1.1-22.6c1.9-3.7,3.7-7.4,5.6-11.3c5-10.4,13.5-30.4,13.5-30.4c0.9-1.7,5.7-10.3,2.7-21.3 c-2.5-11.4-12.6-16.7-12.6-16.7c-12.2-7.9-29.2-15.2-29.2-15.2s0-4.1-1.1-7.1c-1.1-3.1-2.8-5.1-3.9-6.3c4.7-9.7,9.4-19.3,14.1-29 c-4.1-2-8.1-4-12.2-6.1c-4.8,9.8-9.7,19.7-14.5,29.5c-6.7-0.1-12.9,3.5-16.1,9.4c-3.4,6.3-2.7,14.1,1.9,19.8 C343.2,346.5,335,363.3,326.8,380.1z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -0,0 +1 @@
<svg width="98" height="96" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/></svg>

After

Width:  |  Height:  |  Size: 960 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 380 380"><defs><style>.cls-1{fill:#e24329;}.cls-2{fill:#fc6d26;}.cls-3{fill:#fca326;}</style></defs><g id="LOGO"><path class="cls-1" d="M282.83,170.73l-.27-.69-26.14-68.22a6.81,6.81,0,0,0-2.69-3.24,7,7,0,0,0-8,.43,7,7,0,0,0-2.32,3.52l-17.65,54H154.29l-17.65-54A6.86,6.86,0,0,0,134.32,99a7,7,0,0,0-8-.43,6.87,6.87,0,0,0-2.69,3.24L97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82,19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91,40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-2" d="M282.83,170.73l-.27-.69a88.3,88.3,0,0,0-35.15,15.8L190,229.25c19.55,14.79,36.57,27.64,36.57,27.64l40.06-30,.1-.08A48.56,48.56,0,0,0,282.83,170.73Z"/><path class="cls-3" d="M153.43,256.89l19.7,14.91,12,9.06a8.07,8.07,0,0,0,9.76,0l12-9.06,19.7-14.91S209.55,244,190,229.25C170.45,244,153.43,256.89,153.43,256.89Z"/><path class="cls-2" d="M132.58,185.84A88.19,88.19,0,0,0,97.44,170l-.26.69a48.54,48.54,0,0,0,16.1,56.1l.09.07.24.17,39.82,29.82s17-12.85,36.57-27.64Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="705.6" height="720" viewBox="0 0 186.69 190.5" xmlns:v="https://vecta.io/nano"><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-custom-link"/><link xmlns="" type="text/css" rel="stylesheet" id="dark-mode-general-link"/><style xmlns="" lang="en" type="text/css" id="dark-mode-custom-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-style"/><style xmlns="" lang="en" type="text/css" id="dark-mode-native-sheet"/><g transform="translate(1184.583 765.171)"><path clip-path="none" mask="none" d="M-1089.333-687.239v36.888h51.262c-2.251 11.863-9.006 21.908-19.137 28.662l30.913 23.986c18.011-16.625 28.402-41.044 28.402-70.052 0-6.754-.606-13.249-1.732-19.483z" fill="#4285f4"/><path clip-path="none" mask="none" d="M-1142.714-651.791l-6.972 5.337-24.679 19.223h0c15.673 31.086 47.796 52.561 85.03 52.561 25.717 0 47.278-8.486 63.038-23.033l-30.913-23.986c-8.486 5.715-19.31 9.179-32.125 9.179-24.765 0-45.806-16.712-53.34-39.226z" fill="#34a853"/><path clip-path="none" mask="none" d="M-1174.365-712.61c-6.494 12.815-10.217 27.276-10.217 42.689s3.723 29.874 10.217 42.689c0 .086 31.693-24.592 31.693-24.592-1.905-5.715-3.031-11.776-3.031-18.098s1.126-12.383 3.031-18.098z" fill="#fbbc05"/><path d="M-1089.333-727.244c14.028 0 26.497 4.849 36.455 14.201l27.276-27.276c-16.539-15.413-38.013-24.852-63.731-24.852-37.234 0-69.359 21.388-85.032 52.561l31.692 24.592c7.533-22.514 28.575-39.226 53.34-39.226z" fill="#ea4335" clip-path="none" mask="none"/></g></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 21 21"><path fill="#f35325" d="M0 0h10v10H0z"/><path fill="#81bc06" d="M11 0h10v10H11z"/><path fill="#05a6f0" d="M0 11h10v10H0z"/><path fill="#ffba08" d="M11 11h10v10H11z"/></svg>

After

Width:  |  Height:  |  Size: 232 B

View File

@@ -1,5 +1,6 @@
use std::net::IpAddr;
use crate::data::provider::ProviderID;
use actix::{Actor, Context, Handler, Message, MessageResult};
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
@@ -19,6 +20,11 @@ pub trait UsersSyncBackend {
fn save_new_successful_2fa_authentication(&mut self, id: &UserID, ip: IpAddr) -> Res;
fn clear_2fa_login_history(&mut self, id: &UserID) -> Res;
fn delete_account(&mut self, id: &UserID) -> Res;
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res;
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res;
}
@@ -28,12 +34,13 @@ pub enum LoginResult {
AccountNotFound,
InvalidPassword,
AccountDisabled,
LocalAuthForbidden,
Success(Box<User>),
}
#[derive(Message)]
#[rtype(LoginResult)]
pub struct LoginRequest {
pub struct LocalLoginRequest {
pub login: String,
pub password: String,
}
@@ -88,6 +95,16 @@ pub struct AddSuccessful2FALogin(pub UserID, pub IpAddr);
#[rtype(result = "bool")]
pub struct Clear2FALoginHistory(pub UserID);
#[derive(Eq, PartialEq, Debug, Clone)]
pub struct AuthorizedAuthenticationSources {
pub local: bool,
pub upstream: Vec<ProviderID>,
}
#[derive(Message)]
#[rtype(result = "bool")]
pub struct SetAuthorizedAuthenticationSources(pub UserID, pub AuthorizedAuthenticationSources);
#[derive(Message)]
#[rtype(result = "bool")]
pub struct SetGrantedClients(pub UserID, pub GrantedClients);
@@ -119,10 +136,10 @@ impl Actor for UsersActor {
type Context = Context<Self>;
}
impl Handler<LoginRequest> for UsersActor {
type Result = MessageResult<LoginRequest>;
impl Handler<LocalLoginRequest> for UsersActor {
type Result = MessageResult<LocalLoginRequest>;
fn handle(&mut self, msg: LoginRequest, _ctx: &mut Self::Context) -> Self::Result {
fn handle(&mut self, msg: LocalLoginRequest, _ctx: &mut Self::Context) -> Self::Result {
match self.manager.find_by_username_or_email(&msg.login) {
Err(e) => {
log::error!("Failed to find user! {}", e);
@@ -142,6 +159,10 @@ impl Handler<LoginRequest> for UsersActor {
return MessageResult(LoginResult::AccountDisabled);
}
if !user.allow_local_login {
return MessageResult(LoginResult::LocalAuthForbidden);
}
MessageResult(LoginResult::Success(Box::new(user)))
}
}
@@ -241,6 +262,29 @@ impl Handler<Clear2FALoginHistory> for UsersActor {
}
}
impl Handler<SetAuthorizedAuthenticationSources> for UsersActor {
type Result = <SetAuthorizedAuthenticationSources as actix::Message>::Result;
fn handle(
&mut self,
msg: SetAuthorizedAuthenticationSources,
_ctx: &mut Self::Context,
) -> Self::Result {
match self
.manager
.set_authorized_authentication_sources(&msg.0, msg.1)
{
Ok(_) => true,
Err(e) => {
log::error!(
"Failed to set authorized authentication sources for user! {}",
e
);
false
}
}
}
}
impl Handler<SetGrantedClients> for UsersActor {
type Result = <SetGrantedClients as actix::Message>::Result;
fn handle(&mut self, msg: SetGrantedClients, _ctx: &mut Self::Context) -> Self::Result {

View File

@@ -6,6 +6,9 @@ pub const USERS_LIST_FILE: &str = "users.json";
/// File in storage containing clients list
pub const CLIENTS_LIST_FILE: &str = "clients.yaml";
/// File in storage containing providers list
pub const PROVIDERS_LIST_FILE: &str = "providers.yaml";
/// Default built-in credentials
pub const DEFAULT_ADMIN_USERNAME: &str = "admin";
pub const DEFAULT_ADMIN_PASSWORD: &str = "admin";

View File

@@ -6,12 +6,13 @@ use actix_web::{web, HttpResponse, Responder};
use askama::Template;
use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::constants::TEMPORARY_PASSWORDS_LEN;
use crate::controllers::settings_controller::BaseSettingsPage;
use crate::data::action_logger::{Action, ActionLogger};
use crate::data::client::{Client, ClientID, ClientManager};
use crate::data::current_user::CurrentUser;
use crate::data::provider::{Provider, ProviderID, ProvidersManager};
use crate::data::user::{GeneralSettings, GrantedClients, User, UserID};
use crate::utils::string_utils::rand_str;
@@ -22,6 +23,13 @@ struct ClientsListTemplate {
clients: Vec<Client>,
}
#[derive(Template)]
#[template(path = "settings/providers_list.html")]
struct ProvidersListTemplate {
_p: BaseSettingsPage,
providers: Vec<Provider>,
}
#[derive(Template)]
#[template(path = "settings/users_list.html")]
struct UsersListTemplate {
@@ -35,6 +43,7 @@ struct EditUserTemplate {
_p: BaseSettingsPage,
u: User,
clients: Vec<Client>,
providers: Vec<Provider>,
}
pub async fn clients_route(
@@ -51,6 +60,20 @@ pub async fn clients_route(
)
}
pub async fn providers_route(
user: CurrentUser,
providers: web::Data<Arc<ProvidersManager>>,
) -> impl Responder {
HttpResponse::Ok().body(
ProvidersListTemplate {
_p: BaseSettingsPage::get("OpenID Providers list", &user, None, None),
providers: providers.cloned(),
}
.render()
.unwrap(),
)
}
#[derive(serde::Deserialize, Debug)]
pub struct UpdateUserQuery {
uid: UserID,
@@ -62,6 +85,8 @@ pub struct UpdateUserQuery {
enabled: Option<String>,
two_factor_exemption_after_successful_login: Option<String>,
admin: Option<String>,
allow_local_login: Option<String>,
authorized_sources: String,
grant_type: String,
granted_clients: String,
two_factor: String,
@@ -136,6 +161,29 @@ pub async fn users_route(
}
}
// Update the list of authorized authentication sources
let auth_sources = AuthorizedAuthenticationSources {
local: update.0.allow_local_login.is_some(),
upstream: match update.0.authorized_sources.as_str() {
"" => vec![],
s => s.split(',').map(|s| ProviderID(s.to_string())).collect(),
},
};
if edited_user.authorized_authentication_sources() != auth_sources {
logger.log(Action::AdminSetAuthorizedAuthenticationSources(
&edited_user,
&auth_sources,
));
users
.send(users_actor::SetAuthorizedAuthenticationSources(
edited_user.uid.clone(),
auth_sources,
))
.await
.unwrap();
}
// Update list of granted clients
let granted_clients = match update.0.grant_type.as_str() {
"all_clients" => GrantedClients::AllClients,
@@ -240,6 +288,7 @@ pub async fn users_route(
pub async fn create_user(
admin: CurrentUser,
clients: web::Data<Arc<ClientManager>>,
providers: web::Data<Arc<ProvidersManager>>,
) -> impl Responder {
let user = User {
authorized_clients: Some(
@@ -257,6 +306,7 @@ pub async fn create_user(
_p: BaseSettingsPage::get("Create a new user", admin.deref(), None, None),
u: user,
clients: clients.cloned(),
providers: providers.cloned(),
}
.render()
.unwrap(),
@@ -271,6 +321,7 @@ pub struct EditUserQuery {
pub async fn edit_user(
admin: CurrentUser,
clients: web::Data<Arc<ClientManager>>,
providers: web::Data<Arc<ProvidersManager>>,
users: web::Data<Addr<UsersActor>>,
query: web::Query<EditUserQuery>,
) -> impl Responder {
@@ -293,6 +344,7 @@ pub async fn edit_user(
),
u: edited_account.unwrap_or_default(),
clients: clients.cloned(),
providers: providers.cloned(),
}
.render()
.unwrap(),

View File

@@ -132,7 +132,7 @@ pub async fn login_route(
else if let Some(req) = &req {
login = req.login.clone();
let response: LoginResult = users
.send(users_actor::LoginRequest {
.send(users_actor::LocalLoginRequest {
login: login.clone(),
password: req.password.clone(),
})
@@ -163,6 +163,12 @@ pub async fn login_route(
danger = Some("Your account is disabled!".to_string());
}
LoginResult::LocalAuthForbidden => {
log::warn!("Failed login for username {} : attempted to use local auth, but it is forbidden", &login);
logger.log(Action::TryLocalLoginFromUnauthorizedAccount(&login));
danger = Some("You cannot login from local auth with your account!".to_string());
}
LoginResult::Error => {
danger = Some("An unkown error occured while trying to sign you in!".to_string());
}

View File

@@ -8,7 +8,7 @@ use actix_web::dev::Payload;
use actix_web::{web, Error, FromRequest, HttpRequest};
use crate::actors::users_actor;
use crate::actors::users_actor::UsersActor;
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersActor};
use crate::data::client::Client;
use crate::data::remote_ip::RemoteIP;
use crate::data::session_identity::SessionIdentity;
@@ -20,6 +20,7 @@ pub enum Action<'a> {
AdminDeleteUser(&'a User),
AdminResetUserPassword(&'a User),
AdminRemoveUserFactor(&'a User, &'a TwoFactor),
AdminSetAuthorizedAuthenticationSources(&'a User, &'a AuthorizedAuthenticationSources),
AdminSetNewGrantedClientsList(&'a User, &'a GrantedClients),
AdminClear2FAHistory(&'a User),
LoginWebauthnAttempt { success: bool, user_id: UserID },
@@ -28,6 +29,7 @@ pub enum Action<'a> {
UserSuccessfullyAuthenticated(&'a User),
UserNeedNewPasswordOnLogin(&'a User),
TryLoginWithDisabledAccount(&'a str),
TryLocalLoginFromUnauthorizedAccount(&'a str),
FailedLoginWithBadCredentials(&'a str),
UserChangedPasswordOnLogin(&'a UserID),
OTPLoginAttempt { user: &'a User, success: bool },
@@ -64,6 +66,11 @@ impl<'a> Action<'a> {
Action::AdminClear2FAHistory(user) => {
format!("cleared 2FA history of {}", user.quick_identity())
}
Action::AdminSetAuthorizedAuthenticationSources(user, sources) => format!(
"update authorized authentication sources ({:?}) for user ({})",
sources,
user.quick_identity()
),
Action::AdminSetNewGrantedClientsList(user, clients) => format!(
"set new granted clients list ({:?}) for user ({})",
clients,
@@ -90,6 +97,9 @@ impl<'a> Action<'a> {
Action::TryLoginWithDisabledAccount(login) => {
format!("successfully authenticated as {login}, but this is a DISABLED ACCOUNT")
}
Action::TryLocalLoginFromUnauthorizedAccount(login) => {
format!("successfully locally authenticated as {login}, but this is a FORBIDDEN for this account!")
}
Action::FailedLoginWithBadCredentials(login) => {
format!("attempted to authenticate as {login} but with a WRONG PASSWORD")
}

View File

@@ -2,7 +2,7 @@ use std::path::{Path, PathBuf};
use clap::Parser;
use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, USERS_LIST_FILE};
use crate::constants::{APP_NAME, CLIENTS_LIST_FILE, PROVIDERS_LIST_FILE, USERS_LIST_FILE};
/// Basic OIDC provider
#[derive(Parser, Debug, Clone)]
@@ -72,6 +72,10 @@ impl AppConfig {
self.storage_path().join(CLIENTS_LIST_FILE)
}
pub fn providers_file(&self) -> PathBuf {
self.storage_path().join(PROVIDERS_LIST_FILE)
}
pub fn full_url(&self, uri: &str) -> String {
if uri.starts_with('/') {
format!("{}{}", self.website_origin, uri)

View File

@@ -11,6 +11,7 @@ pub mod jwt_signer;
pub mod login_redirect;
pub mod open_id_user_info;
pub mod openid_config;
pub mod provider;
pub mod remote_ip;
pub mod session_identity;
pub mod totp_key;

74
src/data/provider.rs Normal file
View File

@@ -0,0 +1,74 @@
use crate::data::entity_manager::EntityManager;
use crate::utils::string_utils::apply_env_vars;
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, Eq, PartialEq)]
pub struct ProviderID(pub String);
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct Provider {
/// The ID of the provider
pub id: ProviderID,
/// The human-readable name of the client
pub name: String,
/// A logo presented to the users of the provider
pub logo: String,
/// The registration id of BasicOIDC on the provider
pub client_id: String,
/// The registration secret of BasicOIDC on the provider
pub client_secret: String,
/// Specify the URL of the OpenID configuration URL
///
/// (.well-known/openid-configuration endpoint)
pub configuration_url: String,
}
impl Provider {
/// Get the URL where the logo can be located
pub fn logo_url(&self) -> &str {
match self.logo.as_str() {
"gitea" => "/assets/img/brands/gitea.svg",
"gitlab" => "/assets/img/brands/gitlab.svg",
"github" => "/assets/img/brands/github.svg",
"microsoft" => "/assets/img/brands/microsoft.svg",
"google" => "/assets/img/brands/google.svg",
s => s,
}
}
}
impl PartialEq for Provider {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
impl Eq for Provider {}
pub type ProvidersManager = EntityManager<Provider>;
impl EntityManager<Provider> {
pub fn find_by_id(&self, u: &ProviderID) -> Option<Provider> {
for entry in self.iter() {
if entry.id.eq(u) {
return Some(entry.clone());
}
}
None
}
pub fn apply_environment_variables(&mut self) {
for c in self.iter_mut() {
c.id = ProviderID(apply_env_vars(&c.id.0));
c.name = apply_env_vars(&c.name);
c.logo = apply_env_vars(&c.logo);
c.client_id = apply_env_vars(&c.client_id);
c.client_secret = apply_env_vars(&c.client_secret);
c.configuration_url = apply_env_vars(&c.configuration_url);
}
}
}

View File

@@ -1,9 +1,11 @@
use std::collections::HashMap;
use std::net::IpAddr;
use crate::actors::users_actor::AuthorizedAuthenticationSources;
use crate::constants::SECOND_FACTOR_EXEMPTION_AFTER_SUCCESSFUL_LOGIN;
use crate::data::client::{Client, ClientID};
use crate::data::login_redirect::LoginRedirect;
use crate::data::provider::{Provider, ProviderID};
use crate::data::totp_key::TotpKey;
use crate::data::webauthn_manager::WebauthnPubKey;
use crate::utils::time::{fmt_time, time};
@@ -114,6 +116,10 @@ impl Successful2FALogin {
}
}
fn default_true() -> bool {
true
}
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct User {
pub uid: UserID,
@@ -142,6 +148,14 @@ pub struct User {
/// None = all services
/// Some([]) = no service
pub authorized_clients: Option<Vec<ClientID>>,
/// Authorize connection through local login
#[serde(default = "default_true")]
pub allow_local_login: bool,
/// Allowed third party providers
#[serde(default)]
pub allow_login_from_providers: Vec<ProviderID>,
}
impl User {
@@ -162,6 +176,19 @@ impl User {
)
}
/// Get the list of sources from which a user can authenticate from
pub fn authorized_authentication_sources(&self) -> AuthorizedAuthenticationSources {
AuthorizedAuthenticationSources {
local: self.allow_local_login,
upstream: self.allow_login_from_providers.clone(),
}
}
/// Check if a user can authenticate using a givne provider or not
pub fn can_login_from_provider(&self, provider: &Provider) -> bool {
self.allow_login_from_providers.contains(&provider.id)
}
pub fn granted_clients(&self) -> GrantedClients {
match self.authorized_clients.as_deref() {
None => GrantedClients::AllClients,
@@ -296,6 +323,8 @@ impl Default for User {
two_factor_exemption_after_successful_login: false,
last_successful_2fa: Default::default(),
authorized_clients: Some(Vec::new()),
allow_local_login: true,
allow_login_from_providers: vec![],
}
}
}

View File

@@ -1,6 +1,6 @@
use std::net::IpAddr;
use crate::actors::users_actor::UsersSyncBackend;
use crate::actors::users_actor::{AuthorizedAuthenticationSources, UsersSyncBackend};
use crate::data::entity_manager::EntityManager;
use crate::data::user::{FactorID, GeneralSettings, GrantedClients, TwoFactor, User, UserID};
use crate::utils::err::{new_error, Res};
@@ -143,6 +143,18 @@ impl UsersSyncBackend for EntityManager<User> {
self.remove(&user)
}
fn set_authorized_authentication_sources(
&mut self,
id: &UserID,
sources: AuthorizedAuthenticationSources,
) -> Res {
self.update_user(id, |mut user| {
user.allow_local_login = sources.local;
user.allow_login_from_providers = sources.upstream;
user
})
}
fn set_granted_2fa_clients(&mut self, id: &UserID, clients: GrantedClients) -> Res {
self.update_user(id, |mut user| {
user.authorized_clients = clients.to_user();

View File

@@ -20,6 +20,7 @@ use basic_oidc::data::app_config::AppConfig;
use basic_oidc::data::client::ClientManager;
use basic_oidc::data::entity_manager::EntityManager;
use basic_oidc::data::jwt_signer::JWTSigner;
use basic_oidc::data::provider::ProvidersManager;
use basic_oidc::data::user::User;
use basic_oidc::data::webauthn_manager::WebAuthManager;
use basic_oidc::middlewares::auth_middleware::AuthMiddleware;
@@ -77,6 +78,11 @@ async fn main() -> std::io::Result<()> {
clients.apply_environment_variables();
let clients = Arc::new(clients);
let mut providers = ProvidersManager::open_or_create(config.providers_file())
.expect("Failed to load providers list!");
providers.apply_environment_variables();
let providers = Arc::new(providers);
log::info!("Server will listen on {}", config.listen_address);
let listen_address = config.listen_address.to_string();
@@ -101,6 +107,7 @@ async fn main() -> std::io::Result<()> {
.app_data(web::Data::new(bruteforce_actor.clone()))
.app_data(web::Data::new(openid_sessions_actor.clone()))
.app_data(web::Data::new(clients.clone()))
.app_data(web::Data::new(providers.clone()))
.app_data(web::Data::new(jwt_signer.clone()))
.app_data(web::Data::new(webauthn_manager.clone()))
.wrap(
@@ -207,6 +214,10 @@ async fn main() -> std::io::Result<()> {
"/admin/clients",
web::get().to(admin_controller::clients_route),
)
.route(
"/admin/providers",
web::get().to(admin_controller::providers_route),
)
.route("/admin/users", web::get().to(admin_controller::users_route))
.route(
"/admin/users",

View File

@@ -42,6 +42,11 @@
Clients
</a>
</li>
<li>
<a href="/admin/providers" class="nav-link link-dark">
Providers
</a>
</li>
<li>
<a href="/admin/users" class="nav-link link-dark">
Users
@@ -83,6 +88,7 @@
if(el.href === location.href) el.classList.add("active");
else el.classList.remove("active")
})
</script>
{% if _p.ip_location_api.is_some() %}
<script>const IP_LOCATION_API = "{{ _p.ip_location_api.unwrap() }}"</script>

View File

@@ -113,27 +113,60 @@
<ul>
{% for e in u.get_formatted_2fa_successful_logins() %}
{% if e.can_bypass_2fa %}<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% else %}<li>{{ e.ip }} - {{ e.fmt_time() }}</li>{% endif %}
{% if e.can_bypass_2fa %}
<li style="font-weight: bold;">{{ e.ip }} - {{ e.fmt_time() }} - BYPASS 2FA</li>
{% else %}
<li>{{ e.ip }} - {{ e.fmt_time() }}</li>
{% endif %}
{% endfor %}
</ul>
</fieldset>
{% endif %}
<!-- Authorized authentication sources -->
<fieldset class="form-group">
<legend class="mt-4">Authorized authentication sources</legend>
<!-- Local login -->
<div class="form-check">
<input class="form-check-input" type="checkbox" name="allow_local_login" id="allow_local_login"
{% if u.allow_local_login %} checked="" {% endif %}>
<label class="form-check-label" for="allow_local_login">
Allow local login
</label>
</div>
<!-- Upstream providers -->
<input type="hidden" name="authorized_sources" id="authorized_sources"/>
{% for prov in providers %}
<div class="form-check">
<input class="form-check-input authorized_provider" type="checkbox" name="prov-{{ prov.id.0 }}"
id="prov-{{ prov.id.0 }}"
data-id="{{ prov.id.0 }}"
{% if u.can_login_from_provider(prov) %} checked="" {% endif %}>
<label class="form-check-label" for="prov-{{ prov.id.0 }}">
Allow login from {{ prov.name }}
</label>
</div>
{% endfor %}
</fieldset>
<!-- Granted clients -->
<fieldset class="form-group">
<legend class="mt-4">Granted clients</legend>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="all_clients" {% if u.granted_clients() == GrantedClients::AllClients %} checked="" {% endif %}>
value="all_clients" {% if u.granted_clients()== GrantedClients::AllClients %} checked="" {% endif
%}>
Grant all clients
</label>
</div>
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_)) %} checked="checked" {% endif %}>
value="custom_clients" {% if matches!(self.u.granted_clients(), GrantedClients::SomeClients(_))
%} checked="checked" {% endif %}>
Manually specify allowed clients
</label>
</div>
@@ -155,7 +188,8 @@
<div class="form-check">
<label class="form-check-label">
<input type="radio" class="form-check-input" name="grant_type"
value="no_client" {% if u.granted_clients() == GrantedClients::NoClient %} checked="checked" {% endif %}>
value="no_client" {% if u.granted_clients()== GrantedClients::NoClient %} checked="checked" {%
endif %}>
Do not grant any client
</label>
</div>
@@ -215,6 +249,13 @@
form.addEventListener("submit", (ev) => {
ev.preventDefault();
const authorized_sources = [...document.querySelectorAll(".authorized_provider")]
.filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",")
document.querySelector("input[name=authorized_sources]").value = authorized_sources;
const authorized_clients = [...document.querySelectorAll(".authorize_client_checkbox")]
.filter(e => e.checked)
.map(e => e.getAttribute("data-id")).join(",")
@@ -231,6 +272,9 @@
form.submit();
});
</script>
{% endblock content %}

View File

@@ -0,0 +1,37 @@
{% extends "base_settings_page.html" %}
{% block content %}
<style>
#providers td {
vertical-align: middle;
}
</style>
<table id="providers" class="table table-hover" style="max-width: 800px;" aria-describedby="OpenID providers list">
<thead>
<tr>
<th scope="col"></th>
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Configuration URL</th>
<th scope="col">Client ID</th>
</tr>
</thead>
<tbody>
{% for c in providers %}
<tr>
<td>
<img src="{{ c.logo_url() }}" alt="{{ c.name }} logo" width="30px"/>
</td>
<td>{{ c.id.0 }}</td>
<td>{{ c.name }}</td>
<td><a href="{{ c.configuration_url }}" target="_blank" rel="noreferrer">
{{ c.configuration_url }}
</a></td>
<td>{{ c.client_id }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock content %}