Skip to content

Commit 5a50cbb

Browse files
committed
o/confdbstate,o/devicemgmtstate: add confdb handler for device management
1 parent d7c520b commit 5a50cbb

11 files changed

Lines changed: 755 additions & 15 deletions

File tree

overlord/assertstate/assertstate.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
"github.com/snapcore/snapd/httputil"
3535
"github.com/snapcore/snapd/logger"
3636
"github.com/snapcore/snapd/overlord/confdbstate"
37+
"github.com/snapcore/snapd/overlord/devicemgmtstate"
3738
"github.com/snapcore/snapd/overlord/snapstate"
3839
"github.com/snapcore/snapd/overlord/state"
3940
"github.com/snapcore/snapd/release"
@@ -406,6 +407,11 @@ func delayedCrossMgrInit() {
406407
// wire confdbstate helpers that look up confdb-schema assertions
407408
confdbstate.AssertstateFetchConfdbSchemaAssertion = FetchConfdbSchemaAssertion
408409
confdbstate.AssertstateConfdbSchema = ConfdbSchema
410+
// wire devicemgmtstate helpers that look up the assertion database and
411+
// account-key assertions
412+
devicemgmtstate.AssertstateDB = DB
413+
devicemgmtstate.AssertstateAccountKey = AccountKey
414+
devicemgmtstate.AssertstateFetchAccountKey = FetchAccountKey
409415
}
410416

411417
// AutoRefreshAssertions tries to refresh all assertions

overlord/confdbstate/confdbmgr.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/snapcore/snapd/confdb"
2929
"github.com/snapcore/snapd/i18n"
3030
"github.com/snapcore/snapd/logger"
31+
"github.com/snapcore/snapd/overlord/devicemgmtstate"
3132
"github.com/snapcore/snapd/overlord/hookstate"
3233
"github.com/snapcore/snapd/overlord/snapstate"
3334
"github.com/snapcore/snapd/overlord/state"
@@ -72,7 +73,7 @@ func RegisterConfdbHandler(c SystemConfdbHandler) {
7273

7374
type ConfdbManager struct{}
7475

75-
func Manager(st *state.State, hookMgr *hookstate.HookManager, runner *state.TaskRunner) *ConfdbManager {
76+
func Manager(st *state.State, hookMgr *hookstate.HookManager, runner *state.TaskRunner, mgmtMgr *devicemgmtstate.DeviceMgmtManager, device deviceBackend) *ConfdbManager {
7677
snapstate.IsConfdbHookname = IsConfdbHookname
7778
hookstate.IsConfdbHookname = IsConfdbHookname
7879

@@ -102,6 +103,8 @@ func Manager(st *state.State, hookMgr *hookstate.HookManager, runner *state.Task
102103
return &hookstate.SnapHookHandler{}
103104
})
104105

106+
mgmtMgr.RegisterHandler("confdb", &confdbMessageHandler{device: device})
107+
105108
return m
106109
}
107110

overlord/confdbstate/confdbmgr_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"github.com/snapcore/snapd/interfaces/ifacetest"
3131
"github.com/snapcore/snapd/overlord"
3232
"github.com/snapcore/snapd/overlord/confdbstate"
33+
"github.com/snapcore/snapd/overlord/devicemgmtstate"
3334
"github.com/snapcore/snapd/overlord/hookstate"
3435
"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
3536
"github.com/snapcore/snapd/overlord/state"
@@ -351,7 +352,7 @@ func (s *confdbTestSuite) TestManagerOk(c *C) {
351352
hookMgr, err := hookstate.Manager(s.state, runner)
352353
c.Assert(err, IsNil)
353354

354-
mgr := confdbstate.Manager(s.state, hookMgr, runner)
355+
mgr := confdbstate.Manager(s.state, hookMgr, runner, devicemgmtstate.Manager(s.state, runner, nil), nil)
355356
s.o.AddManager(mgr)
356357

357358
err = s.o.Settle(5 * time.Second)

overlord/confdbstate/confdbstate_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
4444
"github.com/snapcore/snapd/overlord/confdbstate"
4545
"github.com/snapcore/snapd/overlord/configstate/config"
46+
"github.com/snapcore/snapd/overlord/devicemgmtstate"
4647
"github.com/snapcore/snapd/overlord/hookstate"
4748
"github.com/snapcore/snapd/overlord/hookstate/hooktest"
4849
"github.com/snapcore/snapd/overlord/ifacestate/ifacerepo"
@@ -86,7 +87,7 @@ func (s *confdbTestSuite) SetUpTest(c *C) {
8687
confdbstate.AssertstateConfdbSchema = assertstate.ConfdbSchema
8788
confdbstate.AssertstateFetchConfdbSchemaAssertion = assertstate.FetchConfdbSchemaAssertion
8889

89-
mgr := confdbstate.Manager(s.state, hookMgr, runner)
90+
mgr := confdbstate.Manager(s.state, hookMgr, runner, devicemgmtstate.Manager(s.state, runner, nil), nil)
9091
s.o.AddManager(mgr)
9192

9293
storeSigning := assertstest.NewStoreStack("can0nical", nil)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
// -*- Mode: Go; indent-tabs-mode: t -*-
2+
3+
/*
4+
* Copyright (C) 2026 Canonical Ltd
5+
*
6+
* This program is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License version 3 as
8+
* published by the Free Software Foundation.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*
18+
*/
19+
20+
package confdbstate
21+
22+
import (
23+
"context"
24+
"encoding/json"
25+
"errors"
26+
"fmt"
27+
"strings"
28+
29+
"github.com/snapcore/snapd/asserts"
30+
"github.com/snapcore/snapd/confdb"
31+
"github.com/snapcore/snapd/overlord/devicemgmtstate"
32+
"github.com/snapcore/snapd/overlord/state"
33+
)
34+
35+
var (
36+
confdbstateGetView = GetView
37+
confdbstateReadConfdb = ReadConfdb
38+
confdbstateWriteConfdb = WriteConfdb
39+
)
40+
41+
// confdbMessageBody is the body of a confdb request message.
42+
type confdbMessageBody struct {
43+
Action string `json:"action"`
44+
Account string `json:"account"`
45+
View string `json:"view"`
46+
Keys []string `json:"keys"`
47+
Constraints map[string]any `json:"constraints"`
48+
Values map[string]any `json:"values"`
49+
}
50+
51+
// deviceBackend fetches the device's confdb-control assertion.
52+
type deviceBackend interface {
53+
ConfdbControl() (*asserts.ConfdbControl, error)
54+
}
55+
56+
// confdbMessageHandler implements devicemgmtstate.MessageHandler for the "confdb" message kind.
57+
type confdbMessageHandler struct {
58+
device deviceBackend
59+
}
60+
61+
// Validate checks that the operator sending the message has been granted
62+
// access to the requested confdb view in the device's confdb-control assertion.
63+
func (h *confdbMessageHandler) Validate(st *state.State, msg *devicemgmtstate.RequestMessage) error {
64+
var body confdbMessageBody
65+
err := json.Unmarshal([]byte(msg.Body), &body)
66+
if err != nil {
67+
return fmt.Errorf("cannot decode message body: %v", err)
68+
}
69+
70+
if body.Action != "get" && body.Action != "set" {
71+
return fmt.Errorf("cannot validate message: unknown action %q", body.Action)
72+
}
73+
74+
if body.Account == "" {
75+
return fmt.Errorf("cannot validate message: account is required")
76+
}
77+
78+
viewParts := strings.Split(body.View, "/")
79+
if len(viewParts) != 2 {
80+
return fmt.Errorf("cannot validate message: invalid view %q, expected <schema>/<view-name>", body.View)
81+
}
82+
83+
if body.Action == "set" && len(body.Values) == 0 {
84+
return fmt.Errorf("cannot validate message: body contains no values to write")
85+
}
86+
87+
cc, err := h.device.ConfdbControl()
88+
if err != nil {
89+
if errors.Is(err, state.ErrNoState) {
90+
return &devicemgmtstate.UnauthorizedError{Operator: msg.AccountID}
91+
}
92+
93+
return fmt.Errorf("cannot validate message: %v", err)
94+
}
95+
96+
// TODO: implement store authentication method. Currently, the store doesn't
97+
// support signing request messages on behalf of operators.
98+
// For now, only "operator-key" is supported.
99+
100+
ctrl := cc.Control()
101+
authMethod := []string{"operator-key"}
102+
delegated, err := ctrl.IsDelegated(msg.AccountID, body.Account+"/"+body.View, authMethod)
103+
if err != nil {
104+
return fmt.Errorf("cannot validate message: %v", err)
105+
}
106+
if !delegated {
107+
return &devicemgmtstate.UnauthorizedError{Operator: msg.AccountID}
108+
}
109+
110+
return nil
111+
}
112+
113+
// Apply schedules the confdb action described in the message and returns the change ID.
114+
func (h *confdbMessageHandler) Apply(st *state.State, msg *devicemgmtstate.RequestMessage) (string, error) {
115+
var body confdbMessageBody
116+
err := json.Unmarshal([]byte(msg.Body), &body)
117+
if err != nil {
118+
return "", fmt.Errorf("cannot decode message body: %v", err)
119+
}
120+
121+
viewParts := strings.Split(body.View, "/")
122+
view, err := confdbstateGetView(st, body.Account, viewParts[0], viewParts[1])
123+
if err != nil {
124+
return "", err
125+
}
126+
127+
var chgID string
128+
switch body.Action {
129+
case "get":
130+
chgID, err = confdbstateReadConfdb(context.Background(), st, view, body.Keys, body.Constraints, confdb.AdminAccess)
131+
case "set":
132+
chgID, err = confdbstateWriteConfdb(context.Background(), st, view, body.Values)
133+
default:
134+
return "", fmt.Errorf("cannot apply message: unknown action %q", body.Action)
135+
}
136+
if err != nil {
137+
return "", err
138+
}
139+
140+
chg := st.Change(chgID)
141+
if chg == nil {
142+
return "", fmt.Errorf("internal error: cannot find change %q created for confdb message", chgID)
143+
}
144+
devicemgmtstate.MarkChangeForMessage(chg, msg)
145+
146+
return chgID, nil
147+
}
148+
149+
// ResultFromChange returns the result of a completed confdb action.
150+
func (h *confdbMessageHandler) ResultFromChange(chg *state.Change) (map[string]any, error) {
151+
if chg.Status() == state.ErrorStatus {
152+
return nil, chg.Err()
153+
}
154+
if chg.Status() != state.DoneStatus {
155+
return nil, fmt.Errorf("internal error: unexpected change status %s", chg.Status())
156+
}
157+
158+
var apiData map[string]any
159+
err := chg.Get("api-data", &apiData)
160+
if errors.Is(err, state.ErrNoState) {
161+
if chg.Kind() == setConfdbChangeKind {
162+
return map[string]any{}, nil
163+
}
164+
165+
return nil, fmt.Errorf("internal error: change %q done with no api-data", chg.Kind())
166+
}
167+
if err != nil {
168+
return nil, err
169+
}
170+
171+
errData, hasErr := apiData["error"]
172+
if !hasErr {
173+
return apiData, nil
174+
}
175+
176+
errMap, ok := errData.(map[string]any)
177+
if !ok {
178+
return nil, fmt.Errorf("internal error: api-data error field is not a map")
179+
}
180+
181+
msg, _ := errMap["message"].(string)
182+
return nil, fmt.Errorf("%s", msg)
183+
}

0 commit comments

Comments
 (0)