This commit is contained in:
Eugene Pankov 2022-09-01 20:57:05 +02:00
parent 2c6dbd01ac
commit 4c8146273b
No known key found for this signature in database
GPG key ID: 5896FCBBDD1CF4F4
26 changed files with 93 additions and 90 deletions

View file

@ -146,7 +146,7 @@ impl DetailApi {
return Ok(UpdateRoleResponse::NotFound);
};
if &role.name == BUILTIN_ADMIN_ROLE_NAME {
if role.name == BUILTIN_ADMIN_ROLE_NAME {
return Ok(UpdateRoleResponse::Forbidden);
}
@ -175,7 +175,7 @@ impl DetailApi {
return Ok(DeleteRoleResponse::NotFound);
};
if &role.name == BUILTIN_ADMIN_ROLE_NAME {
if role.name == BUILTIN_ADMIN_ROLE_NAME {
return Ok(DeleteRoleResponse::Forbidden);
}

View file

@ -281,8 +281,8 @@ impl RolesApi {
let db = db.lock().await;
if !TargetRoleAssignment::Entity::find()
.filter(TargetRoleAssignment::Column::TargetId.eq(id.0.clone()))
.filter(TargetRoleAssignment::Column::RoleId.eq(role_id.0.clone()))
.filter(TargetRoleAssignment::Column::TargetId.eq(id.0))
.filter(TargetRoleAssignment::Column::RoleId.eq(role_id.0))
.all(&*db)
.await
.map_err(WarpgateError::from)?
@ -329,7 +329,7 @@ impl RolesApi {
return Ok(DeleteTargetRoleResponse::NotFound);
};
if &role.name == BUILTIN_ADMIN_ROLE_NAME && target.kind == TargetKind::WebAdmin {
if role.name == BUILTIN_ADMIN_ROLE_NAME && target.kind == TargetKind::WebAdmin {
return Ok(DeleteTargetRoleResponse::Forbidden);
}

View file

@ -262,8 +262,8 @@ impl RolesApi {
let db = db.lock().await;
if !UserRoleAssignment::Entity::find()
.filter(UserRoleAssignment::Column::UserId.eq(id.0.clone()))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0.clone()))
.filter(UserRoleAssignment::Column::UserId.eq(id.0))
.filter(UserRoleAssignment::Column::RoleId.eq(role_id.0))
.all(&*db)
.await
.map_err(WarpgateError::from)?

View file

@ -76,7 +76,7 @@ pub struct User {
pub id: Uuid,
pub username: String,
pub credentials: Vec<UserAuthCredential>,
#[serde(skip_serializing_if = "Option::is_none", rename="require")]
#[serde(skip_serializing_if = "Option::is_none", rename = "require")]
pub credential_policy: Option<UserRequireCredentialsPolicy>,
pub roles: Vec<String>,
}

View file

@ -23,6 +23,8 @@ pub enum WarpgateError {
ExternalHostNotSet,
#[error("URL contains no host")]
NoHostInUrl,
#[error("Inconsistent state error")]
InconsistentState,
}
impl ResponseError for WarpgateError {

View file

@ -229,7 +229,7 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
serialize_auth_state_inner(state_arc).await
@ -247,7 +247,7 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
@ -258,7 +258,7 @@ impl Api {
};
if let AuthResult::Accepted { .. } = auth_result {
services.auth_state_store.lock().await.complete(&*id).await;
services.auth_state_store.lock().await.complete(&id).await;
}
serialize_auth_state_inner(state_arc).await
}
@ -275,11 +275,11 @@ impl Api {
auth: Option<Data<&SessionAuthorization>>,
id: Path<Uuid>,
) -> poem::Result<AuthStateResponse> {
let Some(state_arc) = get_auth_state(&*id, *services, auth.map(|x|x.0)).await else {
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
return Ok(AuthStateResponse::NotFound);
};
state_arc.lock().await.reject();
services.auth_state_store.lock().await.complete(&*id).await;
services.auth_state_store.lock().await.complete(&id).await;
serialize_auth_state_inner(state_arc).await
}
}
@ -299,7 +299,7 @@ async fn get_auth_state(
return None;
};
let Some(state_arc) = store.get(&*id) else {
let Some(state_arc) = store.get(&id) else {
return None;
};

View file

@ -54,7 +54,7 @@ impl Api {
services: Data<&Services>,
) -> poem::Result<GetSsoProvidersResponse> {
let mut providers = services.config.lock().await.store.sso_providers.clone();
providers.sort_by(|a, b| a.label().cmp(&b.label()));
providers.sort_by(|a, b| a.label().cmp(b.label()));
Ok(GetSsoProvidersResponse::Ok(Json(
providers
.into_iter()
@ -130,12 +130,9 @@ impl Api {
state.add_valid_credential(cred);
}
match state.verify() {
AuthResult::Accepted { username } => {
auth_state_store.complete(state.id()).await;
authorize_session(req, username).await?;
}
_ => (),
if let AuthResult::Accepted { username } = state.verify() {
auth_state_store.complete(state.id()).await;
authorize_session(req, username).await?;
}
Ok(Response::new(ReturnToSsoResponse::Ok).header(

View file

@ -51,13 +51,13 @@ impl Api {
SessionAuthorization::Ticket { target_name, .. } => target_name == name,
SessionAuthorization::User(_) => {
let mut config_provider = services.config_provider.lock().await;
match config_provider
.authorize_target(auth.username(), &name)
.await
{
Ok(true) => true,
_ => false,
}
matches!(
config_provider
.authorize_target(auth.username(), &name)
.await,
Ok(true)
)
}
}
}

View file

@ -79,8 +79,7 @@ async fn get_target_for_request(
TargetOptions::Http(ref options) => Some((t, options)),
_ => None,
})
.filter(|(_, o)| o.external_host.as_deref() == Some(host))
.next()
.find(|(_, o)| o.external_host.as_deref() == Some(host))
.map(|(t, _)| t.name.clone())
} else {
None
@ -127,7 +126,7 @@ async fn get_target_for_request(
.config_provider
.lock()
.await
.authorize_target(&auth.username(), &target.0.name)
.authorize_target(auth.username(), &target.0.name)
.await?
{
return Ok(None);
@ -137,5 +136,5 @@ async fn get_target_for_request(
}
}
return Ok(None);
Ok(None)
}

View file

@ -53,7 +53,7 @@ impl SessionExt for Session {
}
fn get_username(&self) -> Option<String> {
return self.get_auth().map(|x| x.username().to_owned());
self.get_auth().map(|x| x.username().to_owned())
}
fn get_auth(&self) -> Option<SessionAuthorization> {
@ -91,7 +91,7 @@ impl SessionAuthorization {
}
async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Result<bool> {
let services: Data<&Services> = <_>::from_request_without_body(&req).await?;
let services: Data<&Services> = <_>::from_request_without_body(req).await?;
let SessionAuthorization::User(username) = auth else {
return Ok(false)
@ -102,7 +102,7 @@ async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Resu
for target in targets {
if matches!(target.options, TargetOptions::WebAdmin(_))
&& config_provider
.authorize_target(&username, &target.name)
.authorize_target(username, &target.name)
.await?
{
drop(config_provider);
@ -169,7 +169,7 @@ pub fn gateway_redirect(req: &Request) -> Response {
.original_uri()
.path_and_query()
.map(|p| p.to_string())
.unwrap_or("".into());
.unwrap_or_else(|| "".into());
let path = format!(
"/@warpgate#/login?next={}",
@ -184,20 +184,17 @@ pub async fn get_auth_state_for_request(
session: &Session,
store: &mut AuthStateStore,
) -> Result<Arc<Mutex<AuthState>>, WarpgateError> {
match session.get_auth_state_id() {
Some(id) => {
if !store.contains_key(&id.0) {
session.remove(AUTH_STATE_ID_SESSION_KEY)
}
if let Some(id) = session.get_auth_state_id() {
if !store.contains_key(&id.0) {
session.remove(AUTH_STATE_ID_SESSION_KEY)
}
None => (),
};
}
match session.get_auth_state_id() {
Some(id) => Ok(store.get(&id.0).unwrap()),
Some(id) => Ok(store.get(&id.0).ok_or(WarpgateError::InconsistentState)?),
None => {
let (id, state) = store
.create(&username, crate::common::PROTOCOL_NAME)
.create(username, crate::common::PROTOCOL_NAME)
.await?;
session.set(AUTH_STATE_ID_SESSION_KEY, AuthStateId(id));
Ok(state)
@ -207,13 +204,13 @@ pub async fn get_auth_state_for_request(
pub async fn authorize_session(req: &Request, username: String) -> poem::Result<()> {
let session_middleware: Data<&Arc<Mutex<SessionStore>>> =
<_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(&req).await?;
<_>::from_request_without_body(req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
let server_handle = session_middleware
.lock()
.await
.create_handle_for(&req)
.create_handle_for(req)
.await?;
server_handle
.lock()

View file

@ -2,7 +2,7 @@ use http::StatusCode;
use poem::IntoResponse;
pub fn error_page(e: poem::Error) -> impl IntoResponse {
return poem::web::Html(format!(
poem::web::Html(format!(
r#"<!DOCTYPE html>
<style>
body {{
@ -24,5 +24,5 @@ pub fn error_page(e: poem::Error) -> impl IntoResponse {
<p>{e}</p>
</main>
"#
)).with_status(StatusCode::BAD_GATEWAY);
)).with_status(StatusCode::BAD_GATEWAY)
}

View file

@ -186,7 +186,7 @@ impl ProtocolServer for HTTPProtocolServer {
crate::proxy::proxy_normal_request(&request, poem::Body::empty(), &options)
.await
.map_err(|e| {
return TargetTestError::ConnectionError(format!("{e}"));
TargetTestError::ConnectionError(format!("{e}"))
})?;
Ok(())
}

View file

@ -62,7 +62,9 @@ impl SomeRequestBuilder for http::request::Builder {
}
lazy_static::lazy_static! {
#[allow(clippy::mutable_key_type)]
static ref DONT_FORWARD_HEADERS: HashSet<HeaderName> = {
#[allow(clippy::mutable_key_type)]
let mut s = HashSet::new();
s.insert(http::header::ACCEPT_ENCODING);
s.insert(http::header::SEC_WEBSOCKET_EXTENSIONS);
@ -78,9 +80,9 @@ lazy_static::lazy_static! {
};
}
const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
static X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
static X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
static X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) -> Result<Uri> {
let target_uri = Uri::try_from(options.url.clone())?;
@ -90,7 +92,7 @@ fn construct_uri(req: &Request, options: &TargetHTTPOptions, websocket: bool) ->
.authority()
.context("No authority in the URL")?
.to_string();
let authority = authority.split("@").last().context("Authority is empty")?;
let authority = authority.split('@').last().context("Authority is empty")?;
let authority: Authority = authority.try_into()?;
let mut uri = http::uri::Builder::new()
.authority(authority)
@ -221,7 +223,7 @@ pub async fn proxy_normal_request(
body: Body,
options: &TargetHTTPOptions,
) -> poem::Result<Response> {
let uri = construct_uri(req, &options, false)?;
let uri = construct_uri(req, options, false)?;
tracing::debug!("URI: {:?}", uri);
@ -257,8 +259,8 @@ pub async fn proxy_normal_request(
let mut client_request = client.request(req.method().into(), uri.to_string());
client_request = copy_server_request(&req, client_request);
client_request = inject_forwarding_headers(&req, client_request)?;
client_request = copy_server_request(req, client_request);
client_request = inject_forwarding_headers(req, client_request)?;
client_request = rewrite_request(client_request, options)?;
client_request = client_request.body(reqwest::Body::wrap_stream(body.into_bytes_stream()));
client_request = client_request.header(
@ -273,7 +275,7 @@ pub async fn proxy_normal_request(
.execute(client_request)
.await
.map_err(|e| anyhow::anyhow!("Could not execute request: {e}"))?;
let status = client_response.status().clone();
let status = client_response.status();
let mut response: Response = "".into();
@ -313,7 +315,7 @@ async fn copy_client_body_and_embed(
r#"<script type="module" src="/@warpgate/{}"></script>"#,
script_manifest.file
);
for css_file in script_manifest.css.unwrap_or(vec![]) {
for css_file in script_manifest.css.unwrap_or_default() {
inject += &format!(
r#"<link rel="stylesheet" href="/@warpgate/{}" />"#,
css_file
@ -344,7 +346,7 @@ pub async fn proxy_websocket_request(
ws: WebSocket,
options: &TargetHTTPOptions,
) -> poem::Result<impl IntoResponse> {
let uri = construct_uri(req, &options, true)?;
let uri = construct_uri(req, options, true)?;
proxy_ws_inner(req, ws, uri.clone(), options)
.await
.map_err(|error| {
@ -375,8 +377,8 @@ async fn proxy_ws_inner(
.to_string(),
);
client_request = copy_server_request(&req, client_request);
client_request = inject_forwarding_headers(&req, client_request)?;
client_request = copy_server_request(req, client_request);
client_request = inject_forwarding_headers(req, client_request)?;
client_request = rewrite_request(client_request, options)?;
let (client, client_response) = connect_async_with_config(

View file

@ -97,16 +97,16 @@ impl SessionStore {
&mut self,
req: &Request,
) -> poem::Result<WarpgateServerHandleFromRequest> {
let session: &Session = <_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
if let Some(handle) = self.handle_for(session) {
return Ok(handle.into());
}
let services = Data::<&Services>::from_request_without_body(&req).await?;
let remote_address: &RemoteAddr = <_>::from_request_without_body(&req).await?;
let services = Data::<&Services>::from_request_without_body(req).await?;
let remote_address: &RemoteAddr = <_>::from_request_without_body(req).await?;
let session_storage =
Data::<&SharedSessionStorage>::from_request_without_body(&req).await?;
Data::<&SharedSessionStorage>::from_request_without_body(req).await?;
let (session_handle, mut session_handle_rx) = HttpSessionHandle::new();
@ -134,13 +134,12 @@ impl SessionStore {
tokio::spawn({
let session_storage = (*session_storage).clone();
let poem_session_id: Option<String> = session.get(POEM_SESSION_ID_SESSION_KEY);
let id = id.clone();
async move {
while let Some(command) = session_handle_rx.recv().await {
match command {
SessionHandleCommand::Close => {
if let Some(ref poem_session_id) = poem_session_id {
let _ = session_storage.remove_session(&poem_session_id).await;
let _ = session_storage.remove_session(poem_session_id).await;
}
info!(%id, "Removed HTTP session");
let mut that = this.lock().await;

View file

@ -10,7 +10,7 @@ use warpgate_core::{SessionHandle, WarpgateServerHandle};
use crate::session::SessionStore;
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SessionHandleCommand {
Close,
}
@ -47,7 +47,7 @@ impl std::ops::Deref for WarpgateServerHandleFromRequest {
impl<'a> FromRequest<'a> for WarpgateServerHandleFromRequest {
async fn from_request(req: &'a Request, _: &mut RequestBody) -> poem::Result<Self> {
let sm = Data::<&Arc<Mutex<SessionStore>>>::from_request_without_body(req).await?;
let session: &Session = <_>::from_request_without_body(&req).await?;
let session: &Session = <_>::from_request_without_body(req).await?;
Ok(sm
.lock()
.await

View file

@ -69,7 +69,7 @@ pub async fn run_server(services: Services, address: SocketAddr) -> Result<()> {
let handler = ServerHandler { id, event_tx };
let session = match ServerSession::new(
let session = match ServerSession::start(
remote_address,
&services,
server_handle,

View file

@ -6,8 +6,8 @@ use bytes::Bytes;
use tokio::sync::{broadcast, mpsc};
pub const ERASE_PROGRESS_SPINNER: &str = "\r \r";
pub const ERASE_PROGRESS_SPINNER_BUF: Bytes = Bytes::from_static(ERASE_PROGRESS_SPINNER.as_bytes());
pub const LINEBREAK: Bytes = Bytes::from_static("\n".as_bytes());
pub const ERASE_PROGRESS_SPINNER_BUF: &[u8] = ERASE_PROGRESS_SPINNER.as_bytes();
pub const LINEBREAK: &[u8] = "\n".as_bytes();
#[derive(Clone)]
pub struct ServiceOutput {
@ -62,8 +62,8 @@ impl ServiceOutput {
pub async fn hide_progress(&mut self) {
self.progress_visible
.store(false, std::sync::atomic::Ordering::Relaxed);
self.emit_output(ERASE_PROGRESS_SPINNER_BUF);
self.emit_output(LINEBREAK);
self.emit_output(Bytes::from_static(ERASE_PROGRESS_SPINNER_BUF));
self.emit_output(Bytes::from_static(LINEBREAK));
}
pub fn subscribe(&self) -> broadcast::Receiver<Bytes> {

View file

@ -102,7 +102,7 @@ impl std::fmt::Debug for ServerSession {
}
impl ServerSession {
pub async fn new(
pub async fn start(
remote_address: SocketAddr,
services: &Services,
server_handle: Arc<Mutex<WarpgateServerHandle>>,

View file

@ -28,6 +28,7 @@
"@otplib/preset-browser": "^12.0.1",
"@sveltejs/vite-plugin-svelte": "^1.0.0-next.49",
"@tsconfig/svelte": "^3.0.0",
"@types/qrcode": "^1.5.0",
"@types/shell-escape": "^0.2.1",
"@typescript-eslint/eslint-plugin": "^5.28.0",
"@typescript-eslint/parser": "^5.28.0",

View file

@ -1,7 +1,7 @@
<script lang="ts">
import { Input } from 'sveltestrap'
import type { User, UserRequireCredentialsPolicy } from './lib/api'
import type { CredentialKind, User, UserRequireCredentialsPolicy } from './lib/api'
export let user: User
export let value: UserRequireCredentialsPolicy
@ -39,7 +39,7 @@ function updateAny () {
function toggle (type: string) {
if (value[protocolId].includes(type)) {
value[protocolId] = value[protocolId].filter(x => x !== type)
value[protocolId] = value[protocolId].filter((x: CredentialKind) => x !== type)
} else {
value[protocolId].push(type)
}

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { api, UserSnapshot, Target, TicketAndSecret } from 'admin/lib/api'
import { api, User, Target, TicketAndSecret } from 'admin/lib/api'
import AsyncButton from 'common/AsyncButton.svelte'
import ConnectionInstructions from 'common/ConnectionInstructions.svelte'
import { TargetKind } from 'gateway/lib/api'
@ -9,9 +9,9 @@ import { firstBy } from 'thenby'
let error: Error|null = null
let targets: Target[]|undefined
let users: UserSnapshot[]|undefined
let users: User[]|undefined
let selectedTarget: Target|undefined
let selectedUser: UserSnapshot|undefined
let selectedUser: User|undefined
let result: TicketAndSecret|undefined
async function load () {

View file

@ -32,11 +32,11 @@ async function load () {
}
}
function deleteCredential (credential) {
function deleteCredential (credential: UserAuthCredential) {
user.credentials = user.credentials.filter(c => c !== credential)
}
function abbreviatePublicKey (key) {
function abbreviatePublicKey (key: string) {
return key.slice(0, 16) + '...' + key.slice(-8)
}

View file

@ -2,7 +2,7 @@
import { onMount } from 'svelte'
import { Alert, Button, FormGroup, Input, Modal, ModalBody, ModalFooter, ModalHeader } from 'sveltestrap'
import QRCode from 'qrcode'
import { KeyEncodings, TOTP, TOTPOptions } from '@otplib/core'
import { TOTP, TOTPOptions } from '@otplib/core'
import { createDigest } from '@otplib/plugin-crypto-js'
import { faRefresh } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
@ -60,9 +60,8 @@ $: {
const uri = totp.keyuri(username, 'Warpgate', base32Encode(new Uint8Array(credential.key), 'RFC4648'))
QRCode.toDataURL(uri, (err, imageUrl) => {
QRCode.toDataURL(uri, (err: Error, imageUrl: string) => {
if (err) {
console.log('Error with QR')
return
}
if (qrImage) {

View file

@ -2,7 +2,7 @@
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
import { Alert } from 'sveltestrap'
import Fa from 'svelte-fa'
import Router, { push } from 'svelte-spa-router'
import Router, { push, RouteDetail } from 'svelte-spa-router'
import { wrap } from 'svelte-spa-router/wrap'
import { get } from 'svelte/store'
import { api } from 'gateway/lib/api'
@ -29,7 +29,7 @@ function onPageResume () {
init()
}
async function requireLogin (detail) {
async function requireLogin (detail: RouteDetail) {
await serverInfoPromise
if (!get(serverInfo)?.username) {
let url = detail.location

View file

@ -290,6 +290,13 @@
resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6"
integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==
"@types/qrcode@^1.5.0":
version "1.5.0"
resolved "https://registry.yarnpkg.com/@types/qrcode/-/qrcode-1.5.0.tgz#6a98fe9a9a7b2a9a3167b6dde17eff999eabe40b"
integrity sha512-x5ilHXRxUPIMfjtM+1vf/GPTRWZ81nqscursm5gMznJeK9M0YnZ1c3bEvRLQ0zSSgedLx1J6MGL231ObQGGhaA==
dependencies:
"@types/node" "*"
"@types/sass@^1.16.0":
version "1.43.1"
resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68"

View file

@ -216,7 +216,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
.await?
.into_iter()
.next()
.ok_or(anyhow::anyhow!("Database inconsistent: no admin role"))?;
.ok_or_else(|| anyhow::anyhow!("Database inconsistent: no admin role"))?;
let admin_user = match User::Entity::find()
.filter(User::Column::Username.eq(BUILTIN_ADMIN_USERNAME))