mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-02 06:08:23 +08:00
905 lines
28 KiB
Diff
905 lines
28 KiB
Diff
diff --git a/access/discord/discord_test.go b/access/discord/discord_test.go
|
|
deleted file mode 100644
|
|
index aa52ae70..00000000
|
|
--- a/access/discord/discord_test.go
|
|
+++ /dev/null
|
|
@@ -1,714 +0,0 @@
|
|
-package main
|
|
-
|
|
-import (
|
|
- "context"
|
|
- "os/user"
|
|
- "regexp"
|
|
- "runtime"
|
|
- "sort"
|
|
- "strings"
|
|
- "sync"
|
|
- "sync/atomic"
|
|
- "testing"
|
|
- "time"
|
|
-
|
|
- "github.com/google/uuid"
|
|
-
|
|
- "github.com/gravitational/teleport-plugins/access/common"
|
|
- "github.com/gravitational/teleport-plugins/lib"
|
|
- "github.com/gravitational/teleport-plugins/lib/logger"
|
|
- "github.com/gravitational/teleport-plugins/lib/testing/integration"
|
|
- "github.com/gravitational/teleport/api/client/proto"
|
|
- "github.com/gravitational/teleport/api/types"
|
|
- "github.com/gravitational/trace"
|
|
-
|
|
- "github.com/stretchr/testify/assert"
|
|
- "github.com/stretchr/testify/require"
|
|
- "github.com/stretchr/testify/suite"
|
|
-)
|
|
-
|
|
-var msgFieldRegexp = regexp.MustCompile(`(?im)^\*([a-zA-Z ]+)\*: (.+)$`)
|
|
-var requestReasonRegexp = regexp.MustCompile("(?im)^\\*Reason\\*:\\ ```\\n(.*?)```(.*?)$")
|
|
-
|
|
-type DiscordSuite struct {
|
|
- integration.Suite
|
|
- appConfig *DiscordConfig
|
|
- userNames struct {
|
|
- ruler string
|
|
- requestor string
|
|
- reviewer1 string
|
|
- reviewer2 string
|
|
- plugin string
|
|
- }
|
|
- raceNumber int
|
|
- fakeDiscord *FakeDiscord
|
|
-
|
|
- clients map[string]*integration.Client
|
|
- teleportFeatures *proto.Features
|
|
- teleportConfig lib.TeleportConfig
|
|
-}
|
|
-
|
|
-func TestDiscordBot(t *testing.T) { suite.Run(t, &DiscordSuite{}) }
|
|
-
|
|
-func (s *DiscordSuite) SetupSuite() {
|
|
- var err error
|
|
- t := s.T()
|
|
-
|
|
- logger.Init()
|
|
- err = logger.Setup(logger.Config{Severity: "debug"})
|
|
- require.NoError(t, err)
|
|
- s.raceNumber = runtime.GOMAXPROCS(0)
|
|
- me, err := user.Current()
|
|
- require.NoError(t, err)
|
|
-
|
|
- // We set such a big timeout because integration.NewFromEnv could start
|
|
- // downloading a Teleport *-bin.tar.gz file which can take a long time.
|
|
- ctx := s.SetContextTimeout(2 * time.Minute)
|
|
-
|
|
- teleport, err := integration.NewFromEnv(ctx)
|
|
- require.NoError(t, err)
|
|
- t.Cleanup(teleport.Close)
|
|
-
|
|
- auth, err := teleport.NewAuthService()
|
|
- require.NoError(t, err)
|
|
- s.StartApp(auth)
|
|
-
|
|
- s.clients = make(map[string]*integration.Client)
|
|
-
|
|
- // Set up the user who has an access to all kinds of resources.
|
|
-
|
|
- s.userNames.ruler = me.Username + "-ruler@example.com"
|
|
- client, err := teleport.MakeAdmin(ctx, auth, s.userNames.ruler)
|
|
- require.NoError(t, err)
|
|
- s.clients[s.userNames.ruler] = client
|
|
-
|
|
- // Get the server features.
|
|
-
|
|
- pong, err := client.Ping(ctx)
|
|
- require.NoError(t, err)
|
|
- teleportFeatures := pong.GetServerFeatures()
|
|
-
|
|
- var bootstrap integration.Bootstrap
|
|
-
|
|
- // Set up user who can request the access to role "editor".
|
|
-
|
|
- conditions := types.RoleConditions{Request: &types.AccessRequestConditions{Roles: []string{"editor"}}}
|
|
- if teleportFeatures.AdvancedAccessWorkflows {
|
|
- conditions.Request.Thresholds = []types.AccessReviewThreshold{types.AccessReviewThreshold{Approve: 2, Deny: 2}}
|
|
- }
|
|
- role, err := bootstrap.AddRole("foo", types.RoleSpecV5{Allow: conditions})
|
|
- require.NoError(t, err)
|
|
-
|
|
- user, err := bootstrap.AddUserWithRoles(me.Username+"@example.com", role.GetName())
|
|
- require.NoError(t, err)
|
|
- s.userNames.requestor = user.GetName()
|
|
-
|
|
- // Set up TWO users who can review access requests to role "editor".
|
|
-
|
|
- conditions = types.RoleConditions{}
|
|
- if teleportFeatures.AdvancedAccessWorkflows {
|
|
- conditions.ReviewRequests = &types.AccessReviewConditions{Roles: []string{"editor"}}
|
|
- }
|
|
- role, err = bootstrap.AddRole("foo-reviewer", types.RoleSpecV5{Allow: conditions})
|
|
- require.NoError(t, err)
|
|
-
|
|
- user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer1@example.com", role.GetName())
|
|
- require.NoError(t, err)
|
|
- s.userNames.reviewer1 = user.GetName()
|
|
-
|
|
- user, err = bootstrap.AddUserWithRoles(me.Username+"-reviewer2@example.com", role.GetName())
|
|
- require.NoError(t, err)
|
|
- s.userNames.reviewer2 = user.GetName()
|
|
-
|
|
- // Set up plugin user.
|
|
- role, err = bootstrap.AddRole("access-discord", types.RoleSpecV5{
|
|
- Allow: types.RoleConditions{
|
|
- Rules: []types.Rule{
|
|
- types.NewRule("access_request", []string{"list", "read"}),
|
|
- types.NewRule("access_plugin_data", []string{"update"}),
|
|
- },
|
|
- },
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- user, err = bootstrap.AddUserWithRoles("access-discord", role.GetName())
|
|
- require.NoError(t, err)
|
|
- s.userNames.plugin = user.GetName()
|
|
-
|
|
- // Bake all the resources.
|
|
-
|
|
- err = teleport.Bootstrap(ctx, auth, bootstrap.Resources())
|
|
- require.NoError(t, err)
|
|
-
|
|
- // Initialize the clients.
|
|
-
|
|
- client, err = teleport.NewClient(ctx, auth, s.userNames.requestor)
|
|
- require.NoError(t, err)
|
|
- s.clients[s.userNames.requestor] = client
|
|
-
|
|
- if teleportFeatures.AdvancedAccessWorkflows {
|
|
- client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer1)
|
|
- require.NoError(t, err)
|
|
- s.clients[s.userNames.reviewer1] = client
|
|
-
|
|
- client, err = teleport.NewClient(ctx, auth, s.userNames.reviewer2)
|
|
- require.NoError(t, err)
|
|
- s.clients[s.userNames.reviewer2] = client
|
|
- }
|
|
-
|
|
- identityPath, err := teleport.Sign(ctx, auth, s.userNames.plugin)
|
|
- require.NoError(t, err)
|
|
-
|
|
- s.teleportConfig.Addr = auth.AuthAddr().String()
|
|
- s.teleportConfig.Identity = identityPath
|
|
- s.teleportFeatures = teleportFeatures
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) SetupTest() {
|
|
- t := s.T()
|
|
-
|
|
- err := logger.Setup(logger.Config{Severity: "debug"})
|
|
- require.NoError(t, err)
|
|
-
|
|
- s.fakeDiscord = NewFakeDiscord(s.raceNumber)
|
|
- t.Cleanup(s.fakeDiscord.Close)
|
|
-
|
|
- var conf DiscordConfig
|
|
- conf.Teleport = s.teleportConfig
|
|
- conf.Discord.Token = "000000"
|
|
- conf.Discord.APIURL = s.fakeDiscord.URL() + "/"
|
|
-
|
|
- s.appConfig = &conf
|
|
- s.SetContextTimeout(5 * time.Second)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) startApp() {
|
|
- t := s.T()
|
|
- t.Helper()
|
|
-
|
|
- app := NewDiscordApp(s.appConfig)
|
|
- s.StartApp(app)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) ruler() *integration.Client {
|
|
- return s.clients[s.userNames.ruler]
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) requestor() *integration.Client {
|
|
- return s.clients[s.userNames.requestor]
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) reviewer1() *integration.Client {
|
|
- return s.clients[s.userNames.reviewer1]
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) reviewer2() *integration.Client {
|
|
- return s.clients[s.userNames.reviewer2]
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) newAccessRequest() types.AccessRequest {
|
|
- t := s.T()
|
|
- t.Helper()
|
|
-
|
|
- req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor")
|
|
- require.NoError(t, err)
|
|
- // max size of request was decreased here: https://github.com/gravitational/teleport/pull/13298
|
|
- req.SetRequestReason("because of " + strings.Repeat("A", 4000))
|
|
- return req
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) createAccessRequest() types.AccessRequest {
|
|
- t := s.T()
|
|
- t.Helper()
|
|
-
|
|
- req := s.newAccessRequest()
|
|
- err := s.requestor().CreateAccessRequest(s.Context(), req)
|
|
- require.NoError(t, err)
|
|
- return req
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) checkPluginData(reqID string, cond func(common.GenericPluginData) bool) common.GenericPluginData {
|
|
- t := s.T()
|
|
- t.Helper()
|
|
-
|
|
- for {
|
|
- rawData, err := s.ruler().PollAccessRequestPluginData(s.Context(), "discord", reqID)
|
|
- require.NoError(t, err)
|
|
- if data := common.DecodePluginData(rawData); cond(data) {
|
|
- return data
|
|
- }
|
|
- }
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestMessagePosting() {
|
|
- t := s.T()
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- "1002", // reviewer 2
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
- request := s.createAccessRequest()
|
|
-
|
|
- pluginData := s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
- assert.Len(t, pluginData.SentMessages, 2)
|
|
-
|
|
- var messages []DiscordMsg
|
|
- messageSet := make(MessageSet)
|
|
- for i := 0; i < 2; i++ {
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- messageSet.Add(common.MessageData{ChannelID: msg.Channel, MessageID: msg.DiscordID})
|
|
- 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(MessageSlice(messages))
|
|
-
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], messages[0].Channel)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][1], messages[1].Channel)
|
|
-
|
|
- msgUser, err := parseMessageField(messages[0], "User")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.userNames.requestor, msgUser)
|
|
-
|
|
- t.Logf("%q", messages[0].Text)
|
|
- matches := requestReasonRegexp.FindAllStringSubmatch(messages[0].Text, -1)
|
|
- require.Equal(t, 1, len(matches))
|
|
- require.Equal(t, 3, len(matches[0]))
|
|
- assert.Equal(t, "because of "+strings.Repeat("A", 489), matches[0][1])
|
|
- assert.Equal(t, " (truncated)", matches[0][2])
|
|
-
|
|
- status, err := parseMessageField(messages[0], "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "⏳ PENDING", status)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestApproval() {
|
|
- t := s.T()
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- req := s.createAccessRequest()
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- err = s.ruler().ApproveAccessRequest(s.Context(), req.GetName(), "okay")
|
|
- require.NoError(t, err)
|
|
-
|
|
- msgUpdate, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msgUpdate.Channel)
|
|
- assert.Equal(t, msg.DiscordID, msgUpdate.DiscordID)
|
|
-
|
|
- status, err := parseMessageField(msgUpdate, "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "✅ APPROVED", status) // Should fail
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestDenial() {
|
|
- t := s.T()
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- req := s.createAccessRequest()
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- // max size of request was decreased here: https://github.com/gravitational/teleport/pull/13298
|
|
- err = s.ruler().DenyAccessRequest(s.Context(), req.GetName(), "not okay "+strings.Repeat("A", 4000))
|
|
- require.NoError(t, err)
|
|
-
|
|
- msgUpdate, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msgUpdate.Channel)
|
|
- assert.Equal(t, msg.DiscordID, msgUpdate.DiscordID)
|
|
-
|
|
- status, err := parseMessageField(msgUpdate, "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "❌ DENIED", status) // Should fail
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestReviewUpdates() {
|
|
- t := s.T()
|
|
-
|
|
- if !s.teleportFeatures.AdvancedAccessWorkflows {
|
|
- t.Skip("Doesn't work in OSS version")
|
|
- }
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- request := s.createAccessRequest()
|
|
-
|
|
- s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
-
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- err = s.reviewer1().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer1,
|
|
- ProposedState: types.RequestState_APPROVED,
|
|
- Created: time.Now(),
|
|
- Reason: "okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[0].Author.Name, s.userNames.reviewer1, "embed must contain the review author")
|
|
- assert.Contains(t, update.Embeds[0].Title, "Approved request", "embed must contain a proposed state")
|
|
- assert.Contains(t, update.Embeds[0].Description, "Reason: ```\nokay```", "reply must contain a reason")
|
|
-
|
|
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer2,
|
|
- ProposedState: types.RequestState_DENIED,
|
|
- Created: time.Now(),
|
|
- Reason: "not okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err = s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[1].Author.Name, s.userNames.reviewer2, "embed must contain the review author")
|
|
- assert.Contains(t, update.Embeds[1].Title, "Denied request", "embed must contain a proposed state")
|
|
- assert.Contains(t, update.Embeds[1].Description, "Reason: ```\nnot okay```", "reply must contain a reason")
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestApprovalByReview() {
|
|
- t := s.T()
|
|
-
|
|
- if !s.teleportFeatures.AdvancedAccessWorkflows {
|
|
- t.Skip("Doesn't work in OSS version")
|
|
- }
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- request := s.createAccessRequest()
|
|
-
|
|
- s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
-
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- err = s.reviewer1().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer1,
|
|
- ProposedState: types.RequestState_APPROVED,
|
|
- Created: time.Now(),
|
|
- Reason: "okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[0].Author.Name, s.userNames.reviewer1, "embed must contain the review author")
|
|
-
|
|
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer2,
|
|
- ProposedState: types.RequestState_APPROVED,
|
|
- Created: time.Now(),
|
|
- Reason: "finally okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err = s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[1].Author.Name, s.userNames.reviewer2, "embed must contain the review author")
|
|
- status, err := parseMessageField(update, "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "✅ APPROVED", status)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestDenialByReview() {
|
|
- t := s.T()
|
|
-
|
|
- if !s.teleportFeatures.AdvancedAccessWorkflows {
|
|
- t.Skip("Doesn't work in OSS version")
|
|
- }
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- request := s.createAccessRequest()
|
|
-
|
|
- s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
-
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- err = s.reviewer1().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer1,
|
|
- ProposedState: types.RequestState_DENIED,
|
|
- Created: time.Now(),
|
|
- Reason: "not okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[0].Author.Name, s.userNames.reviewer1, "embed must contain the review author")
|
|
-
|
|
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), request.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer2,
|
|
- ProposedState: types.RequestState_DENIED,
|
|
- Created: time.Now(),
|
|
- Reason: "finally not okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err = s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
- assert.Equal(t, update.Embeds[1].Author.Name, s.userNames.reviewer2, "embed must contain the review author")
|
|
- status, err := parseMessageField(update, "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "❌ DENIED", status)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestExpiration() {
|
|
- t := s.T()
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.startApp()
|
|
-
|
|
- request := s.createAccessRequest()
|
|
-
|
|
- s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
-
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, s.appConfig.Recipients["editor"][0], msg.Channel)
|
|
-
|
|
- s.checkPluginData(request.GetName(), func(data common.GenericPluginData) bool {
|
|
- return len(data.SentMessages) > 0
|
|
- })
|
|
-
|
|
- err = s.ruler().DeleteAccessRequest(s.Context(), request.GetName()) // simulate expiration
|
|
- require.NoError(t, err)
|
|
-
|
|
- update, err := s.fakeDiscord.CheckMessageUpdateByAPI(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, msg.Channel, update.Channel)
|
|
- assert.Equal(t, msg.DiscordID, update.DiscordID)
|
|
-
|
|
- status, err := parseMessageField(update, "Status")
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, "⌛ EXPIRED", status)
|
|
-}
|
|
-
|
|
-func (s *DiscordSuite) TestRace() {
|
|
- t := s.T()
|
|
-
|
|
- if !s.teleportFeatures.AdvancedAccessWorkflows {
|
|
- t.Skip("Doesn't work in OSS version")
|
|
- }
|
|
-
|
|
- err := logger.Setup(logger.Config{Severity: "info"}) // Turn off noisy debug logging
|
|
- require.NoError(t, err)
|
|
-
|
|
- s.appConfig.Recipients = common.RawRecipientsMap{
|
|
- "editor": []string{
|
|
- "1001", // reviewer 1
|
|
- "1002", // reviewer 2
|
|
- },
|
|
- "*": []string{"fallback"},
|
|
- }
|
|
-
|
|
- s.SetContextTimeout(20 * time.Second)
|
|
- s.startApp()
|
|
-
|
|
- var (
|
|
- raceErr error
|
|
- raceErrOnce sync.Once
|
|
- threadMsgIDs sync.Map
|
|
- threadMsgsCount int32
|
|
- msgUpdateCounters sync.Map
|
|
- )
|
|
- setRaceErr := func(err error) error {
|
|
- raceErrOnce.Do(func() {
|
|
- raceErr = err
|
|
- })
|
|
- return err
|
|
- }
|
|
-
|
|
- // We create X access requests, this will send 2*X messages as "editor" has two recipients
|
|
- process := lib.NewProcess(s.Context())
|
|
- for i := 0; i < s.raceNumber; i++ {
|
|
- process.SpawnCritical(func(ctx context.Context) error {
|
|
- req, err := types.NewAccessRequest(uuid.New().String(), s.userNames.requestor, "editor")
|
|
- if err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
- if err := s.requestor().CreateAccessRequest(ctx, req); err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
- return nil
|
|
- })
|
|
- }
|
|
-
|
|
- // We start 2*X processes, each one will consume a message and approve it
|
|
- for i := 0; i < 2*s.raceNumber; i++ {
|
|
- process.SpawnCritical(func(ctx context.Context) error {
|
|
- msg, err := s.fakeDiscord.CheckNewMessage(ctx)
|
|
- if err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
-
|
|
- threadMsgKey := common.MessageData{ChannelID: msg.Channel, MessageID: msg.DiscordID}
|
|
- if _, loaded := threadMsgIDs.LoadOrStore(threadMsgKey, struct{}{}); loaded {
|
|
- return setRaceErr(trace.Errorf("thread %v already stored", threadMsgKey))
|
|
- }
|
|
- atomic.AddInt32(&threadMsgsCount, 1)
|
|
-
|
|
- var user string
|
|
- switch msg.Channel {
|
|
- case "1001":
|
|
- user = s.userNames.reviewer1
|
|
- case "1002":
|
|
- user = s.userNames.reviewer2
|
|
- }
|
|
-
|
|
- reqID, err := parseMessageField(msg, "ID")
|
|
- if err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
-
|
|
- if err = s.clients[user].SubmitAccessRequestReview(ctx, reqID, types.AccessReview{
|
|
- Author: user,
|
|
- ProposedState: types.RequestState_APPROVED,
|
|
- Created: time.Now(),
|
|
- Reason: "okay",
|
|
- }); err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
- return nil
|
|
- })
|
|
- }
|
|
-
|
|
- // All the access requests should have been approved. Each approval triggers a message update.
|
|
- // As each access requests has 2 messages, this gives us 4 updates per access requests.
|
|
- // We consume all updates and fill counters.
|
|
- for i := 0; i < 2*2*s.raceNumber; i++ {
|
|
- process.SpawnCritical(func(ctx context.Context) error {
|
|
- msg, err := s.fakeDiscord.CheckMessageUpdateByAPI(ctx)
|
|
- if err != nil {
|
|
- return setRaceErr(trace.Wrap(err))
|
|
- }
|
|
-
|
|
- threadMsgKey := common.MessageData{ChannelID: msg.Channel, MessageID: msg.DiscordID}
|
|
- var newCounter int32
|
|
- val, _ := msgUpdateCounters.LoadOrStore(threadMsgKey, &newCounter)
|
|
- counterPtr := val.(*int32)
|
|
- atomic.AddInt32(counterPtr, 1)
|
|
-
|
|
- return nil
|
|
- })
|
|
- }
|
|
-
|
|
- process.Terminate()
|
|
- <-process.Done()
|
|
- require.NoError(t, raceErr)
|
|
-
|
|
- // We check each message was updated twice by using counters computed previously
|
|
- assert.Equal(t, int32(2*s.raceNumber), threadMsgsCount)
|
|
- threadMsgIDs.Range(func(key, value interface{}) bool {
|
|
- next := true
|
|
- val, loaded := msgUpdateCounters.LoadAndDelete(key)
|
|
- next = next && assert.True(t, loaded)
|
|
- counterPtr := val.(*int32)
|
|
- next = next && assert.Equal(t, int32(2), *counterPtr)
|
|
-
|
|
- return next
|
|
- })
|
|
-}
|
|
-
|
|
-func parseMessageField(msg DiscordMsg, field string) (string, error) {
|
|
- if msg.Text == "" {
|
|
- return "", trace.Errorf("message does not contain text")
|
|
- }
|
|
-
|
|
- matches := msgFieldRegexp.FindAllStringSubmatch(msg.Text, -1)
|
|
- if matches == nil {
|
|
- return "", trace.Errorf("cannot parse fields from text %s", msg.Text)
|
|
- }
|
|
- var fields []string
|
|
- for _, match := range matches {
|
|
- if match[1] == field {
|
|
- return match[2], nil
|
|
- }
|
|
- fields = append(fields, match[1])
|
|
- }
|
|
- return "", trace.Errorf("cannot find field %s in %v", field, fields)
|
|
-}
|
|
diff --git a/access/email/email_test.go b/access/email/email_test.go
|
|
index 3ba972df..af4b3890 100644
|
|
--- a/access/email/email_test.go
|
|
+++ b/access/email/email_test.go
|
|
@@ -187,6 +187,10 @@ func (s *EmailSuite) SetupSuite() {
|
|
s.teleportFeatures = teleportFeatures
|
|
}
|
|
|
|
+func (s *EmailSuite) TestNewSuiteMethodAdded() {
|
|
+
|
|
+}
|
|
+
|
|
func (s *EmailSuite) SetupTest() {
|
|
t := s.T()
|
|
|
|
@@ -354,53 +358,6 @@ func (s *EmailSuite) TestDenial() {
|
|
require.Contains(t, messages[0].Body, "Status: ❌ DENIED (not okay)")
|
|
}
|
|
|
|
-func (s *EmailSuite) TestReviewReplies() {
|
|
- t := s.T()
|
|
-
|
|
- if !s.teleportFeatures.AdvancedAccessWorkflows {
|
|
- t.Skip("Doesn't work in OSS version")
|
|
- }
|
|
-
|
|
- req := s.createAccessRequest([]string{s.userNames.reviewer1})
|
|
- s.checkPluginData(req.GetName(), func(data PluginData) bool {
|
|
- return len(data.EmailThreads) > 0
|
|
- })
|
|
-
|
|
- s.skipMessages(s.Context(), t, 2)
|
|
-
|
|
- err := s.reviewer1().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer1,
|
|
- ProposedState: types.RequestState_APPROVED,
|
|
- Created: time.Now(),
|
|
- Reason: "okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- messages := s.getMessages(s.Context(), t, 2)
|
|
-
|
|
- reply := messages[0].Body
|
|
-
|
|
- require.Contains(t, reply, s.userNames.reviewer1+" reviewed the request", "reply must contain a review author")
|
|
- require.Contains(t, reply, "Resolution: ✅ APPROVED", "reply must contain a proposed state")
|
|
- require.Contains(t, reply, "Reason: okay", "reply must contain a reason")
|
|
-
|
|
- err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
|
|
- Author: s.userNames.reviewer2,
|
|
- ProposedState: types.RequestState_DENIED,
|
|
- Created: time.Now(),
|
|
- Reason: "not okay",
|
|
- })
|
|
- require.NoError(t, err)
|
|
-
|
|
- messages = s.getMessages(s.Context(), t, 2)
|
|
-
|
|
- reply = messages[0].Body
|
|
-
|
|
- require.Contains(t, reply, s.userNames.reviewer2+" reviewed the request", "reply must contain a review author")
|
|
- require.Contains(t, reply, "Resolution: ❌ DENIED", "reply must contain a proposed state")
|
|
- require.Contains(t, reply, "Reason: not okay", "reply must contain a reason")
|
|
-}
|
|
-
|
|
func (s *EmailSuite) TestApprovalByReview() {
|
|
t := s.T()
|
|
|
|
@@ -439,6 +396,10 @@ func (s *EmailSuite) TestApprovalByReview() {
|
|
require.Contains(t, messages[0].Body, "Status: ✅ APPROVED (finally okay)")
|
|
}
|
|
|
|
+func (s *EmailSuite) TestAnotherNewMethodAdded() {
|
|
+
|
|
+}
|
|
+
|
|
func (s *EmailSuite) TestDenialByReview() {
|
|
t := s.T()
|
|
|
|
diff --git a/access/jira/jira_test.go b/access/jira/jira_test.go
|
|
index 80760b77..0e0f4e8d 100644
|
|
--- a/access/jira/jira_test.go
|
|
+++ b/access/jira/jira_test.go
|
|
@@ -377,6 +377,9 @@ func (s *JiraSuite) TestReviewComments() {
|
|
Created: time.Now(),
|
|
Reason: "okay",
|
|
})
|
|
+
|
|
+ // change in the middle of a method
|
|
+
|
|
require.NoError(t, err)
|
|
err = s.reviewer2().SubmitAccessRequestReview(s.Context(), req.GetName(), types.AccessReview{
|
|
Author: s.userNames.reviewer2,
|
|
@@ -390,14 +393,9 @@ func (s *JiraSuite) TestReviewComments() {
|
|
return data.IssueID != "" && data.ReviewsCount == 2
|
|
})
|
|
|
|
- comment, err := s.fakeJira.CheckNewIssueComment(s.Context())
|
|
- require.NoError(t, err)
|
|
- assert.Equal(t, pluginData.IssueID, comment.IssueID)
|
|
- assert.Contains(t, comment.Body, "*"+s.userNames.reviewer1+"* reviewed the request", "comment must contain a review author")
|
|
- assert.Contains(t, comment.Body, "Resolution: *APPROVED*", "comment must contain an approval resolution")
|
|
- assert.Contains(t, comment.Body, "Reason: okay", "comment must contain an approval reason")
|
|
+ // replacement in the middle of a method
|
|
|
|
- comment, err = s.fakeJira.CheckNewIssueComment(s.Context())
|
|
+ comment, err := s.fakeJira.CheckNewIssueComment(s.Context())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, pluginData.IssueID, comment.IssueID)
|
|
assert.Contains(t, comment.Body, "*"+s.userNames.reviewer2+"* reviewed the request", "comment must contain a review author")
|
|
diff --git a/access/mattermost/plugindata_test.go b/access/mattermost/plugindata_test.go
|
|
index 681acbc8..b075bbd4 100644
|
|
--- a/access/mattermost/plugindata_test.go
|
|
+++ b/access/mattermost/plugindata_test.go
|
|
@@ -32,17 +32,8 @@ func TestEncodePluginData(t *testing.T) {
|
|
assert.Equal(t, "CHANNEL1/POST01,CHANNEL2/POST02", dataMap["messages"])
|
|
}
|
|
|
|
-func TestDecodePluginData(t *testing.T) {
|
|
- pluginData := DecodePluginData(map[string]string{
|
|
- "user": "user-foo",
|
|
- "roles": "role-foo,role-bar",
|
|
- "request_reason": "foo reason",
|
|
- "reviews_count": "3",
|
|
- "resolution": "APPROVED",
|
|
- "resolve_reason": "foo ok",
|
|
- "messages": "CHANNEL1/POST01,CHANNEL2/POST02",
|
|
- })
|
|
- assert.Equal(t, samplePluginData, pluginData)
|
|
+func TestReplacement(t *testing.T) {
|
|
+
|
|
}
|
|
|
|
func TestEncodeEmptyPluginData(t *testing.T) {
|
|
diff --git a/access/slack/helpers_test.go b/access/slack/helpers_test.go
|
|
index 2ba7c515..f435ae98 100644
|
|
--- a/access/slack/helpers_test.go
|
|
+++ b/access/slack/helpers_test.go
|
|
@@ -28,3 +28,7 @@ func (set SlackDataMessageSet) Contains(msg common.MessageData) bool {
|
|
_, ok := set[msg]
|
|
return ok
|
|
}
|
|
+
|
|
+func (set SlackDataMessageSet) NewMethod() {
|
|
+
|
|
+}
|
|
diff --git a/event-handler/event_handler_test.go b/event-handler/event_handler_test.go
|
|
index 0a03c0e9..03747da7 100644
|
|
--- a/event-handler/event_handler_test.go
|
|
+++ b/event-handler/event_handler_test.go
|
|
@@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
+// This file was changed
|
|
+
|
|
package main
|
|
|
|
import (
|
|
diff --git a/event-handler/mtls_certs_test.go b/event-handler/mtls_certs_test.go
|
|
index 87911bec..ca6ce3cd 100644
|
|
--- a/event-handler/mtls_certs_test.go
|
|
+++ b/event-handler/mtls_certs_test.go
|
|
@@ -28,6 +28,9 @@ import (
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
+func TestNewMethodAddedMultipleLine(t *testing.T) {
|
|
+}
|
|
+
|
|
func TestGenerateClientCertFile(t *testing.T) {
|
|
td := t.TempDir()
|
|
cp := "client.crt"
|
|
@@ -52,3 +55,9 @@ func TestGenerateClientCertFile(t *testing.T) {
|
|
require.NoError(t, err)
|
|
require.Equal(t, certs.clientCert.Issuer.CommonName, rc.Issuer.CommonName)
|
|
}
|
|
+
|
|
+func SomeRandomPublicMethodAdded(t *testing.T) {
|
|
+
|
|
+}
|
|
+
|
|
+func TestNewMethodAddedSingleLine(t *testing.T) {}
|