use std::future::Future;
use chrono::Local;
use super::Database;
use crate::model::auth::{
find_field_in_blob, Authentication, AuthenticationPrincipal, OAuthToken, RegistryUserToken, RegistryUserTokenWithSecret,
TokenKind, TokenUsage, ROLE_ADMIN,
};
use crate::model::cargo::RegistryUser;
use crate::model::config::Configuration;
use crate::model::namegen::generate_name;
use crate::utils::apierror::{
error_conflict, error_forbidden, error_invalid_request, error_not_found, error_unauthorized, specialize, ApiError,
};
use crate::utils::token::{check_hash, generate_token, hash_token};
impl Database {
pub async fn get_user_profile(&self, uid: i64) -> Result<RegistryUser, ApiError> {
let maybe_row = sqlx::query_as!(
RegistryUser,
"SELECT id, isActive AS is_active, email, login, name, roles FROM RegistryUser WHERE id = $1",
uid
)
.fetch_optional(&mut *self.transaction.borrow().await)
.await?;
maybe_row.ok_or_else(error_not_found)
}
pub async fn login_with_oauth_code(&self, configuration: &Configuration, code: &str) -> Result<RegistryUser, ApiError> {
let client = reqwest::Client::new();
let response = client
.post(&configuration.oauth_token_uri)
.form(&[
("grant_type", "authorization_code"),
("code", code),
("redirect_uri", &configuration.oauth_callback_uri),
("client_id", &configuration.oauth_client_id),
("client_secret", &configuration.oauth_client_secret),
])
.send()
.await?;
if !response.status().is_success() {
return Err(specialize(error_unauthorized(), String::from("authentication failed")));
}
let body = response.bytes().await?;
let token = serde_json::from_slice::<OAuthToken>(&body)?;
let response = client
.get(&configuration.oauth_userinfo_uri)
.header("authorization", format!("Bearer {}", token.access_token))
.send()
.await?;
if !response.status().is_success() {
return Err(specialize(error_unauthorized(), String::from("authentication failed")));
}
let body = response.bytes().await?;
let user_info = serde_json::from_slice::<serde_json::Value>(&body)?;
let email = find_field_in_blob(&user_info, &configuration.oauth_userinfo_path_email).ok_or_else(error_unauthorized)?;
let row = sqlx::query!(
"SELECT id, isActive AS is_active, login, name, roles FROM RegistryUser WHERE email = $1 LIMIT 1",
email
)
.fetch_optional(&mut *self.transaction.borrow().await)
.await?;
if let Some(row) = row {
if !row.is_active {
return Err(specialize(error_unauthorized(), String::from("inactive user")));
}
return Ok(RegistryUser {
id: row.id,
is_active: true,
email: email.to_string(),
login: row.login,
name: row.name,
roles: row.roles,
});
}
let count = sqlx::query!("SELECT COUNT(id) AS count FROM RegistryUser")
.fetch_one(&mut *self.transaction.borrow().await)
.await?
.count;
let mut login = email[..email.find('@').unwrap()].to_string();
while sqlx::query!("SELECT COUNT(id) AS count FROM RegistryUser WHERE login = $1", login)
.fetch_one(&mut *self.transaction.borrow().await)
.await?
.count
!= 0
{
login = generate_name();
}
let full_name = find_field_in_blob(&user_info, &configuration.oauth_userinfo_path_fullname).unwrap_or(&login);
let roles = if count == 0 { ROLE_ADMIN } else { "" };
let id = sqlx::query!(
"INSERT INTO RegistryUser (isActive, email, login, name, roles) VALUES (TRUE, $1, $2, $3, $4) RETURNING id",
email,
login,
full_name,
roles
)
.fetch_one(&mut *self.transaction.borrow().await)
.await?
.id;
Ok(RegistryUser {
id,
is_active: true,
email: email.to_string(),
name: login.to_string(),
login,
roles: roles.to_string(),
})
}
pub async fn get_users(&self) -> Result<Vec<RegistryUser>, ApiError> {
let rows = sqlx::query_as!(
RegistryUser,
"SELECT id, isActive AS is_active, email, login, name, roles FROM RegistryUser ORDER BY login",
)
.fetch_all(&mut *self.transaction.borrow().await)
.await?;
Ok(rows)
}
pub async fn update_user(
&self,
principal_uid: i64,
target: &RegistryUser,
can_admin: bool,
) -> Result<RegistryUser, ApiError> {
let row = sqlx::query!("SELECT login, roles FROM RegistryUser WHERE id = $1 LIMIT 1", target.id)
.fetch_optional(&mut *self.transaction.borrow().await)
.await?
.ok_or_else(error_not_found)?;
let old_roles = row.roles;
if !can_admin && target.roles != old_roles {
return Err(specialize(error_forbidden(), String::from("only admins can change roles")));
}
if can_admin && target.id == principal_uid && target.roles.split(',').all(|role| role.trim() != ROLE_ADMIN) {
return Err(specialize(error_forbidden(), String::from("admins cannot remove themselves")));
}
if target.login.is_empty() {
return Err(specialize(error_invalid_request(), String::from("login cannot be empty")));
}
if row.login != target.login {
if sqlx::query!("SELECT COUNT(id) AS count FROM RegistryUser WHERE login = $1", target.login)
.fetch_one(&mut *self.transaction.borrow().await)
.await?
.count
!= 0
{
return Err(specialize(
error_conflict(),
String::from("the specified login is not available"),
));
}
}
sqlx::query!(
"UPDATE RegistryUser SET login = $2, name = $3, roles = $4 WHERE id = $1",
target.id,
target.login,
target.name,
target.roles
)
.execute(&mut *self.transaction.borrow().await)
.await?;
Ok(target.clone())
}
pub async fn deactivate_user(&self, principal_uid: i64, target: &str) -> Result<(), ApiError> {
let target_uid = self.check_is_user(target).await?;
if principal_uid == target_uid {
return Err(specialize(error_forbidden(), String::from("cannot self deactivate")));
}
sqlx::query!("UPDATE RegistryUser SET isActive = FALSE WHERE id = $1", target_uid)
.execute(&mut *self.transaction.borrow().await)
.await?;
Ok(())
}
pub async fn reactivate_user(&self, target: &str) -> Result<(), ApiError> {
sqlx::query!("UPDATE RegistryUser SET isActive = TRUE WHERE email = $1", target)
.execute(&mut *self.transaction.borrow().await)
.await?;
Ok(())
}
pub async fn delete_user(&self, principal_uid: i64, target: &str) -> Result<(), ApiError> {
let target_uid = sqlx::query!("SELECT id FROM RegistryUser WHERE email = $1", target)
.fetch_optional(&mut *self.transaction.borrow().await)
.await?
.ok_or_else(error_not_found)?
.id;
if principal_uid == target_uid {
return Err(specialize(error_forbidden(), String::from("cannot delete self")));
}
sqlx::query!("DELETE FROM RegistryUserToken WHERE user = $1", target_uid)
.execute(&mut *self.transaction.borrow().await)
.await?;
sqlx::query!("DELETE FROM PackageOwner WHERE owner = $1", target_uid)
.execute(&mut *self.transaction.borrow().await)
.await?;
sqlx::query!("DELETE FROM RegistryUser WHERE id = $1", target_uid)
.execute(&mut *self.transaction.borrow().await)
.await?;
Ok(())
}
pub async fn get_tokens(&self, uid: i64) -> Result<Vec<RegistryUserToken>, ApiError> {
let rows = sqlx::query!(
"SELECT id, name, lastUsed AS last_used, canWrite AS can_write, canAdmin AS can_admin FROM RegistryUserToken WHERE user = $1 ORDER BY id",
uid
)
.fetch_all(&mut *self.transaction.borrow().await)
.await?;
Ok(rows
.into_iter()
.map(|row| RegistryUserToken {
id: row.id,
name: row.name,
last_used: row.last_used,
can_write: row.can_write,
can_admin: row.can_admin,
})
.collect())
}
pub async fn create_token(
&self,
uid: i64,
name: &str,
can_write: bool,
can_admin: bool,
) -> Result<RegistryUserTokenWithSecret, ApiError> {
let token_secret = generate_token(64);
let token_hash = hash_token(&token_secret);
let now = Local::now().naive_local();
let id = sqlx::query!(
"INSERT INTO RegistryUserToken (user, name, token, lastUsed, canWrite, canAdmin) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id",
uid,
name,
token_hash,
now,
can_write,
can_admin
)
.fetch_one(&mut *self.transaction.borrow().await)
.await?
.id;
Ok(RegistryUserTokenWithSecret {
id,
name: name.to_string(),
secret: token_secret,
last_used: now,
can_write,
can_admin,
})
}
pub async fn revoke_token(&self, uid: i64, token_id: i64) -> Result<(), ApiError> {
sqlx::query!("DELETE FROM RegistryUserToken WHERE user = $1 AND id = $2", uid, token_id)
.execute(&mut *self.transaction.borrow().await)
.await?;
Ok(())
}
pub async fn check_token<F, FUT>(&self, login: &str, token_secret: &str, on_usage: &F) -> Result<Authentication, ApiError>
where
F: Fn(TokenUsage) -> FUT,
FUT: Future<Output = ()>,
{
if let Some(auth) = self.check_token_global(login, token_secret, &on_usage).await? {
return Ok(auth);
}
if let Some(auth) = self.check_token_user(login, token_secret, &on_usage).await? {
return Ok(auth);
}
Err(error_unauthorized())
}
async fn check_token_user<F, FUT>(
&self,
login: &str,
token_secret: &str,
on_usage: &F,
) -> Result<Option<Authentication>, ApiError>
where
F: Fn(TokenUsage) -> FUT,
FUT: Future<Output = ()>,
{
let rows = sqlx::query!(
"SELECT RegistryUser.id AS uid, email, RegistryUserToken.id, token, canWrite AS can_write, canAdmin AS can_admin
FROM RegistryUser INNER JOIN RegistryUserToken ON RegistryUser.id = RegistryUserToken.user
WHERE isActive = TRUE AND login = $1",
login
)
.fetch_all(&mut *self.transaction.borrow().await)
.await?;
for row in rows {
if check_hash(token_secret, &row.token).is_ok() {
let now = Local::now().naive_local();
on_usage(TokenUsage {
kind: TokenKind::User,
token_id: row.id,
timestamp: now,
})
.await;
return Ok(Some(Authentication {
principal: AuthenticationPrincipal::User {
uid: row.uid,
email: row.email,
},
can_write: row.can_write,
can_admin: row.can_admin,
}));
}
}
Ok(None)
}
async fn check_token_global<F, FUT>(
&self,
login: &str,
token_secret: &str,
on_usage: &F,
) -> Result<Option<Authentication>, ApiError>
where
F: Fn(TokenUsage) -> FUT,
FUT: Future<Output = ()>,
{
let row = sqlx::query!("SELECT id, token FROM RegistryGlobalToken WHERE name = $1 LIMIT 1", login)
.fetch_optional(&mut *self.transaction.borrow().await)
.await?;
let Some(row) = row else { return Ok(None) };
if check_hash(token_secret, &row.token).is_ok() {
let now = Local::now().naive_local();
on_usage(TokenUsage {
kind: TokenKind::Registry,
token_id: row.id,
timestamp: now,
})
.await;
Ok(Some(Authentication::new_service(login.to_string())))
} else {
Ok(None)
}
}
pub async fn update_token_last_usage(&self, event: &TokenUsage) -> Result<(), ApiError> {
if event.kind == TokenKind::User {
sqlx::query!(
"UPDATE RegistryUserToken SET lastUsed = $2 WHERE id = $1",
event.token_id,
event.timestamp
)
.execute(&mut *self.transaction.borrow().await)
.await?;
} else if event.kind == TokenKind::Registry {
sqlx::query!(
"UPDATE RegistryGlobalToken SET lastUsed = $2 WHERE id = $1",
event.token_id,
event.timestamp
)
.execute(&mut *self.transaction.borrow().await)
.await?;
}
Ok(())
}
}