mirror of
https://github.com/PluralKit/PluralKit.git
synced 2026-02-11 16:20:13 +00:00
[WIP] feat: scoped api keys
This commit is contained in:
parent
e7ee593a85
commit
06cb160f95
45 changed files with 1264 additions and 154 deletions
|
|
@ -4,8 +4,10 @@ version = "0.1.0"
|
|||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
pk_macros = { path = "../macros" }
|
||||
jsonwebtoken = { workspace = true }
|
||||
sea-query = "0.32.1"
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true, features = ["preserve_order"] }
|
||||
|
|
|
|||
|
|
@ -6,9 +6,9 @@
|
|||
// note: caller needs to implement From<i32> for their type
|
||||
macro_rules! fake_enum_impls {
|
||||
($n:ident) => {
|
||||
impl Type<Postgres> for $n {
|
||||
fn type_info() -> PgTypeInfo {
|
||||
PgTypeInfo::with_name("INT4")
|
||||
impl ::sqlx::Type<::sqlx::Postgres> for $n {
|
||||
fn type_info() -> ::sqlx::postgres::PgTypeInfo {
|
||||
::sqlx::postgres::PgTypeInfo::with_name("INT4")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -18,14 +18,14 @@ macro_rules! fake_enum_impls {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'r, DB: Database> Decode<'r, DB> for $n
|
||||
impl<'r, DB: ::sqlx::Database> ::sqlx::Decode<'r, DB> for $n
|
||||
where
|
||||
i32: Decode<'r, DB>,
|
||||
i32: ::sqlx::Decode<'r, DB>,
|
||||
{
|
||||
fn decode(
|
||||
value: <DB as Database>::ValueRef<'r>,
|
||||
) -> Result<Self, Box<dyn Error + 'static + Send + Sync>> {
|
||||
let value = <i32 as Decode<DB>>::decode(value)?;
|
||||
value: <DB as ::sqlx::Database>::ValueRef<'r>,
|
||||
) -> Result<Self, Box<dyn ::std::error::Error + 'static + Send + Sync>> {
|
||||
let value = <i32 as ::sqlx::Decode<DB>>::decode(value)?;
|
||||
Ok(Self::from(value))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
104
crates/models/src/api_key.rs
Normal file
104
crates/models/src/api_key.rs
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
use pk_macros::pk_model;
|
||||
|
||||
use chrono::{DateTime, Utc, NaiveDateTime};
|
||||
use uuid::Uuid;
|
||||
use base64::{prelude::BASE64_STANDARD, Engine};
|
||||
use jsonwebtoken::{
|
||||
crypto::{sign, verify},
|
||||
DecodingKey, EncodingKey,
|
||||
};
|
||||
|
||||
use crate::SystemId;
|
||||
|
||||
#[derive(sqlx::Type, Debug, Clone, PartialEq, serde::Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[sqlx(rename_all = "snake_case")]
|
||||
#[sqlx(type_name = "api_key_type")]
|
||||
pub enum ApiKeyType {
|
||||
Dashboard,
|
||||
UserCreated,
|
||||
ExternalApp,
|
||||
}
|
||||
|
||||
#[pk_model]
|
||||
struct ApiKey {
|
||||
#[json = "id"]
|
||||
id: Uuid,
|
||||
system: SystemId,
|
||||
#[json = "type"]
|
||||
kind: ApiKeyType,
|
||||
#[json = "scopes"]
|
||||
scopes: Vec<String>,
|
||||
#[json = "app"]
|
||||
app: Option<Uuid>,
|
||||
#[json = "name"]
|
||||
#[patchable]
|
||||
name: Option<String>,
|
||||
|
||||
#[json = "discord_id"]
|
||||
discord_id: Option<i64>,
|
||||
#[private_patchable]
|
||||
discord_access_token: Option<String>,
|
||||
#[private_patchable]
|
||||
discord_refresh_token: Option<String>,
|
||||
#[private_patchable]
|
||||
discord_expires_at: Option<NaiveDateTime>,
|
||||
|
||||
#[json = "created"]
|
||||
created: DateTime<Utc>,
|
||||
}
|
||||
|
||||
const SIGNATURE_ALGORITHM: jsonwebtoken::Algorithm = jsonwebtoken::Algorithm::ES256;
|
||||
|
||||
impl PKApiKey {
|
||||
pub fn to_header_str(self, system_uuid: Uuid, key: &EncodingKey) -> String {
|
||||
let b64 = BASE64_STANDARD.encode(
|
||||
serde_json::to_vec(&serde_json::json!({
|
||||
"tid": self.id.to_string(),
|
||||
"sid": system_uuid.to_string(),
|
||||
"type": self.kind,
|
||||
"scopes": self.scopes,
|
||||
}))
|
||||
.expect("should not fail"),
|
||||
);
|
||||
|
||||
let signature = sign(b64.as_bytes(), key, SIGNATURE_ALGORITHM).expect("should not fail");
|
||||
|
||||
format!("pkapi:{b64}:{signature}")
|
||||
}
|
||||
|
||||
/// Parse a header string into a token uuid
|
||||
pub fn parse_header_str(token: String, key: &DecodingKey) -> Option<Uuid> {
|
||||
let mut parts = token.split(":");
|
||||
let pkapi = parts.next();
|
||||
if pkapi.is_none_or(|v| v != "pkapi") {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Some(jsonblob) = parts.next() else {
|
||||
return None;
|
||||
};
|
||||
let Some(sig) = parts.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// verify signature before doing anything else
|
||||
let valid = verify(sig, jsonblob.as_bytes(), key, SIGNATURE_ALGORITHM);
|
||||
if valid.is_err() || matches!(valid, Ok(false)) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let Ok(bytes) = BASE64_STANDARD.decode(jsonblob) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(obj) = serde_json::from_slice::<serde_json::Value>(bytes.as_slice()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
obj.get("tid")
|
||||
.map(|v| v.as_str().map(|f| Uuid::parse_str(f).ok()))
|
||||
.flatten()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +1,5 @@
|
|||
mod _util;
|
||||
|
||||
macro_rules! model {
|
||||
($n:ident) => {
|
||||
mod $n;
|
||||
pub use $n::*;
|
||||
};
|
||||
}
|
||||
|
||||
model!(system);
|
||||
model!(system_config);
|
||||
use _util::fake_enum_impls;
|
||||
|
||||
#[derive(serde::Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
|
@ -17,10 +8,7 @@ pub enum PrivacyLevel {
|
|||
Private,
|
||||
}
|
||||
|
||||
// this sucks, put it somewhere else
|
||||
use sqlx::{postgres::PgTypeInfo, Database, Decode, Postgres, Type};
|
||||
use std::error::Error;
|
||||
_util::fake_enum_impls!(PrivacyLevel);
|
||||
fake_enum_impls!(PrivacyLevel);
|
||||
|
||||
impl From<i32> for PrivacyLevel {
|
||||
fn from(value: i32) -> Self {
|
||||
|
|
@ -31,3 +19,24 @@ impl From<i32> for PrivacyLevel {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PrivacyLevel {
|
||||
pub fn to_string(&self) -> String {
|
||||
match self {
|
||||
PrivacyLevel::Public => "public".into(),
|
||||
PrivacyLevel::Private => "private".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! model {
|
||||
($n:ident) => {
|
||||
mod $n;
|
||||
pub use $n::*;
|
||||
};
|
||||
}
|
||||
|
||||
model!(api_key);
|
||||
model!(oauth2_app);
|
||||
model!(system);
|
||||
model!(system_config);
|
||||
|
|
|
|||
28
crates/models/src/oauth2_app.rs
Normal file
28
crates/models/src/oauth2_app.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use pk_macros::pk_model;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[pk_model]
|
||||
struct ExternalApp {
|
||||
#[json = "id"]
|
||||
id: Uuid,
|
||||
#[json = "name"]
|
||||
#[patchable]
|
||||
name: String,
|
||||
#[json = "homepage_url"]
|
||||
#[patchable]
|
||||
homepage_url: String,
|
||||
|
||||
#[private_patchable]
|
||||
oauth2_secret: Option<String>,
|
||||
#[json = "oauth2_allowed_redirects"]
|
||||
#[patchable]
|
||||
oauth2_allowed_redirects: Vec<String>,
|
||||
#[json = "oauth2_scopes"]
|
||||
#[patchable]
|
||||
oauth2_scopes: Vec<String>,
|
||||
|
||||
#[private_patchable]
|
||||
api_rl_token: Option<String>,
|
||||
#[private_patchable]
|
||||
api_rl_rate: Option<i32>,
|
||||
}
|
||||
|
|
@ -1,10 +1,9 @@
|
|||
use pk_macros::pk_model;
|
||||
|
||||
use crate::PrivacyLevel;
|
||||
use chrono::NaiveDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::PrivacyLevel;
|
||||
|
||||
// todo: fix this
|
||||
pub type SystemId = i32;
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue