Skip to content

Commit 456cf5f

Browse files
new filekv storage backend and related changes (#177)
1 parent 416198e commit 456cf5f

37 files changed

+1740
-150
lines changed

cli/cli.go

+29-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import (
88

99
"github.com/micromdm/nanomdm/storage"
1010
"github.com/micromdm/nanomdm/storage/allmulti"
11+
"github.com/micromdm/nanomdm/storage/diskv"
1112
"github.com/micromdm/nanomdm/storage/file"
13+
"github.com/micromdm/nanomdm/storage/inmem"
1214
"github.com/micromdm/nanomdm/storage/mysql"
1315
"github.com/micromdm/nanomdm/storage/pgsql"
1416

@@ -17,6 +19,11 @@ import (
1719
"github.com/micromdm/nanolib/log"
1820
)
1921

22+
var (
23+
ErrNoStorageOptions = errors.New("storage backend does not support options, please specify no (or empty) options")
24+
ErrMissingDSN = errors.New("missing required DSN")
25+
)
26+
2027
type StringAccumulator []string
2128

2229
func (s *StringAccumulator) String() string {
@@ -47,8 +54,8 @@ func (s *Storage) Parse(logger log.Logger) (storage.AllStorage, error) {
4754
}
4855
// default storage and DSN pair
4956
if len(s.Storage) < 1 {
50-
s.Storage = append(s.Storage, "file")
51-
s.DSN = append(s.DSN, "db")
57+
s.Storage = append(s.Storage, "filekv")
58+
s.DSN = append(s.DSN, "dbkv")
5259
}
5360
var mdmStorage []storage.AllStorage
5461
for idx, storage := range s.Storage {
@@ -63,7 +70,13 @@ func (s *Storage) Parse(logger log.Logger) (storage.AllStorage, error) {
6370
)
6471
switch storage {
6572
case "file":
66-
fileStorage, err := fileStorageConfig(dsn, options)
73+
if options != "enable_deprecated=1" {
74+
return nil, errors.New("file backend is deprecated; specify storage options to force enable")
75+
}
76+
if dsn == "" {
77+
return nil, ErrMissingDSN
78+
}
79+
fileStorage, err := file.New(dsn)
6780
if err != nil {
6881
return nil, err
6982
}
@@ -80,6 +93,19 @@ func (s *Storage) Parse(logger log.Logger) (storage.AllStorage, error) {
8093
return nil, err
8194
}
8295
mdmStorage = append(mdmStorage, pgsqlStorage)
96+
case "inmem":
97+
if options != "" {
98+
return nil, ErrNoStorageOptions
99+
}
100+
mdmStorage = append(mdmStorage, inmem.New())
101+
case "filekv":
102+
if dsn == "" {
103+
return nil, ErrMissingDSN
104+
}
105+
if options != "" {
106+
return nil, ErrNoStorageOptions
107+
}
108+
mdmStorage = append(mdmStorage, diskv.New(dsn))
83109
default:
84110
return nil, fmt.Errorf("unknown storage: %s", storage)
85111
}
@@ -97,15 +123,6 @@ func (s *Storage) Parse(logger log.Logger) (storage.AllStorage, error) {
97123
), nil
98124
}
99125

100-
var NoStorageOptions = errors.New("storage backend does not support options, please specify no (or empty) options")
101-
102-
func fileStorageConfig(dsn, options string) (*file.FileStorage, error) {
103-
if options != "" {
104-
return nil, NoStorageOptions
105-
}
106-
return file.New(dsn)
107-
}
108-
109126
func mysqlStorageConfig(dsn, options string, logger log.Logger) (*mysql.MySQLStorage, error) {
110127
logger = logger.With("storage", "mysql")
111128
opts := []mysql.Option{

cmd/nano2nano/main.go

+8
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ func main() {
8282
logger.Info("msg", "sending to migration endpoint", "err", err)
8383
}
8484
}
85+
case *mdm.SetBootstrapToken:
86+
logger.Info(logsFromEnrollment("SetBootstrapToken", &v.Enrollment)...)
87+
if !skipServer {
88+
if err := httpPut(client, *flURL, *flAPIKey, v.Raw); err != nil {
89+
fmt.Println(string(v.Raw))
90+
logger.Info("msg", "sending to migration endpoint", "err", err)
91+
}
92+
}
8593
case error:
8694
logger.Info("msg", "receiving checkin", "err", v)
8795
default:

docs/operations-guide.md

+35-7
Original file line numberDiff line numberDiff line change
@@ -65,17 +65,34 @@ Enable additional debug logging.
6565

6666
### -storage, -storage-dsn, & -storage-options
6767

68-
The `-storage`, `-storage-dsn`, & `-storage-options` flags together configure the storage backend(s). `-storage` specifies the name of the backend while `-storage-dsn` specifies the backend data source name (e.g. the connection string). The optional `-storage-options` flag specifies options for the backend if it supports them. If no storage flags are supplied then it is as if you specified `-storage file -storage-dsn db` meaning we use the `file` storage backend with `db` as its DSN.
68+
The `-storage`, `-storage-dsn`, & `-storage-options` flags together configure the storage backend(s). `-storage` specifies the name of the backend while `-storage-dsn` specifies the backend data source name (e.g. the connection string). The optional `-storage-options` flag specifies options for the backend if it supports them. If no storage flags are supplied then it is as if you specified `-storage filekv -storage-dsn dbkv` meaning we use the `filekv` storage backend with `dbkv` as its DSN.
6969

70-
_Note:_ NanoMDM versions v0.5.0 and below used the `-dsn` flag while later versions switched to the `-storage-dsn` flag.
70+
> [!NOTE]
71+
> NanoMDM **versions v0.5 and below** used the `-dsn` flag while later versions now use the `-storage-dsn` flag.
72+
73+
##### filekv storage backend
74+
75+
* `-storage filekv`
76+
77+
Configures the `filekv` storage backend. This manages enrollment and command queue data within plain filesystem files and directories using a key-value storage system. It has zero dependencies, no options, and should run out of the box. The `-storage-dsn` flag specifies the filesystem directory for the database. If no DSN is specified then `dbkv` is used as a default.
78+
79+
*Example:* `-storage filekv -storage-dsn /path/to/my/db`
7180

7281
#### file storage backend
7382

7483
* `-storage file`
7584

76-
Configures the `file` storage backend. This manages enrollment and command data within plain filesystem files and directories. It has zero dependencies and should run out of the box. The `-storage-dsn` flag specifies the filesystem directory for the database. The `file` backend has no storage options.
85+
> [!WARNING]
86+
> The `file` storage backend is deprecated in NanoMDM **versions after v0.7** and will be removed in a future release.
87+
88+
Configures the `file` storage backend. This manages enrollment and command data within plain filesystem files and directories. It has zero dependencies and should run out of the box. The `-storage-dsn` flag specifies the filesystem directory for the database.
89+
90+
Options are specified as a comma-separated list of "key=value" pairs. Supported options:
7791

78-
*Example:* `-storage file -storage-dsn /path/to/my/db`
92+
* `enable_deprecated=1`
93+
* This option enables the file backend. Without this switch the `file` backend is disabled.
94+
95+
*Example:* `-storage file -storage-dsn /path/to/my/db -storage-options enable_deprecated=1`
7996

8097
#### mysql storage backend
8198

@@ -106,15 +123,26 @@ Options are specified as a comma-separated list of "key=value" pairs. The pgsql
106123

107124
*Example:* `-storage pgsql -storage-dsn postgres://postgres:toor@localhost/nanomdm -storage-options delete=1`
108125

126+
#### in-memory storage backend
127+
128+
* `-storage inmem`
129+
130+
Configure the `inmem` in-memory storage backend. This manages enrollment and command queue data entirely in *volatile* memeory. There are no options and the DSN is ignored.
131+
132+
> [!CAUTION]
133+
> All data is lost when the server process exits when using the in-memory storage backend.
134+
135+
*Example:* `-storage inmem`
136+
109137
#### multi-storage backend
110138

111-
You can configure multiple storage backends to be used simultaneously. Specifying multiple sets of `-storage`, `-storage-dsn`, & `-storage-options` flags will configure the "multi-storage" adapter. The flags must be specified in sets and are related to each other in the order they're specified: for example the first `-storage` flag corresponds to the first `-storage-dsn` flag and so forth.
139+
You can configure multiple storage backends to be used simultaneously. Specifying multiple sets of `-storage`, `-storage-dsn`, & `-storage-options` flags will configure the "multi-storage" adapter. The flags must be specified in sets and are related to each other in the order they're specified: for example the first `-storage` flag corresponds to the first `-storage-dsn` flag and so forth. Note that empty options must be specified even if the backend is not using them.
112140

113141
Be aware that only the first storage backend will be "used" when interacting with the system, all other storage backends are called to, but any *results* are discarded. In other words consider them write-only. Also beware that you will have very bizaare results if you change to using multiple storage backends in the midst of existing enrollments. You will receive errors about missing database rows or data. A storage backend needs to be around when a device (or all devices) initially enroll(s). There is no "sync" or backfill system with multiple storage backends (see the migration ability if you need this).
114142

115-
The multi-storage backend is really only useful if you've always been using multiple storage backends or if you're doing some type of development or testing (perhaps creating a new storage backend).
143+
The multi-storage backend is only really useful if you've always been using multiple storage backends or if you're doing some type of development or testing (perhaps creating a new storage backend).
116144

117-
For example to use both a `file` *and* `mysql` backend your command line might look like: `-storage file -storage-dsn db -storage mysql -storage-dsn nanomdm:nanomdm/mymdmdb`. You can also mix and match backends, or mutliple of the same backend. Behavior is undefined (and probably very bad) if you specify two backends of the same type with the same DSN.
145+
For example to use both a `filekv` *and* `mysql` backend your command line might look like: `-storage filekv -storage-dsn dbkv -storage mysql -storage-dsn nanomdm:nanomdm/mymdmdb`. You can also mix and match backends, or mutliple of the same backend. Behavior is undefined (and probably very bad) if you specify two backends of the same type with the same DSN (i.e. sharing the same data source).
118146

119147
### -dump
120148

go.mod

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ require (
66
github.com/RobotsAndPencils/buford v0.14.0
77
github.com/go-sql-driver/mysql v1.8.1
88
github.com/lib/pq v1.10.9
9-
github.com/micromdm/nanolib v0.2.0
9+
github.com/micromdm/nanolib v0.3.0
1010
github.com/micromdm/plist v0.2.1
11+
github.com/peterbourgon/diskv/v3 v3.0.1
1112
github.com/smallstep/pkcs7 v0.2.1
1213
golang.org/x/net v0.34.0
1314
)
1415

1516
require (
1617
filippo.io/edwards25519 v1.1.0 // indirect
18+
github.com/google/btree v1.0.0 // indirect
1719
golang.org/x/crypto v0.33.0 // indirect
1820
golang.org/x/text v0.22.0 // indirect
1921
)

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,17 @@ github.com/RobotsAndPencils/buford v0.14.0/go.mod h1:F5FvdB/nkMby8Pge6HFpPHgLOeU
55
github.com/aai/gocrypto v0.0.0-20160205191751-93df0c47f8b8/go.mod h1:nE/FnVUmtbP0EbgMVCUtDrm1+86H47QfJIdcmZb+J1s=
66
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
77
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
8+
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
89
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
910
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
1011
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
1112
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
1213
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
13-
github.com/micromdm/nanolib v0.2.0 h1:g5GHQuUpS82WIAB15LyenjF/0/WSUNJMe5XZfCJSXq4=
14-
github.com/micromdm/nanolib v0.2.0/go.mod h1:FwBKCvvphgYvbdUZ+qw5kay7NHJcg6zPi8W7kXNajmE=
14+
github.com/micromdm/nanolib v0.3.0 h1:65xIafn1hP4izGTcLhECpNIVAaccBur7t6wvIrOewSs=
15+
github.com/micromdm/nanolib v0.3.0/go.mod h1:FwBKCvvphgYvbdUZ+qw5kay7NHJcg6zPi8W7kXNajmE=
1516
github.com/micromdm/plist v0.2.1 h1:4SoSMOVAyzv1ThT8IKLgXLJEKezLkcVDN6wivqTTFdo=
1617
github.com/micromdm/plist v0.2.1/go.mod h1:flkfm0od6GzyXBqI28h5sgEyi3iPO28W2t1Zm9LpwWs=
18+
github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU=
1719
github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o=
1820
github.com/smallstep/pkcs7 v0.2.1 h1:6Kfzr/QizdIuB6LSv8y1LJdZ3aPSfTNhTLqAx9CTLfA=
1921
github.com/smallstep/pkcs7 v0.2.1/go.mod h1:RcXHsMfL+BzH8tRhmrF1NkkpebKpq3JEM66cOFxanf0=

storage/all.go

-13
This file was deleted.

storage/bstoken.go

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package storage
2+
3+
import "github.com/micromdm/nanomdm/mdm"
4+
5+
type BootstrapTokenStore interface {
6+
StoreBootstrapToken(r *mdm.Request, msg *mdm.SetBootstrapToken) error
7+
8+
// RetrieveBootstrapToken retrieves the previously-escrowed Bootstrap Token.
9+
// If a token has not yet been escrowed then a nil token and no error should be returned.
10+
RetrieveBootstrapToken(r *mdm.Request, msg *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error)
11+
}

storage/certauth.go

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package storage
2+
3+
import (
4+
"context"
5+
6+
"github.com/micromdm/nanomdm/mdm"
7+
)
8+
9+
// CertAuthStore stores and retrieves cert-to-enrollment associations.
10+
// The request enrollment ID should be normalized for just the device channel.
11+
// The hash parameter, when present, is likely (but not required) to be
12+
// a 64-charachter hex string representation of a SHA-256 digest.
13+
type CertAuthStore interface {
14+
// HasCertHash checks if hash has ever been associated to any enrollment.
15+
HasCertHash(r *mdm.Request, hash string) (has bool, err error)
16+
17+
// EnrollmentHasCertHash checks that r.ID has any hash associated.
18+
// The hash parameter can usually be ignored.
19+
EnrollmentHasCertHash(r *mdm.Request, hash string) (bool, error)
20+
21+
// IsCertHashAssociated checks that r.ID is associated to hash.
22+
IsCertHashAssociated(r *mdm.Request, hash string) (bool, error)
23+
24+
// AssociateCertHash associates r.ID with hash.
25+
// Here hash is a cryptographic hash of the request certificate.
26+
AssociateCertHash(r *mdm.Request, hash string) error
27+
}
28+
29+
type CertAuthRetriever interface {
30+
// EnrollmentFromHash retrieves a normalized enrollment ID from a cert hash.
31+
// The hash parameter, when present, is likely (but not required) to be
32+
// a 64-charachter hex string representation of a SHA-256 digest.
33+
// Implementations should return an empty string if no result is found.
34+
EnrollmentFromHash(ctx context.Context, hash string) (string, error)
35+
}

storage/diskv/diskv.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Package diskv implements a NanoMDM storage backend using the diskv key-value store.
2+
package diskv
3+
4+
import (
5+
"path/filepath"
6+
"strings"
7+
8+
"github.com/micromdm/nanomdm/storage/kv"
9+
10+
nlkv "github.com/micromdm/nanolib/storage/kv"
11+
"github.com/micromdm/nanolib/storage/kv/kvdiskv"
12+
"github.com/micromdm/nanolib/storage/kv/kvtxn"
13+
"github.com/peterbourgon/diskv/v3"
14+
)
15+
16+
// Diskv is a storage backend that uses diskv.
17+
type Diskv struct {
18+
*kv.KV
19+
}
20+
21+
// Split2X2Transform splits key into a path like /00/01 for a key of "0001".
22+
// The key will be prefixed with zeros if its length is less than 4.
23+
func Split2X2Transform(key string) []string {
24+
if len(key) < 4 {
25+
key = strings.Repeat("0", 4-len(key)) + key
26+
}
27+
return []string{key[0:2], key[2:4]}
28+
}
29+
30+
// StripPrefixTransform wraps next in a function that trims prefix from the key.
31+
func StripPrefixTransform(next diskv.TransformFunction, prefix string) diskv.TransformFunction {
32+
return func(key string) []string {
33+
return next(strings.TrimPrefix(key, prefix))
34+
}
35+
}
36+
37+
func newBucket(path, name string) nlkv.TxnBucketWithCRUD {
38+
return newBucketWithTransform(path, name, Split2X2Transform)
39+
}
40+
41+
func newBucketWithTransform(path, name string, transform diskv.TransformFunction) nlkv.TxnBucketWithCRUD {
42+
return kvtxn.New(kvdiskv.New(diskv.New(diskv.Options{
43+
BasePath: filepath.Join(path, name),
44+
Transform: transform,
45+
CacheSizeMax: 1024 * 1024,
46+
})))
47+
}
48+
49+
// New creates a new storage backend that uses diskv.
50+
func New(path string) *Diskv {
51+
return &Diskv{KV: kv.New(
52+
newBucket(path, "users"),
53+
newBucket(path, "cert_auth"),
54+
newBucket(path, "queue"),
55+
// try to store the push certs with transformed keys of the UUID within the Topic
56+
newBucketWithTransform(
57+
path,
58+
"push_cert",
59+
StripPrefixTransform(Split2X2Transform, "com.apple.mgmt.External."),
60+
),
61+
newBucket(path, "devices"),
62+
newBucket(path, "enrollments"),
63+
)}
64+
}

storage/diskv/diskv_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package diskv
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/micromdm/nanomdm/test/e2e"
8+
)
9+
10+
func TestDiskv(t *testing.T) {
11+
t.Run("e2e", func(t *testing.T) { e2e.TestE2E(t, context.Background(), New(t.TempDir())) })
12+
}

storage/file/migrate.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ func (s *FileStorage) RetrieveMigrationCheckins(_ context.Context, c chan<- inte
3636
if err != nil {
3737
c <- err
3838
}
39-
// if an Authenticate doesn't exist then this is a
40-
// user-channel enrollment. skip it for this loop
41-
if !userLoop && !authExists {
39+
// an Authenticate should not exist for a user-channel
40+
// enrollment. skip it if found.
41+
if userLoop && authExists {
4242
continue
43-
}
44-
if !userLoop {
43+
} else if !userLoop && authExists {
4544
sendCheckinMessage(e, AuthenticateFilename, c)
4645
}
4746
tokExists, err := e.fileExists(TokenUpdateFilename)

storage/inmem/inmem.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Package inmem implements an in-memory NanoMDM storage backend.
2+
package inmem
3+
4+
import (
5+
"github.com/micromdm/nanomdm/storage/kv"
6+
7+
"github.com/micromdm/nanolib/storage/kv/kvmap"
8+
"github.com/micromdm/nanolib/storage/kv/kvtxn"
9+
)
10+
11+
// InMem is an in-memory storage backend.
12+
type InMem struct {
13+
*kv.KV
14+
}
15+
16+
// New creates a new in-memory storage backend.
17+
func New() *InMem {
18+
return &InMem{KV: kv.New(
19+
kvtxn.New(kvmap.New()),
20+
kvtxn.New(kvmap.New()),
21+
kvtxn.New(kvmap.New()),
22+
kvtxn.New(kvmap.New()),
23+
kvtxn.New(kvmap.New()),
24+
kvtxn.New(kvmap.New()),
25+
)}
26+
}

0 commit comments

Comments
 (0)