Add initial version of controller

This commit is contained in:
Evie Viau-Chow-Stuart 2022-09-12 02:14:52 -04:00
parent f177d01df6
commit 5fe90e57eb
Signed by: evie
GPG key ID: 928652CDFCEC8099
40 changed files with 6900 additions and 4 deletions

2793
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

46
Cargo.toml Normal file
View file

@ -0,0 +1,46 @@
[package]
name = "driptorch-controller"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
sea-orm = { version = "0.9.2", features = [ "sqlx-postgres", "runtime-tokio-rustls", "macros" ] }
sea-orm-migration = "0.9.2"
migration = { version = "0.1.0", path = "./migration"}
futures = "0.3.24"
dotenv = "0.15.0"
serde = { version = "1.0.144", features = ["derive"] }
rmp = "0.8.11"
rmp-serde = "1.1.0"
ulid = "1.0.0"
axum = "0.6.0-rc.1"
tower = "0.4.13"
blake3 = "1.3.1"
base64ct = { version = "1.5.2", features = ["alloc"] }
rand = "0.8.5"
argon2 = "0.4.1"
tokio = { version = "1.21.0", features = ["full"] }
pretty_env_logger = "0.4.0"
log = "0.4.17"
chrono = "0.4.22"
zxcvbn = "2.2.1"
lazy_static = "1.4.0"
regex = "1.6.0"
async-trait = "0.1.57"
user-agent-parser = "0.3.3"

View file

@ -5,10 +5,10 @@ For more information, visit https://driptorch.net/
---
## Requirements:
### Deployment
#### Deployment
* PostgreSQL
### Development
#### Development
* Rust 1.60+
* PostgreSQL
@ -18,10 +18,8 @@ For more information, visit https://driptorch.net/
| DATABASE_URL | PostgreSQL database connection URL | | Y |
| UAP_REGEXES | Path to the [BrowserScope UA regex YAML](https://github.com/ua-parser/uap-core/blob/master/regexes.yaml) | | N |
---
### See also
* [driptorch-client](https://git.sr.ht/~eviee/driptorch-client)
* [driptorch-panel](https://git.sr.ht/~eviee/driptorch-panel)

2051
migration/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

19
migration/Cargo.toml Normal file
View file

@ -0,0 +1,19 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "^1", features = ["attributes", "tokio1"] }
[dependencies.sea-orm-migration]
version = "^0.9.0"
features = [
"sqlx-postgres",
"runtime-tokio-rustls"
]

41
migration/README.md Normal file
View file

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- migrate generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

28
migration/src/lib.rs Normal file
View file

@ -0,0 +1,28 @@
pub use sea_orm_migration::prelude::*;
mod m20220907_223615_create_users;
mod m20220907_223632_create_sessions;
mod m20220907_223637_create_zones;
mod m20220907_223639_create_records;
mod m20220907_223653_create_proxies;
mod m20220907_223633_create_teams;
mod m20220907_223634_create_team_members;
mod m20220908_204553_create_clients;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220907_223615_create_users::Migration),
Box::new(m20220907_223632_create_sessions::Migration),
Box::new(m20220907_223633_create_teams::Migration),
Box::new(m20220907_223634_create_team_members::Migration),
Box::new(m20220907_223637_create_zones::Migration),
Box::new(m20220907_223639_create_records::Migration),
Box::new(m20220907_223653_create_proxies::Migration),
Box::new(m20220908_204553_create_clients::Migration),
]
}
}

View file

@ -0,0 +1,69 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223615_create_users"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(User::Table)
.if_not_exists()
.col(ColumnDef::new(User::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(User::Name)
.string()
.not_null()
)
.col(ColumnDef::new(User::Email)
.string()
.not_null()
.unique_key()
)
.col(ColumnDef::new(User::Password)
.string()
.not_null()
)
.col(ColumnDef::new(User::Active)
.boolean()
.not_null()
.default(true)
)
.col(ColumnDef::new(User::Admin)
.boolean()
.not_null()
.default(false)
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(User::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum User {
Table,
Id,
Name,
Email,
Password,
Active,
Admin
}

View file

@ -0,0 +1,74 @@
use sea_orm_migration::prelude::*;
use super::m20220907_223615_create_users::User;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223632_create_sessions"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Session::Table)
.if_not_exists()
.col(ColumnDef::new(Session::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Session::Name)
.string()
.not_null()
)
.col(ColumnDef::new(Session::Ip)
.string()
.not_null()
)
.col(ColumnDef::new(Session::Token)
.string()
.not_null()
)
.col(ColumnDef::new(Session::Context)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-sessions-user-id")
.from(Session::Table, Session::Context)
.to(User::Table, User::Id)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Session::Expiry)
.timestamp()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Session::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Session {
Table,
Id,
Name,
Ip,
Token,
Context,
Expiry,
}

View file

@ -0,0 +1,57 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223633_create_teams"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Team::Table)
.if_not_exists()
.col(ColumnDef::new(Team::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Team::Name)
.string()
.not_null()
)
.col(ColumnDef::new(Team::Active)
.boolean()
.not_null()
.default(true)
)
.col(ColumnDef::new(Team::Personal)
.boolean()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Team::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Team {
Table,
Id,
Name,
Active,
Personal
}

View file

@ -0,0 +1,68 @@
use sea_orm_migration::prelude::*;
use crate::m20220907_223615_create_users::User;
use crate::m20220907_223633_create_teams::Team;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223634_create_team_members"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(TeamMember::Table)
.if_not_exists()
.col(ColumnDef::new(TeamMember::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(TeamMember::TeamId)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-team-members-team-id")
.from(TeamMember::Table, TeamMember::TeamId)
.to(Team::Table, Team::Id)
)
.col(ColumnDef::new(TeamMember::UserId)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-team-members-user-id")
.from(TeamMember::Table, TeamMember::UserId)
.to(User::Table, User::Id)
)
.col(ColumnDef::new(TeamMember::Permission)
.string()
.not_null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(TeamMember::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum TeamMember {
Table,
Id,
TeamId,
UserId,
Permission
}

View file

@ -0,0 +1,65 @@
use sea_orm_migration::prelude::*;
use crate::m20220907_223633_create_teams::Team;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223637_create_zones"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Zone::Table)
.if_not_exists()
.col(ColumnDef::new(Zone::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Zone::Owner)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-zone-owner-id")
.from(Zone::Table, Zone::Owner)
.to(Team::Table, Team::Id)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Zone::Origin)
.string()
.not_null()
.unique_key()
)
.col(ColumnDef::new(Zone::Delegated)
.boolean()
.not_null()
.default(false)
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Zone::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Zone {
Table,
Id,
Owner,
Origin,
Delegated
}

View file

@ -0,0 +1,64 @@
use sea_orm_migration::prelude::*;
use crate::m20220907_223637_create_zones::Zone;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223639_create_records"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Record::Table)
.if_not_exists()
.col(ColumnDef::new(Record::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Record::Zone)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-record-zone-id")
.from(Record::Table, Record::Zone)
.to(Zone::Table, Zone::Id)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Record::Value)
.binary()
.not_null()
)
.col(ColumnDef::new(Record::Active)
.boolean()
.not_null()
.default(true)
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Record::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
pub enum Record {
Table,
Id,
Zone,
Value,
Active,
}

View file

@ -0,0 +1,65 @@
use sea_orm_migration::prelude::*;
use crate::m20220907_223639_create_records::Record;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220907_223653_create_proxies"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Proxy::Table)
.if_not_exists()
.col(ColumnDef::new(Proxy::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Proxy::Record)
.string()
.not_null()
)
.foreign_key(ForeignKey::create()
.name("fk-proxy-record-id")
.from(Proxy::Table, Proxy::Record)
.to(Record::Table, Record::Id)
.on_delete(ForeignKeyAction::Cascade)
)
.col(ColumnDef::new(Proxy::Port)
.integer()
.not_null()
.default(443)
)
.col(ColumnDef::new(Proxy::Active)
.boolean()
.not_null()
.default(true)
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Proxy::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum Proxy {
Table,
Id,
Record,
Port,
Active,
}

View file

@ -0,0 +1,81 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m20220908_204553_create_clients"
}
}
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Client::Table)
.if_not_exists()
.col(ColumnDef::new(Client::Id)
.string()
.not_null()
.primary_key()
)
.col(ColumnDef::new(Client::Name)
.string()
.not_null()
.unique_key()
)
.col(ColumnDef::new(Client::Ip)
.string()
.not_null()
)
.col(ColumnDef::new(Client::Key)
.string()
.not_null()
)
.col(ColumnDef::new(Client::Active)
.boolean()
.not_null()
.default(true)
)
.col(ColumnDef::new(Client::DNS)
.boolean()
.not_null()
.default(true)
)
.col(ColumnDef::new(Client::Proxy)
.boolean()
.not_null()
.default(true)
)
.col(ColumnDef::new(Client::Health)
.string()
.not_null()
.default("DEAD")
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Client::Table).to_owned())
.await
}
}
/// Learn more at https://docs.rs/sea-query#iden
#[derive(Iden)]
enum Client {
Table,
Id,
Name,
Ip,
Key,
Active,
DNS,
Proxy,
Health
}

6
migration/src/main.rs Normal file
View file

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

1
src/dns/mod.rs Normal file
View file

@ -0,0 +1 @@
pub mod records;

78
src/dns/records.rs Normal file
View file

@ -0,0 +1,78 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize, Debug)]
pub enum RecordTypes {
SOA {
/// Time-to-live
ttl: i32,
/// Primary master name server
mname: String,
/// Email address of the administrator
rname: String,
/// Serial number
serial: u32,
/// Seconds until secondary name server refresh
refresh: i32,
/// Seconds after initial failure to retry from secondary name servers
retry: i32,
/// Seconds until secondary name servers give up if repeated failures
expire: i32,
/// Minimum time-to-live for negative caching
minimum: u32
},
A {
hostname: String,
ttl: i32,
address: Ipv4Addr
},
AAAA {
hostname: String,
ttl: i32,
address: Ipv6Addr
},
CNAME {
hostname: String,
ttl: i32,
cname: String
},
DNAME {
hostname: String,
ttl: i32,
dname: String
},
MX {
hostname: String,
ttl: i32,
preference: i16,
exchange: String
},
NS {
hostname: String,
ttl: i32,
nsdame: String
},
PTR {
hostname: String,
ttl: i32,
nsdame: String
},
TXT {
hostname: String,
ttl: i32,
txt_data: String
},
CAA {
hostname: String,
ttl: i32,
property: String
},
SRV {
hostname: String,
ttl: i32,
priority: u16,
weight: u16,
port: u16,
target: String
}
}

29
src/entities/client.rs Normal file
View file

@ -0,0 +1,29 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "client")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
#[sea_orm(unique)]
pub name: String,
pub ip: String,
pub key: String,
pub active: bool,
pub dns: bool,
pub proxy: bool,
pub health: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,23 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "controller")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub active: bool,
pub health: String,
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
panic!("No RelationDef")
}
}
impl ActiveModelBehavior for ActiveModel {}

12
src/entities/mod.rs Normal file
View file

@ -0,0 +1,12 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
pub mod prelude;
pub mod client;
pub mod proxy;
pub mod record;
pub mod session;
pub mod team;
pub mod team_member;
pub mod user;
pub mod zone;

10
src/entities/prelude.rs Normal file
View file

@ -0,0 +1,10 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
pub use super::client::Entity as Client;
pub use super::proxy::Entity as Proxy;
pub use super::record::Entity as Record;
pub use super::session::Entity as Session;
pub use super::team::Entity as Team;
pub use super::team_member::Entity as TeamMember;
pub use super::user::Entity as User;
pub use super::zone::Entity as Zone;

33
src/entities/proxy.rs Normal file
View file

@ -0,0 +1,33 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "proxy")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub record: String,
pub port: i32,
pub active: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::record::Entity",
from = "Column::Record",
to = "super::record::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Record,
}
impl Related<super::record::Entity> for Entity {
fn to() -> RelationDef {
Relation::Record.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

41
src/entities/record.rs Normal file
View file

@ -0,0 +1,41 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "record")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub zone: String,
pub value: Vec<u8>,
pub active: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::zone::Entity",
from = "Column::Zone",
to = "super::zone::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Zone,
#[sea_orm(has_many = "super::proxy::Entity")]
Proxy,
}
impl Related<super::zone::Entity> for Entity {
fn to() -> RelationDef {
Relation::Zone.def()
}
}
impl Related<super::proxy::Entity> for Entity {
fn to() -> RelationDef {
Relation::Proxy.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

35
src/entities/session.rs Normal file
View file

@ -0,0 +1,35 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "session")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub ip: String,
pub token: String,
pub context: String,
pub expiry: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::Context",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
User,
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

35
src/entities/team.rs Normal file
View file

@ -0,0 +1,35 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "team")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
pub active: bool,
pub personal: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::team_member::Entity")]
TeamMember,
#[sea_orm(has_many = "super::zone::Entity")]
Zone,
}
impl Related<super::team_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::TeamMember.def()
}
}
impl Related<super::zone::Entity> for Entity {
fn to() -> RelationDef {
Relation::Zone.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -0,0 +1,47 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "team_member")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub team_id: String,
pub user_id: String,
pub permission: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::team::Entity",
from = "Column::TeamId",
to = "super::team::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
Team,
#[sea_orm(
belongs_to = "super::user::Entity",
from = "Column::UserId",
to = "super::user::Column::Id",
on_update = "NoAction",
on_delete = "NoAction"
)]
User,
}
impl Related<super::team::Entity> for Entity {
fn to() -> RelationDef {
Relation::Team.def()
}
}
impl Related<super::user::Entity> for Entity {
fn to() -> RelationDef {
Relation::User.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

38
src/entities/user.rs Normal file
View file

@ -0,0 +1,38 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "user")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub name: String,
#[sea_orm(unique)]
pub email: String,
pub password: String,
pub active: bool,
pub admin: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(has_many = "super::session::Entity")]
Session,
#[sea_orm(has_many = "super::team_member::Entity")]
TeamMember,
}
impl Related<super::session::Entity> for Entity {
fn to() -> RelationDef {
Relation::Session.def()
}
}
impl Related<super::team_member::Entity> for Entity {
fn to() -> RelationDef {
Relation::TeamMember.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

42
src/entities/zone.rs Normal file
View file

@ -0,0 +1,42 @@
//! SeaORM Entity. Generated by sea-orm-codegen 0.9.2
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "zone")]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
pub id: String,
pub owner: String,
#[sea_orm(unique)]
pub origin: String,
pub delegated: bool,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::team::Entity",
from = "Column::Owner",
to = "super::team::Column::Id",
on_update = "NoAction",
on_delete = "Cascade"
)]
Team,
#[sea_orm(has_many = "super::record::Entity")]
Record,
}
impl Related<super::team::Entity> for Entity {
fn to() -> RelationDef {
Relation::Team.def()
}
}
impl Related<super::record::Entity> for Entity {
fn to() -> RelationDef {
Relation::Record.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

116
src/main.rs Normal file
View file

@ -0,0 +1,116 @@
#[macro_use] extern crate log;
use std::env;
use std::net::SocketAddr;
use std::path::Path;
use axum::extract::Extension;
use axum::Router;
use axum::routing::{delete, get, post};
use dotenv::dotenv;
use sea_orm::{ConnectOptions, Database};
use sea_orm_migration::prelude::*;
use tower::ServiceBuilder;
mod entities;
mod dns;
mod util;
mod routes;
const VERSION: &str = env!("CARGO_PKG_VERSION");
#[tokio::main]
async fn main() {
dotenv().ok();
// Default to logging all info logs
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info")
}
pretty_env_logger::init();
info!("██████╗ ██████╗ ██╗██████╗ ████████╗ ██████╗ ██████╗ ██████╗██╗ ██╗");
info!("██╔══██╗██╔══██╗██║██╔══██╗╚══██╔══╝██╔═══██╗██╔══██╗██╔════╝██║ ██║");
info!("██║ ██║██████╔╝██║██████╔╝ ██║ ██║ ██║██████╔╝██║ ███████║");
info!("██║ ██║██╔══██╗██║██╔═══╝ ██║ ██║ ██║██╔══██╗██║ ██╔══██║");
info!("██████╔╝██║ ██║██║██║ ██║ ╚██████╔╝██║ ██║╚██████╗██║ ██║");
info!("╚═════╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝");
info!("._______ ._______ .______ _____._.______ ._______ .___ .___ ._______.______");
info!(":_. ___\\: .___ \\ : \\ \\__ _:|: __ \\ : .___ \\ | | | | : .____/: __ \\ ");
info!("| : |/\\ | : | || | | :|| \\____|| : | || | | | | : _/\\ | \\____|");
info!("| / \\| : || | | | || : \\ | : || |/\\ | |/\\ | / \\| : \\ ");
info!("|. _____/ \\_. ___/ |___| | | || |___\\ \\_. ___/ | / \\| / \\|_.: __/| |___\\");
info!(" :/ :/ |___| |___||___| :/ |______/|______/ :/ |___| ");
info!(" : : : ");
info!("Version {}", VERSION);
info!("Checking for supplemental files...");
if !Path::new(&env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml"))).exists(){
error!("Please download https://github.com/ua-parser/uap-core/blob/master/regexes.yaml either place it next to the executable or add it's path to env variable UAP_REGEXES! Halting start-up.");
std::process::exit(1);
}
info!("Connecting to database...");
let database_url = env::var("DATABASE_URL")
.expect("DATABASE_URL must be set! Halting start-up.");
let connection = Database::connect(
ConnectOptions::new(database_url).sqlx_logging(false).to_owned())
.await
.expect("Failed to connect to the database! Halting start-up.");
info!("Running migrations...");
migration::Migrator::up(&connection, None)
.await
.expect("Failed to run migrations! Halting start-up.");
info!("Starting web server...");
let app = Router::new()
.route("/", get(routes::status::status))
// Auth
.route("/user/register", post(routes::auth::register::register))
.route("/user/login", post(routes::auth::login::login))
.route("/user/logout", post(routes::auth::logout::logout))
.route("/user/delete", delete(routes::auth::delete::delete))
.route("/user/list_sessions", get(routes::auth::list_sessions::list_sessions))
// Teams
// Zones
// Records
// Proxies
.layer(
ServiceBuilder::new()
.layer(Extension(connection))
);
let addr = env::var("LISTEN_ADDR")
.unwrap_or("127.0.0.1:32204".to_string());
let socket_addr: SocketAddr = addr.parse().expect("Failed to parse LISTEN_ADDR! Halting start-up.");
let axum_builder = axum::Server::try_bind(&socket_addr);
match axum_builder {
Ok(_) => {
info!("Driptorch Controller v{} is now listening on {}!", VERSION, socket_addr);
axum_builder
.expect("Passed builder match but still returned error? Halting start-up.")
.serve(app.into_make_service_with_connect_info::<SocketAddr>())
.await
.expect("Failed to bind to port! Halting start-up.");
}
Err(_) => {
error!("Driptorch Controller v{} failed to bind to {}! Halting start-up.", VERSION, socket_addr);
std::process::exit(1);
}
}
}

81
src/routes/auth/delete.rs Normal file
View file

@ -0,0 +1,81 @@
use axum::Extension;
use axum::http::StatusCode;
use axum::response::IntoResponse;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use crate::entities::prelude::{Team, TeamMember};
use crate::entities::{team, team_member, user};
use crate::util::auth::{TeamPermissions, UserFromBearer};
pub async fn delete(
Extension(ref connection): Extension<DatabaseConnection>,
UserFromBearer(user): UserFromBearer,
) -> impl IntoResponse {
let user = user.0;
let owned_teams = TeamMember::find()
.filter(team_member::Column::UserId.eq(user.clone().id))
.filter(team_member::Column::Permission.eq(TeamPermissions::OWNER.to_string()))
.all(connection)
.await
.expect("Failed to retrieve teams from database");
let mut personal_team: Option<team::Model> = None;
for team in owned_teams {
let owned_team = Team::find_by_id(team.clone().team_id)
.one(connection)
.await
.expect("Failed to get owned team by id");
match owned_team {
None => {
error!("team_member still exists for {} for {} but the team doesn't exist!", team.team_id, user.id);
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
}
Some(owned_team) => {
if !owned_team.personal {
return (StatusCode::BAD_REQUEST, "User owns non-personal teams.".to_string());
} else {
personal_team = Some(owned_team.clone());
}
}
}
}
let personal_team = personal_team.unwrap();
// Kick everyone out of the user's personal team
let team_member_delete = team_member::Entity::delete_many()
.filter(team_member::Column::TeamId.eq(personal_team.clone().id))
.exec(connection)
.await
.expect("Could not delete team_member during user deletion!");
if team_member_delete.rows_affected.eq(&0) {
error!("Could not delete personal team_member for {}!", user.id);
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
}
let team_delete = team::Entity::delete_by_id(personal_team.clone().id)
.exec(connection)
.await
.expect("Failed to delete personal team during user deletion!");
if team_delete.rows_affected.eq(&0) {
error!("Could not delete personal team for {}!", user.id);
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
}
let user_delete = user::Entity::delete_by_id(user.clone().id)
.exec(connection)
.await
.expect("Failed to delete user!");
if user_delete.rows_affected.eq(&0) {
error!("Could not delete user {}!", user.id);
return (StatusCode::INTERNAL_SERVER_ERROR, "An internal server error has occurred".to_string());
}
(StatusCode::OK, format!("Goodbye forever, {}!", user.name))
}

View file

@ -0,0 +1,45 @@
use axum::{Extension, Json};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use sea_orm::{DatabaseConnection, EntityTrait};
use crate::entities::prelude::Session;
use crate::entities::session;
use crate::util::auth::UserFromBearer;
use sea_orm::{QueryFilter, ColumnTrait};
use serde::Serialize;
#[derive(Serialize)]
pub struct ListSessionsResponse {
sessions: Vec<ListSession>
}
#[derive(Serialize)]
pub struct ListSession {
id: String,
name: String,
ip: String
}
pub async fn list_sessions(
Extension(ref connection): Extension<DatabaseConnection>,
UserFromBearer(user): UserFromBearer,
) -> impl IntoResponse {
let user = user.0;
let mut listed_sessions: Vec<ListSession> = Vec::new();
let total_sessions: Vec<session::Model> = Session::find()
.filter(session::Column::Context.eq(user.clone().id))
.all(connection)
.await
.expect("Failed to access database");
for session in total_sessions {
listed_sessions.push(ListSession {
id: session.id,
name: session.name,
ip: session.ip
});
}
(StatusCode::OK, Json(ListSessionsResponse { sessions: listed_sessions }))
}

153
src/routes/auth/login.rs Normal file
View file

@ -0,0 +1,153 @@
use std::net::SocketAddr;
use argon2::{Argon2, PasswordHash, PasswordVerifier};
use axum::{Extension, Form, Json};
use axum::extract::ConnectInfo;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use chrono::{Duration, NaiveDateTime};
use lazy_static::lazy_static;
use regex::Regex;
use sea_orm::*;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use crate::entities::session;
use crate::entities::session::Entity as Session;
use crate::entities::user;
use crate::entities::user::Entity as User;
use crate::util::auth::assemble_session_name;
use crate::util::generate_session_token;
#[derive(Deserialize)]
pub struct AuthUserForm {
email: String,
password: String
}
#[derive(Serialize)]
pub struct AuthUserFormIssues {
email: Vec<String>,
password: Vec<String>
}
#[derive(Serialize)]
pub struct AuthUserFormResponse {
session_token: Option<String>,
issues: Option<AuthUserFormIssues>
}
pub async fn login(
Extension(ref connection): Extension<DatabaseConnection>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
Form(input): Form<AuthUserForm>
) -> impl IntoResponse {
let mut validation_issues = AuthUserFormIssues {
email: vec![],
password: vec![]
};
lazy_static! {
static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#)
.expect("Failed to compile email regex");
}
// Ensure form contents are safe
if input.email.is_empty() {
validation_issues.email.push("Email cannot be empty.".to_string());
} else if !EMAIL_RE.is_match(&input.email) {
validation_issues.email.push("Email is invalid.".to_string());
}
if input.password.is_empty(){
validation_issues.password.push("Password cannot be empty.".to_string());
}
// Return early if we have issues with form content so far
if !validation_issues.email.is_empty() || !validation_issues.password.is_empty() {
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
}
// Check to see if a user with the email exists
let existing_user: Option<user::Model> = User::find()
.filter(user::Column::Email.eq(input.email.clone()))
.one(connection)
.await
.expect("Failed to check database.");
if !existing_user.is_some() {
validation_issues.email.push("An account with this email doesn't exist.".to_string());
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
}
let existing_user = existing_user.unwrap();
// Check password
let existing_password_hash = PasswordHash::new(&existing_user.password)
.expect("Failed to generate password hash from database.");
if !Argon2::default().verify_password(input.password.as_bytes(), &existing_password_hash).is_ok() {
validation_issues.password.push("Incorrect password.".to_string());
return (StatusCode::BAD_REQUEST, Json(AuthUserFormResponse { session_token: None, issues: Some(validation_issues) }));
}
let session_name;
match headers.get("User-Agent") {
None => {
session_name = String::from("Unknown");
}
Some(header) => {
match header.to_str() {
Ok(header) => {
session_name = assemble_session_name(header).await;
}
Err(_) => {
session_name = String::from("Unknown");
}
}
}
}
let ip;
match headers.get("X-Real-IP") {
None => {
ip = addr.ip().to_string();
}
Some(header) => {
match header.to_str() {
Ok(header) => {
ip = String::from(header);
}
Err(_) => {
ip = addr.ip().to_string();
}
}
}
}
let session_token = generate_session_token();
let expiry: NaiveDateTime = chrono::offset::Utc::now().naive_local() + Duration::days(20);
// Generate session for newly created user
let new_session = session::ActiveModel {
id: ActiveValue::Set(Ulid::new().to_string()),
name: ActiveValue::Set(session_name.clone()),
ip: ActiveValue::set(ip.clone()),
token: ActiveValue::Set(session_token.clone()),
context: ActiveValue::Set(existing_user.id.clone()),
expiry: ActiveValue::Set(expiry)
};
let session_res = Session::insert(new_session)
.exec(connection)
.await;
match session_res {
Ok(_) => {
(StatusCode::OK, Json(AuthUserFormResponse { session_token: Some(session_token), issues: None }))
}
Err(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(AuthUserFormResponse { session_token: None, issues: None }))
}
}
}

100
src/routes/auth/logout.rs Normal file
View file

@ -0,0 +1,100 @@
use axum::{Extension, extract};
use axum::http::StatusCode;
use axum::response::IntoResponse;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use crate::util::auth::UserFromBearer;
use serde::Deserialize;
use crate::entities::prelude::Session;
use crate::entities::session;
#[derive(Deserialize)]
pub struct LogoutInput {
session_id: String
}
pub async fn logout(
Extension(ref connection): Extension<DatabaseConnection>,
UserFromBearer(user): UserFromBearer,
extract::Json(payload): extract::Json<LogoutInput>
) -> impl IntoResponse {
let accessed_session_id = user.1;
let user = user.0;
return match payload.session_id.as_str() {
"ALL" => {
// Invalidate all of the user's sessions
// By connecting to this route, we know there is at least 1 session
session::Entity::delete_many()
.filter(session::Column::Context.eq(user.clone().id))
.exec(connection)
.await
.expect("Failed to run delete on sessions database");
// Ensure no sessions are left for the user
let total_sessions = Session::find()
.filter(session::Column::Context.eq(user.clone().id))
.all(connection)
.await
.expect("Failed to access database");
if total_sessions.is_empty() {
(StatusCode::OK, format!("Goodbye {}!", user.name))
} else {
error!("Unable to delete {}'s sessions!", user.id);
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
}
}
"" => {
// Invalidate the user's current session
let session_deletion = session::Entity::delete_by_id(accessed_session_id.clone())
.exec(connection)
.await
.expect("Failed to run delete on sessions database");
if session_deletion.rows_affected.eq(&1) {
(StatusCode::OK, format!("Goodbye {}!", user.name))
} else {
error!("Unable to delete {}'s current session! {}", user.id, accessed_session_id);
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
}
}
_ => {
// Check if the ID is valid and exists
let requested_session = Session::find()
.filter(session::Column::Id.eq(payload.session_id.clone()))
.one(connection)
.await;
match requested_session {
Ok(_) => {
let requested_session = requested_session.unwrap();
if requested_session.is_some() {
let requested_session = requested_session.unwrap();
// Ensure the session is for the currently authed user
if requested_session.context != user.id {
return (StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string());
}
// Invalidate the session
let session_deletion = session::Entity::delete_by_id(requested_session.clone().id)
.exec(connection)
.await
.expect("Failed to run delete on sessions database");
if session_deletion.rows_affected.eq(&1) {
(StatusCode::OK, format!("Goodbye {}!", user.name))
} else {
error!("Unable to delete {}'s requested session! {}", user.id, requested_session.clone().id);
(StatusCode::INTERNAL_SERVER_ERROR, "An Internal Server Error has occurred".to_string())
}
} else {
(StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string())
}
}
Err(_) => {
(StatusCode::BAD_REQUEST, "Requested session ID is invalid".to_string())
}
}
}
}
}

5
src/routes/auth/mod.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod register;
pub mod login;
pub mod logout;
pub mod list_sessions;
pub mod delete;

230
src/routes/auth/register.rs Normal file
View file

@ -0,0 +1,230 @@
use std::net::SocketAddr;
use axum::{Extension, Form, Json};
use axum::extract::ConnectInfo;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use chrono::{Duration, NaiveDateTime};
use sea_orm::*;
use serde::{Deserialize, Serialize};
use ulid::Ulid;
use lazy_static::lazy_static;
use regex::Regex;
use crate::entities::{team, team_member, user};
use crate::entities::prelude::{Team, TeamMember};
use crate::entities::user::Entity as User;
use crate::entities::session;
use crate::entities::session::Entity as Session;
use crate::util::{generate_session_token, hash_password};
use crate::util::auth::{assemble_session_name, TeamPermissions};
#[derive(Deserialize, Clone)]
pub struct NewUserForm {
name: String,
email: String,
password: String
}
#[derive(Serialize)]
pub struct NewUserFormIssues {
name: Vec<String>,
email: Vec<String>,
password: Vec<String>
}
#[derive(Serialize)]
pub struct NewUserFormResponse {
session_token: Option<String>,
issues: Option<NewUserFormIssues>
}
pub async fn register(
Extension(ref connection): Extension<DatabaseConnection>,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
headers: HeaderMap,
Form(input): Form<NewUserForm>
) -> impl IntoResponse {
let mut validation_issues = NewUserFormIssues {
name: vec![],
email: vec![],
password: vec![]
};
lazy_static! {
static ref EMAIL_RE: Regex = Regex::new(r#"(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])"#)
.expect("Failed to compile email regex");
}
// Ensure form contents are safe
if input.name.is_empty() {
validation_issues.name.push("Name cannot be empty.".to_string());
} else if input.name.len() > 128 {
validation_issues.name.push("Name is too long.".to_string());
}
if input.email.is_empty() {
validation_issues.email.push("Email cannot be empty.".to_string());
} else if !EMAIL_RE.is_match(&input.email) {
validation_issues.email.push("Email is invalid.".to_string());
}
if input.password.is_empty(){
validation_issues.password.push("Password cannot be empty.".to_string());
} else {
// Check password security
let pass_estimate = zxcvbn::zxcvbn(&input.password, &[])
.expect("Failed to check password security.");
if pass_estimate.score() <= 2 {
for i in pass_estimate.feedback().clone().unwrap().suggestions() {
validation_issues.password.push(i.to_string());
}
}
}
// Return early if we have issues with form content so far
if !validation_issues.name.is_empty() || !validation_issues.email.is_empty() || !validation_issues.password.is_empty() {
return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) }));
}
// Check to see if a user with the email already exists
let existing_user = User::find()
.filter(user::Column::Email.eq(input.email.clone()))
.one(connection)
.await
.expect("Failed to check database.");
if existing_user.is_some() {
validation_issues.email.push("An account with this email already exists.".to_string());
return (StatusCode::BAD_REQUEST, Json(NewUserFormResponse { session_token: None, issues: Some(validation_issues) }));
}
let user_id = Ulid::new().to_string();
let new_user = user::ActiveModel {
id: ActiveValue::Set(user_id.clone()),
name: ActiveValue::Set(input.clone().name),
email: ActiveValue::Set(input.clone().email),
password: ActiveValue::Set(hash_password(input.clone().password)),
..Default::default()
};
let res = User::insert(new_user.clone())
.exec(connection)
.await;
return match res {
Ok(_) => {
// Create a personal team
let team_id = String::from(Ulid::new());
let new_team = team::ActiveModel {
id: ActiveValue::set(team_id.clone()),
name: ActiveValue::Set(
format!("{}'s Personal Team", &input.name)
),
active: Default::default(),
personal: ActiveValue::Set(true)
};
let team_creation = Team::insert(new_team.clone())
.exec(connection)
.await;
if team_creation.is_err() {
// Delete user on team creation error
new_user.delete(connection).await
.expect("Failed to delete user from database after failing to create personal team for said user!");
return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }));
}
// Add user to personal team
let team_addition = TeamMember::insert(
team_member::ActiveModel {
id: ActiveValue::Set(String::from(Ulid::new())),
team_id: ActiveValue::Set(team_id.clone()),
user_id: ActiveValue::Set(user_id.clone()),
permission: ActiveValue::Set(TeamPermissions::OWNER.to_string())
}
).exec(connection)
.await;
if team_addition.is_err() {
// Delete team and user on permission addition error
new_team.delete(connection).await
.expect("Failed to delete team from database after failing to add permissions for personal team for said user!");
new_user.delete(connection).await
.expect("Failed to delete user from database after failing to add permissions for personal team for said user!");
return (StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }));
}
let session_name;
match headers.get("User-Agent") {
None => {
session_name = String::from("Unknown");
}
Some(header) => {
match header.to_str() {
Ok(header) => {
session_name = assemble_session_name(header).await;
}
Err(_) => {
session_name = String::from("Unknown");
}
}
}
}
let ip;
match headers.get("X-Real-IP") {
None => {
ip = addr.ip().to_string();
}
Some(header) => {
match header.to_str() {
Ok(header) => {
ip = String::from(header);
}
Err(_) => {
ip = addr.ip().to_string();
}
}
}
}
let session_token = generate_session_token();
let expiry: NaiveDateTime = chrono::offset::Utc::now().naive_local() + Duration::days(20);
// Generate session for newly created user
let new_session = session::ActiveModel {
id: ActiveValue::Set(Ulid::new().to_string()),
name: ActiveValue::Set(session_name.clone()),
ip: ActiveValue::set(ip.clone()),
token: ActiveValue::Set(session_token.clone()),
context: ActiveValue::Set(user_id.clone()),
expiry: ActiveValue::Set(expiry)
};
let session_res = Session::insert(new_session)
.exec(connection)
.await;
match session_res {
Ok(_) => {
(StatusCode::CREATED, Json(NewUserFormResponse { session_token: Some(session_token), issues: None }))
}
Err(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }))
}
}
}
Err(_) => {
(StatusCode::INTERNAL_SERVER_ERROR, Json(NewUserFormResponse { session_token: None, issues: None }))
}
}
}

2
src/routes/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod status;
pub mod auth;

34
src/routes/status.rs Normal file
View file

@ -0,0 +1,34 @@
use axum::http::StatusCode;
use axum::Json;
use axum::response::IntoResponse;
use serde::{Serialize, Deserialize};
use crate::VERSION;
#[derive(Serialize, Deserialize)]
enum Health {
HEALTHY,
UNHEALTHY,
DEAD
}
#[derive(Serialize, Deserialize)]
enum Context {
CONTROLLER,
CLIENT
}
#[derive(Serialize)]
struct Status {
pub context: Context,
pub version: String,
pub health: Health
}
pub async fn status() -> impl IntoResponse {
(StatusCode::OK, Json(Status {
context: Context::CONTROLLER,
version: VERSION.to_string(),
health: Health::HEALTHY
})
)
}

156
src/util/auth.rs Normal file
View file

@ -0,0 +1,156 @@
use std::borrow::Cow;
use std::env;
use std::fmt;
use std::fmt::Formatter;
use async_trait::async_trait;
use axum::extract::FromRequestParts;
use axum::http::{header::AUTHORIZATION, StatusCode};
use axum::http::request::Parts;
use lazy_static::lazy_static;
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
use user_agent_parser::{OS, Product};
use user_agent_parser::UserAgentParser;
use crate::entities::{session, user};
use crate::entities::prelude::{Session, User};
pub enum TeamPermissions {
OWNER,
ADMIN,
EDITOR,
VIEWER
}
impl fmt::Display for TeamPermissions {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
TeamPermissions::OWNER => write!(f, "OWNER"),
TeamPermissions::ADMIN => write!(f, "ADMIN"),
TeamPermissions::EDITOR => write!(f, "EDITOR"),
TeamPermissions::VIEWER => write!(f, "VIEWER")
}
}
}
/// Gets a user model and session id from a supplied session token
pub async fn get_user_from_token(token: String, connection: &DatabaseConnection) -> Option<(user::Model, String)> {
let requested_session: Option<session::Model> = Session::find()
.filter(session::Column::Token.eq(token))
.one(connection)
.await
.expect("Failed to retrieve session from the database.");
return match requested_session {
None => {
None
}
Some(_) => {
let requested_session = requested_session.unwrap();
let contexted_user: Option<user::Model> = User::find()
.filter(user::Column::Id.eq(requested_session.clone().context))
.one(connection)
.await
.expect("Failed to retrieve user from the database.");
match contexted_user {
None => {
error!("Session {} still exists for user {} of which doesn't exist!", requested_session.id, requested_session.context);
None
}
Some(_) => {
Some((contexted_user.unwrap(), requested_session.id))
}
}
}
}
}
#[derive(Clone)]
pub struct UserFromBearer(pub (user::Model, String));
#[async_trait]
impl<S> FromRequestParts<S> for UserFromBearer
where
S: Send + Sync,
{
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
// Get authorisation header
let authorisation = parts
.headers
.get(AUTHORIZATION)
.ok_or((StatusCode::UNAUTHORIZED, "`Authorization` header is missing"))?
.to_str()
.map_err(|_| {
(
StatusCode::BAD_REQUEST,
"`Authorization` header contains invalid characters",
)
})?;
// Check that its a well-formed bearer and return
let split = authorisation.split_once(' ');
match split {
Some((name, contents)) if name == "Bearer" => {
// Get database connection from header
let connection: &DatabaseConnection = parts.extensions.get::<DatabaseConnection>()
.expect("Failed to get database connection from auth extractor");
match get_user_from_token(contents.to_string(), connection).await {
None => {
Err((StatusCode::UNAUTHORIZED, "Provided token is invalid"))
}
Some(user) => Ok(Self(user))
}
},
_ => Err((
StatusCode::BAD_REQUEST,
"`Authorization` header must be a bearer token",
)),
}
}
}
pub async fn assemble_session_name(header: &str) -> String {
// TODO: Maybe reconsider having a static session name?
lazy_static! {
static ref UAP: UserAgentParser = UserAgentParser::from_path(&env::var("UAP_REGEXES").unwrap_or(String::from("./regexes.yaml"))).expect("Failed to load regexes.yaml");
}
let product: Product = UAP.parse_product(&header);
let os: OS = UAP.parse_os(&header);
let mut session_name_builder: String = String::new();
session_name_builder.push_str(
&product.name.unwrap_or(Cow::from("Unknown"))
);
if product.major.is_some() {
session_name_builder.push_str(" ");
session_name_builder.push_str(
&product.major.unwrap()
)
}
session_name_builder.push_str(" / ");
session_name_builder.push_str(
&os.name.unwrap_or(Cow::from("Unknown"))
);
if os.major.is_some() {
session_name_builder.push_str(" ");
session_name_builder.push_str(
&os.major.unwrap()
)
};
return session_name_builder
}

25
src/util/mod.rs Normal file
View file

@ -0,0 +1,25 @@
pub mod auth;
use argon2::{Argon2, PasswordHasher};
use argon2::password_hash::SaltString;
use base64ct::{Base64UrlUnpadded, Encoding};
use rand::{rngs::OsRng, RngCore};
pub fn generate_session_token() -> String {
let mut randombytes = [0u8; 32];
OsRng.fill_bytes(&mut randombytes);
let session_token = Base64UrlUnpadded::encode_string(blake3::hash(&randombytes).as_bytes());
session_token
}
pub fn hash_password(password: String) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2.hash_password(password.as_bytes(), &salt)
.expect("Failed to hash password!")
.to_string()
}