diff --git a/src/cli/broker.toit b/src/cli/broker.toit index ba8193f4..9568e9cf 100644 --- a/src/cli/broker.toit +++ b/src/cli/broker.toit @@ -17,6 +17,7 @@ import .config import .device import .pod import .pod-specification +import .scope show Scope import .utils import .utils.patch-build show build-diff-patch build-trivial-patch @@ -103,6 +104,16 @@ class Broker: cli_.ui.abort "$error-message (broker)." return broker-connection__ + /** + The $Scope to use when talking to the broker. + + For now derived directly from $organization-id. When the fleet file gains + a per-service scope field this will return the broker's own configured + scope instead. + */ + scope -> Scope: + return Scope.from-organization-id organization-id + short-string-for_ --device-id/Uuid -> string: if not device-short-strings_: throw "Access to device in non-device fleet." return device-short-strings_[device-id] @@ -147,7 +158,7 @@ class Broker: --part-id=id cli_.cache.get-file-path key: | store/FileStore | broker-connection_.pod-registry-upload-pod-part contents --part-id=id - --organization-id=organization-id + --scope=scope store.save contents key := cache-key-pod-manifest --broker-config=server-config @@ -156,12 +167,12 @@ class Broker: cli_.cache.get-file-path key: | store/FileStore | encoded := ubjson.encode manifest broker-connection_.pod-registry-upload-pod-manifest encoded --pod-id=pod.id - --organization-id=organization-id + --scope=scope store.save encoded description-ids := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=organization-id + --scope=scope --names=[pod.name] --create-if-absent @@ -222,7 +233,7 @@ class Broker: cli_.cache.get cache-key: | store/FileStore | trivial := build-trivial-patch patch.bits_ broker-connection_.upload-firmware trivial - --organization-id=organization-id + --scope=scope --firmware-id=trivial-id store.save-via-writer: | writer/io.Writer | trivial.do: writer.write it @@ -239,7 +250,7 @@ class Broker: trivial-old := cli_.cache.get cache-key: | store/FileStore | downloaded := null catch: downloaded = broker-connection_.download-firmware - --organization-id=organization-id + --scope=scope --id=old-id if not downloaded: cli_.ui.emit --warning "Failed to download old firmware for patch $old-id -> $trivial-id." @@ -278,7 +289,7 @@ class Broker: to64 := base64.encode patch.to_ --url-mode cli_.ui.emit --info "Uploading patch $from64 -> $to64 ($diff-size)." broker-connection_.upload-firmware diff - --organization-id=organization-id + --scope=scope --firmware-id=diff-id store.save-via-writer: | writer/io.Writer | diff.do: writer.write it @@ -313,7 +324,7 @@ class Broker: encoded-manifest := cli_.cache.get manifest-key: | store/FileStore | bytes := broker-connection_.pod-registry-download-pod-manifest --pod-id=pod-id - --organization-id=organization-id + --scope=scope store.save bytes manifest := ubjson.decode encoded-manifest return Pod.from-manifest @@ -327,7 +338,7 @@ class Broker: cli_.cache.get key: | store/FileStore | bytes := broker-connection_.pod-registry-download-pod-part part-id - --organization-id=organization-id + --scope=scope store.save bytes list-pods --names/List -> Map: @@ -337,7 +348,7 @@ class Broker: else: descriptions = broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=organization-id + --scope=scope --names=names --no-create-if-absent result := {:} @@ -349,7 +360,7 @@ class Broker: delete --description-names/List: descriptions := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=organization-id + --scope=scope --names=description-names --no-create-if-absent unknown-pod-descriptions := [] @@ -418,7 +429,7 @@ class Broker: descriptions := broker-connection_.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=organization-id + --scope=scope --names=names.to-list --no-create-if-absent @@ -762,13 +773,14 @@ class Broker: store.with-tmp-directory: | tmp-dir | // TODO(florian): do we want to rely on the cache, or should we // do a check to see if the files are really uploaded? + device-scope := Scope.from-organization-id device.organization-id broker-connection_.upload-image program.image32 --app-id=id - --organization-id=device.organization-id + --scope=device-scope --word-size=32 file.write-contents program.image32 --path="$tmp-dir/image32.bin" broker-connection_.upload-image program.image64 - --organization-id=device.organization-id + --scope=device-scope --app-id=id --word-size=64 file.write-contents program.image64 --path="$tmp-dir/image64.bin" diff --git a/src/cli/brokers/broker.toit b/src/cli/brokers/broker.toit index 484b7800..28c8331a 100644 --- a/src/cli/brokers/broker.toit +++ b/src/cli/brokers/broker.toit @@ -11,6 +11,7 @@ import ..config import ..event import ..device import ..pod-registry +import ..scope show Scope import ...shared.server-config import .supabase import .http.base @@ -98,29 +99,29 @@ interface BrokerCli implements Authenticatable: /** Uploads an application image with the given $app-id so that a device in - $organization-id can fetch it. + $scope can fetch it. There may be multiple images for the same $app-id, that differ in the $word-size. Generally $word-size is either 32 or 64. */ upload-image - --organization-id/Uuid + --scope/Scope --app-id/Uuid --word-size/int contents/ByteArray -> none /** Uploads a firmware with the given $firmware-id so that a device in - $organization-id can fetch it. + $scope can fetch it. The $chunks are a list of byte arrays. */ - upload-firmware --organization-id/Uuid --firmware-id/string chunks/List -> none + upload-firmware --scope/Scope --firmware-id/string chunks/List -> none /** - Downloads a firmware chunk inside the given $organization-id. + Downloads a firmware chunk inside the given $scope. */ - download-firmware --organization-id/Uuid --id/string -> ByteArray + download-firmware --scope/Scope --id/string -> ByteArray /** Informs the broker that a device with the given $device-id has been provisioned. @@ -154,7 +155,7 @@ interface BrokerCli implements Authenticatable: */ pod-registry-description-upsert -> int --fleet-id/Uuid - --organization-id/Uuid + --scope/Scope --name/string --description/string? @@ -217,7 +218,7 @@ interface BrokerCli implements Authenticatable: */ pod-registry-descriptions -> List --fleet-id/Uuid - --organization-id/Uuid + --scope/Scope --names/List --create-if-absent/bool @@ -249,14 +250,14 @@ interface BrokerCli implements Authenticatable: Uploads a pod part to the registry. */ pod-registry-upload-pod-part -> none - --organization-id/Uuid + --scope/Scope --part-id/string contents/ByteArray /** Downloads a pod part from the registry. */ - pod-registry-download-pod-part part-id/string --organization-id/Uuid -> ByteArray + pod-registry-download-pod-part part-id/string --scope/Scope -> ByteArray /** Saves the manifest of a pod. @@ -265,7 +266,7 @@ interface BrokerCli implements Authenticatable: a pod from its parts. */ pod-registry-upload-pod-manifest -> none - --organization-id/Uuid + --scope/Scope --pod-id/Uuid contents/ByteArray @@ -273,7 +274,7 @@ interface BrokerCli implements Authenticatable: Downloads the manifest of a pod. */ pod-registry-download-pod-manifest -> ByteArray - --organization-id/Uuid + --scope/Scope --pod-id/Uuid with-broker server-config/ServerConfig --cli/Cli [block]: diff --git a/src/cli/brokers/http/base.toit b/src/cli/brokers/http/base.toit index ec4a1b2b..2a58dcca 100644 --- a/src/cli/brokers/http/base.toit +++ b/src/cli/brokers/http/base.toit @@ -13,6 +13,7 @@ import ..broker import ...device import ...event import ...pod-registry +import ...scope show Scope import ....shared.server-config import ....shared.utils as utils import ....shared.constants show * @@ -183,16 +184,18 @@ class BrokerCliHttp implements BrokerCli: return result upload-image -> none - --organization-id/Uuid + --scope/Scope --app-id/Uuid --word-size/int contents/ByteArray: + organization-id := scope.as-uuid send-request_ COMMAND-UPLOAD_ { "path": "/toit-artemis-assets/$organization-id/images/$app-id.$word-size", "content": contents, } - upload-firmware --organization-id/Uuid --firmware-id/string chunks/List -> none: + upload-firmware --scope/Scope --firmware-id/string chunks/List -> none: + organization-id := scope.as-uuid firmware := #[] chunks.do: firmware += it send-request_ COMMAND-UPLOAD_ { @@ -200,7 +203,8 @@ class BrokerCliHttp implements BrokerCli: "content": firmware, } - download-firmware --organization-id/Uuid --id/string -> ByteArray: + download-firmware --scope/Scope --id/string -> ByteArray: + organization-id := scope.as-uuid return send-request_ COMMAND-DOWNLOAD_ { "path": "/toit-artemis-assets/$organization-id/firmware/$id", } @@ -241,9 +245,10 @@ class BrokerCliHttp implements BrokerCli: /** See $BrokerCli.pod-registry-description-upsert. */ pod-registry-description-upsert -> int --fleet-id/Uuid - --organization-id/Uuid + --scope/Scope --name/string --description/string?: + organization-id := scope.as-uuid return send-request_ COMMAND-POD-REGISTRY-DESCRIPTION-UPSERT_ { "_fleet_id": "$fleet-id", "_organization_id": "$organization-id", @@ -310,12 +315,13 @@ class BrokerCliHttp implements BrokerCli: } return response.map: PodRegistryDescription.from-map it - /** See $(BrokerCli.pod-registry-descriptions --fleet-id --organization-id --names --create-if-absent). */ + /** See $(BrokerCli.pod-registry-descriptions --fleet-id --scope --names --create-if-absent). */ pod-registry-descriptions -> List --fleet-id/Uuid - --organization-id/Uuid + --scope/Scope --names/List --create-if-absent/bool: + organization-id := scope.as-uuid response := send-request_ COMMAND-POD-REGISTRY-DESCRIPTIONS-BY-NAMES_ { "_fleet_id": "$fleet-id", "_organization_id": "$organization-id", @@ -365,32 +371,36 @@ class BrokerCliHttp implements BrokerCli: /** See $BrokerCli.pod-registry-upload-pod-part. */ pod-registry-upload-pod-part -> none - --organization-id/Uuid + --scope/Scope --part-id/string contents/ByteArray: + organization-id := scope.as-uuid send-request_ COMMAND-UPLOAD_ { "path": "/toit-artemis-pods/$organization-id/part/$part-id", "content": contents, } /** See $BrokerCli.pod-registry-download-pod-part. */ - pod-registry-download-pod-part part-id/string --organization-id/Uuid -> ByteArray: + pod-registry-download-pod-part part-id/string --scope/Scope -> ByteArray: + organization-id := scope.as-uuid return send-request_ COMMAND-DOWNLOAD-PRIVATE_ { "path": "/toit-artemis-pods/$organization-id/part/$part-id", } /** See $BrokerCli.pod-registry-upload-pod-manifest. */ pod-registry-upload-pod-manifest -> none - --organization-id/Uuid + --scope/Scope --pod-id/Uuid contents/ByteArray: + organization-id := scope.as-uuid send-request_ COMMAND-UPLOAD_ { "path": "/toit-artemis-pods/$organization-id/manifest/$pod-id", "content": contents, } /** See $BrokerCli.pod-registry-download-pod-manifest. */ - pod-registry-download-pod-manifest --organization-id/Uuid --pod-id/Uuid -> ByteArray: + pod-registry-download-pod-manifest --scope/Scope --pod-id/Uuid -> ByteArray: + organization-id := scope.as-uuid return send-request_ COMMAND-DOWNLOAD-PRIVATE_ { "path": "/toit-artemis-pods/$organization-id/manifest/$pod-id", } diff --git a/src/cli/scope.toit b/src/cli/scope.toit new file mode 100644 index 00000000..1b9aad84 --- /dev/null +++ b/src/cli/scope.toit @@ -0,0 +1,42 @@ +// Copyright (C) 2026 Toit contributors. + +import uuid show Uuid + +/** +A per-service authentication scope. + +A $Scope is the additional bit of information a service needs, on top of the + user's session, to know which slice of resources the operation applies to. + The user's identity comes from their auth provider session (stored in the + CLI's config); the scope comes from the fleet file. + +For now a $Scope always wraps an organization-id UUID. In the future scopes + will become opaque, JSON-encodable blobs that each service's auth provider + interprets independently. Calling code that needs a UUID today should go + through $as-uuid so the conversion point is greppable when the underlying + representation broadens. +*/ +class Scope: + organization-id_/Uuid + + constructor.from-organization-id organization-id/Uuid: + organization-id_ = organization-id + + /** + Returns the scope as a UUID. + + Today the scope is always a UUID; the throw is here for the future + when scopes can also be other shapes. + */ + as-uuid -> Uuid: + return organization-id_ + + operator == other -> bool: + if other is not Scope: return false + return organization-id_ == (other as Scope).organization-id_ + + hash-code -> int: + return organization-id_.hash-code + + stringify -> string: + return "Scope($organization-id_)" diff --git a/tests/broker-test.toit b/tests/broker-test.toit index 2b9b6d63..6c66a739 100644 --- a/tests/broker-test.toit +++ b/tests/broker-test.toit @@ -166,11 +166,11 @@ test-image broker-cli/broker.BrokerCli broker-service/broker.BrokerService --net contents-64 = ("test-image 64" * 10_000).to-byte-array broker-cli.upload-image contents-32 - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --app-id=APP-ID --word-size=32 broker-cli.upload-image contents-64 - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --app-id=APP-ID --word-size=64 @@ -209,11 +209,11 @@ test-firmware broker-cli/broker.BrokerCli broker-service/broker.BrokerService -- broker-cli.upload-firmware chunks --firmware-id=FIRMWARE-ID - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE downloaded-bytes := broker-cli.download-firmware --id=FIRMWARE-ID - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE expect-bytes-equal contents downloaded-bytes broker-connection := broker-service.connect --network=network --device=DEVICE1 diff --git a/tests/pod-registry-test.toit b/tests/pod-registry-test.toit index 6038a9a8..344f2325 100644 --- a/tests/pod-registry-test.toit +++ b/tests/pod-registry-test.toit @@ -40,7 +40,7 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create a description. description-id := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --name="pod1" --description=null descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id @@ -52,7 +52,7 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create the same description again. description-id-received := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --name="pod1" --description=null expect-equals description-id description-id-received @@ -60,7 +60,7 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Create another description. description-id2 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --name="pod2" --description="description2" descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id @@ -85,7 +85,7 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Get the descriptions by name. descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --names=["pod1"] --no-create-if-absent expect-equals 1 descriptions.size @@ -95,7 +95,7 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: // Do the same but create the missing description. descriptions = broker-cli.pod-registry-descriptions --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --names=["pod1", "pod3"] --create-if-absent names = descriptions.map: it.name @@ -266,13 +266,13 @@ test-pod-registry --test-broker/TestBroker broker-cli/broker.BrokerCli: description-id3 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --name="pod3" --description=null description-id4 := broker-cli.pod-registry-description-upsert --fleet-id=fleet-id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --name="pod4" --description=null @@ -322,20 +322,20 @@ test-pods --test-broker/TestBroker broker-cli/broker.BrokerCli: pod-contents.do: | key/string value/string | broker-cli.pod-registry-upload-pod-part - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --part-id=key value.to-byte-array // Upload the keys as a manifest. manifest := ubjson.encode pod broker-cli.pod-registry-upload-pod-manifest - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --pod-id=pod-id manifest // Download the manifest. downloaded-manifest := broker-cli.pod-registry-download-pod-manifest - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE --pod-id=pod-id expect-equals manifest downloaded-manifest decoded := ubjson.decode downloaded-manifest @@ -345,5 +345,5 @@ test-pods --test-broker/TestBroker broker-cli/broker.BrokerCli: // Download the parts. decoded.do: | _ id/string | downloaded-part := broker-cli.pod-registry-download-pod-part id - --organization-id=TEST-ORGANIZATION-UUID + --scope=TEST-SCOPE expect-equals pod-contents[id] downloaded-part.to-string diff --git a/tests/utils.toit b/tests/utils.toit index 3823954b..32e5a40f 100644 --- a/tests/utils.toit +++ b/tests/utils.toit @@ -20,6 +20,7 @@ import uuid show Uuid import artemis.cli as artemis-pkg import artemis.cli.server-config as cli-server-config import artemis.cli.cache as artemis-cache +import artemis.cli.scope as cli-scope import artemis.cli.utils show read-json write-json-to-file untar import artemis.shared.server-config import artemis.shared.version as configured-version @@ -59,6 +60,7 @@ ADMIN-NAME ::= "Admin User" /** Preseeded "Test Organization". */ TEST-ORGANIZATION-NAME ::= "Test Organization" TEST-ORGANIZATION-UUID ::= Uuid.parse "4b6d9e35-cae9-44c0-8da0-6b0e485987e2" +TEST-SCOPE ::= cli-scope.Scope.from-organization-id TEST-ORGANIZATION-UUID /** Preseeded test device in $TEST-ORGANIZATION-UUID. */ TEST-DEVICE-UUID ::= Uuid.parse "eb45c662-356c-4bea-ad8c-ede37688fddf"