mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
API tokens
This commit is contained in:
parent
8087179ea0
commit
f2faa84e1e
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4903,6 +4903,7 @@ dependencies = [
|
|||
"poem-openapi",
|
||||
"regex",
|
||||
"reqwest",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tokio",
|
||||
|
|
6
justfile
6
justfile
|
@ -6,13 +6,13 @@ run *ARGS:
|
|||
RUST_BACKTRACE=1 RUST_LOG=warpgate cargo run --features {{features}} -- --config config.yaml {{ARGS}}
|
||||
|
||||
fmt:
|
||||
for p in {{projects}}; do cargo fmt --features {{features}} -p $p -v; done
|
||||
for p in {{projects}}; do cargo fmt -p $p -v; done
|
||||
|
||||
fix *ARGS:
|
||||
for p in {{projects}}; do cargo fix --features {{features}} -p $p {{ARGS}}; done
|
||||
for p in {{projects}}; do cargo fix --all-features -p $p {{ARGS}}; done
|
||||
|
||||
clippy *ARGS:
|
||||
for p in {{projects}}; do cargo cranky --features {{features}} -p $p {{ARGS}}; done
|
||||
for p in {{projects}}; do cargo cranky --all-features -p $p {{ARGS}}; done
|
||||
|
||||
test:
|
||||
for p in {{projects}}; do cargo test --features {{features}} -p $p; done
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use poem_openapi::OpenApi;
|
||||
use poem_openapi::auth::Bearer;
|
||||
use poem_openapi::{OpenApi, SecurityScheme};
|
||||
|
||||
mod known_hosts_detail;
|
||||
mod known_hosts_list;
|
||||
|
@ -14,6 +15,23 @@ mod tickets_detail;
|
|||
mod tickets_list;
|
||||
mod users;
|
||||
|
||||
#[derive(SecurityScheme)]
|
||||
#[oai(type = "bearer")]
|
||||
pub(crate) struct TokenAuth(Bearer);
|
||||
|
||||
pub struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/__", method = "get", operation_id = "_ignore_me")]
|
||||
async fn _hidden(
|
||||
&self,
|
||||
_auth: TokenAuth, // only needed once for the security schema to be included in the spec
|
||||
) -> poem::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> impl OpenApi {
|
||||
(
|
||||
sessions_list::Api,
|
||||
|
@ -29,5 +47,6 @@ pub fn get() -> impl OpenApi {
|
|||
known_hosts_detail::Api,
|
||||
ssh_keys::Api,
|
||||
logs::Api,
|
||||
Api,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -54,10 +54,7 @@ impl Api {
|
|||
.all(&*db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
let tickets = tickets
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect::<Vec<Ticket::Model>>();
|
||||
|
||||
Ok(GetTicketsResponse::Ok(Json(tickets)))
|
||||
}
|
||||
|
||||
|
|
|
@ -14,7 +14,6 @@ pub fn admin_api_app(services: &Services) -> impl IntoEndpoint {
|
|||
|
||||
let ui = api_service.swagger_ui();
|
||||
let spec = api_service.spec_endpoint();
|
||||
let db = services.db.clone();
|
||||
let config = services.config.clone();
|
||||
let config_provider = services.config_provider.clone();
|
||||
let recordings = services.recordings.clone();
|
||||
|
@ -40,7 +39,6 @@ pub fn admin_api_app(services: &Services) -> impl IntoEndpoint {
|
|||
"/sessions/changes",
|
||||
crate::api::sessions_list::api_get_sessions_changes_stream,
|
||||
)
|
||||
.data(db)
|
||||
.data(config_provider)
|
||||
.data(state)
|
||||
.data(recordings)
|
||||
|
|
37
warpgate-db-entities/src/Token.rs
Normal file
37
warpgate-db-entities/src/Token.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use poem_openapi::Object;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)]
|
||||
#[sea_orm(table_name = "tokens")]
|
||||
#[oai(rename = "Token")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
#[oai(skip)]
|
||||
pub secret: String,
|
||||
pub user_id: Uuid,
|
||||
pub expiry: Option<DateTime<Utc>>,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
impl Related<super::User::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "super::User::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "super::User::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
|
@ -8,5 +8,6 @@ pub mod Session;
|
|||
pub mod Target;
|
||||
pub mod TargetRoleAssignment;
|
||||
pub mod Ticket;
|
||||
pub mod Token;
|
||||
pub mod User;
|
||||
pub mod UserRoleAssignment;
|
||||
|
|
|
@ -20,3 +20,8 @@ sea-orm = { version = "^0.9", features = [
|
|||
sea-orm-migration = { version = "^0.9", default-features = false }
|
||||
uuid = { version = "1.0", features = ["v4", "serde"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
[features]
|
||||
postgres = ["sea-orm/sqlx-postgres"]
|
||||
mysql = ["sea-orm/sqlx-mysql"]
|
||||
sqlite = ["sea-orm/sqlx-sqlite"]
|
||||
|
|
|
@ -10,6 +10,7 @@ mod m00005_create_log_entry;
|
|||
mod m00006_add_session_protocol;
|
||||
mod m00007_targets_and_roles;
|
||||
mod m00008_users;
|
||||
mod m00009_tokens;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
@ -25,6 +26,7 @@ impl MigratorTrait for Migrator {
|
|||
Box::new(m00006_add_session_protocol::Migration),
|
||||
Box::new(m00007_targets_and_roles::Migration),
|
||||
Box::new(m00008_users::Migration),
|
||||
Box::new(m00009_tokens::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use sea_orm::Schema;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod user {
|
||||
pub(crate) mod user {
|
||||
use sea_orm::entity::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
|
|
59
warpgate-db-migrations/src/m00009_tokens.rs
Normal file
59
warpgate-db-migrations/src/m00009_tokens.rs
Normal file
|
@ -0,0 +1,59 @@
|
|||
use sea_orm::Schema;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
mod token {
|
||||
use chrono::{DateTime, Utc};
|
||||
use sea_orm::entity::prelude::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
|
||||
#[sea_orm(table_name = "tokens")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key, auto_increment = false)]
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub secret: String,
|
||||
pub user_id: Uuid,
|
||||
pub expiry: Option<DateTime<Utc>>,
|
||||
pub created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(
|
||||
belongs_to = "crate::m00008_users::user::Entity",
|
||||
from = "Column::UserId",
|
||||
to = "crate::m00008_users::user::Column::Id"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
}
|
||||
|
||||
pub struct Migration;
|
||||
|
||||
impl MigrationName for Migration {
|
||||
fn name(&self) -> &str {
|
||||
"m00009_tokens"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
let builder = manager.get_database_backend();
|
||||
let schema = Schema::new(builder);
|
||||
manager
|
||||
.create_table(schema.create_table_from_entity(token::Entity))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(token::Entity).to_owned())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -25,6 +25,10 @@ poem = { version = "^1.3.42", features = [
|
|||
] }
|
||||
poem-openapi = { version = "^2.0.10", features = ["swagger-ui"] }
|
||||
reqwest = { version = "0.11", features = ["rustls-tls-native-roots", "stream"] }
|
||||
sea-orm = { version = "^0.9", features = [
|
||||
"runtime-tokio-native-tls",
|
||||
"macros",
|
||||
], default-features = false }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.20", features = ["tracing", "signal"] }
|
||||
|
|
|
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||
|
||||
use poem::session::Session;
|
||||
use poem::web::Data;
|
||||
use poem::Request;
|
||||
use poem::{FromRequest, Request};
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Enum, Object, OpenApi};
|
||||
|
@ -14,7 +14,8 @@ use warpgate_common::{Secret, WarpgateError};
|
|||
use warpgate_core::Services;
|
||||
|
||||
use crate::common::{
|
||||
authorize_session, endpoint_auth, get_auth_state_for_request, SessionAuthorization, SessionExt,
|
||||
authorize_session, endpoint_auth, get_auth_state_for_request, RequestAuthorization,
|
||||
SessionAuthorization, SessionExt,
|
||||
};
|
||||
use crate::session::SessionStore;
|
||||
|
||||
|
@ -146,7 +147,7 @@ impl Api {
|
|||
}
|
||||
}
|
||||
|
||||
#[oai(path = "/auth/otp", method = "post", operation_id = "otpLogin")]
|
||||
#[oai(path = "/auth/otp", method = "post", operation_id = "otp_login")]
|
||||
async fn api_auth_otp_login(
|
||||
&self,
|
||||
req: &Request,
|
||||
|
@ -200,7 +201,7 @@ impl Api {
|
|||
#[oai(
|
||||
path = "/auth/state",
|
||||
method = "get",
|
||||
operation_id = "getDefaultAuthState"
|
||||
operation_id = "get_default_auth_state"
|
||||
)]
|
||||
async fn api_default_auth_state(
|
||||
&self,
|
||||
|
@ -220,7 +221,7 @@ impl Api {
|
|||
#[oai(
|
||||
path = "/auth/state",
|
||||
method = "delete",
|
||||
operation_id = "cancelDefaultAuth"
|
||||
operation_id = "cancel_default_auth"
|
||||
)]
|
||||
async fn api_cancel_default_auth(
|
||||
&self,
|
||||
|
@ -249,10 +250,11 @@ impl Api {
|
|||
async fn api_auth_state(
|
||||
&self,
|
||||
services: Data<&Services>,
|
||||
auth: Option<Data<&SessionAuthorization>>,
|
||||
req: &Request,
|
||||
id: Path<Uuid>,
|
||||
) -> poem::Result<AuthStateResponse> {
|
||||
let state_arc = get_auth_state(&id, &services, auth.map(|x| x.0)).await;
|
||||
let auth: Option<RequestAuthorization> = <_>::from_request_without_body(req).await.ok();
|
||||
let state_arc = get_auth_state(&id, &services, auth.as_ref()).await;
|
||||
let Some(state_arc) = state_arc else {
|
||||
return Ok(AuthStateResponse::NotFound);
|
||||
};
|
||||
|
@ -268,10 +270,11 @@ impl Api {
|
|||
async fn api_approve_auth(
|
||||
&self,
|
||||
services: Data<&Services>,
|
||||
auth: Option<Data<&SessionAuthorization>>,
|
||||
req: &Request,
|
||||
id: Path<Uuid>,
|
||||
) -> poem::Result<AuthStateResponse> {
|
||||
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
|
||||
let auth: Option<RequestAuthorization> = <_>::from_request_without_body(req).await.ok();
|
||||
let Some(state_arc) = get_auth_state(&id, &services, auth.as_ref()).await else {
|
||||
return Ok(AuthStateResponse::NotFound);
|
||||
};
|
||||
|
||||
|
@ -296,10 +299,11 @@ impl Api {
|
|||
async fn api_reject_auth(
|
||||
&self,
|
||||
services: Data<&Services>,
|
||||
auth: Option<Data<&SessionAuthorization>>,
|
||||
req: &Request,
|
||||
id: Path<Uuid>,
|
||||
) -> poem::Result<AuthStateResponse> {
|
||||
let Some(state_arc) = get_auth_state(&id, &services, auth.map(|x|x.0)).await else {
|
||||
let auth: Option<RequestAuthorization> = <_>::from_request_without_body(req).await.ok();
|
||||
let Some(state_arc) = get_auth_state(&id, &services, auth.as_ref()).await else {
|
||||
return Ok(AuthStateResponse::NotFound);
|
||||
};
|
||||
state_arc.lock().await.reject();
|
||||
|
@ -311,7 +315,7 @@ impl Api {
|
|||
async fn get_auth_state(
|
||||
id: &Uuid,
|
||||
services: &Services,
|
||||
auth: Option<&SessionAuthorization>,
|
||||
auth: Option<&RequestAuthorization>,
|
||||
) -> Option<Arc<Mutex<AuthState>>> {
|
||||
let store = services.auth_state_store.lock().await;
|
||||
|
||||
|
@ -319,7 +323,7 @@ async fn get_auth_state(
|
|||
return None;
|
||||
};
|
||||
|
||||
let SessionAuthorization::User(username) = auth else {
|
||||
let RequestAuthorization::Session(SessionAuthorization::User(username)) = auth else {
|
||||
return None;
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ use poem_openapi::{ApiResponse, Object, OpenApi};
|
|||
use serde::Serialize;
|
||||
use warpgate_core::Services;
|
||||
|
||||
use crate::common::{SessionAuthorization, SessionExt};
|
||||
use crate::common::{RequestAuthorization, SessionAuthorization, SessionExt};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
|
@ -41,6 +41,7 @@ impl Api {
|
|||
req: &Request,
|
||||
session: &Session,
|
||||
services: Data<&Services>,
|
||||
auth: Option<RequestAuthorization>,
|
||||
) -> poem::Result<InstanceInfoResponse> {
|
||||
let config = services.config.lock().await;
|
||||
let external_host = config
|
||||
|
@ -51,14 +52,16 @@ impl Api {
|
|||
.or_else(|| req.original_uri().host());
|
||||
Ok(InstanceInfoResponse::Ok(Json(Info {
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
username: session.get_username(),
|
||||
username: auth.as_ref().map(|a| a.username().to_owned()),
|
||||
selected_target: session.get_target_name(),
|
||||
external_host: external_host.map(str::to_string),
|
||||
authorized_via_ticket: matches!(
|
||||
session.get_auth(),
|
||||
Some(SessionAuthorization::Ticket { .. })
|
||||
auth,
|
||||
Some(RequestAuthorization::Session(
|
||||
SessionAuthorization::Ticket { .. }
|
||||
))
|
||||
),
|
||||
ports: if session.is_authenticated() {
|
||||
ports: if auth.is_some() {
|
||||
PortsInfo {
|
||||
ssh: if config.store.ssh.enable {
|
||||
Some(config.store.ssh.listen.port())
|
||||
|
|
|
@ -1,10 +1,30 @@
|
|||
use poem_openapi::OpenApi;
|
||||
use poem_openapi::auth::Bearer;
|
||||
use poem_openapi::{OpenApi, SecurityScheme};
|
||||
|
||||
pub mod auth;
|
||||
pub mod info;
|
||||
pub mod sso_provider_detail;
|
||||
pub mod sso_provider_list;
|
||||
pub mod targets_list;
|
||||
mod auth;
|
||||
mod info;
|
||||
mod sso_provider_detail;
|
||||
mod sso_provider_list;
|
||||
mod targets_list;
|
||||
mod tokens_detail;
|
||||
mod tokens_list;
|
||||
|
||||
#[derive(SecurityScheme)]
|
||||
#[oai(type = "bearer")]
|
||||
pub(crate) struct TokenAuth(Bearer);
|
||||
|
||||
pub struct Api;
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/__", method = "get", operation_id = "_ignore_me")]
|
||||
async fn _hidden(
|
||||
&self,
|
||||
_auth: TokenAuth, // only needed once for the security schema to be included in the spec
|
||||
) -> poem::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get() -> impl OpenApi {
|
||||
(
|
||||
|
@ -13,5 +33,8 @@ pub fn get() -> impl OpenApi {
|
|||
targets_list::Api,
|
||||
sso_provider_list::Api,
|
||||
sso_provider_detail::Api,
|
||||
tokens_list::Api,
|
||||
tokens_detail::Api,
|
||||
Api,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ use warpgate_common::TargetOptions;
|
|||
use warpgate_core::Services;
|
||||
use warpgate_db_entities::Target;
|
||||
|
||||
use crate::common::{endpoint_auth, SessionAuthorization};
|
||||
use crate::common::{endpoint_auth, RequestAuthorization, SessionAuthorization};
|
||||
|
||||
pub struct Api;
|
||||
|
||||
|
@ -35,7 +35,7 @@ impl Api {
|
|||
async fn api_get_all_targets(
|
||||
&self,
|
||||
services: Data<&Services>,
|
||||
auth: Data<&SessionAuthorization>,
|
||||
auth: RequestAuthorization,
|
||||
) -> poem::Result<GetTargetsResponse> {
|
||||
let targets = {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
|
@ -48,8 +48,11 @@ impl Api {
|
|||
let name = t.name.clone();
|
||||
async move {
|
||||
match auth {
|
||||
SessionAuthorization::Ticket { target_name, .. } => target_name == name,
|
||||
SessionAuthorization::User(_) => {
|
||||
RequestAuthorization::Session(SessionAuthorization::Ticket {
|
||||
target_name,
|
||||
..
|
||||
}) => target_name == name,
|
||||
_ => {
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
|
||||
matches!(
|
||||
|
|
55
warpgate-protocol-http/src/api/tokens_detail.rs
Normal file
55
warpgate-protocol-http/src/api/tokens_detail.rs
Normal file
|
@ -0,0 +1,55 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use poem::web::Data;
|
||||
use poem::Request;
|
||||
use poem_openapi::param::Path;
|
||||
use poem_openapi::{ApiResponse, OpenApi};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::api::tokens_list::get_user;
|
||||
|
||||
pub struct Api;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum DeleteTokenResponse {
|
||||
#[oai(status = 204)]
|
||||
Deleted,
|
||||
|
||||
#[oai(status = 404)]
|
||||
NotFound,
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/tokens/:id", method = "delete", operation_id = "delete_token")]
|
||||
async fn api_delete_token(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
req: &Request,
|
||||
id: Path<Uuid>,
|
||||
) -> poem::Result<DeleteTokenResponse> {
|
||||
use warpgate_db_entities::Token;
|
||||
let db = db.lock().await;
|
||||
|
||||
let user = get_user(&*db, req).await?;
|
||||
let token = Token::Entity::find()
|
||||
.filter(Token::Column::UserId.eq(user.id))
|
||||
.filter(Token::Column::Id.eq(id.0))
|
||||
.one(&*db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
|
||||
match token {
|
||||
Some(token) => {
|
||||
token
|
||||
.delete(&*db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
Ok(DeleteTokenResponse::Deleted)
|
||||
}
|
||||
None => Ok(DeleteTokenResponse::NotFound),
|
||||
}
|
||||
}
|
||||
}
|
106
warpgate-protocol-http/src/api/tokens_list.rs
Normal file
106
warpgate-protocol-http/src/api/tokens_list.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Context;
|
||||
use http::StatusCode;
|
||||
use poem::web::Data;
|
||||
use poem::{FromRequest, Request};
|
||||
use poem_openapi::payload::Json;
|
||||
use poem_openapi::{ApiResponse, Object, OpenApi};
|
||||
use sea_orm::ActiveValue::Set;
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||
use tokio::sync::Mutex;
|
||||
use uuid::Uuid;
|
||||
use warpgate_common::helpers::hash::generate_ticket_secret;
|
||||
use warpgate_db_entities::{Token, User};
|
||||
|
||||
use crate::common::RequestAuthorization;
|
||||
|
||||
pub struct Api;
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum GetTokensResponse {
|
||||
#[oai(status = 200)]
|
||||
Ok(Json<Vec<Token::Model>>),
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct TokenAndSecret {
|
||||
token: Token::Model,
|
||||
secret: String,
|
||||
}
|
||||
|
||||
#[derive(Object)]
|
||||
struct CreateTokenRequest {
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[derive(ApiResponse)]
|
||||
enum CreateTokenResponse {
|
||||
#[oai(status = 201)]
|
||||
Created(Json<TokenAndSecret>),
|
||||
}
|
||||
|
||||
pub(crate) async fn get_user(db: &DatabaseConnection, req: &Request) -> poem::Result<User::Model> {
|
||||
let auth: Option<RequestAuthorization> = <_>::from_request_without_body(req).await.ok();
|
||||
if let Some(username) = auth.map(|a| a.username().to_owned()) {
|
||||
Ok(User::Entity::find()
|
||||
.filter(User::Column::Username.eq(username))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?
|
||||
.ok_or(anyhow::anyhow!("User not found"))?)
|
||||
} else {
|
||||
Err(poem::error::Error::from_status(StatusCode::UNAUTHORIZED))
|
||||
}
|
||||
}
|
||||
|
||||
#[OpenApi]
|
||||
impl Api {
|
||||
#[oai(path = "/tokens", method = "get", operation_id = "get_tokens")]
|
||||
async fn api_get_all_tokens(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
req: &Request,
|
||||
) -> poem::Result<GetTokensResponse> {
|
||||
use warpgate_db_entities::Token;
|
||||
|
||||
let db = db.lock().await;
|
||||
let user = get_user(&*db, req).await?;
|
||||
let tokens = Token::Entity::find()
|
||||
.filter(Token::Column::UserId.eq(user.id))
|
||||
.all(&*db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
|
||||
Ok(GetTokensResponse::Ok(Json(tokens)))
|
||||
}
|
||||
|
||||
#[oai(path = "/tokens", method = "post", operation_id = "create_token")]
|
||||
async fn api_create_token(
|
||||
&self,
|
||||
db: Data<&Arc<Mutex<DatabaseConnection>>>,
|
||||
body: Json<CreateTokenRequest>,
|
||||
req: &Request,
|
||||
) -> poem::Result<CreateTokenResponse> {
|
||||
use warpgate_db_entities::Token;
|
||||
|
||||
let db = db.lock().await;
|
||||
let user = get_user(&*db, req).await?;
|
||||
let secret = generate_ticket_secret();
|
||||
let values = Token::ActiveModel {
|
||||
id: Set(Uuid::new_v4()),
|
||||
name: Set(body.name.clone()),
|
||||
secret: Set(secret.expose_secret().to_string()),
|
||||
created: Set(chrono::Utc::now()),
|
||||
user_id: Set(user.id),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let token = values.insert(&*db).await.context("Error saving token")?;
|
||||
|
||||
Ok(CreateTokenResponse::Created(Json(TokenAndSecret {
|
||||
secret: secret.expose_secret().to_string(),
|
||||
token,
|
||||
})))
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ use tracing::*;
|
|||
use warpgate_common::{Target, TargetHTTPOptions, TargetOptions};
|
||||
use warpgate_core::{Services, WarpgateServerHandle};
|
||||
|
||||
use crate::common::{SessionAuthorization, SessionExt};
|
||||
use crate::common::{RequestAuthorization, SessionAuthorization, SessionExt};
|
||||
use crate::proxy::{proxy_normal_request, proxy_websocket_request};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -63,7 +63,7 @@ async fn get_target_for_request(
|
|||
) -> poem::Result<Option<(Target, TargetHTTPOptions)>> {
|
||||
let session: &Session = <_>::from_request_without_body(req).await?;
|
||||
let params: QueryParams = req.params()?;
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(req).await?;
|
||||
let auth: RequestAuthorization = <_>::from_request_without_body(req).await?;
|
||||
|
||||
let selected_target_name;
|
||||
let need_role_auth;
|
||||
|
@ -86,12 +86,15 @@ async fn get_target_for_request(
|
|||
None
|
||||
};
|
||||
|
||||
match *auth {
|
||||
SessionAuthorization::Ticket { target_name, .. } => {
|
||||
match auth {
|
||||
RequestAuthorization::Session(SessionAuthorization::Ticket {
|
||||
ref target_name, ..
|
||||
}) => {
|
||||
selected_target_name = Some(target_name.clone());
|
||||
need_role_auth = false;
|
||||
}
|
||||
SessionAuthorization::User(_) => {
|
||||
RequestAuthorization::Token { .. }
|
||||
| RequestAuthorization::Session(SessionAuthorization::User(_)) => {
|
||||
need_role_auth = true;
|
||||
|
||||
selected_target_name =
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
use std::any::type_name;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use http::StatusCode;
|
||||
use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use poem::error::GetDataError;
|
||||
use poem::session::Session;
|
||||
use poem::web::{Data, Redirect};
|
||||
use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse, Request, Response};
|
||||
use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse, Request, RequestBody, Response};
|
||||
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::*;
|
||||
|
@ -13,6 +16,7 @@ use uuid::Uuid;
|
|||
use warpgate_common::auth::AuthState;
|
||||
use warpgate_common::{ProtocolName, TargetOptions, WarpgateError};
|
||||
use warpgate_core::{AuthStateStore, Services};
|
||||
use warpgate_db_entities::{Token, User};
|
||||
|
||||
use crate::session::SessionStore;
|
||||
|
||||
|
@ -28,8 +32,6 @@ pub trait SessionExt {
|
|||
fn has_selected_target(&self) -> bool;
|
||||
fn get_target_name(&self) -> Option<String>;
|
||||
fn set_target_name(&self, target_name: String);
|
||||
fn is_authenticated(&self) -> bool;
|
||||
fn get_username(&self) -> Option<String>;
|
||||
fn get_auth(&self) -> Option<SessionAuthorization>;
|
||||
fn set_auth(&self, auth: SessionAuthorization);
|
||||
fn get_auth_state_id(&self) -> Option<AuthStateId>;
|
||||
|
@ -49,14 +51,6 @@ impl SessionExt for Session {
|
|||
self.set(TARGET_SESSION_KEY, target_name);
|
||||
}
|
||||
|
||||
fn is_authenticated(&self) -> bool {
|
||||
self.get_username().is_some()
|
||||
}
|
||||
|
||||
fn get_username(&self) -> Option<String> {
|
||||
self.get_auth().map(|x| x.username().to_owned())
|
||||
}
|
||||
|
||||
fn get_auth(&self) -> Option<SessionAuthorization> {
|
||||
self.get(AUTH_SESSION_KEY)
|
||||
}
|
||||
|
@ -89,18 +83,77 @@ pub enum SessionAuthorization {
|
|||
impl SessionAuthorization {
|
||||
pub fn username(&self) -> &String {
|
||||
match self {
|
||||
SessionAuthorization::User(username) => username,
|
||||
SessionAuthorization::Ticket { username, .. } => username,
|
||||
Self::User(username) => username,
|
||||
Self::Ticket { username, .. } => username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Result<bool> {
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub enum RequestAuthorization {
|
||||
Session(SessionAuthorization),
|
||||
Token { username: String },
|
||||
}
|
||||
|
||||
impl RequestAuthorization {
|
||||
pub fn username(&self) -> &String {
|
||||
match self {
|
||||
Self::Session(auth) => auth.username(),
|
||||
Self::Token { username, .. } => username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl<'a> FromRequest<'a> for RequestAuthorization {
|
||||
async fn from_request(req: &'a Request, body: &mut RequestBody) -> poem::Result<Self> {
|
||||
let session: &Session = <_>::from_request(req, body).await?;
|
||||
if let Some(auth) = session.get_auth() {
|
||||
return Ok(RequestAuthorization::Session(auth));
|
||||
}
|
||||
|
||||
let token = req
|
||||
.headers()
|
||||
.get("Authorization")
|
||||
.and_then(|x| x.to_str().ok())
|
||||
.and_then(|x| x.strip_prefix("Bearer "))
|
||||
.map(|x| x.to_owned());
|
||||
|
||||
if let Some(token) = token {
|
||||
let db: Data<&Arc<Mutex<DatabaseConnection>>> = <_>::from_request(req, body).await?;
|
||||
let mut db = db.lock().await;
|
||||
|
||||
let token = utf8_percent_encode(&token, NON_ALPHANUMERIC).to_string();
|
||||
let token = Token::Entity::find()
|
||||
.filter(Token::Column::Secret.eq(token))
|
||||
.one(&mut *db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
if let Some(token) = token {
|
||||
let user = token
|
||||
.find_related(User::Entity)
|
||||
.one(&mut *db)
|
||||
.await
|
||||
.map_err(poem::error::InternalServerError)?;
|
||||
if let Some(user) = user {
|
||||
return Ok(RequestAuthorization::Token {
|
||||
username: user.username,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(GetDataError(type_name::<RequestAuthorization>()).into())
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_user_admin(req: &Request, auth: &RequestAuthorization) -> poem::Result<bool> {
|
||||
let services: Data<&Services> = <_>::from_request_without_body(req).await?;
|
||||
|
||||
let SessionAuthorization::User(username) = auth else {
|
||||
return Ok(false)
|
||||
};
|
||||
let username = auth.username();
|
||||
if let RequestAuthorization::Session(SessionAuthorization::Ticket { .. }) = auth {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let mut config_provider = services.config_provider.lock().await;
|
||||
let targets = config_provider.list_targets().await?;
|
||||
|
@ -119,7 +172,7 @@ async fn is_user_admin(req: &Request, auth: &SessionAuthorization) -> poem::Resu
|
|||
|
||||
pub fn endpoint_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(&req).await?;
|
||||
let auth: RequestAuthorization = <_>::from_request_without_body(&req).await?;
|
||||
if is_user_admin(&req, &auth).await? {
|
||||
return Ok(ep.call(req).await?.into_response());
|
||||
}
|
||||
|
@ -129,7 +182,7 @@ pub fn endpoint_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
|||
|
||||
pub fn page_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let auth: Data<&SessionAuthorization> = <_>::from_request_without_body(&req).await?;
|
||||
let auth: RequestAuthorization = <_>::from_request_without_body(&req).await?;
|
||||
let session: &Session = <_>::from_request_without_body(&req).await?;
|
||||
if is_user_admin(&req, &auth).await? {
|
||||
return Ok(ep.call(req).await?.into_response());
|
||||
|
@ -139,33 +192,24 @@ pub fn page_admin_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn _inner_auth<E: Endpoint + 'static>(
|
||||
ep: Arc<E>,
|
||||
req: Request,
|
||||
) -> poem::Result<Option<E::Output>> {
|
||||
let session: &Session = FromRequest::from_request_without_body(&req).await?;
|
||||
|
||||
Ok(match session.get_auth() {
|
||||
Some(auth) => Some(ep.data(auth).call(req).await?),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn endpoint_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint<Output = E::Output> {
|
||||
e.around(|ep, req| async move {
|
||||
_inner_auth(ep, req)
|
||||
Option::<RequestAuthorization>::from_request_without_body(&req)
|
||||
.await?
|
||||
.ok_or_else(|| poem::Error::from_status(StatusCode::UNAUTHORIZED))
|
||||
.ok_or_else(|| poem::Error::from_status(StatusCode::UNAUTHORIZED))?;
|
||||
ep.call(req).await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn page_auth<E: Endpoint + 'static>(e: E) -> impl Endpoint {
|
||||
e.around(|ep, req| async move {
|
||||
let err_resp = gateway_redirect(&req).into_response();
|
||||
Ok(_inner_auth(ep, req)
|
||||
if Option::<RequestAuthorization>::from_request_without_body(&req)
|
||||
.await?
|
||||
.map(IntoResponse::into_response)
|
||||
.unwrap_or(err_resp))
|
||||
.is_none()
|
||||
{
|
||||
return Ok(gateway_redirect(&req).into_response());
|
||||
}
|
||||
Ok(ep.call(req).await?.into_response())
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -65,8 +65,6 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
env!("CARGO_PKG_VERSION"),
|
||||
)
|
||||
.server("/@warpgate/api");
|
||||
let ui = api_service.swagger_ui();
|
||||
let spec = api_service.spec_endpoint();
|
||||
|
||||
let session_storage =
|
||||
SharedSessionStorage(Arc::new(Mutex::new(Box::new(MemoryStorage::default()))));
|
||||
|
@ -86,13 +84,15 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
)
|
||||
};
|
||||
|
||||
let db = self.services.db.clone();
|
||||
|
||||
let app = Route::new()
|
||||
.nest(
|
||||
"/@warpgate",
|
||||
Route::new()
|
||||
.nest("/api/swagger", ui)
|
||||
.nest("/api/swagger", api_service.swagger_ui())
|
||||
.nest("/api/openapi.json", api_service.spec_endpoint())
|
||||
.nest("/api", api_service.with(cache_bust()))
|
||||
.nest("/api/openapi.json", spec)
|
||||
.nest_no_strip(
|
||||
"/assets",
|
||||
EmbeddedFilesEndpoint::<Assets>::new().with(cache_static()),
|
||||
|
@ -156,7 +156,8 @@ impl ProtocolServer for HTTPProtocolServer {
|
|||
.with(CookieHostMiddleware::new())
|
||||
.data(self.services.clone())
|
||||
.data(session_store.clone())
|
||||
.data(session_storage);
|
||||
.data(session_storage)
|
||||
.data(db);
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
|
|
|
@ -66,6 +66,7 @@ static DONT_FORWARD_HEADERS: Lazy<HashSet<HeaderName>> = Lazy::new(|| {
|
|||
#[allow(clippy::mutable_key_type)]
|
||||
let mut s = HashSet::new();
|
||||
s.insert(http::header::ACCEPT_ENCODING);
|
||||
s.insert(http::header::AUTHORIZATION);
|
||||
s.insert(http::header::SEC_WEBSOCKET_EXTENSIONS);
|
||||
s.insert(http::header::SEC_WEBSOCKET_ACCEPT);
|
||||
s.insert(http::header::SEC_WEBSOCKET_KEY);
|
||||
|
|
|
@ -80,16 +80,16 @@ const routes = {
|
|||
</div>
|
||||
</a>
|
||||
{#if $serverInfo?.username}
|
||||
<a use:link use:active href="/">Sessions</a>
|
||||
<a use:link use:active href="/config">Config</a>
|
||||
<a use:link use:active href="/tickets">Tickets</a>
|
||||
<a use:link use:active href="/ssh">SSH</a>
|
||||
<a use:link use:active href="/log">Log</a>
|
||||
<a use:link use:active class="nav" href="/">Sessions</a>
|
||||
<a use:link use:active class="nav" href="/config">Config</a>
|
||||
<a use:link use:active class="nav" href="/tickets">Tickets</a>
|
||||
<a use:link use:active class="nav" href="/ssh">SSH</a>
|
||||
<a use:link use:active class="nav" href="/log">Log</a>
|
||||
{/if}
|
||||
{#if $serverInfo?.username}
|
||||
<div class="username ms-auto">
|
||||
<a href='/@warpgate#/profile' class="username ms-auto">
|
||||
{$serverInfo?.username}
|
||||
</div>
|
||||
</a>
|
||||
<button class="btn btn-link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</button>
|
||||
|
@ -135,7 +135,7 @@ const routes = {
|
|||
padding: 10px 0;
|
||||
margin: 10px 0 20px;
|
||||
|
||||
a, .logo {
|
||||
.nav, .logo {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { api, SessionSnapshot } from 'admin/lib/api'
|
||||
import moment from 'moment'
|
||||
import { timer, Observable, switchMap, from, combineLatest, fromEvent, merge } from 'rxjs'
|
||||
import RelativeDate from './RelativeDate.svelte'
|
||||
import RelativeDate from '../common/RelativeDate.svelte'
|
||||
import AsyncButton from 'common/AsyncButton.svelte'
|
||||
import ItemList, { LoadOptions, PaginatedResponse } from 'common/ItemList.svelte'
|
||||
import { Input } from 'sveltestrap'
|
||||
|
|
|
@ -8,7 +8,7 @@ import { onDestroy } from 'svelte'
|
|||
import { link } from 'svelte-spa-router'
|
||||
import { Alert } from 'sveltestrap'
|
||||
import LogViewer from './LogViewer.svelte'
|
||||
import RelativeDate from './RelativeDate.svelte'
|
||||
import RelativeDate from '../common/RelativeDate.svelte'
|
||||
|
||||
export let params = { id: '' }
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { api, Ticket } from 'admin/lib/api'
|
||||
import { link } from 'svelte-spa-router'
|
||||
import { Alert } from 'sveltestrap'
|
||||
import RelativeDate from './RelativeDate.svelte'
|
||||
import RelativeDate from '../common/RelativeDate.svelte'
|
||||
|
||||
let error: Error|undefined
|
||||
let tickets: Ticket[]|undefined
|
||||
|
|
|
@ -1071,6 +1071,21 @@
|
|||
},
|
||||
"operationId": "get_logs"
|
||||
}
|
||||
},
|
||||
"/__": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"TokenAuth": []
|
||||
}
|
||||
],
|
||||
"operationId": "_ignore_me"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -1284,7 +1299,7 @@
|
|||
},
|
||||
"port": {
|
||||
"type": "integer",
|
||||
"format": "uint16"
|
||||
"format": "int32"
|
||||
},
|
||||
"key_type": {
|
||||
"type": "string"
|
||||
|
@ -1642,7 +1657,7 @@
|
|||
},
|
||||
"uses_left": {
|
||||
"type": "integer",
|
||||
"format": "uint32"
|
||||
"format": "int32"
|
||||
},
|
||||
"expiry": {
|
||||
"type": "string",
|
||||
|
@ -1922,6 +1937,12 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"TokenAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
import { faSignOut } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Alert } from 'sveltestrap'
|
||||
import Fa from 'svelte-fa'
|
||||
import Router, { push, RouteDetail } from 'svelte-spa-router'
|
||||
import Router, { link, push, RouteDetail } from 'svelte-spa-router'
|
||||
import { wrap } from 'svelte-spa-router/wrap'
|
||||
import { get } from 'svelte/store'
|
||||
import { api } from 'gateway/lib/api'
|
||||
|
@ -57,6 +57,14 @@ const routes = {
|
|||
asyncComponent: () => import('./OutOfBandAuth.svelte'),
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
'/profile': wrap({
|
||||
asyncComponent: () => import('./Profile.svelte'),
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
'/profile/tokens': wrap({
|
||||
asyncComponent: () => import('./Tokens.svelte'),
|
||||
conditions: [requireLogin],
|
||||
}),
|
||||
}
|
||||
|
||||
init()
|
||||
|
@ -77,12 +85,12 @@ init()
|
|||
</a>
|
||||
|
||||
{#if $serverInfo?.username}
|
||||
<div class="ms-auto">
|
||||
<a use:link href="/profile" class="ms-auto">
|
||||
{$serverInfo.username}
|
||||
{#if $serverInfo.authorizedViaTicket}
|
||||
<span class="ml-2">(ticket auth)</span>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
<button class="btn btn-link" on:click={logout} title="Log out">
|
||||
<Fa icon={faSignOut} fw />
|
||||
</button>
|
||||
|
|
12
warpgate-web/src/gateway/Profile.svelte
Normal file
12
warpgate-web/src/gateway/Profile.svelte
Normal file
|
@ -0,0 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { link } from 'svelte-spa-router'
|
||||
import { serverInfo } from './lib/store'
|
||||
</script>
|
||||
|
||||
<div class="page-summary-bar">
|
||||
<h1>{$serverInfo?.username ?? 'Profile'}</h1>
|
||||
</div>
|
||||
|
||||
<p><a use:link href="/profile/tokens">Manage API tokens</a></p>
|
||||
<p><a href="/@warpgate/api/swagger">Admin API playground</a></p>
|
||||
<p><a href="/@warpgate/admin/api/swagger">User API playground</a></p>
|
128
warpgate-web/src/gateway/Tokens.svelte
Normal file
128
warpgate-web/src/gateway/Tokens.svelte
Normal file
|
@ -0,0 +1,128 @@
|
|||
<script lang="ts">
|
||||
import { api, Token } from 'gateway/lib/api'
|
||||
import { Alert, Button, FormGroup, Modal, ModalBody, ModalFooter, ModalHeader } from 'sveltestrap'
|
||||
import RelativeDate from 'common/RelativeDate.svelte'
|
||||
import CopyButton from 'common/CopyButton.svelte'
|
||||
|
||||
let error: Error|undefined
|
||||
let tokens: Token[]|undefined
|
||||
let newSecret: string|undefined
|
||||
let isCreateModalOpen = false
|
||||
let newTokenName = ''
|
||||
|
||||
async function load () {
|
||||
tokens = await api.getTokens()
|
||||
}
|
||||
|
||||
function showCreateTokenModal () {
|
||||
isCreateModalOpen = true
|
||||
newTokenName = ''
|
||||
}
|
||||
|
||||
async function createNewToken () {
|
||||
const response = await api.createToken({ createTokenRequest: { name: newTokenName } })
|
||||
tokens = [...tokens!, response.token]
|
||||
newSecret = response.secret
|
||||
isCreateModalOpen = false
|
||||
}
|
||||
|
||||
load().catch(e => {
|
||||
error = e
|
||||
})
|
||||
|
||||
async function deleteToken (token: Token) {
|
||||
newSecret = undefined
|
||||
await api.deleteToken(token)
|
||||
load()
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if error}
|
||||
<Alert color="danger">{error}</Alert>
|
||||
{/if}
|
||||
|
||||
{#if tokens}
|
||||
<div class="page-summary-bar">
|
||||
<h1>API tokens</h1>
|
||||
<button
|
||||
class="btn btn-outline-secondary ms-auto"
|
||||
on:click={showCreateTokenModal}>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if newSecret}
|
||||
<FormGroup floating label="New token" class="d-flex align-items-center">
|
||||
<input type="text" class="form-control" readonly value={newSecret} />
|
||||
<CopyButton text={newSecret} />
|
||||
</FormGroup>
|
||||
|
||||
<Alert color="warning" fade={false}>
|
||||
The API token is only shown once - you won't be able to see it again.
|
||||
</Alert>
|
||||
{/if}
|
||||
|
||||
{#if tokens.length }
|
||||
<div class="list-group list-group-flush">
|
||||
{#each tokens as token}
|
||||
<div class="list-group-item">
|
||||
<strong class="me-auto">
|
||||
{token.name}
|
||||
</strong>
|
||||
<small class="text-muted me-4">
|
||||
<RelativeDate date={token.created} />
|
||||
</small>
|
||||
<a href={''} on:click|preventDefault={() => deleteToken(token)}>Delete</a>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<Alert color="info" fade={false}>
|
||||
Tokens grant access to the Warpgate API.
|
||||
</Alert>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
|
||||
<Modal bind:isOpen={isCreateModalOpen}>
|
||||
<ModalHeader toggle={() => isCreateModalOpen = false}>
|
||||
Create API token
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<form on:submit|preventDefault={createNewToken}>
|
||||
<FormGroup floating label="Name" class="d-flex align-items-center">
|
||||
<!-- svelte-ignore a11y-autofocus -->
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
bind:value={newTokenName}
|
||||
autofocus
|
||||
/>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<div class="d-flex">
|
||||
<Button
|
||||
class="ms-auto"
|
||||
outline
|
||||
disabled={!newTokenName}
|
||||
on:click={createNewToken}
|
||||
>Save</Button>
|
||||
|
||||
<Button
|
||||
class="ms-2"
|
||||
outline
|
||||
color="danger"
|
||||
on:click={() => isCreateModalOpen = false}
|
||||
>Cancel</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
|
||||
<style lang="scss">
|
||||
.list-group-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
|
@ -68,7 +68,7 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"operationId": "otpLogin"
|
||||
"operationId": "otp_login"
|
||||
}
|
||||
},
|
||||
"/auth/logout": {
|
||||
|
@ -98,7 +98,7 @@
|
|||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "getDefaultAuthState"
|
||||
"operationId": "get_default_auth_state"
|
||||
},
|
||||
"delete": {
|
||||
"responses": {
|
||||
|
@ -116,7 +116,7 @@
|
|||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "cancelDefaultAuth"
|
||||
"operationId": "cancel_default_auth"
|
||||
}
|
||||
},
|
||||
"/auth/state/{id}": {
|
||||
|
@ -295,6 +295,21 @@
|
|||
}
|
||||
},
|
||||
"operationId": "return_to_sso"
|
||||
},
|
||||
"post": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"text/html": {
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operationId": "return_to_sso_with_form_data"
|
||||
}
|
||||
},
|
||||
"/sso/providers/{name}/start": {
|
||||
|
@ -338,6 +353,92 @@
|
|||
},
|
||||
"operationId": "start_sso"
|
||||
}
|
||||
},
|
||||
"/tokens": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/Token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operationId": "get_tokens"
|
||||
},
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTokenRequest"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TokenAndSecret"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"operationId": "create_token"
|
||||
}
|
||||
},
|
||||
"/tokens/{id}": {
|
||||
"delete": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "id",
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"deprecated": false,
|
||||
"explode": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": ""
|
||||
},
|
||||
"404": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"operationId": "delete_token"
|
||||
}
|
||||
},
|
||||
"/__": {
|
||||
"get": {
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"TokenAuth": []
|
||||
}
|
||||
],
|
||||
"operationId": "_ignore_me"
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
|
@ -370,6 +471,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"CreateTokenRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Info": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
@ -517,6 +629,57 @@
|
|||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Token": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"user_id",
|
||||
"created"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"format": "uuid"
|
||||
},
|
||||
"expiry": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"created": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TokenAndSecret": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"token",
|
||||
"secret"
|
||||
],
|
||||
"properties": {
|
||||
"token": {
|
||||
"$ref": "#/components/schemas/Token"
|
||||
},
|
||||
"secret": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"securitySchemes": {
|
||||
"TokenAuth": {
|
||||
"type": "http",
|
||||
"scheme": "bearer"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue