Add More Metadata to public ssh keys (#1182)

Co-authored-by: Eugene <inbox@null.page>
This commit is contained in:
Mohammad Al Shakoush 2024-12-22 19:13:42 +01:00 committed by GitHub
parent 7a904dbb8c
commit 59884fbbe9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 255 additions and 65 deletions

View file

@ -1,5 +1,6 @@
use std::sync::Arc;
use chrono::{DateTime, Utc};
use poem::web::Data;
use poem_openapi::param::Path;
use poem_openapi::payload::Json;
@ -19,6 +20,8 @@ use super::AnySecurityScheme;
struct ExistingPublicKeyCredential {
id: Uuid,
label: String,
date_added: Option<DateTime<Utc>>,
last_used: Option<DateTime<Utc>>,
openssh_public_key: String,
}
@ -32,6 +35,8 @@ impl From<PublicKeyCredential::Model> for ExistingPublicKeyCredential {
fn from(credential: PublicKeyCredential::Model) -> Self {
Self {
id: credential.id,
date_added: credential.date_added,
last_used: credential.last_used,
label: credential.label,
openssh_public_key: credential.openssh_public_key,
}
@ -115,6 +120,8 @@ impl ListApi {
let object = PublicKeyCredential::ActiveModel {
id: Set(Uuid::new_v4()),
user_id: Set(*user_id),
date_added: Set(Some(Utc::now())),
last_used: Set(None),
label: Set(body.label.clone()),
..PublicKeyCredential::ActiveModel::from(UserPublicKeyCredential::try_from(&*body)?)
}
@ -158,6 +165,7 @@ impl DetailApi {
let model = PublicKeyCredential::ActiveModel {
id: Set(id.0),
user_id: Set(*user_id),
date_added: Set(Some(Utc::now())),
label: Set(body.label.clone()),
..<_>::from(UserPublicKeyCredential::try_from(&*body)?)
}

View file

@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use async_trait::async_trait;
use chrono::Utc;
use data_encoding::BASE64;
use sea_orm::{
ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, ModelTrait, QueryFilter,
@ -381,4 +382,58 @@ impl ConfigProvider for DatabaseConfigProvider {
Ok(())
}
async fn update_public_key_last_used(
&self,
credential: Option<AuthCredential>,
) -> Result<(), WarpgateError> {
let db = self.db.lock().await;
let Some(AuthCredential::PublicKey {
kind,
public_key_bytes,
}) = credential
else {
error!("Invalid or missing public key credential");
return Err(WarpgateError::InvalidCredentialType);
};
// Encode public key and match it against the database
let base64_bytes = data_encoding::BASE64.encode(&public_key_bytes);
let openssh_public_key = format!("{kind} {base64_bytes}");
debug!(
"Attempting to update last_used for public key: {}",
openssh_public_key
);
// Find the public key credential
let public_key_credential = entities::PublicKeyCredential::Entity::find()
.filter(
entities::PublicKeyCredential::Column::OpensshPublicKey
.eq(openssh_public_key.clone()),
)
.one(&*db)
.await?;
let Some(public_key_credential) = public_key_credential else {
warn!(
"Public key not found in the database: {}",
openssh_public_key
);
return Ok(()); // Gracefully return if the key is not found
};
// Update the `last_used` (last used) timestamp
let mut active_model: entities::PublicKeyCredential::ActiveModel =
public_key_credential.into();
active_model.last_used = Set(Some(Utc::now()));
active_model.update(&*db).await.map_err(|e| {
error!("Failed to update last_used for public key: {:?}", e);
WarpgateError::DatabaseError(e.into())
})?;
Ok(())
}
}

View file

@ -47,6 +47,11 @@ pub trait ConfigProvider {
username: &str,
target: &str,
) -> Result<bool, WarpgateError>;
async fn update_public_key_last_used(
&self,
credential: Option<AuthCredential>,
) -> Result<(), WarpgateError>;
}
//TODO: move this somewhere

View file

@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use sea_orm::entity::prelude::*;
use sea_orm::sea_query::ForeignKeyAction;
use sea_orm::Set;
@ -12,6 +13,8 @@ pub struct Model {
pub id: Uuid,
pub user_id: Uuid,
pub label: String,
pub date_added: Option<DateTime<Utc>>,
pub last_used: Option<DateTime<Utc>>,
pub openssh_public_key: String,
}

View file

@ -14,6 +14,7 @@ mod m00009_credential_models;
mod m00010_parameters;
mod m00011_rsa_key_algos;
mod m00012_add_openssh_public_key_label;
mod m00013_add_openssh_public_key_dates;
pub struct Migrator;
@ -33,6 +34,7 @@ impl MigratorTrait for Migrator {
Box::new(m00010_parameters::Migration),
Box::new(m00011_rsa_key_algos::Migration),
Box::new(m00012_add_openssh_public_key_label::Migration),
Box::new(m00013_add_openssh_public_key_dates::Migration),
]
}
}

View file

@ -21,9 +21,9 @@ impl MigrationTrait for Migration {
ColumnDef::new(Alias::new("label"))
.string()
.not_null()
.default("Public Key")
.default("Public Key"),
)
.to_owned()
.to_owned(),
)
.await
}
@ -38,5 +38,4 @@ impl MigrationTrait for Migration {
)
.await
}
}

View file

@ -0,0 +1,62 @@
use sea_orm_migration::prelude::*;
pub struct Migration;
impl MigrationName for Migration {
fn name(&self) -> &str {
"m00013_add_openssh_public_key_dates"
}
}
use crate::m00009_credential_models::public_key_credential;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Add 'date_added' column
manager
.alter_table(
Table::alter()
.table(public_key_credential::Entity)
.add_column(ColumnDef::new(Alias::new("date_added")).date_time().null())
.to_owned(),
)
.await?;
// Add 'last_used' column
manager
.alter_table(
Table::alter()
.table(public_key_credential::Entity)
.add_column(ColumnDef::new(Alias::new("last_used")).date_time().null())
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Drop 'last_used' column
manager
.alter_table(
Table::alter()
.table(public_key_credential::Entity)
.drop_column(Alias::new("last_used"))
.to_owned(),
)
.await?;
// Drop 'date_added' column
manager
.alter_table(
Table::alter()
.table(public_key_credential::Entity)
.drop_column(Alias::new("date_added"))
.to_owned(),
)
.await?;
Ok(())
}
}

View file

@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use http::StatusCode;
use poem::web::Data;
use poem::{Endpoint, EndpointExt, FromRequest, IntoResponse};
@ -80,6 +81,8 @@ struct NewPublicKeyCredential {
struct ExistingPublicKeyCredential {
id: Uuid,
label: String,
date_added: Option<DateTime<Utc>>,
last_used: Option<DateTime<Utc>>,
abbreviated: String,
}
@ -91,8 +94,8 @@ fn abbreviate_public_key(k: &str) -> String {
format!(
"{}...{}",
&k[..l.min(k.len())], // Take the first `l` characters.
&k[k.len().saturating_sub(l)..] // Take the last `l` characters safely.
&k[..l.min(k.len())], // Take the first `l` characters.
&k[k.len().saturating_sub(l)..] // Take the last `l` characters safely.
)
}
@ -101,6 +104,8 @@ impl From<entities::PublicKeyCredential::Model> for ExistingPublicKeyCredential
Self {
id: credential.id,
label: credential.label,
date_added: credential.date_added,
last_used: credential.last_used,
abbreviated: abbreviate_public_key(&credential.openssh_public_key),
}
}
@ -295,6 +300,8 @@ impl Api {
let object = PublicKeyCredential::ActiveModel {
id: Set(Uuid::new_v4()),
user_id: Set(user_model.id),
date_added: Set(Some(Utc::now())),
last_used: Set(None),
label: Set(body.label.clone()),
openssh_public_key: Set(body.openssh_public_key.clone()),
}

View file

@ -1222,18 +1222,28 @@ impl ServerSession {
key.public_key_base64()
);
let result = self
.try_auth_lazy(
&selector,
Some(AuthCredential::PublicKey {
kind: key.algorithm(),
public_key_bytes: Bytes::from(key.public_key_bytes()),
}),
)
.await;
let key = Some(AuthCredential::PublicKey {
kind: key.algorithm(),
public_key_bytes: Bytes::from(key.public_key_bytes()),
});
let result = self.try_auth_lazy(&selector, key.clone()).await;
match result {
Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept,
Ok(AuthResult::Accepted { .. }) => {
// Update last_used timestamp
if let Err(err) = self
.services
.config_provider
.lock()
.await
.update_public_key_last_used(key.clone())
.await
{
warn!(?err, "Failed to update last_used for public key");
}
russh::server::Auth::Accept
}
Ok(AuthResult::Rejected) => russh::server::Auth::Reject {
proceed_with_methods: Some(MethodSet::all()),
},

View file

@ -20,6 +20,7 @@
import CreateOtpModal from './CreateOtpModal.svelte'
import AuthPolicyEditor from './AuthPolicyEditor.svelte'
import { possibleCredentials } from 'common/protocols'
import CredentialUsedStateBadge from 'common/CredentialUsedStateBadge.svelte'
interface Props {
userId: string
@ -248,27 +249,32 @@
<div class="list-group-item credential">
{#if credential.kind === CredentialKind.Password }
<Fa fw icon={faKeyboard} />
<span class="type">Password</span>
<span class="label me-auto">Password</span>
{/if}
{#if credential.kind === 'PublicKey'}
<Fa fw icon={faKey} />
<span class="type">{credential.label}</span>
<span class="text-muted ms-2">{abbreviatePublicKey(credential.opensshPublicKey)}</span>
<div class="main me-auto">
<div class="label d-flex align-items-center">
{credential.label}
</div>
<small class="d-block text-muted">{abbreviatePublicKey(credential.opensshPublicKey)}</small>
</div>
<CredentialUsedStateBadge credential={credential} />
<div class="me-2"></div>
{/if}
{#if credential.kind === 'Totp'}
<Fa fw icon={faMobileScreen} />
<span class="type">One-time password</span>
<span class="label me-auto">One-time password</span>
{/if}
{#if credential.kind === CredentialKind.Sso}
<Fa fw icon={faIdBadge} />
<span class="type">Single sign-on</span>
<span class="text-muted ms-2">
<span class="label">Single sign-on</span>
<span class="text-muted ms-2 me-auto">
{credential.email}
{#if credential.provider} ({credential.provider}){/if}
</span>
{/if}
<span class="ms-auto"></span>
{#if credential.kind === CredentialKind.PublicKey || credential.kind === CredentialKind.Sso}
<a
class="ms-2"
@ -362,16 +368,8 @@
display: flex;
align-items: center;
.type {
margin-left: .5rem;
}
a {
display: none;
}
&:hover a {
display: initial;
.label:not(:first-child), .main {
margin-left: .75rem;
}
}
</style>

View file

@ -82,8 +82,8 @@
<div>
<h1>session</h1>
<div class="d-flex align-items-center mt-1">
<Tooltip delay="500" target="usernameBadge" animation>Authenticated user</Tooltip>
<Tooltip delay="500" target="targetBadge" animation>Selected target</Tooltip>
<Tooltip delay="250" target="usernameBadge" animation>Authenticated user</Tooltip>
<Tooltip delay="250" target="targetBadge" animation>Selected target</Tooltip>
<Badge id="usernameBadge" color="success" class="me-2 d-flex align-items-center">
{#if session.username}

View file

@ -2165,6 +2165,14 @@
"label": {
"type": "string"
},
"date_added": {
"type": "string",
"format": "date-time"
},
"last_used": {
"type": "string",
"format": "date-time"
},
"openssh_public_key": {
"type": "string"
}

View file

@ -0,0 +1,39 @@
<script lang="ts">
import { uuid } from './sveltestrap-s5-ports/_sveltestrapUtils'
import Badge from './sveltestrap-s5-ports/Badge.svelte'
import Tooltip from './sveltestrap-s5-ports/Tooltip.svelte'
interface DatedCredential {
lastUsed?: Date
dateAdded?: Date
}
export let credential: DatedCredential
const id = uuid()
const lastUseThreshold = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
let badge: HTMLElement | undefined
</script>
<span bind:this={badge}>
{#if credential.lastUsed}
{#if credential.lastUsed.getTime() < lastUseThreshold.getTime()}
<Badge id={id} color="warning">Not used recently</Badge>
{:else}
<Badge id={id} color="success">Used recently</Badge>
{/if}
{:else}
<Badge id={id} color="warning">Never used</Badge>
{/if}
</span>
{#if credential.dateAdded || credential.lastUsed}
<Tooltip target={badge} animation delay="250">
{#if credential.dateAdded}
<div>Added on: {new Date(credential.dateAdded).toLocaleString()}</div>
{/if}
{#if credential.lastUsed}
<div>Last used: {new Date(credential.lastUsed).toLocaleString()}</div>
{/if}
</Tooltip>
{/if}

View file

@ -7,9 +7,9 @@
import { faIdBadge, faKey, faKeyboard, faMobilePhone } from '@fortawesome/free-solid-svg-icons'
import Fa from 'svelte-fa'
import PublicKeyCredentialModal from 'admin/PublicKeyCredentialModal.svelte'
import { Button } from '@sveltestrap/sveltestrap'
import CreatePasswordModal from 'admin/CreatePasswordModal.svelte'
import CreateOtpModal from 'admin/CreateOtpModal.svelte'
import CredentialUsedStateBadge from 'common/CredentialUsedStateBadge.svelte'
let error: string|null = $state(null)
let creds: CredentialsState | undefined = $state()
@ -113,9 +113,9 @@
<div class="d-flex align-items-center mt-4 mb-2">
<h4 class="m-0">One-time passwords</h4>
<span class="ms-auto"></span>
<Button size="sm" color="link" on:click={() => {
<a href={''} color="link" onclick={() => {
creatingOtpCredential = true
}}>Add device</Button>
}}>Add device</a>
</div>
<div class="list-group list-group-flush mb-3">
@ -125,7 +125,7 @@
<span class="label">OTP device</span>
<span class="ms-auto"></span>
<a
class="hover-reveal ms-2"
class="ms-2"
href={''}
onclick={e => {
deleteOtp(credential)
@ -147,20 +147,23 @@
<div class="d-flex align-items-center mt-4 mb-2">
<h4 class="m-0">Public keys</h4>
<span class="ms-auto"></span>
<Button size="sm" color="link" on:click={() => {
<a href={''} color="link" onclick={() => {
creatingPublicKeyCredential = true
}}>Add key</Button>
}}>Add key</a>
</div>
<div class="list-group list-group-flush mb-3">
{#each creds.publicKeys as credential}
<div class="list-group-item credential">
<Fa fw icon={faKey} />
<span class="label">{credential.label}</span>
<span class="text-muted ms-2">{credential.abbreviated}</span>
<div class="main">
<div class="label">{credential.label}</div>
<small class="d-block text-muted">{credential.abbreviated}</small>
</div>
<span class="ms-auto"></span>
<CredentialUsedStateBadge credential={credential} />
<a
class="hover-reveal ms-2"
class="ms-2"
href={''}
onclick={e => {
deletePublicKey(credential)
@ -230,16 +233,8 @@
display: flex;
align-items: center;
.label:not(:first-child) {
margin-left: .5rem;
}
a.hover-reveal {
display: none;
}
&:hover a {
display: initial;
.label:not(:first-child), .main {
margin-left: .75rem;
}
}
</style>

View file

@ -699,6 +699,14 @@
"label": {
"type": "string"
},
"date_added": {
"type": "string",
"format": "date-time"
},
"last_used": {
"type": "string",
"format": "date-time"
},
"abbreviated": {
"type": "string"
}

View file

@ -226,10 +226,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
.to_string_lossy()
.to_string();
store.http.key = data_path
.join("tls.key.pem")
.to_string_lossy()
.to_string();
store.http.key = data_path.join("tls.key.pem").to_string_lossy().to_string();
store.mysql.certificate = store.http.certificate.clone();
store.mysql.key = store.http.key.clone();
@ -239,10 +236,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
// ---
store.ssh.keys = data_path
.join("ssh-keys")
.to_string_lossy()
.to_string();
store.ssh.keys = data_path.join("ssh-keys").to_string_lossy().to_string();
// ---
@ -257,10 +251,7 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
.with_prompt("Do you want to record user sessions?")
.interact()?;
}
store.recordings.path = data_path
.join("recordings")
.to_string_lossy()
.to_string();
store.recordings.path = data_path.join("recordings").to_string_lossy().to_string();
// ---