Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions pg-pkg/src/handlers/api_key_validate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//! `GET /v2/api-key/validate` — confirms a `PG-…` API key is valid and
//! returns the associated tenant identity. Used by sibling services (e.g.
//! cryptify) that need to gate per-tenant behaviour on a validated key
//! without going through signing-key issuance.
//!
//! Auth: gated by `ApiKeyGuard` + `Auth::new(_, AuthType::Key)`. Reaching
//! this handler means the bearer token already passed `business_api_keys`
//! lookup; this handler only serializes the result.

use actix_web::{HttpMessage, HttpRequest, HttpResponse};
use serde::Serialize;

use crate::middleware::auth::ApiKeyData;

#[derive(Debug, Serialize)]
pub struct ApiKeyValidateResponse {
/// `organizations.id` — stable per-tenant identifier.
pub tenant_id: String,
/// Best-effort organisation display name. May be `None` when the
/// business portal has no name configured for the tenant.
pub organisation_name: Option<String>,
}

pub async fn api_key_validate(req: HttpRequest) -> Result<HttpResponse, crate::Error> {
let key_data = req
.extensions()
.get::<ApiKeyData>()
.cloned()
.ok_or(crate::Error::Unexpected)?;

req.extensions_mut().clear();

Ok(HttpResponse::Ok().json(ApiKeyValidateResponse {
tenant_id: key_data.org_id,
organisation_name: key_data.organisation_name,
}))
}
2 changes: 2 additions & 0 deletions pg-pkg/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod api_key_validate;
mod health;
mod jwt;
mod key;
Expand All @@ -6,6 +7,7 @@ mod parameters;
mod signing_key;
mod start;

pub use api_key_validate::*;
pub use health::*;
pub use jwt::*;
pub use key::*;
Expand Down
16 changes: 14 additions & 2 deletions pg-pkg/src/middleware/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ pub(crate) struct AuthResult {
/// `signing_attrs` — see [`PgApiKeyStore::lookup`].
#[derive(Debug, Clone)]
pub struct ApiKeyData {
/// Stable tenant identifier — `organizations.id` (uuid, stringified).
/// Used by callers like cryptify as the per-tenant accounting key for
/// quotas. Not exposed in any signing identity.
pub org_id: String,
pub email: String,
pub organisation_name: Option<String>,
pub phone_number: Option<String>,
Expand Down Expand Up @@ -96,6 +100,7 @@ impl ApiKeyStore for PgApiKeyStore {
let result = sqlx::query_as::<
_,
(
String,
String,
String,
Option<String>,
Expand All @@ -104,7 +109,7 @@ impl ApiKeyStore for PgApiKeyStore {
),
>(
r#"
SELECT o.signing_email, o.name, u.phone, o.kvk_number, k.signing_attrs
SELECT o.id::text, o.signing_email, o.name, u.phone, o.kvk_number, k.signing_attrs
FROM business_api_keys k
JOIN organizations o ON o.id = k.org_id
LEFT JOIN users u ON u.id = o.contact_user_id
Expand All @@ -121,7 +126,7 @@ impl ApiKeyStore for PgApiKeyStore {
crate::Error::Unexpected
})?;

let Some((org_email, org_name, org_phone, org_kvk, signing_attrs)) = result else {
let Some((org_id, org_email, org_name, org_phone, org_kvk, signing_attrs)) = result else {
return Ok(None);
};

Expand Down Expand Up @@ -163,6 +168,7 @@ impl ApiKeyStore for PgApiKeyStore {
// The business schema has no public/private split — every enabled
// attribute is published in the signing identity.
Ok(Some(ApiKeyData {
org_id,
email,
organisation_name,
phone_number,
Expand Down Expand Up @@ -311,6 +317,11 @@ where
priv_attributes: priv_attrs.clone(),
});

// Expose the validated key data to handlers that just need
// identity (e.g. the `/v2/api-key/validate` endpoint) without
// running through signing-key issuance.
req.extensions_mut().insert(key_data.clone());

// Build all attributes as disclosed for the session result.
let all_attrs: Vec<Attribute> =
pub_attrs.into_iter().chain(priv_attrs).collect();
Expand Down Expand Up @@ -571,6 +582,7 @@ pub mod tests {
let store = MockApiKeyStore::new().with_key(
"valid-key",
ApiKeyData {
org_id: "00000000-0000-0000-0000-000000000001".to_string(),
email: "user@example.com".to_string(),
organisation_name: None,
phone_number: None,
Expand Down
Loading
Loading