Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SchemaStore and policy validation #24

Merged
merged 11 commits into from
May 26, 2024
Merged
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,16 @@ Featured Data Stores :
- [x] In-Memory
- [ ] Redis

### Schema Store Management

Cedar-Agent support storing custom schemas, which hold the shape of your data types and actions. Utilising the schema
store enables you to create a strict definition of all the objects used by your application. Cedar-Agent will validate
all your policies and data against this schema.
Featured Polict Stores :

- [x] In-Memory
- [ ] Redis

### Authorization Checks

One of the key features of Cedar-Agent is its ability to perform authorization checks on stored policies and data.
Expand Down Expand Up @@ -95,6 +105,9 @@ Cedar Agent configuration is available using environment variables and command l
- The log level to filter logs. Defaults to `info`.
`CEDAR_AGENT_LOG_LEVEL` environment variable.
`--log-level`, `-l` command line argument.
- Load schema from json file. Defaults to `None`.
`CEDAR_AGENT_SCHEMA` environment variable.
`--schema`, `-s` command line argument.
- Load data from json file. Defaults to `None`.
`CEDAR_AGENT_DATA` environment variable.
`--data`, `-d` command line argument.
Expand Down Expand Up @@ -169,19 +182,25 @@ using Rapidoc and Swagger UI, that you can access through the following routes:
### Quickstart

1. [Run the Cedar Agent](#run)
2. Store policy using this command:
2. Store schema using this command:

```shell
curl -X PUT -H "Content-Type: application/json" -d @./examples/schema.json http://localhost:8180/v1/schema
```

3. Store policy using this command:

```shell
curl -X PUT -H "Content-Type: application/json" -d @./examples/policies.json http://localhost:8180/v1/policies
```

3. Store data using this command:
4. Store data using this command:

```shell
curl -X PUT -H "Content-Type: application/json" -d @./examples/data.json http://localhost:8180/v1/data
```

4. Perform IsAuthorized check using this command:
5. Perform IsAuthorized check using this command:

```shell
curl -X POST -H "Content-Type: application/json" -d @./examples/allowed_authorization_query.json http://localhost:8180/v1/is_authorized
Expand Down
84 changes: 84 additions & 0 deletions examples/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
{
"": {
"entityTypes": {
"User": {
"shape": {
"type": "Record",
"attributes": {}
},
"memberOfTypes": [
"Role"
]
},
"Role": {
"shape": {
"type": "Record",
"attributes": {}
}
},
"Document": {
"shape": {
"type": "Record",
"attributes": {}
}
}
},
"actions": {
"create": {
"appliesTo": {
"principalTypes": [
"User",
"Role"
],
"resourceTypes": [
"Document"
]
}
},
"delete": {
"appliesTo": {
"principalTypes": [
"User",
"Role"
],
"resourceTypes": [
"Document"
]
}
},
"get": {
"appliesTo": {
"principalTypes": [
"User",
"Role"
],
"resourceTypes": [
"Document"
]
}
},
"list": {
"appliesTo": {
"principalTypes": [
"User",
"Role"
],
"resourceTypes": [
"Document"
]
}
},
"update": {
"appliesTo": {
"principalTypes": [
"User",
"Role"
],
"resourceTypes": [
"Document"
]
}
}
}
}
}
7 changes: 7 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ pub struct Config {
pub data: Option<PathBuf>,
#[arg(long)]
pub policies: Option<PathBuf>,
#[arg(short, long)]
pub schema: Option<PathBuf>,
}

impl Into<rocket::figment::Figment> for &Config {
Expand All @@ -45,6 +47,9 @@ impl Into<rocket::figment::Figment> for &Config {
if let Some(policies) = self.policies.borrow() {
config = config.merge(("policies", policies));
}
if let Some(schema) = self.schema.borrow() {
config = config.merge(("schema", schema));
}

config
}
Expand All @@ -59,6 +64,7 @@ impl Config {
log_level: None,
data: None,
policies: None,
schema: None
}
}

Expand All @@ -71,6 +77,7 @@ impl Config {
config.log_level = c.log_level.or(config.log_level);
config.data = c.data.or(config.data);
config.policies = c.policies.or(config.policies);
config.schema = c.schema.or(config.schema);
}

config
Expand Down
7 changes: 7 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crate::services::data::memory::MemoryDataStore;
use crate::services::data::DataStore;
use crate::services::policies::memory::MemoryPolicyStore;
use crate::services::policies::PolicyStore;
use crate::services::schema::memory::MemorySchemaStore;
use crate::services::schema::SchemaStore;

mod authn;
mod common;
Expand All @@ -31,11 +33,13 @@ async fn main() -> ExitCode {
let server_config: rocket::figment::Figment = config.borrow().into();
let launch_result = rocket::custom(server_config)
.attach(common::DefaultContentType::new(ContentType::JSON))
.attach(services::schema::load_from_file::InitSchemaFairing)
.attach(services::data::load_from_file::InitDataFairing)
.attach(services::policies::load_from_file::InitPoliciesFairing)
.manage(config)
.manage(Box::new(MemoryPolicyStore::new()) as Box<dyn PolicyStore>)
.manage(Box::new(MemoryDataStore::new()) as Box<dyn DataStore>)
.manage(Box::new(MemorySchemaStore::new()) as Box<dyn SchemaStore>)
.manage(cedar_policy::Authorizer::new())
.register(
"/",
Expand All @@ -59,6 +63,9 @@ async fn main() -> ExitCode {
routes::data::update_entities,
routes::data::delete_entities,
routes::authorization::is_authorized,
routes::schema::get_schema,
routes::schema::update_schema,
routes::schema::delete_schema
],
)
.mount(
Expand Down
7 changes: 5 additions & 2 deletions src/routes/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use rocket_okapi::openapi;
use crate::authn::ApiKey;
use crate::errors::response::AgentError;
use crate::schemas::data as schemas;
use crate::DataStore;
use crate::{DataStore, SchemaStore};

#[openapi]
#[get("/data")]
Expand All @@ -23,9 +23,12 @@ pub async fn get_entities(
pub async fn update_entities(
_auth: ApiKey,
data_store: &State<Box<dyn DataStore>>,
schema_store: &State<Box<dyn SchemaStore>>,
entities: Json<schemas::Entities>,
) -> Result<Json<schemas::Entities>, AgentError> {
match data_store.update_entities(entities.into_inner()).await {
let schema = schema_store.get_cedar_schema().await;

match data_store.update_entities(entities.into_inner(), schema).await {
Ok(entities) => Ok(Json::from(entities)),
Err(err) => Err(AgentError::BadRequest {
reason: err.to_string(),
Expand Down
1 change: 1 addition & 0 deletions src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use rocket_okapi::openapi;
pub mod authorization;
pub mod data;
pub mod policies;
pub mod schema;

#[openapi]
#[get("/")]
Expand Down
41 changes: 34 additions & 7 deletions src/routes/policies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use rocket_okapi::openapi;
use crate::authn::ApiKey;
use crate::errors::response::AgentError;
use crate::schemas::policies as schemas;
use crate::services::policies::errors::PolicyStoreError;
use crate::services::policies::PolicyStore;
use crate::services::schema::SchemaStore;

#[openapi]
#[get("/policies")]
Expand Down Expand Up @@ -41,15 +43,26 @@ pub async fn create_policy(
_auth: ApiKey,
policy: Json<schemas::Policy>,
policy_store: &State<Box<dyn PolicyStore>>,
schema_store: &State<Box<dyn SchemaStore>>,
) -> Result<Json<schemas::Policy>, AgentError> {
let policy = policy.into_inner();
let added_policy = policy_store.create_policy(policy.borrow()).await;
let schema = schema_store.get_cedar_schema().await;

let added_policy = policy_store.create_policy(policy.borrow(), schema).await;
match added_policy {
Ok(p) => Ok(Json::from(p)),
Err(_) => Err(AgentError::Duplicate {
id: policy.id,
object: "policy",
}),
Err(e) => {
if let Some(PolicyStoreError::PolicyInvalid(_, reason)) = e.downcast_ref::<PolicyStoreError>() {
omer9564 marked this conversation as resolved.
Show resolved Hide resolved
Err(AgentError::BadRequest {
reason: reason.clone()
})
} else {
Err(AgentError::Duplicate {
id: policy.id,
object: "policy",
})
}
},
}
}

Expand All @@ -59,8 +72,14 @@ pub async fn update_policies(
_auth: ApiKey,
policy: Json<Vec<schemas::Policy>>,
policy_store: &State<Box<dyn PolicyStore>>,
schema_store: &State<Box<dyn SchemaStore>>,
) -> Result<Json<Vec<schemas::Policy>>, AgentError> {
let updated_policy = policy_store.update_policies(policy.into_inner()).await;
let schema = schema_store.get_cedar_schema().await;

let updated_policy = policy_store.update_policies(
policy.into_inner(),
schema
).await;
match updated_policy {
Ok(p) => Ok(Json::from(p)),
Err(e) => Err(AgentError::BadRequest {
Expand All @@ -76,8 +95,16 @@ pub async fn update_policy(
id: String,
policy: Json<schemas::PolicyUpdate>,
policy_store: &State<Box<dyn PolicyStore>>,
schema_store: &State<Box<dyn SchemaStore>>,
) -> Result<Json<schemas::Policy>, AgentError> {
let updated_policy = policy_store.update_policy(id, policy.into_inner()).await;
let schema = schema_store.get_cedar_schema().await;

let updated_policy = policy_store.update_policy(
id,
policy.into_inner(),
schema
).await;

match updated_policy {
Ok(p) => Ok(Json::from(p)),
Err(err) => Err(AgentError::BadRequest {
Expand Down
70 changes: 70 additions & 0 deletions src/routes/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use rocket::{delete, get, put, State};
use rocket::response::status;
use rocket::serde::json::Json;
use rocket_okapi::openapi;

use crate::authn::ApiKey;
use crate::errors::response::AgentError;
use cedar_policy::Schema as CedarSchema;
use log::error;
use crate::schemas::schema::Schema as InternalSchema;
use crate::services::{schema::SchemaStore, policies::PolicyStore, data::DataStore};

#[openapi]
#[get("/schema")]
pub async fn get_schema(
_auth: ApiKey,
schema_store: &State<Box<dyn SchemaStore>>
) -> Result<Json<InternalSchema>, AgentError> {
Ok(Json::from(schema_store.get_internal_schema().await))
}

#[openapi]
#[put("/schema", format = "json", data = "<schema>")]
pub async fn update_schema(
_auth: ApiKey,
schema_store: &State<Box<dyn SchemaStore>>,
policy_store: &State<Box<dyn PolicyStore>>,
data_store: &State<Box<dyn DataStore>>,
schema: Json<InternalSchema>
) -> Result<Json<InternalSchema>, AgentError> {
let cedar_schema: CedarSchema = match schema.clone().into_inner().try_into() {
Ok(schema) => schema,
Err(err) => return Err(AgentError::BadRequest {
reason: err.to_string(),
})
};

let current_policies = policy_store.get_policies().await;
match policy_store.update_policies(current_policies, Some(cedar_schema.clone())).await {
Ok(_) => {},
Err(err) => return Err(AgentError::BadRequest {
reason: format!("Existing policies invalid with the new schema: {}", err.to_string()),
})
}

let current_entities = data_store.get_entities().await;
match data_store.update_entities(current_entities, Some(cedar_schema)).await {
Ok(_) => {},
Err(err) => return Err(AgentError::BadRequest {
reason: format!("Existing entities invalid with the new schema: {}", err.to_string()),
})
}

match schema_store.update_schema(schema.into_inner()).await {
Akamatsu21 marked this conversation as resolved.
Show resolved Hide resolved
Ok(schema) => Ok(Json::from(schema)),
Err(err) => return Err(AgentError::BadRequest {
reason: err.to_string(),
})
}
}

#[openapi]
#[delete("/schema")]
pub async fn delete_schema(
_auth: ApiKey,
schema_store: &State<Box<dyn SchemaStore>>
) -> Result<status::NoContent, AgentError> {
schema_store.delete_schema().await;
Ok(status::NoContent)
}
Loading