| 1 | // Deployment-wide settings records: system/<name>.json. |
| 2 | // |
| 3 | // These are NOT features.json. That one is per-app and holds switches the |
| 4 | // platform "stores and serves but never interprets" (apifeatures.go) — an |
| 5 | // app's own vocabulary, passed through. These the platform reads and acts on, |
| 6 | // so each gets a typed shape and a pinned schema. |
| 7 | // |
| 8 | // A typed record per feature rather than one generic settings map, for the |
| 9 | // same reason: a map would be features.json by another name, uninterpreted and |
| 10 | // impossible to schema-pin meaningfully. system/pdf.json later is a new type |
| 11 | // and a new door, not a new magic string. |
| 12 | package release |
| 13 | |
| 14 | import ( |
| 15 | "context" |
| 16 | "encoding/json" |
| 17 | "errors" |
| 18 | "fmt" |
| 19 | "os" |
| 20 | "time" |
| 21 | ) |
| 22 | |
| 23 | // AntivirusSchema is pinned. An unknown schema refuses rather than guessing — |
| 24 | // the rule objstore.Record and ScanSidecar already follow, and it matters more |
| 25 | // here: this record decides whether the deployment reads tenant object bytes. |
| 26 | const AntivirusSchema = 1 |
| 27 | |
| 28 | // SystemKey is where a deployment-wide settings record lives. |
| 29 | func SystemKey(name string) string { return "system/" + name + ".json" } |
| 30 | |
| 31 | // AntivirusKey is the object this package's typed helpers read and write. |
| 32 | const AntivirusName = "antivirus" |
| 33 | |
| 34 | // AntivirusSettings is whether object scanning is on for this deployment. |
| 35 | // |
| 36 | // Policy, not wiring. Where clamd lives stays in the unit file, because those |
| 37 | // are coordinates a console must not guess; whether scanning is ON lives here, |
| 38 | // so turning a compliance control on does not mean editing a systemd unit and |
| 39 | // restarting the control plane. |
| 40 | type AntivirusSettings struct { |
| 41 | Schema int `json:"schema"` |
| 42 | Enabled bool `json:"enabled"` |
| 43 | Engine string `json:"engine"` |
| 44 | At time.Time `json:"at"` |
| 45 | By string `json:"by"` |
| 46 | } |
| 47 | |
| 48 | // GetAntivirus reads the record. Absent is (nil, "", nil) — no record is a |
| 49 | // state, not an error, the same idiom objstore.Get uses. |
| 50 | // |
| 51 | // Absent means NOT ENABLED. A deployment that has never run `carlos system |
| 52 | // antivirus enable` does not scan, which is what makes the migration in |
| 53 | // cmd/console worth its own seeding step rather than a default. |
| 54 | func GetAntivirus(ctx context.Context, s Store) (*AntivirusSettings, string, error) { |
| 55 | raw, etag, err := s.Get(ctx, SystemKey(AntivirusName)) |
| 56 | if err != nil { |
| 57 | if errors.Is(err, os.ErrNotExist) { |
| 58 | return nil, "", nil |
| 59 | } |
| 60 | return nil, "", err |
| 61 | } |
| 62 | var av AntivirusSettings |
| 63 | if err := json.Unmarshal(raw, &av); err != nil { |
| 64 | return nil, "", fmt.Errorf("release: %s: %w", SystemKey(AntivirusName), err) |
| 65 | } |
| 66 | if av.Schema != AntivirusSchema { |
| 67 | return nil, "", fmt.Errorf("release: %s: schema %d is not %d", |
| 68 | SystemKey(AntivirusName), av.Schema, AntivirusSchema) |
| 69 | } |
| 70 | return &av, etag, nil |
| 71 | } |
| 72 | |
| 73 | // PutAntivirus writes with the caller's CAS position; etag "" is create-only. |
| 74 | // It stamps the schema so a caller cannot write a body it would then refuse to |
| 75 | // read back. |
| 76 | func PutAntivirus(ctx context.Context, s Store, av *AntivirusSettings, etag string) (string, error) { |
| 77 | av.Schema = AntivirusSchema |
| 78 | body, err := json.MarshalIndent(av, "", " ") |
| 79 | if err != nil { |
| 80 | return "", err |
| 81 | } |
| 82 | return s.Put(ctx, SystemKey(AntivirusName), append(body, '\n'), etag) |
| 83 | } |
| 84 | |
| 85 | // AntivirusEnabled folds the read into the one question every caller actually |
| 86 | // asks. A read error is NOT "disabled": it is an error, and a caller that |
| 87 | // treated it as disabled would silently stop scanning on a transient bucket |
| 88 | // failure. |
| 89 | func AntivirusEnabled(ctx context.Context, s Store) (bool, error) { |
| 90 | av, _, err := GetAntivirus(ctx, s) |
| 91 | if err != nil { |
| 92 | return false, err |
| 93 | } |
| 94 | return av != nil && av.Enabled, nil |
| 95 | } |
| 96 | |