feat(server): activate projected authentication safely
This commit is contained in:
@@ -22,6 +22,7 @@ import (
|
||||
"unicode/utf16"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authprojection"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/compose"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/output"
|
||||
@@ -46,6 +47,8 @@ var errCommandRefused = errors.New("authentication command refused")
|
||||
|
||||
var writeNewAuthFile = safeio.WriteCanonicalNewFile
|
||||
var removeAuthFile = safeio.RemoveCanonicalPrivateRegular
|
||||
var runProjectedAuthMutation = RunProjectedMutation
|
||||
var publishProjectedAuthCanonical = PublishProjectedCanonical
|
||||
|
||||
// Run implements the host-only authentication operator surface. It accepts password bytes only
|
||||
// from an echo-free terminal or a bounded private file, and never writes them to either stream.
|
||||
@@ -62,27 +65,165 @@ func RunWithRunner(ctx context.Context, installation config.Installation, args [
|
||||
directory := installation.AuthenticationDirectory()
|
||||
switch args[0] {
|
||||
case "configure":
|
||||
if err := configure(directory, args[1:], stdin, stderr); err != nil {
|
||||
if err := runInstallationAuthMutation(ctx, installation, func() error {
|
||||
return configure(directory, args[1:], stdin, stderr)
|
||||
}); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return 0
|
||||
case "publish":
|
||||
if err := publishInstallationAuthentication(ctx, installation, args[1:]); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return 0
|
||||
case "status":
|
||||
if installation.HasRuntimeAuthProjection() {
|
||||
if err := projectedStatus(installation, args[1:], stdout); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return 0
|
||||
}
|
||||
if err := status(directory, args[1:], stdout); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return 0
|
||||
case "user":
|
||||
if err := user(directory, args[1:], stdin, stdout, stderr); err != nil {
|
||||
if err := runInstallationUserMutation(ctx, installation, args[1:], func() error {
|
||||
return user(directory, args[1:], stdin, stdout, stderr)
|
||||
}); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return 0
|
||||
case "check":
|
||||
if err := RequireRuntimeAuthProjectionReady(installation); err != nil {
|
||||
return authFailure(stderr, authMessage(err))
|
||||
}
|
||||
return checkCommand(ctx, installation, args[1:], stdout, stderr, runner)
|
||||
default:
|
||||
return authFailure(stderr, "unknown auth subcommand")
|
||||
}
|
||||
}
|
||||
|
||||
// RuntimeAuthProjectionStatus reads only public runtime-projection metadata and compares it with
|
||||
// a detached, validated snapshot of the canonical authentication store. It never publishes or
|
||||
// repairs the projection.
|
||||
func RuntimeAuthProjectionStatus(installation config.Installation) (ProjectionStatus, error) {
|
||||
projection := installation.RuntimeAuthProjection()
|
||||
if projection == nil {
|
||||
return ProjectionStatus{}, errCommandRefused
|
||||
}
|
||||
canonical, err := loadSnapshotBytes(installation.AuthenticationDirectory())
|
||||
if err != nil {
|
||||
return ProjectionStatus{}, errCommandRefused
|
||||
}
|
||||
published, err := authprojection.Inspect(authprojection.Spec{
|
||||
RuntimeRoot: projection.Directory,
|
||||
UID: projection.UID,
|
||||
GID: projection.GID,
|
||||
})
|
||||
if errors.Is(err, authprojection.ErrBlocked) {
|
||||
return ProjectionStatus{
|
||||
State: "blocked",
|
||||
CanonicalRevision: canonical.CanonicalRevision,
|
||||
Equal: false,
|
||||
}, nil
|
||||
}
|
||||
if err != nil {
|
||||
return ProjectionStatus{}, errCommandRefused
|
||||
}
|
||||
return ProjectionStatus{
|
||||
State: published.Selector.State,
|
||||
Generation: published.Snapshot.Generation,
|
||||
CanonicalRevision: canonical.CanonicalRevision,
|
||||
Equal: equalProjection(published, canonical),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// RequireRuntimeAuthProjectionReady is the shared fail-closed pre-admission check. It is a no-op
|
||||
// for an installation that does not declare a runtime projection.
|
||||
func RequireRuntimeAuthProjectionReady(installation config.Installation) error {
|
||||
if !installation.HasRuntimeAuthProjection() {
|
||||
return nil
|
||||
}
|
||||
status, err := RuntimeAuthProjectionStatus(installation)
|
||||
if err != nil || status.State != "ready" || !status.Equal {
|
||||
return errCommandRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runInstallationAuthMutation(ctx context.Context, installation config.Installation, mutate func() error) error {
|
||||
projection := installation.RuntimeAuthProjection()
|
||||
if projection == nil {
|
||||
return mutate()
|
||||
}
|
||||
if err := requireProjectedAuthMutationPrivilege(); err != nil {
|
||||
return errCommandRefused
|
||||
}
|
||||
return runProjectedAuthMutation(ctx, installation.AuthenticationDirectory(), ProjectionSpec{
|
||||
RuntimeRoot: projection.Directory,
|
||||
UID: projection.UID,
|
||||
GID: projection.GID,
|
||||
}, mutate)
|
||||
}
|
||||
|
||||
func runInstallationUserMutation(ctx context.Context, installation config.Installation, args []string, mutate func() error) error {
|
||||
if len(args) == 0 || args[0] == "list" || !installation.HasRuntimeAuthProjection() {
|
||||
return mutate()
|
||||
}
|
||||
switch args[0] {
|
||||
case "add", "set-password", "enable", "disable", "grant", "revoke", "logout-all":
|
||||
return runInstallationAuthMutation(ctx, installation, mutate)
|
||||
default:
|
||||
return mutate()
|
||||
}
|
||||
}
|
||||
|
||||
func publishInstallationAuthentication(ctx context.Context, installation config.Installation, args []string) error {
|
||||
if len(args) != 0 || !installation.HasRuntimeAuthProjection() {
|
||||
return errCommandRefused
|
||||
}
|
||||
if err := requireProjectedAuthMutationPrivilege(); err != nil {
|
||||
return errCommandRefused
|
||||
}
|
||||
projection := installation.RuntimeAuthProjection()
|
||||
status, err := publishProjectedAuthCanonical(ctx, installation.AuthenticationDirectory(), ProjectionSpec{
|
||||
RuntimeRoot: projection.Directory,
|
||||
UID: projection.UID,
|
||||
GID: projection.GID,
|
||||
})
|
||||
if err != nil || status.State != "ready" || !status.Equal {
|
||||
return errCommandRefused
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func projectedStatus(installation config.Installation, args []string, stdout io.Writer) error {
|
||||
jsonMode := len(args) == 1 && args[0] == "--json"
|
||||
if len(args) != 0 && !jsonMode {
|
||||
return errCommandRefused
|
||||
}
|
||||
status, err := RuntimeAuthProjectionStatus(installation)
|
||||
if err != nil {
|
||||
return errCommandRefused
|
||||
}
|
||||
if jsonMode {
|
||||
return json.NewEncoder(stdout).Encode(struct {
|
||||
State string `json:"state"`
|
||||
Generation string `json:"generation"`
|
||||
CanonicalRevision string `json:"canonicalRevision"`
|
||||
Equal bool `json:"equal"`
|
||||
}{
|
||||
State: status.State,
|
||||
Generation: status.Generation,
|
||||
CanonicalRevision: status.CanonicalRevision,
|
||||
Equal: status.Equal,
|
||||
})
|
||||
}
|
||||
_, err = fmt.Fprintf(stdout, "State: %s\nGeneration: %s\nCanonical revision: %s\nEqual: %t\n", status.State, status.Generation, status.CanonicalRevision, status.Equal)
|
||||
return err
|
||||
}
|
||||
|
||||
// AuthDiagnostic is the closed JSON contract emitted by the backend diagnostic command.
|
||||
type AuthDiagnostic struct {
|
||||
Level string `json:"level"`
|
||||
|
||||
Reference in New Issue
Block a user