Skip to content

Commit

Permalink
Orca: Add session api and other authorization related improvements. (…
Browse files Browse the repository at this point in the history
…#88)
  • Loading branch information
turboMaCk authored and ICTGuerrilla committed Sep 3, 2023
1 parent 148534c commit ef81663
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 43 deletions.
38 changes: 22 additions & 16 deletions orca/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ use validator::ValidationError;

mod applications;
mod registration;
mod session;
mod stats;

use crate::processing::SenderError;
use crate::server::keycloak;

#[derive(Debug)]
pub struct SqlError(sqlx::Error);
Expand Down Expand Up @@ -52,20 +54,25 @@ impl<'r> response::Responder<'r, 'static> for SenderError {
}
}

#[derive(Debug)]
pub struct JwtError(jsonwebtoken::errors::Error);

impl<'r> Responder<'r, 'static> for JwtError {
impl<'r> Responder<'r, 'static> for keycloak::Error {
fn respond_to(self, _request: &'r Request<'_>) -> response::Result<'static> {
use jsonwebtoken::errors::ErrorKind;
use keycloak::Error::*;
use rocket::http::Status;

let kind = self.0.kind();
info!("JWT verification failed with {:?}", self);

info!("JWT verification failed with {:?}", kind);
match kind {
ErrorKind::ExpiredSignature => Err(Status::Forbidden),
_ => Err(Status::Unauthorized),
match self {
Disabled => Err(Status::NotAcceptable),
BadKey(_) => Err(Status::InternalServerError),
MissingRole(_) => Err(Status::Forbidden),
BadToken(jwt_err) => {
use jsonwebtoken::errors::ErrorKind;

match jwt_err.kind() {
ErrorKind::ExpiredSignature => Err(Status::Unauthorized),
_ => Err(Status::Forbidden),
}
}
}
}
}
Expand All @@ -80,9 +87,7 @@ pub enum ApiError {
#[response(status = 500)]
QueueSender(SenderError),
#[response(status = 401)]
InvalidToken(JwtError),
#[response(status = 403)]
MissingRole(String),
InvalidToken(keycloak::Error),
#[response(status = 401)]
AuthorizationDisabled(()),
}
Expand Down Expand Up @@ -111,9 +116,9 @@ impl From<SenderError> for ApiError {
}
}

impl From<jsonwebtoken::errors::Error> for ApiError {
fn from(err: jsonwebtoken::errors::Error) -> Self {
Self::InvalidToken(JwtError(err))
impl From<keycloak::Error> for ApiError {
fn from(err: keycloak::Error) -> Self {
Self::InvalidToken(err)
}
}

Expand All @@ -127,6 +132,7 @@ pub fn build() -> Rocket<Build> {
.mount("/", routes![status_api])
.mount("/registration", registration::routes())
.mount("/applications", applications::routes())
.mount("/session", session::routes())
.mount("/stats", stats::routes())
.register(
"/registration",
Expand Down
37 changes: 37 additions & 0 deletions orca/src/api/session/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use rocket::{Route, State};

use super::Response;
use crate::db::DbPool;
use crate::server::keycloak::{JwtClaims, JwtToken, Keycloak};
use jsonwebtoken::TokenData;
use rocket::serde::{json::Json, Serialize};

#[derive(Debug, Serialize)]
struct SessionInfo {
token_claims: JwtClaims,
}

impl SessionInfo {
fn new(token: TokenData<JwtClaims>) -> Self {
Self {
token_claims: token.claims,
}
}
}

#[get("/current", format = "json")]
async fn current<'r>(
_db_pool: &State<DbPool>,
keycloak: &State<Keycloak>,
token: JwtToken<'r>,
) -> Response<Json<SessionInfo>> {
let token_data = keycloak.inner().decode_jwt(token)?;

let session_info = SessionInfo::new(token_data);

Ok(Json(session_info))
}

pub fn routes() -> Vec<Route> {
routes![current]
}
6 changes: 2 additions & 4 deletions orca/src/api/stats/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
use rocket::serde::json::Json;
use rocket::{serde::Serialize, Responder, Route, State};

use rocket::http::{ContentType, Header};
use rocket::{serde::Serialize, Route, State};

use super::Response;
use crate::db::DbPool;
Expand All @@ -24,7 +22,7 @@ async fn basic_stats<'r>(
token: JwtToken<'r>,
) -> Response<Json<BasicStats>> {
// Every authenticated user is able to see stats
let _ = keycloak.inner().decode_jwt(token);
keycloak.inner().decode_jwt(token)?;

let (unverified,) = query::count_unverified().fetch_one(db_pool.inner()).await?;

Expand Down
2 changes: 1 addition & 1 deletion orca/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
//! cargo build --features proxy-support
//! ```

use server::keycloak::{self, Keycloak};
use server::keycloak::Keycloak;
extern crate rand;
#[macro_use]
extern crate rocket;
Expand Down
37 changes: 15 additions & 22 deletions orca/src/server/keycloak.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ use rocket::request::{FromRequest, Outcome, Request};
use rocket::serde::Deserialize;
use rocket::serde::Serialize;

use crate::api::ApiError;

use super::jwk;

struct ConnectedKeycloak {
Expand Down Expand Up @@ -42,16 +40,12 @@ impl ConnectedKeycloak {
jsonwebtoken::decode::<JwtClaims>(token.0, &self.key, &self.validation)
}

pub fn require_role(
&self,
token: JwtToken,
role: &str,
) -> Result<TokenData<JwtClaims>, ApiError> {
pub fn require_role(&self, token: JwtToken, role: &str) -> Result<TokenData<JwtClaims>, Error> {
let token_data = self.decode_jwt(token)?;
if self.has_role(&token_data.claims, role) {
Ok(token_data)
} else {
Err(ApiError::MissingRole(role.to_string()))
Err(Error::MissingRole(role.to_string()))
}
}

Expand All @@ -63,40 +57,38 @@ impl ConnectedKeycloak {
}
}

pub enum Keycloak {
pub enum KeycloakState {
Connected(Box<ConnectedKeycloak>),
Disconnected,
}

pub struct Keycloak(KeycloakState);

impl Keycloak {
pub async fn fetch(host: &str, realm: &str, client_id: String) -> Result<Keycloak, Error> {
let k = ConnectedKeycloak::fetch(host, realm, client_id).await?;
Ok(Self::Connected(Box::new(k)))
Ok(Keycloak(KeycloakState::Connected(Box::new(k))))
}

pub fn disable() -> Self {
warn!("Keycloak authorization not configured. Authorization disabled");
Self::Disconnected
Keycloak(KeycloakState::Disconnected)
}

pub fn decode_jwt(&self, token: JwtToken) -> Result<TokenData<JwtClaims>, Error> {
match self {
Self::Connected(k) => {
match &self.0 {
KeycloakState::Connected(k) => {
let res = k.decode_jwt(token)?;
Ok(res)
}
Self::Disconnected => Err(Error::Disabled),
KeycloakState::Disconnected => Err(Error::Disabled),
}
}

pub fn require_role(
&self,
token: JwtToken,
role: &str,
) -> Result<TokenData<JwtClaims>, ApiError> {
match self {
Self::Connected(k) => k.require_role(token, role),
Self::Disconnected => Err(ApiError::AuthorizationDisabled(())),
pub fn require_role(&self, token: JwtToken, role: &str) -> Result<TokenData<JwtClaims>, Error> {
match &self.0 {
KeycloakState::Connected(k) => k.require_role(token, role),
KeycloakState::Disconnected => Err(Error::Disabled),
}
}
}
Expand All @@ -105,6 +97,7 @@ impl Keycloak {
pub enum Error {
BadKey(jwk::Error),
BadToken(jsonwebtoken::errors::Error),
MissingRole(String),
Disabled,
}

Expand Down

0 comments on commit ef81663

Please sign in to comment.