oficina / home Public

Allow an operator configured Docs preview launch from Home

Paul Campbell pushed by paul@keymail.dev 3d040335b7c2b814f016dd6d52551d8672b42a99 parent 32b33d1
5 files changed, +92 −14
  • internal/home/directory.go +2 −2
  • internal/home/directory_catalog.go +17 −2
  • internal/home/directory_catalog_test.go +58 −0
  • internal/home/runtime.go +14 −9
  • internal/home/runtime_test.go +1 −1

CI cancelled — superseded by a newer push — run details

diff --git a/internal/home/directory.go b/internal/home/directory.go
index 6f1a734..a84aeac 100644
--- a/internal/home/directory.go
+++ b/internal/home/directory.go
@@ -17,7 +17,7 @@ import (
)
func newDirectory(cfg Runtime, d *db.DB, logger *slog.Logger) (*App, error) {
- if cfg.role != "directory" || cfg.origins.validate() != nil || cfg.secrets.Sealer == nil || d == nil || cfg.signer != nil || !cfg.roster.valid(cfg.role, cfg.keys) {
+ if !cfg.validDocsPreview() || cfg.role != "directory" || cfg.origins.validate() != nil || cfg.secrets.Sealer == nil || d == nil || cfg.signer != nil || !cfg.roster.valid(cfg.role, cfg.keys) {
return nil, errExchangeConfig
}
if logger == nil {
@@ -79,7 +79,7 @@ func (a *App) directoryRouter() http.Handler {
})
r.With(a.sessions.Require).Get("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
- a.render(w, 200, "directory.html", map[string]any{"Apps": directoryCatalog()})
+ a.render(w, 200, "directory.html", map[string]any{"Apps": directoryCatalog(a.runtime.docsPreviewOrigin)})
})
r.Post("/signout", func(w http.ResponseWriter, r *http.Request) {
a.sessions.SignOut(w, r)
diff --git a/internal/home/directory_catalog.go b/internal/home/directory_catalog.go
index 282488f..8d95eab 100644
--- a/internal/home/directory_catalog.go
+++ b/internal/home/directory_catalog.go
@@ -6,12 +6,27 @@ type catalogApp struct {
// This display catalog grants no integration or provisioning capability.
// Those remain governed by directoryApps and the destination checks.
-func directoryCatalog() []catalogApp {
+func directoryCatalog(previewOrigin string) []catalogApp {
+ docs := catalogApp{Name: "Docs", Href: docsEntryPath, Status: "Choose a team"}
+ if previewOrigin != "" {
+ docs.Href = previewOrigin + "/signin"
+ docs.Status = "Open preview"
+ }
return []catalogApp{
- {Name: "Docs", Href: docsEntryPath, Status: "Choose a team"},
+ docs,
{Name: "Sheets", Status: "Not connected to Home yet"},
{Name: "Slides", Status: "Not connected to Home yet"},
{Name: "Calendar", Status: "Not connected to Home yet"},
{Name: "Meet", Status: "Coming soon"},
}
}
+
+// Preview navigation is an operator choice, not an additional team route.
+// The destination must already have its own Home consumer registration.
+func (cfg Runtime) validDocsPreview() bool {
+ if cfg.docsPreviewOrigin == "" {
+ return true
+ }
+ _, err := originHost(cfg.docsPreviewOrigin)
+ return cfg.role == "directory" && err == nil && cfg.docsPreviewOrigin != cfg.origins.Credential && cfg.docsPreviewOrigin != cfg.origins.Directory
+}
diff --git a/internal/home/directory_catalog_test.go b/internal/home/directory_catalog_test.go
index 11b7c49..71bb0fc 100644
--- a/internal/home/directory_catalog_test.go
+++ b/internal/home/directory_catalog_test.go
@@ -34,3 +34,61 @@ func TestDirectoryCatalogRequiresSigninAndDoesNotInventLaunchDestinations(t *tes
t.Fatal("catalog bypassed team selection", entry.Code)
}
}
+
+func TestDirectoryCatalogPreviewKeepsDocsChooserPolicy(t *testing.T) {
+ f := newMountedChooser(t)
+ cfg := *f.p.directory.runtime
+ cfg.docsPreviewOrigin = "https://overnight-docs.bds.oncarlos.com"
+ app, err := NewRuntime(cfg, f.p.directory.db, quietLogger())
+ if err != nil {
+ t.Fatal(err)
+ }
+ f.handler = app.Router()
+ cookie := f.session(t, chooserPerson)
+ page := f.request("GET", "/?return_to=https://evil.example", "", "", cookie)
+ body := page.Body.String()
+ if page.Code != 200 || !strings.Contains(body, `href="https://overnight-docs.bds.oncarlos.com/signin"`) || !strings.Contains(body, "Open preview") || strings.Contains(body, "Choose a team") || strings.Contains(body, "evil.example") || strings.Count(body, "<a ") != 1 {
+ t.Fatal("preview navigation incorrect", page.Code, body)
+ }
+ noStore(t, page)
+ entry := f.request("GET", docsEntryPath, "", "", cookie)
+ if entry.Code != 200 || entry.Header().Get("Location") != "" || !strings.Contains(entry.Body.String(), "This account has no team with Docs available.") {
+ t.Fatal("preview changed team entry", entry.Code)
+ }
+ cfg.docsPreviewOrigin = "https://evil.example/path"
+ if _, err := NewRuntime(cfg, f.p.directory.db, quietLogger()); err != errExchangeConfig {
+ t.Fatal("constructor bypassed preview config validation", err)
+ }
+}
+
+func TestRuntimeDocsPreviewOriginIsOptionalAndStrict(t *testing.T) {
+ _, keys := runtimeEnv(t)
+ t.Setenv("HOME_ROLE", "directory")
+ t.Setenv("HOME_ASSERTION_KEYS", keys)
+ for _, origin := range []string{"", "https://overnight-docs.bds.oncarlos.com"} {
+ t.Setenv("HOME_DOCS_PREVIEW_ORIGIN", origin)
+ cfg, err := LoadRuntime("directory.db", quietLogger())
+ if err != nil || cfg.docsPreviewOrigin != origin {
+ t.Fatal("valid preview config refused", err)
+ }
+ }
+ for _, origin := range []string{
+ "http://docs.example", "//docs.example", "https://user@docs.example", "https://docs.example/", "https://docs.example/signin",
+ "https://docs.example?x=1", "https://docs.example#fragment", "https://docs.example?", "https://docs.example#",
+ "https://docs.example:443", "https://docs.example:8443", "https://DOCS.example", "HTTPS://docs.example",
+ "https://docs.example.", "https://127.0.0.1", "https://[::1]", " https://docs.example", "https://docs.example\n",
+ "https://docs%2eexample", "https://docs_example", "https://-docs.example", "https://docs..example", Origin, directoryOrigin,
+ } {
+ t.Setenv("HOME_DOCS_PREVIEW_ORIGIN", origin)
+ if _, err := LoadRuntime("directory.db", quietLogger()); err != errExchangeConfig {
+ t.Fatalf("invalid preview config accepted %q: %v", origin, err)
+ }
+ }
+ t.Setenv("HOME_DOCS_PREVIEW_ORIGIN", "https://overnight-docs.bds.oncarlos.com")
+ for _, role := range []string{"", "credential"} {
+ t.Setenv("HOME_ROLE", role)
+ if _, err := LoadRuntime("home.db", quietLogger()); err != errExchangeConfig {
+ t.Fatal("preview setting accepted outside directory", role, err)
+ }
+ }
+}
diff --git a/internal/home/runtime.go b/internal/home/runtime.go
index 1da805b..dcc8fa2 100644
--- a/internal/home/runtime.go
+++ b/internal/home/runtime.go
@@ -23,14 +23,15 @@ var errExchangeConfig = errors.New("home: invalid assertion configuration")
var directorySchema = migrate.Merge(sessions.Schema, migrate.MustFromFS(handoff.Migrations, "oficina_handoff"), directorydata.Schema, directorydata.RosterSchema)
type Runtime struct {
- roster rosterRuntime
- role string
- origins Origins
- secrets Secrets
- signer *assertioncrypto.Keypair
- keyID string
- keys map[string][]byte
- consumers map[string]string
+ docsPreviewOrigin string
+ roster rosterRuntime
+ role string
+ origins Origins
+ secrets Secrets
+ signer *assertioncrypto.Keypair
+ keyID string
+ keys map[string][]byte
+ consumers map[string]string
}
// LoadRuntime keeps existing deployments on the legacy path until a role is
@@ -43,6 +44,10 @@ func LoadRuntime(dbPath string, logger *slog.Logger) (Runtime, error) {
return Runtime{}, err
}
cfg.role = os.Getenv("HOME_ROLE")
+ cfg.docsPreviewOrigin = os.Getenv("HOME_DOCS_PREVIEW_ORIGIN")
+ if !cfg.validDocsPreview() {
+ return Runtime{}, errExchangeConfig
+ }
if cfg.role != "directory" {
cfg.roster, err = loadRosterRuntime(cfg.role, nil)
if err != nil {
@@ -151,7 +156,7 @@ func (cfg Runtime) Schema() *migrate.Set {
return BootSchema
}
func NewRuntime(cfg Runtime, d *db.DB, logger *slog.Logger) (*App, error) {
- if !cfg.roster.valid(cfg.role, cfg.keys) {
+ if !cfg.roster.valid(cfg.role, cfg.keys) || !cfg.validDocsPreview() {
return nil, errExchangeConfig
}
if cfg.role == "directory" {
diff --git a/internal/home/runtime_test.go b/internal/home/runtime_test.go
index 05894fe..5686723 100644
--- a/internal/home/runtime_test.go
+++ b/internal/home/runtime_test.go
@@ -18,7 +18,7 @@ func runtimeEnv(t *testing.T) (string, string) {
t.Setenv(k, "")
os.Unsetenv(k)
}
- for _, k := range []string{"HOME_CONSUMERS_BASE64", "HOME_ASSERTION_KEYS_BASE64", "HOME_ROLE", "HOME_ASSERTION_KEY", "HOME_ASSERTION_KEYS", "HOME_CONSUMERS", "HOME_CREDENTIAL_ORIGIN", "HOME_DIRECTORY_ORIGIN"} {
+ for _, k := range []string{"HOME_DOCS_PREVIEW_ORIGIN", "HOME_CONSUMERS_BASE64", "HOME_ASSERTION_KEYS_BASE64", "HOME_ROLE", "HOME_ASSERTION_KEY", "HOME_ASSERTION_KEYS", "HOME_CONSUMERS", "HOME_CREDENTIAL_ORIGIN", "HOME_DIRECTORY_ORIGIN"} {
t.Setenv(k, "")
}
t.Setenv("HOME_CREDENTIAL_ORIGIN", Origin)