Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down
50 changes: 42 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -94,6 +96,9 @@ pub enum SlashstepServerError {
#[error(transparent)]
GroupError(#[from] GroupError),

#[error(transparent)]
GroupMembershipError(#[from] GroupMembershipError),

#[error(transparent)]
AppError(#[from] AppError),

Expand All @@ -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),

Expand Down Expand Up @@ -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(());

Expand All @@ -186,7 +203,13 @@ pub enum HTTPError {
BadRequestError(Option<String>),
NotImplementedError(Option<String>),
InternalServerError(Option<String>),
UnauthorizedError(Option<String>)
UnauthorizedError(Option<String>),
UnprocessableEntity(Option<String>)
}

#[derive(Debug, Serialize)]
pub struct HTTPErrorBody {
pub message: String
}

impl fmt::Display for HTTPError {
Expand All @@ -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()))
}
}

Expand All @@ -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();

}
}
Expand Down Expand Up @@ -273,11 +301,14 @@ async fn create_database_pool() -> Result<deadpool_postgres::Pool, SlashstepServ
let host = get_environment_variable("POSTGRESQL_HOST")?;
let username = get_environment_variable("POSTGRESQL_USERNAME")?;
let database_name = get_environment_variable("POSTGRESQL_DATABASE_NAME")?;
let password_path = get_environment_variable("POSTGRESQL_PASSWORD_PATH")?;
let password = std::fs::read_to_string(password_path)?;

let mut postgres_config = tokio_postgres::Config::new();
postgres_config.host(host);
postgres_config.user(username);
postgres_config.dbname(database_name);
postgres_config.password(password);
let manager_config = deadpool_postgres::ManagerConfig {
recycling_method: deadpool_postgres::RecyclingMethod::Fast
};
Expand Down Expand Up @@ -322,8 +353,11 @@ async fn main() -> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
)
Expand All @@ -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

Expand Down
4 changes: 2 additions & 2 deletions src/queries/access_policies/grant-admin-permissions.sql
Original file line number Diff line number Diff line change
@@ -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
);
54 changes: 30 additions & 24 deletions src/queries/access_policies/initialize_access_policies_table.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 (
Expand Down
Loading