use serde::{Deserialize, Serialize};
use std::{
borrow::Cow,
collections::{BTreeMap, HashMap},
fmt::Debug,
};
mod api;
#[cfg(feature = "local-filtering")]
mod local_filtering;
#[cfg(feature = "local-filtering")]
pub use local_filtering::*;
#[macro_use]
mod macros;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Api Error: {0}")]
Api(#[from] reqwest::Error),
#[error("Oso Server Error: {message}")]
Server {
message: String,
request_id: Option<String>,
},
#[error("Input error: {0}")]
Input(String),
}
impl Error {
pub fn api_request_id(&self) -> Option<&str> {
match self {
Error::Server { request_id, .. } => request_id.as_deref(),
_ => None,
}
}
}
#[derive(Clone)]
pub struct Oso {
environment_id: String,
client: api::Client,
}
impl Debug for Oso {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Oso")
.field("url", &self.client.url)
.field("environment_id", &self.environment_id)
.finish()
}
}
type StringRef<'a> = Cow<'a, str>;
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Value<'a> {
#[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
pub type_: Option<StringRef<'a>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<StringRef<'a>>,
}
impl<'a> Value<'a> {
pub fn new(type_: impl Into<StringRef<'a>>, id: impl Into<StringRef<'a>>) -> Self {
Self {
type_: Some(type_.into()),
id: Some(id.into()),
}
}
pub fn any() -> Self {
Self { type_: None, id: None }
}
pub fn any_of_type(type_: impl Into<StringRef<'a>>) -> Self {
Self {
type_: Some(type_.into()),
id: None,
}
}
}
impl From<bool> for Value<'static> {
fn from(b: bool) -> Self {
Self::new("Boolean", b.to_string())
}
}
impl From<i64> for Value<'static> {
fn from(i: i64) -> Self {
Self::new("Integer", i.to_string())
}
}
impl<'a> From<&'a str> for Value<'a> {
fn from(s: &'a str) -> Self {
Self::new("String", s)
}
}
impl From<String> for Value<'static> {
fn from(s: String) -> Self {
Self::new("String", s)
}
}
#[derive(Serialize, Deserialize, Debug, PartialEq, PartialOrd, Eq, Ord, Clone)]
pub struct Fact<'a> {
pub predicate: String,
pub args: Vec<Value<'a>>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct ResourceMetadata {
roles: Vec<String>,
permissions: Vec<String>,
relations: BTreeMap<String, String>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct PolicyMetadata {
pub resources: BTreeMap<String, ResourceMetadata>,
}
pub struct Builder {
url: String,
api_key: String,
}
impl Default for Builder {
fn default() -> Self {
Self::new()
}
}
impl Builder {
pub fn new() -> Self {
Self {
url: "https://api.osohq.com".to_owned(),
api_key: "".to_owned(),
}
}
pub fn with_url(&mut self, url: &str) -> &mut Self {
url.clone_into(&mut self.url);
self
}
pub fn with_api_key(&mut self, api_key: &str) -> &mut Self {
api_key.clone_into(&mut self.api_key);
self
}
pub fn from_env() -> Self {
let mut builder = Builder::new();
if let Ok(url) = std::env::var("OSO_URL") {
builder.with_url(&url);
}
if let Ok(api_key) = std::env::var("OSO_AUTH") {
builder.with_api_key(&api_key);
}
builder
}
pub fn build(&self) -> Result<Oso, Error> {
if self.api_key.is_empty() {
return Err(Error::Input("API key must be set".to_owned()));
}
Oso::new_with_url(&self.url, &self.api_key)
}
}
#[derive(Deserialize)]
struct ApiResult {
pub message: String,
}
pub struct OsoWithContext<'a> {
client: &'a api::Client,
context: Vec<Fact<'a>>,
}
impl Oso {
pub fn new_with_url(url: &str, api_key: &str) -> Result<Self, Error> {
let client = api::Client::new(url, api_key)?;
let environment_id = api_key.split('_').take(2).collect::<Vec<_>>().join("_");
Ok(Self { client, environment_id })
}
pub fn new(api_key: &str) -> Result<Self, Error> {
Oso::new_with_url("https://api.osohq.com", api_key)
}
pub async fn policy(&self, policy_src: &str) -> Result<(), Error> {
#[derive(Debug, Serialize)]
struct PolicyRequest<'a> {
src: &'a str,
}
let body = PolicyRequest { src: policy_src };
let res: ApiResult = self.client.post("policy", &body, true).await?;
tracing::info!("Policy updated: {}", res.message);
Ok(())
}
pub async fn get_policy_metadata(&self) -> Result<PolicyMetadata, Error> {
#[derive(Debug, Deserialize)]
struct MetadataResponse {
metadata: PolicyMetadata,
}
let res: MetadataResponse = self.client.get("policy_metadata", ()).await?;
Ok(res.metadata)
}
pub async fn tell<'a>(&self, fact: Fact<'a>) -> Result<(), Error> {
self.bulk(&[], &[fact]).await
}
pub async fn delete<'a>(&self, fact: Fact<'a>) -> Result<(), Error> {
self.bulk(&[fact], &[]).await
}
pub async fn bulk_tell<'a>(&self, facts: &[Fact<'a>]) -> Result<(), Error> {
self.bulk(&[], facts).await
}
pub async fn bulk_delete<'a>(&self, facts: &[Fact<'a>]) -> Result<(), Error> {
self.bulk(facts, &[]).await
}
pub async fn bulk<'a>(&self, delete: &[Fact<'a>], tell: &'a [Fact<'a>]) -> Result<(), Error> {
self.client.bulk(delete, tell).await
}
pub async fn get<'a>(&self, fact: &Fact<'a>) -> Result<Vec<Fact<'static>>, Error> {
let mut params = HashMap::new();
for (i, a) in fact.args.iter().enumerate() {
params.insert(format!("args.{i}.type"), a.type_.to_owned());
params.insert(format!("args.{i}.id"), a.id.to_owned());
}
self.client.get("facts", params).await
}
fn with_no_context(&self) -> OsoWithContext<'_> {
OsoWithContext {
client: &self.client,
context: vec![],
}
}
pub async fn authorize(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resource: impl Into<Value<'_>>,
) -> Result<bool, Error> {
self.with_no_context().authorize(actor, action, resource).await
}
pub async fn authorize_resources<T>(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resources: &mut Vec<T>,
) -> Result<(), Error>
where
for<'r> &'r T: Into<Value<'r>>,
{
self.with_no_context()
.authorize_resources(actor, action, resources)
.await
}
pub async fn actions(
&self,
actor: impl Into<Value<'_>>,
resource: impl Into<Value<'_>>,
) -> Result<Vec<String>, Error> {
self.with_no_context().actions(actor, resource).await
}
pub async fn list(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resource_type: &str,
) -> Result<Vec<String>, Error> {
self.with_no_context().list(actor, action, resource_type).await
}
pub async fn query(&self, fact: &Fact<'_>) -> Result<Vec<Fact<'static>>, Error> {
self.with_no_context().query(fact).await
}
pub fn with_context<'a>(&'a self, context: Vec<Fact<'a>>) -> OsoWithContext<'a> {
OsoWithContext {
client: &self.client,
context,
}
}
}
impl<'ctxt> OsoWithContext<'ctxt> {
pub async fn authorize(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resource: impl Into<Value<'_>>,
) -> Result<bool, Error> {
#[derive(Debug, Serialize)]
struct AuthorizeRequest<'a> {
actor_type: &'a str,
actor_id: &'a str,
action: &'a str,
resource_type: &'a str,
resource_id: &'a str,
context_facts: &'a [Fact<'a>],
}
let actor = actor.into();
let resource = resource.into();
let (Some(actor_type), Some(actor_id), Some(resource_type), Some(resource_id)) = (
actor.type_.as_ref(),
actor.id.as_ref(),
resource.type_.as_ref(),
resource.id.as_ref(),
) else {
if actor.type_.is_none() || actor.id.is_none() {
return Err(Error::Input(
"Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors"
.to_owned(),
));
}
if resource.type_.is_none() || resource.id.is_none() {
return Err(Error::Input(
"Resource must be a concrete value. Try `oso.list` if you want to get all allowed resources"
.to_owned(),
));
}
unreachable!();
};
let body = AuthorizeRequest {
actor_type,
actor_id,
action,
resource_type,
resource_id,
context_facts: &self.context,
};
#[derive(Deserialize)]
struct AuthorizeResponse {
allowed: bool,
}
let resp: AuthorizeResponse = self.client.post("authorize", &body, false).await?;
Ok(resp.allowed)
}
pub async fn authorize_resources<T>(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resources: &mut Vec<T>,
) -> Result<(), Error>
where
for<'r> &'r T: Into<Value<'r>>,
{
#[derive(Debug, Serialize)]
struct AuthorizeResourcesRequest<'a> {
actor_type: &'a str,
actor_id: &'a str,
action: &'a str,
resources: &'a Vec<Value<'a>>,
context_facts: &'a Vec<Fact<'a>>,
}
let resource_values = resources.iter().map(|r| r.into()).collect();
#[derive(Deserialize)]
struct AuthorizeResourcesResponse<'a> {
results: Vec<Value<'a>>,
}
let actor = actor.into();
let (Some(actor_type), Some(actor_id)) = (actor.type_.as_ref(), actor.id.as_ref()) else {
return Err(Error::Input(
"Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors".to_owned(),
));
};
let body = AuthorizeResourcesRequest {
actor_type,
actor_id,
action,
resources: &resource_values,
context_facts: &self.context,
};
let resp: AuthorizeResourcesResponse = self.client.post("authorize_resources", &body, false).await?;
if resp.results.len() == resources.len() {
return Ok(());
}
let mut results_iter = resp.results.into_iter();
let mut next = results_iter.next();
resources.retain(|val| {
if let Some(ref next_val) = next {
if next_val == &val.into() {
next = results_iter.next();
return true;
}
}
false
});
Ok(())
}
pub async fn actions(
&self,
actor: impl Into<Value<'_>>,
resource: impl Into<Value<'_>>,
) -> Result<Vec<String>, Error> {
#[derive(Debug, Serialize)]
struct ActionsRequest<'a> {
actor_type: &'a str,
actor_id: &'a str,
resource_type: &'a str,
resource_id: &'a str,
context_facts: &'a Vec<Fact<'a>>,
}
#[derive(Deserialize)]
struct ActionsResponse {
results: Vec<String>,
}
let actor = actor.into();
let resource = resource.into();
let (Some(actor_type), Some(actor_id), Some(resource_type), Some(resource_id)) = (
actor.type_.as_ref(),
actor.id.as_ref(),
resource.type_.as_ref(),
resource.id.as_ref(),
) else {
if actor.type_.is_none() || actor.id.is_none() {
return Err(Error::Input(
"Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors"
.to_owned(),
));
}
if resource.type_.is_none() || resource.id.is_none() {
return Err(Error::Input("Resource must be a concrete value. Try `oso.query` if you want to get all allowed actions and resources".to_owned()));
}
unreachable!();
};
let body = ActionsRequest {
actor_type,
actor_id,
resource_type,
resource_id,
context_facts: &self.context,
};
let resp: ActionsResponse = self.client.post("actions", &body, false).await?;
Ok(resp.results)
}
pub async fn list(
&self,
actor: impl Into<Value<'_>>,
action: &str,
resource_type: &str,
) -> Result<Vec<String>, Error> {
#[derive(Debug, Serialize)]
struct ListRequest<'a> {
actor_type: &'a str,
actor_id: &'a str,
action: &'a str,
resource_type: &'a str,
context_facts: &'a Vec<Fact<'a>>,
}
#[derive(Deserialize)]
struct ListResponse {
results: Vec<String>,
}
let actor = actor.into();
let (Some(actor_type), Some(actor_id)) = (actor.type_.as_ref(), actor.id.as_ref()) else {
return Err(Error::Input(
"Actor must be a concrete value. Try `oso.query` if you want to get all permitted actors".to_owned(),
));
};
let body = ListRequest {
actor_type,
actor_id,
action,
resource_type,
context_facts: &self.context,
};
let resp: ListResponse = self.client.post("list", &body, false).await?;
Ok(resp.results)
}
pub async fn query(&self, fact: &Fact<'_>) -> Result<Vec<Fact<'static>>, Error> {
#[derive(Debug, Serialize)]
struct QueryRequest<'a> {
fact: &'a Fact<'a>,
context_facts: &'a Vec<Fact<'a>>,
}
let body = QueryRequest {
fact,
context_facts: &self.context,
};
#[derive(Deserialize)]
struct QueryResponse {
results: Vec<Fact<'static>>,
}
let resp: QueryResponse = self.client.post("query", &body, false).await?;
Ok(resp.results)
}
}
#[cfg(test)]
mod tests {}