auth: use a global password for the webapp
This commit is contained in:
parent
6342620f70
commit
7bd511eaae
8 changed files with 51 additions and 46 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -874,6 +874,12 @@ version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "constant_time_eq"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
|
@ -1202,6 +1208,7 @@ dependencies = [
|
||||||
"aws-config",
|
"aws-config",
|
||||||
"aws-sdk-s3",
|
"aws-sdk-s3",
|
||||||
"axum",
|
"axum",
|
||||||
|
"constant_time_eq",
|
||||||
"forgejo-api",
|
"forgejo-api",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
|
|
|
@ -29,6 +29,7 @@ tower = "0.5.2"
|
||||||
tower-http = { version = "0.6.2", features = ["trace"] }
|
tower-http = { version = "0.6.2", features = ["trace"] }
|
||||||
tracing-subscriber = "0.3"
|
tracing-subscriber = "0.3"
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
|
constant_time_eq = "0.3.1"
|
||||||
|
|
||||||
[profile.profiling]
|
[profile.profiling]
|
||||||
inherits = "dev"
|
inherits = "dev"
|
||||||
|
|
|
@ -16,9 +16,11 @@
|
||||||
Forgery reads the following environment variables:
|
Forgery reads the following environment variables:
|
||||||
- `FORGE_URL` (**mandatory**): url of the forgejo instance (e.g.
|
- `FORGE_URL` (**mandatory**): url of the forgejo instance (e.g.
|
||||||
https://git.deuxfleurs.fr)
|
https://git.deuxfleurs.fr)
|
||||||
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting admin access*.
|
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting read/write
|
||||||
You can generate an API token using the Forgejo web interface in `Settings ->
|
access* to admin rights, users, issues and repos. You can generate an API
|
||||||
Applications -> Generate New Token`.
|
token using the Forgejo web interface in `Settings -> Applications -> Generate
|
||||||
|
New Token`.
|
||||||
|
- `PASSWORD` (**mandatory**): password required to authenticate to forgery.
|
||||||
- `ACTUALLY_BAN_USERS` (default: `false`): define it to `true` to actually lock
|
- `ACTUALLY_BAN_USERS` (default: `false`): define it to `true` to actually lock
|
||||||
user accounts, send notification emails and eventually delete user accounts.
|
user accounts, send notification emails and eventually delete user accounts.
|
||||||
Otherwise, no actual action is taken: spammers are only listed in the
|
Otherwise, no actual action is taken: spammers are only listed in the
|
||||||
|
@ -57,7 +59,6 @@ Environment variables read when `STORAGE_BACKEND=s3`:
|
||||||
(Current behavior is to periodically retry, avoid deleting if the account
|
(Current behavior is to periodically retry, avoid deleting if the account
|
||||||
could not be locked, but delete the account after the grace period even if
|
could not be locked, but delete the account after the grace period even if
|
||||||
the email could not be sent…)
|
the email could not be sent…)
|
||||||
- auth: add support for connecting to the forge using oauth?
|
|
||||||
- improve error handling? currently the app will panic if writing to the storage
|
- improve error handling? currently the app will panic if writing to the storage
|
||||||
backend fails. Can we do better?
|
backend fails. Can we do better?
|
||||||
- error reporting when the auth token is not working/outdated and background jobs fail
|
- error reporting when the auth token is not working/outdated and background jobs fail
|
||||||
|
|
File diff suppressed because one or more lines are too long
19
src/main.rs
19
src/main.rs
|
@ -62,6 +62,7 @@ const GUESS_LEGIT_THRESHOLD: f32 = 0.3;
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub forge_url: Url,
|
pub forge_url: Url,
|
||||||
pub forge_api_token: String,
|
pub forge_api_token: String,
|
||||||
|
pub password: String,
|
||||||
pub actually_ban: ActuallyBan,
|
pub actually_ban: ActuallyBan,
|
||||||
pub bind_addr: String,
|
pub bind_addr: String,
|
||||||
}
|
}
|
||||||
|
@ -70,6 +71,7 @@ impl Config {
|
||||||
async fn from_env() -> anyhow::Result<Self> {
|
async fn from_env() -> anyhow::Result<Self> {
|
||||||
let forge_url_s = env_var("FORGE_URL")?;
|
let forge_url_s = env_var("FORGE_URL")?;
|
||||||
let forge_api_token = env_var("FORGE_API_TOKEN")?;
|
let forge_api_token = env_var("FORGE_API_TOKEN")?;
|
||||||
|
let password = env_var("PASSWORD")?;
|
||||||
|
|
||||||
let actually_ban = match env_var("ACTUALLY_BAN_USERS").as_deref() {
|
let actually_ban = match env_var("ACTUALLY_BAN_USERS").as_deref() {
|
||||||
Ok("true") => ActuallyBan::Yes {
|
Ok("true") => ActuallyBan::Yes {
|
||||||
|
@ -91,6 +93,7 @@ impl Config {
|
||||||
Ok(Config {
|
Ok(Config {
|
||||||
forge_url: Url::parse(&forge_url_s).context("parsing FORGE_URL")?,
|
forge_url: Url::parse(&forge_url_s).context("parsing FORGE_URL")?,
|
||||||
forge_api_token,
|
forge_api_token,
|
||||||
|
password,
|
||||||
actually_ban,
|
actually_ban,
|
||||||
bind_addr,
|
bind_addr,
|
||||||
})
|
})
|
||||||
|
@ -441,15 +444,19 @@ async fn get_auth(State(data): State<Arc<AppState>>) -> Result<Html<String>, App
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct AuthForm {
|
struct AuthForm {
|
||||||
token: String,
|
password: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_auth(session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
|
async fn post_auth(State(data): State<Arc<AppState>>, session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
|
||||||
session
|
let p1 = form.password.as_bytes();
|
||||||
.insert(session::TOKEN_KEY, &form.token)
|
let p2 = data.config.password.as_bytes();
|
||||||
.await
|
if p1.len() == p2.len() && constant_time_eq::constant_time_eq(p1, p2) {
|
||||||
.unwrap();
|
session::set_authenticated(&session).await;
|
||||||
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
|
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
|
||||||
|
} else {
|
||||||
|
session.flush().await.unwrap();
|
||||||
|
(StatusCode::SEE_OTHER, [(header::LOCATION, "/auth")])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn post_logout(session: Session) -> impl IntoResponse {
|
async fn post_logout(session: Session) -> impl IntoResponse {
|
||||||
|
|
|
@ -185,6 +185,7 @@ pub async fn get_user_data(forge: &Forgejo) -> anyhow::Result<HashMap<UserId, Us
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
info!("done fetching user data from forgejo");
|
||||||
// discard users with an entirely empty profile: there is nothing useful we
|
// discard users with an entirely empty profile: there is nothing useful we
|
||||||
// can say about them
|
// can say about them
|
||||||
let data = data
|
let data = data
|
||||||
|
|
|
@ -2,13 +2,17 @@ use axum::extract::FromRequestParts;
|
||||||
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, StatusCode};
|
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, StatusCode};
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
#[allow(dead_code)] // TODO: WIP
|
pub struct AuthenticatedUser(());
|
||||||
pub struct AuthenticatedUser {
|
|
||||||
session: Session,
|
const IS_AUTH_KEY: &str = "IS_AUTH";
|
||||||
api_token: String,
|
|
||||||
|
pub async fn set_authenticated(session: &Session) {
|
||||||
|
session.insert(IS_AUTH_KEY, true).await.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const TOKEN_KEY: &str = "API_TOKEN";
|
pub async fn is_authenticated(session: &Session) -> bool {
|
||||||
|
matches!(session.get(IS_AUTH_KEY).await.unwrap(), Some(true))
|
||||||
|
}
|
||||||
|
|
||||||
impl<S> FromRequestParts<S> for AuthenticatedUser
|
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||||
where
|
where
|
||||||
|
@ -22,16 +26,9 @@ where
|
||||||
Err((status, body)) => return Err((status, HeaderMap::new(), body.to_string())),
|
Err((status, body)) => return Err((status, HeaderMap::new(), body.to_string())),
|
||||||
};
|
};
|
||||||
|
|
||||||
let api_token: Option<String> = session.get(TOKEN_KEY).await.unwrap(); // TODO: is it safe to [unwrap] here?
|
if is_authenticated(&session).await {
|
||||||
|
Ok(Self(()))
|
||||||
match api_token {
|
} else {
|
||||||
Some(api_token) => Ok(Self { session, api_token }),
|
|
||||||
None => {
|
|
||||||
// Unclear that this case can actually happen, but in doubt,
|
|
||||||
// clear the session and force to re-auth.
|
|
||||||
// It is safe to [unwrap] the result here because we use
|
|
||||||
// [MemoryStore] as the underlying store, on which [delete]
|
|
||||||
// never fails.
|
|
||||||
session.flush().await.unwrap();
|
session.flush().await.unwrap();
|
||||||
Err((
|
Err((
|
||||||
StatusCode::SEE_OTHER,
|
StatusCode::SEE_OTHER,
|
||||||
|
@ -43,4 +40,3 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -4,17 +4,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1>Authentication required</h1>
|
<h1>Authentication required</h1>
|
||||||
|
|
||||||
<p>
|
|
||||||
To obtain a suitable API token, you must login to Forgejo using an
|
|
||||||
administrator account and go to <a href="{{forge_url}}user/settings/applications">{{forge_url}}user/settings/applications</a>.
|
|
||||||
Then, create an API token with access to all organizations and repos, allowing
|
|
||||||
read and writet access to: "admin", "misc", "organization", "issue", "repository" and "user". Store
|
|
||||||
the resulting token safely (e.g. in your password manager).
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<label for="token">Forgejo API token:</label>
|
<label for="password">Password:</label>
|
||||||
<input id="token" name="token" type="password" required/>
|
<input id="password" name="password" type="password" required/>
|
||||||
|
|
||||||
<input type="submit" value="Submit" class="button"/>
|
<input type="submit" value="Submit" class="button"/>
|
||||||
</form>
|
</form>
|
||||||
|
|
Loading…
Add table
Reference in a new issue