authly_client/
access_control.rs

1//! Access control functionality.
2
3use std::{future::Future, pin::Pin, sync::Arc};
4
5use authly_common::{
6    id::{AttrId, EntityId, Id128DynamicArrayConv},
7    proto::service::{self as proto},
8    service::{NamespacePropertyMapping, NamespacedPropertyAttribute},
9};
10use fnv::FnvHashSet;
11use http::header::AUTHORIZATION;
12use tonic::Request;
13use tracing::debug;
14
15use crate::{error, id_codec_error, token::AccessToken, Client, Error};
16
17/// Trait for initiating an access control request
18pub trait AccessControl {
19    /// Make a new access control request, returning a builder for building it.
20    fn access_control_request(&self) -> AccessControlRequestBuilder<'_>;
21
22    /// Evaluate the access control request.
23    fn evaluate(
24        &self,
25        builder: AccessControlRequestBuilder<'_>,
26    ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>>;
27}
28
29/// A builder for making an access control request.
30///
31// TODO: Include peer service(s) in the access control request.
32// For that to work locally, there are two options:
33// 1. The service verifies each incoming peer with a call to authly, to retrieve entity attributes.
34// 2. The service is conscious about its mesh, and is allowed to keep an in-memory map of incoming service entity attributes.
35pub struct AccessControlRequestBuilder<'c> {
36    access_control: &'c (dyn AccessControl + Send + Sync),
37    property_mapping: Arc<NamespacePropertyMapping>,
38    access_token: Option<Arc<AccessToken>>,
39    resource_attributes: FnvHashSet<AttrId>,
40    peer_entity_ids: FnvHashSet<EntityId>,
41}
42
43impl<'c> AccessControlRequestBuilder<'c> {
44    /// Create a new builder with the given [AccessControl] backend.
45    pub fn new(
46        access_control: &'c (dyn AccessControl + Send + Sync),
47        property_mapping: Arc<NamespacePropertyMapping>,
48    ) -> Self {
49        Self {
50            access_control,
51            property_mapping,
52            access_token: None,
53            resource_attributes: Default::default(),
54            peer_entity_ids: Default::default(),
55        }
56    }
57
58    /// Define a labelled resource attribute to be included in the access control request.
59    ///
60    /// The property and attribute labels should be available to this service through authly document manifests.
61    ///
62    /// # Examples
63    ///
64    /// ```rust
65    /// # use authly_client::*;
66    /// # async fn test() -> anyhow::Result<()> {
67    /// // note: Client is not properly built here.
68    /// let client = Client::builder().connect().await?;
69    ///
70    /// client.access_control_request()
71    ///     .resource_attribute(("my_namespace", "type", "orders"))?
72    ///     .resource_attribute(("my_namespace", "action", "read"))?
73    ///     .evaluate()
74    ///     .await?;
75    ///
76    /// # Ok(())
77    /// # }
78    /// ```
79    pub fn resource_attribute(
80        mut self,
81        attr: impl NamespacedPropertyAttribute,
82    ) -> Result<Self, Error> {
83        let attr_id = self.property_mapping.attribute_id(&attr).ok_or_else(|| {
84            debug!(
85                "invalid namespace/property/attribute label: {}/{}/{}",
86                attr.namespace(),
87                attr.property(),
88                attr.attribute(),
89            );
90            Error::InvalidPropertyAttributeLabel
91        })?;
92
93        self.resource_attributes.insert(attr_id);
94        Ok(self)
95    }
96
97    /// Include an access token in the request.
98    ///
99    /// The access token is used as subject properties in the access control request.
100    pub fn access_token(mut self, token: Arc<AccessToken>) -> Self {
101        self.access_token = Some(token);
102        self
103    }
104
105    /// Add a peer entity ID, which represents a client acting as a subject in the access control request.
106    pub fn peer_entity_id(mut self, entity_id: EntityId) -> Self {
107        self.peer_entity_ids.insert(entity_id);
108        self
109    }
110
111    /// Get an iterator over the current resource attributes.
112    pub fn resource_attributes(&self) -> impl Iterator<Item = AttrId> + use<'_> {
113        self.resource_attributes.iter().copied()
114    }
115
116    /// Enforce the access control request.
117    pub async fn enforce(self) -> Result<(), Error> {
118        if self.access_control.evaluate(self).await? {
119            Ok(())
120        } else {
121            Err(Error::AccessDenied)
122        }
123    }
124
125    /// Evaluate the access control request.
126    ///
127    /// The return value represents whether access was granted.
128    pub async fn evaluate(self) -> Result<bool, Error> {
129        self.access_control.evaluate(self).await
130    }
131}
132
133pub(crate) fn get_resource_property_mapping(
134    proto_namespaces: Vec<proto::PropertyMappingNamespace>,
135) -> Result<Arc<NamespacePropertyMapping>, Error> {
136    let mut property_mapping = NamespacePropertyMapping::default();
137
138    for namespace in proto_namespaces {
139        let ns = property_mapping.namespace_mut(namespace.label);
140
141        for property in namespace.properties {
142            let ns_prop = ns.property_mut(property.label);
143
144            for attribute in property.attributes {
145                ns_prop.put(
146                    attribute.label,
147                    AttrId::try_from_bytes_dynamic(&attribute.obj_id).ok_or_else(id_codec_error)?,
148                );
149            }
150        }
151    }
152
153    Ok(Arc::new(property_mapping))
154}
155
156impl AccessControl for Client {
157    fn access_control_request(&self) -> AccessControlRequestBuilder<'_> {
158        AccessControlRequestBuilder::new(
159            self,
160            self.state
161                .configuration
162                .load()
163                .resource_property_mapping
164                .clone(),
165        )
166    }
167
168    fn evaluate(
169        &self,
170        builder: AccessControlRequestBuilder<'_>,
171    ) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>> {
172        Box::pin(async move {
173            let mut request = Request::new(proto::AccessControlRequest {
174                resource_attributes: builder
175                    .resource_attributes
176                    .into_iter()
177                    .map(|attr| attr.to_array_dynamic().to_vec())
178                    .collect(),
179                // Peer entity attributes are currently not known to the service:
180                peer_entity_attributes: vec![],
181                peer_entity_ids: builder
182                    .peer_entity_ids
183                    .into_iter()
184                    .map(|eid| eid.to_array_dynamic().to_vec())
185                    .collect(),
186            });
187            if let Some(access_token) = builder.access_token {
188                request.metadata_mut().append(
189                    AUTHORIZATION.as_str(),
190                    format!("Bearer {}", access_token.token)
191                        .parse()
192                        .map_err(error::unclassified)?,
193                );
194            }
195
196            let access_control_response = self
197                .current_service()
198                .access_control(request)
199                .await
200                .map_err(error::tonic)?
201                .into_inner();
202
203            Ok(access_control_response.value > 0)
204        })
205    }
206}