1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
// SPDX-FileCopyrightText: OpenTalk GmbH <mail@opentalk.eu>
//
// SPDX-License-Identifier: EUPL-1.2

//! Abstract resource authz types.
//!
//! Kustos limits supported resources to be identified by a valid URL path.
//! As a resource can be identified with a URL in general we talk here about reduced URI which consists only of the path part.
//! 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.
//! It is all local to this deployment. Furthermore, we enforce scheme independent thus this is also not part of our reduced URI.
//! 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.
//! Supporting relative resources (e.g. entities with a primary key consisting of a multiple foreign keys) are an open issue currently.
//!
//! All resources need to implement the [`Resource`] trait
use std::{fmt::Display, num::ParseIntError, ops::Deref, str::FromStr};

use snafu::Snafu;

/// The error is returned when a resource failed to be parsed.
///
/// Currently supported types are only uuids and integers, all other use the fallback Other variant.
#[derive(Debug, Snafu)]
pub enum ResourceParseError {
    #[snafu(display("Invalid UUID: {source}"), context(false))]
    Uuid {
        #[snafu(source(from(uuid::Error, Box::new)))]
        source: Box<uuid::Error>,
    },

    #[snafu(display("Invalid integer: {source}"), context(false))]
    ParseInt {
        #[snafu(source(from(ParseIntError, Box::new)))]
        source: Box<ParseIntError>,
    },

    #[snafu(whatever)]
    Other {
        message: String,

        #[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, Some)))]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
}

/// This trait is used to allow the retrieval of resource reduced URL prefixes as well as retrieving
/// the reduced URL
pub trait Resource: Sized + Display + KustosFromStr {
    /// URI prefix of the ID of this resource
    ///
    /// # Example
    ///
    /// * `/rooms/`
    /// * `/users/`
    const PREFIX: &'static str;

    /// Returns path part of the URL to access this specific resource
    fn resource_id(&self) -> ResourceId {
        // Assert correct usage of this trait in debug builds only.
        debug_assert!(Self::PREFIX.starts_with('/') && Self::PREFIX.ends_with('/'));

        ResourceId(format!("{}{}", Self::PREFIX, self))
    }
}

pub trait KustosFromStr: Sized {
    fn kustos_from_str(s: &str) -> Result<Self, ResourceParseError>;
}

impl<T: Resource + FromStr<Err = E>, E: Into<ResourceParseError>> KustosFromStr for T {
    fn kustos_from_str(s: &str) -> Result<Self, ResourceParseError> {
        Self::from_str(s).map_err(Into::into)
    }
}

/// Represents a accessible resource
///
/// Use this to represent the URL without scheme and authority to represent the respective resource.
///
/// # Example
///
/// * `/users/1` to represent the resource of user with id = 1
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct ResourceId(pub(crate) String);

impl ResourceId {
    pub fn into_inner(self) -> String {
        self.0
    }

    pub fn with_suffix<S>(&self, suffix: S) -> ResourceId
    where
        S: AsRef<str>,
    {
        let mut inner = self.0.clone();
        inner.push_str(suffix.as_ref());
        ResourceId(inner)
    }

    pub fn as_str(&self) -> &str {
        self.0.as_str()
    }
}

impl Display for ResourceId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.as_str())
    }
}

impl AsRef<str> for ResourceId {
    fn as_ref(&self) -> &str {
        self.as_str()
    }
}

impl Deref for ResourceId {
    type Target = String;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl From<&str> for ResourceId {
    fn from(s: &str) -> Self {
        Self(s.to_string())
    }
}

impl From<String> for ResourceId {
    fn from(s: String) -> Self {
        Self(s)
    }
}
/// Response from fetching all implicit accessible resources.
///
/// If a subject has access to a wildcard `/*` or `/resourceName/*` [`AccessibleResources::All`]
/// should be returned, else a List of all accessible resources via [`AccessibleResources::List`]
pub enum AccessibleResources<T: Resource> {
    List(Vec<T>),
    All,
}

#[cfg(test)]
mod tests {
    use pretty_assertions::assert_eq;

    use super::*;

    struct ResourceX(uuid::Uuid);

    impl Display for ResourceX {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            self.0.as_hyphenated().fmt(f)
        }
    }

    impl Resource for ResourceX {
        const PREFIX: &'static str = "/resources/";
    }

    impl FromStr for ResourceX {
        type Err = ResourceParseError;
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            s.parse().map(Self).map_err(Into::into)
        }
    }

    #[test]
    fn test_a() {
        let x = ResourceId("/resources/00000000-0000-0000-0000-000000000000".to_string());

        let target: ResourceX = x.strip_prefix(ResourceX::PREFIX).unwrap().parse().unwrap();
        assert_eq!(target.0, uuid::Uuid::nil())
    }
}