Files
teleport/integrations/access/mattermost/testlib/suite.go
T
rosstimothy 4f17314a11 Initial migration to the Opaque API (#67279)
* Convert existing non-gogo codegen to the Hybrid API

Contributes to https://github.com/gravitational/teleport/issues/66776.

All existing protos explicitly set to API_OPEN have been change to
API_HBYRID. The new codegen was performed via make grpc. There are no
other functional changes to the code to start consuming the Hybrid API
those will come later. The intent is to get all Hybrid codegen in and
backported to ease the transition.

* Initial migration to the Opaque API

Contributes to https://github.com/gravitational/teleport/issues/66776.

All of the changes here are mechanical conversions generated from
`open2opaque rewrite -levels=green ./...`. There will be a follow up
to this in teleport.e which does the same. Once all changes have been
merged the process will be repeated with -levels=yellow followed by
-levels=red.


See https://protobuf.dev/reference/go/opaque-migration/ for more
details.
2026-06-05 14:52:58 +00:00

784 lines
29 KiB
Go

/*
* Teleport
* Copyright (C) 2024 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package testlib
import (
"context"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/google/uuid"
"github.com/gravitational/trace"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
accessmonitoringrulesv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/accessmonitoringrules/v1"
v1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/integrations/access/accessrequest"
"github.com/gravitational/teleport/integrations/access/common"
"github.com/gravitational/teleport/integrations/access/mattermost"
"github.com/gravitational/teleport/integrations/lib"
"github.com/gravitational/teleport/integrations/lib/logger"
"github.com/gravitational/teleport/integrations/lib/testing/integration"
)
var msgFieldRegexp = regexp.MustCompile(`(?im)^\*\*([a-zA-Z ]+)\*\*:\ +(.+)$`)
var requestReasonRegexp = regexp.MustCompile("(?im)^\\*\\*Reason\\*\\*:\\ ```\\n(.*?)```(.*?)$")
var resolutionReasonRegexp = regexp.MustCompile("(?im)^\\*\\*Resolution reason\\*\\*:\\ ```\\n(.*?)```(.*?)$")
// MattermostBaseSuite is the Mattermost access plugin test suite.
// It implements the testify.TestingSuite interface.
type MattermostBaseSuite struct {
*integration.AccessRequestSuite
appConfig *mattermost.Config
raceNumber int
fakeMattermost *FakeMattermost
fakeStatusSink *integration.FakeStatusSink
requesterMattermostUser mattermost.User
reviewer1MattermostUser mattermost.User
reviewer2MattermostUser mattermost.User
}
// SetupTest starts a fake Mattermost and generates the plugin configuration.
// It is run for each test.
func (s *MattermostBaseSuite) SetupTest() {
t := s.T()
err := logger.Setup(logger.Config{Severity: "debug"})
require.NoError(t, err)
s.raceNumber = runtime.GOMAXPROCS(0)
s.fakeMattermost = NewFakeMattermost(mattermost.User{Username: "bot", Email: "[email protected]"}, s.raceNumber)
t.Cleanup(s.fakeMattermost.Close)
// load fixtures in the fake Mattermost
s.requesterMattermostUser = s.fakeMattermost.StoreUser(mattermost.User{Email: integration.Requester1UserName})
s.reviewer1MattermostUser = s.fakeMattermost.StoreUser(mattermost.User{Email: integration.Reviewer1UserName})
s.reviewer2MattermostUser = s.fakeMattermost.StoreUser(mattermost.User{Email: integration.Reviewer2UserName})
s.fakeStatusSink = &integration.FakeStatusSink{}
var conf mattermost.Config
conf.Teleport = s.TeleportConfig()
conf.Mattermost.Token = "000000"
conf.Mattermost.URL = s.fakeMattermost.URL()
conf.StatusSink = s.fakeStatusSink
conf.PluginType = types.PluginTypeMattermost
s.appConfig = &conf
}
// startApp starts the Mattermost plugin, waits for it to become ready and returns,
func (s *MattermostBaseSuite) startApp() {
t := s.T()
t.Helper()
app := mattermost.NewMattermostApp(s.appConfig)
integration.RunAndWaitReady(t, app)
}
// MattermostSuiteOSS contains all tests that support running against a Teleport
// OSS Server.
type MattermostSuiteOSS struct {
MattermostBaseSuite
}
// MattermostSuiteEnterprise contains all tests that require a Teleport Enterprise
// to run.
type MattermostSuiteEnterprise struct {
MattermostBaseSuite
}
// SetupTest overrides MattermostBaseSuite.SetupTest to check the Teleport features
// before each test.
func (s *MattermostSuiteEnterprise) SetupTest() {
t := s.T()
s.RequireAdvancedWorkflow(t)
s.MattermostBaseSuite.SetupTest()
}
// TestMattermostMessagePosting validates that a message is sent to each recipient
// specified in the plugin's configuration and optional reviewers.
// It also checks that the message content is correct, even when the reason
// is too large.
func (s *MattermostSuiteOSS) TestMattermostMessagePosting() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
// max size of request was decreased here: https://github.com/gravitational/teleport/pull/13298
s.SetReasonPadding(4000)
// Reviewer 1 is a recipient because they're in the config
s.appConfig.Recipients = common.RawRecipientsMap{
integration.RequestedRoleName: []string{
s.reviewer1MattermostUser.Email,
},
"*": []string{"fallback"},
}
directChannel1 := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser)
directChannel2 := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer2MattermostUser)
s.startApp()
// Test execution: we send an access request
userName := integration.RequesterOSSUserName
// Reviewer 2 is a recipient because they're in the suggested reviewers
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer2MattermostUser.Email})
// We expect 2 messages to be sent by the plugin: one for each recipient.
// We check if the stored plugin data makes sense.
pluginData := s.checkPluginData(ctx, req.GetName(), func(data accessrequest.PluginData) bool {
return len(data.SentMessages) > 0
})
assert.Len(t, pluginData.SentMessages, 2)
// Then we check that our fake Mattermost has received the messages.
var posts []mattermost.Post
postSet := make(MattermostDataPostSet)
for range 2 {
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
postSet.Add(accessrequest.MessageData{ChannelID: post.ChannelID, MessageID: post.ID})
posts = append(posts, post)
}
assert.Len(t, postSet, 2)
assert.Contains(t, postSet, pluginData.SentMessages[0])
assert.Contains(t, postSet, pluginData.SentMessages[1])
// Finally, we validate the messages content
sort.Sort(MattermostPostSlice(posts))
assert.Equal(t, directChannel1.ID, posts[0].ChannelID)
assert.Equal(t, directChannel2.ID, posts[1].ChannelID)
post := posts[0]
reqID, err := parsePostField(post, "Request ID")
require.NoError(t, err)
assert.Equal(t, req.GetName(), reqID)
username, err := parsePostField(post, "User")
require.NoError(t, err)
assert.Equal(t, integration.RequesterOSSUserName, username)
matches := requestReasonRegexp.FindAllStringSubmatch(post.Message, -1)
require.Len(t, matches, 1)
require.Len(t, matches[0], 3)
assert.Equal(t, "because of "+strings.Repeat("A", 489), matches[0][1])
assert.Equal(t, " (truncated)", matches[0][2])
statusLine, err := parsePostField(post, "Status")
require.NoError(t, err)
assert.Equal(t, "⏳ PENDING", statusLine)
assert.Equal(t, types.PluginStatusCode_RUNNING, s.fakeStatusSink.Get().GetCode())
}
// TestApproval tests that when a request is approved, its corresponding message
// is updated to reflect the new request state.
func (s *MattermostSuiteOSS) TestApproval() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
s.startApp()
// Test setup: we create an access request and wait for its message
userName := integration.RequesterOSSUserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
assert.Equal(t, directChannelID, post.ChannelID)
// Test execution: we approve the request
err = s.Ruler().ApproveAccessRequest(ctx, req.GetName(), "okay")
require.NoError(t, err)
// Validating the plugin updated the message to reflect that it got approved
postUpdate, err := s.fakeMattermost.CheckPostUpdate(ctx)
require.NoError(t, err, "no messages updated")
assert.Equal(t, post.ID, postUpdate.ID)
assert.Equal(t, post.ChannelID, postUpdate.ChannelID)
statusLine, err := parsePostField(postUpdate, "Status")
require.NoError(t, err)
assert.Equal(t, "✅ APPROVED", statusLine)
matches := resolutionReasonRegexp.FindAllStringSubmatch(postUpdate.Message, -1)
require.Len(t, matches, 1)
require.Len(t, matches[0], 3)
assert.Equal(t, "okay", matches[0][1])
assert.Empty(t, matches[0][2])
}
// TestDenial tests that when a request is denied, its corresponding message
// is updated to reflect the new request state.
func (s *MattermostSuiteOSS) TestDenial() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
s.startApp()
// Test setup: we create an access request and wait for its message
userName := integration.RequesterOSSUserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
assert.Equal(t, directChannelID, post.ChannelID)
// Test execution: we approve the request
err = s.Ruler().DenyAccessRequest(ctx, req.GetName(), "not okay "+strings.Repeat("A", 4000))
require.NoError(t, err)
// Validating the plugin updated the message to reflect that it got denied
postUpdate, err := s.fakeMattermost.CheckPostUpdate(ctx)
require.NoError(t, err, "no messages updated")
assert.Equal(t, post.ID, postUpdate.ID)
assert.Equal(t, post.ChannelID, postUpdate.ChannelID)
statusLine, err := parsePostField(postUpdate, "Status")
require.NoError(t, err)
assert.Equal(t, "❌ DENIED", statusLine)
matches := resolutionReasonRegexp.FindAllStringSubmatch(postUpdate.Message, -1)
require.Len(t, matches, 1)
require.Len(t, matches[0], 3)
assert.Equal(t, "not okay "+strings.Repeat("A", 491), matches[0][1])
assert.Equal(t, " (truncated)", matches[0][2])
}
// TestReviewComments tests that that update messages are sent after the access
// request is reviewed. Each review should trigger a new message.
func (s *MattermostSuiteEnterprise) TestReviewComments() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
s.startApp()
// Test setup: we create an access request and wait for its message
userName := integration.Requester1UserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
s.checkPluginData(ctx, req.GetName(), func(data accessrequest.PluginData) bool {
return len(data.SentMessages) > 0
})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, directChannelID, post.ChannelID)
// Test execution: we submit a review and validate an update is posted
err = s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer1UserName,
ProposedState: types.RequestState_APPROVED,
Created: time.Now(),
Reason: "okay",
})
require.NoError(t, err)
comment, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer1UserName+" reviewed the request", "comment must contain a review author")
assert.Contains(t, comment.Message, "Resolution: ✅ APPROVED", "comment must contain a proposed state")
assert.Contains(t, comment.Message, "Reason: ```\nokay```", "comment must contain a reason")
// Test execution: we submit a second review and validate an update is posted
err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer2UserName,
ProposedState: types.RequestState_DENIED,
Created: time.Now(),
Reason: "not okay",
})
require.NoError(t, err)
comment, err = s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer2UserName+" reviewed the request", "comment must contain a review author")
assert.Contains(t, comment.Message, "Resolution: ❌ DENIED", "comment must contain a proposed state")
assert.Contains(t, comment.Message, "Reason: ```\nnot okay```", "comment must contain a reason")
}
// TestApprovalByReview tests that the message is updated after the access request
// is reviewed and approved.
func (s *MattermostSuiteEnterprise) TestApprovalByReview() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
s.startApp()
// Test setup: we create an access request and wait for its Discord message
userName := integration.Requester1UserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
assert.Equal(t, directChannelID, post.ChannelID)
// Test execution: we submit a review and validate an update is posted
err = s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer1UserName,
ProposedState: types.RequestState_APPROVED,
Created: time.Now(),
Reason: "okay",
})
require.NoError(t, err)
comment, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer1UserName+" reviewed the request", "comment must contain a review author")
// Test execution: we submit a second review and validate an update is posted
err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer2UserName,
ProposedState: types.RequestState_APPROVED,
Created: time.Now(),
Reason: "finally okay",
})
require.NoError(t, err)
comment, err = s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer2UserName+" reviewed the request", "comment must contain a review author")
// When posting a review, the bot also updates the message to add the amount of reviewers.
// This update is soon superseded by the "access allowed" update
_, _ = s.fakeMattermost.CheckPostUpdate(ctx)
// Finally, we validate the original message got updated to reflect the resolution status.
postUpdate, err := s.fakeMattermost.CheckPostUpdate(ctx)
require.NoError(t, err, "no messages updated")
assert.Equal(t, post.ID, postUpdate.ID)
assert.Equal(t, post.ChannelID, postUpdate.ChannelID)
statusLine, err := parsePostField(postUpdate, "Status")
require.NoError(t, err)
assert.Equal(t, "✅ APPROVED", statusLine)
matches := resolutionReasonRegexp.FindAllStringSubmatch(postUpdate.Message, -1)
require.Len(t, matches, 1)
require.Len(t, matches[0], 3)
assert.Equal(t, "finally okay", matches[0][1])
assert.Empty(t, matches[0][2])
}
// TestDenialByReview tests that the message is updated after the access request
// is reviewed and denied.
func (s *MattermostSuiteEnterprise) TestDenialByReview() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
s.startApp()
// Test setup: we create an access request and wait for its Discord message
userName := integration.Requester1UserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
assert.Equal(t, directChannelID, post.ChannelID)
// Test execution: we submit a review and validate an update is posted
err = s.Reviewer1().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer1UserName,
ProposedState: types.RequestState_DENIED,
Created: time.Now(),
Reason: "not okay",
})
require.NoError(t, err)
comment, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer1UserName+" reviewed the request", "comment must contain a review author")
// Test execution: we submit a second review and validate an update is posted
err = s.Reviewer2().SubmitAccessRequestReview(ctx, req.GetName(), types.AccessReview{
Author: integration.Reviewer2UserName,
ProposedState: types.RequestState_DENIED,
Created: time.Now(),
Reason: "finally not okay",
})
require.NoError(t, err)
// When posting a review, the bot also updates the message to add the amount of reviewers.
// This update is soon superseded by the "access allowed" update
_, _ = s.fakeMattermost.CheckPostUpdate(ctx)
// Finally, we validate the original message got updated to reflect the resolution status.
comment, err = s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
assert.Equal(t, post.ChannelID, comment.ChannelID)
assert.Equal(t, post.ID, comment.RootID)
assert.Contains(t, comment.Message, integration.Reviewer2UserName+" reviewed the request", "comment must contain a review author")
postUpdate, err := s.fakeMattermost.CheckPostUpdate(ctx)
require.NoError(t, err, "no messages updated")
assert.Equal(t, post.ID, postUpdate.ID)
assert.Equal(t, post.ChannelID, postUpdate.ChannelID)
statusLine, err := parsePostField(postUpdate, "Status")
require.NoError(t, err)
assert.Equal(t, "❌ DENIED", statusLine)
matches := resolutionReasonRegexp.FindAllStringSubmatch(postUpdate.Message, -1)
require.Len(t, matches, 1)
require.Len(t, matches[0], 3)
assert.Equal(t, "finally not okay", matches[0][1])
assert.Empty(t, matches[0][2])
}
// TestExpiration tests that when a request expires, its corresponding message
// is updated to reflect the new request state.
func (s *MattermostSuiteOSS) TestExpiration() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
s.startApp()
// Test setup: we create an access request and wait for its Discord message
userName := integration.RequesterOSSUserName
req := s.CreateAccessRequest(ctx, userName, []string{s.reviewer1MattermostUser.Email})
post, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err, "no new messages posted")
directChannelID := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser).ID
assert.Equal(t, directChannelID, post.ChannelID)
s.checkPluginData(ctx, req.GetName(), func(data accessrequest.PluginData) bool {
return len(data.SentMessages) > 0
})
// Test execution: we expire the request
err = s.Ruler().DeleteAccessRequest(ctx, req.GetName()) // simulate expiration
require.NoError(t, err)
// Validating the plugin updated the Discord message to reflect that the request expired
postUpdate, err := s.fakeMattermost.CheckPostUpdate(ctx)
require.NoError(t, err, "no new messages updated")
assert.Equal(t, post.ID, postUpdate.ID)
assert.Equal(t, post.ChannelID, postUpdate.ChannelID)
statusLine, err := parsePostField(postUpdate, "Status")
require.NoError(t, err)
assert.Equal(t, "⌛ EXPIRED", statusLine)
}
// TestRace validates that the plugin behaves properly and performs all the
// message updates when a lot of access requests are sent and reviewed in a very
// short time frame.
func (s *MattermostSuiteEnterprise) TestRace() {
t := s.T()
t.Skip("This test is flaky")
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
t.Cleanup(cancel)
err := logger.Setup(logger.Config{Severity: "info"}) // Turn off noisy debug logging
require.NoError(t, err)
botUser := s.fakeMattermost.GetBotUser()
s.startApp()
var (
raceErr error
raceErrOnce sync.Once
postIDs sync.Map
postsCount int32
postUpdateCounters sync.Map
reviewCommentCounters sync.Map
)
setRaceErr := func(err error) error {
raceErrOnce.Do(func() {
raceErr = err
})
return err
}
process := lib.NewProcess(ctx)
for range s.raceNumber {
process.SpawnCritical(func(ctx context.Context) error {
req, err := types.NewAccessRequest(uuid.New().String(), integration.Requester1UserName, "editor")
if err != nil {
return setRaceErr(trace.Wrap(err))
}
req.SetSuggestedReviewers([]string{s.reviewer1MattermostUser.Email, s.reviewer2MattermostUser.Email})
if _, err := s.Requester1().CreateAccessRequestV2(ctx, req); err != nil {
return setRaceErr(trace.Wrap(err))
}
return nil
})
}
// Having TWO suggested reviewers will post TWO messages for each request.
// We also have approval threshold of TWO set in the role properties
// so lets simply submit the approval from each of the suggested reviewers.
//
// Multiplier SIX means that we handle TWO messages for each request and also
// TWO comments for each message: 2 * (1 message + 2 comments).
for range 6 * s.raceNumber {
process.SpawnCritical(func(ctx context.Context) error {
post, err := s.fakeMattermost.CheckNewPost(ctx)
if err := trace.Wrap(err); err != nil {
return setRaceErr(err)
}
if post.RootID == "" {
// Handle "root" notifications.
postKey := accessrequest.MessageData{ChannelID: post.ChannelID, MessageID: post.ID}
if _, loaded := postIDs.LoadOrStore(postKey, struct{}{}); loaded {
return setRaceErr(trace.Errorf("post %v already stored", postKey))
}
atomic.AddInt32(&postsCount, 1)
reqID, err := parsePostField(post, "Request ID")
if err != nil {
return setRaceErr(trace.Wrap(err))
}
directChannel, ok := s.fakeMattermost.GetDirectChannel(post.ChannelID)
if !ok {
return setRaceErr(trace.Errorf("direct channel %s not found", post.ChannelID))
}
var userID string
if directChannel.User2ID == botUser.ID {
userID = directChannel.User1ID
} else {
userID = directChannel.User2ID
}
user, ok := s.fakeMattermost.GetUser(userID)
if !ok {
return setRaceErr(trace.Errorf("user %s not found", userID))
}
if err = s.ClientByName(user.Email).SubmitAccessRequestReview(ctx, reqID, types.AccessReview{
Author: user.Email,
ProposedState: types.RequestState_APPROVED,
Created: time.Now(),
Reason: "okay",
}); err != nil {
return setRaceErr(trace.Wrap(err))
}
} else {
// Handle review comments.
postKey := accessrequest.MessageData{ChannelID: post.ChannelID, MessageID: post.RootID}
var newCounter int32
val, _ := reviewCommentCounters.LoadOrStore(postKey, &newCounter)
counterPtr := val.(*int32)
atomic.AddInt32(counterPtr, 1)
}
return nil
})
}
// Multiplier TWO means that we handle updates for each of the two messages posted to reviewers.
for range 2 * 2 * s.raceNumber {
process.SpawnCritical(func(ctx context.Context) error {
post, err := s.fakeMattermost.CheckPostUpdate(ctx)
if err != nil {
return setRaceErr(trace.Wrap(err, "error from post update consumer"))
}
postKey := accessrequest.MessageData{ChannelID: post.ChannelID, MessageID: post.ID}
var newCounter int32
val, _ := postUpdateCounters.LoadOrStore(postKey, &newCounter)
counterPtr := val.(*int32)
atomic.AddInt32(counterPtr, 1)
return nil
})
}
process.Terminate()
<-process.Done()
require.NoError(t, raceErr)
assert.Equal(t, int32(2*s.raceNumber), postsCount)
postIDs.Range(func(key, value any) bool {
next := true
val, loaded := reviewCommentCounters.LoadAndDelete(key)
next = next && assert.True(t, loaded)
counterPtr := val.(*int32)
next = next && assert.Equal(t, int32(2), *counterPtr)
val, loaded = postUpdateCounters.LoadAndDelete(key)
next = next && assert.True(t, loaded)
counterPtr = val.(*int32)
// Each message should be updated 2 times
next = next && assert.Equal(t, int32(2), *counterPtr)
return next
})
}
func (s *MattermostSuiteOSS) TestRecipientsConfig() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
directChannel1 := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser)
team := s.fakeMattermost.StoreTeam(mattermost.Team{Name: "team-llama"})
channel2 := s.fakeMattermost.StoreChannel(mattermost.Channel{Name: "channel-llama", TeamID: team.ID})
// Test an email and a team/channel
s.appConfig.Recipients = common.RawRecipientsMap{
types.Wildcard: []string{"team-llama/channel-llama", s.reviewer1MattermostUser.Email},
}
s.startApp()
userName := integration.RequesterOSSUserName
request := s.CreateAccessRequest(ctx, userName, nil)
pluginData := s.checkPluginData(ctx, request.GetName(), func(data accessrequest.PluginData) bool {
return len(data.SentMessages) > 0
})
assert.Len(t, pluginData.SentMessages, 2)
var (
msg mattermost.Post
messages []mattermost.Post
)
messageSet := make(MattermostDataPostSet)
msg, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
messageSet.Add(accessrequest.MessageData{ChannelID: msg.ChannelID, MessageID: msg.ID})
messages = append(messages, msg)
msg, err = s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
messageSet.Add(accessrequest.MessageData{ChannelID: msg.ChannelID, MessageID: msg.ID})
messages = append(messages, msg)
assert.Len(t, messageSet, 2)
assert.Contains(t, messageSet, pluginData.SentMessages[0])
assert.Contains(t, messageSet, pluginData.SentMessages[1])
sort.Sort(MattermostPostSlice(messages))
assert.Equal(t, directChannel1.ID, messages[0].ChannelID)
assert.Equal(t, channel2.ID, messages[1].ChannelID)
}
func (s *MattermostSuiteOSS) TestRecipientsFromAccessMonitoringRule() {
t := s.T()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
t.Cleanup(cancel)
directChannel1 := s.fakeMattermost.GetDirectChannelFor(s.fakeMattermost.GetBotUser(), s.reviewer1MattermostUser)
team := s.fakeMattermost.StoreTeam(mattermost.Team{Name: "team-llama"})
channel2 := s.fakeMattermost.StoreChannel(mattermost.Channel{Name: "channel-llama", TeamID: team.ID})
// Setup base config to ensure access monitoring rule recipient take precidence
s.appConfig.Recipients = common.RawRecipientsMap{
types.Wildcard: []string{
"team-llama/channel-llama",
"somethingThatWillBeOverWritten",
},
}
s.startApp()
_, err := s.ClientByName(integration.RulerUserName).
AccessMonitoringRulesClient().
CreateAccessMonitoringRule(ctx, accessmonitoringrulesv1.AccessMonitoringRule_builder{
Kind: types.KindAccessMonitoringRule,
Version: types.V1,
Metadata: v1.Metadata_builder{
Name: "test-mattermost-amr",
}.Build(),
Spec: accessmonitoringrulesv1.AccessMonitoringRuleSpec_builder{
Subjects: []string{types.KindAccessRequest},
Condition: "!is_empty(access_request.spec.roles)",
Notification: accessmonitoringrulesv1.Notification_builder{
Name: "mattermost",
Recipients: []string{
"team-llama/channel-llama",
s.reviewer1MattermostUser.Email,
},
}.Build(),
}.Build(),
}.Build())
assert.NoError(t, err)
userName := integration.RequesterOSSUserName
request := s.CreateAccessRequest(ctx, userName, nil)
pluginData := s.checkPluginData(ctx, request.GetName(), func(data accessrequest.PluginData) bool {
return len(data.SentMessages) > 0
})
assert.Len(t, pluginData.SentMessages, 2)
messageSet := make(MattermostDataPostSet)
msg, err := s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
messageSet.Add(accessrequest.MessageData{ChannelID: msg.ChannelID, MessageID: msg.ID})
var messages []mattermost.Post
messages = append(messages, msg)
msg, err = s.fakeMattermost.CheckNewPost(ctx)
require.NoError(t, err)
messageSet.Add(accessrequest.MessageData{ChannelID: msg.ChannelID, MessageID: msg.ID})
messages = append(messages, msg)
assert.Len(t, messageSet, 2)
assert.Contains(t, messageSet, pluginData.SentMessages[0])
assert.Contains(t, messageSet, pluginData.SentMessages[1])
sort.Sort(MattermostPostSlice(messages))
assert.Equal(t, directChannel1.ID, messages[0].ChannelID)
assert.Equal(t, channel2.ID, messages[1].ChannelID)
assert.NoError(t, s.ClientByName(integration.RulerUserName).
AccessMonitoringRulesClient().DeleteAccessMonitoringRule(ctx, "test-mattermost-amr"))
}