kustos_shared/resource.rs
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 177
// 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`]
#[derive(Debug)]
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())
}
}