Skip to content

Commit a132954

Browse files
authored
Add feature permission revoke for profiles (#549)
1 parent e06672c commit a132954

11 files changed

Lines changed: 293 additions & 6 deletions

File tree

auction-server/api-types/src/profile.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,35 @@ pub struct Profile {
6060
pub role: ProfileRole,
6161
}
6262

63+
#[derive(Serialize, Deserialize, ToSchema, Clone, ToResponse)]
64+
#[serde(rename_all = "snake_case")]
65+
pub enum PrivilegeFeature {
66+
/// The feature for which the privilege is for.
67+
CancelQuote,
68+
}
69+
70+
#[derive(Serialize, Deserialize, ToSchema, Clone, ToResponse)]
71+
#[serde(rename_all = "snake_case")]
72+
pub enum PrivilegeState {
73+
/// The privilege is enabled.
74+
Enabled,
75+
/// The privilege is disabled.
76+
Disabled,
77+
}
78+
79+
#[derive(Serialize, Deserialize, ToSchema, Clone, ToResponse)]
80+
pub struct CreatePrivilege {
81+
/// The id of the profile to create privilege for.
82+
#[schema(example = "obo3ee3e-58cc-4372-a567-0e02b2c3d479", value_type = String)]
83+
pub profile_id: ProfileId,
84+
/// The feature which the privilege is for.
85+
#[schema(example = "cancel_quote", value_type = PrivilegeFeature)]
86+
pub feature: PrivilegeFeature,
87+
/// The state of the privilege.
88+
#[schema(example = "disabled", value_type = PrivilegeState)]
89+
pub state: PrivilegeState,
90+
}
91+
6392
#[derive(Serialize, Deserialize, ToSchema, Clone, ToResponse)]
6493
pub struct CreateAccessToken {
6594
/// The id of the profile to create token for.
@@ -85,6 +114,8 @@ pub enum Route {
85114
PostProfileAccessToken,
86115
#[strum(serialize = "access_tokens")]
87116
DeleteProfileAccessToken,
117+
#[strum(serialize = "privileges")]
118+
PostPrivilege,
88119
}
89120

90121
impl Routable for Route {
@@ -118,6 +149,11 @@ impl Routable for Route {
118149
method: http::Method::DELETE,
119150
full_path,
120151
},
152+
Route::PostPrivilege => crate::RouteProperties {
153+
access_level: AccessLevel::Admin,
154+
method: http::Method::POST,
155+
full_path,
156+
},
121157
}
122158
}
123159
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
DROP TRIGGER IF EXISTS update_updated_at ON privilege;
2+
DROP TABLE IF EXISTS privilege;
3+
DROP TYPE IF EXISTS privilege_state;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
CREATE TYPE privilege_state AS ENUM ('enabled', 'disabled');
2+
3+
CREATE TABLE privilege
4+
(
5+
id UUID PRIMARY KEY,
6+
profile_id UUID NOT NULL REFERENCES profile(id) ON DELETE CASCADE,
7+
feature VARCHAR(255) NOT NULL,
8+
state privilege_state NOT NULL,
9+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
10+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
11+
);
12+
13+
CREATE TRIGGER update_updated_at
14+
BEFORE UPDATE ON privilege
15+
FOR EACH ROW
16+
EXECUTE FUNCTION update_updated_at_column();

auction-server/src/api.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,8 @@ pub enum RestError {
466466
QuoteIsFinalized,
467467
/// Token mint is not allowed
468468
TokenMintNotAllowed(String, String),
469+
/// Access to cancel quote feature is revoked
470+
CancelQuoteAccessRevoked,
469471
}
470472

471473

@@ -603,6 +605,10 @@ impl RestError {
603605
StatusCode::NOT_FOUND,
604606
format!("Token mint address is not allowed for: {}, mint: {}", msg, mint)
605607
),
608+
RestError::CancelQuoteAccessRevoked => (
609+
StatusCode::FORBIDDEN,
610+
"Access to cancel quote feature is revoked".to_string(),
611+
),
606612
}
607613
}
608614
}
@@ -865,6 +871,7 @@ pub async fn start_api(
865871
ProfileRoute::DeleteProfileAccessToken,
866872
profile::delete_profile_access_token,
867873
)
874+
.route(ProfileRoute::PostPrivilege, profile::post_privilege)
868875
.router;
869876

870877
let routes = Router::new()

auction-server/src/api/profile.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,18 @@ use {
1313
Query,
1414
State,
1515
},
16+
http::StatusCode,
17+
response::IntoResponse,
1618
Json,
1719
},
1820
express_relay_api_types::profile::{
1921
AccessToken,
2022
CreateAccessToken,
23+
CreatePrivilege,
2124
CreateProfile,
2225
GetProfile,
26+
PrivilegeFeature,
27+
PrivilegeState,
2328
Profile,
2429
ProfileRole,
2530
},
@@ -142,3 +147,55 @@ pub async fn delete_profile_access_token(
142147
_ => Ok(()),
143148
}
144149
}
150+
151+
impl From<models::PrivilegeFeature> for PrivilegeFeature {
152+
fn from(feature: models::PrivilegeFeature) -> Self {
153+
match feature {
154+
models::PrivilegeFeature::CancelQuote => PrivilegeFeature::CancelQuote,
155+
}
156+
}
157+
}
158+
159+
impl From<PrivilegeFeature> for models::PrivilegeFeature {
160+
fn from(feature: PrivilegeFeature) -> Self {
161+
match feature {
162+
PrivilegeFeature::CancelQuote => models::PrivilegeFeature::CancelQuote,
163+
}
164+
}
165+
}
166+
167+
impl From<models::PrivilegeState> for PrivilegeState {
168+
fn from(state: models::PrivilegeState) -> Self {
169+
match state {
170+
models::PrivilegeState::Enabled => PrivilegeState::Enabled,
171+
models::PrivilegeState::Disabled => PrivilegeState::Disabled,
172+
}
173+
}
174+
}
175+
176+
impl From<PrivilegeState> for models::PrivilegeState {
177+
fn from(state: PrivilegeState) -> Self {
178+
match state {
179+
PrivilegeState::Enabled => models::PrivilegeState::Enabled,
180+
PrivilegeState::Disabled => models::PrivilegeState::Disabled,
181+
}
182+
}
183+
}
184+
185+
/// Create a privilege for a profile.
186+
///
187+
/// Returns empty response.
188+
#[utoipa::path(post, path = "/v1/profiles/privileges",
189+
security(
190+
("bearerAuth" = []),
191+
),request_body = CreatePrivilege, responses(
192+
(status = 201, description = "The privilege successfully created"),
193+
(status = 400, response = ErrorBodyResponse),
194+
),)]
195+
pub async fn post_privilege(
196+
State(store): State<Arc<StoreNew>>,
197+
Json(params): Json<CreatePrivilege>,
198+
) -> Result<impl IntoResponse, RestError> {
199+
store.store.create_privilege(params).await?;
200+
Ok(StatusCode::CREATED)
201+
}

auction-server/src/auction/service/cancel_bid.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ impl Service {
3636
return Err(RestError::Forbidden);
3737
}
3838

39+
if !self
40+
.store
41+
.has_privilege(
42+
input.profile.id,
43+
crate::models::PrivilegeFeature::CancelQuote,
44+
)
45+
.await?
46+
{
47+
return Err(RestError::CancelQuoteAccessRevoked);
48+
}
49+
3950
match bid.status.clone() {
4051
entities::BidStatusSvm::AwaitingSignature { auction } => {
4152
let tx_hash = bid.chain_data.transaction.signatures[0];

auction-server/src/auction/service/mod.rs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use {
1212
db::DB,
1313
entities::ChainId,
1414
},
15+
state::Store,
1516
},
1617
mockall_double::double,
1718
solana_client::{
@@ -93,6 +94,7 @@ pub struct Config {
9394
}
9495

9596
pub struct ServiceInner {
97+
store: Arc<Store>,
9698
opportunity_service: Arc<OpportunityService>,
9799
config: Config,
98100
repo: Arc<Repository>,
@@ -111,13 +113,15 @@ impl std::ops::Deref for Service {
111113

112114
impl Service {
113115
pub fn new(
116+
store: Arc<Store>,
114117
db: DB,
115118
config: Config,
116119
opportunity_service: Arc<OpportunityService>,
117120
task_tracker: TaskTracker,
118121
event_sender: broadcast::Sender<UpdateEvent>,
119122
) -> Self {
120123
Self(Arc::new(ServiceInner {
124+
store,
121125
repo: Arc::new(repository::Repository::new(db, config.chain_id.clone())),
122126
config,
123127
opportunity_service,
@@ -254,11 +258,13 @@ pub mod tests {
254258
ServiceInner,
255259
},
256260
crate::{
261+
api::ws,
257262
auction::repository::{
258263
Database,
259264
Repository,
260265
},
261266
kernel::{
267+
db::DB,
262268
entities::ChainId,
263269
traced_sender_svm::{
264270
tests::MockRpcClient,
@@ -270,14 +276,21 @@ pub mod tests {
270276
get_submit_bid_instruction_account_positions,
271277
get_swap_instruction_account_positions,
272278
},
279+
state::Store,
273280
},
274281
solana_client::{
275282
nonblocking::rpc_client::RpcClient,
276283
rpc_client::RpcClientConfig,
277284
},
278285
solana_sdk::signature::Keypair,
279-
std::sync::Arc,
280-
tokio::sync::broadcast,
286+
std::{
287+
collections::HashMap,
288+
sync::Arc,
289+
},
290+
tokio::sync::{
291+
broadcast,
292+
RwLock,
293+
},
281294
tokio_util::task::TaskTracker,
282295
};
283296

@@ -289,9 +302,18 @@ pub mod tests {
289302
rpc_client: MockRpcClient,
290303
broadcaster_client: MockRpcClient,
291304
) -> Self {
305+
let store = Arc::new(Store {
306+
db: DB::connect_lazy("https://test").unwrap(),
307+
chains_svm: HashMap::new(),
308+
ws: ws::WsState::new("X-Forwarded-For".to_string(), 100),
309+
secret_key: "test".to_string(),
310+
access_tokens: RwLock::new(HashMap::new()),
311+
privileges: RwLock::new(HashMap::new()),
312+
});
292313
Service(Arc::new(ServiceInner {
314+
store,
293315
opportunity_service: Arc::new(opportunity_service),
294-
config: Config {
316+
config: Config {
295317
chain_id: chain_id.clone(),
296318
chain_config: ConfigSvm {
297319
client: RpcClient::new_sender(
@@ -322,9 +344,9 @@ pub mod tests {
322344
prioritization_fee_percentile: None,
323345
},
324346
},
325-
repo: Arc::new(Repository::new(db, chain_id.clone())),
326-
task_tracker: TaskTracker::new(),
327-
event_sender: broadcast::channel(1).0,
347+
repo: Arc::new(Repository::new(db, chain_id.clone())),
348+
task_tracker: TaskTracker::new(),
349+
event_sender: broadcast::channel(1).0,
328350
}))
329351
}
330352
}

auction-server/src/models.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,50 @@ pub enum ChainType {
6262
Evm,
6363
Svm,
6464
}
65+
66+
pub type PrivilegeId = Uuid;
67+
68+
#[derive(Clone, Debug, PartialEq, PartialOrd, Hash, Eq, sqlx::Type)]
69+
#[sqlx(rename_all = "snake_case")]
70+
pub enum PrivilegeFeature {
71+
CancelQuote,
72+
}
73+
74+
impl TryFrom<String> for PrivilegeFeature {
75+
type Error = sqlx::error::BoxDynError;
76+
77+
fn try_from(value: String) -> Result<Self, Self::Error> {
78+
match value.as_str() {
79+
"cancel_quote" => Ok(PrivilegeFeature::CancelQuote),
80+
_ => Err(sqlx::error::BoxDynError::from("Invalid privilege feature")),
81+
}
82+
}
83+
}
84+
85+
impl PrivilegeFeature {
86+
pub fn as_str(&self) -> &'static str {
87+
match self {
88+
PrivilegeFeature::CancelQuote => "cancel_quote",
89+
}
90+
}
91+
}
92+
93+
#[derive(Clone, Debug, sqlx::Type, PartialEq, PartialOrd)]
94+
#[sqlx(type_name = "privilege_state", rename_all = "snake_case")]
95+
pub enum PrivilegeState {
96+
Enabled,
97+
Disabled,
98+
}
99+
100+
#[derive(Clone, FromRow, Debug, PartialEq)]
101+
pub struct Privilege {
102+
pub id: PrivilegeId,
103+
104+
#[sqlx(try_from = "String")]
105+
pub feature: PrivilegeFeature,
106+
pub profile_id: ProfileId,
107+
pub state: PrivilegeState,
108+
109+
pub created_at: PrimitiveDateTime,
110+
pub updated_at: PrimitiveDateTime,
111+
}

auction-server/src/opportunity/service/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ pub mod tests {
238238
ws: ws::WsState::new("X-Forwarded-For".to_string(), 100),
239239
secret_key: "test".to_string(),
240240
access_tokens: RwLock::new(HashMap::new()),
241+
privileges: RwLock::new(HashMap::new()),
241242
});
242243

243244
let ws_receiver = store.ws.broadcast_receiver.resubscribe();

0 commit comments

Comments
 (0)