Implement Keycloak authentication to Admin API
This commit is contained in:
parent
b6b0628836
commit
25d2e1d036
7 changed files with 91 additions and 84 deletions
22
.env.example
22
.env.example
|
|
@ -8,9 +8,19 @@ BOSS_CDN_URL=https://boss.example.com
|
||||||
AES_KEY=KEY
|
AES_KEY=KEY
|
||||||
HMAC_KEY=KEY
|
HMAC_KEY=KEY
|
||||||
|
|
||||||
# Can be paired with an admin auth server for extra security,
|
# S3 Config for Boss File Storage
|
||||||
# if disabled auth will instead check to see if the sent token
|
S3_ENDPOINT=https://s3.example.com/
|
||||||
# matches ADMIN_AUTH_TOKEN
|
S3_BUCKET_NAME=boss-files
|
||||||
USE_ADMIN_AUTH=true
|
S3_ACCESS_KEY=key
|
||||||
ADMIN_AUTH_URL=http://admin.example.com # Only needed when USE_ADMIN_AUTH=true
|
S3_SECRET_KEY=key
|
||||||
ADMIN_AUTH_TOKEN=AUTH_TOKEN
|
|
||||||
|
# Authentication Config for Admin API
|
||||||
|
# Can either use token or Keycloak server
|
||||||
|
FORCE_KEYCLOAK=false
|
||||||
|
KEYCLOAK_URL=https://keycloak.example.com
|
||||||
|
KEYCLOAK_REALM=admin
|
||||||
|
KEYCLOAK_CLIENT_ID=id
|
||||||
|
KEYCLOAK_CLIENT_SECRET=secret
|
||||||
|
|
||||||
|
# Fallback when not using Keycloak, doesn't have to be set if using Keycloak only
|
||||||
|
AUTH_TOKEN=token
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,4 @@ aws-config = { version = "1.1", features = ["behavior-version-latest"] }
|
||||||
aws-sdk-s3 = "1.17"
|
aws-sdk-s3 = "1.17"
|
||||||
tokio = { version = "1.50.0", features = ["full"] }
|
tokio = { version = "1.50.0", features = ["full"] }
|
||||||
log = "0.4.29"
|
log = "0.4.29"
|
||||||
|
keycloak-helper = { git = "https://git.virintox.com/BloxerHD018/keycloak-helper.git" }
|
||||||
|
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
use std::env;
|
|
||||||
use rocket::{Request, http::Status, request::{FromRequest, Outcome}};
|
|
||||||
use reqwest::Client;
|
|
||||||
|
|
||||||
pub struct AdminAuth(pub String);
|
|
||||||
|
|
||||||
#[rocket::async_trait]
|
|
||||||
impl<'r> FromRequest<'r> for AdminAuth {
|
|
||||||
type Error = ();
|
|
||||||
|
|
||||||
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
|
||||||
let use_admin_server = env::var("USE_ADMIN_AUTH")
|
|
||||||
.map(|v| v == "true")
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if use_admin_server {
|
|
||||||
let auth = match req.headers().get_one("Authorization") {
|
|
||||||
Some(auth) => auth,
|
|
||||||
None => return Outcome::Error((Status::BadRequest, ())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let auth_url = match env::var("ADMIN_AUTH_URL") {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
|
||||||
};
|
|
||||||
let auth_token = match env::var("ADMIN_AUTH_TOKEN") {
|
|
||||||
Ok(url) => url,
|
|
||||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
|
||||||
};
|
|
||||||
|
|
||||||
let http = Client::new();
|
|
||||||
let response = http.get(format!("{}/api/v1/validate_token/boss", auth_url))
|
|
||||||
.header("Authorization", format!("InterServer {}", auth_token))
|
|
||||||
.header("Token", auth)
|
|
||||||
.send()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match response {
|
|
||||||
Ok(response) => {
|
|
||||||
match response.error_for_status() {
|
|
||||||
Ok(response) => match response.text().await {
|
|
||||||
Ok(text) => return Outcome::Success(AdminAuth(text)),
|
|
||||||
Err(_) => return Outcome::Error((Status::InternalServerError, ()))
|
|
||||||
},
|
|
||||||
Err(_) => {
|
|
||||||
return Outcome::Error((Status::Unauthorized, ()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Err(_) => return Outcome::Error((Status::InternalServerError, ())),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let local_token = match env::var("ADMIN_AUTH_TOKEN") {
|
|
||||||
Ok(token) => token,
|
|
||||||
Err(_) => return Outcome::Error((Status::InternalServerError, ()))
|
|
||||||
};
|
|
||||||
|
|
||||||
let token = match req.headers().get_one("Authorization") {
|
|
||||||
Some(auth) => match auth.strip_prefix("Bearer ") {
|
|
||||||
Some(token) => token,
|
|
||||||
None => return Outcome::Error((Status::BadRequest, ()))
|
|
||||||
},
|
|
||||||
None => return Outcome::Error((Status::BadRequest, ())),
|
|
||||||
};
|
|
||||||
|
|
||||||
if token != local_token.as_str() { return Outcome::Error((Status::Unauthorized, ())) }
|
|
||||||
|
|
||||||
Outcome::Success(AdminAuth("admin".to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -2,7 +2,7 @@ use serde::Deserialize;
|
||||||
use rocket::{State, http::Status};
|
use rocket::{State, http::Status};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use crate::Pool;
|
use crate::Pool;
|
||||||
use crate::admin_auth::AdminAuth;
|
use crate::auth::Auth;
|
||||||
use crate::models::task::Task;
|
use crate::models::task::Task;
|
||||||
use crate::models::task::is_valid_task_status;
|
use crate::models::task::is_valid_task_status;
|
||||||
|
|
||||||
|
|
@ -17,10 +17,10 @@ pub struct AddTaskData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[rocket::post("/api/v1/add_task", data = "<input>")]
|
#[rocket::post("/api/v1/add_task", data = "<input>")]
|
||||||
pub async fn add_task(pool: &State<Pool>, input: Json<AddTaskData>, auth: AdminAuth) -> Result<(), Status> {
|
pub async fn add_task(pool: &State<Pool>, input: Json<AddTaskData>, auth: Auth) -> Result<(), Status> {
|
||||||
let pool = pool.inner();
|
let pool = pool.inner();
|
||||||
let data = input.into_inner();
|
let data = input.into_inner();
|
||||||
let admin_username = auth.0;
|
let admin_username = auth.0.username;
|
||||||
|
|
||||||
let task_id_end = data.task_id
|
let task_id_end = data.task_id
|
||||||
.char_indices()
|
.char_indices()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use rocket::{State, http::Status};
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
use md5::{Md5, Digest};
|
use md5::{Md5, Digest};
|
||||||
use crate::Pool;
|
use crate::Pool;
|
||||||
use crate::admin_auth::AdminAuth;
|
use crate::auth::Auth;
|
||||||
use crate::models::file_wup::{is_valid_countries, is_valid_file_notify_conditions, is_valid_file_type, is_valid_languages};
|
use crate::models::file_wup::{is_valid_countries, is_valid_file_notify_conditions, is_valid_file_type, is_valid_languages};
|
||||||
use crate::{crypto::wiiu::encrypt_wiiu, models::file_wup::FileWUPAttributes};
|
use crate::{crypto::wiiu::encrypt_wiiu, models::file_wup::FileWUPAttributes};
|
||||||
use aws_sdk_s3::Client as S3Client;
|
use aws_sdk_s3::Client as S3Client;
|
||||||
|
|
@ -34,12 +34,12 @@ pub async fn upload_file_wup(
|
||||||
pool: &State<Pool>,
|
pool: &State<Pool>,
|
||||||
s3: &State<S3Client>,
|
s3: &State<S3Client>,
|
||||||
input: Json<UploadedWUP>,
|
input: Json<UploadedWUP>,
|
||||||
auth: AdminAuth
|
auth: Auth
|
||||||
) -> Result<(), Status> {
|
) -> Result<(), Status> {
|
||||||
let pool = pool.inner();
|
let pool = pool.inner();
|
||||||
let s3 = s3.inner();
|
let s3 = s3.inner();
|
||||||
let data = input.into_inner();
|
let data = input.into_inner();
|
||||||
let admin_username = auth.0;
|
let admin_username = auth.0.username;
|
||||||
|
|
||||||
info!("starting WUP upload for task: {} by user: {}", data.task_id, admin_username);
|
info!("starting WUP upload for task: {} by user: {}", data.task_id, admin_username);
|
||||||
|
|
||||||
|
|
|
||||||
67
src/auth.rs
Normal file
67
src/auth.rs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
use std::env;
|
||||||
|
use keycloak_helper::types::TokenValidationResponse;
|
||||||
|
use keycloak_helper::errors::TokenValidationError;
|
||||||
|
use keycloak_helper::validate::validate_token;
|
||||||
|
use rocket::{Request, http::Status, request::{FromRequest, Outcome}};
|
||||||
|
|
||||||
|
pub struct AuthResponse {
|
||||||
|
pub keycloak: bool,
|
||||||
|
pub username: String,
|
||||||
|
pub response: Option<TokenValidationResponse>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Auth(pub AuthResponse);
|
||||||
|
|
||||||
|
#[rocket::async_trait]
|
||||||
|
impl<'r> FromRequest<'r> for Auth {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
async fn from_request(req: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||||
|
let force_keycloak = env::var("FORCE_KEYCLOAK")
|
||||||
|
.map(|v| v == "true")
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
let token = match req.headers().get_one("Authorization") {
|
||||||
|
Some(auth) => match auth.strip_prefix("Bearer ") {
|
||||||
|
Some(token) => token.to_string(),
|
||||||
|
None => return Outcome::Error((Status::BadRequest, ()))
|
||||||
|
},
|
||||||
|
None => return Outcome::Error((Status::BadRequest, ())),
|
||||||
|
};
|
||||||
|
|
||||||
|
if !force_keycloak { // Check Local Token
|
||||||
|
let local_token = env::var("AUTH_TOKEN");
|
||||||
|
|
||||||
|
if let Ok(local_token) = local_token {
|
||||||
|
if token == local_token.as_str() {
|
||||||
|
return Outcome::Success(Auth(AuthResponse{
|
||||||
|
keycloak: false,
|
||||||
|
username: "admin".to_string(),
|
||||||
|
response: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate Token with Keycloak
|
||||||
|
match validate_token(&token, Some(vec!["boss", "admin"]), false).await {
|
||||||
|
Ok(res) => return Outcome::Success(Auth(AuthResponse{
|
||||||
|
keycloak: true,
|
||||||
|
username: res.username.clone(),
|
||||||
|
response: Some(res),
|
||||||
|
})),
|
||||||
|
Err(e) => match e {
|
||||||
|
TokenValidationError::MissingEnvs(envs) => {
|
||||||
|
println!("Missing Environment Variables: {envs}");
|
||||||
|
return Outcome::Error((Status::InternalServerError, ()))
|
||||||
|
},
|
||||||
|
TokenValidationError::RequestFailed(e) => {
|
||||||
|
println!("Request to Keycloak Failed: {e}");
|
||||||
|
return Outcome::Error((Status::InternalServerError, ()))
|
||||||
|
},
|
||||||
|
TokenValidationError::Inactive => return Outcome::Error((Status::Unauthorized, ())),
|
||||||
|
TokenValidationError::Forbidden => return Outcome::Error((Status::Unauthorized, ())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,7 @@ mod services;
|
||||||
mod database;
|
mod database;
|
||||||
mod api;
|
mod api;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
mod admin_auth;
|
mod auth;
|
||||||
|
|
||||||
type Pool = sqlx::Pool<sqlx::Postgres>;
|
type Pool = sqlx::Pool<sqlx::Postgres>;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue