Skip to content

Commit

Permalink
feat(notifier): lark support for guardian (#163)
Browse files Browse the repository at this point in the history
* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* lark support

* fix: fix typo in mapstructure annotation

* fix: map old notifier config to new multiple notifiers format

* fix: fix slack client initialization with multiple workspaces

* fix: fix lint errors

* add json config for notifiers

* add json config for notifiers

* add json config for notifiers

* add json config for notifiers

* add json config for notifiers

* add json config for notifiers

* fix review commits

* fix review commits

* feat(email): handling email case sensitive (#165)

* feat(email): handling email case sensitive

* chore: handling list page

* chore: handle case on filter

---------

Co-authored-by: Muhammad Idil Haq Amir <idil.amir@tokopedia.com>

* feat(gate): introduce gate provider (#164)

* feat(gate): introduce gate provider

* fix: send gate api key in query param

* fix: handle non-success http codes

* feat(notifier): expose variables in notifier criteria (#166)

* feat: add support for google_oauth2 token. Related to issue: https://… (#169)

* feat: add support for google_oauth2 token. Related to issue: #168

* add test cases

* add test cases

* add error formatting changes

---------

Co-authored-by: anjali.agarwal <anjali.aggarwal@gojek.com>

* feat: add support for expression in request body when fetching metada… (#170)

* feat: add support for expression in request body when fetching metadata sources for appeal

* add a condition in http mock call

---------

Co-authored-by: anjali.agarwal <anjali.aggarwal@gojek.com>

* ci: lock golangci-lint to v1.59 (#172)

* ci: lock golangci-lint to v1.59

* ci: fix golangci-lint warnings

* chore: fetch creator details before fetching metadata sources as they… (#174)

chore: fetch creator details before fetching metadata sources as they are helpful

Co-authored-by: anjali.agarwal <anjali.aggarwal@gojek.com>

---------

Co-authored-by: Rahmat Hidayat <rahmatramahidayat@gmail.com>
Co-authored-by: Muhammad Idil Haq Amir <idil.amir@gotocompany.com>
Co-authored-by: Muhammad Idil Haq Amir <idil.amir@tokopedia.com>
Co-authored-by: anjali9791 <anjalia271@gmail.com>
Co-authored-by: anjali.agarwal <anjali.aggarwal@gojek.com>
  • Loading branch information
6 people authored Aug 27, 2024
1 parent be2d47c commit 31420a7
Show file tree
Hide file tree
Showing 17 changed files with 570 additions and 18 deletions.
25 changes: 24 additions & 1 deletion cli/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package cli

import (
"context"
"encoding/json"
"fmt"

"github.com/goto/guardian/pkg/log"
"github.com/mitchellh/mapstructure"

"github.com/MakeNowJust/heredoc"
"github.com/go-playground/validator/v10"
Expand Down Expand Up @@ -69,7 +71,28 @@ func runJobCmd() *cobra.Command {
logger := log.NewCtxLogger(config.LogLevel, []string{config.AuditLogTraceIDHeaderKey})
crypto := crypto.NewAES(config.EncryptionSecretKeyKey)
validator := validator.New()
notifier, err := notifiers.NewClient(&config.Notifier, logger)
var notifierMap map[string]interface{}
errr := json.Unmarshal([]byte(config.Notifiers), &notifierMap)
if errr != nil {
return fmt.Errorf("failed to parse notifier config: %w", errr)
}
var notifierConfigMap map[string]notifiers.Config
err = mapstructure.Decode(notifierMap, &notifierConfigMap)
if err != nil {
return fmt.Errorf("failed to parse notifier config: %w", err)
}
notifierConfig := []notifiers.Config{}
if config.Notifiers != "" {
for _, val := range notifierConfigMap {
notifierConfig = append(notifierConfig, val)
}
} else {
// map old to the new format
oldConfig := config.Notifier
oldConfig.Criteria = "true"
notifierConfig = append(notifierConfig, oldConfig)
}
notifier, err := notifiers.NewMultiClient(&notifierConfig, logger)
if err != nil {
return err
}
Expand Down
14 changes: 8 additions & 6 deletions internal/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ type Jobs struct {
}

type Config struct {
Port int `mapstructure:"port" default:"8080"`
GRPC GRPCConfig `mapstructure:"grpc"`
EncryptionSecretKeyKey string `mapstructure:"encryption_secret_key"`
Notifier notifiers.Config `mapstructure:"notifier"`
LogLevel string `mapstructure:"log_level" default:"info"`
DB store.Config `mapstructure:"db"`
Port int `mapstructure:"port" default:"8080"`
GRPC GRPCConfig `mapstructure:"grpc"`
EncryptionSecretKeyKey string `mapstructure:"encryption_secret_key"`
Notifiers string `mapstructure:"notifiers"`
// Deprecated: use Notifiers instead
Notifier notifiers.Config `mapstructure:"notifier"`
LogLevel string `mapstructure:"log_level" default:"info"`
DB store.Config `mapstructure:"db"`
// Deprecated: use Auth.Default.HeaderKey instead note on the AuthenticatedUserHeaderKey
AuthenticatedUserHeaderKey string `mapstructure:"authenticated_user_header_key"`
AuditLogTraceIDHeaderKey string `mapstructure:"audit_log_trace_id_header_key" default:"X-Trace-Id"`
Expand Down
22 changes: 15 additions & 7 deletions internal/server/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,21 @@ DB:
NAME:
PORT: 5432
SSLMODE: disable
NOTIFIER:
PROVIDER: slack
ACCESS_TOKEN:
WORKSPACES:
- WORKSPACE: goto
ACCESS_TOKEN:
CRITERIA: "email contains '@goto'"
NOTIFIERS: |
{
"my_lark": {
"provider": "lark",
"client_id": "",
"client_secret": "",
"messages": {}
},
"my_slack": {
"provider": "slack",
"access_token": "",
"messages": {}
}
}
JOBS:
REVOKE_GRANTS_BY_USER_CRITERIA:
CONFIG:
Expand Down
27 changes: 26 additions & 1 deletion internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (

"github.com/goto/guardian/domain"

"encoding/json"

"github.com/go-playground/validator/v10"
handlerv1beta1 "github.com/goto/guardian/api/handler/v1beta1"
guardianv1beta1 "github.com/goto/guardian/api/proto/gotocompany/guardian/v1beta1"
Expand All @@ -25,6 +27,7 @@ import (
grpc_logrus "github.com/grpc-ecosystem/go-grpc-middleware/logging/logrus"
grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/mitchellh/mapstructure"
"github.com/sirupsen/logrus"
"github.com/uptrace/opentelemetry-go-extra/otelgorm"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
Expand All @@ -49,7 +52,29 @@ func RunServer(config *Config) error {
logger := log.NewCtxLogger(config.LogLevel, []string{domain.TraceIDKey})
crypto := crypto.NewAES(config.EncryptionSecretKeyKey)
validator := validator.New()
notifier, err := notifiers.NewClient(&config.Notifier, logger)

var notifierMap map[string]interface{}
err := json.Unmarshal([]byte(config.Notifiers), &notifierMap)
if err != nil {
return fmt.Errorf("failed to parse notifier config: %w", err)
}
var notifierConfigMap map[string]notifiers.Config
err = mapstructure.Decode(notifierMap, &notifierConfigMap)
if err != nil {
return fmt.Errorf("failed to parse notifier config: %w", err)
}
notifierConfig := []notifiers.Config{}
if config.Notifiers != "" {
for _, val := range notifierConfigMap {
notifierConfig = append(notifierConfig, val)
}
} else {
// map old to the new format
oldConfig := config.Notifier
oldConfig.Criteria = "true"
notifierConfig = append(notifierConfig, oldConfig)
}
notifier, err := notifiers.NewMultiClient(&notifierConfig, logger)
if err != nil {
return err
}
Expand Down
110 changes: 107 additions & 3 deletions plugins/notifiers/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,53 @@ import (
"net/http"
"time"

"github.com/goto/guardian/pkg/evaluator"
"github.com/goto/guardian/pkg/log"
"github.com/goto/guardian/plugins/notifiers/lark"
"github.com/goto/guardian/plugins/notifiers/slack"
"github.com/mitchellh/mapstructure"

"github.com/goto/guardian/domain"
"github.com/goto/guardian/plugins/notifiers/slack"
)

type Client interface {
Notify(context.Context, []domain.Notification) []error
}

type NotifyManager struct {
clients []Client
configs []Config
}

func (m *NotifyManager) Notify(ctx context.Context, notification []domain.Notification) []error {
var errs []error
for i, client := range m.clients {
// evaluate criteria
config := m.configs[i]
v, err := evaluator.Expression(config.Criteria).EvaluateWithVars(map[string]interface{}{
"email": notification[0].User,
})
if err != nil {
errs = append(errs, err)
continue
}

// if the expression evaluates to true, notify the client
if match, ok := v.(bool); !ok {
errs = append(errs, fmt.Errorf("notifier expression did not evaluate to a boolean: %s", config.Criteria))
} else if match {
if notifyErrs := client.Notify(ctx, notification); notifyErrs != nil {
errs = append(errs, notifyErrs...)
}
}

}
return errs
}

const (
ProviderTypeSlack = "slack"
ProviderTypeLark = "lark"
)

// SlackConfig is a map of workspace name to config
Expand All @@ -30,16 +64,47 @@ func (c SlackConfig) Decode(v interface{}) error {
}

type Config struct {
Provider string `mapstructure:"provider" validate:"omitempty,oneof=slack"`
Provider string `mapstructure:"provider" validate:"omitempty,oneof=slack lark"`
Name string `mapstructure:"name"`
ClientID string `mapstructure:"client_id,omitempty"`
ClientSecret string `mapstructure:"client_secret,omitempty"`
Criteria string `mapstructure:"criteria"`

// slack
AccessToken string `mapstructure:"access_token" validate:"required_without=SlackConfig"`
SlackConfig SlackConfig `mapstructure:"slack_config" validate:"required_without=AccessToken,dive"`

// custom messages
Messages domain.NotificationMessages
}

func NewMultiClient(notifiers *[]Config, logger log.Logger) (*NotifyManager, error) {
notifyManager := &NotifyManager{}
for _, notifier := range *notifiers {
if notifier.Provider == ProviderTypeSlack {
slackConfig, err := NewSlackConfig(&notifier)
if err != nil {
return nil, err
}
httpClient := &http.Client{Timeout: 10 * time.Second}
slackClient := slack.NewNotifier(slackConfig, httpClient, logger)
notifyManager.addClient(slackClient)
notifyManager.addNotifier(notifier)
}
if notifier.Provider == ProviderTypeLark {
larkConfig, err := getLarkConfig(&notifier, notifier.Messages)
if err != nil {
return nil, err
}
httpClient := &http.Client{Timeout: 10 * time.Second}
larkClient := lark.NewNotifier(larkConfig, httpClient, logger)
notifyManager.addClient(larkClient)
notifyManager.addNotifier(notifier)
}
}

return notifyManager, nil
}

func NewClient(config *Config, logger log.Logger) (Client, error) {
if config.Provider == ProviderTypeSlack {
slackConfig, err := NewSlackConfig(config)
Expand Down Expand Up @@ -92,3 +157,42 @@ func NewSlackConfig(config *Config) (*slack.Config, error) {

return slackConfig, nil
}

func getLarkConfig(config *Config, messages domain.NotificationMessages) (*lark.Config, error) {
// validation
if config.ClientID == "" && config.ClientSecret == "" {
return nil, errors.New("lark clientid & clientSecret must be provided")
}
if config.ClientID == "" && config.ClientSecret != "" {
return nil, errors.New("lark clientid & clientSecret must be provided")
}
if config.ClientID != "" && config.ClientSecret == "" {
return nil, errors.New("lark clientid & clientSecret must be provided")
}

var larkConfig *lark.Config
if config.ClientID != "" {
workspace := lark.LarkWorkspace{
WorkspaceName: config.Provider,
ClientID: config.ClientID,
ClientSecret: config.ClientSecret,
Criteria: config.Criteria,
}
larkConfig = &lark.Config{
Workspace: workspace,
Messages: messages,
}
return larkConfig, nil

}

return larkConfig, nil
}

func (nm *NotifyManager) addClient(client Client) {
nm.clients = append(nm.clients, client)
}

func (nm *NotifyManager) addNotifier(notifier Config) {
nm.configs = append(nm.configs, notifier)
}
101 changes: 101 additions & 0 deletions plugins/notifiers/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"reflect"
"testing"

"github.com/goto/guardian/domain"
"github.com/goto/guardian/plugins/notifiers/lark"
"github.com/goto/guardian/plugins/notifiers/slack"
)

Expand Down Expand Up @@ -114,3 +116,102 @@ func TestNewSlackConfig(t *testing.T) {
})
}
}

func TestNewSlackLarkConfig(t *testing.T) {
type args struct {
config Config
}
tests := []struct {
name string
args args
want *lark.Config
wantErr bool
}{
{
name: "should return lark config when clientid is provided",
args: args{
config: Config{
Provider: "lark",
AccessToken: "",
ClientID: "foo",
ClientSecret: "foo",
Criteria: "$email contains '@gojek'",
},
},
want: &lark.Config{
Workspace: lark.LarkWorkspace{
WorkspaceName: "lark",
ClientID: "foo",
ClientSecret: "foo",
Criteria: "$email contains '@gojek'",
},
Messages: domain.NotificationMessages{},
},
wantErr: false,
},
{
name: "should return error when no Client id or workspaces are provided",
args: args{
config: Config{
Provider: "provider",
AccessToken: "config.Notifier.AccessToken",
ClientID: "",
ClientSecret: "",
Criteria: ".send_to_slack == true",
},
},
want: nil,
wantErr: true,
}, {
name: "should return error when both Client id and workspaces are provided",
args: args{
config: Config{
Provider: "provider",
AccessToken: "config.Notifier.AccessToken",
ClientID: "",
ClientSecret: "",
Criteria: ".send_to_slack == true",
},
},
want: nil,
wantErr: true,
}, {
name: "should return lark config when workspaces are provided",
args: args{
config: Config{
Provider: "provider",
AccessToken: "config.Notifier.AccessToken",
ClientID: "foo",
ClientSecret: "foo",
Criteria: ".send_to_slack == true",
},
},
want: &lark.Config{
Workspace: lark.LarkWorkspace{
WorkspaceName: "provider",
ClientID: "foo",
ClientSecret: "foo",
Criteria: ".send_to_slack == true",
},
Messages: domain.NotificationMessages{},
},
wantErr: false,
},
}
for _, tt := range tests {

t.Run(tt.name, func(t *testing.T) {

got, err := getLarkConfig(&tt.args.config, domain.NotificationMessages{})

if (err != nil) != tt.wantErr {
t.Errorf("NewLarkConfig() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewLarkConfig() got = %v, want %v", got, tt.want)
}

})
}
}
Loading

0 comments on commit 31420a7

Please sign in to comment.