103 lines
3.6 KiB
Go
103 lines
3.6 KiB
Go
package backup
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// FormatVersion is the archive format this build reads and writes. Restore
|
|
// refuses anything else rather than guessing at a layout it does not know.
|
|
const FormatVersion = 1
|
|
|
|
// ManifestName is the archive member holding the manifest.
|
|
const ManifestName = "manifest.json"
|
|
|
|
// ErrUnknownFormat is returned for an archive this build cannot read.
|
|
var ErrUnknownFormat = errors.New("unsupported archive format version")
|
|
|
|
// ErrBadCollectionName is returned for a manifest naming a collection that
|
|
// cannot safely be used as a path component.
|
|
var ErrBadCollectionName = errors.New("manifest names an unusable collection")
|
|
|
|
// CollectionEntry describes one collection in the archive. Bytes and SHA256
|
|
// cover the uncompressed .bson member, which is what restore verifies before
|
|
// writing anything.
|
|
type CollectionEntry struct {
|
|
Name string `json:"name"`
|
|
Documents int64 `json:"documents"`
|
|
Bytes int64 `json:"bytes"`
|
|
SHA256 string `json:"sha256"`
|
|
}
|
|
|
|
// Manifest is the archive's index and its provenance.
|
|
//
|
|
// KeyFingerprint is a pointer so "this archive recorded no key" is a distinct
|
|
// state from "this archive recorded the empty string". A null here is a real
|
|
// condition an operator must be told about, not a default.
|
|
type Manifest struct {
|
|
FormatVersion int `json:"format_version"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
VantageVersion string `json:"vantage_version"`
|
|
Hostname string `json:"hostname"`
|
|
MongoDB string `json:"mongo_db"`
|
|
MongoServerVersion string `json:"mongo_server_version"`
|
|
KeyFingerprint *string `json:"key_fingerprint"`
|
|
Collections []CollectionEntry `json:"collections"`
|
|
Excluded []string `json:"excluded"`
|
|
}
|
|
|
|
// Check validates what can be validated without reading the rest of the archive.
|
|
func (m Manifest) Check() error {
|
|
if m.FormatVersion != FormatVersion {
|
|
return fmt.Errorf("%w: archive is version %d, this build reads version %d",
|
|
ErrUnknownFormat, m.FormatVersion, FormatVersion)
|
|
}
|
|
// Collection names become path components inside the extraction directory,
|
|
// and an archive is operator-supplied input that may not be one we wrote.
|
|
for _, c := range m.Collections {
|
|
if err := checkCollectionName(c.Name); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// checkCollectionName refuses a name that could escape a directory when joined
|
|
// as a path component.
|
|
func checkCollectionName(name string) error {
|
|
switch {
|
|
case name == "":
|
|
return fmt.Errorf("%w: a collection entry has no name", ErrBadCollectionName)
|
|
case name == "." || name == "..":
|
|
return fmt.Errorf("%w: %q", ErrBadCollectionName, name)
|
|
case strings.ContainsAny(name, "/\\"), strings.Contains(name, ".."):
|
|
return fmt.Errorf("%w: %q", ErrBadCollectionName, name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Collection looks up one entry by name.
|
|
func (m Manifest) Collection(name string) (CollectionEntry, bool) {
|
|
for _, c := range m.Collections {
|
|
if c.Name == name {
|
|
return c, true
|
|
}
|
|
}
|
|
return CollectionEntry{}, false
|
|
}
|
|
|
|
// CiphertextCollections names the collections holding AES-GCM ciphertext.
|
|
//
|
|
// It exists to be printed. When a restore proceeds under a key that does not
|
|
// match the archive, this is the list of what will be unreadable afterwards,
|
|
// and an operator deserves to see it before the write rather than discover it
|
|
// a week later.
|
|
//
|
|
// settings is not in the list: it holds no encrypted material. Its ESO read
|
|
// token is a SHA-256 hash, not ciphertext.
|
|
func CiphertextCollections() []string {
|
|
return []string{"keys", "secrets", "auth_providers", "console_sessions"}
|
|
}
|