Skip to content

Commit

Permalink
Redid Permissions and API Tokens. Frontend is angry
Browse files Browse the repository at this point in the history
  • Loading branch information
wyatt-herkamp committed Aug 22, 2024
1 parent 644fe75 commit 0f612ff
Show file tree
Hide file tree
Showing 35 changed files with 1,228 additions and 484 deletions.
99 changes: 61 additions & 38 deletions crates/core/src/database/user/auth_token.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
use serde::{Deserialize, Serialize};
use sqlx::{prelude::FromRow, PgPool};
use tracing::instrument;
use uuid::Uuid;

use crate::database::DateTime;
use crate::{
database::DateTime,
user::{permissions::RepositoryActionOptions, scopes::Scopes},
};

use super::ReferencesUser;

mod repository_scope;
mod scope;
mod utils;
pub use repository_scope::*;
pub use scope::*;
pub use utils::*;
/// Table Name: user_auth_tokens
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
pub struct AuthToken {
pub id: i32,
Expand All @@ -14,18 +25,6 @@ pub struct AuthToken {
pub expires_at: Option<DateTime>,
pub created_at: DateTime,
}
impl Default for AuthToken {
fn default() -> Self {
Self {
id: 0,
user_id: 0,
token: "".to_string(),
active: false,
expires_at: None,
created_at: DateTime::default(),
}
}
}
impl ReferencesUser for AuthToken {
fn user_id(&self) -> i32 {
self.user_id
Expand All @@ -36,37 +35,61 @@ impl ReferencesUser for AuthToken {
Self: Sized,
{
let tokens = sqlx::query_as(
r#"SELECT * FROM auth_tokens WHERE user_id = $1 ORDER BY created_at DESC"#,
r#"SELECT * FROM user_auth_tokens WHERE user_id = $1 ORDER BY created_at DESC"#,
)
.bind(user_id);
tokens.fetch_all(database).await
}
}
impl AuthToken {
pub async fn insert(self, database: &PgPool) -> sqlx::Result<AuthToken> {
let Self {
user_id,
token,
active,
expires_at,
created_at,
..
} = self;
let token = sqlx::query_as(
r#"INSERT INTO auth_tokens (user_id, token, active, expires_at, created_at) VALUES ($1, $2, $3, $4, $4) RETURNING *"#,
).bind(user_id)
.bind(token)
.bind(active)
.bind(expires_at)
.bind(created_at)
.fetch_one(database).await?;
Ok(token)
}
pub async fn get_by_token(token: &str, database: &PgPool) -> sqlx::Result<Option<Self>> {
let token = sqlx::query_as(r#"SELECT * FROM auth_tokens WHERE token = $1"#)
.bind(token)
.fetch_optional(database)
.await?;
let token =
sqlx::query_as(r#"SELECT * FROM user_auth_tokens WHERE token = $1 AND active = true"#)
.bind(hash_token(token))
.fetch_optional(database)
.await?;
Ok(token)
}
pub async fn has_scope(&self, scope: Scopes, database: &PgPool) -> sqlx::Result<bool> {
let can_read: i64 = sqlx::query_scalar(
r#"SELECT COUNT(id) FROM user_auth_token_scopes WHERE user_auth_token_id = $1 AND scope = $2"#,
)
.bind(self.id)
.bind(scope)
.fetch_one(database).await?;
Ok(can_read > 0)
}
pub async fn get_scopes(&self, database: &PgPool) -> sqlx::Result<Vec<AuthTokenScope>> {
let scopes =
sqlx::query_as(r#"SELECT * FROM user_auth_token_scopes WHERE user_auth_token_id = $1"#)
.bind(self.id)
.fetch_all(database)
.await?;
Ok(scopes)
}
/// Checks if the user has the general scope for the repository action.
/// If it will check if the user has the specific scope for the repository action
#[instrument]
pub async fn has_repository_action(
&self,
repository_id: Uuid,
repository_action: RepositoryActionOptions,
database: &PgPool,
) -> sqlx::Result<bool> {
// Check if the user has the general scope. See RepositoryActions for more info
if self.has_scope(repository_action.into(), database).await? {
// The user has the general scope for this action
return Ok(true);
}
// TODO condense this into one query
let Some(actions) = sqlx::query_scalar::<_, Vec<RepositoryActionOptions>>(
r#"SELECT actions FROM user_auth_token_repository_scopes WHERE user_auth_token_id = $1 AND repository_id = $2"#,
)
.bind(self.id)
.bind(repository_id)
.fetch_optional(database).await? else{
return Ok(false);
};
Ok(actions.contains(&repository_action))
}
}
116 changes: 116 additions & 0 deletions crates/core/src/database/user/auth_token/repository_scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use sqlx::{
prelude::{FromRow, Type},
PgPool,
};
use tracing::{debug, instrument, span};
use uuid::Uuid;

use crate::{database::DateTime, user::permissions::RepositoryActionOptions};

use super::{create_token, hash_token};
/// Represents the actions that can be taken on a repository
///
/// Repository Scopes can be overridden by having a scope for all repositories
///
/// RepositoryActions::Read has Scopes::ReadRepository meaning they can read all repositories that the user has access to
/// RepositoryActions::Write has Scopes::WriteRepository meaning they can write to all repositories that the user has access to
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
pub struct AuthTokenRepositoryScope {
pub id: i32,
pub user_auth_token_id: i32,
pub repository_id: Uuid,
pub action: Vec<RepositoryActionOptions>,
pub created_at: DateTime,
}

#[derive(Debug, Clone, PartialEq, Eq, Builder)]
pub struct NewRepositoryToken {
pub user_id: i32,
pub source: String,
pub repositories: Vec<(Uuid, Vec<RepositoryActionOptions>)>,
pub expires_at: Option<DateTime>,
}
impl NewRepositoryToken {
pub fn new(
user_id: i32,
source: String,
repository: Uuid,
actions: Vec<RepositoryActionOptions>,
) -> Self {
Self {
user_id,
source,
repositories: vec![(repository, actions)],
expires_at: None,
}
}
pub fn add_repository(
mut self,
repository: Uuid,
actions: Vec<RepositoryActionOptions>,
) -> Self {
self.repositories.push((repository, actions));
self
}
#[instrument]
pub async fn insert(self, database: &PgPool) -> sqlx::Result<(i32, String)> {
let token = create_token(database).await?;
let hashed_token = hash_token(&token);
let Self {
user_id,
source,
repositories,
expires_at,
} = self;

let token_id: i32 = sqlx::query_scalar(
r#"INSERT INTO user_auth_tokens (user_id, token, source, expires_at) VALUES ($1, $2, $3, $4) RETURNING id"#,
).bind(user_id)
.bind(hashed_token)
.bind(source)
.bind(expires_at)
.fetch_one(database).await?;
let span = span!(tracing::Level::DEBUG, "inserting scopes");
let _guard = span.enter();
for (repository_id, actions) in repositories {
debug!(?repository_id, ?actions, "Inserting scope");
NewRepositoryScope {
token_id: token_id,
repository: repository_id,
actions,
}
.insert_no_return(database)
.await?;
}
Ok((token_id, token))
}
}
#[derive(Debug)]
pub struct NewRepositoryScope {
pub token_id: i32,
pub repository: Uuid,
pub actions: Vec<RepositoryActionOptions>,
}
impl NewRepositoryScope {
#[instrument]
pub async fn insert_no_return(self, database: &PgPool) -> sqlx::Result<()> {
let Self {
token_id,
repository,
actions,
} = self;
sqlx::query(
r#"INSERT INTO user_auth_token_repository_scopes (user_auth_token_id, repository, actions) VALUES ($1, $2, $3)"#,
)
.bind(token_id)
.bind(repository)
.bind(actions)
.execute(database)
.await?;

Ok(())
}
}
31 changes: 31 additions & 0 deletions crates/core/src/database/user/auth_token/scope.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use serde::{Deserialize, Serialize};
use sqlx::{prelude::FromRow, PgPool};

use crate::{database::DateTime, user::scopes::Scopes};

/// Table Name: user_auth_token_scopes
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, FromRow)]
pub struct AuthTokenScope {
pub id: i32,
pub user_auth_token_id: i32,
pub scope: Scopes,
pub created_at: DateTime,
}
#[derive(Debug)]
pub struct NewAuthTokenScope {
pub user_auth_token_id: i32,
pub scope: Scopes,
}
impl NewAuthTokenScope {
pub async fn insert_no_return(&self, database: &PgPool) -> sqlx::Result<()> {
sqlx::query(
r#"INSERT INTO user_auth_token_scopes (user_auth_token_id, scope) VALUES ($1, $2)"#,
)
.bind(self.user_auth_token_id)
.bind(self.scope)
.execute(database)
.await?;
Ok(())
}
}
35 changes: 35 additions & 0 deletions crates/core/src/database/user/auth_token/utils.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use crate::utils::base64_utils;
use rand::distributions::Alphanumeric;
use rand::{thread_rng, Rng};
use sha2::{Digest, Sha256};
use sqlx::PgPool;
/// Creates a new token checking if it already exists
pub async fn create_token(database: &PgPool) -> Result<String, sqlx::Error> {
let token = loop {
let token = generate_token();
let exists: i64 =
sqlx::query_scalar(r#"SELECT COUNT(id) FROM user_auth_tokens WHERE token = $1"#)
.bind(&token)
.fetch_one(database)
.await?;
if exists == 0 {
break token;
}
};
Ok(token)
}
/// Generates a new token for the user
pub fn generate_token() -> String {
// TODO: Secure this
thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect()
}

pub fn hash_token(token: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(token);
base64_utils::encode(&hasher.finalize())
}
Loading

0 comments on commit 0f612ff

Please sign in to comment.