diff --git a/.gitattributes b/.gitattributes index 61c3cadbd..bd5990190 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,11 @@ module/services/telemetry/code/pkg/gen/** linguist-generated=true # Build recipes emitted by the pinned Go service agent. module/services/accounts/builder/** linguist-generated=true module/services/auth-gateway/builder/** linguist-generated=true + +# Native protocol and catalog output; authored proto inputs and dependency files stay visible. +/module/services/accounts/code/pkg/gen/** linguist-generated=true +/module/services/accounts/generated/** linguist-generated=true +/module/services/frontend/code/src/gen/** linguist-generated=true +/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/** linguist-generated=true +/module/contracts/api/accounts/authority/proto/host/** linguist-generated=true +/module/contracts/api/accounts/connect/proto/host/** linguist-generated=true diff --git a/libraries/source-read-sdk/library.codefly.yaml b/libraries/source-read-sdk/library.codefly.yaml index 0171cc00f..8d8b419a5 100644 --- a/libraries/source-read-sdk/library.codefly.yaml +++ b/libraries/source-read-sdk/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: authority - contract-digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + contract-digest: sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5 services: - ModuleCapabilitiesService generated-by: diff --git a/module/KEY_ROTATION.md b/module/KEY_ROTATION.md index 604c0bc2f..4e5c5f71a 100644 --- a/module/KEY_ROTATION.md +++ b/module/KEY_ROTATION.md @@ -62,7 +62,26 @@ TTL — or within TTL + grace if accounts is unreachable for the whole window. > so there is no projection to read and no agent that owns the driver the way > `service-vault` owns Vault's. Filed as codefly-dev/cli#924. Everything above > the seam is already vendor-neutral, so when that kind exists the driver moves -> below it and nothing here changes. +> below it and nothing here changes. New managed providers use the owner runtime +> client described below; the legacy GCP implementation remains unchanged. + +`KEY_SERVICE_MANAGED_BINDING` optionally carries the deployment-produced, +non-secret provider descriptor consumed by `service-vault/keyservice`. Accounts +depends on that client's generic encryption, MAC and wrapping operations; no +second cloud SDK or driver lives here. The existing `kms` / `kms-wrapped` +selections still apply. Mixing a descriptor with legacy KMS key names is refused. + +The client owns provider ciphertexts, key-version checks and workload identity; +Accounts retains `cfs1` envelope framing, domain errors, tenant-key selection and +the `signing-key-wrap` purpose. The unwrapped value must still be a base64 +Ed25519 seed. A provider outage never falls back to ephemeral signing, including +when a local caller otherwise permits a fixture key. + +Build or reuse: the existing Host `Sealer`, `KeyFactory`, `Cipher` and wrapped-seed +path already define the required contracts. The owner client's provider driver +fills the missing implementation; no new Host crypto primitive, signing algorithm +or key store is introduced. Infrastructure allocation, exact workload grants, +recovery custody and live deployment remain separate qualification requirements. Two backends, selected per key family in the `key-service` group diff --git a/module/configurations/local/key-service.env b/module/configurations/local/key-service.env index b75efdf25..85b20b80c 100644 --- a/module/configurations/local/key-service.env +++ b/module/configurations/local/key-service.env @@ -119,3 +119,8 @@ KEY_SERVICE_KMS_MAC_KEY= # in the envelope framing, each by name. KEY_SERVICE_KMS_SIGNING_WRAP_KEY= KEY_SERVICE_SIGNING_KEY_WRAPPED= + +# Optional provider-owned managed installation. Supplied by the deployment +# owner; mutually exclusive with the legacy KMS key names above. The existing +# kms and kms-wrapped selections retain their custody and signing semantics. +KEY_SERVICE_MANAGED_BINDING= diff --git a/module/contracts/api/accounts/authority/contract.binpb b/module/contracts/api/accounts/authority/contract.binpb index 8cfc99aa6..39c17ac8b 100644 Binary files a/module/contracts/api/accounts/authority/contract.binpb and b/module/contracts/api/accounts/authority/contract.binpb differ diff --git a/module/contracts/api/accounts/authority/proto/host/DIGESTS.sha256 b/module/contracts/api/accounts/authority/proto/host/DIGESTS.sha256 index 1d31bafed..680171a92 100644 --- a/module/contracts/api/accounts/authority/proto/host/DIGESTS.sha256 +++ b/module/contracts/api/accounts/authority/proto/host/DIGESTS.sha256 @@ -2,5 +2,5 @@ 5f743acb2e21e23eb916092313f54b0737742fe73c77d3f43208987a4aabf4c4 authority/v1/authority.proto f1ea4a9113527f8ed972660c3d2134a39135e96cf212b780ec04d14321ddcab1 authority/v1/work_context.proto 65353da82976f3f00d0a87792b93a723adbfc9deec48a5f7e337ff9526be53f5 composition/v1/composition.proto -d69c70528987ef59180b0ac683ac3c30de9b18077dacc3eece1342465953314e installation/v1/installation.proto -5d1e2f8fb2e7c3a7b8e027d8de23e26e53fa21212831e23fac492372ac53f28b session/v1/session.proto +5de7bd8b5f68d5bdf081dab59352473a04c13c6bb5cecae0ec32beb6f5a45305 installation/v1/installation.proto +8754895a7f8ab7d69994b11cf88be17a575f19699f78975041e17e78008556b1 session/v1/session.proto diff --git a/module/contracts/api/accounts/authority/proto/host/installation/v1/installation.proto b/module/contracts/api/accounts/authority/proto/host/installation/v1/installation.proto index c1ebefd92..f3f3f1397 100644 --- a/module/contracts/api/accounts/authority/proto/host/installation/v1/installation.proto +++ b/module/contracts/api/accounts/authority/proto/host/installation/v1/installation.proto @@ -30,10 +30,16 @@ service InstallationService { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. rpc ListRoleGrants(ListRoleGrantsRequest) returns (ListRoleGrantsResponse); - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. rpc List(ListRequest) returns (ListResponse); + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + rpc ListHeld(ListHeldRequest) returns (ListHeldResponse); } message CurrentInstallationRequest { @@ -309,3 +315,25 @@ message OfferTool { // True when the tool changes data. bool write = 3; } + +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +message ListHeldRequest { + // The organisation. + string organization_id = 1; +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +message ListHeldResponse { + // The installations. + repeated HeldInstallation installations = 1; +} + +// HeldInstallation is one installation the person may use. +message HeldInstallation { + // The installation. + Installation installation = 1; + // The roles the person holds on it, directly or through a team, sorted. + repeated string roles = 2; +} diff --git a/module/contracts/api/accounts/authority/proto/host/session/v1/session.proto b/module/contracts/api/accounts/authority/proto/host/session/v1/session.proto index bf7894518..6b07d758b 100644 --- a/module/contracts/api/accounts/authority/proto/host/session/v1/session.proto +++ b/module/contracts/api/accounts/authority/proto/host/session/v1/session.proto @@ -54,6 +54,9 @@ message FixturePerson { repeated string team_ids = 5; // The token SignIn takes for them. string fixture_token = 6; + // The organisation's name, as the fixture spells it, for a page that shows + // a person where they sign into; the id above is the identity. + string organization_display_name = 7; } // CurrentRequest asks for the caller's own session; the bearer is the call's. diff --git a/module/contracts/api/accounts/connect/contract.binpb b/module/contracts/api/accounts/connect/contract.binpb index 8cfc99aa6..39c17ac8b 100644 Binary files a/module/contracts/api/accounts/connect/contract.binpb and b/module/contracts/api/accounts/connect/contract.binpb differ diff --git a/module/contracts/api/accounts/connect/proto/host/DIGESTS.sha256 b/module/contracts/api/accounts/connect/proto/host/DIGESTS.sha256 index 1d31bafed..680171a92 100644 --- a/module/contracts/api/accounts/connect/proto/host/DIGESTS.sha256 +++ b/module/contracts/api/accounts/connect/proto/host/DIGESTS.sha256 @@ -2,5 +2,5 @@ 5f743acb2e21e23eb916092313f54b0737742fe73c77d3f43208987a4aabf4c4 authority/v1/authority.proto f1ea4a9113527f8ed972660c3d2134a39135e96cf212b780ec04d14321ddcab1 authority/v1/work_context.proto 65353da82976f3f00d0a87792b93a723adbfc9deec48a5f7e337ff9526be53f5 composition/v1/composition.proto -d69c70528987ef59180b0ac683ac3c30de9b18077dacc3eece1342465953314e installation/v1/installation.proto -5d1e2f8fb2e7c3a7b8e027d8de23e26e53fa21212831e23fac492372ac53f28b session/v1/session.proto +5de7bd8b5f68d5bdf081dab59352473a04c13c6bb5cecae0ec32beb6f5a45305 installation/v1/installation.proto +8754895a7f8ab7d69994b11cf88be17a575f19699f78975041e17e78008556b1 session/v1/session.proto diff --git a/module/contracts/api/accounts/connect/proto/host/installation/v1/installation.proto b/module/contracts/api/accounts/connect/proto/host/installation/v1/installation.proto index c1ebefd92..f3f3f1397 100644 --- a/module/contracts/api/accounts/connect/proto/host/installation/v1/installation.proto +++ b/module/contracts/api/accounts/connect/proto/host/installation/v1/installation.proto @@ -30,10 +30,16 @@ service InstallationService { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. rpc ListRoleGrants(ListRoleGrantsRequest) returns (ListRoleGrantsResponse); - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. rpc List(ListRequest) returns (ListResponse); + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + rpc ListHeld(ListHeldRequest) returns (ListHeldResponse); } message CurrentInstallationRequest { @@ -309,3 +315,25 @@ message OfferTool { // True when the tool changes data. bool write = 3; } + +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +message ListHeldRequest { + // The organisation. + string organization_id = 1; +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +message ListHeldResponse { + // The installations. + repeated HeldInstallation installations = 1; +} + +// HeldInstallation is one installation the person may use. +message HeldInstallation { + // The installation. + Installation installation = 1; + // The roles the person holds on it, directly or through a team, sorted. + repeated string roles = 2; +} diff --git a/module/contracts/api/accounts/connect/proto/host/session/v1/session.proto b/module/contracts/api/accounts/connect/proto/host/session/v1/session.proto index bf7894518..6b07d758b 100644 --- a/module/contracts/api/accounts/connect/proto/host/session/v1/session.proto +++ b/module/contracts/api/accounts/connect/proto/host/session/v1/session.proto @@ -54,6 +54,9 @@ message FixturePerson { repeated string team_ids = 5; // The token SignIn takes for them. string fixture_token = 6; + // The organisation's name, as the fixture spells it, for a page that shows + // a person where they sign into; the id above is the identity. + string organization_display_name = 7; } // CurrentRequest asks for the caller's own session; the bearer is the call's. diff --git a/module/contracts/api/catalog.codefly.json b/module/contracts/api/catalog.codefly.json index 192675738..5a3c82f5a 100644 --- a/module/contracts/api/catalog.codefly.json +++ b/module/contracts/api/catalog.codefly.json @@ -10,7 +10,7 @@ "kind": "protobuf", "package": "saas.accounts.v1", "path": "contracts/api/accounts/authority/contract.binpb", - "digest": "sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831", + "digest": "sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5", "services": [ { "name": "ModuleCapabilitiesService", @@ -66,7 +66,7 @@ "kind": "protobuf", "package": "", "path": "contracts/api/accounts/connect/contract.binpb", - "digest": "sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831", + "digest": "sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5", "services": [ { "name": "ArtifactService", @@ -109,6 +109,7 @@ "/host.installation.v1.InstallationService/GrantRole", "/host.installation.v1.InstallationService/Install", "/host.installation.v1.InstallationService/List", + "/host.installation.v1.InstallationService/ListHeld", "/host.installation.v1.InstallationService/ListRoleGrants", "/host.installation.v1.InstallationService/Review", "/host.installation.v1.InstallationService/RevokeRole" diff --git a/module/frontendpayload/README.md b/module/frontendpayload/README.md index b5d258615..552bd096f 100644 --- a/module/frontendpayload/README.md +++ b/module/frontendpayload/README.md @@ -11,6 +11,11 @@ repository's own host (Artifact Registry answers every blob read that way); redirects elsewhere and foreign authentication realms are refused. HTTPS is explicit; plain HTTP requires the mesh Service assertion or a loopback development source. No registry is selected by default. +The Azure SDK reader also follows ACR's dedicated data-download redirect to +`..data.azurecr.io` over HTTPS. Only credential-free GETs +reach that endpoint; authentication and token exchanges remain on the registry +login host. The cell must admit that registry's regional data endpoint as well. + `DirectorySource` reads Codefly's existing native artifact cache, whose files are named by the SHA-256 hex digest. It never writes a cache and is refused when the host reports that it runs in a Pod. Neither adapter is an artifact store: diff --git a/module/frontendpayload/azure.go b/module/frontendpayload/azure.go new file mode 100644 index 000000000..398363d64 --- /dev/null +++ b/module/frontendpayload/azure.go @@ -0,0 +1,111 @@ +package frontendpayload + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "github.com/codefly-dev/core/solutionhost" + "oras.land/oras-go/v2/registry" +) + +// The same workload-reader stopgap as workload.go, using Azure's credential, +// token exchange and renewal implementation rather than another auth flow. +type azureWorkloadSource struct { + client *azcontainerregistry.BlobClient + repository string +} + +func newAzureWorkloadSource(repository string) (Source, error) { + ref, err := registry.ParseReference(repository) + if err != nil || ref.Reference != "" || ref.Repository == "" { + return nil, errors.New("frontend artifact repository must name one repository without a tag or digest") + } + credential, err := azidentity.NewWorkloadIdentityCredential(nil) + if err != nil { + return nil, &readerIdentityError{"Azure frontend artifact reads require the pod's bound workload identity"} + } + return azureRegistrySource(ref, credential, http.DefaultTransport) +} + +func azureRegistrySource(ref registry.Reference, credential azcore.TokenCredential, base http.RoundTripper) (Source, error) { + client, err := azcontainerregistry.NewBlobClient("https://"+ref.Registry, credential, + &azcontainerregistry.BlobClientOptions{ClientOptions: azcore.ClientOptions{ + Transport: &http.Client{Timeout: 30 * time.Second, + Transport: azureRegistryTransport{registry: ref.Registry, base: base}, + CheckRedirect: func(next *http.Request, via []*http.Request) error { + if len(via) > 3 || next.URL.Scheme != "https" || next.URL.User != nil || + (next.URL.Host != ref.Registry && !azureDataEndpoint(ref.Registry, next.URL.Host)) { + return errRedirectRefused + } + if next.URL.Host != ref.Registry { + next.Header.Del("Authorization") + } + return nil + }}, + }}) + if err != nil { + return nil, &readerIdentityError{"could not configure the Azure frontend artifact reader"} + } + return &azureWorkloadSource{client: client, repository: ref.Repository}, nil +} + +// Private-link ACR downloads redirect to this registry's regional data host. +// Neither arbitrary Azure storage hosts nor another registry are admitted. +func azureDataEndpoint(registry, host string) bool { + name, ok := strings.CutSuffix(registry, ".azurecr.io") + if !ok || strings.ContainsAny(name, ".:") { + return false + } + parts := strings.Split(host, ".") + if len(parts) != 5 || parts[0] != name || parts[2] != "data" || parts[3] != "azurecr" || parts[4] != "io" || parts[1] == "" { + return false + } + for _, ch := range parts[1] { + if !(ch >= 'a' && ch <= 'z' || ch >= '0' && ch <= '9') { + return false + } + } + return true +} + +type azureRegistryTransport struct { + registry string + base http.RoundTripper +} + +func (t azureRegistryTransport) RoundTrip(r *http.Request) (*http.Response, error) { + if r.URL.Scheme != "https" || r.URL.User != nil { + return nil, errRedirectRefused + } + if r.URL.Host != t.registry && (!azureDataEndpoint(t.registry, r.URL.Host) || r.Method != http.MethodGet || r.Header.Get("Authorization") != "") { + return nil, errRedirectRefused + } + return t.base.RoundTrip(r) +} + +func (s *azureWorkloadSource) Open(ctx context.Context, digest solutionhost.PayloadDigest) (io.ReadCloser, error) { + response, err := s.client.GetBlob(ctx, s.repository, string(digest), nil) + if err != nil { + var status *azcore.ResponseError + if errors.As(err, &status) { + return nil, fmt.Errorf("%w: the registry answered HTTP %d", ErrUnavailable, status.StatusCode) + } + return nil, unavailable(err) + } + // A redirected storage response may omit the registry-specific digest header. + // Prepare verifies the downloaded bytes against the signed digest before serving. + if (response.DockerContentDigest != nil && *response.DockerContentDigest != string(digest)) || + response.ContentLength == nil || *response.ContentLength < 0 || *response.ContentLength > MaxBundleBytes { + response.BlobData.Close() + return nil, errors.New("frontend artifact descriptor differs from the admitted bundle") + } + return response.BlobData, nil +} diff --git a/module/frontendpayload/azure_redirect_test.go b/module/frontendpayload/azure_redirect_test.go new file mode 100644 index 000000000..1648b74de --- /dev/null +++ b/module/frontendpayload/azure_redirect_test.go @@ -0,0 +1,103 @@ +package frontendpayload + +import ( + "context" + "encoding/base64" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "oras.land/oras-go/v2/registry" +) + +type azureRedirectRoundTripper func(*http.Request) (*http.Response, error) + +func (f azureRedirectRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestAzureBundleDownloadFollowsRegistryDataRedirectWithoutCredentials(t *testing.T) { + data := archiveFor(t, "azure-data-endpoint") + frontend := frontendFor(data) + digest := string(frontend.Payload.Digest) + refresh := "e30." + base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"exp":%d}`, time.Now().Add(time.Hour).Unix()))) + ".sig" + downloads := 0 + digestHeader := true + tamper := false + transport := azureRedirectRoundTripper(func(r *http.Request) (*http.Response, error) { + response := &http.Response{StatusCode: 200, Header: make(http.Header), Request: r, Body: io.NopCloser(strings.NewReader(""))} + switch { + case r.URL.Host == "example.eastus2.data.azurecr.io": + require.Equal(t, http.MethodGet, r.Method) + require.Empty(t, r.Header.Get("Authorization"), "a registry bearer never reaches the download endpoint") + downloads++ + if digestHeader { + response.Header.Set("Docker-Content-Digest", digest) + } + response.Header.Set("Content-Length", fmt.Sprint(len(data))) + body := append([]byte(nil), data...) + if tamper { + body[len(body)-1] ^= 1 + } + response.Body = io.NopCloser(strings.NewReader(string(body))) + response.ContentLength = int64(len(data)) + case r.URL.Path == "/oauth2/exchange": + response.Body = io.NopCloser(strings.NewReader(fmt.Sprintf(`{"refresh_token":%q}`, refresh))) + case r.URL.Path == "/oauth2/token": + response.Body = io.NopCloser(strings.NewReader(`{"access_token":"registry-reader-token"}`)) + case r.Header.Get("Authorization") != "Bearer registry-reader-token": + response.StatusCode = 401 + response.Header.Set("WWW-Authenticate", `Bearer realm="https://example.azurecr.io/oauth2/token",service="example.azurecr.io",scope="repository:frontend-bundles:pull"`) + default: + response.StatusCode = 307 + response.Header.Set("Location", "https://example.eastus2.data.azurecr.io/download?sig=private-download-capability") + } + return response, nil + }) + ref, err := registry.ParseReference("example.azurecr.io/frontend-bundles") + require.NoError(t, err) + source, err := azureRegistrySource(ref, &azureTestCredential{}, transport) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(t.Context(), 3*time.Second) + defer cancel() + bundle, err := Prepare(ctx, source, frontend) + require.NoError(t, err) + require.Equal(t, "azure-data-endpoint", readAsset(t, bundle, "deferred.js")) + require.Equal(t, 1, downloads) + digestHeader = false + bundle, err = Prepare(ctx, source, frontend) + require.NoError(t, err, "the blob storage redirect response need not repeat registry-specific digest headers") + require.Equal(t, "azure-data-endpoint", readAsset(t, bundle, "deferred.js")) + tamper = true + _, err = Prepare(ctx, source, frontend) + require.ErrorContains(t, err, "digest mismatch", "signed bytes remain verified without a registry digest header") + require.Equal(t, 3, downloads) +} + +func TestAzureDownloadTransportRefusesOtherDestinationsAndCredentials(t *testing.T) { + for _, target := range []struct{ url, method, bearer string }{ + {"https://other.eastus2.data.azurecr.io/blob", "GET", ""}, + {"https://example.eastus2.data.azurecr.io.evil.example/blob", "GET", ""}, + {"https://example..data.azurecr.io/blob", "GET", ""}, + {"http://example.eastus2.data.azurecr.io/blob", "GET", ""}, + {"https://example.eastus2.data.azurecr.io:8443/blob", "GET", ""}, + {"https://example.eastus2.data.azurecr.io/blob", "POST", ""}, + {"https://example.eastus2.data.azurecr.io/blob", "GET", "reader-secret"}, + } { + t.Run(target.url+target.method+target.bearer, func(t *testing.T) { + calls := 0 + transport := azureRegistryTransport{registry: "example.azurecr.io", base: azureRedirectRoundTripper(func(*http.Request) (*http.Response, error) { calls++; return nil, nil })} + req, err := http.NewRequest(target.method, target.url, nil) + require.NoError(t, err) + if target.bearer != "" { + req.Header.Set("Authorization", "Bearer "+target.bearer) + } + _, err = transport.RoundTrip(req) + require.ErrorIs(t, err, errRedirectRefused) + require.Equal(t, 0, calls) + require.NotContains(t, err.Error(), "reader-secret") + }) + } +} diff --git a/module/frontendpayload/azure_test.go b/module/frontendpayload/azure_test.go new file mode 100644 index 000000000..2c5baf1dc --- /dev/null +++ b/module/frontendpayload/azure_test.go @@ -0,0 +1,84 @@ +package frontendpayload + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/stretchr/testify/require" + "oras.land/oras-go/v2/registry" +) + +type azureTestCredential struct{ calls int } + +func (c *azureTestCredential) GetToken(_ context.Context, options policy.TokenRequestOptions) (azcore.AccessToken, error) { + c.calls++ + return azcore.AccessToken{Token: "test-workload-token", ExpiresOn: time.Now().Add(time.Hour)}, nil +} + +func TestAzureWorkloadSourceExchangesIdentityAndReadsAdmittedBundle(t *testing.T) { + data := archiveFor(t, "azure-bundle") + frontend := frontendFor(data) + digest := string(frontend.Payload.Digest) + refresh := "e30." + base64.RawStdEncoding.EncodeToString([]byte(fmt.Sprintf(`{"exp":%d}`, time.Now().Add(time.Hour).Unix()))) + ".sig" + mode := "ok" + var server *httptest.Server + server = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/oauth2/exchange": + require.NoError(t, r.ParseForm()) + require.Equal(t, "test-workload-token", r.Form.Get("access_token")) + require.Equal(t, "access_token", r.Form.Get("grant_type")) + fmt.Fprintf(w, `{"refresh_token":%q}`, refresh) + case "/oauth2/token": + require.NoError(t, r.ParseForm()) + require.Equal(t, refresh, r.Form.Get("refresh_token")) + require.Equal(t, "repository:frontend-bundles:pull", r.Form.Get("scope")) + fmt.Fprint(w, `{"access_token":"test-registry-token"}`) + default: + require.Equal(t, "/v2/frontend-bundles/blobs/"+digest, r.URL.Path) + if r.Header.Get("Authorization") != "Bearer test-registry-token" { + w.Header().Set("WWW-Authenticate", `Bearer realm="`+server.URL+`/oauth2/token",service="`+strings.TrimPrefix(server.URL, "https://")+`",scope="repository:frontend-bundles:pull"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + if mode == "forbidden" { + http.Error(w, "private-response-must-not-leak", http.StatusForbidden) + return + } + if mode == "wrong-digest" { + w.Header().Set("Docker-Content-Digest", "sha256:"+strings.Repeat("0", 64)) + } else { + w.Header().Set("Docker-Content-Digest", digest) + } + w.Header().Set("Content-Length", fmt.Sprint(len(data))) + _, _ = w.Write(data) + } + })) + defer server.Close() + ref, err := registry.ParseReference(strings.TrimPrefix(server.URL, "https://") + "/frontend-bundles") + require.NoError(t, err) + credential := &azureTestCredential{} + source, err := azureRegistrySource(ref, credential, server.Client().Transport) + require.NoError(t, err) + bundle, err := Prepare(t.Context(), source, frontend) + require.NoError(t, err) + require.Equal(t, "azure-bundle", readAsset(t, bundle, "deferred.js")) + require.Equal(t, 1, credential.calls) + mode = "wrong-digest" + _, err = source.Open(t.Context(), frontend.Payload.Digest) + require.ErrorContains(t, err, "descriptor differs") + mode = "forbidden" + _, err = source.Open(t.Context(), frontend.Payload.Digest) + require.ErrorIs(t, err, ErrUnavailable) + require.ErrorContains(t, err, "HTTP 403") + require.NotContains(t, err.Error(), "private-response") + require.Equal(t, 1, credential.calls, "the SDK reuses its workload token exchange") +} diff --git a/module/frontendpayload/go.mod b/module/frontendpayload/go.mod index c8088de2f..9b584d05a 100644 --- a/module/frontendpayload/go.mod +++ b/module/frontendpayload/go.mod @@ -3,6 +3,9 @@ module github.com/codefly-dev/module-saas-starter/module/frontendpayload go 1.27.3 require ( + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 + github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 github.com/codefly-dev/core v0.19.5-0.20261014005330-835460f16fb5 github.com/stretchr/testify v1.12.1 oras.land/oras-go/v2 v2.6.2 @@ -13,6 +16,8 @@ require ( buf.build/go/protovalidate v1.4.0 // indirect cel.dev/cel-go v0.32.0 // indirect cel.dev/expr v0.25.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/antlr4-go/antlr/v4 v4.13.1 // indirect @@ -33,19 +38,24 @@ require ( github.com/go-openapi/swag/yamlutils v0.29.2 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect github.com/gofrs/flock v0.13.1 // indirect + github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/go-github/v89 v89.0.0 // indirect github.com/google/go-querystring v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 // indirect github.com/yoheimuta/go-protoparser/v4 v4.14.2 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.57.0 // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/net v0.60.0 // indirect golang.org/x/sync v0.23.0 // indirect diff --git a/module/frontendpayload/go.sum b/module/frontendpayload/go.sum index 24dc91555..48bbdb089 100644 --- a/module/frontendpayload/go.sum +++ b/module/frontendpayload/go.sum @@ -6,6 +6,20 @@ cel.dev/cel-go v0.32.0 h1:irvpFKr5EuGPyxeME03ERh0rii1TX+BDAnB9eL3IvNk= cel.dev/cel-go v0.32.0/go.mod h1:DnVip7tpJSsgZymwfT+m1tnEVy3ivAjSMXPx12YrMkU= cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= @@ -36,6 +50,7 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -110,6 +125,8 @@ github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPE github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofrs/flock v0.13.1 h1:jjREztyBeSKBZYAC+mgc1laB+xsgy4kYMf3FbKF2UBo= github.com/gofrs/flock v0.13.1/go.mod h1:sf4BFiHwnvgxa25DlQoDqXQnwRMEOwqxRq37P6MzzmE= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= @@ -128,6 +145,14 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -151,12 +176,16 @@ github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3I github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/rodaine/protogofakeit v0.1.1 h1:ZKouljuRM3A+TArppfBqnH8tGZHOwM/pjvtXe9DaXH8= github.com/rodaine/protogofakeit v0.1.1/go.mod h1:pXn/AstBYMaSfc1/RqH3N82pBuxtWgejz1AlYpY1mI0= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3 h1:1EYB5IzjZawrrnELUi78f9fPu57HuXjmddZPjrls/28= github.com/santhosh-tekuri/jsonschema/v6 v6.0.3/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= @@ -193,12 +222,15 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= +golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= +golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= @@ -213,8 +245,9 @@ google.golang.org/grpc v1.84.0 h1:soMyaPJ8pAak5PIQ0DGBUir0XRo2fRoMqhNWMLlLxO0= google.golang.org/grpc v1.84.0/go.mod h1:ljCht0DrxQrXBDRTZp52Qxh3Ffk8CdYm2sj4O2QN2C0= google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/module/frontendpayload/workload.go b/module/frontendpayload/workload.go index bcb35f1ee..7d0a1da6a 100644 --- a/module/frontendpayload/workload.go +++ b/module/frontendpayload/workload.go @@ -12,8 +12,8 @@ package frontendpayload // which carries the same label for the same reason: Codefly has no // "this cell's workload identity" dependency kind yet, so there is nothing // to resolve a reader through. When one exists this moves below that seam. -// Another cloud's registry is refused by name here rather than read -// anonymously. +// Azure Container Registry uses the pod's federated workload identity through +// the Azure SDK (azure.go). Registries without a workload reader are refused. import ( "context" @@ -32,12 +32,15 @@ const workloadMetadataBase = "http://metadata.google.internal/computeMetadata/v1 // NewWorkloadRegistrySource reads a published bundle repository over HTTPS as // the pod's own workload identity. -// Any other registry is refused when the host starts, by name: failing closed +// Unsupported registries are refused when the host starts, by name: failing closed // at boot is visible, where an anonymous read would leave every page pending. -func NewWorkloadRegistrySource(repository string) (*RegistrySource, error) { +func NewWorkloadRegistrySource(repository string) (Source, error) { host, _, _ := strings.Cut(repository, "/") + if strings.HasSuffix(host, ".azurecr.io") { + return newAzureWorkloadSource(repository) + } if !strings.HasSuffix(host, ".pkg.dev") { - return nil, fmt.Errorf("frontend artifact repository %q is not on a registry this host reads as its workload identity (Artifact Registry)", repository) + return nil, fmt.Errorf("frontend artifact repository %q is not on a registry this host reads as its workload identity (Artifact Registry or Azure Container Registry)", repository) } return workloadRegistrySource(repository, &workloadToken{metadataBase: workloadMetadataBase, client: &http.Client{Timeout: 10 * time.Second}, now: time.Now}, nil) } diff --git a/module/module.package.codefly.yaml b/module/module.package.codefly.yaml index 8f9a3cb0a..f705c3be2 100644 --- a/module/module.package.codefly.yaml +++ b/module/module.package.codefly.yaml @@ -18,12 +18,12 @@ services: kind: protobuf package: saas.accounts.v1 path: contracts/api/accounts/authority/contract.binpb - digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + digest: sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5 - endpoint: connect kind: protobuf package: "" path: contracts/api/accounts/connect/contract.binpb - digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + digest: sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5 - name: auth-gateway endpoints: [grpc, rest] api-contracts: diff --git a/module/services/accounts/AUTHZ_MATRIX.md b/module/services/accounts/AUTHZ_MATRIX.md index 2e90fa0d0..427e6addc 100644 --- a/module/services/accounts/AUTHZ_MATRIX.md +++ b/module/services/accounts/AUTHZ_MATRIX.md @@ -2,7 +2,7 @@ > Generated from protobuf service descriptors and `saas.policy.v1.method_policy`; only the prose description is joined from `pkg/business/introspection.go`. Do not edit by hand. Run `go generate ./pkg/business` from `module/services/accounts/code`. -Inventory: **296 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. +Inventory: **297 RPCs** across **41 services**. Missing, invalid, or unclassified procedures are denied by both Connect and gRPC interceptors. The compact tier is retained for compatibility. Guards, resource bindings, audit events, limiter class, and data sensitivity are descriptor-authoritative. Domain handlers may enforce stronger state-dependent rules but may not weaken this floor. @@ -27,6 +27,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit | `/host.installation.v1.InstallationService/GrantRole` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.role_granted | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Grant a person or a team one of an installed solution's roles. | | `/host.installation.v1.InstallationService/Install` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.installation_requested, saas.authority.installation_activated | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Install one solution at one organisation under the consent its administrator reviewed; pending until its install step converges. | | `/host.installation.v1.InstallationService/List` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List what one organisation has installed. | +| `/host.installation.v1.InstallationService/ListHeld` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List only the signed-in member's current installation roles, including team grants. | | `/host.installation.v1.InstallationService/ListRoleGrants` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | List who holds which role on one installation. | | `/host.installation.v1.InstallationService/Review` | unary | `—` | `org_admin` | exposure=AUTHENTICATED; tenant=ORG_ADMIN; impersonation=FORBIDDEN | — | — | — | OPTIONAL / STANDARD_READ | CONFIDENTIAL → CONFIDENTIAL | Read the exact authority offer an administrator consents to, without recording consent. | | `/host.installation.v1.InstallationService/RevokeRole` | unary | `—` | `auth` | exposure=AUTHENTICATED; tenant=NONE; impersonation=FORBIDDEN | — | — | SUCCESS: saas.authority.role_revoked | FORBIDDEN / STANDARD_WRITE | CONFIDENTIAL → CONFIDENTIAL | Revoke one role grant; its subject's capabilities through the installation end. | @@ -307,7 +308,7 @@ The compact tier is retained for compatibility. Guards, resource bindings, audit ## Tier totals -- `auth`: 50 +- `auth`: 51 - `internal`: 55 - `mfa`: 5 - `org_admin`: 68 diff --git a/module/services/accounts/code/go.mod b/module/services/accounts/code/go.mod index d320900bf..e6880e1e6 100644 --- a/module/services/accounts/code/go.mod +++ b/module/services/accounts/code/go.mod @@ -7,11 +7,11 @@ require ( buf.build/go/protovalidate v1.4.0 connectrpc.com/connect v1.21.0 connectrpc.com/otelconnect v0.9.0 - github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1 - github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1 + github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261028150114-269005645a32 + github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261028150114-269005645a32 + github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261028150114-269005645a32 + github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261028150114-269005645a32 + github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261028150114-269005645a32 github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 github.com/codefly-dev/interface-cache/go/cache v0.4.1 @@ -21,6 +21,7 @@ require ( github.com/codefly-dev/service-object-storage v0.0.19 github.com/codefly-dev/service-postgres/libs/go v0.0.0-20260929200903-bae6c31c7211 github.com/codefly-dev/service-redis/cache v0.3.0 + github.com/codefly-dev/service-vault/keyservice v0.0.0-20261023210413-a6c7256bfae7 github.com/go-webauthn/webauthn v0.18.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/google/uuid v1.6.0 @@ -54,7 +55,10 @@ require ( cel.dev/expr v0.25.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect @@ -132,3 +136,9 @@ require ( golang.org/x/text v0.42.0 // indirect oras.land/oras-go/v2 v2.6.2 // indirect ) + +// DEVELOPMENT CANDIDATE: service-vault PR #86; replace with an owner release before promotion. +replace github.com/codefly-dev/service-vault/keyservice => github.com/crisfiorini-oss/service-vault/keyservice v0.0.0-20261023210413-a6c7256bfae7 + +// DEVELOPMENT CANDIDATE: frontend registry reader in PR #1234; adopt an owner release before promotion. +replace github.com/codefly-dev/module-saas-starter/module/frontendpayload => github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352 diff --git a/module/services/accounts/code/go.sum b/module/services/accounts/code/go.sum index 9f99cf9b9..a20239f49 100644 --- a/module/services/accounts/code/go.sum +++ b/module/services/accounts/code/go.sum @@ -18,8 +18,14 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5C github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0 h1:MaKvxE6D0KkjOg6Wd9M00iqP5PR0kUxCfiezes4JweM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.5.0/go.mod h1:i2h9fsTFKZorh8RdV2IcSUf/Qj98GlTkrTvUbX/s8as= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.9.0 h1:MDT4FxAPve5FnYn6vOL1r7RCRDG+l9cI7a5LlCuHsqA= @@ -56,18 +62,18 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= -github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1 h1:y8J/hDtidFnBvfdRoApLx5qsbWxtpxXjAO9OyhuXXT4= -github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= -github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1 h1:nszx0AoJd9w08MwUWr380g1fVEJ4V8KiDQQ+hTfGYes= -github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:kTjeuNl03XSCkH3UhGKuHAKamkIzNsC2RPFu/g0fqSk= -github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1 h1:3KYJZ1TSC7nFyPls9PM6zXgt6QcnsQcPjRm2s+QwRSg= -github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:or3dd9bqacXWMjke/n81l/QxRaLwq2lq/hL4V2qtAZo= -github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1 h1:5Lm7ZEss7Y/pJS6owYdYNNqfVR+cNv31dc2HWtW3x+I= -github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261028150114-269005645a32 h1:ndQ+PvrDgIdji2Z30u9tQh9StU/c6ZOkKMxCyToEKf4= +github.com/codefly-dev/authority/libraries/authority-client/go v0.0.0-20261028150114-269005645a32/go.mod h1:4hRjiWuJOKzrEk8AS87pbWCbpiEmhEvQBGQaJo5Hmkg= +github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261028150114-269005645a32 h1:sPE6yEqf9oeH5oGeMQuwaF+CILGXUds+orxMqo9Rk/I= +github.com/codefly-dev/authority/libraries/authority-core/go v0.0.0-20261028150114-269005645a32/go.mod h1:kTjeuNl03XSCkH3UhGKuHAKamkIzNsC2RPFu/g0fqSk= +github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261028150114-269005645a32 h1:H54hlos+kQHF0Afc7A+s0YpJJLNo/EVEHxP56s/SGZQ= +github.com/codefly-dev/authority/libraries/conformance/go v0.0.0-20261028150114-269005645a32/go.mod h1:or3dd9bqacXWMjke/n81l/QxRaLwq2lq/hL4V2qtAZo= +github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261028150114-269005645a32 h1:NrZGR1ZpSP7ueBNxOWh3otEBYRYl7AXtgm8on8TGWY8= +github.com/codefly-dev/authority/libraries/delivery/go v0.0.0-20261028150114-269005645a32/go.mod h1:XsOFnO2MQdr37vM3ULa0Uwi8ZWptyVx7hRSUyIdaZ1Q= github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027063450-22b86ccbf2a5 h1:pHywzlszDDjNJIOg52YhmDCpW+7N4c/Ftma/qx5HxxE= github.com/codefly-dev/authority/libraries/hop/go v0.0.0-20261027063450-22b86ccbf2a5/go.mod h1:9WF8dd98HYX9g/49uhrTgaTbWXo3hBvYzS3UVWYiZHw= -github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1 h1:xjWJja3adt61lpYvwtz3VYekrxU4r1sHvOmW9YQ0K0Q= -github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261027132425-c3568f113db1/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= +github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261028150114-269005645a32 h1:b9MmgqQE66C049nHg49hIcrz7IAsFcN9GfOjEEp1QVA= +github.com/codefly-dev/authority/libraries/resources/go v0.0.0-20261028150114-269005645a32/go.mod h1:1rmjrVyr0h7zOuwOHS9i+BoeS9dr3XNvtupLZFwY6As= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc h1:olDnVJ1A1Retn0z9A59YMwDRmDLgkV2y1lbl+HzNrj8= github.com/codefly-dev/cli/contracts/deployment v0.0.0-20261006151622-5824cf5342fc/go.mod h1:VWPYY3FH4te7SzdN2xl9odNpWidJ1Grl/Hjs1SNoEF8= github.com/codefly-dev/core v0.19.7-0.20261026092055-b23dc13543b2 h1:I1wL0ANXhTdQuEAnxXF+s4+rKILlBu/QHILq4Skyi3I= @@ -76,8 +82,6 @@ github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1 github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= github.com/codefly-dev/interface-cache/go/cache v0.4.1 h1:S7a20cVs3ilW8/HSpzmsyfJx/GJHgJ1xW9Dm9MYXYPI= github.com/codefly-dev/interface-cache/go/cache v0.4.1/go.mod h1:ki57cBwXnpFLx1uO29KiDY06lzyG/gXXBkqgi5ZNKWQ= -github.com/codefly-dev/module-saas-starter/module/frontendpayload v0.0.0-20261021192505-6fd56dae2e04 h1:I3x8ihrzBMpv9UrLYc/3TmKl+lBvA5+xGcigENG/IR8= -github.com/codefly-dev/module-saas-starter/module/frontendpayload v0.0.0-20261021192505-6fd56dae2e04/go.mod h1:LZl0I41APmLxG3Awdd4gn/pIhMOOgJqLuMuNGyXUJMg= github.com/codefly-dev/sdk-go v0.4.1 h1:WUKYWxXfojTr88SkLtq0T8aqpV0o5pR/muUUcsUUlzk= github.com/codefly-dev/sdk-go v0.4.1/go.mod h1:zr9ts3kkWNv2XEFUXZB+ydfW6x5b6/NbUvBm4KVYNqs= github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 h1:wBOL7QsuoC0HsS43gsH2+tpsipk4os077C2VUDkMUHg= @@ -92,6 +96,14 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261027005009-3e9f20a706ff h1:dtG4SGf3deNecwpfQzpFlZOY9fwY86E/xOCVCgqs3BA= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261027005009-3e9f20a706ff/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028173843-c2485a8f50cf h1:5HLTb1dvcbrZkmVF8byhqqXvtrUsxoYKXHhS77fGC0c= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028173843-c2485a8f50cf/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352 h1:TzMok5bj4nTOK1Vb/U/HVVM0MIgYagu3WuuGbG/xuAI= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= +github.com/crisfiorini-oss/service-vault/keyservice v0.0.0-20261023210413-a6c7256bfae7 h1:QhfPRUrZ8tp6XFg2/jXvx1fwtNqVHWYOkKeXkb7bZ14= +github.com/crisfiorini-oss/service-vault/keyservice v0.0.0-20261023210413-a6c7256bfae7/go.mod h1:15o4NPE329wd93fpl/zFPlj+Dx7bGoQqrDT5+nO7Yso= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/module/services/accounts/code/pkg/adapters/auth.go b/module/services/accounts/code/pkg/adapters/auth.go index 1a998dd09..6bfe652f8 100644 --- a/module/services/accounts/code/pkg/adapters/auth.go +++ b/module/services/accounts/code/pkg/adapters/auth.go @@ -230,7 +230,7 @@ func requireOrgAdmin(ctx context.Context, actorID, orgID string) error { if role == gen.OrgRole_ORG_ROLE_ADMIN.String() || role == gen.OrgRole_ORG_ROLE_OWNER.String() { return nil } - return status.Error(codes.PermissionDenied, "requires org admin or owner role") + return status.Error(codes.PermissionDenied, "requires an organization administrator (admin or owner role)") } // subjectOutsideOrg is the one answer every way a subject can fail to belong diff --git a/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go b/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go index 1c672b012..ef069be17 100644 --- a/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go +++ b/module/services/accounts/code/pkg/adapters/connect_handlers_host_interface.go @@ -96,6 +96,10 @@ func (h *hostInstallationConnectHandler) List(ctx context.Context, req *connect. return unary(ctx, req, h.server().List) } +func (h *hostInstallationConnectHandler) ListHeld(ctx context.Context, req *connect.Request[installationv1.ListHeldRequest]) (*connect.Response[installationv1.ListHeldResponse], error) { + return unary(ctx, req, h.server().ListHeld) +} + type hostSessionConnectHandler struct{ svc *business.Service } func (h *hostSessionConnectHandler) SignIn(ctx context.Context, req *connect.Request[sessionv1.SignInRequest]) (*connect.Response[sessionv1.Session], error) { diff --git a/module/services/accounts/code/pkg/adapters/host_interface.go b/module/services/accounts/code/pkg/adapters/host_interface.go index db83bf604..dc2d29159 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface.go +++ b/module/services/accounts/code/pkg/adapters/host_interface.go @@ -557,6 +557,28 @@ func (s *HostInstallationServer) List(ctx context.Context, req *installationv1.L return out, nil } +// ListHeld reads the signed-in member's current installation roles through +// the shared Authority core, including team grants and their live revocation. +func (s *HostInstallationServer) ListHeld(ctx context.Context, req *installationv1.ListHeldRequest) (*installationv1.ListHeldResponse, error) { + core, err := authorityCore() + if err != nil { + return nil, err + } + caller, err := signedInPerson(ctx, "list held installations") + if err != nil { + return nil, err + } + held, err := core.HeldInstallations(ctx, caller, req.GetOrganizationId()) + if err != nil { + return nil, coreStatus(err) + } + out := &installationv1.ListHeldResponse{} + for _, h := range held { + out.Installations = append(out.Installations, &installationv1.HeldInstallation{Installation: installationToWire(core, ctx, h.Installation), Roles: h.Roles}) + } + return out, nil +} + // installationToWire is an installation as the interface states it, with the // roles its solution declares. func installationToWire(core *authority.Service, ctx context.Context, i domain.Installation) *installationv1.Installation { @@ -747,9 +769,14 @@ func (s *HostSessionServer) ListFixturePeople(ctx context.Context, _ *sessionv1. } out := &sessionv1.ListFixturePeopleResponse{} for _, p := range people { + _, organization, err := s.service.SessionDisplayNames(ctx, p.PersonID, p.OrganizationID) + if err != nil { + return nil, status.Errorf(codes.Unavailable, "cannot read fixture display names: %v", err) + } out.People = append(out.People, &sessionv1.FixturePerson{ PersonId: p.PersonID, Email: p.Email, OrganizationId: p.OrganizationID, Roles: p.Roles, TeamIds: p.TeamIDs, FixtureToken: p.FixtureToken, + OrganizationDisplayName: organization, }) } return out, nil diff --git a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go index 0a928a1fe..77e22db79 100644 --- a/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go +++ b/module/services/accounts/code/pkg/adapters/host_interface_conformance_test.go @@ -68,6 +68,7 @@ const ( fixtureOrganization = "00000000-0000-7000-8000-0000000000c1" globexOrganization = "00000000-0000-7000-8000-0000000000c2" conformanceAlex = "00000000-0000-7000-8000-0000000000b3" + conformanceAdmin = "00000000-0000-7000-8000-0000000000a2" conformanceIvy = "00000000-0000-7000-8000-0000000000b9" conformanceZoe = "00000000-0000-7000-8000-0000000000b5" ) @@ -394,6 +395,15 @@ func runHostInterfaceFixture(t *testing.T) { // waits until the harness resumes it, and uninstalling revokes it. Lifecycle: &conformance.InstallationLifecycleCase{ Target: "conformance-lifecycle", Credential: credential("conformance-lifecycle"), Kind: "conformance-lifecycle.page", Team: finance, + SetAdministrator: func(ctx context.Context, administrator bool) error { + role := gen.OrgRole_ORG_ROLE_MEMBER + if administrator { + role = gen.OrgRole_ORG_ROLE_ADMIN + } + return svc.ConvergeFixtureOrgMember(ctx, &gen.AddOrgMemberRequest{ + OrgId: fixtureOrganization, UserId: conformanceAdmin, Role: role, + }) + }, PauseInstall: func() func() { release := make(chan struct{}) svc.SetAuthorityInstallStep(func(ctx context.Context, _ domain.Installation) error { diff --git a/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go b/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go index d113b16fe..99aea596b 100644 --- a/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go +++ b/module/services/accounts/code/pkg/adapters/impersonation_method_restriction_test.go @@ -99,7 +99,8 @@ func TestRestrictedProceduresAreDeniedOnlyWhileImpersonating(t *testing.T) { withoutCentralEnforcement(t) restricted := restrictedProcedures(t) - require.Len(t, restricted, 68, "the declared restriction set changed; confirm it against AUTHZ_MATRIX.md") // +11: Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check + require.Contains(t, restricted, "/host.installation.v1.InstallationService/ListHeld") + require.Len(t, restricted, 69, "the declared restriction set changed; confirm it against AUTHZ_MATRIX.md") // +12: Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, ListHeld, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check for _, procedure := range restricted { t.Run(procedure, func(t *testing.T) { diff --git a/module/services/accounts/code/pkg/business/interface_policies.go b/module/services/accounts/code/pkg/business/interface_policies.go index 377f715ed..fec1704b9 100644 --- a/module/services/accounts/code/pkg/business/interface_policies.go +++ b/module/services/accounts/code/pkg/business/interface_policies.go @@ -229,7 +229,9 @@ func InterfaceMethodPolicies() map[string]*policyv1.MethodPolicy { "/host.installation.v1.InstallationService/GrantRole": roleChange(EventAuthorityRoleGranted), "/host.installation.v1.InstallationService/RevokeRole": roleChange(EventAuthorityRoleRevoked), "/host.installation.v1.InstallationService/ListRoleGrants": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), - "/host.installation.v1.InstallationService/List": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_ORG_ADMIN, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // an administrator's read; a member's view is the entitlement listing + "/host.installation.v1.InstallationService/List": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_ORG_ADMIN, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // an administrator's read; a member's own holdings are ListHeld + // The core restricts this read to the signed-in member's own holdings. + "/host.installation.v1.InstallationService/ListHeld": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), // Current is the caller's own session: authenticated by the bearer it // describes, organisation-less like the session itself may be. "/host.session.v1.SessionService/Current": read(policyv1.Exposure_EXPOSURE_AUTHENTICATED, policyv1.TenantRequirement_TENANT_REQUIREMENT_NONE, policyv1.Sensitivity_SENSITIVITY_CONFIDENTIAL), @@ -314,6 +316,7 @@ var interfaceDescriptions = map[string]string{ "InstallationService/GrantRole": "Grant a person or a team one of an installed solution's roles.", "InstallationService/RevokeRole": "Revoke one role grant; its subject's capabilities through the installation end.", "InstallationService/ListRoleGrants": "List who holds which role on one installation.", + "InstallationService/ListHeld": "List only the signed-in member's current installation roles, including team grants.", "InstallationService/List": "List what one organisation has installed.", "AuthorityService/ListResources": "Answer which resources of one kind a checked capability covers for one action, from its owner's current grants.", "AuthorityService/Delegate": "Record a signed-in person's grant to a module, within what they hold.", diff --git a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go index 40ee4bcc4..b3b80169a 100644 --- a/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go +++ b/module/services/accounts/code/pkg/cataloggen/authz_methods_test.go @@ -184,5 +184,6 @@ func TestImpersonationRestrictionsMatchTheDescriptorsTheInterceptorReads(t *test } require.Equal(t, catalogued, enforced) - require.Len(t, enforced, 68) // 57 of the API package + Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check + require.Contains(t, enforced, "/host.installation.v1.InstallationService/ListHeld") + require.Len(t, enforced, 69) // 57 of the API package + Issue, Delegate, RevokeDelegation, GetDelegation, Install, Review, GrantRole, RevokeRole, ListRoleGrants, List, ListHeld, Current of the implemented interfaces; Check and ListResources are public, authenticated by the token they check } diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go index 9bc9cbdd4..d7a3b8b8d 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installation.pb.go @@ -1633,6 +1633,155 @@ func (x *OfferTool) GetWrite() bool { return false } +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +type ListHeldRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The organisation. + OrganizationId string `protobuf:"bytes,1,opt,name=organization_id,json=organizationId,proto3" json:"organization_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHeldRequest) Reset() { + *x = ListHeldRequest{} + mi := &file_host_installation_v1_installation_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHeldRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHeldRequest) ProtoMessage() {} + +func (x *ListHeldRequest) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListHeldRequest.ProtoReflect.Descriptor instead. +func (*ListHeldRequest) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{24} +} + +func (x *ListHeldRequest) GetOrganizationId() string { + if x != nil { + return x.OrganizationId + } + return "" +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +type ListHeldResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The installations. + Installations []*HeldInstallation `protobuf:"bytes,1,rep,name=installations,proto3" json:"installations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListHeldResponse) Reset() { + *x = ListHeldResponse{} + mi := &file_host_installation_v1_installation_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListHeldResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListHeldResponse) ProtoMessage() {} + +func (x *ListHeldResponse) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListHeldResponse.ProtoReflect.Descriptor instead. +func (*ListHeldResponse) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{25} +} + +func (x *ListHeldResponse) GetInstallations() []*HeldInstallation { + if x != nil { + return x.Installations + } + return nil +} + +// HeldInstallation is one installation the person may use. +type HeldInstallation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The installation. + Installation *Installation `protobuf:"bytes,1,opt,name=installation,proto3" json:"installation,omitempty"` + // The roles the person holds on it, directly or through a team, sorted. + Roles []string `protobuf:"bytes,2,rep,name=roles,proto3" json:"roles,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeldInstallation) Reset() { + *x = HeldInstallation{} + mi := &file_host_installation_v1_installation_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeldInstallation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeldInstallation) ProtoMessage() {} + +func (x *HeldInstallation) ProtoReflect() protoreflect.Message { + mi := &file_host_installation_v1_installation_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeldInstallation.ProtoReflect.Descriptor instead. +func (*HeldInstallation) Descriptor() ([]byte, []int) { + return file_host_installation_v1_installation_proto_rawDescGZIP(), []int{26} +} + +func (x *HeldInstallation) GetInstallation() *Installation { + if x != nil { + return x.Installation + } + return nil +} + +func (x *HeldInstallation) GetRoles() []string { + if x != nil { + return x.Roles + } + return nil +} + var File_host_installation_v1_installation_proto protoreflect.FileDescriptor const file_host_installation_v1_installation_proto_rawDesc = "" + @@ -1746,7 +1895,14 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\tOfferTool\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x18\n" + "\abinding\x18\x02 \x01(\tR\abinding\x12\x14\n" + - "\x05write\x18\x03 \x01(\bR\x05write2\x98\x05\n" + + "\x05write\x18\x03 \x01(\bR\x05write\":\n" + + "\x0fListHeldRequest\x12'\n" + + "\x0forganization_id\x18\x01 \x01(\tR\x0eorganizationId\"`\n" + + "\x10ListHeldResponse\x12L\n" + + "\rinstallations\x18\x01 \x03(\v2&.host.installation.v1.HeldInstallationR\rinstallations\"p\n" + + "\x10HeldInstallation\x12F\n" + + "\finstallation\x18\x01 \x01(\v2\".host.installation.v1.InstallationR\finstallation\x12\x14\n" + + "\x05roles\x18\x02 \x03(\tR\x05roles2\xf3\x05\n" + "\x13InstallationService\x12f\n" + "\aCurrent\x120.host.installation.v1.CurrentInstallationRequest\x1a).host.installation.v1.CurrentInstallation\x12S\n" + "\aInstall\x12$.host.installation.v1.InstallRequest\x1a\".host.installation.v1.Installation\x12T\n" + @@ -1755,7 +1911,8 @@ const file_host_installation_v1_installation_proto_rawDesc = "" + "\n" + "RevokeRole\x12'.host.installation.v1.RevokeRoleRequest\x1a\".host.installation.v1.Installation\x12k\n" + "\x0eListRoleGrants\x12+.host.installation.v1.ListRoleGrantsRequest\x1a,.host.installation.v1.ListRoleGrantsResponse\x12M\n" + - "\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse2p\n" + + "\x04List\x12!.host.installation.v1.ListRequest\x1a\".host.installation.v1.ListResponse\x12Y\n" + + "\bListHeld\x12%.host.installation.v1.ListHeldRequest\x1a&.host.installation.v1.ListHeldResponse2p\n" + "\x12InstallableService\x12Z\n" + "\aInstall\x12(.host.installation.v1.InstallationNotice\x1a%.host.installation.v1.InstallationAck2\xc8\x01\n" + "\fOfferService\x12_\n" + @@ -1777,7 +1934,7 @@ func file_host_installation_v1_installation_proto_rawDescGZIP() []byte { } var file_host_installation_v1_installation_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 24) +var file_host_installation_v1_installation_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_host_installation_v1_installation_proto_goTypes = []any{ (Installation_Status)(0), // 0: host.installation.v1.Installation.Status (*CurrentInstallationRequest)(nil), // 1: host.installation.v1.CurrentInstallationRequest @@ -1804,6 +1961,9 @@ var file_host_installation_v1_installation_proto_goTypes = []any{ (*OfferScope)(nil), // 22: host.installation.v1.OfferScope (*OfferCapability)(nil), // 23: host.installation.v1.OfferCapability (*OfferTool)(nil), // 24: host.installation.v1.OfferTool + (*ListHeldRequest)(nil), // 25: host.installation.v1.ListHeldRequest + (*ListHeldResponse)(nil), // 26: host.installation.v1.ListHeldResponse + (*HeldInstallation)(nil), // 27: host.installation.v1.HeldInstallation } var file_host_installation_v1_installation_proto_depIdxs = []int32{ 5, // 0: host.installation.v1.InstallRequest.approval:type_name -> host.installation.v1.ConsentApproval @@ -1821,31 +1981,35 @@ var file_host_installation_v1_installation_proto_depIdxs = []int32{ 23, // 12: host.installation.v1.OfferRole.capabilities:type_name -> host.installation.v1.OfferCapability 22, // 13: host.installation.v1.OfferCapability.requires:type_name -> host.installation.v1.OfferScope 24, // 14: host.installation.v1.OfferCapability.tools:type_name -> host.installation.v1.OfferTool - 1, // 15: host.installation.v1.InstallationService.Current:input_type -> host.installation.v1.CurrentInstallationRequest - 3, // 16: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest - 4, // 17: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest - 6, // 18: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest - 7, // 19: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest - 8, // 20: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest - 11, // 21: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest - 14, // 22: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice - 16, // 23: host.installation.v1.OfferService.ListOffers:input_type -> host.installation.v1.ListOffersRequest - 4, // 24: host.installation.v1.OfferService.ReviewOffer:input_type -> host.installation.v1.ReviewRequest - 2, // 25: host.installation.v1.InstallationService.Current:output_type -> host.installation.v1.CurrentInstallation - 13, // 26: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation - 5, // 27: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval - 13, // 28: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation - 13, // 29: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation - 9, // 30: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse - 12, // 31: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse - 15, // 32: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck - 17, // 33: host.installation.v1.OfferService.ListOffers:output_type -> host.installation.v1.ListOffersResponse - 19, // 34: host.installation.v1.OfferService.ReviewOffer:output_type -> host.installation.v1.ReviewedOffer - 25, // [25:35] is the sub-list for method output_type - 15, // [15:25] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 27, // 15: host.installation.v1.ListHeldResponse.installations:type_name -> host.installation.v1.HeldInstallation + 13, // 16: host.installation.v1.HeldInstallation.installation:type_name -> host.installation.v1.Installation + 1, // 17: host.installation.v1.InstallationService.Current:input_type -> host.installation.v1.CurrentInstallationRequest + 3, // 18: host.installation.v1.InstallationService.Install:input_type -> host.installation.v1.InstallRequest + 4, // 19: host.installation.v1.InstallationService.Review:input_type -> host.installation.v1.ReviewRequest + 6, // 20: host.installation.v1.InstallationService.GrantRole:input_type -> host.installation.v1.GrantRoleRequest + 7, // 21: host.installation.v1.InstallationService.RevokeRole:input_type -> host.installation.v1.RevokeRoleRequest + 8, // 22: host.installation.v1.InstallationService.ListRoleGrants:input_type -> host.installation.v1.ListRoleGrantsRequest + 11, // 23: host.installation.v1.InstallationService.List:input_type -> host.installation.v1.ListRequest + 25, // 24: host.installation.v1.InstallationService.ListHeld:input_type -> host.installation.v1.ListHeldRequest + 14, // 25: host.installation.v1.InstallableService.Install:input_type -> host.installation.v1.InstallationNotice + 16, // 26: host.installation.v1.OfferService.ListOffers:input_type -> host.installation.v1.ListOffersRequest + 4, // 27: host.installation.v1.OfferService.ReviewOffer:input_type -> host.installation.v1.ReviewRequest + 2, // 28: host.installation.v1.InstallationService.Current:output_type -> host.installation.v1.CurrentInstallation + 13, // 29: host.installation.v1.InstallationService.Install:output_type -> host.installation.v1.Installation + 5, // 30: host.installation.v1.InstallationService.Review:output_type -> host.installation.v1.ConsentApproval + 13, // 31: host.installation.v1.InstallationService.GrantRole:output_type -> host.installation.v1.Installation + 13, // 32: host.installation.v1.InstallationService.RevokeRole:output_type -> host.installation.v1.Installation + 9, // 33: host.installation.v1.InstallationService.ListRoleGrants:output_type -> host.installation.v1.ListRoleGrantsResponse + 12, // 34: host.installation.v1.InstallationService.List:output_type -> host.installation.v1.ListResponse + 26, // 35: host.installation.v1.InstallationService.ListHeld:output_type -> host.installation.v1.ListHeldResponse + 15, // 36: host.installation.v1.InstallableService.Install:output_type -> host.installation.v1.InstallationAck + 17, // 37: host.installation.v1.OfferService.ListOffers:output_type -> host.installation.v1.ListOffersResponse + 19, // 38: host.installation.v1.OfferService.ReviewOffer:output_type -> host.installation.v1.ReviewedOffer + 28, // [28:39] is the sub-list for method output_type + 17, // [17:28] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_host_installation_v1_installation_proto_init() } @@ -1859,7 +2023,7 @@ func file_host_installation_v1_installation_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_host_installation_v1_installation_proto_rawDesc), len(file_host_installation_v1_installation_proto_rawDesc)), NumEnums: 1, - NumMessages: 24, + NumMessages: 27, NumExtensions: 0, NumServices: 3, }, diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go index 0fdefdf19..492488672 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installation_grpc.pb.go @@ -33,6 +33,7 @@ const ( InstallationService_RevokeRole_FullMethodName = "/host.installation.v1.InstallationService/RevokeRole" InstallationService_ListRoleGrants_FullMethodName = "/host.installation.v1.InstallationService/ListRoleGrants" InstallationService_List_FullMethodName = "/host.installation.v1.InstallationService/List" + InstallationService_ListHeld_FullMethodName = "/host.installation.v1.InstallationService/ListHeld" ) // InstallationServiceClient is the client API for InstallationService service. @@ -61,10 +62,16 @@ type InstallationServiceClient interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(ctx context.Context, in *ListRoleGrantsRequest, opts ...grpc.CallOption) (*ListRoleGrantsResponse, error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(ctx context.Context, in *ListRequest, opts ...grpc.CallOption) (*ListResponse, error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(ctx context.Context, in *ListHeldRequest, opts ...grpc.CallOption) (*ListHeldResponse, error) } type installationServiceClient struct { @@ -145,6 +152,16 @@ func (c *installationServiceClient) List(ctx context.Context, in *ListRequest, o return out, nil } +func (c *installationServiceClient) ListHeld(ctx context.Context, in *ListHeldRequest, opts ...grpc.CallOption) (*ListHeldResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListHeldResponse) + err := c.cc.Invoke(ctx, InstallationService_ListHeld_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // InstallationServiceServer is the server API for InstallationService service. // All implementations must embed UnimplementedInstallationServiceServer // for forward compatibility. @@ -171,10 +188,16 @@ type InstallationServiceServer interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *ListRoleGrantsRequest) (*ListRoleGrantsResponse, error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *ListRequest) (*ListResponse, error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *ListHeldRequest) (*ListHeldResponse, error) mustEmbedUnimplementedInstallationServiceServer() } @@ -206,6 +229,9 @@ func (UnimplementedInstallationServiceServer) ListRoleGrants(context.Context, *L func (UnimplementedInstallationServiceServer) List(context.Context, *ListRequest) (*ListResponse, error) { return nil, status.Error(codes.Unimplemented, "method List not implemented") } +func (UnimplementedInstallationServiceServer) ListHeld(context.Context, *ListHeldRequest) (*ListHeldResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListHeld not implemented") +} func (UnimplementedInstallationServiceServer) mustEmbedUnimplementedInstallationServiceServer() {} func (UnimplementedInstallationServiceServer) testEmbeddedByValue() {} @@ -353,6 +379,24 @@ func _InstallationService_List_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _InstallationService_ListHeld_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListHeldRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InstallationServiceServer).ListHeld(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InstallationService_ListHeld_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InstallationServiceServer).ListHeld(ctx, req.(*ListHeldRequest)) + } + return interceptor(ctx, in, info, handler) +} + // InstallationService_ServiceDesc is the grpc.ServiceDesc for InstallationService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -388,6 +432,10 @@ var InstallationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "List", Handler: _InstallationService_List_Handler, }, + { + MethodName: "ListHeld", + Handler: _InstallationService_ListHeld_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "host/installation/v1/installation.proto", diff --git a/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go b/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go index f5cb5ade7..42d72ee8e 100644 --- a/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go +++ b/module/services/accounts/code/pkg/gen/host/installation/v1/installationv1connect/installation.connect.go @@ -64,6 +64,9 @@ const ( // InstallationServiceListProcedure is the fully-qualified name of the InstallationService's List // RPC. InstallationServiceListProcedure = "/host.installation.v1.InstallationService/List" + // InstallationServiceListHeldProcedure is the fully-qualified name of the InstallationService's + // ListHeld RPC. + InstallationServiceListHeldProcedure = "/host.installation.v1.InstallationService/ListHeld" // InstallableServiceInstallProcedure is the fully-qualified name of the InstallableService's // Install RPC. InstallableServiceInstallProcedure = "/host.installation.v1.InstallableService/Install" @@ -96,10 +99,16 @@ type InstallationServiceClient interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *connect.Request[v1.ListRoleGrantsRequest]) (*connect.Response[v1.ListRoleGrantsResponse], error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *connect.Request[v1.ListRequest]) (*connect.Response[v1.ListResponse], error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) } // NewInstallationServiceClient constructs a client for the host.installation.v1.InstallationService @@ -155,6 +164,12 @@ func NewInstallationServiceClient(httpClient connect.HTTPClient, baseURL string, connect.WithSchema(installationServiceMethods.ByName("List")), connect.WithClientOptions(opts...), ), + listHeld: connect.NewClient[v1.ListHeldRequest, v1.ListHeldResponse]( + httpClient, + baseURL+InstallationServiceListHeldProcedure, + connect.WithSchema(installationServiceMethods.ByName("ListHeld")), + connect.WithClientOptions(opts...), + ), } } @@ -167,6 +182,7 @@ type installationServiceClient struct { revokeRole *connect.Client[v1.RevokeRoleRequest, v1.Installation] listRoleGrants *connect.Client[v1.ListRoleGrantsRequest, v1.ListRoleGrantsResponse] list *connect.Client[v1.ListRequest, v1.ListResponse] + listHeld *connect.Client[v1.ListHeldRequest, v1.ListHeldResponse] } // Current calls host.installation.v1.InstallationService.Current. @@ -204,6 +220,11 @@ func (c *installationServiceClient) List(ctx context.Context, req *connect.Reque return c.list.CallUnary(ctx, req) } +// ListHeld calls host.installation.v1.InstallationService.ListHeld. +func (c *installationServiceClient) ListHeld(ctx context.Context, req *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) { + return c.listHeld.CallUnary(ctx, req) +} + // InstallationServiceHandler is an implementation of the host.installation.v1.InstallationService // service. type InstallationServiceHandler interface { @@ -227,10 +248,16 @@ type InstallationServiceHandler interface { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. ListRoleGrants(context.Context, *connect.Request[v1.ListRoleGrantsRequest]) (*connect.Response[v1.ListRoleGrantsResponse], error) - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. List(context.Context, *connect.Request[v1.ListRequest]) (*connect.Response[v1.ListResponse], error) + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) } // NewInstallationServiceHandler builds an HTTP handler from the service implementation. It returns @@ -282,6 +309,12 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne connect.WithSchema(installationServiceMethods.ByName("List")), connect.WithHandlerOptions(opts...), ) + installationServiceListHeldHandler := connect.NewUnaryHandler( + InstallationServiceListHeldProcedure, + svc.ListHeld, + connect.WithSchema(installationServiceMethods.ByName("ListHeld")), + connect.WithHandlerOptions(opts...), + ) return "/host.installation.v1.InstallationService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case InstallationServiceCurrentProcedure: @@ -298,6 +331,8 @@ func NewInstallationServiceHandler(svc InstallationServiceHandler, opts ...conne installationServiceListRoleGrantsHandler.ServeHTTP(w, r) case InstallationServiceListProcedure: installationServiceListHandler.ServeHTTP(w, r) + case InstallationServiceListHeldProcedure: + installationServiceListHeldHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -335,6 +370,10 @@ func (UnimplementedInstallationServiceHandler) List(context.Context, *connect.Re return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.List is not implemented")) } +func (UnimplementedInstallationServiceHandler) ListHeld(context.Context, *connect.Request[v1.ListHeldRequest]) (*connect.Response[v1.ListHeldResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("host.installation.v1.InstallationService.ListHeld is not implemented")) +} + // InstallableServiceClient is a client for the host.installation.v1.InstallableService service. type InstallableServiceClient interface { // Install sets up the solution for one installation; it must converge. diff --git a/module/services/accounts/code/pkg/gen/host/session/v1/session.pb.go b/module/services/accounts/code/pkg/gen/host/session/v1/session.pb.go index 7c83adf2d..311a88f70 100644 --- a/module/services/accounts/code/pkg/gen/host/session/v1/session.pb.go +++ b/module/services/accounts/code/pkg/gen/host/session/v1/session.pb.go @@ -125,9 +125,12 @@ type FixturePerson struct { // The teams they belong to. TeamIds []string `protobuf:"bytes,5,rep,name=team_ids,json=teamIds,proto3" json:"team_ids,omitempty"` // The token SignIn takes for them. - FixtureToken string `protobuf:"bytes,6,opt,name=fixture_token,json=fixtureToken,proto3" json:"fixture_token,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + FixtureToken string `protobuf:"bytes,6,opt,name=fixture_token,json=fixtureToken,proto3" json:"fixture_token,omitempty"` + // The organisation's name, as the fixture spells it, for a page that shows + // a person where they sign into; the id above is the identity. + OrganizationDisplayName string `protobuf:"bytes,7,opt,name=organization_display_name,json=organizationDisplayName,proto3" json:"organization_display_name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *FixturePerson) Reset() { @@ -202,6 +205,13 @@ func (x *FixturePerson) GetFixtureToken() string { return "" } +func (x *FixturePerson) GetOrganizationDisplayName() string { + if x != nil { + return x.OrganizationDisplayName + } + return "" +} + // CurrentRequest asks for the caller's own session; the bearer is the call's. type CurrentRequest struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -403,14 +413,15 @@ const file_host_session_v1_session_proto_rawDesc = "" + "\x1dhost/session/v1/session.proto\x12\x0fhost.session.v1\x1a\x1cgoogle/api/annotations.proto\"\x1a\n" + "\x18ListFixturePeopleRequest\"S\n" + "\x19ListFixturePeopleResponse\x126\n" + - "\x06people\x18\x01 \x03(\v2\x1e.host.session.v1.FixturePersonR\x06people\"\xc1\x01\n" + + "\x06people\x18\x01 \x03(\v2\x1e.host.session.v1.FixturePersonR\x06people\"\xfd\x01\n" + "\rFixturePerson\x12\x1b\n" + "\tperson_id\x18\x01 \x01(\tR\bpersonId\x12\x14\n" + "\x05email\x18\x02 \x01(\tR\x05email\x12'\n" + "\x0forganization_id\x18\x03 \x01(\tR\x0eorganizationId\x12\x14\n" + "\x05roles\x18\x04 \x03(\tR\x05roles\x12\x19\n" + "\bteam_ids\x18\x05 \x03(\tR\ateamIds\x12#\n" + - "\rfixture_token\x18\x06 \x01(\tR\ffixtureToken\"\x10\n" + + "\rfixture_token\x18\x06 \x01(\tR\ffixtureToken\x12:\n" + + "\x19organization_display_name\x18\a \x01(\tR\x17organizationDisplayName\"\x10\n" + "\x0eCurrentRequest\"4\n" + "\rSignInRequest\x12#\n" + "\rfixture_token\x18\x01 \x01(\tR\ffixtureToken\"\xa3\x02\n" + diff --git a/module/services/accounts/code/pkg/keyservice/load.go b/module/services/accounts/code/pkg/keyservice/load.go index 34dc30606..915b820b4 100644 --- a/module/services/accounts/code/pkg/keyservice/load.go +++ b/module/services/accounts/code/pkg/keyservice/load.go @@ -9,6 +9,7 @@ import ( "strings" codefly "github.com/codefly-dev/sdk-go" + managed "github.com/codefly-dev/service-vault/keyservice" "accounts/pkg/vaultconnection" ) @@ -67,7 +68,8 @@ type Binding struct { // VaultHealth probes the Vault that holds the envelope key, or nil. VaultHealth func(ctx context.Context) error - kms KMSConfig + kms KMSConfig + managed managed.Client // wrappedSigningKey is the configured ciphertext, read once at selection so // a missing one refuses with every other configuration problem rather than // after a key service has already been reached. @@ -138,7 +140,24 @@ func Load(ctx context.Context) (*Binding, error) { Backend: backend, Previous: previous, SigningBackend: signing, wrappedSigningKey: groupValue(ctx, SigningKeyWrappedKey), } - if backend == BackendKMS || previous == BackendKMS || signing == BackendKMSWrapped { + descriptor := groupValue(ctx, ManagedBindingKey) + usesManaged := backend == BackendKMS || previous == BackendKMS || signing == BackendKMSWrapped + if descriptor != "" { + if !usesManaged { + return nil, errors.New("managed key-service binding requires a kms consumer") + } + for _, key := range []string{KMSEnvelopeKeyKey, KMSMACKeyKey, KMSSigningWrapKeyKey} { + if groupValue(ctx, key) != "" { + return nil, errors.New("managed key-service binding cannot be combined with legacy KMS key names") + } + } + if signing == BackendKMSWrapped && binding.wrappedSigningKey == "" { + return nil, fmt.Errorf("%s is required for wrapped signing", SigningKeyWrappedKey) + } + if binding.managed, err = managed.New(ctx, descriptor); err != nil { + return nil, err + } + } else if usesManaged { if binding.kms, err = kmsConfig(ctx, backend, previous, signing); err != nil { return nil, err } @@ -175,6 +194,9 @@ func (b *Binding) sealer(ctx context.Context, backend Backend) (Sealer, error) { } return sealer, nil case BackendKMS: + if b.managed != nil { + return managedSealer{b.managed}, nil + } return NewKMSSealer(ctx, b.kms) default: return nil, fmt.Errorf("unsupported key-service backend %q", backend) @@ -209,6 +231,9 @@ func (b *Binding) vaultConnection(ctx context.Context) (*vaultconnection.Connect // gateway pinned. func (b *Binding) SigningKey(ctx context.Context, ephemeral bool) (ed25519.PrivateKey, error) { if b.SigningBackend == BackendKMSWrapped { + if b.managed != nil { + return managedSigningKey(ctx, b.managed, b.wrappedSigningKey) + } wrap, err := NewKMSKeyWrap(ctx, b.kms) if err != nil { return nil, err diff --git a/module/services/accounts/code/pkg/keyservice/managed.go b/module/services/accounts/code/pkg/keyservice/managed.go new file mode 100644 index 000000000..a635a8ee2 --- /dev/null +++ b/module/services/accounts/code/pkg/keyservice/managed.go @@ -0,0 +1,70 @@ +package keyservice + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "errors" + "fmt" + + managed "github.com/codefly-dev/service-vault/keyservice" + + "accounts/pkg/business" +) + +// ManagedBindingKey is the non-secret installation projected by the key-service +// owner. Provider drivers live in that owner's runtime client, never in Host. +const ManagedBindingKey = "KEY_SERVICE_MANAGED_BINDING" + +type managedSealer struct{ managed.Client } + +func (s managedSealer) Accepts(e Envelope) bool { + return e.Backend == s.Tag() && s.Client.Accepts(e.Payload) +} + +func (s managedSealer) Open(ctx context.Context, purpose, payload string) (string, error) { + value, err := s.Client.Open(ctx, purpose, payload) + if errors.Is(err, managed.ErrInvalidCiphertext) { + return "", fmt.Errorf("%w: %w", business.ErrInvalidSecretEnvelope, err) + } + return value, err +} + +func (s managedSealer) MAC(ctx context.Context, value string) (string, error) { + hash, err := s.Client.MAC(ctx, value) + if err != nil { + return "", err + } + return (Envelope{Backend: s.Tag(), Payload: hash}).String(), nil +} + +func (s managedSealer) SealerFor(ctx context.Context, key string) (Sealer, error) { + client, err := s.Client.ForKey(ctx, key) + if err != nil { + return nil, err + } + return managedSealer{client}, nil +} + +func managedSigningKey(ctx context.Context, client managed.Client, value string) (ed25519.PrivateKey, error) { + envelope, err := ParseEnvelope(value) + if err != nil { + return nil, err + } + if envelope.Backend != client.Tag() { + return nil, errors.New("wrapped signing key belongs to another key service") + } + encoded, err := client.Unwrap(ctx, signingKeyWrapPurpose, envelope.Payload) + if err != nil { + return nil, fmt.Errorf("unwrap managed signing key: %w", err) + } + seed, err := base64.StdEncoding.Strict().DecodeString(encoded) + if err != nil || len(seed) != ed25519.SeedSize { + return nil, errors.New("managed signing key must unwrap to a base64 Ed25519 seed") + } + defer clear(seed) + return ed25519.NewKeyFromSeed(seed), nil +} + +var _ Sealer = managedSealer{} +var _ KeyFactory = managedSealer{} diff --git a/module/services/accounts/code/pkg/keyservice/managed_test.go b/module/services/accounts/code/pkg/keyservice/managed_test.go new file mode 100644 index 000000000..92277d7b2 --- /dev/null +++ b/module/services/accounts/code/pkg/keyservice/managed_test.go @@ -0,0 +1,116 @@ +package keyservice + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "errors" + "testing" + + managed "github.com/codefly-dev/service-vault/keyservice" + "github.com/stretchr/testify/require" + + "accounts/pkg/business" +) + +// Only Host's adaptation is controlled here. Provider cryptography and retained +// key behavior are qualified by the runtime client's own SDK transport suite. +type projectedClient struct { + managed.Client + sealer testSealer + seed string + err error + purpose string +} + +func (c *projectedClient) Tag() string { return c.sealer.Tag() } +func (c *projectedClient) Identity() string { return c.sealer.Identity() } +func (c *projectedClient) Accepts(payload string) bool { + return c.sealer.Accepts(Envelope{Backend: c.Tag(), Payload: payload}) +} +func (c *projectedClient) Seal(ctx context.Context, purpose, value string) (string, error) { + return c.sealer.Seal(ctx, purpose, value) +} +func (c *projectedClient) Open(ctx context.Context, purpose, value string) (string, error) { + if c.err != nil { + return "", c.err + } + return c.sealer.Open(ctx, purpose, value) +} +func (c *projectedClient) MAC(ctx context.Context, value string) (string, error) { + return c.sealer.MAC(ctx, value) +} +func (c *projectedClient) Unwrap(_ context.Context, purpose, _ string) (string, error) { + c.purpose = purpose + return c.seed, c.err +} +func (c *projectedClient) ForKey(_ context.Context, key string) (managed.Client, error) { + return &projectedClient{sealer: testSealer{tag: c.Tag(), key: key}}, nil +} + +func TestManagedSealerRetainsHostFramingAndErrorSemantics(t *testing.T) { + client := &projectedClient{sealer: testSealer{tag: "managed-test", key: "deployment"}} + sealer := managedSealer{client} + cipher, err := NewCipher(sealer, nil) + require.NoError(t, err) + stored, err := cipher.EncryptSecret(t.Context(), "row", "credential") + require.NoError(t, err) + require.True(t, IsEnvelopeFraming(stored)) + parsed, err := ParseEnvelope(stored) + require.NoError(t, err) + require.Equal(t, "managed-test", parsed.Backend) + opened, err := cipher.DecryptSecret(t.Context(), "row", stored) + require.NoError(t, err) + require.Equal(t, "credential", opened) + hash, err := sealer.MAC(t.Context(), "api-key") + require.NoError(t, err) + require.Equal(t, "cfs1:managed-test:deployment:api-key", hash) + org, err := sealer.SealerFor(t.Context(), "organization") + require.NoError(t, err) + require.NotEqual(t, sealer.Identity(), org.Identity()) + client.err = managed.ErrInvalidCiphertext + _, err = cipher.DecryptSecret(t.Context(), "row", stored) + require.ErrorIs(t, err, business.ErrInvalidSecretEnvelope) + client.err = errors.New("provider unavailable") + _, err = cipher.DecryptSecret(t.Context(), "row", stored) + require.ErrorIs(t, err, client.err) + require.NotErrorIs(t, err, business.ErrInvalidSecretEnvelope) +} + +func TestManagedWrappedSigningPreservesExistingEd25519Contract(t *testing.T) { + seed := make([]byte, ed25519.SeedSize) + seed[0] = 19 + client := &projectedClient{sealer: testSealer{tag: "managed-test"}, seed: base64.StdEncoding.EncodeToString(seed)} + binding := &Binding{SigningBackend: BackendKMSWrapped, managed: client, wrappedSigningKey: "cfs1:managed-test:opaque"} + key, err := binding.SigningKey(t.Context(), false) + require.NoError(t, err) + require.Equal(t, ed25519.NewKeyFromSeed(seed), key) + require.Equal(t, signingKeyWrapPurpose, client.purpose) + again, err := binding.SigningKey(t.Context(), false) + require.NoError(t, err) + require.Equal(t, key, again) + client.err = errors.New("access revoked") + _, err = binding.SigningKey(t.Context(), true) + require.ErrorIs(t, err, client.err) + require.NotErrorIs(t, err, ErrNoSigningKey, "managed custody must never permit ephemeral fallback") + client.err = nil + client.seed = base64.StdEncoding.EncodeToString([]byte("short")) + _, err = binding.SigningKey(t.Context(), false) + require.Error(t, err) + binding.wrappedSigningKey = "cfs1:other:opaque" + _, err = binding.SigningKey(t.Context(), false) + require.Error(t, err) +} + +func TestManagedSelectionRefusesMixedBindingsBeforeProviderAccess(t *testing.T) { + t.Setenv(BackendKey, "kms") + t.Setenv(SigningBackendKey, "kms-wrapped") + t.Setenv(ManagedBindingKey, `{"provider":"unreachable"}`) + t.Setenv(KMSEnvelopeKeyKey, "legacy-key") + _, err := Load(t.Context()) + require.ErrorContains(t, err, "legacy KMS") + t.Setenv(KMSEnvelopeKeyKey, "") + t.Setenv(SigningKeyWrappedKey, "") + _, err = Load(t.Context()) + require.ErrorContains(t, err, SigningKeyWrappedKey) +} diff --git a/module/services/accounts/generated/api-contract-surfaces.json b/module/services/accounts/generated/api-contract-surfaces.json index 69272354e..e8f4a7823 100644 --- a/module/services/accounts/generated/api-contract-surfaces.json +++ b/module/services/accounts/generated/api-contract-surfaces.json @@ -88,6 +88,7 @@ "/host.installation.v1.InstallationService/GrantRole", "/host.installation.v1.InstallationService/Install", "/host.installation.v1.InstallationService/List", + "/host.installation.v1.InstallationService/ListHeld", "/host.installation.v1.InstallationService/ListRoleGrants", "/host.installation.v1.InstallationService/Review", "/host.installation.v1.InstallationService/RevokeRole" diff --git a/module/services/accounts/generated/authz-methods.json b/module/services/accounts/generated/authz-methods.json index 0fdfbb7d7..ba2429346 100644 --- a/module/services/accounts/generated/authz-methods.json +++ b/module/services/accounts/generated/authz-methods.json @@ -732,6 +732,43 @@ "policy_sha256": "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0", "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" }, + { + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "full_service": "host.installation.v1.InstallationService", + "method": "ListHeld", + "input_type": "host.installation.v1.ListHeldRequest", + "output_type": "host.installation.v1.ListHeldResponse", + "client_streaming": false, + "server_streaming": false, + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_OPTIONAL", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_FORBIDDEN" + }, + "policy_tier": "auth", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "source_proto": "host/installation/v1/installation.proto", + "policy_sha256": "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26", + "rate_limit_backend_failure_mode": "RATE_LIMIT_BACKEND_FAILURE_MODE_FAIL_OPEN" + }, { "procedure": "/host.installation.v1.InstallationService/ListRoleGrants", "full_service": "host.installation.v1.InstallationService", diff --git a/module/services/accounts/generated/gateway-routes.json b/module/services/accounts/generated/gateway-routes.json index faa25c69e..492d2c5e9 100644 --- a/module/services/accounts/generated/gateway-routes.json +++ b/module/services/accounts/generated/gateway-routes.json @@ -3681,6 +3681,22 @@ "rewrite_path": "", "remove_after": "" }, + { + "protocol": "GATEWAY_PROTOCOL_CONNECT", + "method": "POST", + "path": "/host.installation.v1.InstallationService/ListHeld", + "match": "GATEWAY_MATCH_EXACT", + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "owner": { + "module": "saas-starter", + "service": "accounts" + }, + "upstream_endpoint": "connect", + "exposure": "EXPOSURE_AUTHENTICATED", + "source": "GATEWAY_ROUTE_SOURCE_DESCRIPTOR", + "rewrite_path": "", + "remove_after": "" + }, { "protocol": "GATEWAY_PROTOCOL_CONNECT", "method": "POST", diff --git a/module/services/accounts/generated/openapi-raw/api.swagger.json b/module/services/accounts/generated/openapi-raw/api.swagger.json index 64cd4108a..8c5fc81cd 100644 --- a/module/services/accounts/generated/openapi-raw/api.swagger.json +++ b/module/services/accounts/generated/openapi-raw/api.swagger.json @@ -8813,6 +8813,10 @@ "fixtureToken": { "type": "string", "description": "The token SignIn takes for them." + }, + "organizationDisplayName": { + "type": "string", + "description": "The organisation's name, as the fixture spells it, for a page that shows\na person where they sign into; the id above is the identity." } }, "description": "FixturePerson is one person a fixture run signs in." diff --git a/module/services/accounts/generated/service-catalog.json b/module/services/accounts/generated/service-catalog.json index f3faf12e5..6aa0ddf69 100644 --- a/module/services/accounts/generated/service-catalog.json +++ b/module/services/accounts/generated/service-catalog.json @@ -48,6 +48,7 @@ "/host.installation.v1.InstallationService/GrantRole", "/host.installation.v1.InstallationService/Install", "/host.installation.v1.InstallationService/List", + "/host.installation.v1.InstallationService/ListHeld", "/host.installation.v1.InstallationService/ListRoleGrants", "/host.installation.v1.InstallationService/Review", "/host.installation.v1.InstallationService/RevokeRole" @@ -1408,6 +1409,49 @@ "service": "accounts" } }, + { + "procedure": "/host.installation.v1.InstallationService/ListHeld", + "service": "InstallationService", + "full_service": "host.installation.v1.InstallationService", + "method": "ListHeld", + "input_type": "host.installation.v1.ListHeldRequest", + "output_type": "host.installation.v1.ListHeldResponse", + "client_streaming": false, + "server_streaming": false, + "deprecated": false, + "source_proto": "host/installation/v1/installation.proto", + "protocols": [ + "PROTOCOL_GRPC", + "PROTOCOL_CONNECT" + ], + "http_bindings": [], + "policy": { + "exposure": "EXPOSURE_AUTHENTICATED", + "tenant": "TENANT_REQUIREMENT_NONE", + "permissions": [], + "scopes": [], + "resource_bindings": [], + "mfa": "MFA_REQUIREMENT_NONE", + "audit": { + "events": [], + "emission": "AUDIT_EMISSION_NONE" + }, + "idempotency": "IDEMPOTENCY_REQUIREMENT_OPTIONAL", + "rate_limit": "RATE_LIMIT_CLASS_STANDARD_READ", + "request_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "response_sensitivity": "SENSITIVITY_CONFIDENTIAL", + "platform_role": "PLATFORM_ROLE_REQUIREMENT_NONE", + "authentication_factor_attempt": false, + "conditions": [], + "impersonation": "IMPERSONATION_REQUIREMENT_FORBIDDEN" + }, + "policy_tier": "auth", + "description": "List only the signed-in member's current installation roles, including team grants.", + "owner": { + "module": "saas-starter", + "service": "accounts" + } + }, { "procedure": "/host.installation.v1.InstallationService/ListRoleGrants", "service": "InstallationService", diff --git a/module/services/accounts/proto/host/DIGESTS.sha256 b/module/services/accounts/proto/host/DIGESTS.sha256 index 1d31bafed..680171a92 100644 --- a/module/services/accounts/proto/host/DIGESTS.sha256 +++ b/module/services/accounts/proto/host/DIGESTS.sha256 @@ -2,5 +2,5 @@ 5f743acb2e21e23eb916092313f54b0737742fe73c77d3f43208987a4aabf4c4 authority/v1/authority.proto f1ea4a9113527f8ed972660c3d2134a39135e96cf212b780ec04d14321ddcab1 authority/v1/work_context.proto 65353da82976f3f00d0a87792b93a723adbfc9deec48a5f7e337ff9526be53f5 composition/v1/composition.proto -d69c70528987ef59180b0ac683ac3c30de9b18077dacc3eece1342465953314e installation/v1/installation.proto -5d1e2f8fb2e7c3a7b8e027d8de23e26e53fa21212831e23fac492372ac53f28b session/v1/session.proto +5de7bd8b5f68d5bdf081dab59352473a04c13c6bb5cecae0ec32beb6f5a45305 installation/v1/installation.proto +8754895a7f8ab7d69994b11cf88be17a575f19699f78975041e17e78008556b1 session/v1/session.proto diff --git a/module/services/accounts/proto/host/installation/v1/installation.proto b/module/services/accounts/proto/host/installation/v1/installation.proto index c1ebefd92..f3f3f1397 100644 --- a/module/services/accounts/proto/host/installation/v1/installation.proto +++ b/module/services/accounts/proto/host/installation/v1/installation.proto @@ -30,10 +30,16 @@ service InstallationService { // ListRoleGrants answers who holds which role on the installation; the same // callers as GrantRole. rpc ListRoleGrants(ListRoleGrantsRequest) returns (ListRoleGrantsResponse); - // List answers what one organisation has installed: an administrator's read. A - // member's view, which installed solutions they may use, is their navigation and - // belongs to the entitlement read, not here. + // List answers what one organisation has installed: an administrator's read. + // A member's view, which installed solutions they may use, is ListHeld. rpc List(ListRequest) returns (ListResponse); + // ListHeld answers the signed-in person which installations of their + // organisation they may use: each active installation on which they hold a + // role, granted to them or to a team of theirs, with the roles they hold. + // It is their navigation and answers their own holdings alone, never who + // else holds what. A member holding nothing reads an empty list; a caller + // acting at another organisation is refused naming it. + rpc ListHeld(ListHeldRequest) returns (ListHeldResponse); } message CurrentInstallationRequest { @@ -309,3 +315,25 @@ message OfferTool { // True when the tool changes data. bool write = 3; } + +// ListHeldRequest names the organisation the person reads at: the one their +// session was opened at. +message ListHeldRequest { + // The organisation. + string organization_id = 1; +} + +// ListHeldResponse is every installation the person holds a role on, sorted +// by installation id. +message ListHeldResponse { + // The installations. + repeated HeldInstallation installations = 1; +} + +// HeldInstallation is one installation the person may use. +message HeldInstallation { + // The installation. + Installation installation = 1; + // The roles the person holds on it, directly or through a team, sorted. + repeated string roles = 2; +} diff --git a/module/services/accounts/proto/host/session/v1/session.proto b/module/services/accounts/proto/host/session/v1/session.proto index bf7894518..6b07d758b 100644 --- a/module/services/accounts/proto/host/session/v1/session.proto +++ b/module/services/accounts/proto/host/session/v1/session.proto @@ -54,6 +54,9 @@ message FixturePerson { repeated string team_ids = 5; // The token SignIn takes for them. string fixture_token = 6; + // The organisation's name, as the fixture spells it, for a page that shows + // a person where they sign into; the id above is the identity. + string organization_display_name = 7; } // CurrentRequest asks for the caller's own session; the bearer is the call's. diff --git a/module/services/auth-gateway/code/authz_catalog_gen.go b/module/services/auth-gateway/code/authz_catalog_gen.go index d7971ce6e..ca7cfda7c 100644 --- a/module/services/auth-gateway/code/authz_catalog_gen.go +++ b/module/services/auth-gateway/code/authz_catalog_gen.go @@ -30,6 +30,7 @@ var generatedAuthorizationByProcedure = map[string]generatedAuthorizationMetadat "/host.installation.v1.InstallationService/GrantRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "fc01eebe90648f5e096a7d4bbbd8f2aeb3d755bbd808c962352ed78936b12a4a"}, "/host.installation.v1.InstallationService/Install": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "9848918817951cb839354fccd74257a45731b1ff0ae86aaa3af89dadc0f32a1b"}, "/host.installation.v1.InstallationService/List": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0"}, + "/host.installation.v1.InstallationService/ListHeld": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26"}, "/host.installation.v1.InstallationService/ListRoleGrants": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "79921072468c5f99274839371a7ac834d857df6e5f0708942165c4b31084de26"}, "/host.installation.v1.InstallationService/Review": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardRead, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "b387739192840b24f771349cb2cded72d5f7eabd9d56c433b5b5937e6f4e58f0"}, "/host.installation.v1.InstallationService/RevokeRole": {exposure: edgeExposureAuthenticated, rateLimitClass: edgeRateLimitClassStandardWrite, rateLimitBackendFailClosed: false, authenticationFactorAttempt: false, policySHA256: "912f1c45666c1a2c3089918be4723973ca4fadf2dc78e5e80253d93499e74c92"}, diff --git a/module/services/auth-gateway/code/go.mod b/module/services/auth-gateway/code/go.mod index b5754c8d9..0cf19a222 100644 --- a/module/services/auth-gateway/code/go.mod +++ b/module/services/auth-gateway/code/go.mod @@ -33,6 +33,11 @@ require ( buf.build/go/protovalidate v1.4.0 // indirect cel.dev/cel-go v0.32.0 // indirect cel.dev/expr v0.25.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.5.0 // indirect github.com/VividCortex/ewma v1.2.0 // indirect @@ -70,6 +75,7 @@ require ( github.com/google/go-querystring v1.2.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.24 // indirect @@ -78,6 +84,7 @@ require ( github.com/oklog/ulid/v2 v2.1.2 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/pkg/errors v0.9.1 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect @@ -97,6 +104,7 @@ require ( go.opentelemetry.io/otel/trace v1.47.0 // indirect go.uber.org/atomic v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect + golang.org/x/crypto v0.57.0 // indirect golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect golang.org/x/net v0.60.0 // indirect golang.org/x/sync v0.23.0 // indirect @@ -107,3 +115,6 @@ require ( ) exclude google.golang.org/genproto v0.0.0-20210226172003-ab064af71705 + +// DEVELOPMENT CANDIDATE: frontend registry reader in PR #1234; adopt an owner release before promotion. +replace github.com/codefly-dev/module-saas-starter/module/frontendpayload => github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352 diff --git a/module/services/auth-gateway/code/go.sum b/module/services/auth-gateway/code/go.sum index 76176a9d7..0cfa1eff3 100644 --- a/module/services/auth-gateway/code/go.sum +++ b/module/services/auth-gateway/code/go.sum @@ -8,6 +8,20 @@ cel.dev/expr v0.25.3 h1:A2jO8jwOugrrovveCWfj0KEZOfqiLgAcwjpHPhzIGw0= cel.dev/expr v0.25.3/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1 h1:zvXfGJCWvywnCA814d8ZiVyt+fm9nnTE8xSb99zRyfo= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.23.1/go.mod h1:iptorS+VYKFL2N6PnebpS91dubG35eAOEERnT4PJbQU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1 h1:u93s+zU2JD62im61Bm5CZIc1ZrOJaIAWEg0WOrMVkEo= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.14.1/go.mod h1:oXtinPO4OLj9d1DOTrqrL1oRwGhcqadvAmrl6wTeGlk= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0 h1:xFaZZ+IubdftrDHnGGwZ6QvQ3KHTtWl2MCK+GMt2vxs= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.4.0/go.mod h1:mCBhUhlMjLLJKr5aqw2TNS/VqJOie8MzWq3DAMJeKso= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 h1:fhqpLE3UEXi9lPaBRpQ6XuRW0nU7hgg4zlmZZa+a9q4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0/go.mod h1:7dCRMLwisfRH3dBupKeNCioWYUZ4SS09Z14H+7i8ZoY= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0 h1:Nljr4q1GRA/5vCrMONS+g4u4LRHNgOXVSh3O43J2CnI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.8.0/go.mod h1:Y33QHnf0FfdVewFFISOGe20mkZbxX4H839o955/PoeI= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= @@ -46,8 +60,6 @@ github.com/codefly-dev/core v0.19.6-0.20261018051341-b81d92e6feda h1:ytTihtIEfVh github.com/codefly-dev/core v0.19.6-0.20261018051341-b81d92e6feda/go.mod h1:0uX/7lTBVaGV0RI2Mb3mpEr/PPdx/jZqXHym/U8FVDI= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= github.com/codefly-dev/gortk v0.2.0/go.mod h1:dDWUMFgAP063OCGhTEpV7FFUj9+zTcxxO/nV80VKEwg= -github.com/codefly-dev/module-saas-starter/module/frontendpayload v0.0.0-20261021192505-6fd56dae2e04 h1:I3x8ihrzBMpv9UrLYc/3TmKl+lBvA5+xGcigENG/IR8= -github.com/codefly-dev/module-saas-starter/module/frontendpayload v0.0.0-20261021192505-6fd56dae2e04/go.mod h1:LZl0I41APmLxG3Awdd4gn/pIhMOOgJqLuMuNGyXUJMg= github.com/codefly-dev/sdk-go v0.4.1 h1:WUKYWxXfojTr88SkLtq0T8aqpV0o5pR/muUUcsUUlzk= github.com/codefly-dev/sdk-go v0.4.1/go.mod h1:zr9ts3kkWNv2XEFUXZB+ydfW6x5b6/NbUvBm4KVYNqs= github.com/codefly-dev/sdk-go/workcontext v0.1.1-0.20261018052022-70dc7a546458 h1:wBOL7QsuoC0HsS43gsH2+tpsipk4os077C2VUDkMUHg= @@ -56,6 +68,12 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261027005009-3e9f20a706ff h1:dtG4SGf3deNecwpfQzpFlZOY9fwY86E/xOCVCgqs3BA= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261027005009-3e9f20a706ff/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028173843-c2485a8f50cf h1:5HLTb1dvcbrZkmVF8byhqqXvtrUsxoYKXHhS77fGC0c= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028173843-c2485a8f50cf/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352 h1:TzMok5bj4nTOK1Vb/U/HVVM0MIgYagu3WuuGbG/xuAI= +github.com/crisfiorini-oss/module-saas-starter/module/frontendpayload v0.0.0-20261028181434-d67666bdc352/go.mod h1:k7Jens34+w8LAGA6J/eM9UMKjYewzYCMlIHX0ePPj6I= github.com/cyphar/filepath-securejoin v0.7.0 h1:s0Y3ITPy6sQn5xt54DuYvTF8hu134ooYLUb58DX/HjE= github.com/cyphar/filepath-securejoin v0.7.0/go.mod h1:ymLGms/u3BYaviIiuKFnUx8EkQEZeK6cInNoAPJA3o4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -174,12 +192,16 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.6.0 h1:J1FBfmuVosPHf5GRdltRLhPJtJpTlMdKTBjRgTaQBFY= github.com/kevinburke/ssh_config v1.6.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE= github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak= github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= @@ -205,6 +227,8 @@ github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgr github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pjbgf/sha1cd v0.7.0 h1:ZRNPKHj+gfkLBf0KJv/p1Hmz+7bqCT5o311YX0nq+DA= github.com/pjbgf/sha1cd v0.7.0/go.mod h1:pKR5Li+qTCo+ebqITZfwPlJGoCFCvSO00qxjbNtTUFQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= diff --git a/module/services/auth-gateway/code/routing_catalog_gen.go b/module/services/auth-gateway/code/routing_catalog_gen.go index 7d41dc208..16b42d3dc 100644 --- a/module/services/auth-gateway/code/routing_catalog_gen.go +++ b/module/services/auth-gateway/code/routing_catalog_gen.go @@ -236,6 +236,7 @@ func generatedCatalogConnectRoutes() []*RouteEntry { {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/GrantRole", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/GrantRole"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/Install", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/Install"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/List", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/List"}, + {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/ListHeld", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/ListHeld"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/ListRoleGrants", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/ListRoleGrants"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/Review", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/Review"}, {Service: "accounts_connect", Method: "POST", Path: "/host.installation.v1.InstallationService/RevokeRole", UpstreamPath: "", Procedure: "/host.installation.v1.InstallationService/RevokeRole"}, diff --git a/module/services/frontend/code/package-lock.json b/module/services/frontend/code/package-lock.json index f843f8fbc..b979da7db 100644 --- a/module/services/frontend/code/package-lock.json +++ b/module/services/frontend/code/package-lock.json @@ -15,7 +15,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.40", + "@codefly-dev/saas-sdk": "0.3.41", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", @@ -15742,7 +15742,7 @@ }, "packages/saas-sdk": { "name": "@codefly-dev/saas-sdk", - "version": "0.3.40", + "version": "0.3.41", "license": "Apache-2.0", "dependencies": { "@bufbuild/protobuf": "^2.11.0", diff --git a/module/services/frontend/code/package.json b/module/services/frontend/code/package.json index 4dd0ffcb3..5c4fce115 100644 --- a/module/services/frontend/code/package.json +++ b/module/services/frontend/code/package.json @@ -39,7 +39,7 @@ "@bufbuild/protobuf": "^2.11.0", "@codefly-dev/saas-plugin-contract": "2.4.4", "@codefly-dev/saas-plugin-react": "0.4.5", - "@codefly-dev/saas-sdk": "0.3.40", + "@codefly-dev/saas-sdk": "0.3.41", "@codefly-dev/saas-ui": "0.13.0", "@codefly-dev/ui": "0.13.0", "@codefly/saas-plugin-manifest": "0.1.2", diff --git a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml index 5cb12c298..c84d70da3 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml +++ b/module/services/frontend/code/packages/saas-sdk/generated/library.codefly.yaml @@ -12,7 +12,7 @@ sources: module: saas-starter service: accounts endpoint: connect - contract-digest: sha256:743b63bad9a546f105e994c754d845865e189465e6c72816e5785efe38ab7831 + contract-digest: sha256:6eb1f3eb1ba262017ff4bdfafe047cba7b441855affd67f44285804bdda9e4d5 services: - AccessibleScopeService - AuditService diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts index 3ef395d61..7349ddf76 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/installation/v1/installation_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/installation/v1/installation.proto. */ export const file_host_installation_v1_installation: GenFile = /*@__PURE__*/ - fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgymAUKE0luc3RhbGxhdGlvblNlcnZpY2USZgoHQ3VycmVudBIwLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0GikuaG9zdC5pbnN0YWxsYXRpb24udjEuQ3VycmVudEluc3RhbGxhdGlvbhJTCgdJbnN0YWxsEiQuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SVAoGUmV2aWV3EiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBolLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkNvbnNlbnRBcHByb3ZhbBJXCglHcmFudFJvbGUSJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5HcmFudFJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElkKClJldm9rZVJvbGUSJy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZva2VSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJrCg5MaXN0Um9sZUdyYW50cxIrLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVxdWVzdBosLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USTQoETGlzdBIhLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlc3BvbnNlMnAKEkluc3RhbGxhYmxlU2VydmljZRJaCgdJbnN0YWxsEiguaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uTm90aWNlGiUuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uQWNrMsgBCgxPZmZlclNlcnZpY2USXwoKTGlzdE9mZmVycxInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RPZmZlcnNSZXF1ZXN0GiguaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1Jlc3BvbnNlElcKC1Jldmlld09mZmVyEiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBojLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld2VkT2ZmZXJCNVozZ2F0ZXdheS9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxYgZwcm90bzM"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgiKgoPTGlzdEhlbGRSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJRChBMaXN0SGVsZFJlc3BvbnNlEj0KDWluc3RhbGxhdGlvbnMYASADKAsyJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5IZWxkSW5zdGFsbGF0aW9uIlsKEEhlbGRJbnN0YWxsYXRpb24SOAoMaW5zdGFsbGF0aW9uGAEgASgLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEg0KBXJvbGVzGAIgAygJMvMFChNJbnN0YWxsYXRpb25TZXJ2aWNlEmYKB0N1cnJlbnQSMC5ob3N0Lmluc3RhbGxhdGlvbi52MS5DdXJyZW50SW5zdGFsbGF0aW9uUmVxdWVzdBopLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb24SUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElQKBlJldmlldxIjLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld1JlcXVlc3QaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSVwoJR3JhbnRSb2xlEiYuaG9zdC5pbnN0YWxsYXRpb24udjEuR3JhbnRSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJZCgpSZXZva2VSb2xlEicuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2b2tlUm9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SawoOTGlzdFJvbGVHcmFudHMSKy5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1JlcXVlc3QaLC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1Jlc3BvbnNlEk0KBExpc3QSIS5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXNwb25zZRJZCghMaXN0SGVsZBIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVxdWVzdBomLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2syyAEKDE9mZmVyU2VydmljZRJfCgpMaXN0T2ZmZXJzEicuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1JlcXVlc3QaKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0T2ZmZXJzUmVzcG9uc2USVwoLUmV2aWV3T2ZmZXISIy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZpZXdSZXF1ZXN0GiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3ZWRPZmZlckI1WjNnYXRld2F5L3BrZy9nZW4vaG9zdC9pbnN0YWxsYXRpb24vdjE7aW5zdGFsbGF0aW9udjFiBnByb3RvMw"); /** * @generated from message host.installation.v1.CurrentInstallationRequest @@ -884,6 +884,78 @@ export type OfferTool = Message<"host.installation.v1.OfferTool"> & { export const OfferToolSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 23); +/** + * ListHeldRequest names the organisation the person reads at: the one their + * session was opened at. + * + * @generated from message host.installation.v1.ListHeldRequest + */ +export type ListHeldRequest = Message<"host.installation.v1.ListHeldRequest"> & { + /** + * The organisation. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message host.installation.v1.ListHeldRequest. + * Use `create(ListHeldRequestSchema)` to create a new message. + */ +export const ListHeldRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 24); + +/** + * ListHeldResponse is every installation the person holds a role on, sorted + * by installation id. + * + * @generated from message host.installation.v1.ListHeldResponse + */ +export type ListHeldResponse = Message<"host.installation.v1.ListHeldResponse"> & { + /** + * The installations. + * + * @generated from field: repeated host.installation.v1.HeldInstallation installations = 1; + */ + installations: HeldInstallation[]; +}; + +/** + * Describes the message host.installation.v1.ListHeldResponse. + * Use `create(ListHeldResponseSchema)` to create a new message. + */ +export const ListHeldResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 25); + +/** + * HeldInstallation is one installation the person may use. + * + * @generated from message host.installation.v1.HeldInstallation + */ +export type HeldInstallation = Message<"host.installation.v1.HeldInstallation"> & { + /** + * The installation. + * + * @generated from field: host.installation.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * The roles the person holds on it, directly or through a team, sorted. + * + * @generated from field: repeated string roles = 2; + */ + roles: string[]; +}; + +/** + * Describes the message host.installation.v1.HeldInstallation. + * Use `create(HeldInstallationSchema)` to create a new message. + */ +export const HeldInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 26); + /** * InstallationService is implemented by the host and by the local authority. * @@ -959,9 +1031,8 @@ export const InstallationService: GenService<{ output: typeof ListRoleGrantsResponseSchema; }, /** - * List answers what one organisation has installed: an administrator's read. A - * member's view, which installed solutions they may use, is their navigation and - * belongs to the entitlement read, not here. + * List answers what one organisation has installed: an administrator's read. + * A member's view, which installed solutions they may use, is ListHeld. * * @generated from rpc host.installation.v1.InstallationService.List */ @@ -970,6 +1041,21 @@ export const InstallationService: GenService<{ input: typeof ListRequestSchema; output: typeof ListResponseSchema; }, + /** + * ListHeld answers the signed-in person which installations of their + * organisation they may use: each active installation on which they hold a + * role, granted to them or to a team of theirs, with the roles they hold. + * It is their navigation and answers their own holdings alone, never who + * else holds what. A member holding nothing reads an empty list; a caller + * acting at another organisation is refused naming it. + * + * @generated from rpc host.installation.v1.InstallationService.ListHeld + */ + listHeld: { + methodKind: "unary"; + input: typeof ListHeldRequestSchema; + output: typeof ListHeldResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_host_installation_v1_installation, 0); diff --git a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/session/v1/session_pb.ts b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/session/v1/session_pb.ts index c71af3fa8..c1cdec3de 100644 --- a/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/session/v1/session_pb.ts +++ b/module/services/frontend/code/packages/saas-sdk/generated/typescript/src/gen/host/session/v1/session_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/session/v1/session.proto. */ export const file_host_session_v1_session: GenFile = /*@__PURE__*/ - fileDesc("Ch1ob3N0L3Nlc3Npb24vdjEvc2Vzc2lvbi5wcm90bxIPaG9zdC5zZXNzaW9uLnYxIhoKGExpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdCJLChlMaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlEi4KBnBlb3BsZRgBIAMoCzIeLmhvc3Quc2Vzc2lvbi52MS5GaXh0dXJlUGVyc29uIoIBCg1GaXh0dXJlUGVyc29uEhEKCXBlcnNvbl9pZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSFQoNZml4dHVyZV90b2tlbhgGIAEoCSIQCg5DdXJyZW50UmVxdWVzdCImCg1TaWduSW5SZXF1ZXN0EhUKDWZpeHR1cmVfdG9rZW4YASABKAkiugEKB1Nlc3Npb24SEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAMgASgJEg0KBXJvbGVzGAQgAygJEhAKCHRlYW1faWRzGAUgAygJEg4KBmJlYXJlchgGIAEoCRIbChNwZXJzb25fZGlzcGxheV9uYW1lGAcgASgJEiEKGW9yZ2FuaXphdGlvbl9kaXNwbGF5X25hbWUYCCABKAky4AIKDlNlc3Npb25TZXJ2aWNlEmIKBlNpZ25JbhIeLmhvc3Quc2Vzc2lvbi52MS5TaWduSW5SZXF1ZXN0GhguaG9zdC5zZXNzaW9uLnYxLlNlc3Npb24iHoLT5JMCGDoBKiITL3YxL3Nlc3Npb246c2lnbi1pbhJZCgdDdXJyZW50Eh8uaG9zdC5zZXNzaW9uLnYxLkN1cnJlbnRSZXF1ZXN0GhguaG9zdC5zZXNzaW9uLnYxLlNlc3Npb24iE4LT5JMCDRILL3YxL3Nlc3Npb24SjgEKEUxpc3RGaXh0dXJlUGVvcGxlEikuaG9zdC5zZXNzaW9uLnYxLkxpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdBoqLmhvc3Quc2Vzc2lvbi52MS5MaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlIiKC0+STAhwSGi92MS9zZXNzaW9uL2ZpeHR1cmUtcGVvcGxlQitaKWdhdGV3YXkvcGtnL2dlbi9ob3N0L3Nlc3Npb24vdjE7c2Vzc2lvbnYxYgZwcm90bzM", [file_google_api_annotations]); + fileDesc("Ch1ob3N0L3Nlc3Npb24vdjEvc2Vzc2lvbi5wcm90bxIPaG9zdC5zZXNzaW9uLnYxIhoKGExpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdCJLChlMaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlEi4KBnBlb3BsZRgBIAMoCzIeLmhvc3Quc2Vzc2lvbi52MS5GaXh0dXJlUGVyc29uIqUBCg1GaXh0dXJlUGVyc29uEhEKCXBlcnNvbl9pZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSFQoNZml4dHVyZV90b2tlbhgGIAEoCRIhChlvcmdhbml6YXRpb25fZGlzcGxheV9uYW1lGAcgASgJIhAKDkN1cnJlbnRSZXF1ZXN0IiYKDVNpZ25JblJlcXVlc3QSFQoNZml4dHVyZV90b2tlbhgBIAEoCSK6AQoHU2Vzc2lvbhISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXBlcnNvbl9pZBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSDgoGYmVhcmVyGAYgASgJEhsKE3BlcnNvbl9kaXNwbGF5X25hbWUYByABKAkSIQoZb3JnYW5pemF0aW9uX2Rpc3BsYXlfbmFtZRgIIAEoCTLgAgoOU2Vzc2lvblNlcnZpY2USYgoGU2lnbkluEh4uaG9zdC5zZXNzaW9uLnYxLlNpZ25JblJlcXVlc3QaGC5ob3N0LnNlc3Npb24udjEuU2Vzc2lvbiIegtPkkwIYOgEqIhMvdjEvc2Vzc2lvbjpzaWduLWluElkKB0N1cnJlbnQSHy5ob3N0LnNlc3Npb24udjEuQ3VycmVudFJlcXVlc3QaGC5ob3N0LnNlc3Npb24udjEuU2Vzc2lvbiITgtPkkwINEgsvdjEvc2Vzc2lvbhKOAQoRTGlzdEZpeHR1cmVQZW9wbGUSKS5ob3N0LnNlc3Npb24udjEuTGlzdEZpeHR1cmVQZW9wbGVSZXF1ZXN0GiouaG9zdC5zZXNzaW9uLnYxLkxpc3RGaXh0dXJlUGVvcGxlUmVzcG9uc2UiIoLT5JMCHBIaL3YxL3Nlc3Npb24vZml4dHVyZS1wZW9wbGVCK1opZ2F0ZXdheS9wa2cvZ2VuL2hvc3Qvc2Vzc2lvbi92MTtzZXNzaW9udjFiBnByb3RvMw", [file_google_api_annotations]); /** * ListFixturePeopleRequest asks for every fixture person. @@ -101,6 +101,14 @@ export type FixturePerson = Message<"host.session.v1.FixturePerson"> & { * @generated from field: string fixture_token = 6; */ fixtureToken: string; + + /** + * The organisation's name, as the fixture spells it, for a page that shows + * a person where they sign into; the id above is the identity. + * + * @generated from field: string organization_display_name = 7; + */ + organizationDisplayName: string; }; /** diff --git a/module/services/frontend/code/packages/saas-sdk/package.json b/module/services/frontend/code/packages/saas-sdk/package.json index 1a723c22a..15472f8a5 100644 --- a/module/services/frontend/code/packages/saas-sdk/package.json +++ b/module/services/frontend/code/packages/saas-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@codefly-dev/saas-sdk", - "version": "0.3.40", + "version": "0.3.41", "description": "Typed Connect client, gateway-bound facades, and audit data-graph tooling for the saas-starter public API.", "repository": { "type": "git", diff --git a/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts b/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts index f2604fb4c..16e21e465 100644 --- a/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts +++ b/module/services/frontend/code/src/gen/host/installation/v1/installation_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/installation/v1/installation.proto. */ export const file_host_installation_v1_installation: GenFile = /*@__PURE__*/ - fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgymAUKE0luc3RhbGxhdGlvblNlcnZpY2USZgoHQ3VycmVudBIwLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0GikuaG9zdC5pbnN0YWxsYXRpb24udjEuQ3VycmVudEluc3RhbGxhdGlvbhJTCgdJbnN0YWxsEiQuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbFJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SVAoGUmV2aWV3EiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBolLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkNvbnNlbnRBcHByb3ZhbBJXCglHcmFudFJvbGUSJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5HcmFudFJvbGVSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElkKClJldm9rZVJvbGUSJy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZva2VSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJrCg5MaXN0Um9sZUdyYW50cxIrLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVxdWVzdBosLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSb2xlR3JhbnRzUmVzcG9uc2USTQoETGlzdBIhLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdFJlc3BvbnNlMnAKEkluc3RhbGxhYmxlU2VydmljZRJaCgdJbnN0YWxsEiguaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uTm90aWNlGiUuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uQWNrMsgBCgxPZmZlclNlcnZpY2USXwoKTGlzdE9mZmVycxInLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RPZmZlcnNSZXF1ZXN0GiguaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1Jlc3BvbnNlElcKC1Jldmlld09mZmVyEiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3UmVxdWVzdBojLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld2VkT2ZmZXJC1QEKGGNvbS5ob3N0Lmluc3RhbGxhdGlvbi52MUIRSW5zdGFsbGF0aW9uUHJvdG9QAVo0YWNjb3VudHMvcGtnL2dlbi9ob3N0L2luc3RhbGxhdGlvbi92MTtpbnN0YWxsYXRpb252MaICA0hJWKoCFEhvc3QuSW5zdGFsbGF0aW9uLlYxygIUSG9zdFxJbnN0YWxsYXRpb25cVjHiAiBIb3N0XEluc3RhbGxhdGlvblxWMVxHUEJNZXRhZGF0YeoCFkhvc3Q6Okluc3RhbGxhdGlvbjo6VjFiBnByb3RvMw"); + fileDesc("Cidob3N0L2luc3RhbGxhdGlvbi92MS9pbnN0YWxsYXRpb24ucHJvdG8SFGhvc3QuaW5zdGFsbGF0aW9uLnYxIkwKGkN1cnJlbnRJbnN0YWxsYXRpb25SZXF1ZXN0EhQKDHBhcmVudF90b2tlbhgBIAEoCRIYChBhY3Rvcl9jcmVkZW50aWFsGAIgASgJIm8KE0N1cnJlbnRJbnN0YWxsYXRpb24SCgoCaWQYASABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAIgASgJEg4KBnRhcmdldBgDIAEoCRIQCghyZXZpc2lvbhgEIAEoBBIRCgluYW1lc3BhY2UYBSABKAkimQEKDkluc3RhbGxSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIOCgZ0YXJnZXQYAiABKAkSDAoEcm9sZRgDIAEoCRIXCg9vd25lcl9wZXJzb25faWQYBCABKAkSNwoIYXBwcm92YWwYBSABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwiOAoNUmV2aWV3UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkSDgoGdGFyZ2V0GAIgASgJInkKD0NvbnNlbnRBcHByb3ZhbBIRCgl0YXJnZXRfaWQYASABKAkSEgoKZ2VuZXJhdGlvbhgCIAEoBBIZChFleHBlY3RlZF9yZXZpc2lvbhgDIAEoBBIUCgxvcGVyYXRpb25faWQYBCABKAkSDgoGZGlnZXN0GAUgASgJInYKEEdyYW50Um9sZVJlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCRIMCgRyb2xlGAMgASgJEhEKCXBlcnNvbl9pZBgEIAEoCRIPCgd0ZWFtX2lkGAUgASgJIncKEVJldm9rZVJvbGVSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCRIXCg9pbnN0YWxsYXRpb25faWQYAiABKAkSDAoEcm9sZRgDIAEoCRIRCglwZXJzb25faWQYBCABKAkSDwoHdGVhbV9pZBgFIAEoCSJJChVMaXN0Um9sZUdyYW50c1JlcXVlc3QSFwoPb3JnYW5pemF0aW9uX2lkGAEgASgJEhcKD2luc3RhbGxhdGlvbl9pZBgCIAEoCSJJChZMaXN0Um9sZUdyYW50c1Jlc3BvbnNlEi8KBmdyYW50cxgBIAMoCzIfLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJvbGVHcmFudCI9CglSb2xlR3JhbnQSDAoEcm9sZRgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSDwoHdGVhbV9pZBgDIAEoCSImCgtMaXN0UmVxdWVzdBIXCg9vcmdhbml6YXRpb25faWQYASABKAkiSQoMTGlzdFJlc3BvbnNlEjkKDWluc3RhbGxhdGlvbnMYASADKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24i3QIKDEluc3RhbGxhdGlvbhIKCgJpZBgBIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAiABKAkSDgoGdGFyZ2V0GAMgASgJEjkKBnN0YXR1cxgEIAEoDjIpLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbi5TdGF0dXMSEAoIcmV2aXNpb24YBSABKAQSDAoEcm9sZRgGIAEoCRIRCgluYW1lc3BhY2UYCSABKAkSFAoMcHJpbmNpcGFsX2lkGAogASgJEg0KBXJvbGVzGAsgAygJIlsKBlN0YXR1cxIWChJTVEFUVVNfVU5TUEVDSUZJRUQQABISCg5TVEFUVVNfUEVORElORxABEhEKDVNUQVRVU19BQ1RJVkUQAhISCg5TVEFUVVNfUkVWT0tFRBADSgQIBxAISgQICBAJUgh0ZWFtX2lkc1ISYWdlbnRfcHJpbmNpcGFsX2lkIk4KEkluc3RhbGxhdGlvbk5vdGljZRI4CgxpbnN0YWxsYXRpb24YASABKAsyIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24iEQoPSW5zdGFsbGF0aW9uQWNrIiwKEUxpc3RPZmZlcnNSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJBChJMaXN0T2ZmZXJzUmVzcG9uc2USKwoGb2ZmZXJzGAEgAygLMhsuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXIiKwoFT2ZmZXISDgoGdGFyZ2V0GAEgASgJEhIKCmdlbmVyYXRpb24YAiABKAQijwIKDVJldmlld2VkT2ZmZXISNwoIYXBwcm92YWwYASABKAsyJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSDgoGdGFyZ2V0GAIgASgJEi4KBXJvbGVzGAMgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJSb2xlEjgKDGluc3RhbGxhdGlvbhgEIAEoCzIiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhIzCgdjZWlsaW5nGAUgAygLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJCaW5kaW5nEhYKDnRvb2xzX2NvbXBvc2VkGAYgASgIImEKDE9mZmVyQmluZGluZxIPCgdiaW5kaW5nGAEgASgJEg4KBm1vZHVsZRgCIAEoCRIwCgZzY29wZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlIogBCglPZmZlclJvbGUSDAoEbmFtZRgBIAEoCRIwCgZzY29wZXMYAiADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEjsKDGNhcGFiaWxpdGllcxgDIAMoCzIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLk9mZmVyQ2FwYWJpbGl0eSJhCgpPZmZlclNjb3BlEhUKDXJlc291cmNlX2tpbmQYASABKAkSDwoHYWN0aW9ucxgCIAMoCRIVCg1hdHRhY2hlZF9vbmx5GAMgASgIEhQKDHJlc291cmNlX2lkcxgEIAMoCSKYAQoPT2ZmZXJDYXBhYmlsaXR5Eg4KBm1vZHVsZRgBIAEoCRIRCglvcGVyYXRpb24YAiABKAkSMgoIcmVxdWlyZXMYAyADKAsyIC5ob3N0Lmluc3RhbGxhdGlvbi52MS5PZmZlclNjb3BlEi4KBXRvb2xzGAQgAygLMh8uaG9zdC5pbnN0YWxsYXRpb24udjEuT2ZmZXJUb29sIjkKCU9mZmVyVG9vbBIMCgRuYW1lGAEgASgJEg8KB2JpbmRpbmcYAiABKAkSDQoFd3JpdGUYAyABKAgiKgoPTGlzdEhlbGRSZXF1ZXN0EhcKD29yZ2FuaXphdGlvbl9pZBgBIAEoCSJRChBMaXN0SGVsZFJlc3BvbnNlEj0KDWluc3RhbGxhdGlvbnMYASADKAsyJi5ob3N0Lmluc3RhbGxhdGlvbi52MS5IZWxkSW5zdGFsbGF0aW9uIlsKEEhlbGRJbnN0YWxsYXRpb24SOAoMaW5zdGFsbGF0aW9uGAEgASgLMiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uEg0KBXJvbGVzGAIgAygJMvMFChNJbnN0YWxsYXRpb25TZXJ2aWNlEmYKB0N1cnJlbnQSMC5ob3N0Lmluc3RhbGxhdGlvbi52MS5DdXJyZW50SW5zdGFsbGF0aW9uUmVxdWVzdBopLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkN1cnJlbnRJbnN0YWxsYXRpb24SUwoHSW5zdGFsbBIkLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxSZXF1ZXN0GiIuaG9zdC5pbnN0YWxsYXRpb24udjEuSW5zdGFsbGF0aW9uElQKBlJldmlldxIjLmhvc3QuaW5zdGFsbGF0aW9uLnYxLlJldmlld1JlcXVlc3QaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5Db25zZW50QXBwcm92YWwSVwoJR3JhbnRSb2xlEiYuaG9zdC5pbnN0YWxsYXRpb24udjEuR3JhbnRSb2xlUmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkluc3RhbGxhdGlvbhJZCgpSZXZva2VSb2xlEicuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2b2tlUm9sZVJlcXVlc3QaIi5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb24SawoOTGlzdFJvbGVHcmFudHMSKy5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1JlcXVlc3QaLC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0Um9sZUdyYW50c1Jlc3BvbnNlEk0KBExpc3QSIS5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0UmVxdWVzdBoiLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RSZXNwb25zZRJZCghMaXN0SGVsZBIlLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVxdWVzdBomLmhvc3QuaW5zdGFsbGF0aW9uLnYxLkxpc3RIZWxkUmVzcG9uc2UycAoSSW5zdGFsbGFibGVTZXJ2aWNlEloKB0luc3RhbGwSKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25Ob3RpY2UaJS5ob3N0Lmluc3RhbGxhdGlvbi52MS5JbnN0YWxsYXRpb25BY2syyAEKDE9mZmVyU2VydmljZRJfCgpMaXN0T2ZmZXJzEicuaG9zdC5pbnN0YWxsYXRpb24udjEuTGlzdE9mZmVyc1JlcXVlc3QaKC5ob3N0Lmluc3RhbGxhdGlvbi52MS5MaXN0T2ZmZXJzUmVzcG9uc2USVwoLUmV2aWV3T2ZmZXISIy5ob3N0Lmluc3RhbGxhdGlvbi52MS5SZXZpZXdSZXF1ZXN0GiMuaG9zdC5pbnN0YWxsYXRpb24udjEuUmV2aWV3ZWRPZmZlckLVAQoYY29tLmhvc3QuaW5zdGFsbGF0aW9uLnYxQhFJbnN0YWxsYXRpb25Qcm90b1ABWjRhY2NvdW50cy9wa2cvZ2VuL2hvc3QvaW5zdGFsbGF0aW9uL3YxO2luc3RhbGxhdGlvbnYxogIDSElYqgIUSG9zdC5JbnN0YWxsYXRpb24uVjHKAhRIb3N0XEluc3RhbGxhdGlvblxWMeICIEhvc3RcSW5zdGFsbGF0aW9uXFYxXEdQQk1ldGFkYXRh6gIWSG9zdDo6SW5zdGFsbGF0aW9uOjpWMWIGcHJvdG8z"); /** * @generated from message host.installation.v1.CurrentInstallationRequest @@ -884,6 +884,78 @@ export type OfferTool = Message<"host.installation.v1.OfferTool"> & { export const OfferToolSchema: GenMessage = /*@__PURE__*/ messageDesc(file_host_installation_v1_installation, 23); +/** + * ListHeldRequest names the organisation the person reads at: the one their + * session was opened at. + * + * @generated from message host.installation.v1.ListHeldRequest + */ +export type ListHeldRequest = Message<"host.installation.v1.ListHeldRequest"> & { + /** + * The organisation. + * + * @generated from field: string organization_id = 1; + */ + organizationId: string; +}; + +/** + * Describes the message host.installation.v1.ListHeldRequest. + * Use `create(ListHeldRequestSchema)` to create a new message. + */ +export const ListHeldRequestSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 24); + +/** + * ListHeldResponse is every installation the person holds a role on, sorted + * by installation id. + * + * @generated from message host.installation.v1.ListHeldResponse + */ +export type ListHeldResponse = Message<"host.installation.v1.ListHeldResponse"> & { + /** + * The installations. + * + * @generated from field: repeated host.installation.v1.HeldInstallation installations = 1; + */ + installations: HeldInstallation[]; +}; + +/** + * Describes the message host.installation.v1.ListHeldResponse. + * Use `create(ListHeldResponseSchema)` to create a new message. + */ +export const ListHeldResponseSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 25); + +/** + * HeldInstallation is one installation the person may use. + * + * @generated from message host.installation.v1.HeldInstallation + */ +export type HeldInstallation = Message<"host.installation.v1.HeldInstallation"> & { + /** + * The installation. + * + * @generated from field: host.installation.v1.Installation installation = 1; + */ + installation?: Installation; + + /** + * The roles the person holds on it, directly or through a team, sorted. + * + * @generated from field: repeated string roles = 2; + */ + roles: string[]; +}; + +/** + * Describes the message host.installation.v1.HeldInstallation. + * Use `create(HeldInstallationSchema)` to create a new message. + */ +export const HeldInstallationSchema: GenMessage = /*@__PURE__*/ + messageDesc(file_host_installation_v1_installation, 26); + /** * InstallationService is implemented by the host and by the local authority. * @@ -959,9 +1031,8 @@ export const InstallationService: GenService<{ output: typeof ListRoleGrantsResponseSchema; }, /** - * List answers what one organisation has installed: an administrator's read. A - * member's view, which installed solutions they may use, is their navigation and - * belongs to the entitlement read, not here. + * List answers what one organisation has installed: an administrator's read. + * A member's view, which installed solutions they may use, is ListHeld. * * @generated from rpc host.installation.v1.InstallationService.List */ @@ -970,6 +1041,21 @@ export const InstallationService: GenService<{ input: typeof ListRequestSchema; output: typeof ListResponseSchema; }, + /** + * ListHeld answers the signed-in person which installations of their + * organisation they may use: each active installation on which they hold a + * role, granted to them or to a team of theirs, with the roles they hold. + * It is their navigation and answers their own holdings alone, never who + * else holds what. A member holding nothing reads an empty list; a caller + * acting at another organisation is refused naming it. + * + * @generated from rpc host.installation.v1.InstallationService.ListHeld + */ + listHeld: { + methodKind: "unary"; + input: typeof ListHeldRequestSchema; + output: typeof ListHeldResponseSchema; + }, }> = /*@__PURE__*/ serviceDesc(file_host_installation_v1_installation, 0); diff --git a/module/services/frontend/code/src/gen/host/session/v1/session_pb.ts b/module/services/frontend/code/src/gen/host/session/v1/session_pb.ts index 5ebfd8a55..1da433663 100644 --- a/module/services/frontend/code/src/gen/host/session/v1/session_pb.ts +++ b/module/services/frontend/code/src/gen/host/session/v1/session_pb.ts @@ -16,7 +16,7 @@ import type { Message } from "@bufbuild/protobuf"; * Describes the file host/session/v1/session.proto. */ export const file_host_session_v1_session: GenFile = /*@__PURE__*/ - fileDesc("Ch1ob3N0L3Nlc3Npb24vdjEvc2Vzc2lvbi5wcm90bxIPaG9zdC5zZXNzaW9uLnYxIhoKGExpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdCJLChlMaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlEi4KBnBlb3BsZRgBIAMoCzIeLmhvc3Quc2Vzc2lvbi52MS5GaXh0dXJlUGVyc29uIoIBCg1GaXh0dXJlUGVyc29uEhEKCXBlcnNvbl9pZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSFQoNZml4dHVyZV90b2tlbhgGIAEoCSIQCg5DdXJyZW50UmVxdWVzdCImCg1TaWduSW5SZXF1ZXN0EhUKDWZpeHR1cmVfdG9rZW4YASABKAkiugEKB1Nlc3Npb24SEgoKc2Vzc2lvbl9pZBgBIAEoCRIRCglwZXJzb25faWQYAiABKAkSFwoPb3JnYW5pemF0aW9uX2lkGAMgASgJEg0KBXJvbGVzGAQgAygJEhAKCHRlYW1faWRzGAUgAygJEg4KBmJlYXJlchgGIAEoCRIbChNwZXJzb25fZGlzcGxheV9uYW1lGAcgASgJEiEKGW9yZ2FuaXphdGlvbl9kaXNwbGF5X25hbWUYCCABKAky4AIKDlNlc3Npb25TZXJ2aWNlEmIKBlNpZ25JbhIeLmhvc3Quc2Vzc2lvbi52MS5TaWduSW5SZXF1ZXN0GhguaG9zdC5zZXNzaW9uLnYxLlNlc3Npb24iHoLT5JMCGDoBKiITL3YxL3Nlc3Npb246c2lnbi1pbhJZCgdDdXJyZW50Eh8uaG9zdC5zZXNzaW9uLnYxLkN1cnJlbnRSZXF1ZXN0GhguaG9zdC5zZXNzaW9uLnYxLlNlc3Npb24iE4LT5JMCDRILL3YxL3Nlc3Npb24SjgEKEUxpc3RGaXh0dXJlUGVvcGxlEikuaG9zdC5zZXNzaW9uLnYxLkxpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdBoqLmhvc3Quc2Vzc2lvbi52MS5MaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlIiKC0+STAhwSGi92MS9zZXNzaW9uL2ZpeHR1cmUtcGVvcGxlQq0BChNjb20uaG9zdC5zZXNzaW9uLnYxQgxTZXNzaW9uUHJvdG9QAVoqYWNjb3VudHMvcGtnL2dlbi9ob3N0L3Nlc3Npb24vdjE7c2Vzc2lvbnYxogIDSFNYqgIPSG9zdC5TZXNzaW9uLlYxygIPSG9zdFxTZXNzaW9uXFYx4gIbSG9zdFxTZXNzaW9uXFYxXEdQQk1ldGFkYXRh6gIRSG9zdDo6U2Vzc2lvbjo6VjFiBnByb3RvMw", [file_google_api_annotations]); + fileDesc("Ch1ob3N0L3Nlc3Npb24vdjEvc2Vzc2lvbi5wcm90bxIPaG9zdC5zZXNzaW9uLnYxIhoKGExpc3RGaXh0dXJlUGVvcGxlUmVxdWVzdCJLChlMaXN0Rml4dHVyZVBlb3BsZVJlc3BvbnNlEi4KBnBlb3BsZRgBIAMoCzIeLmhvc3Quc2Vzc2lvbi52MS5GaXh0dXJlUGVyc29uIqUBCg1GaXh0dXJlUGVyc29uEhEKCXBlcnNvbl9pZBgBIAEoCRINCgVlbWFpbBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSFQoNZml4dHVyZV90b2tlbhgGIAEoCRIhChlvcmdhbml6YXRpb25fZGlzcGxheV9uYW1lGAcgASgJIhAKDkN1cnJlbnRSZXF1ZXN0IiYKDVNpZ25JblJlcXVlc3QSFQoNZml4dHVyZV90b2tlbhgBIAEoCSK6AQoHU2Vzc2lvbhISCgpzZXNzaW9uX2lkGAEgASgJEhEKCXBlcnNvbl9pZBgCIAEoCRIXCg9vcmdhbml6YXRpb25faWQYAyABKAkSDQoFcm9sZXMYBCADKAkSEAoIdGVhbV9pZHMYBSADKAkSDgoGYmVhcmVyGAYgASgJEhsKE3BlcnNvbl9kaXNwbGF5X25hbWUYByABKAkSIQoZb3JnYW5pemF0aW9uX2Rpc3BsYXlfbmFtZRgIIAEoCTLgAgoOU2Vzc2lvblNlcnZpY2USYgoGU2lnbkluEh4uaG9zdC5zZXNzaW9uLnYxLlNpZ25JblJlcXVlc3QaGC5ob3N0LnNlc3Npb24udjEuU2Vzc2lvbiIegtPkkwIYOgEqIhMvdjEvc2Vzc2lvbjpzaWduLWluElkKB0N1cnJlbnQSHy5ob3N0LnNlc3Npb24udjEuQ3VycmVudFJlcXVlc3QaGC5ob3N0LnNlc3Npb24udjEuU2Vzc2lvbiITgtPkkwINEgsvdjEvc2Vzc2lvbhKOAQoRTGlzdEZpeHR1cmVQZW9wbGUSKS5ob3N0LnNlc3Npb24udjEuTGlzdEZpeHR1cmVQZW9wbGVSZXF1ZXN0GiouaG9zdC5zZXNzaW9uLnYxLkxpc3RGaXh0dXJlUGVvcGxlUmVzcG9uc2UiIoLT5JMCHBIaL3YxL3Nlc3Npb24vZml4dHVyZS1wZW9wbGVCrQEKE2NvbS5ob3N0LnNlc3Npb24udjFCDFNlc3Npb25Qcm90b1ABWiphY2NvdW50cy9wa2cvZ2VuL2hvc3Qvc2Vzc2lvbi92MTtzZXNzaW9udjGiAgNIU1iqAg9Ib3N0LlNlc3Npb24uVjHKAg9Ib3N0XFNlc3Npb25cVjHiAhtIb3N0XFNlc3Npb25cVjFcR1BCTWV0YWRhdGHqAhFIb3N0OjpTZXNzaW9uOjpWMWIGcHJvdG8z", [file_google_api_annotations]); /** * ListFixturePeopleRequest asks for every fixture person. @@ -101,6 +101,14 @@ export type FixturePerson = Message<"host.session.v1.FixturePerson"> & { * @generated from field: string fixture_token = 6; */ fixtureToken: string; + + /** + * The organisation's name, as the fixture spells it, for a page that shows + * a person where they sign into; the id above is the identity. + * + * @generated from field: string organization_display_name = 7; + */ + organizationDisplayName: string; }; /**