Skip to main content

dinio_sdk/
auth.rs

1//! Credentials and secrets. A secret never appears in `Debug` output and is never
2//! serialized; reading it takes an explicit `expose()`.
3
4use std::fmt;
5
6use serde::Deserialize;
7
8/// Prefix of a zone-scoped key (`../core/src/keys.ts`).
9const ZONE_KEY_PREFIX: &str = "dz_";
10/// Prefix of a personal key (`../core/src/keys.ts`).
11const USER_KEY_PREFIX: &str = "du_";
12
13/// A secret string (API key). Deserializable (Core returns a new key once, when it issues
14/// it), but deliberately not `Serialize`, so it cannot leak into JSON output by accident.
15#[derive(Clone, PartialEq, Eq, Deserialize)]
16#[serde(transparent)]
17pub struct Secret(String);
18
19impl Secret {
20    /// Wraps a secret value.
21    pub fn new(value: impl Into<String>) -> Self {
22        Self(value.into())
23    }
24
25    /// The secret in plain text. Named so that every place that reveals it is easy to find.
26    pub fn expose(&self) -> &str {
27        &self.0
28    }
29}
30
31impl fmt::Debug for Secret {
32    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33        f.write_str("<redacted>")
34    }
35}
36
37/// How the client authenticates.
38#[derive(Debug, Clone)]
39pub enum Credentials {
40    /// A Core-issued API key, personal (`du_…`) or zone-scoped (`dz_…`), sent as
41    /// `Authorization: Bearer <key>`. Core tells the two apart by the prefix.
42    ApiKey(Secret),
43}
44
45/// Which kind of API key a key is, read from its prefix. Core decides by the same prefix,
46/// so this is known without a request.
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
48pub enum ApiKeyKind {
49    /// `dz_…`: full access to exactly one zone, no user behind it. Issued by
50    /// [`Zones::create_api_key`](crate::api::Zones::create_api_key).
51    Zone,
52    /// `du_…`: the access of the user it belongs to. Issued by
53    /// [`Users::create_api_key`](crate::api::Users::create_api_key).
54    Personal,
55}
56
57impl ApiKeyKind {
58    /// The kind of `key`, or `None` if it has neither prefix. Core treats such a token as a
59    /// JWT, so a key without a prefix (e.g. one from before Core issued keys) gets
60    /// [`ErrorCode::InvalidToken`](crate::ErrorCode::InvalidToken).
61    ///
62    /// # Examples
63    ///
64    /// ```
65    /// use dinio_sdk::ApiKeyKind;
66    ///
67    /// assert_eq!(ApiKeyKind::of("dz_abc"), Some(ApiKeyKind::Zone));
68    /// assert_eq!(ApiKeyKind::of("du_abc"), Some(ApiKeyKind::Personal));
69    /// assert_eq!(ApiKeyKind::of("dinio_abc"), None);
70    /// ```
71    pub fn of(key: &str) -> Option<Self> {
72        if key.starts_with(ZONE_KEY_PREFIX) {
73            Some(Self::Zone)
74        } else if key.starts_with(USER_KEY_PREFIX) {
75            Some(Self::Personal)
76        } else {
77            None
78        }
79    }
80}
81
82impl fmt::Display for ApiKeyKind {
83    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84        f.write_str(match self {
85            Self::Zone => "zone",
86            Self::Personal => "personal",
87        })
88    }
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn debug_redacts_key() {
97        let shown = format!("{:?}", Credentials::ApiKey(Secret::new("secret-key")));
98        assert!(!shown.contains("secret-key"));
99    }
100}