Skip to content
Open
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
16 changes: 16 additions & 0 deletions crates/cli/src/subcommands/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,18 @@ If a parent is given, the new database inherits the team permissions from the pa
A parent can only be set when a database is created, not when it is updated."
)
)
.arg(
Arg::new("organization")
.help("Name or identity of an organization for this database")
.long("organization")
.alias("org")
.long_help(
"The name or identity of an existing organization this database should be created under.

If an organization is given, the organization member's permissions apply to the new database.
An organization can only be set when a database is created, not when it is updated."
)
)
.arg(
Arg::new("name|identity")
.help("A valid domain or identity for this database")
Expand Down Expand Up @@ -139,6 +151,7 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
let num_replicas = args.get_one::<u8>("num_replicas");
let force_break_clients = args.get_flag("break_clients");
let parent = args.get_one::<String>("parent");
let org = args.get_one::<String>("organization");

// If the user didn't specify an identity and we didn't specify an anonymous identity, then
// we want to use the default identity
Expand Down Expand Up @@ -227,6 +240,9 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
if let Some(parent) = parent {
builder = builder.query(&[("parent", parent)]);
}
if let Some(org) = org {
builder = builder.query(&[("org", org)]);
}

println!("Publishing module...");

Expand Down
21 changes: 16 additions & 5 deletions crates/client-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ pub struct DatabaseDef {
pub num_replicas: Option<NonZeroU8>,
/// The host type of the supplied program.
pub host_type: HostType,
/// The optional identity of an existing database the database shall be a
/// child of.
pub parent: Option<Identity>,
/// The optional identity of an organization the database shall belong to.
pub organization: Option<Identity>,
}

/// Parameters for resetting a database via [`ControlStateDelegate::reset_database`].
Expand Down Expand Up @@ -508,9 +512,12 @@ impl axum::response::IntoResponse for Unauthorized {
}

/// Action to be authorized via [Authorization::authorize_action].
#[derive(Debug)]
#[derive(Clone, Copy, Debug)]
pub enum Action {
CreateDatabase { parent: Option<Identity> },
CreateDatabase {
parent: Option<Identity>,
organization: Option<Identity>,
},
UpdateDatabase,
ResetDatabase,
DeleteDatabase,
Expand All @@ -521,9 +528,13 @@ pub enum Action {
impl fmt::Display for Action {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::CreateDatabase { parent } => match parent {
Some(parent) => write!(f, "create database with parent {}", parent),
None => f.write_str("create database"),
Self::CreateDatabase { parent, organization } => match (parent, organization) {
(Some(parent), Some(org)) => {
write!(f, "create database with parent {} and organization {}", parent, org)
}
(Some(parent), None) => write!(f, "create database with parent {}", parent),
(None, Some(org)) => write!(f, "create database with organization {}", org),
(None, None) => f.write_str("create database"),
},
Self::UpdateDatabase => f.write_str("update database"),
Self::ResetDatabase => f.write_str("reset database"),
Expand Down
28 changes: 18 additions & 10 deletions crates/client-api/src/routes/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -650,6 +650,8 @@ pub struct PublishDatabaseQueryParams {
#[serde(default)]
host_type: HostType,
parent: Option<NameOrIdentity>,
#[serde(alias = "org")]
organization: Option<NameOrIdentity>,
}

pub async fn publish<S: NodeDelegate + ControlStateDelegate + Authorization>(
Expand All @@ -662,6 +664,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate + Authorization>(
policy,
host_type,
parent,
organization,
}): Query<PublishDatabaseQueryParams>,
Extension(auth): Extension<SpacetimeAuth>,
program_bytes: Bytes,
Expand Down Expand Up @@ -709,6 +712,10 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate + Authorization>(
None => None,
Some(parent) => parent.resolve(&ctx).await.map(Some)?,
};
let maybe_org_identity = match organization.as_ref() {
None => None,
Some(org) => org.resolve(&ctx).await.map(Some)?,
};

// Check that the replication factor looks somewhat sane.
let num_replicas = num_replicas.map(validate_replication_factor).transpose()?.flatten();
Expand All @@ -721,19 +728,18 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate + Authorization>(
.await
.map_err(log_and_500)?;
match existing.as_ref() {
// If not, check that the we caller is sufficiently authenticated.
None => {
allow_creation(&auth)?;
if let Some(parent) = maybe_parent_database_identity {
ctx.authorize_action(
auth.claims.identity,
database_identity,
Action::CreateDatabase { parent: Some(parent) },
)
.await?;
}
ctx.authorize_action(
auth.claims.identity,
database_identity,
Action::CreateDatabase {
parent: maybe_parent_database_identity,
organization: maybe_org_identity,
},
)
.await?;
}
// If yes, authorize via ctx.
Some(database) => {
ctx.authorize_action(auth.claims.identity, database.database_identity, Action::UpdateDatabase)
.await?;
Expand Down Expand Up @@ -767,6 +773,7 @@ pub async fn publish<S: NodeDelegate + ControlStateDelegate + Authorization>(
num_replicas,
host_type,
parent,
organization: maybe_org_identity,
},
schema_migration_policy,
)
Expand Down Expand Up @@ -921,6 +928,7 @@ pub async fn pre_publish<S: NodeDelegate + ControlStateDelegate + Authorization>
num_replicas: None,
host_type,
parent: None,
organization: None,
},
style,
)
Expand Down
7 changes: 7 additions & 0 deletions crates/standalone/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,13 @@ impl spacetimedb_client_api::Authorization for StandaloneEnv {
database: Identity,
action: spacetimedb_client_api::Action,
) -> Result<(), spacetimedb_client_api::Unauthorized> {
// Creating a database is always allowed.
if let spacetimedb_client_api::Action::CreateDatabase { .. } = action {
return Ok(());
}

// Otherwise, the database must already exist,
// and the `subject` equal to `database.owner_identity`.
let database = self
.get_database_by_identity(&database)
.await?
Expand Down
1 change: 1 addition & 0 deletions crates/testing/src/modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ impl CompiledModule {
num_replicas: None,
host_type: self.host_type,
parent: None,
organization: None,
},
MigrationPolicy::Compatible,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ Run `spacetime help publish` for more detailed information.

If a parent is given, the new database inherits the team permissions from the parent.
A parent can only be set when a database is created, not when it is updated.
* `--organization <ORGANIZATION>` — The name or identity of an existing organization this database should be created under.

If an organization is given, the organization member's permissions apply to the new database.
An organization can only be set when a database is created, not when it is updated.
* `-s`, `--server <SERVER>` — The nickname, domain name or URL of the server to host the database.
* `-y`, `--yes` — Run non-interactively wherever possible. This will answer "yes" to almost all prompts, but will sometimes answer "no" to preserve non-interactivity (e.g. when prompting whether to log in with spacetimedb.com).

Expand Down
6 changes: 4 additions & 2 deletions smoketests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,8 @@ def log_records(self, n):
logs = self.spacetime("logs", "--format=json", "-n", str(n), "--", self.database_identity)
return list(map(json.loads, logs.splitlines()))

def publish_module(self, domain=None, *, clear=True, capture_stderr=True, num_replicas=None, break_clients=False):
def publish_module(self, domain=None, *, clear=True, capture_stderr=True,
num_replicas=None, break_clients=False, organization=None):
publish_output = self.spacetime(
"publish",
*[domain] if domain is not None else [],
Expand All @@ -247,6 +248,7 @@ def publish_module(self, domain=None, *, clear=True, capture_stderr=True, num_re
"--yes",
*["--num-replicas", f"{num_replicas}"] if num_replicas is not None else [],
*["--break-clients"] if break_clients else [],
*["--organization", f"{organization}"] if organization is not None else [],
capture_stderr=capture_stderr,
)
self.resolved_identity = re.search(r"identity: ([0-9a-fA-F]+)", publish_output)[1]
Expand Down Expand Up @@ -406,7 +408,7 @@ def enterClassContext(cls, cm):
result = cm.__enter__()
cls.addClassCleanup(cm.__exit__, None, None, None)
return result

def assertSql(self, sql: str, expected: str):
"""Assert that executing `sql` produces the expected output."""
self.maxDiff = None
Expand Down
Loading
Loading