authly_client/
access_control.rsuse std::{future::Future, pin::Pin, sync::Arc};
use authly_common::{
id::{AttrId, EntityId, Id128DynamicArrayConv},
proto::service::{self as proto, authly_service_client::AuthlyServiceClient},
service::{NamespacePropertyMapping, NamespacedPropertyAttribute},
};
use fnv::FnvHashSet;
use http::header::AUTHORIZATION;
use tonic::{transport::Channel, Request};
use tracing::debug;
use crate::{error, id_codec_error, token::AccessToken, Client, Error};
pub trait AccessControl {
fn access_control_request(&self) -> AccessControlRequestBuilder<'_>;
fn evaluate(
&self,
builder: AccessControlRequestBuilder<'_>,
) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>>;
}
pub struct AccessControlRequestBuilder<'c> {
access_control: &'c (dyn AccessControl + Send + Sync),
property_mapping: Arc<NamespacePropertyMapping>,
access_token: Option<Arc<AccessToken>>,
resource_attributes: FnvHashSet<AttrId>,
peer_entity_ids: FnvHashSet<EntityId>,
}
impl<'c> AccessControlRequestBuilder<'c> {
pub fn new(
access_control: &'c (dyn AccessControl + Send + Sync),
property_mapping: Arc<NamespacePropertyMapping>,
) -> Self {
Self {
access_control,
property_mapping,
access_token: None,
resource_attributes: Default::default(),
peer_entity_ids: Default::default(),
}
}
pub fn resource_attribute(
mut self,
attr: impl NamespacedPropertyAttribute,
) -> Result<Self, Error> {
let attr_id = self.property_mapping.attribute_id(&attr).ok_or_else(|| {
debug!(
"invalid namespace/property/attribute label: {}/{}/{}",
attr.namespace(),
attr.property(),
attr.attribute(),
);
Error::InvalidPropertyAttributeLabel
})?;
self.resource_attributes.insert(attr_id);
Ok(self)
}
pub fn access_token(mut self, token: Arc<AccessToken>) -> Self {
self.access_token = Some(token);
self
}
pub fn peer_entity_id(mut self, entity_id: EntityId) -> Self {
self.peer_entity_ids.insert(entity_id);
self
}
pub fn resource_attributes(&self) -> impl Iterator<Item = AttrId> + use<'_> {
self.resource_attributes.iter().copied()
}
pub async fn enforce(self) -> Result<(), Error> {
if self.access_control.evaluate(self).await? {
Ok(())
} else {
Err(Error::AccessDenied)
}
}
pub async fn evaluate(self) -> Result<bool, Error> {
self.access_control.evaluate(self).await
}
}
pub(crate) async fn get_resource_property_mapping(
mut service: AuthlyServiceClient<Channel>,
) -> Result<Arc<NamespacePropertyMapping>, Error> {
let response = service
.get_resource_property_mappings(proto::Empty::default())
.await
.map_err(error::tonic)?;
let mut property_mapping = NamespacePropertyMapping::default();
for namespace in response.into_inner().namespaces {
let ns = property_mapping.namespace_mut(namespace.label);
for property in namespace.properties {
let ns_prop = ns.property_mut(property.label);
for attribute in property.attributes {
ns_prop.put(
attribute.label,
AttrId::try_from_bytes_dynamic(&attribute.obj_id).ok_or_else(id_codec_error)?,
);
}
}
}
Ok(Arc::new(property_mapping))
}
impl AccessControl for Client {
fn access_control_request(&self) -> AccessControlRequestBuilder<'_> {
AccessControlRequestBuilder::new(self, self.state.resource_property_mapping.load_full())
}
fn evaluate(
&self,
builder: AccessControlRequestBuilder<'_>,
) -> Pin<Box<dyn Future<Output = Result<bool, Error>> + Send + '_>> {
Box::pin(async move {
let mut request = Request::new(proto::AccessControlRequest {
resource_attributes: builder
.resource_attributes
.into_iter()
.map(|attr| attr.to_array_dynamic().to_vec())
.collect(),
peer_entity_attributes: vec![],
peer_entity_ids: builder
.peer_entity_ids
.into_iter()
.map(|eid| eid.to_array_dynamic().to_vec())
.collect(),
});
if let Some(access_token) = builder.access_token {
request.metadata_mut().append(
AUTHORIZATION.as_str(),
format!("Bearer {}", access_token.token)
.parse()
.map_err(error::unclassified)?,
);
}
let access_control_response = self
.current_service()
.access_control(request)
.await
.map_err(error::tonic)?
.into_inner();
Ok(access_control_response.value > 0)
})
}
}