Skip to content

Commit

Permalink
feat: implement setting labels for advisories
Browse files Browse the repository at this point in the history
  • Loading branch information
ctron committed Jun 28, 2024
1 parent d16b909 commit 22741d5
Show file tree
Hide file tree
Showing 9 changed files with 317 additions and 25 deletions.
30 changes: 30 additions & 0 deletions common/src/id.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use hex::ToHex;
use ring::digest::Digest;
use sea_orm::{EntityTrait, QueryFilter, Select, UpdateMany};
use sea_query::Condition;
use serde::{
de::{Error, Visitor},
Deserialize, Deserializer, Serialize, Serializer,
Expand All @@ -24,6 +26,34 @@ pub enum Id {
Sha512(String),
}

/// Create a filter for an ID
pub trait TryFilterForId {
/// Return a condition, filtering for the [`Id`]. Or an `Err(IdError::UnsupportedAlgorithm)` if the ID type is not supported.
fn try_filter(id: Id) -> Result<Condition, IdError>;
}

pub trait TrySelectForId: Sized {
fn try_filter(self, id: Id) -> Result<Self, IdError>;
}

impl<E> TrySelectForId for Select<E>
where
E: EntityTrait + TryFilterForId,
{
fn try_filter(self, id: Id) -> Result<Self, IdError> {
Ok(self.filter(E::try_filter(id)?))
}
}

impl<E> TrySelectForId for UpdateMany<E>
where
E: EntityTrait + TryFilterForId,
{
fn try_filter(self, id: Id) -> Result<Self, IdError> {
Ok(self.filter(E::try_filter(id)?))
}
}

impl Id {
pub fn prefix(&self) -> &'static str {
match self {
Expand Down
17 changes: 15 additions & 2 deletions entity/src/advisory.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::{advisory_vulnerability, cvss3, labels::Labels, organization, vulnerability};
use async_graphql::*;
use sea_orm::entity::prelude::*;
use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition};
use std::sync::Arc;
use time::OffsetDateTime;
use trustify_common::db;
use trustify_common::{
db,
id::{Id, IdError, TryFilterForId},
};

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)]
#[graphql(complex)]
Expand Down Expand Up @@ -83,3 +86,13 @@ impl Related<cvss3::Entity> for Entity {
}

impl ActiveModelBehavior for ActiveModel {}

impl TryFilterForId for Entity {
fn try_filter(id: Id) -> Result<Condition, IdError> {
Ok(match id {
Id::Uuid(uuid) => Column::Id.eq(uuid).into_condition(),
Id::Sha256(hash) => Column::Sha256.eq(hash).into_condition(),
n => return Err(IdError::UnsupportedAlgorithm(n.prefix().to_string())),
})
}
}
47 changes: 47 additions & 0 deletions entity/src/labels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use std::ops::{Deref, DerefMut};
serde::Serialize,
serde::Deserialize,
sea_orm::FromJsonQueryResult,
utoipa::ToSchema,
)]
pub struct Labels(pub HashMap<String, String>);

Expand All @@ -31,6 +32,32 @@ impl Labels {
self.0.insert(k.into(), v.into());
self
}

pub fn extend<I, K, V>(mut self, i: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
self.0
.extend(i.into_iter().map(|(k, v)| (k.into(), v.into())));
self
}

/// Apply a label update.
///
/// This will apply the provided update to the current set of labels. Updates with an empty
/// value will remove the label.
pub fn apply(mut self, update: Labels) -> Self {
for (k, v) in update.0 {
if v.is_empty() {
self.remove(&k);
} else {
self.insert(k, v);
}
}
self
}
}

impl<'a> FromIterator<(&'a str, &'a str)> for Labels {
Expand Down Expand Up @@ -86,3 +113,23 @@ impl DerefMut for Labels {
&mut self.0
}
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn apply_update() {
let original = Labels::new().extend([("foo", "1"), ("bar", "2")]);
let modified =
original.apply(Labels::new().extend([("foo", "2"), ("bar", ""), ("baz", "3")]));

assert_eq!(
modified.0,
HashMap::from_iter([
("foo".to_string(), "2".to_string()),
("baz".to_string(), "3".to_string())
])
);
}
}
13 changes: 12 additions & 1 deletion entity/src/sbom.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::labels::Labels;
use async_graphql::SimpleObject;
use sea_orm::{entity::prelude::*, LinkDef};
use sea_orm::{entity::prelude::*, sea_query::IntoCondition, Condition, LinkDef};
use time::OffsetDateTime;
use trustify_common::id::{Id, IdError, TryFilterForId};

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, SimpleObject)]
#[sea_orm(table_name = "sbom")]
Expand Down Expand Up @@ -61,3 +62,13 @@ impl Related<super::sbom_node::Entity> for Entity {
}

impl ActiveModelBehavior for ActiveModel {}

impl TryFilterForId for Entity {
fn try_filter(id: Id) -> Result<Condition, IdError> {
Ok(match id {
Id::Uuid(uuid) => Column::SbomId.eq(uuid).into_condition(),
Id::Sha256(hash) => Column::Sha256.eq(hash).into_condition(),
n => return Err(IdError::UnsupportedAlgorithm(n.prefix().to_string())),
})
}
}
61 changes: 61 additions & 0 deletions modules/fundamental/src/advisory/endpoints/label.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use crate::advisory::service::AdvisoryService;
use actix_web::{patch, put, web, HttpResponse, Responder};
use trustify_common::id::Id;
use trustify_entity::labels::Labels;

/// Replace the labels of an advisory
#[utoipa::path(
tag = "advisory",
context_path = "/api",
request_body = inline(Labels),
params(
("id" = string, Path, description = "Digest/hash of the document, prefixed by hash type, such as 'sha256:<hash>' or 'urn:uuid:<uuid>'"),
),
responses(
(status = 204, description = "Replaced the labels of the advisory"),
(status = 404, description = "The advisory could not be found"),
),
)]
#[put("/v1/advisory/{id}/label")]
pub async fn set(
advisory: web::Data<AdvisoryService>,
id: web::Path<Id>,
web::Json(labels): web::Json<Labels>,
) -> actix_web::Result<impl Responder> {
Ok(
match advisory.set_labels(id.into_inner(), labels, ()).await? {
Some(()) => HttpResponse::NoContent(),
None => HttpResponse::NotFound(),
},
)
}

/// Modify existing labels of an advisory
#[utoipa::path(
tag = "advisory",
context_path = "/api",
request_body = inline(Labels),
params(
("id" = string, Path, description = "Digest/hash of the document, prefixed by hash type, such as 'sha256:<hash>' or 'urn:uuid:<uuid>'"),
),
responses(
(status = 204, description = "Modified the labels of the advisory"),
(status = 404, description = "The advisory could not be found"),
),
)]
#[patch("/v1/advisory/{id}/label")]
pub async fn update(
advisory: web::Data<AdvisoryService>,
id: web::Path<Id>,
web::Json(update): web::Json<Labels>,
) -> actix_web::Result<impl Responder> {
Ok(
match advisory
.update_labels(id.into_inner(), |labels| labels.apply(update))
.await?
{
Some(()) => HttpResponse::NoContent(),
None => HttpResponse::NotFound(),
},
)
}
7 changes: 5 additions & 2 deletions modules/fundamental/src/advisory/endpoints/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod label;
#[cfg(test)]
mod test;

Expand All @@ -23,12 +24,14 @@ pub fn configure(config: &mut web::ServiceConfig, db: Database) {
.service(all)
.service(get)
.service(upload)
.service(download);
.service(download)
.service(label::set)
.service(label::update);
}

#[derive(OpenApi)]
#[openapi(
paths(all, get, upload, download),
paths(all, get, upload, download, label::set),
components(schemas(
crate::advisory::model::AdvisoryDetails,
crate::advisory::model::AdvisoryHead,
Expand Down
53 changes: 53 additions & 0 deletions modules/fundamental/src/advisory/endpoints/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,14 @@ use trustify_cvss::cvss3::{
AttackComplexity, AttackVector, Availability, Confidentiality, Cvss3Base, Integrity,
PrivilegesRequired, Scope, UserInteraction,
};
use trustify_entity::labels::Labels;
use trustify_module_ingestor::{
graph::{advisory::AdvisoryInformation, Graph},
model::IngestResult,
service::IngestorService,
};
use trustify_module_storage::service::fs::FileSystemBackend;
use uuid::Uuid;

async fn query<S, B>(app: &S, q: &str) -> PaginatedResults<AdvisorySummary>
where
Expand Down Expand Up @@ -584,3 +586,54 @@ async fn download_advisory_by_id(ctx: TrustifyContext) -> Result<(), anyhow::Err
})
.await
}

/// Test setting labels
#[test_context(TrustifyContext, skip_teardown)]
#[test(actix_web::test)]
async fn set_labels(ctx: TrustifyContext) -> Result<(), anyhow::Error> {
with_upload(ctx, |result, app| {
Box::pin(async move {
// update labels

let request = TestRequest::patch()
.uri(&format!("/api/v1/advisory/{}/label", result.id))
.set_json(Labels::new().extend([("foo", "1"), ("bar", "2")]))
.to_request();

let response = app.call_service(request).await;

log::debug!("Code: {}", response.status());
assert_eq!(response.status(), StatusCode::NO_CONTENT);

Ok(())
})
})
.await
}

/// Test setting labels, for a document that does not exists
#[test_context(TrustifyContext, skip_teardown)]
#[test(actix_web::test)]
async fn set_labels_not_found(ctx: TrustifyContext) -> Result<(), anyhow::Error> {
with_upload(ctx, |_result, app| {
Box::pin(async move {
// update labels

let request = TestRequest::patch()
.uri(&format!(
"/api/v1/advisory/{}/label",
Id::Uuid(Uuid::now_v7())
))
.set_json(Labels::new().extend([("foo", "1"), ("bar", "2")]))
.to_request();

let response = app.call_service(request).await;

log::debug!("Code: {}", response.status());
assert_eq!(response.status(), StatusCode::NOT_FOUND);

Ok(())
})
})
.await
}
Loading

0 comments on commit 22741d5

Please sign in to comment.