Skip to content

Commit

Permalink
feat(notifiers): support multiple slack workspaces (#67)
Browse files Browse the repository at this point in the history
- changes to configure multiple slack workspaces and corresponding bot tokens
- slack notifier will use email (other user attributes are difficult since for approvers we wouldn't have any additional details) for evaluating configured criteria and send notifications to corresponding slack workspace
- additional test cases
- maintains backward compatibility
- add test-short make directive
  • Loading branch information
bsushmith authored Sep 12, 2023
1 parent 7023ec4 commit b6afb30
Show file tree
Hide file tree
Showing 7 changed files with 351 additions and 28 deletions.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ clean: tidy
test:
go test ./... -race -coverprofile=coverage.out

test-short:
@echo "Running short tests by disabling store tests..."
go test ./... -race -short -coverprofile=coverage.out

coverage: test
@echo "Generating coverage report..."
@go tool cover -html=coverage.out
Expand Down
4 changes: 4 additions & 0 deletions internal/server/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ DB:
NOTIFIER:
PROVIDER: slack
ACCESS_TOKEN:
WORKSPACES:
- WORKSPACE: goto
ACCESS_TOKEN:
CRITERIA: "email contains '@goto'"
JOBS:
FETCH_RESOURCES:
ENABLED: true
Expand Down
65 changes: 65 additions & 0 deletions pkg/evaluator/expression_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,71 @@ func TestEvaluate(t *testing.T) {
},
expectedResult: true,
},
{
// a complex test case to evaluate the expression where "the appeal role must either not contain viewer or (the resource type is gcloud_iam and the duration is 0h)
expression: "!($appeal.role contains 'viewer') || $appeal.resource.provider_type == 'gcloud_iam' && $appeal.options.duration in ['0h','']",
params: map[string]interface{}{
"appeal": map[string]interface{}{
"role": "Editor",
"resource": map[string]interface{}{
"provider_type": "gcloud_iam",
},
"options": map[string]interface{}{
"duration": "0h",
},
},
},
expectedResult: true,
},
{
// a complex test case to evaluate the expression where "the appeal role must either not contain viewer or (the resource type is gcloud_iam and the duration is 0h and the role must contain viewer)
expression: "!($appeal.role contains 'viewer') || $appeal.resource.provider_type == 'gcloud_iam' && $appeal.options.duration in ['0h',''] && $appeal.role contains 'viewer'",
params: map[string]interface{}{
"appeal": map[string]interface{}{
"role": "viewer",
"resource": map[string]interface{}{
"provider_type": "gcloud_iam",
},
"options": map[string]interface{}{
"duration": "0h",
},
},
},
expectedResult: true,
},
{
// a complex test case to evaluate the expression where "the appeal role must either not contain viewer or (the resource type is gcloud_iam and the duration is 0h and the role must contain viewer
expression: "!($appeal.role contains 'viewer') || $appeal.resource.provider_type == 'gcloud_iam' && $appeal.options.duration in ['0h',''] && $appeal.role contains 'viewer'",
params: map[string]interface{}{
"appeal": map[string]interface{}{
"role": "viewer",
"resource": map[string]interface{}{
"provider_type": "gcloud_iam",
},
"options": map[string]interface{}{
"duration": "2160h",
},
},
},
expectedResult: false,
},
{
// a complex test case to evaluate the expression where "the appeal role must either not contain admin_user or (the resource type is gcloud_iam and the duration is 0h and the role must contain admin_user)
expression: "!($appeal.role contains 'admin_user') || $appeal.resource.provider_type == 'gcloud_iam' && $appeal.options.duration in ['0h',''] && $appeal.role contains 'admin_user'",
params: map[string]interface{}{
"appeal": map[string]interface{}{
"role": "viewer",
"resource": map[string]interface{}{
"provider_type": "gcloud_iam",
},
"options": map[string]interface{}{
"duration": "2160h",
},
},
},
expectedResult: true,
},

{
expression: "len(Split($user.email_id, '@')[0]) > 2",
params: map[string]interface{}{
Expand Down
45 changes: 41 additions & 4 deletions plugins/notifiers/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,58 @@ type Config struct {
Provider string `mapstructure:"provider" validate:"omitempty,oneof=slack"`

// slack
AccessToken string `mapstructure:"access_token" validate:"required_if=Provider slack"`
AccessToken string `mapstructure:"access_token" validate:"required_without=Workspaces"`
Workspaces []slack.SlackWorkspace `mapstructure:"workspaces" validate:"required_without=AccessToken,dive"`

// custom messages
Messages domain.NotificationMessages
}

func NewClient(config *Config) (Client, error) {
if config.Provider == ProviderTypeSlack {
slackConfig := &slack.Config{
AccessToken: config.AccessToken,
Messages: config.Messages,

slackConfig, err := NewSlackConfig(config)
if err != nil {
return nil, err
}

httpClient := &http.Client{Timeout: 10 * time.Second}
return slack.NewNotifier(slackConfig, httpClient), nil
}

return nil, errors.New("invalid notifier provider type")
}

func NewSlackConfig(config *Config) (*slack.Config, error) {

// validation
if config.AccessToken == "" && len(config.Workspaces) == 0 {
return nil, errors.New("slack access token or workspaces must be provided")
}
if config.AccessToken != "" && len(config.Workspaces) != 0 {
return nil, errors.New("slack access token and workspaces cannot be provided at the same time")
}

var slackConfig *slack.Config
if config.AccessToken != "" {
workspaces := []slack.SlackWorkspace{
{
WorkspaceName: "default",
AccessToken: config.AccessToken,
Criteria: "1==1",
},
}
slackConfig = &slack.Config{
Workspaces: workspaces,
Messages: config.Messages,
}
return slackConfig, nil
}

slackConfig = &slack.Config{
Workspaces: config.Workspaces,
Messages: config.Messages,
}

return slackConfig, nil
}
111 changes: 111 additions & 0 deletions plugins/notifiers/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package notifiers

import (
"github.com/goto/guardian/plugins/notifiers/slack"
"reflect"
"testing"
)

func TestNewSlackConfig(t *testing.T) {
type args struct {
config *Config
}
tests := []struct {
name string
args args
want *slack.Config
wantErr bool
}{
{
name: "should return error when no access token or workspaces are provided",
args: args{
config: &Config{
Provider: ProviderTypeSlack,
},
},
want: nil,
wantErr: true,
}, {
name: "should return error when both access token and workspaces are provided",
args: args{
config: &Config{
Provider: ProviderTypeSlack,
AccessToken: "foo",
Workspaces: []slack.SlackWorkspace{
{
WorkspaceName: "default",
AccessToken: "bar",
Criteria: "1==1",
},
},
},
},
want: nil,
wantErr: true,
}, {
name: "should return slack config when access token is provided",
args: args{
config: &Config{
Provider: ProviderTypeSlack,
AccessToken: "foo",
},
},
want: &slack.Config{
Workspaces: []slack.SlackWorkspace{
{
WorkspaceName: "default",
AccessToken: "foo",
Criteria: "1==1",
},
},
},
wantErr: false,
}, {
name: "should return slack config when workspaces are provided",
args: args{
config: &Config{
Provider: ProviderTypeSlack,
Workspaces: []slack.SlackWorkspace{
{
WorkspaceName: "A",
AccessToken: "foo",
Criteria: "$email contains '@abc'",
},
{
WorkspaceName: "B",
AccessToken: "bar",
Criteria: "$email contains '@xyz'",
},
},
},
},
want: &slack.Config{
Workspaces: []slack.SlackWorkspace{
{
WorkspaceName: "A",
AccessToken: "foo",
Criteria: "$email contains '@abc'",
},
{
WorkspaceName: "B",
AccessToken: "bar",
Criteria: "$email contains '@xyz'",
},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := NewSlackConfig(tt.args.config)
if (err != nil) != tt.wantErr {
t.Errorf("NewSlackConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewSlackConfig() got = %v, want %v", got, tt.want)
}
})
}
}
Loading

0 comments on commit b6afb30

Please sign in to comment.