diff --git a/Cargo.lock b/Cargo.lock index 9bbc0ca..d28cba8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,9 +157,12 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", + "serde_core", + "serde_json", "tower-layer", "tower-service", "tracing", + "typed-json", ] [[package]] @@ -3367,6 +3370,16 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typed-json" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6024a8d0025400b3f6b189366e9aa92012cf9c4fe1cd2620848dd61425c49eed" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "typeid" version = "1.0.3" diff --git a/Cargo.toml b/Cargo.toml index 61c805c..cc46afc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ publish = false [dependencies] axum = { version = "0.8.7", features = ["macros"] } -chrono = "0.4.42" +chrono = { version = "0.4.42", features = ["serde"] } colored = "3.0.0" deadpool-postgres = { version = "0.14.1", features = ["serde"] } dotenvy = "0.15.7" @@ -30,7 +30,7 @@ uuid = { version = "1.18.1", features = ["v7", "serde"] } reqwest = { version = "0.12.24", features = ["json"] } tower = "0.5.2" ntest = "0.9.3" -axum-extra = { version = "0.12.2", features = ["cookie"] } +axum-extra = { version = "0.12.2", features = ["cookie", "erased-json"] } jsonwebtoken = { version = "10.2.0", features = ["rust_crypto"] } anyhow = "1.0.100" axum-macros = "0.5.0" diff --git a/src/main.rs b/src/main.rs index b94ecca..4c18287 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,17 +10,19 @@ mod pre_definitions; mod tests; use std::{fmt, sync::Arc}; -use axum::{Json, body::Body, response::{IntoResponse, Response}}; +use axum::{body::Body, response::{IntoResponse, Response}}; +use axum_extra::response::ErasedJson; use deadpool_postgres::{Pool, tokio_postgres}; use local_ip_address::local_ip; use postgres::NoTls; use reqwest::{StatusCode}; +use serde::Serialize; use tokio::net::TcpListener; use colored::Colorize; use uuid::Uuid; use thiserror::Error; -use crate::{pre_definitions::initialize_pre_defined_actions, pre_definitions::initialize_pre_defined_roles, resources::{access_policy::{AccessPolicy, AccessPolicyError}, action::{Action, ActionError}, app::{App, AppError}, app_authorization::{AppAuthorization, AppAuthorizationError}, app_authorization_credential::{AppAuthorizationCredential, AppAuthorizationCredentialError}, app_credential::{AppCredential, AppCredentialError}, group::{Group, GroupError}, http_transaction::{HTTPTransaction, HTTPTransactionError}, item::{Item, ItemError}, milestone::{Milestone, MilestoneError}, project::{Project, ProjectError}, role::{Role, RoleError}, role_memberships::{RoleMembership, RoleMembershipError}, server_log_entry::{ServerLogEntry, ServerLogEntryError}, session::{Session, SessionError}, user::{User, UserError}, workspace::{Workspace, WorkspaceError}}}; +use crate::{pre_definitions::{initialize_pre_defined_actions, initialize_pre_defined_roles}, resources::{access_policy::{AccessPolicy, AccessPolicyError}, action::{Action, ActionError}, action_log_entry::{ActionLogEntry, ActionLogEntryError}, app::{App, AppError}, app_authorization::{AppAuthorization, AppAuthorizationError}, app_authorization_credential::{AppAuthorizationCredential, AppAuthorizationCredentialError}, app_credential::{AppCredential, AppCredentialError}, group::{Group, GroupError}, group_membership::{GroupMembership, GroupMembershipError}, http_transaction::{HTTPTransaction, HTTPTransactionError}, item::{Item, ItemError}, milestone::{Milestone, MilestoneError}, project::{Project, ProjectError}, role::{Role, RoleError}, role_memberships::{RoleMembership, RoleMembershipError}, server_log_entry::{ServerLogEntry, ServerLogEntryError}, session::{Session, SessionError}, user::{User, UserError}, workspace::{Workspace, WorkspaceError}}}; const DEFAULT_APP_PORT: i16 = 8080; const DEFAULT_MAXIMUM_POSTGRES_CONNECTION_COUNT: u32 = 5; @@ -94,6 +96,9 @@ pub enum SlashstepServerError { #[error(transparent)] GroupError(#[from] GroupError), + #[error(transparent)] + GroupMembershipError(#[from] GroupMembershipError), + #[error(transparent)] AppError(#[from] AppError), @@ -112,9 +117,15 @@ pub enum SlashstepServerError { #[error(transparent)] ActionError(#[from] ActionError), + #[error(transparent)] + ActionLogEntryError(#[from] ActionLogEntryError), + #[error(transparent)] AppAuthorizationError(#[from] AppAuthorizationError), + #[error(transparent)] + ServerLogEntryError(#[from] ServerLogEntryError), + #[error(transparent)] AppAuthorizationCredentialError(#[from] AppAuthorizationCredentialError), @@ -157,21 +168,27 @@ pub async fn initialize_required_tables(postgres_client: &mut deadpool_postgres: // Because the access_policies table depends on other tables, we need to initialize them in a specific order. HTTPTransaction::initialize_http_transactions_table(postgres_client).await?; + ServerLogEntry::initialize_server_log_entries_table(postgres_client).await?; User::initialize_users_table(postgres_client).await?; Session::initialize_sessions_table(postgres_client).await?; Group::initialize_groups_table(postgres_client).await?; App::initialize_apps_table(postgres_client).await?; + GroupMembership::initialize_app_authorizations_table(postgres_client).await?; Workspace::initialize_workspaces_table(postgres_client).await?; Project::initialize_projects_table(postgres_client).await?; Role::initialize_roles_table(postgres_client).await?; + RoleMembership::initialize_role_memberships_table(postgres_client).await?; Item::initialize_items_table(postgres_client).await?; Action::initialize_actions_table(postgres_client).await?; AppCredential::initialize_app_credentials_table(postgres_client).await?; AppAuthorization::initialize_app_authorizations_table(postgres_client).await?; AppAuthorizationCredential::initialize_app_authorization_credentials_table(postgres_client).await?; Milestone::initialize_milestones_table(postgres_client).await?; + ActionLogEntry::initialize_action_log_entries_table(postgres_client).await?; AccessPolicy::initialize_access_policies_table(postgres_client).await?; - RoleMembership::initialize_role_memberships_table(postgres_client).await?; + + let query = include_str!("./queries/action_log_entries/add_access_policies_reference.sql"); + postgres_client.execute(query, &[]).await?; return Ok(()); @@ -186,7 +203,13 @@ pub enum HTTPError { BadRequestError(Option), NotImplementedError(Option), InternalServerError(Option), - UnauthorizedError(Option) + UnauthorizedError(Option), + UnprocessableEntity(Option) +} + +#[derive(Debug, Serialize)] +pub struct HTTPErrorBody { + pub message: String } impl fmt::Display for HTTPError { @@ -200,7 +223,8 @@ impl fmt::Display for HTTPError { HTTPError::BadRequestError(message) => write!(f, "{}", message.to_owned().unwrap_or("Bad request.".to_string())), HTTPError::NotImplementedError(message) => write!(f, "{}", message.to_owned().unwrap_or("Not implemented.".to_string())), HTTPError::InternalServerError(message) => write!(f, "{}", message.to_owned().unwrap_or("Internal server error.".to_string())), - HTTPError::UnauthorizedError(message) => write!(f, "{}", message.to_owned().unwrap_or("Unauthorized.".to_string())) + HTTPError::UnauthorizedError(message) => write!(f, "{}", message.to_owned().unwrap_or("Unauthorized.".to_string())), + HTTPError::UnprocessableEntity(message) => write!(f, "{}", message.to_owned().unwrap_or("Unprocessable entity.".to_string())) } } @@ -224,11 +248,15 @@ impl IntoResponse for HTTPError { HTTPError::NotImplementedError(message) => (StatusCode::NOT_IMPLEMENTED, message.unwrap_or("Not implemented.".to_string())), + HTTPError::UnprocessableEntity(message) => (StatusCode::UNPROCESSABLE_ENTITY, message.unwrap_or("Unprocessable entity.".to_string())), + HTTPError::InternalServerError(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Something bad happened on our side. Please try again later.".to_string()), }; - return (status_code, Json(serde_json::json!({"message": error_message}))).into_response(); + return (status_code, ErasedJson::pretty(HTTPErrorBody { + message: error_message + })).into_response(); } } @@ -273,11 +301,14 @@ async fn create_database_pool() -> Result Result<(), SlashstepServerError> { database_pool: Arc::new(pool), }; - let _ = initialize_pre_defined_actions(&mut state.database_pool.get().await?).await?; - let _ = initialize_pre_defined_roles(&mut state.database_pool.get().await?).await?; + let mut postgres_client = state.database_pool.get().await?; + initialize_required_tables(&mut postgres_client).await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + drop(postgres_client); // Drop the client to release the connection back to the pool. For some reason, this doesn't happen automatically. let app_port = get_app_port_string(); let router = routes::get_router(state.clone()).with_state(state); diff --git a/src/queries/access_policies/create_function_can_principal_get_access_policy.sql b/src/queries/access_policies/create_function_can_principal_get_access_policy.sql index 873e5a1..349ea6f 100644 --- a/src/queries/access_policies/create_function_can_principal_get_access_policy.sql +++ b/src/queries/access_policies/create_function_can_principal_get_access_policy.sql @@ -14,35 +14,35 @@ CREATE OR REPLACE FUNCTION can_principal_get_access_policy(parameter_principal_t -- Set the selected resource type and ID based on the principal type. get_access_policy_action_id := ( - select - id - from - actions - where - name = 'slashstep.accessPolicies.get' + select + id + from + actions + where + name = 'slashstep.accessPolicies.get' ); selected_resource_type := access_policy_record.scoped_resource_type; selected_resource_id := CASE selected_resource_type - WHEN 'Action' THEN access_policy_record.scoped_action_id - WHEN 'ActionLogEntry' THEN access_policy_record.scoped_action_log_entry_id - WHEN 'App' THEN access_policy_record.scoped_app_id - WHEN 'AppAuthorization' THEN access_policy_record.scoped_app_authorization_id - WHEN 'AppAuthorizationCredential' THEN access_policy_record.scoped_app_authorization_credential_id - WHEN 'AppCredential' THEN access_policy_record.scoped_app_credential_id - WHEN 'Group' THEN access_policy_record.scoped_group_id - WHEN 'GroupMembership' THEN access_policy_record.scoped_group_membership_id - WHEN 'HTTPTransaction' THEN access_policy_record.scoped_http_transaction_id - WHEN 'HTTPTransactionLogEntry' THEN access_policy_record.scoped_http_transaction_log_entry_id - WHEN 'Instance' THEN NULL - WHEN 'Item' THEN access_policy_record.scoped_item_id - WHEN 'Milestone' THEN access_policy_record.scoped_milestone_id - WHEN 'Project' THEN access_policy_record.scoped_project_id - WHEN 'Role' THEN access_policy_record.scoped_role_id - WHEN 'RoleMembership' THEN access_policy_record.scoped_role_membership_id - WHEN 'Session' THEN access_policy_record.scoped_session_id - WHEN 'User' THEN access_policy_record.scoped_user_id - WHEN 'Workspace' THEN access_policy_record.scoped_workspace_id + WHEN 'Action' THEN access_policy_record.scoped_action_id + WHEN 'ActionLogEntry' THEN access_policy_record.scoped_action_log_entry_id + WHEN 'App' THEN access_policy_record.scoped_app_id + WHEN 'AppAuthorization' THEN access_policy_record.scoped_app_authorization_id + WHEN 'AppAuthorizationCredential' THEN access_policy_record.scoped_app_authorization_credential_id + WHEN 'AppCredential' THEN access_policy_record.scoped_app_credential_id + WHEN 'Group' THEN access_policy_record.scoped_group_id + WHEN 'GroupMembership' THEN access_policy_record.scoped_group_membership_id + WHEN 'HTTPTransaction' THEN access_policy_record.scoped_http_transaction_id + WHEN 'Instance' THEN NULL + WHEN 'Item' THEN access_policy_record.scoped_item_id + WHEN 'Milestone' THEN access_policy_record.scoped_milestone_id + WHEN 'Project' THEN access_policy_record.scoped_project_id + WHEN 'Role' THEN access_policy_record.scoped_role_id + WHEN 'RoleMembership' THEN access_policy_record.scoped_role_membership_id + WHEN 'ServerLogEntry' THEN access_policy_record.scoped_server_log_entry_id + WHEN 'Session' THEN access_policy_record.scoped_session_id + WHEN 'User' THEN access_policy_record.scoped_user_id + WHEN 'Workspace' THEN access_policy_record.scoped_workspace_id END; LOOP @@ -474,9 +474,9 @@ CREATE OR REPLACE FUNCTION can_principal_get_access_policy(parameter_principal_t selected_resource_type := 'Instance'; selected_resource_id := NULL; - ELSIF selected_resource_type = 'HTTPTransactionLogEntry' THEN + ELSIF selected_resource_type = 'ServerLogEntry' THEN - -- HTTPTransactionLogEntry -> HTTPTransaction + -- ServerLogEntry -> Instance -- Check if the HTTP transaction log entry has an associated access policy. SELECT permission_level, @@ -487,8 +487,8 @@ CREATE OR REPLACE FUNCTION can_principal_get_access_policy(parameter_principal_t FROM get_principal_access_policies(parameter_principal_type, parameter_principal_user_id, parameter_principal_app_id, get_access_policy_action_id) principal_access_policies WHERE - principal_access_policies.scoped_resource_type = 'HTTPTransactionLogEntry' AND - principal_access_policies.scoped_http_transaction_log_entry_id = selected_resource_id AND ( + principal_access_policies.scoped_resource_type = 'ServerLogEntry' AND + principal_access_policies.scoped_server_log_entry_id = selected_resource_id AND ( NOT needs_inheritance OR principal_access_policies.is_inheritance_enabled ) @@ -506,24 +506,8 @@ CREATE OR REPLACE FUNCTION can_principal_get_access_policy(parameter_principal_t -- Use the parent resource type. needs_inheritance := TRUE; - - SELECT - http_transaction_id - INTO - selected_resource_parent_id - FROM - http_transaction_log_entries - WHERE - http_transaction_log_entries.id = selected_resource_id; - - IF selected_resource_parent_id IS NULL THEN - - RAISE EXCEPTION 'Couldn''t find a parent HTTP transaction for HTTP transaction log entry %.', selected_resource_id; - - END IF; - - selected_resource_type := 'HTTPTransaction'; - selected_resource_id := selected_resource_parent_id; + selected_resource_type := 'Instance'; + selected_resource_id := NULL; ELSIF selected_resource_type = 'Item' THEN diff --git a/src/queries/access_policies/grant-admin-permissions.sql b/src/queries/access_policies/grant-admin-permissions.sql index ad90924..6765030 100644 --- a/src/queries/access_policies/grant-admin-permissions.sql +++ b/src/queries/access_policies/grant-admin-permissions.sql @@ -1,6 +1,6 @@ insert into access_policies ( principal_type, principal_user_id, scope_type, action_id, - permission_level, inheritance_level + permission_level, is_inheritance_enabled ) values ( - 'User', $1, 'Instance', $2, 'Admin', 'Required' + 'User', $1, 'Instance', $2, 'Admin', TRUE ); \ No newline at end of file diff --git a/src/queries/access_policies/initialize_access_policies_table.sql b/src/queries/access_policies/initialize_access_policies_table.sql index 28590a8..cf7736b 100644 --- a/src/queries/access_policies/initialize_access_policies_table.sql +++ b/src/queries/access_policies/initialize_access_policies_table.sql @@ -9,27 +9,27 @@ DO $$ ); END IF; - IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'inheritance_level') THEN - CREATE TYPE inheritance_level AS ENUM ( - 'Disabled', - 'Enabled', - 'Required' - ); - END IF; - if not exists (SELECT 1 FROM pg_type WHERE typname = 'access_policy_resource_type') THEN CREATE TYPE access_policy_resource_type AS ENUM ( - 'Instance', - 'Workspace', - 'Project', - 'Item', 'Action', - 'User', - 'Role', - 'Group', + 'ActionLogEntry', 'App', + 'AppAuthorization', + 'AppAuthorizationCredential', 'AppCredential', - 'Milestone' + 'Group', + 'GroupMembership', + 'HTTPTransaction', + 'Instance', + 'Item', + 'Milestone', + 'Project', + 'Role', + 'RoleMembership', + 'ServerLogEntry', + 'Session', + 'User', + 'Workspace' ); END IF; @@ -54,23 +54,29 @@ DO $$ /* Scopes */ scoped_resource_type access_policy_resource_type not null, - scoped_workspace_id UUID references workspaces(id) on delete cascade, - scoped_project_id UUID references projects(id) on delete cascade, - scoped_item_id UUID references items(id) on delete cascade, scoped_action_id UUID references actions(id) on delete cascade, - scoped_user_id UUID references users(id) on delete cascade, - scoped_role_id UUID references roles(id) on delete cascade, - scoped_group_id UUID references groups(id) on delete cascade, + scoped_action_log_entry_id UUID references action_log_entries(id) on delete cascade, scoped_app_id UUID references apps(id) on delete cascade, - scoped_app_credential_id UUID references app_credentials(id) on delete cascade, scoped_app_authorization_id UUID references app_authorizations(id) on delete cascade, scoped_app_authorization_credential_id UUID references app_authorization_credentials(id) on delete cascade, + scoped_app_credential_id UUID references app_credentials(id) on delete cascade, + scoped_group_id UUID references groups(id) on delete cascade, + scoped_group_membership_id UUID references group_memberships(id) on delete cascade, + scoped_http_transaction_id UUID references http_transactions(id) on delete cascade, + scoped_item_id UUID references items(id) on delete cascade, scoped_milestone_id UUID references milestones(id) on delete cascade, + scoped_project_id UUID references projects(id) on delete cascade, + scoped_role_id UUID references roles(id) on delete cascade, + scoped_role_membership_id UUID references role_memberships(id) on delete cascade, + scoped_server_log_entry_id UUID references server_log_entries(id) on delete cascade, + scoped_session_id UUID references sessions(id) on delete cascade, + scoped_user_id UUID references users(id) on delete cascade, + scoped_workspace_id UUID references workspaces(id) on delete cascade, /* Permissions */ action_id UUID not null references actions(id) on delete cascade, permission_level permission_level not null, - inheritance_level inheritance_level not null, + is_inheritance_enabled BOOLEAN not null, /* Constraints */ constraint one_principal_type check ( diff --git a/src/queries/access_policies/insert-access-policy-row.sql b/src/queries/access_policies/insert-access-policy-row.sql index 644c359..a1918a9 100644 --- a/src/queries/access_policies/insert-access-policy-row.sql +++ b/src/queries/access_policies/insert-access-policy-row.sql @@ -1,21 +1,55 @@ -insert into access_policies ( - principal_type, - principal_user_id, - principal_group_id, +INSERT INTO access_policies ( + action_id, + permission_level, + is_inheritance_enabled, + principal_type, + principal_user_id, + principal_group_id, principal_role_id, principal_app_id, - scoped_resource_type, + scoped_resource_type, scoped_action_id, + scoped_action_log_entry_id, scoped_app_id, + scoped_app_authorization_id, + scoped_app_authorization_credential_id, scoped_app_credential_id, - scoped_group_id, + scoped_group_membership_id, + scoped_http_transaction_id, scoped_item_id, scoped_milestone_id, scoped_project_id, scoped_role_id, + scoped_role_membership_id, + scoped_server_log_entry_id, + scoped_session_id, scoped_user_id, - scoped_workspace_id, - permission_level, - inheritance_level, - action_id -) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19) returning *; \ No newline at end of file + scoped_workspace_id +) VALUES ( + $1, + $2, + $3, + $4, + $5, + $6, + $7, + $8, + $9, + $10, + $11, + $12, + $13, + $14, + $15, + $16, + $17, + $18, + $19, + $20, + $21, + $22, + $23, + $24, + $25, + $26 +) RETURNING *; \ No newline at end of file diff --git a/src/queries/action_log_entries/add_access_policies_reference.sql b/src/queries/action_log_entries/add_access_policies_reference.sql new file mode 100644 index 0000000..5973c4f --- /dev/null +++ b/src/queries/action_log_entries/add_access_policies_reference.sql @@ -0,0 +1,7 @@ +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'action_log_entries_access_policies_fk') THEN + ALTER TABLE action_log_entries ADD CONSTRAINT action_log_entries_access_policies_fk FOREIGN KEY (target_access_policy_id) REFERENCES access_policies(id); + END IF; + END +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/queries/action_log_entries/get_action_log_entry_row_by_id.sql b/src/queries/action_log_entries/get_action_log_entry_row_by_id.sql new file mode 100644 index 0000000..de11056 --- /dev/null +++ b/src/queries/action_log_entries/get_action_log_entry_row_by_id.sql @@ -0,0 +1 @@ +SELECT * FROM action_log_entries WHERE id = $1; \ No newline at end of file diff --git a/src/queries/action_log_entries/initialize_action_log_entries_table.sql b/src/queries/action_log_entries/initialize_action_log_entries_table.sql new file mode 100644 index 0000000..7095e8b --- /dev/null +++ b/src/queries/action_log_entries/initialize_action_log_entries_table.sql @@ -0,0 +1,65 @@ +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'action_log_entry_actor_type') THEN + CREATE TYPE action_log_entry_actor_type AS ENUM ( + 'User', + 'App' + ); + END IF; + + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'action_log_entry_target_resource_type') THEN + CREATE TYPE action_log_entry_target_resource_type AS ENUM ( + 'AccessPolicy', + 'Action', + 'ActionLogEntry', + 'App', + 'AppAuthorization', + 'AppAuthorizationCredential', + 'AppCredential', + 'Group', + 'GroupMembership', + 'HTTPTransaction', + 'Instance', + 'Item', + 'Project', + 'Role', + 'RoleMembership', + 'ServerLogEntry', + 'Session', + 'User', + 'Workspace', + 'Milestone' + ); + END IF; + + CREATE TABLE IF NOT EXISTS action_log_entries ( + id UUID default uuidv7() PRIMARY KEY, + action_id UUID NOT NULL REFERENCES actions(id), + http_transaction_id UUID REFERENCES http_transactions(id), + actor_type action_log_entry_actor_type not null, + actor_user_id UUID REFERENCES users(id), + actor_app_id UUID REFERENCES apps(id), + target_resource_type action_log_entry_target_resource_type not null, + target_access_policy_id UUID, -- This needs to be referenced after the access policies table is created. + target_action_id UUID REFERENCES actions(id), + target_action_log_entry_id UUID REFERENCES action_log_entries(id), + target_app_id UUID REFERENCES apps(id), + target_app_authorization_id UUID REFERENCES app_authorizations(id), + target_app_authorization_credential_id UUID REFERENCES app_authorization_credentials(id), + target_app_credential_id UUID REFERENCES app_credentials(id), + target_group_id UUID REFERENCES groups(id), + target_group_membership_id UUID REFERENCES group_memberships(id), + target_http_transaction_id UUID REFERENCES http_transactions(id), + target_item_id UUID REFERENCES items(id), + target_milestone_id UUID REFERENCES milestones(id), + target_project_id UUID REFERENCES projects(id), + target_role_id UUID REFERENCES roles(id), + target_role_membership_id UUID REFERENCES role_memberships(id), + target_server_log_entry_id UUID REFERENCES server_log_entries(id), + target_session_id UUID REFERENCES sessions(id), + target_user_id UUID REFERENCES users(id), + target_workspace_id UUID REFERENCES workspaces(id), + reason TEXT + ); + END +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/queries/app-authorization-credentials/get-app-authorization-credential-row.sql b/src/queries/app-authorization-credentials/get-app-authorization-credential-row-by-id.sql similarity index 100% rename from src/queries/app-authorization-credentials/get-app-authorization-credential-row.sql rename to src/queries/app-authorization-credentials/get-app-authorization-credential-row-by-id.sql diff --git a/src/queries/group_memberships/get_group_membership_row_by_id.sql b/src/queries/group_memberships/get_group_membership_row_by_id.sql new file mode 100644 index 0000000..f676c85 --- /dev/null +++ b/src/queries/group_memberships/get_group_membership_row_by_id.sql @@ -0,0 +1 @@ +SELECT * FROM group_memberships WHERE id = $1; \ No newline at end of file diff --git a/src/queries/group_memberships/initialize_group_memberships_table.sql b/src/queries/group_memberships/initialize_group_memberships_table.sql new file mode 100644 index 0000000..5721389 --- /dev/null +++ b/src/queries/group_memberships/initialize_group_memberships_table.sql @@ -0,0 +1,21 @@ +DO $$ + BEGIN + IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'group_membership_principal_type') THEN + CREATE TYPE group_membership_principal_type AS ENUM ( + 'App', + 'Group', + 'User' + ); + END IF; + + CREATE TABLE IF NOT EXISTS group_memberships ( + id UUID default uuidv7() primary key, + group_id UUID references groups(id) on delete cascade, + principal_id UUID references users(id) on delete cascade, + principal_type group_membership_principal_type not null, + principal_user_id UUID references users(id) on delete cascade, + principal_group_id UUID references groups(id) on delete cascade, + principal_app_id UUID references apps(id) on delete cascade + ); + END +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/queries/http-transactions/create-http-transactions-table.sql b/src/queries/http-transactions/create-http-transactions-table.sql index 1c9c6f5..b47e42f 100644 --- a/src/queries/http-transactions/create-http-transactions-table.sql +++ b/src/queries/http-transactions/create-http-transactions-table.sql @@ -1,9 +1,9 @@ -create table if not exists http_requests ( - id UUID default uuidv7() primary key, - method text not null, - url text not null, - ip_address inet not null, - headers text not null, - status_code integer, - expiration_date timestamptz not null default now() + interval '14 days' +CREATE TABLE IF NOT EXISTS http_transactions ( + id UUID DEFAULT uuidv7() PRIMARY KEY, + method TEXT NOT NULL, + url TEXT NOT NULL, + ip_address INET NOT NULL, + headers TEXT NOT NULL, + status_code INTEGER, + expiration_date TIMESTAMPTZ NOT NULL DEFAULT NOW() + INTERVAL '14 days' ); \ No newline at end of file diff --git a/src/queries/http-transactions/create-hydrated-http-transactions-view.sql b/src/queries/http-transactions/create-hydrated-http-transactions-view.sql deleted file mode 100644 index eb014a9..0000000 --- a/src/queries/http-transactions/create-hydrated-http-transactions-view.sql +++ /dev/null @@ -1,5 +0,0 @@ -create or replace view hydrated_http_requests as - select - http_requests.* - from - http_requests \ No newline at end of file diff --git a/src/queries/http-transactions/insert-http-transaction-row.sql b/src/queries/http-transactions/insert-http-transaction-row.sql index 8c3a2c5..831d782 100644 --- a/src/queries/http-transactions/insert-http-transaction-row.sql +++ b/src/queries/http-transactions/insert-http-transaction-row.sql @@ -1,15 +1,15 @@ -insert into http_requests ( +INSERT INTO http_transactions ( method, url, ip_address, headers, status_code, expiration_date -) values ( +) VALUES ( $1, $2, $3, $4, $5, $6 -) returning *; \ No newline at end of file +) RETURNING *; \ No newline at end of file diff --git a/src/queries/server_log_entries/create-hydrated-server-log-entries-view.sql b/src/queries/server_log_entries/create-hydrated-server-log-entries-view.sql deleted file mode 100644 index 4fb0843..0000000 --- a/src/queries/server_log_entries/create-hydrated-server-log-entries-view.sql +++ /dev/null @@ -1,5 +0,0 @@ -create or replace view hydrated_server_log_entries as - select - server_log_entries.* - from - server_log_entries \ No newline at end of file diff --git a/src/queries/server_log_entries/create-server-log-entries-table.sql b/src/queries/server_log_entries/initialize_server_log_entries_table.sql similarity index 51% rename from src/queries/server_log_entries/create-server-log-entries-table.sql rename to src/queries/server_log_entries/initialize_server_log_entries_table.sql index 1d6ea3f..7a7e919 100644 --- a/src/queries/server_log_entries/create-server-log-entries-table.sql +++ b/src/queries/server_log_entries/initialize_server_log_entries_table.sql @@ -10,12 +10,12 @@ begin 'Critical' ); end if; -end -$$ LANGUAGE plpgsql; -create table if not exists server_log_entries ( - id UUID default uuidv7() primary key, - message text not null, - http_request_id UUID references http_requests(id) on delete cascade, - level server_log_entry_level not null -); \ No newline at end of file + CREATE TABLE IF NOT EXISTS server_log_entries ( + id UUID DEFAULT uuidv7() PRIMARY KEY, + message TEXT NOT NULL, + http_transaction_id UUID REFERENCES http_transactions(id), + level server_log_entry_level NOT NULL + ); +end +$$ LANGUAGE plpgsql; \ No newline at end of file diff --git a/src/resources.rs b/src/resources.rs index d978238..31b596a 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -1,10 +1,12 @@ pub mod access_policy; pub mod action; +pub mod action_log_entry; pub mod app_authorization_credential; pub mod app_authorization; pub mod app_credential; pub mod app; pub mod group; +pub mod group_membership; pub mod item; pub mod milestone; pub mod project; diff --git a/src/resources/access_policy/mod.rs b/src/resources/access_policy/mod.rs index c22337e..abec03d 100644 --- a/src/resources/access_policy/mod.rs +++ b/src/resources/access_policy/mod.rs @@ -22,7 +22,7 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use uuid::Uuid; use crate::{ - resources::{action::{Action, ActionError}, app::{App, AppError, AppParentResourceType}, app_credential::{AppCredential, AppCredentialError}, item::{Item, ItemError}, milestone::{Milestone, MilestoneError, MilestoneParentResourceType}, project::{Project, ProjectError}, role::{Role, RoleError, RoleParentResourceType}}, utilities::slashstepql::{ + resources::{action::{Action, ActionError}, action_log_entry::{ActionLogEntry, ActionLogEntryError}, app::{App, AppError, AppParentResourceType}, app_authorization::{AppAuthorization, AppAuthorizationError, AppAuthorizationParentResourceType}, app_authorization_credential::{AppAuthorizationCredential, AppAuthorizationCredentialError}, app_credential::{AppCredential, AppCredentialError}, group_membership::{GroupMembership, GroupMembershipError}, item::{Item, ItemError}, milestone::{Milestone, MilestoneError, MilestoneParentResourceType}, project::{Project, ProjectError}, role::{Role, RoleError, RoleParentResourceType}, role_memberships::{RoleMembership, RoleMembershipError}, session::{Session, SessionError}}, utilities::slashstepql::{ SlashstepQLError, SlashstepQLFilterSanitizer, SlashstepQLParameterType, @@ -71,6 +71,7 @@ pub const UUID_QUERY_KEYS: &[&str] = &[ ]; pub const DEFAULT_ACCESS_POLICY_LIST_LIMIT: i64 = 1000; +pub const DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT: i64 = 1000; #[derive(Debug, PartialEq, Eq, ToSql, FromSql, Clone, Copy, Serialize, Deserialize, Default, PartialOrd)] #[postgres(name = "permission_level")] @@ -137,6 +138,9 @@ pub enum AccessPolicyError { #[error(transparent)] ActionError(#[from] ActionError), + #[error(transparent)] + ActionLogEntryError(#[from] ActionLogEntryError), + #[error(transparent)] AppError(#[from] AppError), @@ -147,7 +151,22 @@ pub enum AccessPolicyError { RoleError(#[from] RoleError), #[error(transparent)] - MilestoneError(#[from] MilestoneError) + RoleMembershipError(#[from] RoleMembershipError), + + #[error(transparent)] + SessionError(#[from] SessionError), + + #[error(transparent)] + MilestoneError(#[from] MilestoneError), + + #[error(transparent)] + AppAuthorizationError(#[from] AppAuthorizationError), + + #[error(transparent)] + AppAuthorizationCredentialError(#[from] AppAuthorizationCredentialError), + + #[error(transparent)] + GroupMembershipError(#[from] GroupMembershipError), } impl FromStr for AccessPolicyPermissionLevel { @@ -168,73 +187,53 @@ impl FromStr for AccessPolicyPermissionLevel { } -#[derive(Debug, PartialEq, Eq, ToSql, FromSql, Clone, Copy, Serialize, Deserialize, Default)] -#[postgres(name = "inheritance_level")] -pub enum AccessPolicyInheritanceLevel { - #[default] - Disabled, - Enabled, - Required -} - -impl fmt::Display for AccessPolicyInheritanceLevel { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - AccessPolicyInheritanceLevel::Disabled => write!(f, "Disabled"), - AccessPolicyInheritanceLevel::Enabled => write!(f, "Enabled"), - AccessPolicyInheritanceLevel::Required => write!(f, "Required") - } - } -} - -impl FromStr for AccessPolicyInheritanceLevel { - - type Err = AccessPolicyError; - - fn from_str(string: &str) -> Result { - - match string { - "Disabled" => Ok(AccessPolicyInheritanceLevel::Disabled), - "Enabled" => Ok(AccessPolicyInheritanceLevel::Enabled), - "Required" => Ok(AccessPolicyInheritanceLevel::Required), - _ => Err(AccessPolicyError::InvalidInheritanceLevel(string.to_string())) - } - - } - -} - #[derive(Debug, Clone, PartialEq, Eq, ToSql, FromSql, Serialize, Deserialize, Default)] #[postgres(name = "access_policy_resource_type")] pub enum AccessPolicyResourceType { + Action, + ActionLogEntry, + App, + AppAuthorization, + AppAuthorizationCredential, + AppCredential, + Group, + GroupMembership, + HTTPTransaction, #[default] Instance, - Workspace, - Project, Item, - Action, - User, + Project, Role, - Group, - App, - AppCredential, + RoleMembership, + ServerLogEntry, + Session, + User, Milestone, + Workspace } impl fmt::Display for AccessPolicyResourceType { fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { match self { - AccessPolicyResourceType::Workspace => write!(formatter, "Workspace"), - AccessPolicyResourceType::Project => write!(formatter, "Project"), - AccessPolicyResourceType::Milestone => write!(formatter, "Milestone"), - AccessPolicyResourceType::Item => write!(formatter, "Item"), AccessPolicyResourceType::Action => write!(formatter, "Action"), - AccessPolicyResourceType::Role => write!(formatter, "Role"), - AccessPolicyResourceType::Group => write!(formatter, "Group"), - AccessPolicyResourceType::User => write!(formatter, "User"), + AccessPolicyResourceType::ActionLogEntry => write!(formatter, "ActionLogEntry"), AccessPolicyResourceType::App => write!(formatter, "App"), + AccessPolicyResourceType::AppAuthorization => write!(formatter, "AppAuthorization"), + AccessPolicyResourceType::AppAuthorizationCredential => write!(formatter, "AppAuthorizationCredential"), AccessPolicyResourceType::AppCredential => write!(formatter, "AppCredential"), - AccessPolicyResourceType::Instance => write!(formatter, "Instance") + AccessPolicyResourceType::Group => write!(formatter, "Group"), + AccessPolicyResourceType::GroupMembership => write!(formatter, "GroupMembership"), + AccessPolicyResourceType::HTTPTransaction => write!(formatter, "HTTPTransaction"), + AccessPolicyResourceType::Instance => write!(formatter, "Instance"), + AccessPolicyResourceType::Item => write!(formatter, "Item"), + AccessPolicyResourceType::Milestone => write!(formatter, "Milestone"), + AccessPolicyResourceType::Project => write!(formatter, "Project"), + AccessPolicyResourceType::Role => write!(formatter, "Role"), + AccessPolicyResourceType::RoleMembership => write!(formatter, "RoleMembership"), + AccessPolicyResourceType::ServerLogEntry => write!(formatter, "ServerLogEntry"), + AccessPolicyResourceType::Session => write!(formatter, "Session"), + AccessPolicyResourceType::User => write!(formatter, "User"), + AccessPolicyResourceType::Workspace => write!(formatter, "Workspace") } } } @@ -246,16 +245,25 @@ impl FromStr for AccessPolicyResourceType { fn from_str(string: &str) -> Result { match string { + "Action" => Ok(AccessPolicyResourceType::Action), + "ActionLogEntry" => Ok(AccessPolicyResourceType::ActionLogEntry), + "App" => Ok(AccessPolicyResourceType::App), + "AppAuthorization" => Ok(AccessPolicyResourceType::AppAuthorization), + "AppAuthorizationCredential" => Ok(AccessPolicyResourceType::AppAuthorizationCredential), + "AppCredential" => Ok(AccessPolicyResourceType::AppCredential), + "Group" => Ok(AccessPolicyResourceType::Group), + "GroupMembership" => Ok(AccessPolicyResourceType::GroupMembership), + "HTTPTransaction" => Ok(AccessPolicyResourceType::HTTPTransaction), "Instance" => Ok(AccessPolicyResourceType::Instance), - "Workspace" => Ok(AccessPolicyResourceType::Workspace), - "Project" => Ok(AccessPolicyResourceType::Project), - "Milestone" => Ok(AccessPolicyResourceType::Milestone), "Item" => Ok(AccessPolicyResourceType::Item), - "Action" => Ok(AccessPolicyResourceType::Action), + "Milestone" => Ok(AccessPolicyResourceType::Milestone), + "Project" => Ok(AccessPolicyResourceType::Project), "Role" => Ok(AccessPolicyResourceType::Role), - "Group" => Ok(AccessPolicyResourceType::Group), + "RoleMembership" => Ok(AccessPolicyResourceType::RoleMembership), + "ServerLogEntry" => Ok(AccessPolicyResourceType::ServerLogEntry), + "Session" => Ok(AccessPolicyResourceType::Session), "User" => Ok(AccessPolicyResourceType::User), - "App" => Ok(AccessPolicyResourceType::App), + "Workspace" => Ok(AccessPolicyResourceType::Workspace), _ => Err(AccessPolicyError::InvalidScopedResourceType(string.to_string())) } @@ -331,7 +339,7 @@ pub struct InitialAccessPolicyProperties { pub permission_level: AccessPolicyPermissionLevel, - pub inheritance_level: AccessPolicyInheritanceLevel, + pub is_inheritance_enabled: bool, pub principal_type: AccessPolicyPrincipalType, @@ -347,12 +355,22 @@ pub struct InitialAccessPolicyProperties { pub scoped_action_id: Option, + pub scoped_action_log_entry_id: Option, + pub scoped_app_id: Option, + pub scoped_app_authorization_id: Option, + + pub scoped_app_authorization_credential_id: Option, + pub scoped_app_credential_id: Option, pub scoped_group_id: Option, + pub scoped_group_membership_id: Option, + + pub scoped_http_transaction_id: Option, + pub scoped_item_id: Option, pub scoped_milestone_id: Option, @@ -361,6 +379,12 @@ pub struct InitialAccessPolicyProperties { pub scoped_role_id: Option, + pub scoped_role_membership_id: Option, + + pub scoped_server_log_entry_id: Option, + + pub scoped_session_id: Option, + pub scoped_user_id: Option, pub scoped_workspace_id: Option @@ -373,7 +397,7 @@ pub struct EditableAccessPolicyProperties { permission_level: Option, - inheritance_level: Option, + is_inheritance_enabled: Option, } @@ -397,7 +421,7 @@ pub struct AccessPolicy { pub permission_level: AccessPolicyPermissionLevel, - pub inheritance_level: AccessPolicyInheritanceLevel, + pub is_inheritance_enabled: bool, pub principal_type: AccessPolicyPrincipalType, @@ -413,12 +437,22 @@ pub struct AccessPolicy { pub scoped_action_id: Option, + pub scoped_action_log_entry_id: Option, + pub scoped_app_id: Option, + pub scoped_app_authorization_id: Option, + + pub scoped_app_authorization_credential_id: Option, + pub scoped_app_credential_id: Option, pub scoped_group_id: Option, + pub scoped_group_membership_id: Option, + + pub scoped_http_transaction_id: Option, + pub scoped_item_id: Option, pub scoped_milestone_id: Option, @@ -427,6 +461,12 @@ pub struct AccessPolicy { pub scoped_role_id: Option, + pub scoped_role_membership_id: Option, + + pub scoped_server_log_entry_id: Option, + + pub scoped_session_id: Option, + pub scoped_user_id: Option, pub scoped_workspace_id: Option @@ -492,6 +532,9 @@ impl AccessPolicy { // Insert the access policy into the database. let query = include_str!("../../queries/access_policies/insert-access-policy-row.sql"); let parameters: &[&(dyn ToSql + Sync)] = &[ + &initial_properties.action_id, + &initial_properties.permission_level, + &initial_properties.is_inheritance_enabled, &initial_properties.principal_type, &initial_properties.principal_user_id, &initial_properties.principal_group_id, @@ -499,18 +542,22 @@ impl AccessPolicy { &initial_properties.principal_app_id, &initial_properties.scoped_resource_type, &initial_properties.scoped_action_id, + &initial_properties.scoped_action_log_entry_id, &initial_properties.scoped_app_id, + &initial_properties.scoped_app_authorization_id, + &initial_properties.scoped_app_authorization_credential_id, &initial_properties.scoped_app_credential_id, - &initial_properties.scoped_group_id, + &initial_properties.scoped_group_membership_id, + &initial_properties.scoped_http_transaction_id, &initial_properties.scoped_item_id, &initial_properties.scoped_milestone_id, &initial_properties.scoped_project_id, &initial_properties.scoped_role_id, + &initial_properties.scoped_role_membership_id, + &initial_properties.scoped_server_log_entry_id, + &initial_properties.scoped_session_id, &initial_properties.scoped_user_id, - &initial_properties.scoped_workspace_id, - &initial_properties.permission_level, - &initial_properties.inheritance_level, - &initial_properties.action_id + &initial_properties.scoped_workspace_id ]; let row = postgres_client.query_one(query, parameters).await.map_err(|error| match error.as_db_error() { @@ -530,28 +577,7 @@ impl AccessPolicy { })?; - let access_policy = AccessPolicy { - id: row.get("id"), - action_id: row.get("action_id"), - permission_level: row.get("permission_level"), - inheritance_level: row.get("inheritance_level"), - principal_type: row.get("principal_type"), - principal_user_id: row.get("principal_user_id"), - principal_group_id: row.get("principal_group_id"), - principal_role_id: row.get("principal_role_id"), - principal_app_id: row.get("principal_app_id"), - scoped_resource_type: row.get("scoped_resource_type"), - scoped_action_id: row.get("scoped_action_id"), - scoped_app_id: row.get("scoped_app_id"), - scoped_app_credential_id: row.get("scoped_app_credential_id"), - scoped_group_id: row.get("scoped_group_id"), - scoped_item_id: row.get("scoped_item_id"), - scoped_milestone_id: row.get("scoped_milestone_id"), - scoped_project_id: row.get("scoped_project_id"), - scoped_role_id: row.get("scoped_role_id"), - scoped_user_id: row.get("scoped_user_id"), - scoped_workspace_id: row.get("scoped_workspace_id") - }; + let access_policy = AccessPolicy::convert_from_row(&row); return Ok(access_policy); @@ -588,7 +614,7 @@ impl AccessPolicy { id: row.get("id"), action_id: row.get("action_id"), permission_level: row.get("permission_level"), - inheritance_level: row.get("inheritance_level"), + is_inheritance_enabled: row.get("is_inheritance_enabled"), principal_type: row.get("principal_type"), principal_user_id: row.get("principal_user_id"), principal_group_id: row.get("principal_group_id"), @@ -596,13 +622,21 @@ impl AccessPolicy { principal_app_id: row.get("principal_app_id"), scoped_resource_type: row.get("scoped_resource_type"), scoped_action_id: row.get("scoped_action_id"), + scoped_action_log_entry_id: row.get("scoped_action_log_entry_id"), scoped_app_id: row.get("scoped_app_id"), + scoped_app_authorization_id: row.get("scoped_app_authorization_id"), + scoped_app_authorization_credential_id: row.get("scoped_app_authorization_credential_id"), scoped_app_credential_id: row.get("scoped_app_credential_id"), scoped_group_id: row.get("scoped_group_id"), + scoped_group_membership_id: row.get("scoped_group_membership_id"), + scoped_http_transaction_id: row.get("scoped_http_transaction_id"), scoped_item_id: row.get("scoped_item_id"), scoped_milestone_id: row.get("scoped_milestone_id"), scoped_project_id: row.get("scoped_project_id"), scoped_role_id: row.get("scoped_role_id"), + scoped_role_membership_id: row.get("scoped_role_membership_id"), + scoped_server_log_entry_id: row.get("scoped_server_log_entry_id"), + scoped_session_id: row.get("scoped_session_id"), scoped_user_id: row.get("scoped_user_id"), scoped_workspace_id: row.get("scoped_workspace_id") }; @@ -657,13 +691,6 @@ impl AccessPolicy { }, - "inheritance_level" => { - - let inheritance_level = AccessPolicyInheritanceLevel::from_str(string_value)?; - parameters.push(Box::new(inheritance_level)); - - }, - "permission_level" => { let permission_level = AccessPolicyPermissionLevel::from_str(string_value)?; @@ -710,8 +737,8 @@ impl AccessPolicy { let sanitizer_options = SlashstepQLSanitizeFunctionOptions { filter: query.to_string(), allowed_fields: ALLOWED_QUERY_KEYS.into_iter().map(|string| string.to_string()).collect(), - default_limit: Some(DEFAULT_ACCESS_POLICY_LIST_LIMIT), - maximum_limit: None, + default_limit: Some(DEFAULT_ACCESS_POLICY_LIST_LIMIT), // TODO: Make this configurable through resource policies. + maximum_limit: Some(DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT), // TODO: Make this configurable through resource policies. should_ignore_limit: false, should_ignore_offset: false }; @@ -744,7 +771,6 @@ impl AccessPolicy { }; let where_clause = if where_clause == "" { where_clause } else { format!(" where {}", where_clause) }; - let limit_clause = sanitized_filter.limit.and_then(|limit| Some(format!(" limit {}", limit))).unwrap_or("".to_string()); let offset_clause = sanitized_filter.offset.and_then(|offset| Some(format!(" offset {}", offset))).unwrap_or("".to_string()); let query = format!("select * from access_policies{}{}{}", where_clause, limit_clause, offset_clause); @@ -767,17 +793,25 @@ impl AccessPolicy { match resource_type { + AccessPolicyResourceType::Action => query_clauses.push(format!("scoped_action_id = {}", resource_id.expect("An action ID must be provided."))), + AccessPolicyResourceType::ActionLogEntry => query_clauses.push(format!("scoped_action_log_entry_id = {}", resource_id.expect("An action log entry ID must be provided."))), + AccessPolicyResourceType::App => query_clauses.push(format!("scoped_app_id = {}", resource_id.expect("An app ID must be provided."))), + AccessPolicyResourceType::AppAuthorization => query_clauses.push(format!("scoped_app_authorization_id = {}", resource_id.expect("An app authorization ID must be provided."))), + AccessPolicyResourceType::AppAuthorizationCredential => query_clauses.push(format!("scoped_app_authorization_credential_id = {}", resource_id.expect("An app authorization credential ID must be provided."))), + AccessPolicyResourceType::AppCredential => query_clauses.push(format!("scoped_app_credential_id = {}", resource_id.expect("An app credential ID must be provided."))), + AccessPolicyResourceType::Group => query_clauses.push(format!("scoped_group_id = {}", resource_id.expect("A group ID must be provided."))), + AccessPolicyResourceType::GroupMembership => query_clauses.push(format!("scoped_group_membership_id = {}", resource_id.expect("A group membership ID must be provided."))), + AccessPolicyResourceType::HTTPTransaction => query_clauses.push(format!("scoped_http_transaction_id = {}", resource_id.expect("An HTTP transaction ID must be provided."))), AccessPolicyResourceType::Instance => query_clauses.push(format!("scoped_resource_type = 'Instance'")), - AccessPolicyResourceType::Workspace => query_clauses.push(format!("scoped_workspace_id = {}", resource_id.expect("A workspace ID must be provided."))), - AccessPolicyResourceType::Project => query_clauses.push(format!("scoped_project_id = {}", resource_id.expect("A project ID must be provided."))), - AccessPolicyResourceType::Milestone => query_clauses.push(format!("scoped_milestone_id = {}", resource_id.expect("A milestone ID must be provided."))), AccessPolicyResourceType::Item => query_clauses.push(format!("scoped_item_id = {}", resource_id.expect("An item ID must be provided."))), - AccessPolicyResourceType::Action => query_clauses.push(format!("scoped_action_id = {}", resource_id.expect("An action ID must be provided."))), - AccessPolicyResourceType::User => query_clauses.push(format!("scoped_user_id = {}", resource_id.expect("A user ID must be provided."))), + AccessPolicyResourceType::Milestone => query_clauses.push(format!("scoped_milestone_id = {}", resource_id.expect("A milestone ID must be provided."))), + AccessPolicyResourceType::Project => query_clauses.push(format!("scoped_project_id = {}", resource_id.expect("A project ID must be provided."))), AccessPolicyResourceType::Role => query_clauses.push(format!("scoped_role_id = {}", resource_id.expect("A role ID must be provided."))), - AccessPolicyResourceType::Group => query_clauses.push(format!("scoped_group_id = {}", resource_id.expect("A group ID must be provided."))), - AccessPolicyResourceType::App => query_clauses.push(format!("scoped_app_id = {}", resource_id.expect("An app ID must be provided."))), - AccessPolicyResourceType::AppCredential => query_clauses.push(format!("scoped_app_credential_id = {}", resource_id.expect("An app credential ID must be provided."))) + AccessPolicyResourceType::RoleMembership => query_clauses.push(format!("scoped_role_membership_id = {}", resource_id.expect("A role membership ID must be provided."))), + AccessPolicyResourceType::ServerLogEntry => query_clauses.push(format!("scoped_server_log_entry_id = {}", resource_id.expect("A server log entry ID must be provided."))), + AccessPolicyResourceType::Session => query_clauses.push(format!("scoped_session_id = {}", resource_id.expect("A session ID must be provided."))), + AccessPolicyResourceType::User => query_clauses.push(format!("scoped_user_id = {}", resource_id.expect("A user ID must be provided."))), + AccessPolicyResourceType::Workspace => query_clauses.push(format!("scoped_workspace_id = {}", resource_id.expect("A workspace ID must be provided."))) } @@ -845,7 +879,7 @@ impl AccessPolicy { postgres_client.query("begin;", &[]).await?; let (parameter_boxes, query) = Self::add_parameter(parameter_boxes, query, "permission_level", &properties.permission_level); - let (mut parameter_boxes, mut query) = Self::add_parameter(parameter_boxes, query, "inheritance_level", &properties.inheritance_level); + let (mut parameter_boxes, mut query) = Self::add_parameter(parameter_boxes, query, "is_inheritance_enabled", &properties.is_inheritance_enabled); query.push_str(format!(" where id = ${} returning *;", parameter_boxes.len() + 1).as_str()); parameter_boxes.push(Box::new(&self.id)); @@ -864,26 +898,24 @@ impl AccessPolicy { let mut selected_resource_type: AccessPolicyResourceType = self.scoped_resource_type.clone(); let mut selected_resource_id: Option = match self.scoped_resource_type { - AccessPolicyResourceType::Instance => None, - AccessPolicyResourceType::Action => self.scoped_action_id, - + AccessPolicyResourceType::ActionLogEntry => self.scoped_action_log_entry_id, AccessPolicyResourceType::App => self.scoped_app_id, - + AccessPolicyResourceType::AppAuthorization => self.scoped_app_authorization_id, + AccessPolicyResourceType::AppAuthorizationCredential => self.scoped_app_authorization_credential_id, AccessPolicyResourceType::AppCredential => self.scoped_app_credential_id, - AccessPolicyResourceType::Group => self.scoped_group_id, - + AccessPolicyResourceType::GroupMembership => self.scoped_group_membership_id, + AccessPolicyResourceType::HTTPTransaction => self.scoped_http_transaction_id, + AccessPolicyResourceType::Instance => None, AccessPolicyResourceType::Item => self.scoped_item_id, - AccessPolicyResourceType::Milestone => self.scoped_milestone_id, - AccessPolicyResourceType::Project => self.scoped_project_id, - AccessPolicyResourceType::Role => self.scoped_role_id, - + AccessPolicyResourceType::RoleMembership => self.scoped_role_membership_id, + AccessPolicyResourceType::ServerLogEntry => self.scoped_server_log_entry_id, + AccessPolicyResourceType::Session => self.scoped_session_id, AccessPolicyResourceType::User => self.scoped_user_id, - AccessPolicyResourceType::Workspace => self.scoped_workspace_id }; @@ -892,9 +924,6 @@ impl AccessPolicy { match selected_resource_type { - // Instance - AccessPolicyResourceType::Instance => break, - // Action -> (App | Instance) AccessPolicyResourceType::Action => { @@ -932,119 +961,133 @@ impl AccessPolicy { } - } + }, - // Workspace -> Instance - AccessPolicyResourceType::Workspace => { + // ActionLogEntry -> Action + AccessPolicyResourceType::ActionLogEntry => { - let Some(scoped_workspace_id) = self.scoped_workspace_id else { + let Some(scoped_action_log_entry_id) = self.scoped_action_log_entry_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::ActionLogEntry)); }; - hierarchy.push((AccessPolicyResourceType::Workspace, Some(scoped_workspace_id))); + hierarchy.push((AccessPolicyResourceType::ActionLogEntry, Some(scoped_action_log_entry_id))); + + let action_log_entry = match ActionLogEntry::get_by_id(&scoped_action_log_entry_id, postgres_client).await { + + Ok(action_log_entry) => action_log_entry, + + Err(error) => return Err(AccessPolicyError::ActionLogEntryError(error)) + + }; + + selected_resource_type = AccessPolicyResourceType::Action; + selected_resource_id = Some(action_log_entry.action_id); }, - // Project -> Workspace - AccessPolicyResourceType::Project => { + // App -> (Workspace | User | Instance) + AccessPolicyResourceType::App => { - let Some(scoped_project_id) = self.scoped_project_id else { + let Some(app_id) = selected_resource_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Project)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::App)); }; - hierarchy.push((AccessPolicyResourceType::Project, Some(scoped_project_id))); + hierarchy.push((AccessPolicyResourceType::App, Some(app_id))); - let project = match Project::get_by_id(&scoped_project_id, postgres_client).await { + let app = match App::get_by_id(&app_id, postgres_client).await { + + Ok(app) => app, - Ok(project) => project, - Err(error) => match error { - ProjectError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::Project)), + AppError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::App)), - _ => return Err(AccessPolicyError::ProjectError(error)) + _ => return Err(AccessPolicyError::AppError(error)) } }; - selected_resource_type = AccessPolicyResourceType::Workspace; - selected_resource_id = Some(project.workspace_id); + match app.parent_resource_type { - }, + AppParentResourceType::Instance => { - // Item -> Project - AccessPolicyResourceType::Item => { + selected_resource_type = AccessPolicyResourceType::Instance; + selected_resource_id = None; - let Some(scoped_item_id) = self.scoped_item_id else { + }, - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Item)); + AppParentResourceType::Workspace => { - }; + let Some(workspace_id) = app.parent_workspace_id else { - hierarchy.push((AccessPolicyResourceType::Item, Some(scoped_item_id))); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); - let item = Item::get_by_id(&scoped_item_id, postgres_client).await?; + }; - selected_resource_type = AccessPolicyResourceType::Project; - selected_resource_id = Some(item.project_id); + selected_resource_type = AccessPolicyResourceType::Workspace; + selected_resource_id = Some(workspace_id); - }, + }, - // User -> Instance - AccessPolicyResourceType::User => { + AppParentResourceType::User => { - let Some(scoped_user_id) = self.scoped_user_id else { + let Some(user_id) = app.parent_user_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::User)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::User)); - }; + }; - hierarchy.push((AccessPolicyResourceType::User, Some(scoped_user_id))); + selected_resource_type = AccessPolicyResourceType::User; + selected_resource_id = Some(user_id); + + } + + } }, - // Role -> (Project | Workspace | Group | Instance) - AccessPolicyResourceType::Role => { + // AppAuthorization -> (User | Workspace | Instance) + AccessPolicyResourceType::AppAuthorization => { - let Some(scoped_role_id) = self.scoped_role_id else { + let Some(app_authorization_id) = selected_resource_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Role)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::AppAuthorization)); }; - hierarchy.push((AccessPolicyResourceType::Role, Some(scoped_role_id))); + hierarchy.push((AccessPolicyResourceType::AppAuthorization, Some(app_authorization_id))); - let role = match Role::get_by_id(&scoped_role_id, postgres_client).await { + let app_authorization = match AppAuthorization::get_by_id(&app_authorization_id, postgres_client).await { - Ok(role) => role, + Ok(app_authorization) => app_authorization, Err(error) => match error { - RoleError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::Role)), + AppAuthorizationError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::AppAuthorization)), - _ => return Err(AccessPolicyError::RoleError(error)) + _ => return Err(AccessPolicyError::AppAuthorizationError(error)) } }; - match role.parent_resource_type { + match app_authorization.parent_resource_type { - RoleParentResourceType::Instance => { + AppAuthorizationParentResourceType::Instance => { selected_resource_type = AccessPolicyResourceType::Instance; selected_resource_id = None; }, - RoleParentResourceType::Workspace => { + AppAuthorizationParentResourceType::Workspace => { - let Some(workspace_id) = role.parent_workspace_id else { + let Some(workspace_id) = app_authorization.parent_workspace_id else { return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); @@ -1055,29 +1098,16 @@ impl AccessPolicy { }, - RoleParentResourceType::Project => { - - let Some(project_id) = role.parent_project_id else { - - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Project)); - - }; - - selected_resource_type = AccessPolicyResourceType::Project; - selected_resource_id = Some(project_id); - - }, - - RoleParentResourceType::Group => { + AppAuthorizationParentResourceType::User => { - let Some(group_id) = role.parent_group_id else { + let Some(user_id) = app_authorization.parent_user_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Group)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::User)); }; - selected_resource_type = AccessPolicyResourceType::Group; - selected_resource_id = Some(group_id); + selected_resource_type = AccessPolicyResourceType::User; + selected_resource_id = Some(user_id); } @@ -1085,115 +1115,148 @@ impl AccessPolicy { }, - // Group -> Instance - AccessPolicyResourceType::Group => { + // AppAuthorizationCredential -> AppAuthorization + AccessPolicyResourceType::AppAuthorizationCredential => { - let Some(group_id) = selected_resource_id else { + let Some(app_authorization_credential_id) = selected_resource_id else { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Group)); + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::AppAuthorizationCredential)); }; - hierarchy.push((AccessPolicyResourceType::Group, Some(group_id))); + hierarchy.push((AccessPolicyResourceType::AppAuthorizationCredential, Some(app_authorization_credential_id))); - selected_resource_type = AccessPolicyResourceType::Instance; - selected_resource_id = None; + let app_authorization_credential = match AppAuthorizationCredential::get_by_id(&app_authorization_credential_id, postgres_client).await { - } + Ok(app_authorization_credential) => app_authorization_credential, - // App -> (Workspace | User | Instance) - AccessPolicyResourceType::App => { + Err(error) => match error { - let Some(app_id) = selected_resource_id else { + AppAuthorizationCredentialError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::AppAuthorizationCredential)), - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::App)); + _ => return Err(AccessPolicyError::AppAuthorizationCredentialError(error)) + + } }; - hierarchy.push((AccessPolicyResourceType::App, Some(app_id))); + selected_resource_type = AccessPolicyResourceType::AppAuthorization; + selected_resource_id = Some(app_authorization_credential.app_authorization_id); - let app = match App::get_by_id(&app_id, postgres_client).await { + }, - Ok(app) => app, + // AppCredential -> App + AccessPolicyResourceType::AppCredential => { + + let Some(app_credential_id) = selected_resource_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::AppCredential)); + + }; + + hierarchy.push((AccessPolicyResourceType::AppCredential, Some(app_credential_id))); + + let app_credential = match AppCredential::get_by_id(&app_credential_id, postgres_client).await { + + Ok(app_credential) => app_credential, Err(error) => match error { - AppError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::App)), + AppCredentialError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::AppCredential)), - _ => return Err(AccessPolicyError::AppError(error)) + _ => return Err(AccessPolicyError::AppCredentialError(error)) } }; - match app.parent_resource_type { + selected_resource_type = AccessPolicyResourceType::App; + selected_resource_id = Some(app_credential.app_id); - AppParentResourceType::Instance => { + }, - selected_resource_type = AccessPolicyResourceType::Instance; - selected_resource_id = None; + // Group -> Instance + AccessPolicyResourceType::Group => { - }, + let Some(group_id) = selected_resource_id else { - AppParentResourceType::Workspace => { + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Group)); - let Some(workspace_id) = app.parent_workspace_id else { + }; - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); + hierarchy.push((AccessPolicyResourceType::Group, Some(group_id))); - }; + selected_resource_type = AccessPolicyResourceType::Instance; + selected_resource_id = None; - selected_resource_type = AccessPolicyResourceType::Workspace; - selected_resource_id = Some(workspace_id); + }, - }, + // GroupMembership -> Group + AccessPolicyResourceType::GroupMembership => { - AppParentResourceType::User => { + let Some(group_membership_id) = selected_resource_id else { - let Some(user_id) = app.parent_user_id else { + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::GroupMembership)); - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::User)); + }; - }; + hierarchy.push((AccessPolicyResourceType::GroupMembership, Some(group_membership_id))); - selected_resource_type = AccessPolicyResourceType::User; - selected_resource_id = Some(user_id); + let group_membership = match GroupMembership::get_by_id(&group_membership_id, postgres_client).await { + + Ok(group_membership) => group_membership, + + Err(error) => match error { + + GroupMembershipError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::GroupMembership)), + + _ => return Err(AccessPolicyError::GroupMembershipError(error)) } - } + }; - } + selected_resource_type = AccessPolicyResourceType::Group; + selected_resource_id = Some(group_membership.group_id); - // AppCredential -> App - AccessPolicyResourceType::AppCredential => { + }, - let Some(app_credential_id) = selected_resource_id else { + // HTTPTransaction -> Instance + AccessPolicyResourceType::HTTPTransaction => { - return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::AppCredential)); + let Some(http_transaction_id) = selected_resource_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::HTTPTransaction)); }; - hierarchy.push((AccessPolicyResourceType::AppCredential, Some(app_credential_id))); + hierarchy.push((AccessPolicyResourceType::HTTPTransaction, Some(http_transaction_id))); - let app_credential = match AppCredential::get_by_id(&app_credential_id, postgres_client).await { - - Ok(app_credential) => app_credential, + selected_resource_type = AccessPolicyResourceType::Instance; + selected_resource_id = None; - Err(error) => match error { + }, + + // Instance + AccessPolicyResourceType::Instance => break, - AppCredentialError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::AppCredential)), + // Item -> Project + AccessPolicyResourceType::Item => { - _ => return Err(AccessPolicyError::AppCredentialError(error)) + let Some(scoped_item_id) = self.scoped_item_id else { - } + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Item)); }; - selected_resource_type = AccessPolicyResourceType::App; - selected_resource_id = Some(app_credential.app_id); + hierarchy.push((AccessPolicyResourceType::Item, Some(scoped_item_id))); - } + let item = Item::get_by_id(&scoped_item_id, postgres_client).await?; + + selected_resource_type = AccessPolicyResourceType::Project; + selected_resource_id = Some(item.project_id); + + }, // Milestone -> (Project | Workspace) AccessPolicyResourceType::Milestone => { @@ -1250,6 +1313,215 @@ impl AccessPolicy { } + }, + + // Project -> Workspace + AccessPolicyResourceType::Project => { + + let Some(scoped_project_id) = self.scoped_project_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Project)); + + }; + + hierarchy.push((AccessPolicyResourceType::Project, Some(scoped_project_id))); + + let project = match Project::get_by_id(&scoped_project_id, postgres_client).await { + + Ok(project) => project, + + Err(error) => match error { + + ProjectError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::Project)), + + _ => return Err(AccessPolicyError::ProjectError(error)) + + } + + }; + + selected_resource_type = AccessPolicyResourceType::Workspace; + selected_resource_id = Some(project.workspace_id); + + }, + + // Role -> (Project | Workspace | Group | Instance) + AccessPolicyResourceType::Role => { + + let Some(scoped_role_id) = self.scoped_role_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Role)); + + }; + + hierarchy.push((AccessPolicyResourceType::Role, Some(scoped_role_id))); + + let role = match Role::get_by_id(&scoped_role_id, postgres_client).await { + + Ok(role) => role, + + Err(error) => match error { + + RoleError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::Role)), + + _ => return Err(AccessPolicyError::RoleError(error)) + + } + + }; + + match role.parent_resource_type { + + RoleParentResourceType::Instance => { + + selected_resource_type = AccessPolicyResourceType::Instance; + selected_resource_id = None; + + }, + + RoleParentResourceType::Workspace => { + + let Some(workspace_id) = role.parent_workspace_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); + + }; + + selected_resource_type = AccessPolicyResourceType::Workspace; + selected_resource_id = Some(workspace_id); + + }, + + RoleParentResourceType::Project => { + + let Some(project_id) = role.parent_project_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Project)); + + }; + + selected_resource_type = AccessPolicyResourceType::Project; + selected_resource_id = Some(project_id); + + }, + + RoleParentResourceType::Group => { + + let Some(group_id) = role.parent_group_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Group)); + + }; + + selected_resource_type = AccessPolicyResourceType::Group; + selected_resource_id = Some(group_id); + + } + + } + + }, + + // RoleMembership -> Role + AccessPolicyResourceType::RoleMembership => { + + let Some(role_membership_id) = selected_resource_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::RoleMembership)); + + }; + + hierarchy.push((AccessPolicyResourceType::RoleMembership, Some(role_membership_id))); + + let role_membership = match RoleMembership::get_by_id(&role_membership_id, postgres_client).await { + + Ok(role_membership) => role_membership, + + Err(error) => match error { + + RoleMembershipError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::RoleMembership)), + + _ => return Err(AccessPolicyError::RoleMembershipError(error)) + + } + + }; + + selected_resource_type = AccessPolicyResourceType::Role; + selected_resource_id = Some(role_membership.role_id); + + } + + // ServerLogEntry -> Instance + AccessPolicyResourceType::ServerLogEntry => { + + let Some(scoped_server_log_entry_id) = self.scoped_server_log_entry_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::ServerLogEntry)); + + }; + + hierarchy.push((AccessPolicyResourceType::ServerLogEntry, Some(scoped_server_log_entry_id))); + + selected_resource_type = AccessPolicyResourceType::Instance; + selected_resource_id = None; + + }, + + // Session -> User + AccessPolicyResourceType::Session => { + + let Some(scoped_session_id) = self.scoped_session_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Session)); + + }; + + hierarchy.push((AccessPolicyResourceType::Session, Some(scoped_session_id))); + + let session = match Session::get_by_id(&scoped_session_id, postgres_client).await { + + Ok(role_membership) => role_membership, + + Err(error) => match error { + + SessionError::NotFoundError(_) => return Err(AccessPolicyError::OrphanedResourceError(AccessPolicyResourceType::Session)), + + _ => return Err(AccessPolicyError::SessionError(error)) + + } + + }; + + selected_resource_type = AccessPolicyResourceType::User; + selected_resource_id = Some(session.user_id); + + }, + + // User -> Instance + AccessPolicyResourceType::User => { + + let Some(scoped_user_id) = self.scoped_user_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::User)); + + }; + + hierarchy.push((AccessPolicyResourceType::User, Some(scoped_user_id))); + + }, + + // Workspace -> Instance + AccessPolicyResourceType::Workspace => { + + let Some(scoped_workspace_id) = self.scoped_workspace_id else { + + return Err(AccessPolicyError::ScopedResourceIDMissingError(AccessPolicyResourceType::Workspace)); + + }; + + hierarchy.push((AccessPolicyResourceType::Workspace, Some(scoped_workspace_id))); + } } diff --git a/src/resources/access_policy/tests.rs b/src/resources/access_policy/tests.rs index bcbc0a9..2824417 100644 --- a/src/resources/access_policy/tests.rs +++ b/src/resources/access_policy/tests.rs @@ -11,8 +11,7 @@ use crate::{ resources::access_policy::{ - AccessPolicy, - AccessPolicyInheritanceLevel, + AccessPolicy, AccessPolicyPermissionLevel, AccessPolicyPrincipalType, AccessPolicyResourceType, @@ -27,7 +26,7 @@ fn assert_access_policy_is_equal_to_initial_properties(access_policy: &AccessPol assert_eq!(access_policy.action_id, initial_properties.action_id); assert_eq!(access_policy.permission_level, initial_properties.permission_level); - assert_eq!(access_policy.inheritance_level, initial_properties.inheritance_level); + assert_eq!(access_policy.is_inheritance_enabled, initial_properties.is_inheritance_enabled); assert_eq!(access_policy.principal_type, initial_properties.principal_type); assert_eq!(access_policy.principal_user_id, initial_properties.principal_user_id); assert_eq!(access_policy.principal_group_id, initial_properties.principal_group_id); @@ -50,7 +49,7 @@ fn assert_access_policies_are_equal(access_policy_1: &AccessPolicy, access_polic assert_eq!(access_policy_1.id, access_policy_2.id); assert_eq!(access_policy_1.action_id, access_policy_2.action_id); assert_eq!(access_policy_1.permission_level, access_policy_2.permission_level); - assert_eq!(access_policy_1.inheritance_level, access_policy_2.inheritance_level); + assert_eq!(access_policy_1.is_inheritance_enabled, access_policy_2.is_inheritance_enabled); assert_eq!(access_policy_1.principal_type, access_policy_2.principal_type); assert_eq!(access_policy_1.principal_user_id, access_policy_2.principal_user_id); assert_eq!(access_policy_1.principal_group_id, access_policy_2.principal_group_id); @@ -93,7 +92,7 @@ async fn create_access_policy() -> Result<()> { let access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -179,7 +178,7 @@ async fn list_access_policies_with_query() -> Result<()> { let access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: if remaining_action_count == 1 { created_access_policies[0].principal_user_id } else { Some(user.id) }, scoped_resource_type: AccessPolicyResourceType::Instance, @@ -256,7 +255,7 @@ async fn count_access_policies() -> Result<()> { let access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -290,7 +289,7 @@ async fn list_access_policies_by_hierarchy() -> Result<()> { let instance_access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -342,7 +341,7 @@ async fn update_access_policy() -> Result<()> { let instance_access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -351,12 +350,12 @@ async fn update_access_policy() -> Result<()> { let instance_access_policy = AccessPolicy::create(&instance_access_policy_properties, &mut postgres_client).await?; let updated_access_policy_properties = EditableAccessPolicyProperties { permission_level: Some(AccessPolicyPermissionLevel::Editor), - inheritance_level: Some(AccessPolicyInheritanceLevel::Disabled) + is_inheritance_enabled: Some(false) }; let updated_access_policy = instance_access_policy.update(&updated_access_policy_properties, &mut postgres_client).await?; assert_eq!(updated_access_policy.permission_level, AccessPolicyPermissionLevel::Editor); - assert_eq!(updated_access_policy.inheritance_level, AccessPolicyInheritanceLevel::Disabled); + assert_eq!(updated_access_policy.is_inheritance_enabled, false); return Ok(()); diff --git a/src/resources/action_log_entry/mod.rs b/src/resources/action_log_entry/mod.rs new file mode 100644 index 0000000..b7dc5d6 --- /dev/null +++ b/src/resources/action_log_entry/mod.rs @@ -0,0 +1,204 @@ +use postgres_types::{FromSql, ToSql}; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Debug, Error)] +pub enum ActionLogEntryError { + #[error("An action log entry with the ID \"{0}\" does not exist.")] + NotFoundError(String), + + #[error(transparent)] + PostgresError(#[from] postgres::Error) +} + +#[derive(Debug, Clone, FromSql, ToSql)] +#[postgres(name = "action_log_entry_actor_type")] +pub enum ActionLogEntryActorType { + User, + App +} + +#[derive(Debug, Clone, FromSql, ToSql)] +#[postgres(name = "action_log_entry_target_resource_type")] +pub enum ActionLogEntryTargetResourceType { + AccessPolicy, + Action, + ActionLogEntry, + App, + AppAuthorization, + AppAuthorizationCredential, + AppCredential, + Group, + GroupMembership, + HTTPTransaction, + Instance, + Item, + Project, + Role, + RoleMembership, + ServerLogEntry, + Session, + User, + Milestone, + Workspace +} + +#[derive(Debug, Clone)] +pub struct ActionLogEntry { + + /// The ID of the action log entry. + pub id: Uuid, + + /// The ID of the action. + pub action_id: Uuid, + + /// The ID of the HTTP transaction related to the action log entry, if applicable. + pub http_transaction_id: Option, + + /// The type of actor that performed the action. + pub actor_type: ActionLogEntryActorType, + + /// The ID of the user actor that performed the action, if applicable. + pub actor_user_id: Option, + + /// The ID of the app actor that performed the action, if applicable. + pub actor_app_id: Option, + + /// The type of the target resource of the action. + pub target_resource_type: ActionLogEntryTargetResourceType, + + /// The target action ID of the action, if applicable. + pub target_action_id: Option, + + /// The target action log entry ID of the action, if applicable. + pub target_action_log_entry_id: Option, + + /// The target app ID of the action, if applicable. + pub target_app_id: Option, + + /// The target app authorization ID of the action, if applicable. + pub target_app_authorization_id: Option, + + /// The target app authorization credential ID of the action, if applicable. + pub target_app_authorization_credential_id: Option, + + /// The target app credential ID of the action, if applicable. + pub target_app_credential_id: Option, + + /// The target group ID of the action, if applicable. + pub target_group_id: Option, + + /// The target group membership ID of the action, if applicable. + pub target_group_membership_id: Option, + + /// The target HTTP transaction ID of the action, if applicable. + pub target_http_transaction_id: Option, + + /// The target HTTP transaction log entry ID of the action, if applicable. + pub target_http_transaction_log_entry_id: Option, + + /// The target instance ID of the action, if applicable. + pub target_instance_id: Option, + + /// The target item ID of the action, if applicable. + pub target_item_id: Option, + + /// The target milestone ID of the action, if applicable. + pub target_milestone_id: Option, + + /// The target project ID of the action, if applicable. + pub target_project_id: Option, + + /// The target role ID of the action, if applicable. + pub target_role_id: Option, + + /// The target role membership ID of the action, if applicable. + pub target_role_membership_id: Option, + + /// The target server log entry ID of the action, if applicable. + pub target_server_log_entry_id: Option, + + /// The target session ID of the action, if applicable. + pub target_session_id: Option, + + /// The target user ID of the action, if applicable. + pub target_user_id: Option, + + /// The target workspace ID of the action, if applicable. + pub target_workspace_id: Option, + + /// The reason why the action was performed, if applicable. + pub reason: Option + +} + +impl ActionLogEntry { + + pub async fn get_by_id(id: &Uuid, postgres_client: &mut deadpool_postgres::Client) -> Result { + + let query = include_str!("../../queries/action_log_entries/get_action_log_entry_row_by_id.sql"); + let row = match postgres_client.query_opt(query, &[&id]).await { + + Ok(row) => match row { + + Some(row) => row, + + None => return Err(ActionLogEntryError::NotFoundError(id.to_string())) + + }, + + Err(error) => return Err(ActionLogEntryError::PostgresError(error)) + + }; + + let action_log_entry = ActionLogEntry::from_row(&row); + + return Ok(action_log_entry); + + } + + fn from_row(row: &postgres::Row) -> Self { + + return ActionLogEntry { + id: row.get("id"), + action_id: row.get("action_id"), + http_transaction_id: row.get("http_transaction_id"), + actor_type: row.get("actor_type"), + actor_user_id: row.get("actor_user_id"), + actor_app_id: row.get("actor_app_id"), + target_resource_type: row.get("target_resource_type"), + target_action_id: row.get("target_action_id"), + target_action_log_entry_id: row.get("target_action_log_entry_id"), + target_app_id: row.get("target_app_id"), + target_app_authorization_id: row.get("target_app_authorization_id"), + target_app_authorization_credential_id: row.get("target_app_authorization_credential_id"), + target_app_credential_id: row.get("target_app_credential_id"), + target_group_id: row.get("target_group_id"), + target_group_membership_id: row.get("target_group_membership_id"), + target_http_transaction_id: row.get("target_http_transaction_id"), + target_http_transaction_log_entry_id: row.get("target_http_transaction_log_entry_id"), + target_instance_id: row.get("target_instance_id"), + target_item_id: row.get("target_item_id"), + target_milestone_id: row.get("target_milestone_id"), + target_project_id: row.get("target_project_id"), + target_role_id: row.get("target_role_id"), + target_role_membership_id: row.get("target_role_membership_id"), + target_server_log_entry_id: row.get("target_server_log_entry_id"), + target_session_id: row.get("target_session_id"), + target_user_id: row.get("target_user_id"), + target_workspace_id: row.get("target_workspace_id"), + reason: row.get("reason") + }; + + } + + /// Initializes the action_log_entries table. + pub async fn initialize_action_log_entries_table(postgres_client: &mut deadpool_postgres::Client) -> Result<(), ActionLogEntryError> { + + let query = include_str!("../../queries/action_log_entries/initialize_action_log_entries_table.sql"); + postgres_client.execute(query, &[]).await?; + return Ok(()); + + } + +} \ No newline at end of file diff --git a/src/resources/app_authorization/mod.rs b/src/resources/app_authorization/mod.rs index 30e1dc8..a19221a 100644 --- a/src/resources/app_authorization/mod.rs +++ b/src/resources/app_authorization/mod.rs @@ -1,15 +1,80 @@ use thiserror::Error; +use uuid::Uuid; +use postgres_types::{FromSql, ToSql}; + +#[derive(Debug, Clone, ToSql, FromSql)] +#[postgres(name = "app_authorization_parent_resource_type")] +pub enum AppAuthorizationParentResourceType { + Instance, + Workspace, + User +} #[derive(Debug, Error)] pub enum AppAuthorizationError { + #[error("An app authorization with the ID \"{0}\" does not exist.")] + NotFoundError(String), + #[error(transparent)] PostgresError(#[from] postgres::Error) } -pub struct AppAuthorization {} +pub struct AppAuthorization { + + /// The ID of the app authorization. + pub id: Uuid, + + /// The ID of the app. + pub app_id: Uuid, + + /// The parent resource type of the app authorization. + pub parent_resource_type: AppAuthorizationParentResourceType, + + /// The ID of the parent workspace of the app authorization, if applicable. + pub parent_workspace_id: Option, + + /// The ID of the parent user of the app authorization, if applicable. + pub parent_user_id: Option + +} impl AppAuthorization { + pub async fn get_by_id(id: &Uuid, postgres_client: &mut deadpool_postgres::Client) -> Result { + + let query = include_str!("../../queries/app-authorizations/get-app-authorization-row-by-id.sql"); + let row = match postgres_client.query_opt(query, &[&id]).await { + + Ok(row) => match row { + + Some(row) => row, + + None => return Err(AppAuthorizationError::NotFoundError(id.to_string())) + + }, + + Err(error) => return Err(AppAuthorizationError::PostgresError(error)) + + }; + + let app_authorization = AppAuthorization::from_row(&row); + + return Ok(app_authorization); + + } + + fn from_row(row: &postgres::Row) -> Self { + + return AppAuthorization { + id: row.get("id"), + app_id: row.get("app_id"), + parent_resource_type: row.get("parent_resource_type"), + parent_workspace_id: row.get("parent_workspace_id"), + parent_user_id: row.get("parent_user_id") + }; + + } + /// Initializes the app_authorizations table. pub async fn initialize_app_authorizations_table(postgres_client: &mut deadpool_postgres::Client) -> Result<(), AppAuthorizationError> { diff --git a/src/resources/app_authorization_credential/mod.rs b/src/resources/app_authorization_credential/mod.rs index 059eb35..6ca857a 100644 --- a/src/resources/app_authorization_credential/mod.rs +++ b/src/resources/app_authorization_credential/mod.rs @@ -1,15 +1,59 @@ use thiserror::Error; +use uuid::Uuid; #[derive(Debug, Error)] pub enum AppAuthorizationCredentialError { + #[error("An app authorization credential with the ID \"{0}\" does not exist.")] + NotFoundError(String), + #[error(transparent)] PostgresError(#[from] postgres::Error) } -pub struct AppAuthorizationCredential {} +pub struct AppAuthorizationCredential { + + /// The ID of the app authorization credential. + pub id: Uuid, + + /// The ID of the app authorization. + pub app_authorization_id: Uuid + +} impl AppAuthorizationCredential { + pub async fn get_by_id(id: &Uuid, postgres_client: &mut deadpool_postgres::Client) -> Result { + + let query = include_str!("../../queries/app-authorization-credentials/get-app-authorization-credential-row-by-id.sql"); + let row = match postgres_client.query_opt(query, &[&id]).await { + + Ok(row) => match row { + + Some(row) => row, + + None => return Err(AppAuthorizationCredentialError::NotFoundError(id.to_string())) + + }, + + Err(error) => return Err(AppAuthorizationCredentialError::PostgresError(error)) + + }; + + let app_authorization_credential = AppAuthorizationCredential::from_row(&row); + + return Ok(app_authorization_credential); + + } + + fn from_row(row: &postgres::Row) -> Self { + + return AppAuthorizationCredential { + id: row.get("id"), + app_authorization_id: row.get("app_authorization_id") + }; + + } + /// Initializes the app_authorization_credentials table. pub async fn initialize_app_authorization_credentials_table(postgres_client: &mut deadpool_postgres::Client) -> Result<(), AppAuthorizationCredentialError> { diff --git a/src/resources/group_membership/mod.rs b/src/resources/group_membership/mod.rs new file mode 100644 index 0000000..bb1735f --- /dev/null +++ b/src/resources/group_membership/mod.rs @@ -0,0 +1,96 @@ +use thiserror::Error; +use uuid::Uuid; +use postgres_types::{FromSql, ToSql}; + +#[derive(Debug, Clone, ToSql, FromSql)] +#[postgres(name = "group_membership_principal_type")] +pub enum GroupMembershipPrincipalType { + App, + Group, + User +} + +#[derive(Debug, Error)] +pub enum GroupMembershipError { + #[error("A group membership with the ID \"{0}\" does not exist.")] + NotFoundError(String), + + #[error(transparent)] + PostgresError(#[from] postgres::Error) +} + +#[derive(Debug, Clone)] +pub struct GroupMembership { + + /// The ID of the group membership. + pub id: Uuid, + + /// The ID of the group. + pub group_id: Uuid, + + /// The ID of the principal. + pub principal_id: Uuid, + + /// The type of the principal. + pub principal_type: GroupMembershipPrincipalType, + + /// The ID of the principal user, if applicable. + pub principal_user_id: Option, + + /// The ID of the principal group, if applicable. + pub principal_group_id: Option, + + /// The ID of the principal app, if applicable. + pub principal_app_id: Option + +} + +impl GroupMembership { + + pub async fn get_by_id(id: &Uuid, postgres_client: &mut deadpool_postgres::Client) -> Result { + + let query = include_str!("../../queries/group_memberships/get_group_membership_row_by_id.sql"); + let row = match postgres_client.query_opt(query, &[&id]).await { + + Ok(row) => match row { + + Some(row) => row, + + None => return Err(GroupMembershipError::NotFoundError(id.to_string())) + + }, + + Err(error) => return Err(GroupMembershipError::PostgresError(error)) + + }; + + let app_authorization = GroupMembership::from_row(&row); + + return Ok(app_authorization); + + } + + fn from_row(row: &postgres::Row) -> Self { + + return GroupMembership { + id: row.get("id"), + group_id: row.get("group_id"), + principal_id: row.get("principal_id"), + principal_type: row.get("principal_type"), + principal_user_id: row.get("principal_user_id"), + principal_group_id: row.get("principal_group_id"), + principal_app_id: row.get("principal_app_id") + }; + + } + + /// Initializes the app_authorizations table. + pub async fn initialize_app_authorizations_table(postgres_client: &mut deadpool_postgres::Client) -> Result<(), GroupMembershipError> { + + let query = include_str!("../../queries/group_memberships/initialize_group_memberships_table.sql"); + postgres_client.execute(query, &[]).await?; + return Ok(()); + + } + +} \ No newline at end of file diff --git a/src/resources/server_log_entry/mod.rs b/src/resources/server_log_entry/mod.rs index 6b2b52e..eecef0f 100644 --- a/src/resources/server_log_entry/mod.rs +++ b/src/resources/server_log_entry/mod.rs @@ -44,8 +44,8 @@ pub struct ServerLogEntry { /// The message of the server log entry. pub message: String, - /// The HTTP request ID of the server log entry, if applicable. - pub http_request_id: Option, + /// The HTTP transaction ID of the server log entry, if applicable. + pub http_transaction_id: Option, /// The level of the server log entry. pub level: ServerLogEntryLevel @@ -58,7 +58,7 @@ pub struct InitialServerLogEntryProperties<'a> { pub message: &'a str, /// The HTTP request ID of the server log entry, if applicable. - pub http_request_id: Option<&'a Uuid>, + pub http_transaction_id: Option<&'a Uuid>, /// The level of the server log entry. pub level: &'a ServerLogEntryLevel @@ -67,61 +67,61 @@ pub struct InitialServerLogEntryProperties<'a> { impl ServerLogEntry { - pub async fn critical(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn critical(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Critical; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn trace(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn trace(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Trace; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn info(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn info(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Info; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn warning(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn warning(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Warning; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn error(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn error(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Error; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn success(message: &str, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn success(message: &str, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = &ServerLogEntryLevel::Success; - let properties = InitialServerLogEntryProperties { message, http_request_id, level }; + let properties = InitialServerLogEntryProperties { message, http_transaction_id, level }; let server_log_entry_result = ServerLogEntry::create(&properties, postgres_client, true).await; return server_log_entry_result; } - pub async fn from_http_error(http_error: &HTTPError, http_request_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { + pub async fn from_http_error(http_error: &HTTPError, http_transaction_id: Option<&Uuid>, postgres_client: &mut deadpool_postgres::Client) -> Result { let level = match http_error { HTTPError::InternalServerError(_) => &ServerLogEntryLevel::Critical, @@ -130,7 +130,7 @@ impl ServerLogEntry { let message = &http_error.to_string(); let properties = InitialServerLogEntryProperties { message, - http_request_id, + http_transaction_id, level }; let server_log_entry = ServerLogEntry::create(&properties, postgres_client, true).await?; @@ -138,12 +138,20 @@ impl ServerLogEntry { } + pub async fn initialize_server_log_entries_table(postgres_client: &mut deadpool_postgres::Client) -> Result<(), ServerLogEntryError> { + + let query = include_str!("../../queries/server_log_entries/initialize_server_log_entries_table.sql"); + postgres_client.execute(query, &[]).await?; + return Ok(()); + + } + pub async fn create<'a>(properties: &InitialServerLogEntryProperties<'a>, postgres_client: &mut deadpool_postgres::Client, should_print_to_console: bool) -> Result { let query = include_str!("../../queries/server_log_entries/insert-server-log-entry-row.sql"); let parameters: &[&(dyn ToSql + Sync)] = &[ &properties.message, - &properties.http_request_id, + &properties.http_transaction_id, &properties.level ]; let row_result = postgres_client.query_one(query, parameters).await; @@ -159,7 +167,7 @@ impl ServerLogEntry { let temporary_server_log_entry = ServerLogEntry { id: Uuid::now_v7(), message: properties.message.to_string(), - http_request_id: properties.http_request_id.copied(), + http_transaction_id: properties.http_transaction_id.copied(), level: *properties.level }; @@ -176,7 +184,7 @@ impl ServerLogEntry { let server_log_entry = ServerLogEntry { id: row.get("id"), message: row.get("message"), - http_request_id: row.get("http_request_id"), + http_transaction_id: row.get("http_transaction_id"), level: row.get("level") }; @@ -193,9 +201,9 @@ impl ServerLogEntry { pub fn get_formatted_message(&self) -> String { let level_prefix = format!("[{}]", self.level); - let request_id_prefix = match &self.http_request_id { + let request_id_prefix = match &self.http_transaction_id { - Some(http_request_id) => format!("[{}] ", http_request_id), + Some(http_transaction_id) => format!("[{}] ", http_transaction_id), None => String::new() diff --git a/src/routes/access-policies/mod.rs b/src/routes/access-policies/mod.rs index b406baa..e7e2fb8 100644 --- a/src/routes/access-policies/mod.rs +++ b/src/routes/access-policies/mod.rs @@ -1,28 +1,93 @@ use std::sync::Arc; -use axum::{Extension, Json, Router, extract::{Path, State}}; +use axum::{Extension, Router, extract::{Query, State}}; +use axum_extra::response::ErasedJson; +use serde::{Deserialize, Serialize}; -use crate::{AppState, HTTPError, resources::{access_policy::AccessPolicy, http_transaction::HTTPTransaction, user::User}}; +use crate::{AppState, HTTPError, middleware::authentication_middleware, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, AccessPolicyResourceType, DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT, IndividualPrincipal, ResourceHierarchy}, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::{route_handler_utilities::{get_action_from_name, get_user_from_option_user, map_postgres_error_to_http_error, verify_user_permissions}, slashstepql::SlashstepQLError}}; #[path = "./{access_policy_id}/mod.rs"] mod access_policy_id; +#[derive(Debug, Deserialize)] +pub struct AccessPolicyListQueryParameters { + query: Option +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct ListAccessPolicyResponseBody { + access_policies: Vec, + total_count: i64 +} + #[axum::debug_handler] async fn handle_list_access_policies_request( - Path(access_policy_id): Path, + Query(query_parameters): Query, State(state): State, Extension(http_transaction): Extension>, Extension(user): Extension>> -) -> Result>, HTTPError> { +) -> Result { + + let http_transaction = http_transaction.clone(); + let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; + let action = get_action_from_name("slashstep.accessPolicies.list", &http_transaction, &mut postgres_client).await?; + let user = get_user_from_option_user(&user, &http_transaction, &mut postgres_client).await?; + let resource_hierarchy: ResourceHierarchy = vec![(AccessPolicyResourceType::Instance, None)]; + verify_user_permissions(&user, &action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::User, &mut postgres_client).await?; + let query = query_parameters.query.unwrap_or("".to_string()); + let access_policies = match AccessPolicy::list(&query, &mut postgres_client, Some(&IndividualPrincipal::User(user.id))).await { + + Ok(access_policies) => access_policies, + + Err(error) => { + + let http_error = match error { + + AccessPolicyError::SlashstepQLError(error) => match error { + + SlashstepQLError::SlashstepQLInvalidLimitError(error) => HTTPError::UnprocessableEntity(Some(format!("The provided limit must be zero or a positive integer of {} or less. You provided {}.", DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT, error.limit_string))), // TODO: Make this configurable through resource policies. + + SlashstepQLError::InvalidFieldError(field) => HTTPError::UnprocessableEntity(Some(format!("The provided query is invalid. The field \"{}\" is not allowed.", field))), + + SlashstepQLError::InvalidQueryError(()) => HTTPError::UnprocessableEntity(Some(format!("The provided query is invalid."))), + + _ => HTTPError::InternalServerError(Some(format!("Failed to list access policies: {:?}", error))) + + }, + + _ => HTTPError::InternalServerError(Some(format!("Failed to list access policies: {:?}", error))) + + }; + + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + } + + }; + + let _ = ServerLogEntry::trace(&format!("Counting access policies..."), Some(&http_transaction.id), &mut postgres_client).await; + let access_policy_count = match AccessPolicy::count(&query, &mut postgres_client, Some(&IndividualPrincipal::User(user.id))).await { + + Ok(access_policy_count) => access_policy_count, + + Err(error) => { + + let http_error = HTTPError::InternalServerError(Some(format!("Failed to count access policies: {:?}", error))); + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + } + + }; - // let http_transaction = http_transaction.clone(); - // let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; - // let access_policy = get_access_policy(&access_policy_id, &http_transaction, &mut postgres_client).await?; - // let user = get_user_from_option_user(&user, &http_transaction, &mut postgres_client).await?; - // let resource_hierarchy = get_resource_hierarchy(&access_policy, &http_transaction, &mut postgres_client).await?; - // let action = get_action_from_name("slashstep.accessPolicies.get", &http_transaction, &mut postgres_client).await?; + let _ = ServerLogEntry::success(&format!("Successfully {} returned access policies.", access_policies.len()), Some(&http_transaction.id), &mut postgres_client).await; + let response_body = ListAccessPolicyResponseBody { + access_policies, + total_count: access_policy_count + }; - return Err(HTTPError::NotImplementedError(Some(format!("Not implemented.")))); + return Ok(ErasedJson::pretty(&response_body)); } @@ -30,6 +95,7 @@ pub fn get_router(state: AppState) -> Router { let router = Router::::new() .route("/access-policies", axum::routing::get(handle_list_access_policies_request)) + .layer(axum::middleware::from_fn_with_state(state.clone(), authentication_middleware::authenticate_user)) .merge(access_policy_id::get_router(state.clone())); return router; diff --git a/src/routes/access-policies/tests.rs b/src/routes/access-policies/tests.rs index 2c59b73..7df00ee 100644 --- a/src/routes/access-policies/tests.rs +++ b/src/routes/access-policies/tests.rs @@ -1,24 +1,355 @@ -#[test] -fn list_access_policies() { +use std::net::SocketAddr; + +use axum::middleware; +use axum_extra::extract::cookie::Cookie; +use axum_test::TestServer; +use reqwest::StatusCode; + +use crate::{AppState, SlashstepServerError, middleware::http_request_middleware, pre_definitions::{initialize_pre_defined_actions, initialize_pre_defined_roles}, resources::{access_policy::{AccessPolicy, AccessPolicyPermissionLevel, AccessPolicyPrincipalType, AccessPolicyResourceType, DEFAULT_ACCESS_POLICY_LIST_LIMIT, IndividualPrincipal, InitialAccessPolicyProperties}, action::Action, session::Session}, routes::access_policies::ListAccessPolicyResponseBody, tests::TestEnvironment}; + +/// Verifies that the router can return a 200 status code and the requested access policy list. +#[tokio::test] +async fn verify_returned_access_policy_list() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.get", &mut postgres_client).await?; + let get_access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&get_access_policy_properties, &mut postgres_client).await?; + + let list_access_policies_action = Action::get_by_name("slashstep.accessPolicies.list", &mut postgres_client).await?; + let list_access_policy_properties = InitialAccessPolicyProperties { + action_id: list_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&list_access_policy_properties, &mut postgres_client).await?; + + let response = test_server.get(&format!("/access-policies")) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), 200); + + let response_access_policies: ListAccessPolicyResponseBody = response.json(); + assert!(response_access_policies.total_count > 0); + assert!(response_access_policies.access_policies.len() > 0); + + let actual_access_policy_count = AccessPolicy::count("", &mut postgres_client, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.total_count, actual_access_policy_count); + + let actual_access_policies = AccessPolicy::list("", &mut postgres_client, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.access_policies.len(), actual_access_policies.len()); + + for actual_access_policy in actual_access_policies { + + let found_access_policy = response_access_policies.access_policies.iter().find(|access_policy| access_policy.id == actual_access_policy.id); + assert!(found_access_policy.is_some()); + + } + + return Ok(()); } -#[test] -fn list_maximum_default_access_policies() { +/// Verifies that the default access policy list limit is 1000. +#[tokio::test] +async fn verify_default_access_policy_list_limit() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.get", &mut postgres_client).await?; + let get_access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&get_access_policy_properties, &mut postgres_client).await?; + + let access_policy_count = AccessPolicy::count("", &mut postgres_client, None).await?; + for _ in 0..(DEFAULT_ACCESS_POLICY_LIST_LIMIT - access_policy_count + 1) { + + let random_action = test_environment.create_random_action().await?; + let random_user = test_environment.create_random_user().await?; + let access_policy_properties = InitialAccessPolicyProperties { + action_id: random_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(random_user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&access_policy_properties, &mut postgres_client).await?; + + } + + let list_access_policies_action = Action::get_by_name("slashstep.accessPolicies.list", &mut postgres_client).await?; + let list_access_policy_properties = InitialAccessPolicyProperties { + action_id: list_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&list_access_policy_properties, &mut postgres_client).await?; + + let response = test_server.get(&format!("/access-policies")) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::OK); + + let response_body: ListAccessPolicyResponseBody = response.json(); + assert_eq!(response_body.access_policies.len(), DEFAULT_ACCESS_POLICY_LIST_LIMIT as usize); + + return Ok(()); } -#[test] -fn verify_query_when_listing_access_policies() { +/// Verifies that the server returns a 422 status code when the provided limit is over the maximum limit. +#[tokio::test] +async fn verify_maximum_access_policy_list_limit() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.get", &mut postgres_client).await?; + let get_access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&get_access_policy_properties, &mut postgres_client).await?; + + let list_access_policies_action = Action::get_by_name("slashstep.accessPolicies.list", &mut postgres_client).await?; + let list_access_policy_properties = InitialAccessPolicyProperties { + action_id: list_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&list_access_policy_properties, &mut postgres_client).await?; + + let response = test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("limit {}", DEFAULT_ACCESS_POLICY_LIST_LIMIT + 1)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); + + return Ok(()); } -#[test] -fn verify_authentication_when_listing_access_policies() { +/// Verifies that the server returns a 400 status code when the query is invalid. +#[tokio::test] +async fn verify_query_when_listing_access_policies() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.get", &mut postgres_client).await?; + let get_access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&get_access_policy_properties, &mut postgres_client).await?; + + let list_access_policies_action = Action::get_by_name("slashstep.accessPolicies.list", &mut postgres_client).await?; + let list_access_policy_properties = InitialAccessPolicyProperties { + action_id: list_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyResourceType::Instance, + ..Default::default() + }; + AccessPolicy::create(&list_access_policy_properties, &mut postgres_client).await?; + + let requests = vec![ + test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("action_ied = {}", get_access_policies_action.id)), + test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("SELECT * FROM access_policies")), + test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("1 = 1")), + test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("SELECT PG_SLEEP(10)")), + test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("SELECT * FROM access_policies WHERE action_id = {}", get_access_policies_action.id)) + ]; + + for request in requests { + + let response = request + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); + + } + + return Ok(()); } -#[test] -fn verify_permission_when_listing_access_policies() { +/// Verifies that the server returns a 401 status code when the user lacks permissions and is unauthenticated. +#[tokio::test] +async fn verify_authentication_when_listing_access_policies() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.get(&format!("/access-policies")) + .await; + + assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED); + + return Ok(()); + +} + +/// Verifies that the server returns a 403 status code when the user lacks permissions and is authenticated. +#[tokio::test] +async fn verify_permission_when_listing_access_policies() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + initialize_pre_defined_actions(&mut postgres_client).await?; + initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + + let response = test_server.get(&format!("/access-policies")) + .add_query_param("query", format!("limit {}", DEFAULT_ACCESS_POLICY_LIST_LIMIT + 1)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::FORBIDDEN); + + return Ok(()); } \ No newline at end of file diff --git a/src/routes/access-policies/{access_policy_id}/mod.rs b/src/routes/access-policies/{access_policy_id}/mod.rs index a3fde84..346629a 100644 --- a/src/routes/access-policies/{access_policy_id}/mod.rs +++ b/src/routes/access-policies/{access_policy_id}/mod.rs @@ -3,31 +3,8 @@ use std::sync::Arc; use axum::{Extension, Json, Router, extract::{Path, State, rejection::JsonRejection}}; use reqwest::StatusCode; use uuid::Uuid; -use colored::Colorize; -use crate::{AppState, HTTPError, middleware::authentication_middleware, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, EditableAccessPolicyProperties, Principal, ResourceHierarchy}, action::Action, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::principal_permission_verifier::{PrincipalPermissionVerifier, PrincipalPermissionVerifierError}}; - -fn map_postgres_error_to_http_error(error: deadpool_postgres::PoolError) -> HTTPError { - - let http_error = HTTPError::InternalServerError(Some(error.to_string())); - eprintln!("{}", format!("Failed to get database connection, so the log cannot be saved. Printing to the console: {}", error).red()); - return http_error; - -} - -async fn get_user_from_option_user(user: &Option>, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result, HTTPError> { - - let Some(user) = user else { - - let http_error = HTTPError::InternalServerError(Some(format!("Couldn't find a user for the request. This is a bug. Make sure the authentication middleware is installed and is working properly."))); - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); - - }; - - return Ok(user.clone()); - -} +use crate::{AppState, HTTPError, middleware::authentication_middleware, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, EditableAccessPolicyProperties, ResourceHierarchy}, action::Action, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::route_handler_utilities::{get_action_from_name, get_user_from_option_user, map_postgres_error_to_http_error, verify_user_permissions}}; async fn get_resource_hierarchy(access_policy: &AccessPolicy, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { @@ -117,27 +94,6 @@ async fn get_access_policy(access_policy_id: &str, http_transaction: &HTTPTransa } -async fn get_action_from_name(action_name: &str, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { - - let _ = ServerLogEntry::trace(&format!("Getting action \"{}\"...", action_name), Some(&http_transaction.id), &mut postgres_client).await; - let action = match Action::get_by_name(&action_name, &mut postgres_client).await { - - Ok(action) => action, - - Err(error) => { - - let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action \"{}\": {:?}", action_name, error))); - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); - - } - - }; - - return Ok(action); - -} - async fn get_action_from_id(action_id: &Uuid, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { let _ = ServerLogEntry::trace(&format!("Getting action {}", action_id), Some(&http_transaction.id), postgres_client).await; @@ -159,47 +115,6 @@ async fn get_action_from_id(action_id: &Uuid, http_transaction: &HTTPTransaction } -async fn verify_user_permissions(user: &User, action: &Action, resource_hierarchy: &ResourceHierarchy, http_transaction: &HTTPTransaction, minimum_permission_level: &AccessPolicyPermissionLevel, mut postgres_client: &mut deadpool_postgres::Client) -> Result<(), HTTPError> { - - let _ = ServerLogEntry::trace(&format!("Verifying principal may use \"{}\" action...", action.name), Some(&http_transaction.id), &mut postgres_client).await; - - match PrincipalPermissionVerifier::verify_permissions(&Principal::User(user.id), &action.id, &resource_hierarchy, &minimum_permission_level, &mut postgres_client).await { - - Ok(_) => {}, - - Err(error) => { - - let http_error = match error { - - PrincipalPermissionVerifierError::ForbiddenError { .. } => { - - let message = format!("You need at least {} permission to the \"{}\" action.", minimum_permission_level.to_string(), action.name); - if user.is_anonymous { - - HTTPError::UnauthorizedError(Some(message)) - - } else { - - HTTPError::ForbiddenError(Some(message)) - - } - - }, - - _ => HTTPError::InternalServerError(Some(error.to_string())) - - }; - let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); - - } - - } - - return Ok(()); - -} - #[axum::debug_handler] async fn handle_get_access_policy_request( Path(access_policy_id): Path, diff --git a/src/routes/access-policies/{access_policy_id}/tests.rs b/src/routes/access-policies/{access_policy_id}/tests.rs index 6a87770..fe74476 100644 --- a/src/routes/access-policies/{access_policy_id}/tests.rs +++ b/src/routes/access-policies/{access_policy_id}/tests.rs @@ -18,8 +18,7 @@ use crate::{ resources::{ access_policy::{ AccessPolicy, - AccessPolicyError, - AccessPolicyInheritanceLevel, + AccessPolicyError, AccessPolicyPermissionLevel, AccessPolicyPrincipalType, AccessPolicyResourceType, @@ -58,7 +57,7 @@ async fn verify_returned_access_policy_by_id() -> Result<(), SlashstepServerErro let access_policy_properties = InitialAccessPolicyProperties { action_id: get_access_policies_action.id, permission_level: AccessPolicyPermissionLevel::User, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -76,7 +75,7 @@ async fn verify_returned_access_policy_by_id() -> Result<(), SlashstepServerErro assert_eq!(response_access_policy.id, access_policy.id); assert_eq!(response_access_policy.action_id, access_policy.action_id); assert_eq!(response_access_policy.permission_level, access_policy.permission_level); - assert_eq!(response_access_policy.inheritance_level, access_policy.inheritance_level); + assert_eq!(response_access_policy.is_inheritance_enabled, access_policy.is_inheritance_enabled); assert_eq!(response_access_policy.principal_type, access_policy.principal_type); assert_eq!(response_access_policy.principal_user_id, access_policy.principal_user_id); assert_eq!(response_access_policy.principal_group_id, access_policy.principal_group_id); @@ -246,7 +245,7 @@ async fn verify_successful_deletion_when_deleting_access_policy_by_id() -> Resul let access_policy_properties = InitialAccessPolicyProperties { action_id: delete_access_policies_action.id, permission_level: AccessPolicyPermissionLevel::Editor, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -419,7 +418,7 @@ async fn verify_successful_patch_access_policy_by_id() -> Result<(), SlashstepSe let access_policy_properties = InitialAccessPolicyProperties { action_id: get_access_policies_action.id, permission_level: AccessPolicyPermissionLevel::Editor, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -431,7 +430,7 @@ async fn verify_successful_patch_access_policy_by_id() -> Result<(), SlashstepSe .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) .json(&serde_json::json!({ "permission_level": "User", - "inheritance_level": "Disabled" + "is_inheritance_enabled": false })) .await; @@ -441,7 +440,7 @@ async fn verify_successful_patch_access_policy_by_id() -> Result<(), SlashstepSe assert_eq!(response_access_policy.id, access_policy.id); assert_eq!(response_access_policy.action_id, access_policy.action_id); assert_eq!(response_access_policy.permission_level, AccessPolicyPermissionLevel::User); - assert_eq!(response_access_policy.inheritance_level, AccessPolicyInheritanceLevel::Disabled); + assert_eq!(response_access_policy.is_inheritance_enabled, false); assert_eq!(response_access_policy.principal_type, access_policy.principal_type); assert_eq!(response_access_policy.principal_user_id, access_policy.principal_user_id); assert_eq!(response_access_policy.principal_group_id, access_policy.principal_group_id); @@ -540,7 +539,7 @@ async fn verify_request_body_json_when_patching_access_policy_by_id() -> Result< .add_header("Content-Type", "application/json") .json(&serde_json::json!({ "permission_level": "Super Duper Admin", - "inheritance_level": "Required", + "is_inheritance_enabled": "true", "principal_type": "User2", })) .await; @@ -573,7 +572,7 @@ async fn verify_uuid_when_patching_access_policy_by_id() -> Result<(), Slashstep .add_header("Content-Type", "application/json") .json(&serde_json::json!({ "permission_level": "Editor", - "inheritance_level": "Disabled" + "is_inheritance_enabled": false })) .await; @@ -606,7 +605,7 @@ async fn verify_authentication_when_patching_access_policy_by_id() -> Result<(), let access_policy_properties = InitialAccessPolicyProperties { action_id: get_access_policies_action.id, permission_level: AccessPolicyPermissionLevel::Editor, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -617,7 +616,7 @@ async fn verify_authentication_when_patching_access_policy_by_id() -> Result<(), let response = test_server.patch(&format!("/access-policies/{}", access_policy.id)) .json(&serde_json::json!({ "permission_level": "User", - "inheritance_level": "Disabled" + "is_inheritance_enabled": false })) .await; @@ -654,7 +653,7 @@ async fn verify_permission_when_patching_access_policy() -> Result<(), Slashstep let access_policy_properties = InitialAccessPolicyProperties { action_id: update_access_policies_action.id, permission_level: AccessPolicyPermissionLevel::None, - inheritance_level: AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: AccessPolicyResourceType::Instance, @@ -666,7 +665,7 @@ async fn verify_permission_when_patching_access_policy() -> Result<(), Slashstep .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) .json(&serde_json::json!({ "permission_level": "User", - "inheritance_level": "Disabled" + "is_inheritance_enabled": false })) .await; @@ -698,7 +697,7 @@ async fn verify_access_policy_exists_when_patching_access_policy() -> Result<(), let response = test_server.patch(&format!("/access-policies/{}", Uuid::now_v7())) .json(&serde_json::json!({ "permission_level": "User", - "inheritance_level": "Disabled" + "is_inheritance_enabled": false })) .await; diff --git a/src/tests.rs b/src/tests.rs index bc2b169..4e6b1bf 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -126,7 +126,7 @@ impl TestEnvironment { let access_policy_properties = InitialAccessPolicyProperties { action_id: action.id, permission_level: crate::resources::access_policy::AccessPolicyPermissionLevel::User, - inheritance_level: crate::resources::access_policy::AccessPolicyInheritanceLevel::Enabled, + is_inheritance_enabled: true, principal_type: crate::resources::access_policy::AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), scoped_resource_type: crate::resources::access_policy::AccessPolicyResourceType::Instance, diff --git a/src/utilities.rs b/src/utilities.rs index aa8134f..0942412 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,2 +1,3 @@ pub mod slashstepql; -pub mod principal_permission_verifier; \ No newline at end of file +pub mod principal_permission_verifier; +pub mod route_handler_utilities; \ No newline at end of file diff --git a/src/utilities/route_handler_utilities.rs b/src/utilities/route_handler_utilities.rs new file mode 100644 index 0000000..00d50bd --- /dev/null +++ b/src/utilities/route_handler_utilities.rs @@ -0,0 +1,88 @@ +use std::sync::Arc; + +use crate::{HTTPError, resources::{access_policy::{AccessPolicyPermissionLevel, Principal, ResourceHierarchy}, action::Action, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::principal_permission_verifier::{PrincipalPermissionVerifier, PrincipalPermissionVerifierError}}; +use colored::Colorize; + +pub fn map_postgres_error_to_http_error(error: deadpool_postgres::PoolError) -> HTTPError { + + let http_error = HTTPError::InternalServerError(Some(error.to_string())); + eprintln!("{}", format!("Failed to get database connection, so the log cannot be saved. Printing to the console: {}", error).red()); + return http_error; + +} + +pub async fn get_action_from_name(action_name: &str, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { + + let _ = ServerLogEntry::trace(&format!("Getting action \"{}\"...", action_name), Some(&http_transaction.id), &mut postgres_client).await; + let action = match Action::get_by_name(&action_name, &mut postgres_client).await { + + Ok(action) => action, + + Err(error) => { + + let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action \"{}\": {:?}", action_name, error))); + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + } + + }; + + return Ok(action); + +} + +pub async fn get_user_from_option_user(user: &Option>, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result, HTTPError> { + + let Some(user) = user else { + + let http_error = HTTPError::InternalServerError(Some(format!("Couldn't find a user for the request. This is a bug. Make sure the authentication middleware is installed and is working properly."))); + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + }; + + return Ok(user.clone()); + +} + +pub async fn verify_user_permissions(user: &User, action: &Action, resource_hierarchy: &ResourceHierarchy, http_transaction: &HTTPTransaction, minimum_permission_level: &AccessPolicyPermissionLevel, mut postgres_client: &mut deadpool_postgres::Client) -> Result<(), HTTPError> { + + let _ = ServerLogEntry::trace(&format!("Verifying principal may use \"{}\" action...", action.name), Some(&http_transaction.id), &mut postgres_client).await; + + match PrincipalPermissionVerifier::verify_permissions(&Principal::User(user.id), &action.id, &resource_hierarchy, &minimum_permission_level, &mut postgres_client).await { + + Ok(_) => {}, + + Err(error) => { + + let http_error = match error { + + PrincipalPermissionVerifierError::ForbiddenError { .. } => { + + let message = format!("You need at least {} permission to the \"{}\" action.", minimum_permission_level.to_string(), action.name); + if user.is_anonymous { + + HTTPError::UnauthorizedError(Some(message)) + + } else { + + HTTPError::ForbiddenError(Some(message)) + + } + + }, + + _ => HTTPError::InternalServerError(Some(error.to_string())) + + }; + let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + } + + } + + return Ok(()); + +} \ No newline at end of file