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 all administrative app policy subtrees
99// (bind mounts, device grants, and capability 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 if err := validateDeviceConfig(v, cfg); err != nil {
107 return err
108 }
109 return validateCapabilityConfig(v, cfg)
110}