mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 00:47:53 +00:00
simplify
This commit is contained in:
1 parent
fe0610b779
commit
b3d281a7f5
5 files changed
+38
-72
No files matched your search
Generated
+1
@@ -11563,6 +11563,7 @@ dependencies = [
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"artcraft_api_defs",
|
||||
"artcraft_api_keys",
|
||||
"artcraft_router",
|
||||
"async-stripe 0.26.0",
|
||||
"async-trait",
|
||||
|
||||
@@ -36,6 +36,7 @@ actix_artcraft.workspace = true
|
||||
actix_cors_configs = { path = "../../../lib/actix_cors_configs" }
|
||||
actix_helpers.workspace = true
|
||||
artcraft_api_defs.workspace = true
|
||||
artcraft_api_keys.workspace = true
|
||||
artcraft_router.workspace = true
|
||||
bootstrap.workspace = true
|
||||
bucket_paths.workspace = true
|
||||
|
||||
+4
-2
@@ -75,7 +75,7 @@ pub async fn create_api_key_handler(
|
||||
ip_address: &ip_address,
|
||||
name: &name,
|
||||
maybe_description: maybe_description.as_deref(),
|
||||
api_key: &api_key,
|
||||
api_key: api_key.as_str(),
|
||||
mysql_executor: &mut *conn,
|
||||
phantom: PhantomData,
|
||||
}).await.map_err(|err| {
|
||||
@@ -86,6 +86,8 @@ pub async fn create_api_key_handler(
|
||||
Ok(Json(CreateApiKeySuccessResponse {
|
||||
success: true,
|
||||
api_key_token: token,
|
||||
api_key,
|
||||
// Return the full secret value (never the redacted Debug/Display form). This is the only
|
||||
// time it is ever exposed.
|
||||
api_key: api_key.as_str().to_string(),
|
||||
}))
|
||||
}
|
||||
+18
-61
@@ -1,74 +1,23 @@
|
||||
use actix_http::header::{HeaderMap, HeaderName};
|
||||
use actix_web::HttpRequest;
|
||||
|
||||
/// `Authorization: Bearer <api_key>` — the widely-accepted style for most APIs.
|
||||
const BEARER_SCHEME: &str = "bearer";
|
||||
|
||||
/// `Authorization: Key <api_key>` — legacy GitHub-style / Fal-style.
|
||||
const KEY_SCHEME: &str = "key";
|
||||
|
||||
use artcraft_api_keys::ArtcraftApiKey;
|
||||
|
||||
const AUTHORIZATION_HEADER_NAME: HeaderName = HeaderName::from_static("authorization");
|
||||
|
||||
/// Extract an API key from a request's `Authorization` header.
|
||||
/// Extract an [`ArtcraftApiKey`] from a request's `Authorization` header.
|
||||
///
|
||||
/// The header name is matched case-insensitively (HTTP header names always are),
|
||||
/// and the value is accepted in three forms:
|
||||
///
|
||||
/// - `Authorization: Bearer <api_key>` (widely-accepted style for most APIs)
|
||||
/// - `Authorization: Key <api_key>` (legacy GitHub-style / Fal-style)
|
||||
/// - `Authorization: <api_key>` (AWS-style — the bare key, no scheme)
|
||||
///
|
||||
/// The scheme keyword is matched case-insensitively. Returns `None` if the
|
||||
/// header is absent, unreadable (non-ASCII bytes), empty, carries an
|
||||
/// unsupported scheme (e.g. `Basic ...`), or names a scheme with no credential.
|
||||
pub fn get_authorization_header_api_key(http_request: &HttpRequest) -> Option<String> {
|
||||
/// The header name is matched case-insensitively (HTTP header names always are). The value parsing
|
||||
/// — the `Bearer <api_key>`, `Key <api_key>`, and bare `<api_key>` forms — lives in the
|
||||
/// [`artcraft_api_keys`] crate. Returns `None` if the header is absent, unreadable (non-ASCII
|
||||
/// bytes), or does not contain a usable key.
|
||||
pub fn get_authorization_header_api_key(http_request: &HttpRequest) -> Option<ArtcraftApiKey> {
|
||||
let header_map: &HeaderMap = http_request.headers();
|
||||
let header_value = header_map.get(AUTHORIZATION_HEADER_NAME)?
|
||||
.to_str()
|
||||
.ok()?;
|
||||
|
||||
parse_authorization_header_api_key(header_value)
|
||||
}
|
||||
|
||||
fn parse_authorization_header_api_key(header_value: &str) -> Option<String> {
|
||||
let trimmed = header_value.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
match trimmed.split_once(char::is_whitespace) {
|
||||
// A scheme word followed by a value: "Bearer <key>" / "Key <key>".
|
||||
Some((scheme, rest)) => {
|
||||
if is_supported_scheme(scheme) {
|
||||
let key = rest.trim();
|
||||
if key.is_empty() {
|
||||
// A supported scheme with no credential.
|
||||
None
|
||||
} else {
|
||||
Some(key.to_string())
|
||||
}
|
||||
} else {
|
||||
// An unsupported scheme (e.g. "Basic ..."). A bare API key never
|
||||
// contains whitespace, so a multi-token value we don't recognize is
|
||||
// not a usable key.
|
||||
None
|
||||
}
|
||||
}
|
||||
// No whitespace: AWS-style bare API key ("<api_key>") — unless the value is
|
||||
// just a bare scheme keyword (a malformed header carrying no credential).
|
||||
None => {
|
||||
if is_supported_scheme(trimmed) {
|
||||
None
|
||||
} else {
|
||||
Some(trimmed.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_supported_scheme(word: &str) -> bool {
|
||||
word.eq_ignore_ascii_case(BEARER_SCHEME) || word.eq_ignore_ascii_case(KEY_SCHEME)
|
||||
ArtcraftApiKey::parse_from_authorization_header_value(header_value)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -111,7 +60,7 @@ mod tests {
|
||||
.insert_header((header_name, value.as_str()))
|
||||
.to_http_request();
|
||||
assert_eq!(
|
||||
get_authorization_header_api_key(&http_request),
|
||||
get_authorization_header_api_key(&http_request).map(|key| key.as_str().to_string()),
|
||||
Some(SAMPLE_KEY.to_string()),
|
||||
"failed for header name {header_name:?}");
|
||||
}
|
||||
@@ -178,10 +127,18 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Thin string-returning adapters over the real implementation so the original test cases above
|
||||
// can stay unchanged after the parser moved into the `artcraft_api_keys` crate.
|
||||
|
||||
fn parse_authorization_header_api_key(header_value: &str) -> Option<String> {
|
||||
ArtcraftApiKey::parse_from_authorization_header_value(header_value)
|
||||
.map(|key| key.as_str().to_string())
|
||||
}
|
||||
|
||||
fn api_key_for_authorization_header(value: &str) -> Option<String> {
|
||||
let http_request = TestRequest::default()
|
||||
.insert_header(("Authorization", value))
|
||||
.to_http_request();
|
||||
get_authorization_header_api_key(&http_request)
|
||||
get_authorization_header_api_key(&http_request).map(|key| key.as_str().to_string())
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
use rand::Rng;
|
||||
|
||||
use artcraft_api_keys::ArtcraftApiKey;
|
||||
|
||||
/// All Artcraft API keys begin with this prefix. It counts toward the total key length.
|
||||
///
|
||||
/// The underscores in this literal are the ONLY underscores permitted in a key — the random
|
||||
@@ -27,7 +29,10 @@ const ENTROPY_CHAR_COUNT: usize = 40;
|
||||
///
|
||||
/// Infallible: it draws from the thread-local CSPRNG (`rand::rng()`, a ChaCha-based, OS-seeded
|
||||
/// generator), which never fails.
|
||||
pub fn generate_api_key() -> String {
|
||||
///
|
||||
/// The full secret lives inside the returned [`ArtcraftApiKey`]; use `as_str()` to obtain it for
|
||||
/// storage (`Debug`/`Display` deliberately redact it).
|
||||
pub fn generate_api_key() -> ArtcraftApiKey {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
let mut key = String::with_capacity(API_KEY_PREFIX.len() + ENTROPY_CHAR_COUNT);
|
||||
@@ -38,7 +43,7 @@ pub fn generate_api_key() -> String {
|
||||
key.push(CROCKFORD_LOWERCASE_CHARSET[index] as char);
|
||||
}
|
||||
|
||||
key
|
||||
ArtcraftApiKey(key)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -49,19 +54,19 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn starts_with_prefix() {
|
||||
assert!(generate_api_key().starts_with("artcraft_api_"));
|
||||
assert!(generate_api_key().starts_with(API_KEY_PREFIX));
|
||||
assert!(generate_api_key().as_str().starts_with("artcraft_api_"));
|
||||
assert!(generate_api_key().as_str().starts_with(API_KEY_PREFIX));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_expected_total_length() {
|
||||
assert_eq!(generate_api_key().len(), API_KEY_PREFIX.len() + ENTROPY_CHAR_COUNT);
|
||||
assert_eq!(generate_api_key().as_str().len(), API_KEY_PREFIX.len() + ENTROPY_CHAR_COUNT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_suffix_is_crockford_lowercase() {
|
||||
let key = generate_api_key();
|
||||
let suffix = key.strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
|
||||
assert_eq!(suffix.len(), ENTROPY_CHAR_COUNT);
|
||||
for byte in suffix.bytes() {
|
||||
@@ -79,21 +84,21 @@ mod tests {
|
||||
fn forbidden_characters_are_absent() {
|
||||
let key = generate_api_key();
|
||||
for forbidden in ['.', '+', '/'] {
|
||||
assert!(!key.contains(forbidden), "key must not contain {:?}", forbidden);
|
||||
assert!(!key.as_str().contains(forbidden), "key must not contain {:?}", forbidden);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn underscores_only_appear_in_the_prefix() {
|
||||
let key = generate_api_key();
|
||||
let suffix = key.strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
assert!(!suffix.contains('_'), "the random body must not contain underscores");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entropy_suffix_is_alphanumeric() {
|
||||
let key = generate_api_key();
|
||||
let suffix = key.strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
let suffix = key.as_str().strip_prefix(API_KEY_PREFIX).expect("key must start with the prefix");
|
||||
assert!(suffix.chars().all(|c| c.is_ascii_alphanumeric()));
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user