Skip to content

Commit

Permalink
feat: add a store for user preferences
Browse files Browse the repository at this point in the history
Closes #926
  • Loading branch information
ctron committed Nov 6, 2024
1 parent 3d2e504 commit 57006c2
Show file tree
Hide file tree
Showing 17 changed files with 772 additions and 7 deletions.
23 changes: 23 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ members = [
"cvss",
"entity",
"migration",
"modules/importer",
"modules/ui",
"modules/analysis",
"modules/fundamental",
"modules/graphql",
"modules/importer",
"modules/ingestor",
"modules/storage",
"modules/graphql",
"modules/analysis",
"modules/ui",
"modules/user",
"server",
"trustd",
"test-context",
"trustd",
"xtask",
]

Expand Down Expand Up @@ -167,6 +168,7 @@ trustify-module-storage = { path = "modules/storage" }
trustify-module-graphql = { path = "modules/graphql" }
trustify-test-context = { path = "test-context" }
trustify-module-analysis = { path = "modules/analysis" }
trustify-module-user = { path = "modules/user" }

# These dependencies are active during both the build time and the run time. So they are normal dependencies
# as well as build-dependencies. However, we can't control feature flags for build dependencies the way we do
Expand Down
1 change: 1 addition & 0 deletions entity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub mod sbom_package_cpe_ref;
pub mod sbom_package_purl_ref;
pub mod source_document;
pub mod status;
pub mod user_preferences;
pub mod version_range;
pub mod version_scheme;
pub mod versioned_purl;
Expand Down
18 changes: 18 additions & 0 deletions entity/src/user_preferences.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "user_preferences")]
pub struct Model {
#[sea_orm(primary_key)]
pub user_id: String,
#[sea_orm(primary_key)]
pub key: String,

pub revision: Uuid,
pub data: serde_json::Value,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
2 changes: 2 additions & 0 deletions migration/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ mod m0000670_version_cmp;
mod m0000680_fix_update_deprecated_advisory;
mod m0000690_alter_sbom_details;
mod m0000700_advisory_add_reserved;
mod m0000710_create_user_prefs;

pub struct Migrator;

Expand Down Expand Up @@ -181,6 +182,7 @@ impl MigratorTrait for Migrator {
Box::new(m0000680_fix_update_deprecated_advisory::Migration),
Box::new(m0000690_alter_sbom_details::Migration),
Box::new(m0000700_advisory_add_reserved::Migration),
Box::new(m0000710_create_user_prefs::Migration),
]
}
}
Expand Down
65 changes: 65 additions & 0 deletions migration/src/m0000710_create_user_prefs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(UserPreferences::Table)
.col(
ColumnDef::new(UserPreferences::UserId)
.string()
.not_null()
.to_owned(),
)
.col(
ColumnDef::new(UserPreferences::Key)
.string()
.not_null()
.to_owned(),
)
.col(
ColumnDef::new(UserPreferences::Revision)
.uuid()
.not_null()
.to_owned(),
)
.col(
ColumnDef::new(UserPreferences::Data)
.json_binary()
.not_null()
.to_owned(),
)
.primary_key(
Index::create()
.col(UserPreferences::UserId)
.col(UserPreferences::Key),
)
.to_owned(),
)
.await?;

Ok(())
}

async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(UserPreferences::Table).to_owned())
.await?;

Ok(())
}
}

#[derive(DeriveIden)]
enum UserPreferences {
Table,
UserId,
Key,
Revision,
Data,
}
28 changes: 28 additions & 0 deletions modules/user/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "trustify-module-user"
version.workspace = true
edition.workspace = true
publish.workspace = true
license.workspace = true

[dependencies]
trustify-auth = { workspace = true }
trustify-common = { workspace = true }
trustify-entity = { workspace = true }

actix-web = { workspace = true }
anyhow = { workspace = true }
sea-orm = { workspace = true, features = ["sea-query-binder", "sqlx-postgres", "runtime-tokio-rustls", "macros", "debug-print"] }
sea-query = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
utoipa = { workspace = true, features = ["actix_extras", "time", "url"] }
utoipa-actix-web = { workspace = true }

[dev-dependencies]
trustify-test-context = { workspace = true }

actix-http = { workspace = true }
test-context = { workspace = true }
test-log = { workspace = true, features = ["log", "trace"] }
tokio = { workspace = true, features = ["full"] }
35 changes: 35 additions & 0 deletions modules/user/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# User management

## Store user preferences

This will create or replace the existing value.

```bash
http PUT localhost:8080/api/v1/userPreferences/foo key=value
```

## Get user preferences

```bash
http GET localhost:8080/api/v1/userPreferences/foo
```

## Delete user preferences

```bash
http DELETE localhost:8080/api/v1/userPreferences/foo
```

## Conditionally update user preferences

It is possible to update the value only if the expected precondition matches. Meaning that you can provide an
etag value as received from a `GET` or `PUT`, and the update will only take place if the revision/etag value hasn't
been changed yet by another update. If it did, the request will fail with a `412 Precondition failed` status and
appropriate action would be to re-fetch the data, apply the update again, and request another store operation. This
is also called "optimistic locking."

```bash
http PUT localhost:8080/api/v1/userPreferences/foo key=value 'If-Match:"<etag from get>"'
```

**NOTE:** Be sure that the `If-Match` value is actually quoted, e.g. `If-Match:"79e5d346-876f-42b6-b0d0-51ee1be73a4c"`
124 changes: 124 additions & 0 deletions modules/user/src/endpoints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
use crate::service::{Error, UserPreferenceService};
use actix_web::{
delete, get,
http::header::{self, ETag, EntityTag, IfMatch},
put, web, HttpResponse, Responder,
};
use trustify_auth::authenticator::user::UserDetails;
use trustify_common::{db::Database, model::Revisioned};

/// mount the "user" module
pub fn configure(svc: &mut utoipa_actix_web::service_config::ServiceConfig, db: Database) {
svc.app_data(web::Data::new(UserPreferenceService::new(db)))
.service(set)
.service(get)
.service(delete);
}

#[utoipa::path(
tag = "userPreferences",
operation_id = "getUserPreferences",
params(
("key", Path, description = "The key to the user preferences"),
),
responses(
(
status = 200,
description = "User preference stored under this key",
body = serde_json::Value,
headers(
("etag" = String, description = "Revision ID")
)
),
(status = 404, description = "Unknown user preference key"),
)
)]
#[get("/v1/userPreference/{key}")]
/// Get user preferences
async fn get(
service: web::Data<UserPreferenceService>,
key: web::Path<String>,
user: UserDetails,
) -> Result<impl Responder, Error> {
Ok(match service.get(user.id, key.into_inner()).await? {
Some(Revisioned { value, revision }) => HttpResponse::Ok()
.append_header((header::ETAG, ETag(EntityTag::new_strong(revision))))
.json(value),
None => HttpResponse::NotFound().finish(),
})
}

#[utoipa::path(
tag = "userPreferences",
operation_id = "setUserPreferences",
request_body = serde_json::Value,
params(
("key", Path, description = "The key to the user preferences"),
("if-match" = Option<String>, Header, description = "The revision to update"),
),
responses(
(
status = 200,
description = "User preference stored under this key",
headers(
("etag" = String, description = "Revision ID")
)
),
(status = 412, description = "The provided If-Match revision did not match the actual revision")
)
)]
#[put("/v1/userPreference/{key}")]
/// Set user preferences
async fn set(
service: web::Data<UserPreferenceService>,
key: web::Path<String>,
user: UserDetails,
web::Header(if_match): web::Header<IfMatch>,
web::Json(data): web::Json<serde_json::Value>,
) -> Result<impl Responder, Error> {
let revision = match &if_match {
IfMatch::Any => None,
IfMatch::Items(items) => items.first().map(|etag| etag.tag()),
};

let Revisioned {
value: (),
revision,
} = service
.set(user.id, key.into_inner(), revision, data)
.await?;

Ok(HttpResponse::NoContent()
.append_header((header::ETAG, ETag(EntityTag::new_strong(revision))))
.finish())
}

#[utoipa::path(
tag = "userPreferences",
operation_id = "deleteUserPreferences",
request_body = serde_json::Value,
params(
("key", Path, description = "The key to the user preferences"),
("if-match" = Option<String>, Header, description = "The revision to delete"),
),
responses(
(status = 201, description = "User preferences are deleted"),
(status = 412, description = "The provided If-Match revision did not match the actual revision")
)
)]
#[delete("/v1/userPreference/{key}")]
/// Delete user preferences
async fn delete(
service: web::Data<UserPreferenceService>,
key: web::Path<String>,
user: UserDetails,
web::Header(if_match): web::Header<IfMatch>,
) -> Result<impl Responder, Error> {
let revision = match &if_match {
IfMatch::Any => None,
IfMatch::Items(items) => items.first().map(|etag| etag.tag()),
};

service.delete(user.id, key.into_inner(), revision).await?;
Ok(HttpResponse::NoContent().finish())
}
5 changes: 5 additions & 0 deletions modules/user/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#![recursion_limit = "256"]

pub mod endpoints;
pub mod service;
pub mod test;
Loading

0 comments on commit 57006c2

Please sign in to comment.