use axum::{
extract::{Path, Query},
Json,
};
use pep::oidc::types::JwtClaims;
use serde::Deserialize;
use utoipa::IntoParams;
use crate::auth::AuthenticatedUser;
use crate::cedar::enforcement::TenantState;
use crate::error::Result;
use crate::models::{
AddTagRequest, Asset, CreateAssetRequest, PaginatedResponse, Tag,
UpdateAssetRequest, UpdateAuthContextRequest,
};
use crate::service::AssetService;
fn default_limit() -> i64 {
20
}
fn default_sort_by() -> String {
"updated_at".to_string()
}
fn default_order() -> String {
"desc".to_string()
}
#[derive(Debug, Deserialize, IntoParams)]
#[into_params(parameter_in = Query)]
pub struct ListAssetsParams {
#[serde(default = "default_limit")]
pub limit: i64,
#[serde(default)]
pub cursor: Option<String>,
pub asset_type: Option<String>,
#[serde(default = "default_sort_by")]
pub sort_by: String,
#[serde(default = "default_order")]
pub order: String,
}
#[utoipa::path(
post,
path = "/api/assets",
request_body = CreateAssetRequest,
responses(
(status = 200, description = "Asset created successfully", body = Asset),
(status = 400, description = "Validation error"),
),
tag = "assets",
)]
pub async fn create_asset(
tx: TenantState,
user: AuthenticatedUser,
Json(request): Json<CreateAssetRequest>,
) -> Result<Json<Asset>> {
let user_id = &user.user_id;
let asset = AssetService::create(tx.services(), request, user_id).await?;
Ok(Json(asset))
}
#[utoipa::path(
get,
path = "/api/assets/{id}",
params(
("id" = String, Path, description = "Asset ID"),
),
responses(
(status = 200, description = "Asset found", body = Asset),
(status = 404, description = "Asset not found"),
),
tag = "assets",
)]
pub async fn get_asset(
tx: TenantState,
user: AuthenticatedUser,
Path(id): Path<String>,
) -> Result<Json<Asset>> {
let asset = AssetService::get(tx.services(), &id).await?;
if let Some(authorizer) = tx.authorizer() {
if crate::cedar::enforcement::should_enforce_cedar(&asset) {
let claims = extract_claims(&user);
crate::cedar::enforcement::check_permission(authorizer, &claims, "View", &asset)?;
}
}
Ok(Json(asset))
}
#[utoipa::path(
put,
path = "/api/assets/{id}",
params(
("id" = String, Path, description = "Asset ID"),
),
request_body = UpdateAssetRequest,
responses(
(status = 200, description = "Asset updated successfully", body = Asset),
(status = 404, description = "Asset not found"),
),
tag = "assets",
)]
pub async fn update_asset(
tx: TenantState,
user: AuthenticatedUser,
Path(id): Path<String>,
Json(request): Json<UpdateAssetRequest>,
) -> Result<Json<Asset>> {
let user_id = &user.user_id;
if let Some(authorizer) = tx.authorizer() {
let asset = AssetService::get(tx.services(), &id).await?;
if crate::cedar::enforcement::should_enforce_cedar(&asset) {
let claims = extract_claims(&user);
crate::cedar::enforcement::check_permission(authorizer, &claims, "Edit", &asset)?;
}
}
let asset = AssetService::update(tx.services(), &id, request, user_id).await?;
Ok(Json(asset))
}
#[utoipa::path(
delete,
path = "/api/assets/{id}",
params(
("id" = String, Path, description = "Asset ID"),
),
responses(
(status = 200, description = "Asset deleted successfully"),
(status = 404, description = "Asset not found"),
),
tag = "assets",
)]
pub async fn delete_asset(
tx: TenantState,
user: AuthenticatedUser,
Path(id): Path<String>,
) -> Result<Json<serde_json::Value>> {
let user_id = &user.user_id;
if let Some(authorizer) = tx.authorizer() {
let asset = AssetService::get(tx.services(), &id).await?;
if crate::cedar::enforcement::should_enforce_cedar(&asset) {
let claims = extract_claims(&user);
crate::cedar::enforcement::check_permission(authorizer, &claims, "Delete", &asset)?;
}
}
AssetService::delete(tx.services(), &id, user_id).await?;
Ok(Json(serde_json::json!({ "deleted": true })))
}
#[utoipa::path(
get,
path = "/api/assets",
params(ListAssetsParams),
responses(
(status = 200, description = "List of assets", body = PaginatedResponse<Asset>),
),
tag = "assets",
)]
pub async fn list_assets(
tx: TenantState,
user: AuthenticatedUser,
Query(params): Query<ListAssetsParams>,
) -> Result<Json<PaginatedResponse<Asset>>> {
let (assets, next_cursor) = AssetService::list(
tx.services(),
params.limit,
params.cursor.as_deref(),
params.asset_type.as_deref(),
¶ms.sort_by,
¶ms.order,
)
.await?;
let filtered = if let Some(authorizer) = tx.authorizer() {
let claims = extract_claims(&user);
crate::cedar::enforcement::filter_by_permission(authorizer, &claims, "View", assets)
} else {
assets
};
Ok(Json(PaginatedResponse {
data: filtered,
next_cursor,
total: None,
}))
}
#[utoipa::path(
post,
path = "/api/assets/{id}/tags",
params(
("id" = String, Path, description = "Asset ID"),
),
request_body = AddTagRequest,
responses(
(status = 200, description = "Tag added successfully", body = Tag),
(status = 404, description = "Asset not found"),
),
tag = "assets",
)]
pub async fn add_tag(
tx: TenantState,
user: AuthenticatedUser,
Path(id): Path<String>,
Json(request): Json<AddTagRequest>,
) -> Result<Json<Tag>> {
let user_id = &user.user_id;
if let Some(authorizer) = tx.authorizer() {
let asset = AssetService::get(tx.services(), &id).await?;
if crate::cedar::enforcement::should_enforce_cedar(&asset) {
let claims = extract_claims(&user);
crate::cedar::enforcement::check_permission(authorizer, &claims, "Tag", &asset)?;
}
}
let tag = AssetService::add_tag(tx.services(), &id, request, user_id).await?;
Ok(Json(tag))
}
#[utoipa::path(
delete,
path = "/api/assets/{id}/tags/{tag_id}",
params(
("id" = String, Path, description = "Asset ID"),
("tag_id" = String, Path, description = "Tag ID to remove"),
),
responses(
(status = 200, description = "Tag removed successfully"),
(status = 404, description = "Asset or tag not found"),
),
tag = "assets",
)]
pub async fn remove_tag(
tx: TenantState,
user: AuthenticatedUser,
Path((id, tag_id)): Path<(String, String)>,
) -> Result<Json<serde_json::Value>> {
let user_id = &user.user_id;
if let Some(authorizer) = tx.authorizer() {
let asset = AssetService::get(tx.services(), &id).await?;
if crate::cedar::enforcement::should_enforce_cedar(&asset) {
let claims = extract_claims(&user);
crate::cedar::enforcement::check_permission(authorizer, &claims, "Tag", &asset)?;
}
}
AssetService::remove_tag(tx.services(), &id, &tag_id, user_id).await?;
Ok(Json(serde_json::json!({ "deleted": true })))
}
#[utoipa::path(
put,
path = "/api/assets/{id}/auth-context",
params(
("id" = String, Path, description = "Asset ID"),
),
request_body = UpdateAuthContextRequest,
responses(
(status = 200, description = "Auth context updated successfully", body = Asset),
(status = 404, description = "Asset not found"),
),
tag = "assets",
)]
pub async fn update_auth_context(
tx: TenantState,
user: AuthenticatedUser,
Path(id): Path<String>,
Json(request): Json<UpdateAuthContextRequest>,
) -> Result<Json<Asset>> {
let user_id = &user.user_id;
let asset = AssetService::update_auth_context(tx.services(), &id, request, user_id).await?;
Ok(Json(asset))
}
fn extract_claims(user: &AuthenticatedUser) -> JwtClaims {
user.to_cedar_claims()
}