Workspaces

Storage connections

client.storage_connections manages the storage connections a new workspace can keep its files in, instead of Nace's managed storage. See Use your own storage for the concept, and the API reference for every field.

  • A connection names one destination: an S3 bucket and region, or an Azure storage account and container, with an optional base prefix. Each workspace gets its own folder beneath it. The destination can't change.
  • Who can do what: any member of the account lists and reads connections and binds a ready one to a new workspace. Only a connection's creator, or an owner or admin of the account, changes it; anyone else raises NacePermissionDeniedError (access_denied).
  • Cost: managing connections costs nothing and works at any balance.
  • Availability: a deployment that doesn't offer customer storage raises NaceNotFoundError (storage_connection_unavailable) on every call.

A connection's status is one of:

StateWhat you can do
draftSet it up, save credentials and validate it. It can't hold a workspace yet.
readyGive it to a new workspace. Validated: Nace can write, read and delete in the destination.
degradedA later check failed. Fix the access in your cloud and validate it again.
revokedNace stopped using it, and workspaces in it stop reading and writing. Validate one you revoked with reactivate=True to restore it.

A connection is set up in five steps:

  1. create makes a draft.
  2. setup returns what to create in your cloud: for S3, an IAM role trust policy carrying the connection's external_id, its permissions policy, a CloudFormation template and a quick-create link; for Azure, the az commands that give a service principal access to the container. Save your AWS account id and role name, or your Azure subscription and resource group, with set_setup first, and the setup comes back filled in.
  3. Create the role or service principal in your cloud.
  4. set_credentials saves the role's ARN, or the service principal and its secret.
  5. validate starts a storage_validation job, and wait_for_validation waits for it. Once it succeeds, the connection is ready.

A new IAM role or Azure role assignment can take a few minutes to apply, so a fresh one may fail its first validation: wait and validate again. Then pass the connection's id as storage_connection_id to client.workspaces.create. Only a connection whose is_ready is True can be bound: status is ready, and credentials_pending, validation_running and reactivation_requires_approval are all False.

MethodParametersReturnsDescription
create(*, name, destination, idempotency_key=None)name: str (1 to 200 characters), destination: S3Destination | AzureBlobDestination | dict, idempotency_key: str (up to 200 characters)StorageConnectionCreates a draft connection. It carries an Idempotency-Key, yours or one minted per call, so a retry returns the same connection.
list(**options)limit: int (1 to 200), cursor: strStorageConnectionPageOne page of the account's connections.
iter()Iterator[StorageConnection]Every connection across every page, 200 a page.
get(connection_id)connection_id: strStorageConnectionOne connection and its state.
setup(connection_id)connection_id: strStorageConnectionSetupWhat to create in your cloud, generated from the saved options.
set_setup(connection_id, options)connection_id: str, options: S3SetupOptions | AzureSetupOptions | dictStorageConnectionSetupSaves the non-secret inputs the setup is generated from, and returns the setup. Their type must match the destination's.
set_credentials(connection_id, credential)connection_id: str, credential: AwsRoleCredential | AzureServicePrincipalCredential | dictStorageConnectionSaves the role or service principal Nace uses. They are never returned. The connection has credentials_pending until a validation passes.
validate(connection_id, *, reactivate=None)connection_id: str, reactivate: boolDocumentJobStarts a storage_validation job that checks Nace can write, read and delete in the destination. reactivate=True restores a connection you revoked once it passes.
validation(connection_id)connection_id: strDocumentJobThe latest validation job. A failed job's error says what Nace could not do.
wait_for_validation(connection_id, *, timeout=180.0, poll_interval=2.0)connection_id: str, timeout: float, poll_interval: floatDocumentJobPolls validation every poll_interval seconds until the job is succeeded, failed or cancelled, for up to timeout seconds, and returns it. A failed job is returned, not raised.
revoke(connection_id)connection_id: strStorageConnectionStops Nace using the connection. Workspaces bound to it stop reading and writing until a validation with reactivate=True passes. Their files stay in your bucket.
delete(connection_id)connection_id: strNoneDeletes a connection no workspace uses. Nothing in the bucket is touched.
  • The Azure secret is a SecretStr on AzureServicePrincipalCredential: its repr, str() and model_dump_json() show **********, a validation error never quotes it, and SDK debug logs mask client_secret in request bodies. Only the request to Drex carries it. A dict credential works too, with the secret as a str or a SecretStr.
  • Retries: list, iter, get, setup, set_setup, set_credentials, validation and revoke are retried, and so is create, which carries an idempotency key. validate and delete aren't, unless you pass retry: a retry after the first call landed starts a second validation, or raises NaceNotFoundError for the deleted connection.
WhenIt raises
A connection_id your account doesn't have, or customer storage is off on the deploymentNaceNotFoundError (storage_connection_unavailable)
A change by someone who isn't the connection's creator, or an owner or adminNacePermissionDeniedError (access_denied)
set_credentials with credentials the cloud refusesNaceUnprocessableEntityError (storage_credentials_refused). The message never quotes them.
delete on a connection a workspace uses, or validate on one Nace revokedNaceConflictError
wait_for_validation still running after timeout secondsNaceJobTimeoutError; the validation keeps running
A limit outside 1 to 200, or a connection_id holding anything but letters, digits, - and _NaceError, before any request

List connections

list_connections.py
from nace_sdk import NaceClient

with NaceClient() as client:
    page = client.storage_connections.list(limit=20)
    for connection in page.items:
        print(connection.id, connection.name, connection.status)

    for connection in client.storage_connections.iter():
        print(connection.id, connection.destination.type, connection.is_ready)

Connect an S3 bucket

connect_s3.py
from nace_sdk import AwsRoleCredential, S3Destination, S3SetupOptions

connection = client.storage_connections.create(
    name="Audit evidence bucket",
    destination=S3Destination(bucket="acme-audit-evidence", region="us-east-1", prefix="nace"),
)
setup = client.storage_connections.set_setup(
    connection.id, S3SetupOptions(aws_account_id="444455556666", role_name="nace-workspaces")
)
print(setup.launch_url)  # opens the CloudFormation stack that creates the role

# Once the stack has created the role:
client.storage_connections.set_credentials(connection.id, AwsRoleCredential(role_arn=setup.role_arn))
client.storage_connections.validate(connection.id)
job = client.storage_connections.wait_for_validation(connection.id)
print(job.status, job.error_message)

Connect an Azure Blob container

connect_azure.py
import os

from nace_sdk import AzureBlobDestination, AzureServicePrincipalCredential, AzureSetupOptions

connection = client.storage_connections.create(
    name="Audit evidence container",
    destination=AzureBlobDestination(account="acmeaudit", container="evidence"),
)
setup = client.storage_connections.set_setup(
    connection.id, AzureSetupOptions(subscription_id="<subscription id>", resource_group="audit-rg")
)
print(setup.azure_cli)  # run these to give your service principal access

client.storage_connections.set_credentials(
    connection.id,
    AzureServicePrincipalCredential(
        tenant_id="<tenant id>", client_id="<client id>", client_secret=os.environ["AZURE_CLIENT_SECRET"]
    ),
)
client.storage_connections.validate(connection.id)
print(client.storage_connections.wait_for_validation(connection.id).status)

Create a workspace in your storage

Pass a ready connection's id as storage_connection_id. See Create a workspace.

workspace_with_storage.py
connection = client.storage_connections.get(connection.id)
if connection.is_ready:
    workspace = client.workspaces.create(name="fy25-audit", storage_connection_id=connection.id)
    print(workspace.storage.location)

On this page