kustos_shared/
resource.rs

1// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
2//
3// SPDX-License-Identifier: EUPL-1.2
4
5//! Abstract resource authz types.
6//!
7//! Kustos limits supported resources to be identified by a valid URL path.
8//! As a resource can be identified with a URL in general we talk here about reduced URI which consists only of the path part.
9//! Reason is that for different authorities you can have different permissions. Thus in the context of authz a resource without the authority part makes more sense.
10//! It is all local to this deployment. Furthermore, we enforce scheme independent thus this is also not part of our reduced URI.
11//! The URIs used with Kustos are currently limited to the following format `/{resourceName}/{resourceId}` where resourceName is static for all instances of that particular resource.
12//! Supporting relative resources (e.g. entities with a primary key consisting of a multiple foreign keys) are an open issue currently.
13//!
14//! All resources need to implement the [`Resource`] trait
15use std::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr};
16
17use snafu::Snafu;
18
19/// The error is returned when a resource failed to be parsed.
20///
21/// Currently supported types are only uuids and integers, all other use the fallback Other variant.
22#[derive(Debug, Snafu)]
23pub enum ResourceParseError {
24    #[snafu(display("Invalid UUID: {source}"), context(false))]
25    Uuid {
26        #[snafu(source(from(uuid::Error, Box::new)))]
27        source: Box<uuid::Error>,
28    },
29
30    #[snafu(display("Invalid integer: {source}"), context(false))]
31    ParseInt {
32        #[snafu(source(from(ParseIntError, Box::new)))]
33        source: Box<ParseIntError>,
34    },
35
36    #[snafu(whatever)]
37    Other {
38        message: String,
39
40        #[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, Some)))]
41        source: Option<Box<dyn std::error::Error + Send + Sync>>,
42    },
43}
44
45/// This trait is used to allow the retrieval of resource reduced URL prefixes as well as retrieving
46/// the reduced URL
47pub trait Resource: Sized + Display + KustosFromStr {
48    /// URI prefix of the ID of this resource
49    ///
50    /// # Example
51    ///
52    /// * `/rooms/`
53    /// * `/users/`
54    const PREFIX: &'static str;
55
56    /// Returns path part of the URL to access this specific resource
57    fn resource_id(&self) -> ResourceId {
58        // Assert correct usage of this trait in debug builds only.
59        debug_assert!(Self::PREFIX.starts_with('/') && Self::PREFIX.ends_with('/'));
60
61        ResourceId(format!("{}{}", Self::PREFIX, self))
62    }
63}
64
65pub trait KustosFromStr: Sized {
66    fn kustos_from_str(s: &str) -> Result<Self, ResourceParseError>;
67}
68
69impl<T: Resource + FromStr<Err = E>, E: Into<ResourceParseError>> KustosFromStr for T {
70    fn kustos_from_str(s: &str) -> Result<Self, ResourceParseError> {
71        Self::from_str(s).map_err(Into::into)
72    }
73}
74
75/// Represents a accessible resource
76///
77/// Use this to represent the URL without scheme and authority to represent the respective resource.
78///
79/// # Example
80///
81/// * `/users/1` to represent the resource of user with id = 1
82#[derive(Debug, Clone, Hash, PartialEq, Eq)]
83pub struct ResourceId(pub(crate) String);
84
85impl ResourceId {
86    pub fn into_inner(self) -> String {
87        self.0
88    }
89
90    pub fn with_suffix<S>(&self, suffix: S) -> ResourceId
91    where
92        S: AsRef<str>,
93    {
94        let mut inner = self.0.clone();
95        inner.push_str(suffix.as_ref());
96        ResourceId(inner)
97    }
98
99    pub fn as_str(&self) -> &str {
100        self.0.as_str()
101    }
102}
103
104impl Display for ResourceId {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "{}", self.as_str())
107    }
108}
109
110impl AsRef<str> for ResourceId {
111    fn as_ref(&self) -> &str {
112        self.as_str()
113    }
114}
115
116impl Deref for ResourceId {
117    type Target = String;
118
119    fn deref(&self) -> &Self::Target {
120        &self.0
121    }
122}
123
124impl From<&str> for ResourceId {
125    fn from(s: &str) -> Self {
126        Self(s.to_string())
127    }
128}
129
130impl From<String> for ResourceId {
131    fn from(s: String) -> Self {
132        Self(s)
133    }
134}
135/// Response from fetching all implicit accessible resources.
136///
137/// If a subject has access to a wildcard `/*` or `/resourceName/*` [`AccessibleResources::All`]
138/// should be returned, else a List of all accessible resources via [`AccessibleResources::List`]
139#[derive(Debug)]
140pub enum AccessibleResources<T: Resource> {
141    List(Vec<T>),
142    All,
143}
144
145#[cfg(test)]
146mod tests {
147    use pretty_assertions::assert_eq;
148
149    use super::*;
150
151    struct ResourceX(uuid::Uuid);
152
153    impl Display for ResourceX {
154        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155            self.0.as_hyphenated().fmt(f)
156        }
157    }
158
159    impl Resource for ResourceX {
160        const PREFIX: &'static str = "/resources/";
161    }
162
163    impl FromStr for ResourceX {
164        type Err = ResourceParseError;
165        fn from_str(s: &str) -> Result<Self, Self::Err> {
166            s.parse().map(Self).map_err(Into::into)
167        }
168    }
169
170    #[test]
171    fn test_a() {
172        let x = ResourceId("/resources/00000000-0000-0000-0000-000000000000".to_string());
173
174        let target: ResourceX = x.strip_prefix(ResourceX::PREFIX).unwrap().parse().unwrap();
175        assert_eq!(target.0, uuid::Uuid::nil())
176    }
177}