Skip to content

Commit

Permalink
Merge pull request #4222 from systeminit/wendy/eng-2609-allow-sdf-to-…
Browse files Browse the repository at this point in the history
…control-who-or-who-doesnt-have-access-to-create

feat(sdf) - create workspace permissions system
  • Loading branch information
wendybujalski authored Jul 24, 2024
2 parents 101e6dc + 33b7c70 commit 54e9167
Show file tree
Hide file tree
Showing 11 changed files with 217 additions and 15 deletions.
1 change: 0 additions & 1 deletion bin/auth-api/src/services/workspaces.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export async function createWorkspace(
instanceUrl,
instanceEnvType: newWorkspace.instanceEnvType,
isDefaultWorkspace: newWorkspace.isDefault,
// TODO: track env type and other data when it becomes useful
});

await prisma.workspaceMembers.create({
Expand Down
29 changes: 27 additions & 2 deletions bin/sdf/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ use std::path::PathBuf;

use clap::{builder::EnumValueParser, builder::PossibleValuesParser, ArgAction, Parser};

use sdf_server::{Config, ConfigError, ConfigFile, FeatureFlag, MigrationMode, StandardConfigFile};
use sdf_server::{
server::{WorkspacePermissions, WorkspacePermissionsMode},
Config, ConfigError, ConfigFile, FeatureFlag, MigrationMode, StandardConfigFile,
};
use si_std::SensitiveString;

const NAME: &str = "sdf";
Expand Down Expand Up @@ -172,7 +175,7 @@ pub(crate) struct Args {
#[arg(long, env = "SI_MODULE_INDEX_URL")]
pub(crate) module_index_url: Option<String>,

/// The base URL for the module-index API server
/// Allow for Posthog feature flags in SDF
#[arg(
long,
env = "SI_FEATURES",
Expand All @@ -181,6 +184,19 @@ pub(crate) struct Args {
rename_all = "snake_case",
)]
pub(crate) features: Vec<FeatureFlag>,

/// Create Workspace Permissions Mode [default: closed]
#[arg(long, env = "SI_CREATE_WORKSPACE_PERMISSIONS", value_parser = PossibleValuesParser::new(WorkspacePermissionsMode::variants()))]
pub(crate) create_workspace_permissions: Option<String>,

/// Create Workspace Permissions Allowlist
#[arg(
long,
env = "SI_CREATE_WORKSPACE_ALLOWLIST",
value_delimiter = ',',
rename_all = "snake_case"
)]
pub(crate) create_workspace_allowlist: Vec<WorkspacePermissions>,
}

impl TryFrom<Args> for Config {
Expand Down Expand Up @@ -282,6 +298,15 @@ impl TryFrom<Args> for Config {

config_map.set("boot_feature_flags", args.features);

if let Some(create_workspace_permissions) = args.create_workspace_permissions {
config_map.set("create_workspace_permissions", create_workspace_permissions);
}

config_map.set(
"create_workspace_allowlist",
args.create_workspace_allowlist,
);

config_map.set("nats.connection_name", NAME);
config_map.set("pg.application_name", NAME);
config_map.set("layer_db_config.pg_pool_config.application_name", NAME);
Expand Down
2 changes: 2 additions & 0 deletions component/init/configs/service.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
pkgs_path = "/tmp"
create_workspace_permissions = "$SI_WORKSPACE_PERMISSIONS"
create_workspace_allowlist = [ "$SI_WORKSPACE_ALLOW_LIST" ]

[crypto]
encryption_key_base64 = "$SI_ENCRYPTION_KEY_BASE64"
Expand Down
17 changes: 17 additions & 0 deletions lib/dal/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,23 @@ impl User {
Ok(())
}

pub async fn is_first_user(&self, ctx: &DalContext) -> UserResult<bool> {
let row = ctx
.txns()
.await?
.pg()
.query_opt("SELECT pk FROM users ORDER BY created_at ASC LIMIT 1", &[])
.await?;

match row {
Some(row) => {
let oldest_user_pk: UserPk = row.get("pk");
Ok(oldest_user_pk == self.pk)
}
None => Ok(false),
}
}

pub async fn delete_user_from_workspace(
ctx: &DalContext,
user_pk: UserPk,
Expand Down
3 changes: 2 additions & 1 deletion lib/sdf-server/src/server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub use config::{
detect_and_configure_development, Config, ConfigBuilder, ConfigError, ConfigFile,
IncomingStream, StandardConfig, StandardConfigFile,
IncomingStream, StandardConfig, StandardConfigFile, WorkspacePermissions,
WorkspacePermissionsMode,
};
pub use dal::{JobQueueProcessor, MigrationMode, NatsProcessor, ServicesContext};
pub use nats_multiplexer::CRDT_MULTIPLEXER_SUBJECT;
Expand Down
54 changes: 54 additions & 0 deletions lib/sdf-server/src/server/config.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use dal::jwt_key::JwtConfig;
use serde_with::{DeserializeFromStr, SerializeDisplay};
use si_crypto::VeritechCryptoConfig;
use si_layer_cache::{db::LayerDbConfig, error::LayerDbError};
use std::collections::HashSet;
Expand All @@ -7,6 +8,7 @@ use std::{
net::{SocketAddr, ToSocketAddrs},
path::{Path, PathBuf},
};
use strum::{Display, EnumString, VariantNames};

use buck2_resources::Buck2Resources;
use dal::feature_flags::FeatureFlag;
Expand All @@ -26,6 +28,36 @@ pub use si_settings::{StandardConfig, StandardConfigFile};

const DEFAULT_MODULE_INDEX_URL: &str = "https://module-index.systeminit.com";

#[derive(
Debug,
Default,
Clone,
Copy,
SerializeDisplay,
Display,
DeserializeFromStr,
EnumString,
VariantNames,
PartialEq,
Eq,
)]
#[strum(serialize_all = "camelCase")]
pub enum WorkspacePermissionsMode {
#[default]
Closed,
Allowlist,
Open,
}

impl WorkspacePermissionsMode {
#[must_use]
pub const fn variants() -> &'static [&'static str] {
<WorkspacePermissionsMode as strum::VariantNames>::VARIANTS
}
}

pub type WorkspacePermissions = String;

#[remain::sorted]
#[derive(Debug, Error)]
pub enum ConfigError {
Expand Down Expand Up @@ -88,6 +120,10 @@ pub struct Config {
pkgs_path: CanonicalFile,

boot_feature_flags: HashSet<FeatureFlag>,

create_workspace_permissions: WorkspacePermissionsMode,

create_workspace_allowlist: Vec<WorkspacePermissions>,
}

impl StandardConfig for Config {
Expand Down Expand Up @@ -163,6 +199,16 @@ impl Config {
pub fn layer_db_config(&self) -> &LayerDbConfig {
&self.layer_db_config
}

// The Create Workspace Permissions Mode should be set via an env variable or it will default to Closed
pub fn create_workspace_permissions(&self) -> &WorkspacePermissionsMode {
&self.create_workspace_permissions
}

// This Allowlist is a list of email addresses only used in WorkspacePermissionsMode::Allowlist
pub fn create_workspace_allowlist(&self) -> &Vec<WorkspacePermissions> {
&self.create_workspace_allowlist
}
}

impl ConfigBuilder {
Expand Down Expand Up @@ -199,6 +245,10 @@ pub struct ConfigFile {
symmetric_crypto_service: SymmetricCryptoServiceConfigFile,
#[serde(default)]
boot_feature_flags: Vec<FeatureFlag>,
#[serde(default)]
create_workspace_permissions: WorkspacePermissionsMode,
#[serde(default)]
create_workspace_allowlist: Vec<WorkspacePermissions>,
}

impl Default for ConfigFile {
Expand All @@ -215,6 +265,8 @@ impl Default for ConfigFile {
module_index_url: default_module_index_url(),
symmetric_crypto_service: default_symmetric_crypto_config(),
boot_feature_flags: Default::default(),
create_workspace_permissions: Default::default(),
create_workspace_allowlist: Default::default(),
}
}
}
Expand All @@ -241,6 +293,8 @@ impl TryFrom<ConfigFile> for Config {
config.symmetric_crypto_service(value.symmetric_crypto_service.try_into()?);
config.layer_db_config(value.layer_db_config);
config.boot_feature_flags(value.boot_feature_flags.into_iter().collect::<HashSet<_>>());
config.create_workspace_permissions(value.create_workspace_permissions);
config.create_workspace_allowlist(value.create_workspace_allowlist);
config.build().map_err(Into::into)
}
}
Expand Down
22 changes: 21 additions & 1 deletion lib/sdf-server/src/server/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ use ulid::Ulid;
use veritech_client::Client as VeritechClient;

use super::state::AppState;
use super::{routes, Config, IncomingStream, UdsIncomingStream, UdsIncomingStreamError};
use super::{
routes, Config, IncomingStream, UdsIncomingStream, UdsIncomingStreamError,
WorkspacePermissions, WorkspacePermissionsMode,
};
use crate::server::config::VeritechKeyPair;

#[remain::sorted]
Expand Down Expand Up @@ -136,6 +139,8 @@ impl Server<(), ()> {
posthog_client,
ws_multiplexer_client,
crdt_multiplexer_client,
*config.create_workspace_permissions(),
config.create_workspace_allowlist().to_vec(),
)?;

tokio::spawn(ws_multiplexer.run(shutdown_broadcast_rx.resubscribe()));
Expand Down Expand Up @@ -180,6 +185,8 @@ impl Server<(), ()> {
posthog_client,
ws_multiplexer_client,
crdt_multiplexer_client,
*config.create_workspace_permissions(),
config.create_workspace_allowlist().to_vec(),
)?;

tokio::spawn(ws_multiplexer.run(shutdown_broadcast_rx.resubscribe()));
Expand Down Expand Up @@ -473,6 +480,8 @@ pub fn build_service_for_tests(
posthog_client: PosthogClient,
ws_multiplexer_client: MultiplexerClient,
crdt_multiplexer_client: MultiplexerClient,
create_workspace_permissions: WorkspacePermissionsMode,
create_workspace_allowlist: Vec<WorkspacePermissions>,
) -> Result<(Router, oneshot::Receiver<()>, broadcast::Receiver<()>)> {
build_service_inner(
services_context,
Expand All @@ -481,6 +490,8 @@ pub fn build_service_for_tests(
true,
ws_multiplexer_client,
crdt_multiplexer_client,
create_workspace_permissions,
create_workspace_allowlist,
)
}

Expand All @@ -490,6 +501,8 @@ pub fn build_service(
posthog_client: PosthogClient,
ws_multiplexer_client: MultiplexerClient,
crdt_multiplexer_client: MultiplexerClient,
create_workspace_permissions: WorkspacePermissionsMode,
create_workspace_allowlist: Vec<WorkspacePermissions>,
) -> Result<(Router, oneshot::Receiver<()>, broadcast::Receiver<()>)> {
build_service_inner(
services_context,
Expand All @@ -498,16 +511,21 @@ pub fn build_service(
false,
ws_multiplexer_client,
crdt_multiplexer_client,
create_workspace_permissions,
create_workspace_allowlist,
)
}

#[allow(clippy::too_many_arguments)]
fn build_service_inner(
services_context: ServicesContext,
jwt_public_signing_key: JwtPublicSigningKey,
posthog_client: PosthogClient,
for_tests: bool,
ws_multiplexer_client: MultiplexerClient,
crdt_multiplexer_client: MultiplexerClient,
create_workspace_permissions: WorkspacePermissionsMode,
create_workspace_allowlist: Vec<WorkspacePermissions>,
) -> Result<(Router, oneshot::Receiver<()>, broadcast::Receiver<()>)> {
let (shutdown_tx, shutdown_rx) = mpsc::channel(1);
let (shutdown_broadcast_tx, shutdown_broadcast_rx) = broadcast::channel(1);
Expand All @@ -521,6 +539,8 @@ fn build_service_inner(
for_tests,
ws_multiplexer_client,
crdt_multiplexer_client,
create_workspace_permissions,
create_workspace_allowlist,
);

let path_filter = Box::new(|path: &str| match path {
Expand Down
5 changes: 5 additions & 0 deletions lib/sdf-server/src/server/service/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ pub enum SessionError {
Workspace(#[from] WorkspaceError),
#[error("workspace {0} not yet migrated to new snapshot graph version. Migration required")]
WorkspaceNotYetMigrated(WorkspacePk),
#[error("you do not have permission to create a workspace on this instance")]
WorkspacePermissions,
}

#[derive(Debug, Serialize, Deserialize)]
Expand All @@ -67,6 +69,9 @@ impl IntoResponse for SessionError {
Some("WORKSPACE_NOT_INITIALIZED"),
None,
),
SessionError::WorkspacePermissions => {
(StatusCode::UNAUTHORIZED, None, Some(self.to_string()))
}
_ => (StatusCode::INTERNAL_SERVER_ERROR, None, None),
};

Expand Down
Loading

0 comments on commit 54e9167

Please sign in to comment.