v3.3.1 31f1eec

app_devices.go RawHistoryFolder

1package app
2
3import (
4	"fmt"
5	"strings"
6
7	"github.com/spf13/viper"
8
9	"github.com/bnema/gordon/internal/domain"
10)
11
12// AppDevicePolicy is one administrative device grant declared under
13// [app_devices.<name>]. The map key is the stable logical device name an
14// app manifest device references. CDI holds the explicit CDI device IDs
15// granted (aggregate "=all" selectors are rejected). AllowedApps and
16// AllowedServices are exact, non-empty allowlists.
17type AppDevicePolicy struct {
18	CDI             []string `mapstructure:"cdi"`
19	AllowedApps     []string `mapstructure:"allowed_apps"`
20	AllowedServices []string `mapstructure:"allowed_services"`
21}
22
23// knownAppDeviceKeys is the allowlist of [app_devices.<name>] sub-keys.
24// Unknown keys (for example a typo like "cdis") fail closed instead of
25// silently dropping a grant.
26var knownAppDeviceKeys = map[string]struct{}{
27	"cdi":              {},
28	"allowed_apps":     {},
29	"allowed_services": {},
30}
31
32// validateAppDeviceKeys rejects unknown sub-keys inside [app_devices.*].
33// Viper unmarshals leniently, so a typo would otherwise drop the grant
34// silently. settings is the decoded app_devices subtree (viper lowercases
35// all keys) used only for key inspection: values still come from Config.
36func validateAppDeviceKeys(devices map[string]any) error {
37	for name, raw := range devices {
38		entry, ok := raw.(map[string]any)
39		if !ok {
40			return fmt.Errorf("invalid app_devices.%s: entry must be a table", name)
41		}
42		for key := range entry {
43			if _, ok := knownAppDeviceKeys[key]; !ok {
44				return fmt.Errorf("invalid app_devices.%s: unknown key %q (allowed: cdi, allowed_apps, allowed_services)", name, key)
45			}
46		}
47	}
48	return nil
49}
50
51// buildAppDevicePolicies converts configured app devices into validated
52// domain policies. Failures name only the device and the field-level
53// reason: host inventory never appears in config errors.
54func buildAppDevicePolicies(cfg Config) (map[string]domain.AppDevicePolicy, error) {
55	policies := make(map[string]domain.AppDevicePolicy, len(cfg.AppDevices))
56	for name, device := range cfg.AppDevices {
57		policy := domain.AppDevicePolicy{
58			Name:            name,
59			CDI:             append([]string(nil), device.CDI...),
60			AllowedApps:     append([]string(nil), device.AllowedApps...),
61			AllowedServices: append([]string(nil), device.AllowedServices...),
62		}
63		if err := policy.Validate(); err != nil {
64			return nil, fmt.Errorf("invalid app_devices.%s: %w: %s", name, domain.ErrDevicePolicy, redactedDevicePolicyReason(err))
65		}
66		policies[name] = policy
67	}
68	return policies, nil
69}
70
71// redactedDevicePolicyReason keeps the field-level reason from a domain
72// device policy validation error while stripping every quoted value so an
73// invalid config never echoes host inventory (CDI IDs, app names).
74func redactedDevicePolicyReason(err error) string {
75	reason := policyQuotedValue.ReplaceAllString(err.Error(), "")
76	reason = strings.Join(strings.Fields(reason), " ")
77	// Keep only the innermost detail: the outer wrap repeats the generic
78	// violation and the policy name, which is already part of the device key.
79	if idx := strings.LastIndex(reason, ": "); idx >= 0 {
80		reason = reason[idx+2:]
81	}
82	return reason
83}
84
85// validateDeviceConfig validates the [app_devices] subtree: unknown keys
86// fail closed, then the decoded policies validate. Split from
87// applyLoadedConfig to keep its complexity within budget.
88func validateDeviceConfig(v *viper.Viper, cfg Config) error {
89	if err := validateAppDeviceKeys(v.GetStringMap("app_devices")); err != nil {
90		return err
91	}
92	if _, err := buildAppDevicePolicies(cfg); err != nil {
93		return err
94	}
95	return nil
96}
97
98// validateAppPolicies validates both administrative app policy subtrees
99// (bind mounts and device grants) before publication. Failed validation
100// keeps the previous policies live. Split from applyLoadedConfig to keep
101// its complexity within budget.
102func validateAppPolicies(v *viper.Viper, cfg Config) error {
103	if _, err := buildAppMountPolicies(cfg); err != nil {
104		return err
105	}
106	return validateDeviceConfig(v, cfg)
107}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close