Skip to content

Commit

Permalink
o/state,daemon: add snap-run-inhibit notice
Browse files Browse the repository at this point in the history
Signed-off-by: Zeyad Gouda <[email protected]>
  • Loading branch information
ZeyadYasser committed Mar 27, 2024
1 parent 7b30c56 commit 0e74216
Show file tree
Hide file tree
Showing 5 changed files with 221 additions and 11 deletions.
65 changes: 62 additions & 3 deletions daemon/api_notices.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package daemon

import (
"context"
"encoding/json"
"errors"
"fmt"
"math"
Expand All @@ -31,13 +32,16 @@ import (
var noticeReadInterfaces = map[state.NoticeType][]string{
state.ChangeUpdateNotice: {"snap-refresh-observe"},
state.RefreshInhibitNotice: {"snap-refresh-observe"},
state.SnapRunInhibitNotice: {"snap-refresh-observe"},
}

var (
noticesCmd = &Command{
Path: "/v2/notices",
GET: getNotices,
ReadAccess: interfaceOpenAccess{Interface: "snap-refresh-observe"},
Path: "/v2/notices",
GET: getNotices,
POST: postNotices,
ReadAccess: interfaceOpenAccess{Interface: "snap-refresh-observe"},
WriteAccess: authenticatedAccess{Polkit: polkitActionManage},
}

noticeCmd = &Command{
Expand All @@ -47,6 +51,14 @@ var (
}
)

const (
maxNoticeKeyLength = 256
)

type addedNotice struct {
ID string `json:"id"`
}

func getNotices(c *Command, r *http.Request, user *auth.UserState) Response {
query := r.URL.Query()

Expand Down Expand Up @@ -218,6 +230,53 @@ func allowedNoticeTypesForInterface(iface string) []state.NoticeType {
return types
}

func postNotices(c *Command, r *http.Request, user *auth.UserState) Response {
requestUID, err := uidFromRequest(r)
if err != nil {
return Forbidden("cannot determine UID of request, so cannot create notice")
}

var payload struct {
Action string `json:"action"`
Type string `json:"type"`
Key string `json:"key"`
// NOTE: Data and RepeatAfter fields are not needed for snap-run-inhibit notices.
}
decoder := json.NewDecoder(r.Body)
if err := decoder.Decode(&payload); err != nil {
return BadRequest("cannot decode request body: %v", err)
}

if payload.Action != "add" {
return BadRequest("invalid action %q", payload.Action)
}
if payload.Type != "snap-run-inhibit" {
return BadRequest(`invalid type %q (can only add "snap-run-inhibit" notices)`, payload.Type)
}
if len(payload.Key) > maxNoticeKeyLength {
return BadRequest("key must be %d bytes or less", maxNoticeKeyLength)
}

st := c.d.overlord.State()
st.Lock()
defer st.Unlock()

exists, err := snapInstanceExists(st, payload.Key)
if err != nil {
return InternalError("cannot check snap in state: %v", err)
}
if !exists {
return BadRequest("snap %q does not exist", payload.Key)
}

noticeId, err := st.AddNotice(&requestUID, state.SnapRunInhibitNotice, payload.Key, nil)
if err != nil {
return InternalError("%v", err)
}

return SyncResponse(addedNotice{ID: noticeId})
}

func getNotice(c *Command, r *http.Request, user *auth.UserState) Response {
requestUID, err := uidFromRequest(r)
if err != nil {
Expand Down
135 changes: 130 additions & 5 deletions daemon/api_notices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,24 @@
package daemon_test

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"

. "gopkg.in/check.v1"

"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
)

Expand All @@ -41,6 +46,7 @@ func (s *noticesSuite) SetUpTest(c *C) {
s.apiBaseSuite.SetUpTest(c)

s.expectReadAccess(daemon.InterfaceOpenAccess{Interface: "snap-refresh-observe"})
s.expectWriteAccess(daemon.AuthenticatedAccess{Polkit: "io.snapcraft.snapd.manage"})
}

func (s *noticesSuite) TestNoticesFilterUserID(c *C) {
Expand Down Expand Up @@ -232,6 +238,7 @@ func (s *noticesSuite) TestNoticesShowsTypesAllowedForSnap(c *C) {
addNotice(c, st, nil, state.ChangeUpdateNotice, "123", nil)
addNotice(c, st, nil, state.RefreshInhibitNotice, "-", nil)
addNotice(c, st, nil, state.WarningNotice, "danger", nil)
addNotice(c, st, nil, state.SnapRunInhibitNotice, "snap-name", nil)
st.Unlock()

// Check that a snap request without specifying types filter only shows
Expand All @@ -255,7 +262,7 @@ func (s *noticesSuite) TestNoticesShowsTypesAllowedForSnap(c *C) {
c.Check(rsp.Status, Equals, 200)
notices, ok = rsp.Result.([]*state.Notice)
c.Assert(ok, Equals, true)
c.Assert(notices, HasLen, 2)
c.Assert(notices, HasLen, 3)

seenNoticeType := make(map[string]int)
for _, notice := range notices {
Expand All @@ -265,6 +272,7 @@ func (s *noticesSuite) TestNoticesShowsTypesAllowedForSnap(c *C) {
}
c.Check(seenNoticeType["change-update"], Equals, 1)
c.Check(seenNoticeType["refresh-inhibit"], Equals, 1)
c.Check(seenNoticeType["snap-run-inhibit"], Equals, 1)
}

func (s *noticesSuite) TestNoticesFilterTypesForSnap(c *C) {
Expand All @@ -275,20 +283,21 @@ func (s *noticesSuite) TestNoticesFilterTypesForSnap(c *C) {
addNotice(c, st, nil, state.ChangeUpdateNotice, "123", nil)
addNotice(c, st, nil, state.RefreshInhibitNotice, "-", nil)
addNotice(c, st, nil, state.WarningNotice, "danger", nil)
addNotice(c, st, nil, state.SnapRunInhibitNotice, "snap-name", nil)
st.Unlock()

// Check that a snap request with types filter allows access to
// snaps with required interfaces only.

// snap-refresh-observe interface allows accessing change-update notices
req, err := http.NewRequest("GET", "/v2/notices?types=change-update,refresh-inhibit", nil)
// snap-refresh-observe interface allows accessing change-update, refresh-inhibir and snap-run-inhibit notices
req, err := http.NewRequest("GET", "/v2/notices?types=change-update,refresh-inhibit,snap-run-inhibit", nil)
c.Assert(err, IsNil)
req.RemoteAddr = fmt.Sprintf("pid=100;uid=1000;socket=%s;iface=snap-refresh-observe;", dirs.SnapSocket)
rsp := s.syncReq(c, req, nil)
c.Check(rsp.Status, Equals, 200)
notices, ok := rsp.Result.([]*state.Notice)
c.Assert(ok, Equals, true)
c.Assert(notices, HasLen, 2)
c.Assert(notices, HasLen, 3)

seenNoticeType := make(map[string]int)
for _, notice := range notices {
Expand All @@ -298,6 +307,7 @@ func (s *noticesSuite) TestNoticesFilterTypesForSnap(c *C) {
}
c.Check(seenNoticeType["change-update"], Equals, 1)
c.Check(seenNoticeType["refresh-inhibit"], Equals, 1)
c.Check(seenNoticeType["snap-run-inhibit"], Equals, 1)
}

func (s *noticesSuite) TestNoticesFilterTypesForSnapForbidden(c *C) {
Expand All @@ -308,6 +318,7 @@ func (s *noticesSuite) TestNoticesFilterTypesForSnapForbidden(c *C) {
addNotice(c, st, nil, state.ChangeUpdateNotice, "123", nil)
addNotice(c, st, nil, state.RefreshInhibitNotice, "-", nil)
addNotice(c, st, nil, state.WarningNotice, "danger", nil)
addNotice(c, st, nil, state.SnapRunInhibitNotice, "snap-name", nil)
st.Unlock()

// Check that a snap request with types filter denies access to
Expand Down Expand Up @@ -341,8 +352,15 @@ func (s *noticesSuite) TestNoticesFilterTypesForSnapForbidden(c *C) {
rsp = s.errorReq(c, req, nil)
c.Check(rsp.Status, Equals, 403)

// snap-themes-control doesn't give access to snap-run-inhibit notices.
req, err = http.NewRequest("GET", "/v2/notices?types=snap-run-inhibit", nil)
c.Assert(err, IsNil)
req.RemoteAddr = fmt.Sprintf("pid=100;uid=1000;socket=%s;iface=snap-themes-control;", dirs.SnapSocket)
rsp = s.errorReq(c, req, nil)
c.Check(rsp.Status, Equals, 403)

// No interfaces connected.
req, err = http.NewRequest("GET", "/v2/notices?types=change-update,refresh-inhibit", nil)
req, err = http.NewRequest("GET", "/v2/notices?types=change-update,refresh-inhibit,snap-run-inhibit", nil)
c.Assert(err, IsNil)
req.RemoteAddr = fmt.Sprintf("pid=100;uid=1000;socket=%s;iface=;", dirs.SnapSocket)
rsp = s.errorReq(c, req, nil)
Expand Down Expand Up @@ -663,6 +681,113 @@ func (s *noticesSuite) testNoticesBadRequest(c *C, query, errorMatch string) {
c.Assert(rsp.Message, Matches, errorMatch)
}

func (s *noticesSuite) TestAddNotice(c *C) {
s.daemon(c)

st := s.d.Overlord().State()
st.Lock()
// mock existing snap
snapstate.Set(st, "snap-name", &snapstate.SnapState{
Active: true,
Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{{RealName: "snap-name", Revision: snap.R(2)}}),
})
st.Unlock()

start := time.Now()
body := []byte(`{
"action": "add",
"type": "snap-run-inhibit",
"key": "snap-name"
}`)
req, err := http.NewRequest("POST", "/v2/notices", bytes.NewReader(body))
c.Assert(err, IsNil)
req.RemoteAddr = "pid=100;uid=1000;socket=;"
rsp := s.syncReq(c, req, nil)
c.Assert(rsp.Status, Equals, 200)

resultBytes, err := json.Marshal(rsp.Result)
c.Assert(err, IsNil)

st.Lock()
notices := st.Notices(nil)
st.Unlock()
c.Assert(notices, HasLen, 1)
n := noticeToMap(c, notices[0])
noticeID, ok := n["id"].(string)
c.Assert(ok, Equals, true)
c.Assert(string(resultBytes), Equals, `{"id":"`+noticeID+`"}`)

firstOccurred, err := time.Parse(time.RFC3339, n["first-occurred"].(string))
c.Assert(err, IsNil)
c.Assert(firstOccurred.After(start), Equals, true)
lastOccurred, err := time.Parse(time.RFC3339, n["last-occurred"].(string))
c.Assert(err, IsNil)
c.Assert(lastOccurred.Equal(firstOccurred), Equals, true)
lastRepeated, err := time.Parse(time.RFC3339, n["last-repeated"].(string))
c.Assert(err, IsNil)
c.Assert(lastRepeated.Equal(firstOccurred), Equals, true)

delete(n, "first-occurred")
delete(n, "last-occurred")
delete(n, "last-repeated")
c.Assert(n, DeepEquals, map[string]any{
"id": noticeID,
"user-id": 1000.0,
"type": "snap-run-inhibit",
"key": "snap-name",
"occurrences": 1.0,
"expire-after": "168h0m0s",
})
}

func (s *noticesSuite) TestAddNoticeInvalidRequestUid(c *C) {
s.daemon(c)

body := []byte(`{
"action": "add",
"type": "snap-run-inhibit",
"key": "snap-name"
}`)
req, err := http.NewRequest("POST", "/v2/notices", bytes.NewReader(body))
c.Assert(err, IsNil)
req.RemoteAddr = "pid=100;uid=;socket=;"
rsp := s.errorReq(c, req, nil)
c.Assert(rsp.Status, Equals, 403)
}

func (s *noticesSuite) TestAddNoticeInvalidAction(c *C) {
s.testAddNoticeBadRequest(c, `{"action": "bad"}`, "invalid action.*")
}

func (s *noticesSuite) TestAddNoticeInvalidType(c *C) {
s.testAddNoticeBadRequest(c, `{"action": "add", "type": "foo"}`, "invalid type.*")
}

func (s *noticesSuite) TestAddNoticeKeyTooLong(c *C) {
request, err := json.Marshal(map[string]any{
"action": "add",
"type": "snap-run-inhibit",
"key": strings.Repeat("x", 257),
})
c.Assert(err, IsNil)
s.testAddNoticeBadRequest(c, string(request), "key must be 256 bytes or less")
}

func (s *noticesSuite) TestAddNoticeInvalidSnap(c *C) {
s.testAddNoticeBadRequest(c, `{"action": "add", "type": "snap-run-inhibit", "key": "snap-name"}`, `snap "snap-name" does not exist`)
}

func (s *noticesSuite) testAddNoticeBadRequest(c *C, body, errorMatch string) {
s.daemon(c)

req, err := http.NewRequest("POST", "/v2/notices", strings.NewReader(body))
c.Assert(err, IsNil)
req.RemoteAddr = "pid=100;uid=1000;socket=;"
rsp := s.errorReq(c, req, nil)
c.Check(rsp.Status, Equals, 400)
c.Assert(rsp.Message, Matches, errorMatch)
}

func (s *noticesSuite) TestNotice(c *C) {
s.daemon(c)

Expand Down
13 changes: 13 additions & 0 deletions daemon/snap.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,3 +284,16 @@ func snapIcon(info snap.PlaceInfo) string {

return found[0]
}

func snapInstanceExists(st *state.State, name string) (bool, error) {
var snapst snapstate.SnapState
err := snapstate.Get(st, name, &snapst)
if errors.Is(err, state.ErrNoState) {
return false, nil
}
if err != nil {
return false, err
}

return true, nil
}
5 changes: 4 additions & 1 deletion overlord/state/notices.go
Original file line number Diff line number Diff line change
Expand Up @@ -199,11 +199,14 @@ const (

// Recorded whenever an auto-refresh is inhibited for one or more snaps.
RefreshInhibitNotice NoticeType = "refresh-inhibit"

// Recorded whenever "snap run" is inhibited due refresh.
SnapRunInhibitNotice NoticeType = "snap-run-inhibit"
)

func (t NoticeType) Valid() bool {
switch t {
case ChangeUpdateNotice, WarningNotice, RefreshInhibitNotice:
case ChangeUpdateNotice, WarningNotice, RefreshInhibitNotice, SnapRunInhibitNotice:
return true
}
return false
Expand Down
14 changes: 12 additions & 2 deletions overlord/state/notices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,16 @@ func (s *noticesSuite) TestNoticesFilterType(c *C) {
addNotice(c, st, nil, state.WarningNotice, "Warning 1!", nil)
time.Sleep(time.Microsecond)
addNotice(c, st, nil, state.WarningNotice, "Warning 2!", nil)
time.Sleep(time.Microsecond)
addNotice(c, st, nil, state.SnapRunInhibitNotice, "snap-name", nil)

// No filter
notices := st.Notices(nil)
c.Assert(notices, HasLen, 4)
c.Assert(notices, HasLen, 5)

// No types
notices = st.Notices(&state.NoticeFilter{})
c.Assert(notices, HasLen, 4)
c.Assert(notices, HasLen, 5)

// One type
notices = st.Notices(&state.NoticeFilter{Types: []state.NoticeType{state.WarningNotice}})
Expand Down Expand Up @@ -323,6 +325,14 @@ func (s *noticesSuite) TestNoticesFilterType(c *C) {
c.Check(n["type"], Equals, "refresh-inhibit")
c.Check(n["key"], Equals, "-")

// Another type
notices = st.Notices(&state.NoticeFilter{Types: []state.NoticeType{state.SnapRunInhibitNotice}})
c.Assert(notices, HasLen, 1)
n = noticeToMap(c, notices[0])
c.Check(n["user-id"], Equals, nil)
c.Check(n["type"], Equals, "snap-run-inhibit")
c.Check(n["key"], Equals, "snap-name")

// Multiple types
notices = st.Notices(&state.NoticeFilter{Types: []state.NoticeType{
state.ChangeUpdateNotice,
Expand Down

0 comments on commit 0e74216

Please sign in to comment.