Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(notifiers): support multiple slack workspaces #67

Merged
merged 6 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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'",
bsushmith marked this conversation as resolved.
Show resolved Hide resolved
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"`
rahmatrhd marked this conversation as resolved.
Show resolved Hide resolved
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
Loading