console

command module
v0.3.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 12, 2026 License: BSD-3-Clause Imports: 23 Imported by: 0

README

console

The admin console for go-fileshare. One page manages the shares, grants and state of every fileshare server a portal knows. It is written once in Go with go-widgets and runs in a browser (wasm) or as a native window. Pure Go, CGO_ENABLED=0.

This is phase 3 of the UI design.

In a browser, behind the portal

./build.sh                                  # dist/: console.wasm(.gz), wasm_exec.js, the page
sudo cp -r dist /usr/share/fileshare-console
# portal.hcl
ui "console" { dir = "/usr/share/fileshare-console" }

Open https://<portal>/console/. Without a session the console sends you to log in at your identity provider and comes back. It never holds a token: it calls /api/<server>/fileshare.admin.v1.AdminService/<Method> on the portal, which adds the token of that server.

What the console does with the portal, through its client package (portal/client, which the explorer uses too):

  • A token per server, refreshed once. When the portal answers that a server's token is missing or expiring (401, Portal-Refresh), the console asks it to refresh and sends the call again. It asks for the servers it used most recently, at most five, so that switching between two servers does not refresh at every click.
  • One refresh at a time, across tabs. A provider that rotates refresh tokens ends the whole login when one is spent twice. Refreshes therefore go through the browser's Web Locks API, so two tabs never refresh together, nor a console and an explorer tab, which share the session: both take client.RefreshLock. Before each refresh, the console checks whether another tab has just done it.
  • The portal's CSRF defence. Every call carries Portal-Request: 1. The page loads no inline script, which the portal's Content-Security-Policy would refuse.

The page weighs 5.5 MB compressed (23 MB of wasm; build.sh writes the .gz and the portal sends it). It used to be 7.1 MB. internal/adminpb is generated here from fileshare's admin.proto, the messages and a Connect client only, because fileshare's own package carries gRPC stubs, and those alone put 3 MB of compressed gRPC in the page. CI checks that the .proto is byte for byte fileshare v0.29.0's, and that the generated code is what the pinned generators make of it.

Natively

go install github.com/go-fileshare/console@latest
console                     # reads ~/Library/Application Support/fileshare-console/console.hcl (macOS)
console -config my.hcl      # or $XDG_CONFIG_HOME/fileshare-console/console.hcl elsewhere
console -demo               # over a built-in stand-in, to see it without servers

Natively there is no portal: the console logs in itself and calls each server directly.

issuer "https://login.example.org" {
  client_id = "fileshare-console"     # a PUBLIC client: no secret
}

server "paris" {
  label    = "Paris"
  url      = "https://fs-paris.example.org:8443"   # fileshare's admin { web { listen } }
  resource = "https://fs-paris.example.org/"       # that server's audience
  # ca_file = "fs-ca.pem"                          # relative to this file
}
  • Logging in goes through the system browser and a listener on 127.0.0.1, with go-authn/rp (RFC 8252): PKCE, state, nonce, and a knock on the port with another state is ignored. The client is public, because a secret shipped inside an application is no secret. go-authn/bridge accepts any loopback port for a client registered with http://127.0.0.1/callback.
  • Each server gets a token addressed to it alone (resource, RFC 8707), bought with the refresh token. The refresh token stays in memory only and is spent one refresh at a time. Without that lock, two concurrent refreshes trigger the provider's reuse detection and the login is lost; the test checks exactly that.
  • A server that refuses its token gets one refresh and one retry. A grant that has ended means logging in again, in the browser.
  • Only https servers. A bearer token sent in the clear is anybody's.
  • No redirect is followed (since v0.3.2). Go's client keeps Authorization across a redirect to the same host on any scheme or port, so a 307 http://... from a misconfigured proxy would have sent the token in clear. go-authn/rp v0.1.1 does the same for the provider's token endpoint.

What it does

servers every server the portal knows; one behind another identity provider is listed but not reachable from this login
shares name, source, enabled or not; details: kind of source, configuration file or admin API, filesystem, read-only and why, size, protocols, what it is served over, and why not over the others
create a directory share, with its first grant (fileshare refuses a share nobody may use)
enable / disable disabling closes its connections and lets go of its source
delete two presses; the files stay where they are
grant / revoke alice, @group, oidc:groups:<group>, oidc:user:<name>, as in fileshare's configuration file; revoking the last grant is refused by the server
volumes on a server with fileshare's provisioner: the parents (ZFS, btrfs, XFS, ext4) with their free space; each volume's quota, use, snapshots and the shares that use it. Create (500G, 2T: binary units, at most the server's max_volume), resize (never below what is used), snapshot (ZFS and btrfs only; elsewhere the button is greyed), share it (the share form gets volume:<parent>/<name> as its source), delete (two presses, refused while a share uses it). The console never destroys data: a volume that holds data or snapshots is refused by the server, and the console says so. Destroying data is not something to do from a list with a button. A server without a provisioner is shown as such, not as a failure

How it is built

  • MVVM, enforced. viewmodel.go holds all state as go-widgets/mvvm observables and imports no widget. scene.go binds widgets to it through mvvmtk and never writes widget state. CI runs mvvmlint and bricolint, each with a control showing that it fails on a violation.
  • Network results reach the screen safely. Every call runs on its own goroutine and posts its result to an mvvm.Queue. The root container drains the queue in SetBounds, which every window back-end calls before drawing. The queue's wake is the back-end's Repaint, so a result shows up with nobody touching the window, in a browser tab as in a native one. A result about a server no longer selected is dropped.
  • One tree, two targets. window.Open picks the back-end; w.Run(root) is the same call for both.

Tests

go test -race ./...
  • ViewModel tests: the real portal client against a stand-in for the portal and two fileshare servers (internal/admin/admintest, real Connect handlers). They cover login, listing, every command, failures, a login that ends, and a late answer about another server.
  • Portal client: one refresh for ten calls at once; the servers in use kept together; falling back to one server past the portal's cookie budget; the static header on every call.
  • View: built, bound, laid out and drawn natively. A click reaches the command, and a result posted from a goroutine is applied by the next frame and not before.
  • e2e/: the console as deployed. console.wasm runs in headless Chrome, behind the portal binary over TLS, in front of two fileshare binaries, with a test OpenID provider. The browser is sent to log in, comes back, lists A, creates a share there, and A's audit names oidc=<issuer> alice-sub while B audits nothing. With go-widgets/window v0.87.0, whose browser tab could not be woken, the same test stays on "Loading...": the answers wait in the queue for a frame that never comes. A second test runs the native backend against the same two real servers: loopback login as a public client, a token per server, a share created on A and audited there.
  • Volumes: a stand-in server with a provisioner (ZFS that snapshots, XFS that does not). Covered: create (and the same again), resize (refused below use before the server is asked), snapshot, share it, delete refused while shared, refused when it holds data, deleted when empty, a server without a provisioner, and every form refusal before the server is asked. Each of these makes a test fail when removed: sending destroy_data, deleting in one press, a resize below use reaching the server, no reload after a share is deleted, a missing provisioner shown as a failure, snapshots offered everywhere. Against real fileshare, the browser test checks the no-provisioner page.
  • Native backend: in-process provider (public client, loopback, rotating refresh tokens) and servers that verify their audience as fileshare does. Covered: one login per session, one refresh at a time for 20 concurrent calls, a retry after a 401, an ended grant, logout, and a server reached only through its ca_file. Removing the lock, the resource, the drop of a refused token or the invalid_grant handling makes a test fail.

Each defence was removed in turn, and a test failed each time:

  • the refresh lock;
  • the "already refreshed" check;
  • keeping the servers in use;
  • the static header;
  • the late-answer check;
  • the two-press delete;
  • the redirect to login;
  • draining the queue at the frame.

Licence

BSD 3-Clause.

Documentation

Overview

The native build: it logs in through the system browser (go-authn/rp, RFC 8252) and calls each server with a token of its own, with no portal in between. -demo opens it over a built-in stand-in instead, to see it.

Directories

Path Synopsis
internal
admin
Package admin is how the console reaches fileshare servers: their admin API (fileshare.admin.v1.AdminService, over Connect), and who is logged in.
Package admin is how the console reaches fileshare servers: their admin API (fileshare.admin.v1.AdminService, over Connect), and who is logged in.
admin/admintest
Package admintest is a stand-in for go-fileshare/portal and the fileshare servers behind it, for tests: in one process, with the portal's rules where the console depends on them.
Package admintest is a stand-in for go-fileshare/portal and the fileshare servers behind it, for tests: in one process, with the portal's rules where the console depends on them.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL