mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 12:33:16 -05:00
With authentication off every caller that reaches the port is the same local owner, and the checks that tell a person from a script are headers the caller chooses. Any process or agent that could reach the port could grant host operations, set deny rules and turn the blanket approval on. UI host grants are now locked by default in that mode. The policy file and flags still grant, and every write route answers 403 locked_by_deployment with a sentence that says why. The overview and the preview carry the lock and its reason for the page. The new flag --addon-host-ui-grants-without-auth turns the UI grants on knowingly and logs a startup warning. With sign-in on nothing changes. Assisted-by: Claude:claude-sonnet-5-5 go gofmt
431 lines
21 KiB
Go
431 lines
21 KiB
Go
// SPDX-License-Identifier: MIT
|
|
package application
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/core/config"
|
|
"github.com/mudler/LocalAI/core/http/auth"
|
|
"github.com/mudler/LocalAI/core/services/addons"
|
|
"github.com/mudler/LocalAI/core/services/addons/catalog"
|
|
"github.com/mudler/LocalAI/core/services/addons/gateway"
|
|
"github.com/mudler/LocalAI/core/services/addons/hostops"
|
|
"github.com/mudler/LocalAI/pkg/addons/contract"
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
)
|
|
|
|
// These specs run the owner the way local-ai run does, install a package
|
|
// through the real host access service and ask the Authority through a broker.
|
|
var _ = Describe("host grants on the owner", func() {
|
|
var (
|
|
ownerData string
|
|
keys string
|
|
port string
|
|
sourceURL string
|
|
digests map[string]string
|
|
actor = contract.Actor{UserID: auth.AddonLocalOwnerID, CredentialID: auth.AddonLocalOwnerID, Mode: auth.AddonModeLocalOwner}
|
|
)
|
|
|
|
pack := func(version string, required, optional []string) (data []byte) {
|
|
manifest, e := os.ReadFile("../../tests/addons/fixtures/packages/manifest.json")
|
|
ExpectWithOffset(1, e).NotTo(HaveOccurred())
|
|
var m map[string]any
|
|
ExpectWithOffset(1, json.Unmarshal(manifest, &m)).To(Succeed())
|
|
m["version"] = version
|
|
m["capabilities"] = map[string]any{"service": true, "hostOperations": map[string]any{"required": required, "optional": optional}}
|
|
manifest, _ = json.Marshal(m)
|
|
var b bytes.Buffer
|
|
tw := tar.NewWriter(&b)
|
|
for name, content := range map[string][]byte{"manifest.json": manifest, "bin/sample": []byte("binary " + version)} {
|
|
ExpectWithOffset(1, tw.WriteHeader(&tar.Header{Name: name, Mode: 0755, Size: int64(len(content))})).To(Succeed())
|
|
_, e = tw.Write(content)
|
|
ExpectWithOffset(1, e).NotTo(HaveOccurred())
|
|
}
|
|
ExpectWithOffset(1, tw.Close()).To(Succeed())
|
|
return b.Bytes()
|
|
}
|
|
|
|
BeforeEach(func() {
|
|
requireAddonRuntime()
|
|
keys = filepath.Join(realTempDir(), "keys")
|
|
Expect(gateway.GenerateCarrierKeys(keys, []string{"127.0.0.1"})).To(Succeed())
|
|
ownerData = realTempDir()
|
|
l, e := net.Listen("tcp", "127.0.0.1:0")
|
|
Expect(e).NotTo(HaveOccurred())
|
|
port = strconv.Itoa(l.Addr().(*net.TCPAddr).Port)
|
|
Expect(l.Close()).To(Succeed())
|
|
|
|
archives := map[string][]byte{
|
|
"/1.2.3.tar": pack("1.2.3", []string{"workspace.materialize"}, []string{"egress.check"}),
|
|
"/1.3.0.tar": pack("1.3.0", []string{"workspace.materialize", "egress.audit"}, []string{"egress.check"}),
|
|
}
|
|
digests = map[string]string{}
|
|
for path, data := range archives {
|
|
digests[path] = fmt.Sprintf("sha256:%x", sha256.Sum256(data))
|
|
}
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if data, ok := archives[r.URL.Path]; ok {
|
|
_, _ = w.Write(data)
|
|
return
|
|
}
|
|
rel := func(v string) catalog.Release {
|
|
return catalog.Release{ID: "sample", Name: "Sample", Version: v, OS: "linux", Arch: "amd64", Artifact: sourceURL + "/" + v + ".tar", Digest: digests["/"+v+".tar"]}
|
|
}
|
|
_ = catalog.WriteIndex(w, []catalog.Release{rel("1.2.3"), rel("1.3.0")})
|
|
}))
|
|
DeferCleanup(server.Close)
|
|
sourceURL = server.URL
|
|
root := filepath.Join(ownerData, "addons")
|
|
DeferCleanup(func() {
|
|
_ = filepath.Walk(root, func(p string, i os.FileInfo, e error) error {
|
|
if e == nil && i.IsDir() {
|
|
_ = os.Chmod(p, 0700)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
Expect(os.MkdirAll(root, 0700)).To(Succeed())
|
|
sources, e := catalog.NewSources(filepath.Join(root, "sources.json"), func(context.Context, contract.Actor) error { return nil }, catalog.CredentialResolver{})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(sources.Save(context.Background(), actor, catalog.Source{ID: "test", Name: "Test", URL: sourceURL, Enabled: true, AllowLoopbackHTTP: true})).To(Succeed())
|
|
})
|
|
|
|
// startOwner builds the owner on the same data directory. Each start
|
|
// stands for one run of the process.
|
|
startOwner := func(opts ...func(*config.ApplicationConfig)) (*Application, func()) {
|
|
transport, e := gateway.LoadCarrierConfig(keys, "owner", "https://127.0.0.1:"+port)
|
|
Expect(e).NotTo(HaveOccurred())
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
// The suite runs in local-owner mode, where UI grants are locked unless the
|
|
// administrator turns them on knowingly. Most specs test the grants.
|
|
cfg := &config.ApplicationConfig{Context: ctx, DataPath: ownerData, AddonHostUIGrantsWithoutAuth: true}
|
|
for _, o := range opts {
|
|
o(cfg)
|
|
}
|
|
cfg.Addons = config.AddonConfig{
|
|
Enabled: true,
|
|
InstanceID: transport.InstanceID, OwnerID: gateway.OwnerIdentity, OwnerAddress: "https://127.0.0.1:" + port,
|
|
StorageID: "local", Root: filepath.Join(ownerData, "addons"), Transport: transport, OwnershipMode: auth.AddonModeLocalOwner,
|
|
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"},
|
|
ListenAddress: "127.0.0.1:" + port,
|
|
}
|
|
a := &Application{applicationConfig: cfg}
|
|
stopped := false
|
|
stop := func() {
|
|
if !stopped {
|
|
stopped = true
|
|
cancel()
|
|
_ = a.Shutdown()
|
|
}
|
|
}
|
|
if e = a.startAddons(); e != nil {
|
|
stop()
|
|
Fail("owner did not start: " + e.Error())
|
|
}
|
|
DeferCleanup(stop)
|
|
return a, stop
|
|
}
|
|
|
|
human := func() context.Context { return gateway.WithHumanSurface(context.Background()) }
|
|
call := func(a *Application, c gateway.HostGrantsCall) (map[string]any, error) {
|
|
raw, e := a.AddonGateway().HostGrants(human(), actor, c)
|
|
if e != nil {
|
|
return nil, e
|
|
}
|
|
out := map[string]any{}
|
|
ExpectWithOffset(1, json.Unmarshal(raw, &out)).To(Succeed())
|
|
return out, nil
|
|
}
|
|
body := func(v any) json.RawMessage {
|
|
b, e := json.Marshal(v)
|
|
ExpectWithOffset(1, e).NotTo(HaveOccurred())
|
|
return b
|
|
}
|
|
revision := func(a *Application) uint64 {
|
|
out, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
ExpectWithOffset(1, e).NotTo(HaveOccurred())
|
|
return uint64(out["revision"].(float64))
|
|
}
|
|
// install previews a release and installs it with the approval of ops.
|
|
install := func(a *Application, opID, action, version string, ops []string, expected string) error {
|
|
pv, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsPreview, Body: body(gateway.HostGrantsPreviewRequest{SourceID: "test", AddonID: "sample", Version: version})})
|
|
ExpectWithOffset(1, e).NotTo(HaveOccurred())
|
|
req := gateway.HostGrantsInstallRequest{OperationID: opID, Action: action, SourceID: "test", AddonID: "sample", Version: version,
|
|
Approve: gateway.HostGrantsApprove{Digest: pv["digest"].(string), Ops: ops, Revision: uint64(pv["revision"].(float64)), Nonce: pv["nonce"].(string)}}
|
|
if expected != "" {
|
|
req.ExpectedDigest, req.CandidateDigest = expected, pv["digest"].(string)
|
|
}
|
|
_, e = call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsInstall, OperationID: opID, Body: body(req)})
|
|
return e
|
|
}
|
|
// ask makes a host operation call the way the broker serves it, with the
|
|
// Authority of the application.
|
|
ask := func(a *Application, op string) int {
|
|
in := hostops.NewInFlight()
|
|
b := hostops.New(in, func(string, string) bool { return true }, contract.DefaultLimits())
|
|
ExpectWithOffset(1, b.Register(hostops.Spec{Op: op, Scope: "read", Feature: "x", Handler: func(context.Context, hostops.Call) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"ok":true}`), nil
|
|
}})).To(Succeed())
|
|
h := b.HandlerForRuntime("sample", "secret", a.addonAuthority(), 0, "")
|
|
end := in.Begin(contract.Envelope{
|
|
Request: contract.Request{AddonID: "sample", OperationID: "op1", Generation: 1, Action: "start", Arguments: json.RawMessage(`{}`)},
|
|
Authority: contract.Authority{Actor: contract.Actor{UserID: "u1", CredentialID: "c1", Mode: "named-user"}, Scope: "read", ExpiresAt: time.Now().Add(time.Minute)},
|
|
})
|
|
defer end()
|
|
raw, _ := json.Marshal(contract.HostRequest{OperationID: "op1", Op: op, Arguments: json.RawMessage(`{}`)})
|
|
req := httptest.NewRequest(http.MethodPost, "/host", bytes.NewReader(raw))
|
|
req.Header.Set("X-Addon-Host-Credential", "secret")
|
|
rec := httptest.NewRecorder()
|
|
h.ServeHTTP(rec, req)
|
|
return rec.Code
|
|
}
|
|
submit := func(a *Application, opID, action, version, expected, candidate string) error {
|
|
_, e := a.addonManager.Submit(context.Background(), actor, addons.LifecycleCommand{OperationID: opID, AddonID: "sample", Action: action, SourceID: "test", Version: version, ExpectedDigest: expected, CandidateDigest: candidate})
|
|
return e
|
|
}
|
|
|
|
It("grants at install, revokes and denies without a restart, and keeps the grants across restarts", func() {
|
|
a, stop := startOwner()
|
|
Expect(a.AddonGateway().HostGrantsOwner).NotTo(BeNil())
|
|
|
|
// Nothing approved: the install is refused and says what to approve.
|
|
pv, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsPreview, Body: body(gateway.HostGrantsPreviewRequest{SourceID: "test", AddonID: "sample", Version: "1.2.3"})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(pv["canInstall"]).To(BeTrue())
|
|
Expect(install(a, "i-none", "install", "1.2.3", nil, "")).To(HaveOccurred())
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusForbidden))
|
|
|
|
// Approved: the required operation works, the unticked optional one does not.
|
|
Expect(install(a, "i-1", "install", "1.2.3", []string{"workspace.materialize"}, "")).To(Succeed())
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusForbidden))
|
|
Expect(a.addonAuthority().Snapshot("sample").Operations).To(Equal([]string{"workspace.materialize"}))
|
|
|
|
// Revoke: the very next call is refused, with no restart.
|
|
_, e = call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsRevoke, AddonID: "sample", OperationID: "r-1", Body: body(gateway.HostGrantsRevokeRequest{OperationID: "r-1", Revision: revision(a)})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusForbidden))
|
|
|
|
// Edit gives it back; a dry run of a deny names who loses what and writes nothing.
|
|
_, e = call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsEdit, AddonID: "sample", OperationID: "e-1", Body: body(gateway.HostGrantsEditRequest{OperationID: "e-1", Revision: revision(a), Ops: []string{"workspace.materialize", "egress.check"}})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusOK))
|
|
before := revision(a)
|
|
dry, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsDenyPreview, Body: body(gateway.HostGrantsDenyRequest{Revision: before, Deny: map[string][]string{"sample": {"egress."}}, DryRun: true})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(dry["affects"]).NotTo(BeEmpty())
|
|
Expect(revision(a)).To(Equal(before))
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusOK))
|
|
|
|
// A deny set in the UI beats the grant at once; removing it restores the grant.
|
|
_, e = call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsDeny, OperationID: "d-1", Body: body(gateway.HostGrantsDenyRequest{OperationID: "d-1", Revision: before, Deny: map[string][]string{"sample": {"egress."}}})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusForbidden))
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
_, e = call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsDeny, OperationID: "d-2", Body: body(gateway.HostGrantsDenyRequest{OperationID: "d-2", Revision: revision(a), Deny: map[string][]string{}})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusOK))
|
|
|
|
// The audit file records the changes and names no path or secret.
|
|
audit, e := os.ReadFile(filepath.Join(ownerData, "addons", addonHostGrantsAudit))
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(string(audit)).To(ContainSubstring(`"event":"grant"`))
|
|
Expect(string(audit)).To(ContainSubstring(`"event":"revoke"`))
|
|
Expect(string(audit)).NotTo(ContainSubstring(ownerData))
|
|
|
|
// A restart reopens the document: the grants are still there.
|
|
stop()
|
|
again, _ := startOwner()
|
|
Expect(ask(again, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
Expect(ask(again, "egress.check")).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("lets a deny of the deployment beat a grant of the UI", func() {
|
|
a, stop := startOwner()
|
|
Expect(install(a, "i-1", "install", "1.2.3", []string{"workspace.materialize"}, "")).To(Succeed())
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
stop()
|
|
denied, stopDenied := startOwner(func(c *config.ApplicationConfig) {
|
|
c.AddonHostDeny = map[string][]string{"*": {"workspace."}}
|
|
})
|
|
Expect(ask(denied, "workspace.materialize")).To(Equal(http.StatusForbidden))
|
|
stopDenied()
|
|
// The grant was not lost: without the deny it works again.
|
|
last, _ := startOwner()
|
|
Expect(ask(last, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("refuses everything when the document cannot be read", func() {
|
|
Expect(os.MkdirAll(filepath.Join(ownerData, "addons"), 0700)).To(Succeed())
|
|
Expect(os.WriteFile(filepath.Join(ownerData, "addons", addonHostGrantsFile), []byte("{not json"), 0600)).To(Succeed())
|
|
a, _ := startOwner(func(c *config.ApplicationConfig) {
|
|
c.AddonHostOps = map[string][]string{"sample": {"workspace."}}
|
|
})
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusForbidden), "the deny rules in the document are unknown")
|
|
Expect(a.AddonHostGrantsStore()).To(BeNil())
|
|
})
|
|
|
|
It("holds an upgrade that asks for more and keeps the old version running", func() {
|
|
a, _ := startOwner()
|
|
Expect(install(a, "i-1", "install", "1.2.3", []string{"workspace.materialize"}, "")).To(Succeed())
|
|
old := digests["/1.2.3.tar"]
|
|
newer := digests["/1.3.0.tar"]
|
|
// Without an approval the upgrade is refused and nothing changes.
|
|
e := submit(a, "u-0", "upgrade", "1.3.0", old, newer)
|
|
var refusal *addons.HostRequirementsRefusal
|
|
Expect(e).To(HaveOccurred())
|
|
Expect(errors.As(e, &refusal)).To(BeTrue(), e.Error())
|
|
st, e := a.addonManager.Status(context.Background(), "sample")
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(st.Generation).To(Equal(uint64(1)))
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK), "the old version keeps its grant")
|
|
Expect(ask(a, "egress.audit")).To(Equal(http.StatusForbidden))
|
|
|
|
// Approved in the browser: the new operation counts once the upgrade completed.
|
|
Expect(install(a, "u-1", "upgrade", "1.3.0", []string{"egress.audit"}, old)).To(Succeed())
|
|
st, _ = a.addonManager.Status(context.Background(), "sample")
|
|
Expect(st.Generation).To(Equal(uint64(2)))
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
Expect(ask(a, "egress.audit")).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("applies the blanket mode a deployment fixes without a dialog, and records it", func() {
|
|
a, _ := startOwner(func(c *config.ApplicationConfig) { c.AddonHostBlanket = "any" })
|
|
Expect(submit(a, "b-1", "install", "1.2.3", "", "")).To(Succeed())
|
|
// Required operations are granted; the optional one needs includeOptional.
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
Expect(ask(a, "egress.check")).To(Equal(http.StatusForbidden))
|
|
audit, e := os.ReadFile(filepath.Join(ownerData, "addons", addonHostGrantsAudit))
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(string(audit)).To(ContainSubstring(`"event":"blanket-grant"`))
|
|
Expect(string(audit)).To(ContainSubstring(`"blanket":"any@rev0"`))
|
|
})
|
|
|
|
It("never copies a blanket mode a deployment fixes into the document", func() {
|
|
a, stop := startOwner(func(c *config.ApplicationConfig) { c.AddonHostBlanket = "any" })
|
|
Expect(submit(a, "b-1", "install", "1.2.3", "", "")).To(Succeed())
|
|
out, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
blanket := out["blanket"].(map[string]any)
|
|
Expect(blanket["mode"]).To(Equal("any"))
|
|
Expect(blanket["setBy"]).To(Equal("deployment"))
|
|
doc, e := os.ReadFile(filepath.Join(ownerData, "addons", addonHostGrantsFile))
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(string(doc)).NotTo(ContainSubstring(`"any"`), "the deployment value must not reach the document")
|
|
|
|
// Restart without the flag: the switch is what a person set, which is off.
|
|
stop()
|
|
again, stopAgain := startOwner()
|
|
out, e = call(again, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
blanket = out["blanket"].(map[string]any)
|
|
Expect(blanket["mode"]).To(Equal("off"))
|
|
Expect(blanket["setBy"]).To(Equal("ui"))
|
|
// An upgrade that needs more is held again, as nobody typed the confirmation.
|
|
e = submit(again, "u-0", "upgrade", "1.3.0", digests["/1.2.3.tar"], digests["/1.3.0.tar"])
|
|
var refusal *addons.HostRequirementsRefusal
|
|
Expect(errors.As(e, &refusal)).To(BeTrue(), fmt.Sprint(e))
|
|
stopAgain()
|
|
|
|
// A flag set again takes effect with no stored copy.
|
|
fixed, _ := startOwner(func(c *config.ApplicationConfig) { c.AddonHostBlanket = "any" })
|
|
Expect(submit(fixed, "u-1", "upgrade", "1.3.0", digests["/1.2.3.tar"], digests["/1.3.0.tar"])).To(Succeed())
|
|
Expect(ask(fixed, "egress.audit")).To(Equal(http.StatusOK))
|
|
})
|
|
|
|
It("refuses an install and leaves the document unchanged when the audit line cannot be written", func() {
|
|
a, _ := startOwner()
|
|
before := revision(a)
|
|
Expect(a.addonGrants.audit.Close()).To(Succeed())
|
|
Expect(install(a, "i-1", "install", "1.2.3", []string{"workspace.materialize"}, "")).NotTo(Succeed())
|
|
Expect(revision(a)).To(Equal(before))
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusForbidden))
|
|
st, e := a.addonManager.Status(context.Background(), "sample")
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(st.Installed).To(BeFalse())
|
|
})
|
|
|
|
It("locks the UI grants while sign-in is off, and opens them only when the administrator says so", func() {
|
|
a, stop := startOwner(func(c *config.ApplicationConfig) {
|
|
c.AddonHostUIGrantsWithoutAuth = false
|
|
c.AddonHostOps = map[string][]string{"sample": {"workspace."}}
|
|
})
|
|
out, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(out["locked"]).To(BeTrue())
|
|
Expect(out["lockedReason"]).To(Equal(gateway.LockedNoAuth))
|
|
// Every write is refused with the typed code, and nothing is stored.
|
|
before := revision(a)
|
|
for _, c := range []gateway.HostGrantsCall{
|
|
{Op: gateway.HostGrantsEdit, AddonID: "sample", OperationID: "e-1", Body: body(gateway.HostGrantsEditRequest{OperationID: "e-1", Ops: []string{"egress.check"}})},
|
|
{Op: gateway.HostGrantsRevoke, AddonID: "sample", OperationID: "r-1", Body: body(gateway.HostGrantsRevokeRequest{OperationID: "r-1"})},
|
|
{Op: gateway.HostGrantsDeny, OperationID: "d-1", Body: body(gateway.HostGrantsDenyRequest{OperationID: "d-1", Deny: map[string][]string{"sample": {"egress."}}})},
|
|
{Op: gateway.HostGrantsBlanket, OperationID: "b-1", Body: body(gateway.HostGrantsBlanketRequest{OperationID: "b-1", Mode: "any", Confirm: gateway.BlanketConfirmAny})},
|
|
} {
|
|
_, e = call(a, c)
|
|
var he *gateway.HostGrantsError
|
|
Expect(errors.As(e, &he)).To(BeTrue(), c.Op)
|
|
Expect(he.Type).To(Equal(gateway.HostGrantsLocked), c.Op)
|
|
Expect(he.Message).To(ContainSubstring("Sign-in is off"))
|
|
}
|
|
Expect(revision(a)).To(Equal(before))
|
|
// The preview tells the install dialog, and the policy file still grants.
|
|
pv, e := call(a, gateway.HostGrantsCall{Op: gateway.HostGrantsPreview, Body: body(gateway.HostGrantsPreviewRequest{SourceID: "test", AddonID: "sample", Version: "1.2.3"})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(pv["locked"]).To(BeTrue())
|
|
Expect(submit(a, "i-1", "install", "1.2.3", "", "")).To(Succeed())
|
|
Expect(ask(a, "workspace.materialize")).To(Equal(http.StatusOK))
|
|
stop()
|
|
|
|
// Turned on knowingly, the same server accepts the write.
|
|
open, _ := startOwner()
|
|
out, e = call(open, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(out["locked"]).To(BeFalse())
|
|
_, e = call(open, gateway.HostGrantsCall{Op: gateway.HostGrantsEdit, AddonID: "sample", OperationID: "e-2", Body: body(gateway.HostGrantsEditRequest{OperationID: "e-2", Revision: revision(open), Ops: []string{"egress.check"}})})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
})
|
|
|
|
It("keeps the grants of a split deployment on the owner only", func() {
|
|
a, _ := startOwner()
|
|
Expect(install(a, "i-1", "install", "1.2.3", []string{"workspace.materialize"}, "")).To(Succeed())
|
|
transport, e := gateway.LoadCarrierConfig(keys, "frontend", "https://127.0.0.1:"+port)
|
|
Expect(e).NotTo(HaveOccurred())
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
DeferCleanup(cancel)
|
|
frontendData := realTempDir()
|
|
f := &Application{applicationConfig: &config.ApplicationConfig{Context: ctx, DataPath: frontendData,
|
|
AddonHostOps: map[string][]string{"sample": {"egress."}}}}
|
|
f.applicationConfig.Addons = config.AddonConfig{Enabled: true, InstanceID: transport.InstanceID, OwnerID: gateway.OwnerIdentity,
|
|
OwnerAddress: "https://127.0.0.1:" + port, StorageID: "local", Transport: transport, OwnershipMode: auth.AddonModeLocalOwner,
|
|
Host: contract.HostCompatibility{Host: 1, API: contract.HostAPIVersion, Protocol: 1, SDK: 1, DataFormat: 1, OS: "linux", Arch: "amd64"}}
|
|
Expect(f.startAddons()).To(Succeed())
|
|
DeferCleanup(func() { _ = f.Shutdown() })
|
|
// The frontend stores nothing and enforces only its own floor, never the grants of the owner.
|
|
Expect(f.AddonHostGrantsStore()).To(BeNil())
|
|
Expect(f.AddonGateway().HostGrantsOwner).To(BeNil())
|
|
Expect(ask(f, "workspace.materialize")).To(Equal(http.StatusForbidden))
|
|
entries, e := os.ReadDir(frontendData)
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(entries).To(BeEmpty())
|
|
// Reading the owner's grants through the frontend works, and writes forward.
|
|
out, e := call(f, gateway.HostGrantsCall{Op: gateway.HostGrantsRead})
|
|
Expect(e).NotTo(HaveOccurred())
|
|
Expect(out["revision"]).NotTo(BeZero())
|
|
})
|
|
})
|