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"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
|
@ -1202,6 +1208,7 @@ dependencies = [
|
|||
"aws-config",
|
||||
"aws-sdk-s3",
|
||||
"axum",
|
||||
"constant_time_eq",
|
||||
"forgejo-api",
|
||||
"include_dir",
|
||||
"lazy_static",
|
||||
|
|
|
@ -29,6 +29,7 @@ tower = "0.5.2"
|
|||
tower-http = { version = "0.6.2", features = ["trace"] }
|
||||
tracing-subscriber = "0.3"
|
||||
tracing = "0.1.41"
|
||||
constant_time_eq = "0.3.1"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "dev"
|
||||
|
|
|
@ -16,9 +16,11 @@
|
|||
Forgery reads the following environment variables:
|
||||
- `FORGE_URL` (**mandatory**): url of the forgejo instance (e.g.
|
||||
https://git.deuxfleurs.fr)
|
||||
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting admin access*.
|
||||
You can generate an API token using the Forgejo web interface in `Settings ->
|
||||
Applications -> Generate New Token`.
|
||||
- `FORGE_API_TOKEN` (**mandatory**): Forgejo API token *granting read/write
|
||||
access* to admin rights, users, issues and repos. You can generate an API
|
||||
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
|
||||
user accounts, send notification emails and eventually delete user accounts.
|
||||
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
|
||||
could not be locked, but delete the account after the grace period even if
|
||||
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
|
||||
backend fails. Can we do better?
|
||||
- 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 forge_url: Url,
|
||||
pub forge_api_token: String,
|
||||
pub password: String,
|
||||
pub actually_ban: ActuallyBan,
|
||||
pub bind_addr: String,
|
||||
}
|
||||
|
@ -70,6 +71,7 @@ impl Config {
|
|||
async fn from_env() -> anyhow::Result<Self> {
|
||||
let forge_url_s = env_var("FORGE_URL")?;
|
||||
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() {
|
||||
Ok("true") => ActuallyBan::Yes {
|
||||
|
@ -91,6 +93,7 @@ impl Config {
|
|||
Ok(Config {
|
||||
forge_url: Url::parse(&forge_url_s).context("parsing FORGE_URL")?,
|
||||
forge_api_token,
|
||||
password,
|
||||
actually_ban,
|
||||
bind_addr,
|
||||
})
|
||||
|
@ -441,15 +444,19 @@ async fn get_auth(State(data): State<Arc<AppState>>) -> Result<Html<String>, App
|
|||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthForm {
|
||||
token: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
async fn post_auth(session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
|
||||
session
|
||||
.insert(session::TOKEN_KEY, &form.token)
|
||||
.await
|
||||
.unwrap();
|
||||
async fn post_auth(State(data): State<Arc<AppState>>, session: Session, Form(form): Form<AuthForm>) -> impl IntoResponse {
|
||||
let p1 = form.password.as_bytes();
|
||||
let p2 = data.config.password.as_bytes();
|
||||
if p1.len() == p2.len() && constant_time_eq::constant_time_eq(p1, p2) {
|
||||
session::set_authenticated(&session).await;
|
||||
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
|
||||
} else {
|
||||
session.flush().await.unwrap();
|
||||
(StatusCode::SEE_OTHER, [(header::LOCATION, "/auth")])
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// can say about them
|
||||
let data = data
|
||||
|
|
|
@ -2,13 +2,17 @@ use axum::extract::FromRequestParts;
|
|||
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, StatusCode};
|
||||
use tower_sessions::Session;
|
||||
|
||||
#[allow(dead_code)] // TODO: WIP
|
||||
pub struct AuthenticatedUser {
|
||||
session: Session,
|
||||
api_token: String,
|
||||
pub struct AuthenticatedUser(());
|
||||
|
||||
const IS_AUTH_KEY: &str = "IS_AUTH";
|
||||
|
||||
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
|
||||
where
|
||||
|
@ -22,16 +26,9 @@ where
|
|||
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?
|
||||
|
||||
match api_token {
|
||||
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.
|
||||
if is_authenticated(&session).await {
|
||||
Ok(Self(()))
|
||||
} else {
|
||||
session.flush().await.unwrap();
|
||||
Err((
|
||||
StatusCode::SEE_OTHER,
|
||||
|
@ -42,5 +39,4 @@ where
|
|||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,17 +4,9 @@
|
|||
{% block content %}
|
||||
<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">
|
||||
<label for="token">Forgejo API token:</label>
|
||||
<input id="token" name="token" type="password" required/>
|
||||
<label for="password">Password:</label>
|
||||
<input id="password" name="password" type="password" required/>
|
||||
|
||||
<input type="submit" value="Submit" class="button"/>
|
||||
</form>
|
||||
|
|
Loading…
Add table
Reference in a new issue