diff --git a/interfaces/prompting/requestprompts/requestprompts.go b/interfaces/prompting/requestprompts/requestprompts.go index 01577090f69c..72f094639d63 100644 --- a/interfaces/prompting/requestprompts/requestprompts.go +++ b/interfaces/prompting/requestprompts/requestprompts.go @@ -103,7 +103,7 @@ type PromptDB struct { maxIDPath string mutex sync.Mutex // Function to issue a notice for a change in a prompt - notifyPrompt func(userID uint32, promptID string) error + notifyPrompt func(userID uint32, promptID string, data map[string]string) error } // New creates and returns a new prompt database. @@ -111,7 +111,7 @@ type PromptDB struct { // The given notifyPrompt closure should record a notice of type // "interfaces-requests-prompt" for the given user with the given // promptID as its key. -func New(notifyPrompt func(userID uint32, promptID string) error) *PromptDB { +func New(notifyPrompt func(userID uint32, promptID string, data map[string]string) error) *PromptDB { pdb := PromptDB{ perUser: make(map[uint32]*userPromptDB), notifyPrompt: notifyPrompt, @@ -212,7 +212,7 @@ func (pdb *PromptDB) AddOrMerge(metadata *prompting.Metadata, path string, permi // have replied with a malformed response and not retried after // receiving the error, so this notice encourages it to try again // if the user retries the operation. - pdb.notifyPrompt(metadata.User, prompt.ID) + pdb.notifyPrompt(metadata.User, prompt.ID, nil) return prompt, true } } @@ -228,7 +228,7 @@ func (pdb *PromptDB) AddOrMerge(metadata *prompting.Metadata, path string, permi listenerReqs: []*listener.Request{listenerReq}, } userEntry.ByID[id] = prompt - pdb.notifyPrompt(metadata.User, id) + pdb.notifyPrompt(metadata.User, id, nil) return prompt, false } @@ -293,7 +293,9 @@ func (pdb *PromptDB) Reply(user uint32, id string, outcome prompting.OutcomeType } } delete(userEntry.ByID, id) - pdb.notifyPrompt(user, id) + data := make(map[string]string) + data["resolved"] = "replied" + pdb.notifyPrompt(user, id, data) return prompt, nil } @@ -344,8 +346,8 @@ func (pdb *PromptDB) HandleNewRule(metadata *prompting.Metadata, constraints *pr if !modified { continue } - pdb.notifyPrompt(metadata.User, id) if len(prompt.Constraints.Permissions) > 0 && allow == true { + pdb.notifyPrompt(metadata.User, id, nil) continue } // All permissions of prompt satisfied, or any permission denied @@ -354,6 +356,9 @@ func (pdb *PromptDB) HandleNewRule(metadata *prompting.Metadata, constraints *pr } delete(userEntry.ByID, id) satisfiedPromptIDs = append(satisfiedPromptIDs, id) + data := make(map[string]string) + data["resolved"] = "satisfied" + pdb.notifyPrompt(metadata.User, id, data) } return satisfiedPromptIDs, nil } @@ -363,11 +368,13 @@ func (pdb *PromptDB) HandleNewRule(metadata *prompting.Metadata, constraints *pr // This should be called when snapd is shutting down, to notify prompt clients // that the given prompts are no longer awaiting a reply. func (pdb *PromptDB) Close() { + data := make(map[string]string) + data["resolved"] = "cancelled" pdb.mutex.Lock() defer pdb.mutex.Unlock() for user, userEntry := range pdb.perUser { for id := range userEntry.ByID { - pdb.notifyPrompt(user, id) + pdb.notifyPrompt(user, id, data) } } // Clear all outstanding prompts diff --git a/interfaces/prompting/requestprompts/requestprompts_test.go b/interfaces/prompting/requestprompts/requestprompts_test.go index 190be3422adb..a2b5a17c4df6 100644 --- a/interfaces/prompting/requestprompts/requestprompts_test.go +++ b/interfaces/prompting/requestprompts/requestprompts_test.go @@ -20,10 +20,11 @@ package requestprompts_test import ( + "cmp" "fmt" "os" "path/filepath" - "sort" + "slices" "testing" "time" @@ -39,10 +40,15 @@ import ( func Test(t *testing.T) { TestingT(t) } +type noticeInfo struct { + promptID string + data map[string]string +} + type requestpromptsSuite struct { - defaultNotifyPrompt func(userID uint32, promptID string) error + defaultNotifyPrompt func(userID uint32, promptID string, data map[string]string) error defaultUser uint32 - noticePromptIDs []string + promptNotices []*noticeInfo tmpdir string } @@ -51,19 +57,23 @@ var _ = Suite(&requestpromptsSuite{}) func (s *requestpromptsSuite) SetUpTest(c *C) { s.defaultUser = 1000 - s.defaultNotifyPrompt = func(userID uint32, promptID string) error { + s.defaultNotifyPrompt = func(userID uint32, promptID string, data map[string]string) error { c.Check(userID, Equals, s.defaultUser) - s.noticePromptIDs = append(s.noticePromptIDs, promptID) + info := ¬iceInfo{ + promptID: promptID, + data: data, + } + s.promptNotices = append(s.promptNotices, info) return nil } - s.noticePromptIDs = make([]string, 0) + s.promptNotices = make([]*noticeInfo, 0) s.tmpdir = c.MkDir() dirs.SetRootDir(s.tmpdir) c.Assert(os.MkdirAll(dirs.SnapRunDir, 0700), IsNil) } func (s *requestpromptsSuite) TestNew(c *C) { - notifyPrompt := func(userID uint32, promptID string) error { + notifyPrompt := func(userID uint32, promptID string, data map[string]string) error { c.Fatalf("unexpected notice with userID %d and ID %s", userID, promptID) return nil } @@ -73,7 +83,7 @@ func (s *requestpromptsSuite) TestNew(c *C) { } func (s *requestpromptsSuite) TestLoadMaxID(c *C) { - notifyPrompt := func(userID uint32, promptID string) error { + notifyPrompt := func(userID uint32, promptID string, data map[string]string) error { c.Fatalf("unexpected notice with userID %d and ID %s", userID, promptID) return nil } @@ -194,7 +204,7 @@ func (s *requestpromptsSuite) TestAddOrMerge(c *C) { after := time.Now() c.Assert(merged, Equals, false) - s.checkNewNotices(c, []string{prompt1.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID}, nil) c.Check(pdb.MaxID(), Equals, uint64(1)) s.checkWrittenMaxID(c, prompt1.ID) @@ -203,7 +213,7 @@ func (s *requestpromptsSuite) TestAddOrMerge(c *C) { c.Assert(prompt2, Equals, prompt1) // Merged prompts should re-record notice - s.checkNewNotices(c, []string{prompt1.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID}, nil) // Merged prompts should not advance the max ID c.Check(pdb.MaxID(), Equals, uint64(1)) s.checkWrittenMaxID(c, prompt1.ID) @@ -225,28 +235,51 @@ func (s *requestpromptsSuite) TestAddOrMerge(c *C) { c.Check(storedPrompt, Equals, prompt1) // Looking up prompt should not record notice - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) prompt3, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq3) c.Check(merged, Equals, true) c.Check(prompt3, Equals, prompt1) // Merged prompts should re-record notice - s.checkNewNotices(c, []string{prompt1.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID}, nil) // Merged prompts should not advance the max ID c.Check(pdb.MaxID(), Equals, uint64(1)) s.checkWrittenMaxID(c, prompt1.ID) } -func (s *requestpromptsSuite) checkNewNotices(c *C, expectedPromptIDs []string) { - c.Check(s.noticePromptIDs, DeepEquals, expectedPromptIDs) - s.noticePromptIDs = s.noticePromptIDs[:0] +func (s *requestpromptsSuite) checkNewNoticesSimple(c *C, expectedPromptIDs []string, expectedData map[string]string) { + s.checkNewNotices(c, applyNotices(expectedPromptIDs, expectedData)) +} + +func applyNotices(expectedPromptIDs []string, expectedData map[string]string) []*noticeInfo { + expectedNotices := make([]*noticeInfo, len(expectedPromptIDs)) + for i, id := range expectedPromptIDs { + info := ¬iceInfo{ + promptID: id, + data: expectedData, + } + expectedNotices[i] = info + } + return expectedNotices +} + +func (s *requestpromptsSuite) checkNewNotices(c *C, expectedNotices []*noticeInfo) { + c.Check(s.promptNotices, DeepEquals, expectedNotices) + s.promptNotices = s.promptNotices[:0] } -func (s *requestpromptsSuite) checkNewNoticesUnordered(c *C, expectedPromptIDs []string) { - sort.Strings(s.noticePromptIDs) - sort.Strings(expectedPromptIDs) - s.checkNewNotices(c, expectedPromptIDs) +func (s *requestpromptsSuite) checkNewNoticesUnorderedSimple(c *C, expectedPromptIDs []string, expectedData map[string]string) { + s.checkNewNoticesUnordered(c, applyNotices(expectedPromptIDs, expectedData)) +} + +func (s *requestpromptsSuite) checkNewNoticesUnordered(c *C, expectedNotices []*noticeInfo) { + sortFunc := func(a, b *noticeInfo) int { + return cmp.Compare(a.promptID, b.promptID) + } + slices.SortFunc(s.promptNotices, sortFunc) + slices.SortFunc(expectedNotices, sortFunc) + s.checkNewNotices(c, expectedNotices) } func (s *requestpromptsSuite) TestPromptWithIDErrors(c *C) { @@ -271,7 +304,7 @@ func (s *requestpromptsSuite) TestPromptWithIDErrors(c *C) { prompt, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt.ID}) + s.checkNewNoticesSimple(c, []string{prompt.ID}, nil) result, err := pdb.PromptWithID(metadata.User, prompt.ID) c.Check(err, IsNil) @@ -286,7 +319,7 @@ func (s *requestpromptsSuite) TestPromptWithIDErrors(c *C) { c.Check(result, IsNil) // Looking up prompts (with or without errors) should not record notices - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) } func (s *requestpromptsSuite) TestReply(c *C) { @@ -316,14 +349,14 @@ func (s *requestpromptsSuite) TestReply(c *C) { prompt1, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq1) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt1.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID}, nil) prompt2, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq2) c.Check(merged, Equals, true) c.Check(prompt2, Equals, prompt1) // Merged prompts should re-record notice - s.checkNewNotices(c, []string{prompt1.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID}, nil) repliedPrompt, err := pdb.Reply(metadata.User, prompt1.ID, outcome) c.Check(err, IsNil) @@ -339,7 +372,8 @@ func (s *requestpromptsSuite) TestReply(c *C) { c.Check(allowed, Equals, expected) } - s.checkNewNotices(c, []string{repliedPrompt.ID}) + expectedData := map[string]string{"resolved": "replied"} + s.checkNewNoticesSimple(c, []string{repliedPrompt.ID}, expectedData) } } @@ -379,7 +413,7 @@ func (s *requestpromptsSuite) TestReplyErrors(c *C) { prompt, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt.ID}) + s.checkNewNoticesSimple(c, []string{prompt.ID}, nil) outcome := prompting.OutcomeAllow @@ -396,7 +430,7 @@ func (s *requestpromptsSuite) TestReplyErrors(c *C) { c.Check(err, Equals, fakeError) // Failed replies should not record notice - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) } func (s *requestpromptsSuite) TestHandleNewRuleAllowPermissions(c *C) { @@ -438,7 +472,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleAllowPermissions(c *C) { prompt4, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq4) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt1.ID, prompt2.ID, prompt3.ID, prompt4.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID, prompt2.ID, prompt3.ID, prompt4.ID}, nil) stored := pdb.Prompts(metadata.User) c.Assert(stored, HasLen, 4) @@ -459,7 +493,11 @@ func (s *requestpromptsSuite) TestHandleNewRuleAllowPermissions(c *C) { // Read and write permissions of prompt1 satisfied, so notice re-issued, // but it has one remaining permission. prompt2 and prompt3 fully satisfied. - s.checkNewNoticesUnordered(c, []string{prompt1.ID, prompt2.ID, prompt3.ID}) + e1 := ¬iceInfo{promptID: prompt1.ID, data: nil} + e2 := ¬iceInfo{promptID: prompt2.ID, data: map[string]string{"resolved": "satisfied"}} + e3 := ¬iceInfo{promptID: prompt3.ID, data: map[string]string{"resolved": "satisfied"}} + expectedNotices := []*noticeInfo{e1, e2, e3} + s.checkNewNoticesUnordered(c, expectedNotices) for i := 0; i < 2; i++ { satisfiedReq, result, err := s.waitForListenerReqAndReply(c, listenerReqChan, replyChan) @@ -487,7 +525,8 @@ func (s *requestpromptsSuite) TestHandleNewRuleAllowPermissions(c *C) { c.Check(satisfied, HasLen, 1) c.Check(satisfied[0], Equals, prompt1.ID) - s.checkNewNotices(c, []string{prompt1.ID}) + expectedData := map[string]string{"resolved": "satisfied"} + s.checkNewNoticesSimple(c, []string{prompt1.ID}, expectedData) satisfiedReq, result, err := s.waitForListenerReqAndReply(c, listenerReqChan, replyChan) c.Check(err, IsNil) @@ -536,7 +575,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleDenyPermissions(c *C) { prompt4, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq4) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt1.ID, prompt2.ID, prompt3.ID, prompt4.ID}) + s.checkNewNoticesSimple(c, []string{prompt1.ID, prompt2.ID, prompt3.ID, prompt4.ID}, nil) stored := pdb.Prompts(metadata.User) c.Assert(stored, HasLen, 4) @@ -557,7 +596,8 @@ func (s *requestpromptsSuite) TestHandleNewRuleDenyPermissions(c *C) { c.Check(strutil.ListContains(satisfied, prompt2.ID), Equals, true) c.Check(strutil.ListContains(satisfied, prompt3.ID), Equals, true) - s.checkNewNoticesUnordered(c, []string{prompt1.ID, prompt2.ID, prompt3.ID}) + expectedData := map[string]string{"resolved": "satisfied"} + s.checkNewNoticesUnorderedSimple(c, []string{prompt1.ID, prompt2.ID, prompt3.ID}, expectedData) for i := 0; i < 3; i++ { satisfiedReq, result, err := s.waitForListenerReqAndReply(c, listenerReqChan, replyChan) @@ -600,7 +640,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { prompt, merged := pdb.AddOrMerge(metadata, path, permissions, listenerReq) c.Check(merged, Equals, false) - s.checkNewNotices(c, []string{prompt.ID}) + s.checkNewNoticesSimple(c, []string{prompt.ID}, nil) pathPattern := "/home/test/Documents/**" constraints := &prompting.Constraints{ @@ -632,7 +672,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { c.Check(err, ErrorMatches, `internal error: invalid outcome.*`) c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) otherUserMetadata := &prompting.Metadata{ User: otherUser, @@ -643,7 +683,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { c.Check(err, IsNil) c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) otherSnapMetadata := &prompting.Metadata{ User: user, @@ -654,7 +694,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { c.Check(err, IsNil) c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) otherInterfaceMetadata := &prompting.Metadata{ User: user, @@ -665,7 +705,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { c.Check(err, IsNil) c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) // TODO: change this once path pattern matching lands match, _ := otherConstraints.Match(path) // delete this once matching lands. @@ -674,7 +714,7 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { //c.Check(err, IsNil) //c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) // TODO: change this once path pattern validation lands c.Check(badConstraints.ValidateForInterface(metadata.Interface), IsNil) // expected to fail once validation lands @@ -682,13 +722,14 @@ func (s *requestpromptsSuite) TestHandleNewRuleNonMatches(c *C) { //c.Check(err, ErrorMatches, "syntax error in pattern") //c.Check(satisfied, HasLen, 0) - s.checkNewNotices(c, []string{}) + s.checkNewNoticesSimple(c, []string{}, nil) satisfied, err = pdb.HandleNewRule(metadata, constraints, outcome) c.Check(err, IsNil) c.Assert(satisfied, HasLen, 1) - s.checkNewNotices(c, []string{prompt.ID}) + expectedData := map[string]string{"resolved": "satisfied"} + s.checkNewNoticesSimple(c, []string{prompt.ID}, expectedData) satisfiedReq, result, err := s.waitForListenerReqAndReply(c, listenerReqChan, replyChan) c.Check(err, IsNil) @@ -738,12 +779,13 @@ func (s *requestpromptsSuite) TestClose(c *C) { c.Check(pdb.MaxID(), Equals, uint64(3)) // One notice for each prompt when created - s.checkNewNotices(c, expectedPromptIDs) + s.checkNewNoticesSimple(c, expectedPromptIDs, nil) pdb.Close() // Once notice for each prompt when cleaned up - s.checkNewNoticesUnordered(c, expectedPromptIDs) + expectedData := map[string]string{"resolved": "cancelled"} + s.checkNewNoticesUnorderedSimple(c, expectedPromptIDs, expectedData) // All prompts have been cleared, and all per-user maps deleted c.Check(pdb.PerUser(), HasLen, 0)