#![allow(dead_code)]
use std::borrow::Cow;
use std::future::Future;
use std::pin::Pin;
#[cfg(feature = "rest")]
use std::sync::Arc;
use crate::client::Client;
use crate::control::SchemasClient;
#[cfg(feature = "rest")]
use crate::transport::{TransportCheckRequest, TransportClient, TransportWriteRequest};
use crate::types::{ConsistencyToken, Context, Decision, Relationship};
use crate::{AccessDenied, Error};
use futures::Stream;
#[derive(Clone)]
pub struct VaultClient {
client: Client,
organization_id: String,
vault_id: String,
}
impl VaultClient {
pub(crate) fn new(client: Client, organization_id: String, vault_id: String) -> Self {
Self {
client,
organization_id,
vault_id,
}
}
pub fn organization_id(&self) -> &str {
&self.organization_id
}
pub fn vault_id(&self) -> &str {
&self.vault_id
}
pub fn client(&self) -> &Client {
&self.client
}
#[cfg(feature = "rest")]
pub(super) fn transport(&self) -> Option<&std::sync::Arc<dyn TransportClient + Send + Sync>> {
self.client.transport()
}
pub fn check<'a>(
&self,
subject: impl Into<Cow<'a, str>>,
permission: impl Into<Cow<'a, str>>,
resource: impl Into<Cow<'a, str>>,
) -> CheckRequest<'a> {
CheckRequest {
vault: self.clone(),
subject: subject.into(),
permission: permission.into(),
resource: resource.into(),
context: None,
consistency: None,
}
}
pub fn check_batch<'a, I, S, P, R>(&self, checks: I) -> BatchCheckRequest<'a>
where
I: IntoIterator<Item = (S, P, R)>,
S: Into<Cow<'a, str>>,
P: Into<Cow<'a, str>>,
R: Into<Cow<'a, str>>,
{
let items: Vec<_> = checks
.into_iter()
.map(|(s, p, r)| BatchCheckItem {
subject: s.into(),
permission: p.into(),
resource: r.into(),
})
.collect();
BatchCheckRequest {
vault: self.clone(),
items,
context: None,
consistency: None,
}
}
pub fn relationships(&self) -> RelationshipsClient {
RelationshipsClient::new(self.clone())
}
pub fn resources(&self) -> ResourcesClient<'_> {
ResourcesClient::new(self)
}
pub fn subjects(&self) -> SubjectsClient<'_> {
SubjectsClient::new(self)
}
pub fn explain_permission(&self) -> ExplainPermissionRequest {
ExplainPermissionRequest::new(self.clone())
}
pub fn simulate(&self) -> super::simulate::SimulateBuilder {
super::simulate::SimulateBuilder::new(self.clone())
}
pub fn watch(&self) -> super::watch::WatchBuilder {
super::watch::WatchBuilder::new(self)
}
pub fn schemas(&self) -> SchemasClient {
SchemasClient::new(
self.client.clone(),
self.organization_id.clone(),
self.vault_id.clone(),
)
}
}
impl std::fmt::Debug for VaultClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("VaultClient")
.field("organization_id", &self.organization_id)
.field("vault_id", &self.vault_id)
.finish_non_exhaustive()
}
}
pub struct CheckRequest<'a> {
vault: VaultClient,
subject: Cow<'a, str>,
permission: Cow<'a, str>,
resource: Cow<'a, str>,
context: Option<Context>,
consistency: Option<ConsistencyToken>,
}
impl<'a> CheckRequest<'a> {
#[must_use]
pub fn with_context(mut self, context: Context) -> Self {
self.context = Some(context);
self
}
#[must_use]
pub fn at_least_as_fresh(mut self, token: ConsistencyToken) -> Self {
self.consistency = Some(token);
self
}
pub fn require(self) -> RequireCheckRequest<'a> {
RequireCheckRequest { inner: self }
}
pub async fn detailed(self) -> Result<Decision, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let request = TransportCheckRequest {
subject: self.subject.into_owned(),
permission: self.permission.into_owned(),
resource: self.resource.into_owned(),
context: self.context,
consistency: self.consistency,
trace: false,
};
let response = transport.check(request).await?;
return Ok(response.decision);
}
}
Ok(Decision::allowed())
}
async fn execute(self) -> Result<bool, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let request = TransportCheckRequest {
subject: self.subject.into_owned(),
permission: self.permission.into_owned(),
resource: self.resource.into_owned(),
context: self.context,
consistency: self.consistency,
trace: false,
};
let response = transport.check(request).await?;
return Ok(response.allowed);
}
}
let _ = (self.context, self.consistency);
Ok(true)
}
}
impl<'a> std::future::IntoFuture for CheckRequest<'a> {
type Output = Result<bool, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
pub struct RequireCheckRequest<'a> {
inner: CheckRequest<'a>,
}
impl<'a> RequireCheckRequest<'a> {
#[must_use]
pub fn with_context(mut self, context: Context) -> Self {
self.inner.context = Some(context);
self
}
#[must_use]
pub fn at_least_as_fresh(mut self, token: ConsistencyToken) -> Self {
self.inner.consistency = Some(token);
self
}
async fn execute(self) -> Result<(), AccessDenied> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.inner.vault.transport() {
let request = TransportCheckRequest {
subject: self.inner.subject.clone().into_owned(),
permission: self.inner.permission.clone().into_owned(),
resource: self.inner.resource.clone().into_owned(),
context: self.inner.context.clone(),
consistency: self.inner.consistency.clone(),
trace: false,
};
let response = transport.check(request).await.map_err(|_| {
AccessDenied::new(
self.inner.subject.clone().into_owned(),
self.inner.permission.clone().into_owned(),
self.inner.resource.clone().into_owned(),
)
})?;
if response.allowed {
return Ok(());
} else {
return Err(AccessDenied::new(
self.inner.subject.into_owned(),
self.inner.permission.into_owned(),
self.inner.resource.into_owned(),
));
}
}
}
Ok(())
}
}
impl<'a> std::future::IntoFuture for RequireCheckRequest<'a> {
type Output = Result<(), AccessDenied>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[derive(Debug, Clone)]
pub struct BatchCheckItem<'a> {
subject: Cow<'a, str>,
permission: Cow<'a, str>,
resource: Cow<'a, str>,
}
impl<'a> BatchCheckItem<'a> {
pub fn new(
subject: impl Into<Cow<'a, str>>,
permission: impl Into<Cow<'a, str>>,
resource: impl Into<Cow<'a, str>>,
) -> Self {
Self {
subject: subject.into(),
permission: permission.into(),
resource: resource.into(),
}
}
pub fn subject(&self) -> &str {
&self.subject
}
pub fn permission(&self) -> &str {
&self.permission
}
pub fn resource(&self) -> &str {
&self.resource
}
}
pub struct BatchCheckRequest<'a> {
vault: VaultClient,
items: Vec<BatchCheckItem<'a>>,
context: Option<Context>,
consistency: Option<ConsistencyToken>,
}
impl<'a> BatchCheckRequest<'a> {
#[must_use]
pub fn with_context(mut self, context: Context) -> Self {
self.context = Some(context);
self
}
#[must_use]
pub fn at_least_as_fresh(mut self, token: ConsistencyToken) -> Self {
self.consistency = Some(token);
self
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
async fn execute(self) -> Result<Vec<bool>, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let requests: Vec<TransportCheckRequest> = self
.items
.iter()
.map(|item| TransportCheckRequest {
subject: item.subject.clone().into_owned(),
permission: item.permission.clone().into_owned(),
resource: item.resource.clone().into_owned(),
context: self.context.clone(),
consistency: self.consistency.clone(),
trace: false,
})
.collect();
let responses = transport.check_batch(requests).await?;
return Ok(responses.into_iter().map(|r| r.allowed).collect());
}
}
let _ = (self.context, self.consistency);
Ok(vec![true; self.items.len()])
}
}
impl<'a> std::future::IntoFuture for BatchCheckRequest<'a> {
type Output = Result<Vec<bool>, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[derive(Debug, Clone)]
pub struct BatchCheckResult {
pub results: Vec<bool>,
pub decisions: Option<Vec<Decision>>,
pub consistency_token: Option<ConsistencyToken>,
}
impl BatchCheckResult {
pub fn as_slice(&self) -> &[bool] {
&self.results
}
pub fn iter(&self) -> impl Iterator<Item = bool> + '_ {
self.results.iter().copied()
}
pub fn len(&self) -> usize {
self.results.len()
}
pub fn is_empty(&self) -> bool {
self.results.is_empty()
}
pub fn all_allowed(&self) -> bool {
self.results.iter().all(|&r| r)
}
pub fn any_allowed(&self) -> bool {
self.results.iter().any(|&r| r)
}
pub fn denied_indices(&self) -> Vec<usize> {
self.results
.iter()
.enumerate()
.filter_map(|(i, &allowed)| if !allowed { Some(i) } else { None })
.collect()
}
}
#[derive(Clone)]
pub struct RelationshipsClient {
vault: VaultClient,
}
impl RelationshipsClient {
pub(crate) fn new(vault: VaultClient) -> Self {
Self { vault }
}
pub fn write<'a>(&self, relationship: Relationship<'a>) -> WriteRelationshipRequest<'a> {
WriteRelationshipRequest {
client: self.clone(),
relationship,
}
}
pub fn write_batch<'a, I>(&self, relationships: I) -> WriteBatchRequest<'a>
where
I: IntoIterator<Item = Relationship<'a>>,
{
WriteBatchRequest {
client: self.clone(),
relationships: relationships.into_iter().collect(),
}
}
pub fn delete<'a>(&self, relationship: Relationship<'a>) -> DeleteRelationshipRequest<'a> {
DeleteRelationshipRequest {
client: self.clone(),
relationship,
}
}
pub fn list(&self) -> ListRelationshipsRequest {
ListRelationshipsRequest {
client: self.clone(),
resource: None,
relation: None,
subject: None,
limit: None,
cursor: None,
}
}
pub fn delete_where(&self) -> DeleteWhereBuilder {
DeleteWhereBuilder {
client: self.clone(),
resource: None,
relation: None,
subject: None,
}
}
}
impl std::fmt::Debug for RelationshipsClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RelationshipsClient")
.field("vault_id", &self.vault.vault_id)
.finish_non_exhaustive()
}
}
pub struct WriteRelationshipRequest<'a> {
client: RelationshipsClient,
relationship: Relationship<'a>,
}
impl<'a> WriteRelationshipRequest<'a> {
async fn execute(self) -> Result<ConsistencyToken, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.client.vault.transport() {
let request = TransportWriteRequest {
relationship: self.relationship.into_owned(),
idempotency_key: None,
};
let response = transport.write(request).await?;
return Ok(response.consistency_token);
}
}
Ok(ConsistencyToken::new(format!(
"token_{}",
uuid::Uuid::new_v4()
)))
}
}
impl<'a> std::future::IntoFuture for WriteRelationshipRequest<'a> {
type Output = Result<ConsistencyToken, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
pub struct WriteBatchRequest<'a> {
client: RelationshipsClient,
relationships: Vec<Relationship<'a>>,
}
impl<'a> WriteBatchRequest<'a> {
pub fn len(&self) -> usize {
self.relationships.len()
}
pub fn is_empty(&self) -> bool {
self.relationships.is_empty()
}
async fn execute(self) -> Result<ConsistencyToken, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.client.vault.transport() {
let requests: Vec<TransportWriteRequest> = self
.relationships
.into_iter()
.map(|r| TransportWriteRequest {
relationship: r.into_owned(),
idempotency_key: None,
})
.collect();
let response = transport.write_batch(requests).await?;
return Ok(response.consistency_token);
}
}
Ok(ConsistencyToken::new(format!(
"token_{}",
uuid::Uuid::new_v4()
)))
}
}
impl<'a> std::future::IntoFuture for WriteBatchRequest<'a> {
type Output = Result<ConsistencyToken, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
pub struct DeleteRelationshipRequest<'a> {
client: RelationshipsClient,
relationship: Relationship<'a>,
}
impl<'a> DeleteRelationshipRequest<'a> {
async fn execute(self) -> Result<(), Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.client.vault.transport() {
transport.delete(self.relationship.into_owned()).await?;
return Ok(());
}
}
Ok(())
}
}
impl<'a> std::future::IntoFuture for DeleteRelationshipRequest<'a> {
type Output = Result<(), Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
pub struct DeleteWhereBuilder {
client: RelationshipsClient,
resource: Option<String>,
relation: Option<String>,
subject: Option<String>,
}
impl DeleteWhereBuilder {
#[must_use]
pub fn resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
#[must_use]
pub fn relation(mut self, relation: impl Into<String>) -> Self {
self.relation = Some(relation.into());
self
}
#[must_use]
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
async fn execute(self) -> Result<DeleteWhereResult, Error> {
if self.resource.is_none() && self.relation.is_none() && self.subject.is_none() {
return Err(Error::configuration(
"delete_where requires at least one filter (resource, relation, or subject)",
));
}
#[cfg(feature = "rest")]
{
if let Some(transport) = self.client.vault.transport() {
let response = transport
.list_relationships(
self.resource.as_deref(),
self.relation.as_deref(),
self.subject.as_deref(),
None, None, )
.await?;
let mut deleted = 0;
for rel in response.relationships {
transport.delete(rel).await?;
deleted += 1;
}
return Ok(DeleteWhereResult {
deleted_count: deleted,
});
}
}
let _ = (self.resource, self.relation, self.subject);
Ok(DeleteWhereResult { deleted_count: 0 })
}
}
impl std::future::IntoFuture for DeleteWhereBuilder {
type Output = Result<DeleteWhereResult, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'static>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[derive(Debug, Clone, Copy)]
pub struct DeleteWhereResult {
pub deleted_count: u64,
}
impl DeleteWhereResult {
pub fn deleted_count(&self) -> u64 {
self.deleted_count
}
pub fn any_deleted(&self) -> bool {
self.deleted_count > 0
}
}
pub struct ListRelationshipsRequest {
client: RelationshipsClient,
resource: Option<String>,
relation: Option<String>,
subject: Option<String>,
limit: Option<usize>,
cursor: Option<String>,
}
impl ListRelationshipsRequest {
#[must_use]
pub fn resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
#[must_use]
pub fn relation(mut self, relation: impl Into<String>) -> Self {
self.relation = Some(relation.into());
self
}
#[must_use]
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
#[must_use]
pub fn limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
#[must_use]
pub fn cursor(mut self, cursor: impl Into<String>) -> Self {
self.cursor = Some(cursor.into());
self
}
async fn execute(self) -> Result<ListRelationshipsResponse, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.client.vault.transport() {
let response = transport
.list_relationships(
self.resource.as_deref(),
self.relation.as_deref(),
self.subject.as_deref(),
self.limit.map(|l| l as u32),
self.cursor.as_deref(),
)
.await?;
return Ok(ListRelationshipsResponse {
relationships: response.relationships,
next_cursor: response.next_cursor,
});
}
}
let _ = (
self.resource,
self.relation,
self.subject,
self.limit,
self.cursor,
);
Ok(ListRelationshipsResponse {
relationships: vec![],
next_cursor: None,
})
}
}
impl std::future::IntoFuture for ListRelationshipsRequest {
type Output = Result<ListRelationshipsResponse, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Self::Output> + Send + 'static>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[derive(Debug, Clone)]
pub struct ListRelationshipsResponse {
pub relationships: Vec<Relationship<'static>>,
pub next_cursor: Option<String>,
}
impl ListRelationshipsResponse {
pub fn has_more(&self) -> bool {
self.next_cursor.is_some()
}
pub fn iter(&self) -> impl Iterator<Item = &Relationship<'static>> {
self.relationships.iter()
}
}
impl IntoIterator for ListRelationshipsResponse {
type Item = Relationship<'static>;
type IntoIter = std::vec::IntoIter<Relationship<'static>>;
fn into_iter(self) -> Self::IntoIter {
self.relationships.into_iter()
}
}
pub struct ResourcesClient<'a> {
vault: &'a VaultClient,
}
impl<'a> ResourcesClient<'a> {
fn new(vault: &'a VaultClient) -> Self {
Self { vault }
}
pub fn accessible_by(self, subject: impl Into<Cow<'a, str>>) -> ResourcesQueryBuilder<'a> {
ResourcesQueryBuilder {
vault: self.vault,
subject: subject.into(),
}
}
}
pub struct ResourcesQueryBuilder<'a> {
vault: &'a VaultClient,
subject: Cow<'a, str>,
}
impl<'a> ResourcesQueryBuilder<'a> {
#[must_use]
pub fn with_permission(self, permission: impl Into<Cow<'a, str>>) -> ResourcesListBuilder<'a> {
ResourcesListBuilder {
vault: self.vault,
subject: self.subject,
permission: permission.into(),
resource_type: None,
consistency: None,
page_size: None,
}
}
}
pub struct ResourcesListBuilder<'a> {
vault: &'a VaultClient,
subject: Cow<'a, str>,
permission: Cow<'a, str>,
resource_type: Option<Cow<'a, str>>,
consistency: Option<ConsistencyToken>,
page_size: Option<u32>,
}
impl<'a> ResourcesListBuilder<'a> {
#[must_use]
pub fn resource_type(mut self, resource_type: impl Into<Cow<'a, str>>) -> Self {
self.resource_type = Some(resource_type.into());
self
}
#[must_use]
pub fn at_least_as_fresh_as(mut self, token: ConsistencyToken) -> Self {
self.consistency = Some(token);
self
}
#[must_use]
pub fn page_size(mut self, size: u32) -> Self {
self.page_size = Some(size);
self
}
pub fn stream(self) -> ResourceStream<'a> {
ResourceStream::new(self)
}
#[must_use]
pub fn take(self, n: usize) -> ResourcesListTake<'a> {
ResourcesListTake {
inner: self,
limit: n,
}
}
pub async fn collect(self) -> Result<Vec<String>, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let mut all_resources = Vec::new();
let mut cursor: Option<String> = None;
loop {
let response = transport
.list_resources(
&self.subject,
&self.permission,
self.resource_type.as_ref().map(|s| s.as_ref()),
self.page_size,
cursor.as_deref(),
)
.await?;
all_resources.extend(response.resources);
if let Some(next) = response.next_cursor {
cursor = Some(next);
} else {
break;
}
}
return Ok(all_resources);
}
}
let _ = (self.consistency, self.page_size);
Ok(Vec::new())
}
pub async fn cursor(self, cursor: Option<&str>) -> Result<ResourcesPage, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let response = transport
.list_resources(
&self.subject,
&self.permission,
self.resource_type.as_ref().map(|s| s.as_ref()),
self.page_size,
cursor,
)
.await?;
return Ok(ResourcesPage {
resources: response.resources,
next_cursor: response.next_cursor,
});
}
}
let _ = (self.consistency, self.page_size, cursor);
Ok(ResourcesPage {
resources: Vec::new(),
next_cursor: None,
})
}
}
pub struct ResourcesListTake<'a> {
inner: ResourcesListBuilder<'a>,
limit: usize,
}
impl<'a> ResourcesListTake<'a> {
pub async fn collect(self) -> Result<Vec<String>, Error> {
let results = self.inner.collect().await?;
Ok(results.into_iter().take(self.limit).collect())
}
}
#[derive(Debug, Clone)]
pub struct ResourcesPage {
pub resources: Vec<String>,
pub next_cursor: Option<String>,
}
impl ResourcesPage {
pub fn has_more(&self) -> bool {
self.next_cursor.is_some()
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.resources.iter().map(|s| s.as_str())
}
}
impl IntoIterator for ResourcesPage {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.resources.into_iter()
}
}
pub struct ResourceStream<'a> {
#[cfg(feature = "rest")]
transport: Option<Arc<dyn TransportClient + Send + Sync>>,
subject: String,
permission: String,
resource_type: Option<String>,
page_size: Option<u32>,
cursor: Option<String>,
buffer: std::collections::VecDeque<String>,
done: bool,
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> ResourceStream<'a> {
fn new(builder: ResourcesListBuilder<'a>) -> Self {
Self {
#[cfg(feature = "rest")]
transport: builder.vault.transport().cloned(),
subject: builder.subject.into_owned(),
permission: builder.permission.into_owned(),
resource_type: builder.resource_type.map(|s| s.into_owned()),
page_size: builder.page_size,
cursor: None,
buffer: std::collections::VecDeque::new(),
done: false,
_marker: std::marker::PhantomData,
}
}
pub async fn try_next(&mut self) -> Result<Option<String>, Error> {
use futures::StreamExt;
self.next().await.transpose()
}
}
impl<'a> Stream for ResourceStream<'a> {
type Item = Result<String, Error>;
#[allow(unused_variables)]
fn poll_next(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(resource) = this.buffer.pop_front() {
return std::task::Poll::Ready(Some(Ok(resource)));
}
if this.done {
return std::task::Poll::Ready(None);
}
#[cfg(feature = "rest")]
{
if let Some(transport) = &this.transport {
let transport = transport.clone();
let subject = this.subject.clone();
let permission = this.permission.clone();
let resource_type = this.resource_type.clone();
let page_size = this.page_size;
let cursor = this.cursor.clone();
let fut = async move {
transport
.list_resources(
&subject,
&permission,
resource_type.as_deref(),
page_size,
cursor.as_deref(),
)
.await
};
let mut fut = Box::pin(fut);
match fut.as_mut().poll(cx) {
std::task::Poll::Ready(Ok(response)) => {
if let Some(next_cursor) = response.next_cursor {
this.cursor = Some(next_cursor);
} else {
this.done = true;
}
this.buffer.extend(response.resources);
if let Some(resource) = this.buffer.pop_front() {
return std::task::Poll::Ready(Some(Ok(resource)));
} else {
this.done = true;
return std::task::Poll::Ready(None);
}
}
std::task::Poll::Ready(Err(e)) => {
this.done = true;
return std::task::Poll::Ready(Some(Err(e)));
}
std::task::Poll::Pending => {
return std::task::Poll::Pending;
}
}
}
}
this.done = true;
std::task::Poll::Ready(None)
}
}
pub struct SubjectsClient<'a> {
vault: &'a VaultClient,
}
impl<'a> SubjectsClient<'a> {
fn new(vault: &'a VaultClient) -> Self {
Self { vault }
}
pub fn with_permission(self, permission: impl Into<Cow<'a, str>>) -> SubjectsQueryBuilder<'a> {
SubjectsQueryBuilder {
vault: self.vault,
permission: permission.into(),
}
}
}
pub struct SubjectsQueryBuilder<'a> {
vault: &'a VaultClient,
permission: Cow<'a, str>,
}
impl<'a> SubjectsQueryBuilder<'a> {
#[must_use]
pub fn on_resource(self, resource: impl Into<Cow<'a, str>>) -> SubjectsListBuilder<'a> {
SubjectsListBuilder {
vault: self.vault,
permission: self.permission,
resource: resource.into(),
subject_type: None,
consistency: None,
page_size: None,
}
}
}
pub struct SubjectsListBuilder<'a> {
vault: &'a VaultClient,
permission: Cow<'a, str>,
resource: Cow<'a, str>,
subject_type: Option<Cow<'a, str>>,
consistency: Option<ConsistencyToken>,
page_size: Option<u32>,
}
impl<'a> SubjectsListBuilder<'a> {
#[must_use]
pub fn subject_type(mut self, subject_type: impl Into<Cow<'a, str>>) -> Self {
self.subject_type = Some(subject_type.into());
self
}
#[must_use]
pub fn at_least_as_fresh_as(mut self, token: ConsistencyToken) -> Self {
self.consistency = Some(token);
self
}
#[must_use]
pub fn page_size(mut self, size: u32) -> Self {
self.page_size = Some(size);
self
}
pub fn stream(self) -> SubjectStream<'a> {
SubjectStream::new(self)
}
#[must_use]
pub fn take(self, n: usize) -> SubjectsListTake<'a> {
SubjectsListTake {
inner: self,
limit: n,
}
}
pub async fn collect(self) -> Result<Vec<String>, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let mut all_subjects = Vec::new();
let mut cursor: Option<String> = None;
loop {
let response = transport
.list_subjects(
&self.permission,
&self.resource,
self.subject_type.as_ref().map(|s| s.as_ref()),
self.page_size,
cursor.as_deref(),
)
.await?;
all_subjects.extend(response.subjects);
if let Some(next) = response.next_cursor {
cursor = Some(next);
} else {
break;
}
}
return Ok(all_subjects);
}
}
let _ = (self.consistency, self.page_size);
Ok(Vec::new())
}
pub async fn cursor(self, cursor: Option<&str>) -> Result<SubjectsPage, Error> {
#[cfg(feature = "rest")]
{
if let Some(transport) = self.vault.transport() {
let response = transport
.list_subjects(
&self.permission,
&self.resource,
self.subject_type.as_ref().map(|s| s.as_ref()),
self.page_size,
cursor,
)
.await?;
return Ok(SubjectsPage {
subjects: response.subjects,
next_cursor: response.next_cursor,
});
}
}
let _ = (self.consistency, self.page_size, cursor);
Ok(SubjectsPage {
subjects: Vec::new(),
next_cursor: None,
})
}
}
pub struct SubjectsListTake<'a> {
inner: SubjectsListBuilder<'a>,
limit: usize,
}
impl<'a> SubjectsListTake<'a> {
pub async fn collect(self) -> Result<Vec<String>, Error> {
let results = self.inner.collect().await?;
Ok(results.into_iter().take(self.limit).collect())
}
}
#[derive(Debug, Clone)]
pub struct SubjectsPage {
pub subjects: Vec<String>,
pub next_cursor: Option<String>,
}
impl SubjectsPage {
pub fn has_more(&self) -> bool {
self.next_cursor.is_some()
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.subjects.iter().map(|s| s.as_str())
}
}
impl IntoIterator for SubjectsPage {
type Item = String;
type IntoIter = std::vec::IntoIter<String>;
fn into_iter(self) -> Self::IntoIter {
self.subjects.into_iter()
}
}
pub struct SubjectStream<'a> {
#[cfg(feature = "rest")]
transport: Option<Arc<dyn TransportClient + Send + Sync>>,
permission: String,
resource: String,
subject_type: Option<String>,
page_size: Option<u32>,
cursor: Option<String>,
buffer: std::collections::VecDeque<String>,
done: bool,
_marker: std::marker::PhantomData<&'a ()>,
}
impl<'a> SubjectStream<'a> {
fn new(builder: SubjectsListBuilder<'a>) -> Self {
Self {
#[cfg(feature = "rest")]
transport: builder.vault.transport().cloned(),
permission: builder.permission.into_owned(),
resource: builder.resource.into_owned(),
subject_type: builder.subject_type.map(|s| s.into_owned()),
page_size: builder.page_size,
cursor: None,
buffer: std::collections::VecDeque::new(),
done: false,
_marker: std::marker::PhantomData,
}
}
pub async fn try_next(&mut self) -> Result<Option<String>, Error> {
use futures::StreamExt;
self.next().await.transpose()
}
}
impl<'a> Stream for SubjectStream<'a> {
type Item = Result<String, Error>;
#[allow(unused_variables)]
fn poll_next(
self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(subject) = this.buffer.pop_front() {
return std::task::Poll::Ready(Some(Ok(subject)));
}
if this.done {
return std::task::Poll::Ready(None);
}
#[cfg(feature = "rest")]
{
if let Some(transport) = &this.transport {
let transport = transport.clone();
let permission = this.permission.clone();
let resource = this.resource.clone();
let subject_type = this.subject_type.clone();
let page_size = this.page_size;
let cursor = this.cursor.clone();
let fut = async move {
transport
.list_subjects(
&permission,
&resource,
subject_type.as_deref(),
page_size,
cursor.as_deref(),
)
.await
};
let mut fut = Box::pin(fut);
match fut.as_mut().poll(cx) {
std::task::Poll::Ready(Ok(response)) => {
if let Some(next_cursor) = response.next_cursor {
this.cursor = Some(next_cursor);
} else {
this.done = true;
}
this.buffer.extend(response.subjects);
if let Some(subject) = this.buffer.pop_front() {
return std::task::Poll::Ready(Some(Ok(subject)));
} else {
this.done = true;
return std::task::Poll::Ready(None);
}
}
std::task::Poll::Ready(Err(e)) => {
this.done = true;
return std::task::Poll::Ready(Some(Err(e)));
}
std::task::Poll::Pending => {
return std::task::Poll::Pending;
}
}
}
}
this.done = true;
std::task::Poll::Ready(None)
}
}
use super::explain::{DenialReason, PermissionExplanation};
pub struct ExplainPermissionRequest {
vault: VaultClient,
subject: Option<String>,
permission: Option<String>,
resource: Option<String>,
context: Option<Context>,
}
impl ExplainPermissionRequest {
fn new(vault: VaultClient) -> Self {
Self {
vault,
subject: None,
permission: None,
resource: None,
context: None,
}
}
#[must_use]
pub fn subject(mut self, subject: impl Into<String>) -> Self {
self.subject = Some(subject.into());
self
}
#[must_use]
pub fn permission(mut self, permission: impl Into<String>) -> Self {
self.permission = Some(permission.into());
self
}
#[must_use]
pub fn resource(mut self, resource: impl Into<String>) -> Self {
self.resource = Some(resource.into());
self
}
#[must_use]
pub fn with_context(mut self, context: Context) -> Self {
self.context = Some(context);
self
}
async fn execute(self) -> Result<PermissionExplanation, Error> {
let subject = self
.subject
.ok_or_else(|| Error::invalid_argument("subject is required"))?;
let permission = self
.permission
.ok_or_else(|| Error::invalid_argument("permission is required"))?;
let resource = self
.resource
.ok_or_else(|| Error::invalid_argument("resource is required"))?;
#[cfg(feature = "rest")]
if let Some(transport) = self.vault.transport() {
let start = std::time::Instant::now();
let request = TransportCheckRequest {
subject: subject.clone(),
permission: permission.clone(),
resource: resource.clone(),
context: self.context.clone(),
consistency: None,
trace: true, };
let response = transport.check(request).await?;
let evaluation_time = start.elapsed();
let mut explanation = if response.allowed {
PermissionExplanation::allowed(&subject, &permission, &resource)
} else {
PermissionExplanation::denied(&subject, &permission, &resource)
.with_denial_reason(DenialReason::no_path())
};
if let Some(trace) = response.trace {
explanation = explanation
.with_evaluation_time(std::time::Duration::from_micros(trace.duration_micros));
if let Some(root) = trace.root {
let paths = Self::extract_paths_from_tree(&root, &subject, &resource);
for path in paths {
explanation = explanation.with_path(path);
}
}
} else {
explanation = explanation.with_evaluation_time(evaluation_time);
}
return Ok(explanation);
}
Ok(
PermissionExplanation::denied(&subject, &permission, &resource)
.with_denial_reason(DenialReason::no_path()),
)
}
fn extract_paths_from_tree(
node: &crate::transport::traits::EvaluationNode,
subject: &str,
resource: &str,
) -> Vec<Vec<crate::vault::explain::PathNode>> {
use crate::transport::traits::EvaluationNodeType;
use crate::vault::explain::PathNode;
let mut paths = Vec::new();
if node.result {
match &node.node_type {
EvaluationNodeType::DirectCheck {
resource: res,
relation,
subject: subj,
} => {
paths.push(vec![
PathNode::new(subj).with_relation(relation.clone()),
PathNode::new(res),
]);
}
EvaluationNodeType::ComputedUserset { relation } => {
for child in &node.children {
let child_paths = Self::extract_paths_from_tree(child, subject, resource);
for mut path in child_paths {
if let Some(first) = path.first_mut() {
first.derived_from = Some(format!("computed:{}", relation));
}
paths.push(path);
}
}
}
EvaluationNodeType::RelatedObjectUserset {
relationship,
computed,
} => {
for child in &node.children {
let child_paths = Self::extract_paths_from_tree(child, subject, resource);
for mut path in child_paths {
if let Some(first) = path.first_mut() {
first.derived_from = Some(format!("{}#{}", relationship, computed));
}
paths.push(path);
}
}
}
EvaluationNodeType::Union
| EvaluationNodeType::Intersection
| EvaluationNodeType::Exclusion => {
for child in &node.children {
paths.extend(Self::extract_paths_from_tree(child, subject, resource));
}
}
EvaluationNodeType::WasmModule { module_name } => {
paths.push(vec![
PathNode::new(subject).with_derived_from(format!("wasm:{}", module_name)),
PathNode::new(resource),
]);
}
}
}
if paths.is_empty() {
for child in &node.children {
paths.extend(Self::extract_paths_from_tree(child, subject, resource));
}
}
paths
}
}
impl std::future::IntoFuture for ExplainPermissionRequest {
type Output = Result<PermissionExplanation, Error>;
type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.execute())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::auth::BearerCredentialsConfig;
use crate::transport::mock::MockTransport;
use std::sync::Arc;
async fn create_test_vault() -> VaultClient {
let mock_transport = Arc::new(MockTransport::new());
let client = Client::builder()
.url("https://api.example.com")
.credentials(BearerCredentialsConfig::new("test"))
.build_with_transport(mock_transport)
.await
.unwrap();
client.organization("org_test").vault("vlt_test")
}
async fn create_test_vault_with_relationships() -> VaultClient {
let mock_transport = Arc::new(MockTransport::new());
mock_transport.add_relationship(Relationship::new("doc:1", "view", "user:alice"));
mock_transport.add_relationship(Relationship::new("doc:1", "edit", "user:bob"));
let client = Client::builder()
.url("https://api.example.com")
.credentials(BearerCredentialsConfig::new("test"))
.build_with_transport(mock_transport)
.await
.unwrap();
client.organization("org_test").vault("vlt_test")
}
#[tokio::test]
async fn test_check_basic() {
let vault = create_test_vault().await;
let result = vault.check("user:alice", "view", "doc:1").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_check_with_context() {
let vault = create_test_vault().await;
let result = vault
.check("user:alice", "view", "doc:1")
.with_context(Context::new().with("env", "prod"))
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_check_with_consistency() {
let vault = create_test_vault().await;
let token = ConsistencyToken::new("test_token");
let result = vault
.check("user:alice", "view", "doc:1")
.at_least_as_fresh(token)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_require() {
let vault = create_test_vault_with_relationships().await;
let result = vault.check("user:alice", "view", "doc:1").require().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_require_with_context() {
let vault = create_test_vault_with_relationships().await;
let result = vault
.check("user:alice", "view", "doc:1")
.require()
.with_context(Context::new().with("env", "prod"))
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_require_with_consistency() {
let vault = create_test_vault_with_relationships().await;
let token = ConsistencyToken::new("test_token");
let result = vault
.check("user:alice", "view", "doc:1")
.require()
.at_least_as_fresh(token)
.await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_detailed() {
let vault = create_test_vault_with_relationships().await;
let decision = vault
.check("user:alice", "view", "doc:1")
.detailed()
.await
.unwrap();
assert!(decision.is_allowed());
}
#[tokio::test]
async fn test_vault_client_debug() {
let vault = create_test_vault().await;
let debug = format!("{:?}", vault);
assert!(debug.contains("VaultClient"));
assert!(debug.contains("org_test"));
assert!(debug.contains("vlt_test"));
}
#[tokio::test]
async fn test_vault_client_accessors() {
let vault = create_test_vault().await;
assert_eq!(vault.organization_id(), "org_test");
assert_eq!(vault.vault_id(), "vlt_test");
let _ = vault.client();
}
#[test]
fn test_batch_check_item_new() {
let item = BatchCheckItem::new("user:alice", "view", "doc:1");
assert_eq!(item.subject(), "user:alice");
assert_eq!(item.permission(), "view");
assert_eq!(item.resource(), "doc:1");
}
#[test]
fn test_batch_check_item_debug() {
let item = BatchCheckItem::new("user:alice", "view", "doc:1");
let debug = format!("{:?}", item);
assert!(debug.contains("user:alice"));
}
#[tokio::test]
async fn test_check_batch_basic() {
let vault = create_test_vault().await;
let checks = vec![
("user:alice", "view", "doc:1"),
("user:bob", "edit", "doc:2"),
];
let results = vault.check_batch(checks).await.unwrap();
assert_eq!(results.len(), 2);
}
#[tokio::test]
async fn test_check_batch_with_context() {
let vault = create_test_vault().await;
let checks = vec![("user:alice", "view", "doc:1")];
let results = vault
.check_batch(checks)
.with_context(Context::new().with("env", "prod"))
.await
.unwrap();
assert_eq!(results.len(), 1);
}
#[tokio::test]
async fn test_check_batch_with_consistency() {
let vault = create_test_vault().await;
let token = ConsistencyToken::new("test_token");
let checks = vec![("user:alice", "view", "doc:1")];
let results = vault
.check_batch(checks)
.at_least_as_fresh(token)
.await
.unwrap();
assert_eq!(results.len(), 1);
}
#[tokio::test]
async fn test_check_batch_len_is_empty() {
let vault = create_test_vault().await;
let batch = vault.check_batch(vec![("user:alice", "view", "doc:1")]);
assert_eq!(batch.len(), 1);
assert!(!batch.is_empty());
let empty_batch = vault.check_batch(Vec::<(&str, &str, &str)>::new());
assert_eq!(empty_batch.len(), 0);
assert!(empty_batch.is_empty());
}
#[test]
fn test_batch_check_result() {
let result = BatchCheckResult {
results: vec![true, false, true],
decisions: None,
consistency_token: Some(ConsistencyToken::new("token")),
};
assert_eq!(result.as_slice(), &[true, false, true]);
assert_eq!(result.len(), 3);
assert!(!result.is_empty());
assert!(!result.all_allowed());
assert!(result.any_allowed());
assert_eq!(result.denied_indices(), vec![1]);
let items: Vec<_> = result.iter().collect();
assert_eq!(items, vec![true, false, true]);
}
#[test]
fn test_batch_check_result_all_allowed() {
let result = BatchCheckResult {
results: vec![true, true, true],
decisions: None,
consistency_token: None,
};
assert!(result.all_allowed());
assert!(result.any_allowed());
assert!(result.denied_indices().is_empty());
}
#[test]
fn test_batch_check_result_all_denied() {
let result = BatchCheckResult {
results: vec![false, false],
decisions: None,
consistency_token: None,
};
assert!(!result.all_allowed());
assert!(!result.any_allowed());
assert_eq!(result.denied_indices(), vec![0, 1]);
}
#[test]
fn test_batch_check_result_empty() {
let result = BatchCheckResult {
results: vec![],
decisions: None,
consistency_token: None,
};
assert!(result.is_empty());
assert!(result.all_allowed()); assert!(!result.any_allowed());
}
#[tokio::test]
async fn test_relationships_client_debug() {
let vault = create_test_vault().await;
let rels = vault.relationships();
let debug = format!("{:?}", rels);
assert!(debug.contains("RelationshipsClient"));
}
#[tokio::test]
async fn test_relationships_write() {
let vault = create_test_vault().await;
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let token = vault.relationships().write(rel).await.unwrap();
assert!(!token.value().is_empty());
}
#[tokio::test]
async fn test_relationships_write_batch() {
let vault = create_test_vault().await;
let rels = vec![
Relationship::new("doc:1", "viewer", "user:alice"),
Relationship::new("doc:1", "editor", "user:bob"),
];
let batch = vault.relationships().write_batch(rels);
assert_eq!(batch.len(), 2);
assert!(!batch.is_empty());
let token = batch.await.unwrap();
assert!(!token.value().is_empty());
}
#[tokio::test]
async fn test_relationships_write_batch_empty() {
let vault = create_test_vault().await;
let batch = vault
.relationships()
.write_batch(Vec::<Relationship>::new());
assert!(batch.is_empty());
}
#[tokio::test]
async fn test_relationships_delete() {
let vault = create_test_vault().await;
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let result = vault.relationships().delete(rel).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_relationships_list() {
let vault = create_test_vault().await;
let response = vault.relationships().list().await.unwrap();
assert!(response.relationships.is_empty());
}
#[tokio::test]
async fn test_relationships_list_with_filters() {
let vault = create_test_vault().await;
let response = vault
.relationships()
.list()
.resource("doc:1")
.relation("viewer")
.subject("user:alice")
.limit(100)
.cursor("cursor123")
.await
.unwrap();
assert!(response.relationships.is_empty());
}
#[test]
fn test_list_relationships_response() {
let response = ListRelationshipsResponse {
relationships: vec![Relationship::new("doc:1", "viewer", "user:alice").into_owned()],
next_cursor: Some("cursor123".to_string()),
};
assert!(response.has_more());
assert_eq!(response.iter().count(), 1);
let items: Vec<_> = response.into_iter().collect();
assert_eq!(items.len(), 1);
}
#[test]
fn test_list_relationships_response_no_more() {
let response = ListRelationshipsResponse {
relationships: vec![],
next_cursor: None,
};
assert!(!response.has_more());
}
#[tokio::test]
async fn test_resources_accessible_by() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_with_type() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.resource_type("document")
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_with_consistency() {
let vault = create_test_vault().await;
let token = ConsistencyToken::new("test_token");
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.at_least_as_fresh_as(token)
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_with_page_size() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.page_size(50)
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_take() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.take(10)
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_cursor() {
let vault = create_test_vault().await;
let page = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.cursor(None)
.await
.unwrap();
assert!(!page.has_more());
}
#[test]
fn test_resources_page() {
let page = ResourcesPage {
resources: vec!["doc:1".to_string(), "doc:2".to_string()],
next_cursor: Some("cursor".to_string()),
};
assert!(page.has_more());
let items: Vec<_> = page.iter().collect();
assert_eq!(items, vec!["doc:1", "doc:2"]);
let owned: Vec<_> = page.into_iter().collect();
assert_eq!(owned.len(), 2);
}
#[test]
fn test_resources_page_no_more() {
let page = ResourcesPage {
resources: vec![],
next_cursor: None,
};
assert!(!page.has_more());
}
#[tokio::test]
async fn test_subjects_with_permission() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_with_type() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.subject_type("user")
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_with_consistency() {
let vault = create_test_vault().await;
let token = ConsistencyToken::new("test_token");
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.at_least_as_fresh_as(token)
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_with_page_size() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.page_size(50)
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_take() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.take(10)
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_cursor() {
let vault = create_test_vault().await;
let page = vault
.subjects()
.with_permission("edit")
.on_resource("doc:1")
.cursor(None)
.await
.unwrap();
assert!(!page.has_more());
}
#[test]
fn test_subjects_page() {
let page = SubjectsPage {
subjects: vec!["user:alice".to_string(), "user:bob".to_string()],
next_cursor: Some("cursor".to_string()),
};
assert!(page.has_more());
let items: Vec<_> = page.iter().collect();
assert_eq!(items, vec!["user:alice", "user:bob"]);
let owned: Vec<_> = page.into_iter().collect();
assert_eq!(owned.len(), 2);
}
#[test]
fn test_subjects_page_no_more() {
let page = SubjectsPage {
subjects: vec![],
next_cursor: None,
};
assert!(!page.has_more());
}
#[tokio::test]
async fn test_explain_permission_missing_subject() {
let vault = create_test_vault().await;
let result = vault
.explain_permission()
.permission("view")
.resource("doc:1")
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("subject"));
}
#[tokio::test]
async fn test_explain_permission_missing_permission() {
let vault = create_test_vault().await;
let result = vault
.explain_permission()
.subject("user:alice")
.resource("doc:1")
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("permission"));
}
#[tokio::test]
async fn test_explain_permission_missing_resource() {
let vault = create_test_vault().await;
let result = vault
.explain_permission()
.subject("user:alice")
.permission("view")
.await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("resource"));
}
#[tokio::test]
async fn test_explain_permission_denied() {
let vault = create_test_vault().await;
let explanation = vault
.explain_permission()
.subject("user:alice")
.permission("view")
.resource("doc:1")
.await
.unwrap();
assert!(!explanation.allowed);
assert!(!explanation.denial_reasons.is_empty());
}
#[cfg(feature = "rest")]
#[tokio::test]
async fn test_explain_permission_allowed() {
let vault = create_test_vault_with_relationships().await;
let explanation = vault
.explain_permission()
.subject("user:alice")
.permission("view")
.resource("doc:1")
.await
.unwrap();
assert!(explanation.allowed);
}
#[tokio::test]
async fn test_explain_permission_with_context() {
let vault = create_test_vault().await;
let explanation = vault
.explain_permission()
.subject("user:alice")
.permission("view")
.resource("doc:1")
.with_context(Context::new().with("environment", "production"))
.await
.unwrap();
assert!(!explanation.allowed);
}
#[tokio::test]
async fn test_delete_where_no_filter_error() {
let vault = create_test_vault().await;
let result = vault.relationships().delete_where().await;
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("at least one filter"));
}
#[tokio::test]
async fn test_delete_where_with_resource() {
let vault = create_test_vault().await;
let result = vault
.relationships()
.delete_where()
.resource("doc:1")
.await
.unwrap();
assert_eq!(result.deleted_count(), 0);
assert!(!result.any_deleted());
}
#[tokio::test]
async fn test_delete_where_with_relation() {
let vault = create_test_vault().await;
let result = vault
.relationships()
.delete_where()
.relation("viewer")
.await
.unwrap();
assert_eq!(result.deleted_count, 0);
}
#[tokio::test]
async fn test_delete_where_with_subject() {
let vault = create_test_vault().await;
let result = vault
.relationships()
.delete_where()
.subject("user:alice")
.await
.unwrap();
assert!(!result.any_deleted());
}
#[tokio::test]
async fn test_delete_where_with_all_filters() {
let vault = create_test_vault().await;
let result = vault
.relationships()
.delete_where()
.resource("doc:1")
.relation("viewer")
.subject("user:alice")
.await
.unwrap();
assert_eq!(result.deleted_count(), 0);
}
#[test]
fn test_delete_where_result_any_deleted() {
let result = DeleteWhereResult { deleted_count: 5 };
assert!(result.any_deleted());
assert_eq!(result.deleted_count(), 5);
let empty_result = DeleteWhereResult { deleted_count: 0 };
assert!(!empty_result.any_deleted());
}
#[tokio::test]
async fn test_vault_client_clone() {
let vault = create_test_vault().await;
let cloned = vault.clone();
assert_eq!(cloned.organization_id(), vault.organization_id());
assert_eq!(cloned.vault_id(), vault.vault_id());
}
#[tokio::test]
async fn test_vault_simulate_accessor() {
let vault = create_test_vault().await;
let _simulate = vault.simulate();
}
#[tokio::test]
async fn test_vault_watch_accessor() {
let vault = create_test_vault().await;
let _watch = vault.watch();
}
#[test]
fn test_batch_check_item_clone() {
let item = BatchCheckItem::new("user:alice", "view", "doc:1");
let cloned = item.clone();
assert_eq!(cloned.subject(), "user:alice");
assert_eq!(cloned.permission(), "view");
assert_eq!(cloned.resource(), "doc:1");
}
#[test]
fn test_batch_check_result_debug() {
let result = BatchCheckResult {
results: vec![true, false],
decisions: None,
consistency_token: None,
};
let debug = format!("{:?}", result);
assert!(debug.contains("BatchCheckResult"));
}
#[test]
fn test_batch_check_result_clone() {
let result = BatchCheckResult {
results: vec![true, false],
decisions: None,
consistency_token: Some(ConsistencyToken::new("token")),
};
let cloned = result.clone();
assert_eq!(cloned.results, result.results);
}
#[tokio::test]
async fn test_relationships_client_clone() {
let vault = create_test_vault().await;
let rels = vault.relationships();
let _cloned = rels.clone();
}
#[cfg(feature = "rest")]
#[tokio::test]
async fn test_require_denied_returns_error() {
let vault = create_test_vault().await;
let result = vault
.check("user:bob", "admin", "doc:secret")
.require()
.await;
assert!(result.is_err());
}
#[cfg(feature = "rest")]
#[tokio::test]
async fn test_detailed_check_denied() {
let vault = create_test_vault().await;
let decision = vault
.check("user:bob", "admin", "doc:secret")
.detailed()
.await
.unwrap();
assert!(!decision.is_allowed());
}
#[tokio::test]
async fn test_check_request_result_authorized() {
let vault = create_test_vault_with_relationships().await;
let result = vault.check("user:alice", "view", "doc:1").await;
assert!(result.is_ok());
assert!(result.unwrap()); }
#[cfg(feature = "rest")]
#[tokio::test]
async fn test_check_request_result_denied() {
let vault = create_test_vault().await;
let result = vault.check("user:alice", "view", "doc:1").await;
assert!(result.is_ok());
assert!(!result.unwrap()); }
#[tokio::test]
async fn test_require_check_request_authorized() {
let vault = create_test_vault_with_relationships().await;
let result = vault.check("user:alice", "view", "doc:1").require().await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_write_relationship_request() {
let vault = create_test_vault().await;
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let result = vault.relationships().write(rel).await;
assert!(result.is_ok());
let token = result.unwrap();
assert!(!token.value().is_empty());
}
#[tokio::test]
async fn test_delete_relationship_request() {
let vault = create_test_vault().await;
let rel = Relationship::new("doc:1", "viewer", "user:alice");
let result = vault.relationships().delete(rel).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_subjects_list_builder_collect() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("view")
.on_resource("doc:1")
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_list_builder_with_options() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("edit")
.on_resource("doc:2")
.subject_type("user")
.take(10)
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_subjects_list_cursor() {
let vault = create_test_vault().await;
let page = vault
.subjects()
.with_permission("view")
.on_resource("doc:1")
.cursor(None)
.await
.unwrap();
assert!(!page.has_more());
assert!(page.subjects.is_empty());
}
#[tokio::test]
async fn test_resources_list_take() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.take(5)
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_resources_list_cursor() {
let vault = create_test_vault().await;
let page = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.cursor(None)
.await
.unwrap();
assert!(!page.has_more());
assert!(page.resources.is_empty());
}
#[tokio::test]
async fn test_write_batch_request_len() {
let vault = create_test_vault().await;
let rels = vec![
Relationship::new("doc:1", "viewer", "user:alice"),
Relationship::new("doc:2", "editor", "user:bob"),
];
let request = vault.relationships().write_batch(rels);
assert_eq!(request.len(), 2);
assert!(!request.is_empty());
}
#[tokio::test]
async fn test_write_batch_request_empty() {
let vault = create_test_vault().await;
let rels: Vec<Relationship> = vec![];
let request = vault.relationships().write_batch(rels);
assert_eq!(request.len(), 0);
assert!(request.is_empty());
}
#[tokio::test]
async fn test_resources_list_builder_stream() {
let vault = create_test_vault().await;
let stream = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.stream();
use futures::StreamExt;
let results: Vec<Result<String, Error>> = stream.collect().await;
assert!(results.is_empty());
}
#[tokio::test]
async fn test_subjects_list_builder_stream() {
let vault = create_test_vault().await;
let stream = vault
.subjects()
.with_permission("view")
.on_resource("doc:1")
.stream();
use futures::StreamExt;
let results: Vec<Result<String, Error>> = stream.collect().await;
assert!(results.is_empty());
}
#[tokio::test]
async fn test_resources_list_builder_try_next() {
let vault = create_test_vault().await;
let mut stream = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.stream();
let result = stream.try_next().await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_subjects_list_builder_try_next() {
let vault = create_test_vault().await;
let mut stream = vault
.subjects()
.with_permission("view")
.on_resource("doc:1")
.stream();
let result = stream.try_next().await;
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[tokio::test]
async fn test_resources_list_with_type_filter() {
let vault = create_test_vault().await;
let resources = vault
.resources()
.accessible_by("user:alice")
.with_permission("view")
.resource_type("document")
.take(50)
.collect()
.await
.unwrap();
assert!(resources.is_empty());
}
#[tokio::test]
async fn test_subjects_list_with_type_filter() {
let vault = create_test_vault().await;
let subjects = vault
.subjects()
.with_permission("view")
.on_resource("doc:1")
.subject_type("user")
.take(50)
.collect()
.await
.unwrap();
assert!(subjects.is_empty());
}
#[tokio::test]
async fn test_vault_schemas_method() {
let vault = create_test_vault().await;
let schemas = vault.schemas();
assert_eq!(schemas.organization_id(), "org_test");
assert_eq!(schemas.vault_id(), "vlt_test");
}
}