use stateful tokens

This commit is contained in:
Hannaeko 2022-04-24 12:42:53 +02:00
parent b70195bfe1
commit f1f64665cc
13 changed files with 118 additions and 249 deletions

169
Cargo.lock generated
View file

@ -115,12 +115,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "base64"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.13.0" version = "0.13.0"
@ -163,12 +157,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"
[[package]] [[package]]
name = "byteorder" name = "byteorder"
version = "1.4.3" version = "1.4.3"
@ -262,7 +250,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05"
dependencies = [ dependencies = [
"aes-gcm", "aes-gcm",
"base64 0.13.0", "base64",
"hkdf", "hkdf",
"hmac", "hmac",
"percent-encoding", "percent-encoding",
@ -347,6 +335,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d" checksum = "b28135ecf6b7d446b43e27e225622a038cc4e2930a1022f51cdb97ada19b8e4d"
dependencies = [ dependencies = [
"byteorder", "byteorder",
"chrono",
"diesel_derives", "diesel_derives",
"libsqlite3-sys", "libsqlite3-sys",
"r2d2", "r2d2",
@ -754,29 +743,6 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "js-sys"
version = "0.3.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jsonwebtoken"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32"
dependencies = [
"base64 0.12.3",
"pem",
"ring",
"serde",
"serde_json",
"simple_asn1",
]
[[package]] [[package]]
name = "lazy_static" name = "lazy_static"
version = "1.4.0" version = "1.4.0"
@ -918,7 +884,7 @@ dependencies = [
"log", "log",
"memchr", "memchr",
"mime", "mime",
"spin 0.9.3", "spin",
"tokio", "tokio",
"tokio-util 0.6.9", "tokio-util 0.6.9",
"version_check", "version_check",
@ -938,7 +904,7 @@ name = "nomilo"
version = "0.1.0-dev" version = "0.1.0-dev"
dependencies = [ dependencies = [
"argon2", "argon2",
"base64 0.13.0", "base64",
"chrono", "chrono",
"clap", "clap",
"diesel", "diesel",
@ -946,13 +912,12 @@ dependencies = [
"diesel_migrations", "diesel_migrations",
"figment", "figment",
"humantime", "humantime",
"jsonwebtoken", "rand",
"rocket", "rocket",
"rocket_sync_db_pools", "rocket_sync_db_pools",
"serde", "serde",
"serde_json", "serde_json",
"tokio", "tokio",
"toml",
"trust-dns-client", "trust-dns-client",
"trust-dns-proto", "trust-dns-proto",
"uuid", "uuid",
@ -967,17 +932,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-bigint"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304"
dependencies = [
"autocfg",
"num-integer",
"num-traits",
]
[[package]] [[package]]
name = "num-integer" name = "num-integer"
version = "0.1.44" version = "0.1.44"
@ -1116,17 +1070,6 @@ dependencies = [
"syn", "syn",
] ]
[[package]]
name = "pem"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb"
dependencies = [
"base64 0.13.0",
"once_cell",
"regex",
]
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@ -1337,21 +1280,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "ring"
version = "0.16.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc"
dependencies = [
"cc",
"libc",
"once_cell",
"spin 0.5.2",
"untrusted",
"web-sys",
"winapi",
]
[[package]] [[package]]
name = "rocket" name = "rocket"
version = "0.5.0-rc.1" version = "0.5.0-rc.1"
@ -1545,17 +1473,6 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "simple_asn1"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b"
dependencies = [
"chrono",
"num-bigint",
"num-traits",
]
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.6" version = "0.4.6"
@ -1578,12 +1495,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "spin"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.3" version = "0.9.3"
@ -1985,12 +1896,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.2.2" version = "2.2.2"
@ -2053,70 +1958,6 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4"
dependencies = [
"bumpalo",
"lazy_static",
"log",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.80"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744"
[[package]]
name = "web-sys"
version = "0.3.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View file

@ -13,16 +13,15 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", version = "0.5.0-rc.1", features = ["json"] } rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", version = "0.5.0-rc.1", features = ["json"] }
rocket_sync_db_pools = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", default-features = false, features = ["diesel_sqlite_pool"], version = "0.1.0-rc.1"} rocket_sync_db_pools = { git = "https://github.com/SergioBenitez/Rocket", rev = "6bdd2f8", default-features = false, features = ["diesel_sqlite_pool"], version = "0.1.0-rc.1"}
toml = "0.5"
base64 = "0.13.0" base64 = "0.13.0"
uuid = { version = "0.8.2", features = ["v4", "serde"] } uuid = { version = "0.8.2", features = ["v4", "serde"] }
diesel = { version = "1.4", features = ["sqlite"] } diesel = { version = "1.4", features = ["sqlite", "chrono"] }
diesel_migrations = "1.4" diesel_migrations = "1.4"
diesel-derive-enum = { version = "1", features = ["sqlite"] } diesel-derive-enum = { version = "1", features = ["sqlite"] }
jsonwebtoken = "7.2.0"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
humantime = "2.1.0" humantime = "2.1.0"
tokio = "1" tokio = "1"
figment = { version = "0.10.6", features = ["toml", "env"] } figment = { version = "0.10.6", features = ["toml", "env"] }
clap = {version = "3", features = ["derive", "cargo"]} clap = {version = "3", features = ["derive", "cargo"]}
argon2 = "0.4.0" argon2 = {version = "0.4", default-features = false, features = ["alloc", "password-hash"] }
rand = "0.8"

View file

@ -3,4 +3,4 @@
[print_schema] [print_schema]
file = "src/schema.rs" file = "src/schema.rs"
import_types = ["diesel::sql_types::*", "crate::models::users::*"] import_types = ["diesel::sql_types::*", "crate::models::user::*", "crate::models::auth::*"]

View file

@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE session;

View file

@ -0,0 +1,8 @@
-- Your SQL goes here
CREATE TABLE session (
`session_id` VARCHAR NOT NULL,
`user_id` VARCHAR NOT NULL,
`expires_at` VARCHAR NOT NULL,
PRIMARY KEY(session_id),
FOREIGN KEY(user_id) REFERENCES user(id)
);

View file

@ -2,8 +2,6 @@
url = "db.sqlite" url = "db.sqlite"
[release.web_app] [release.web_app]
# base64 secret, change it (openssl rand -base64 32)
secret = "Y2hhbmdlbWUK"
token_duration = "1d" token_duration = "1d"
[release.dns] [release.dns]

View file

@ -23,7 +23,6 @@ pub struct DnsClientConfig {
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct WebAppConfig { pub struct WebAppConfig {
pub secret: String,
#[serde(deserialize_with = "from_duration")] #[serde(deserialize_with = "from_duration")]
pub token_duration: Duration, pub token_duration: Duration,
} }

View file

@ -1,63 +1,77 @@
use uuid::Uuid;
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use chrono::serde::ts_seconds; use chrono::naive::serde::ts_seconds::serialize as ts_seconds_naive;
use chrono::prelude::{DateTime, Utc}; use chrono::{Duration, NaiveDateTime, Utc, DateTime};
use chrono::Duration;
use jsonwebtoken::{ use diesel::prelude::*;
encode, decode, use rand::Rng;
Header, Validation, use rand::rngs::OsRng;
Algorithm as JwtAlgorithm, EncodingKey, DecodingKey, use rand::distributions::Alphanumeric;
errors::Result as JwtResult
};
use crate::models::user::UserInfo; use crate::models::user::UserInfo;
use crate::schema::*;
use crate::models::errors::UserError;
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthClaims {
pub jti: String,
pub sub: String,
#[serde(with = "ts_seconds")]
pub exp: DateTime<Utc>,
#[serde(with = "ts_seconds")]
pub iat: DateTime<Utc>,
}
#[derive(Debug, Serialize)]
pub struct AuthTokenResponse {
pub token: String
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct AuthTokenRequest { pub struct AuthTokenRequest {
pub username: String, pub username: String,
pub password: String, pub password: String,
} }
impl AuthClaims { #[derive(Debug, Serialize, Queryable, Identifiable, Insertable)]
pub fn new(user_info: &UserInfo, token_duration: Duration) -> AuthClaims { #[table_name = "session"]
let jti = Uuid::new_v4().to_simple().to_string(); #[primary_key(session_id)]
let iat = Utc::now(); pub struct Session {
let exp = iat + token_duration; #[serde(rename = "token")]
pub session_id: String,
AuthClaims { #[serde(skip)]
jti, pub user_id: String,
sub: user_info.id.clone(), #[serde(serialize_with = "ts_seconds_naive")]
exp, pub expires_at: NaiveDateTime,
iat,
}
} }
pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> { impl Session {
decode::<AuthClaims>( pub fn generate_id() -> String {
token, OsRng
&DecodingKey::from_secret(secret.as_ref()), .sample_iter(&Alphanumeric)
&Validation::new(JwtAlgorithm::HS256) .take(50)
).map(|data| data.claims) .map(char::from)
.collect()
} }
pub fn encode(self, secret: &str) -> JwtResult<String> { pub fn from_session_id(conn: &diesel::SqliteConnection, id: &str) -> Result<Session, UserError> {
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref())) use crate::schema::session::dsl::*;
session
.find(id)
.get_result(conn)
.map_err(|_| UserError::ExpiredSession)
.and_then(|s: Session| {
let expires = DateTime::<Utc>::from_utc(s.expires_at, Utc);
if expires < Utc::now() {
Err(UserError::ExpiredSession)
} else {
Ok(s)
}
})
}
pub fn new(conn: &diesel::SqliteConnection, user_info: &UserInfo, token_duration: Duration) -> Result<Session, UserError> {
use crate::schema::session::dsl::*;
let expires = Utc::now() + token_duration;
let user_session = Session {
session_id: Session::generate_id(),
user_id: user_info.id.clone(),
expires_at: expires.naive_utc(),
};
diesel::insert_into(session)
.values(&user_session)
.execute(conn)
.map_err(UserError::DbError)?;
Ok(user_session)
} }
} }

View file

@ -18,7 +18,7 @@ pub enum UserError {
UserConflict, UserConflict,
BadCreds, BadCreds,
BadToken, BadToken,
ExpiredToken, ExpiredSession,
MalformedHeader, MalformedHeader,
PermissionDenied, PermissionDenied,
DbError(DieselError), DbError(DieselError),
@ -91,7 +91,7 @@ impl From<UserError> for ErrorResponse {
UserError::UserConflict => ErrorResponse::new(Status::Conflict, "This user already exists".into()), UserError::UserConflict => ErrorResponse::new(Status::Conflict, "This user already exists".into()),
UserError::NotFound => ErrorResponse::new(Status::NotFound, "User does not exist".into()), UserError::NotFound => ErrorResponse::new(Status::NotFound, "User does not exist".into()),
UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()), UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()),
UserError::ExpiredToken => ErrorResponse::new(Status::Unauthorized, "The provided token has expired".into()), UserError::ExpiredSession => ErrorResponse::new(Status::Unauthorized, "The provided session token has expired".into()),
UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()), UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()),
UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()), UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()),
UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exist".into()), UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exist".into()),

View file

@ -9,7 +9,7 @@ pub mod user;
pub mod zone; pub mod zone;
// Reexport types for convenience // Reexport types for convenience
pub use auth::{AuthClaims, AuthTokenRequest, AuthTokenResponse}; pub use auth::{AuthTokenRequest, Session};
pub use class::DNSClass; pub use class::DNSClass;
pub use errors::{UserError, ErrorResponse, make_500}; pub use errors::{UserError, ErrorResponse, make_500};
pub use name::{AbsoluteName, SerdeName}; pub use name::{AbsoluteName, SerdeName};

View file

@ -2,23 +2,21 @@ use uuid::Uuid;
use diesel::prelude::*; use diesel::prelude::*;
use diesel::result::Error as DieselError; use diesel::result::Error as DieselError;
use diesel_derive_enum::DbEnum; use diesel_derive_enum::DbEnum;
use rocket::{State, request::{FromRequest, Request, Outcome}}; use rocket::request::{FromRequest, Request, Outcome};
use rocket::outcome::try_outcome; use rocket::outcome::try_outcome;
use serde::{Deserialize}; use serde::{Deserialize};
use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
use argon2::password_hash::errors::Error as PasswordHashError; use argon2::password_hash::errors::Error as PasswordHashError;
use argon2::password_hash::rand_core::OsRng; use rand::rngs::OsRng;
use argon2::{Algorithm, Argon2, Version, Params}; use argon2::{Algorithm, Argon2, Version, Params};
use jsonwebtoken::{
errors::ErrorKind as JwtErrorKind
};
use crate::schema::*; use crate::schema::*;
use crate::DbConn; use crate::DbConn;
use crate::config::Config;
use crate::models::errors::{UserError, ErrorResponse, make_500}; use crate::models::errors::{UserError, ErrorResponse, make_500};
use crate::models::zone::Zone; use crate::models::zone::Zone;
use crate::models::auth::AuthClaims; use crate::models::auth::Session;
const BEARER: &str = "Bearer "; const BEARER: &str = "Bearer ";
@ -127,31 +125,24 @@ impl<'r> FromRequest<'r> for UserInfo {
}; };
let token = if auth_header.starts_with(BEARER) { let token = if auth_header.starts_with(BEARER) {
auth_header.trim_start_matches(BEARER) auth_header.trim_start_matches(BEARER).to_string()
} else { } else {
return ErrorResponse::from(UserError::MalformedHeader).into() return ErrorResponse::from(UserError::MalformedHeader).into()
}; };
let config = try_outcome!(request.guard::<&State<Config>>().await.map_failure(make_500));
let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500)); let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500));
let token_data = AuthClaims::decode( let session_res = conn.run(move |c| {
token, &config.web_app.secret Session::from_session_id(c, &token)
).map_err(|e| match e.into_kind() { }).await;
JwtErrorKind::ExpiredSignature => UserError::ExpiredToken,
_ => UserError::BadToken,
});
let token_data = match token_data { let session = match session_res {
Err(e) => return ErrorResponse::from(e).into(), Err(e) => return ErrorResponse::from(e).into(),
Ok(data) => data Ok(s) => s,
}; };
let user_id = token_data.sub;
conn.run(move |c| { conn.run(move |c| {
match LocalUser::get_user_by_uuid(c, &user_id) { match LocalUser::get_user_by_uuid(c, &session.user_id) {
Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(),
Err(e) => ErrorResponse::from(e).into(), Err(e) => ErrorResponse::from(e).into(),
Ok(d) => Outcome::Success(d), Ok(d) => Outcome::Success(d),
} }

View file

@ -12,17 +12,22 @@ pub async fn create_auth_token(
conn: DbConn, conn: DbConn,
config: &State<Config>, config: &State<Config>,
auth_request: Json<models::AuthTokenRequest> auth_request: Json<models::AuthTokenRequest>
) -> Result<Json<models::AuthTokenResponse>, models::ErrorResponse> { ) -> Result<Json<models::Session>, models::ErrorResponse> {
let user_info = conn.run(move |c| { let session_duration = config.web_app.token_duration;
models::LocalUser::get_user_by_creds(c, &auth_request.username, &auth_request.password)
let session = conn.run(move |c| {
let user_info = models::LocalUser::get_user_by_creds(
c,
&auth_request.username,
&auth_request.password
)?;
models::Session::new(c, &user_info, session_duration)
}).await?; }).await?;
let token = models::AuthClaims::new(&user_info, config.web_app.token_duration)
.encode(&config.web_app.secret)
.map_err(models::make_500)?;
Ok(Json(models::AuthTokenResponse { token })) Ok(Json(session))
} }
#[post("/users", data = "<user_request>")] #[post("/users", data = "<user_request>")]

View file

@ -1,6 +1,6 @@
table! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use crate::models::user::*; use crate::models::user::RoleMapping;
localuser (user_id) { localuser (user_id) {
user_id -> Text, user_id -> Text,
@ -10,6 +10,16 @@ table! {
} }
} }
table! {
use diesel::sql_types::*;
session (session_id) {
session_id -> Text,
user_id -> Text,
expires_at -> Timestamp,
}
}
table! { table! {
use diesel::sql_types::*; use diesel::sql_types::*;
@ -37,11 +47,13 @@ table! {
} }
joinable!(localuser -> user (user_id)); joinable!(localuser -> user (user_id));
joinable!(session -> user (user_id));
joinable!(user_zone -> user (user_id)); joinable!(user_zone -> user (user_id));
joinable!(user_zone -> zone (zone_id)); joinable!(user_zone -> zone (zone_id));
allow_tables_to_appear_in_same_query!( allow_tables_to_appear_in_same_query!(
localuser, localuser,
session,
user, user,
user_zone, user_zone,
zone, zone,