Skip to content

Commit

Permalink
Merge pull request #353 from ca-risken/feature/send_authz_request_api
Browse files Browse the repository at this point in the history
Add RequestProjectRole API
  • Loading branch information
senk8 authored Mar 6, 2024
2 parents 2b27f76 + b5c1cba commit d9769d5
Show file tree
Hide file tree
Showing 36 changed files with 1,897 additions and 507 deletions.
7 changes: 7 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,13 @@ test-notification:
-d '{"project_id":1001, "notification_id":1001}' \
$(CORE_API_ADDR) core.alert.AlertService.TestNotification

.PHONY: request-project-role-notification
request-project-role-notification:
$(GRPCURL) \
-plaintext \
-d '{"project_id":1001, "user_id": 1001}' \
$(CORE_API_ADDR) core.alert.AlertService.RequestProjectRoleNotification

.PHONY: analyze-alert
analyze-alert:
$(GRPCURL) \
Expand Down
1 change: 1 addition & 0 deletions pkg/db/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ func (c *Client) ListNotification(ctx context.Context, projectID uint32, notifyT
query += " and type = ?"
params = append(params, notifyType)
}
query += " order by notification_id asc"
var data []model.Notification
if err := c.Slave.WithContext(ctx).Raw(query, params...).Scan(&data).Error; err != nil {
return nil, err
Expand Down
35 changes: 34 additions & 1 deletion pkg/server/alert/alert_notification.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"github.com/ca-risken/core/pkg/model"
"github.com/ca-risken/core/proto/alert"
"github.com/ca-risken/core/proto/finding"
"github.com/ca-risken/core/proto/iam"
"github.com/ca-risken/core/proto/project"
"github.com/golang/protobuf/ptypes/empty"
"github.com/vikyd/zero"
"gorm.io/gorm"
Expand Down Expand Up @@ -214,7 +216,38 @@ func (a *AlertService) TestNotification(ctx context.Context, req *alert.TestNoti
return nil, err
}
default:
a.logger.Warnf(ctx, "This notification_type is unimprement. type: %v", notification.Type)
a.logger.Warnf(ctx, "This notification_type is unimplemented. type: %v", notification.Type)
}
return &empty.Empty{}, nil
}

func (a *AlertService) RequestProjectRoleNotification(ctx context.Context, req *alert.RequestProjectRoleNotificationRequest) (*empty.Empty, error) {
if err := req.Validate(); err != nil {
return nil, err
}
notifications, err := a.repository.ListNotification(ctx, req.ProjectId, "slack", 0, time.Now().Unix())
if err != nil {
return nil, err
}
notification := (*notifications)[0]
projects, err := a.projectClient.ListProject(ctx, &project.ListProjectRequest{ProjectId: req.ProjectId})
if err != nil {
return nil, err
}
user, err := a.iamClient.GetUser(ctx, &iam.GetUserRequest{UserId: req.UserId})
if err != nil {
return nil, err
}
switch notification.Type {
case "slack":
err = a.sendSlackRequestProjectRoleNotification(ctx, a.baseURL, notification.NotifySetting, a.defaultLocale, user.User.Name, projects.Project[0].Name, req.ProjectId)
if err != nil {
a.logger.Errorf(ctx, "Error occured when sending request authz slack notification. err: %v", err)
return nil, err
}
default:
a.logger.Warnf(ctx, "This notification_type is unimplemented. type: %v", notification.Type)
return nil, fmt.Errorf("this notification_type is unavailable. type: %v", notification.Type)
}
return &empty.Empty{}, nil
}
Expand Down
148 changes: 148 additions & 0 deletions pkg/server/alert/alert_notification_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ import (
"github.com/ca-risken/core/proto/alert"
"github.com/ca-risken/core/proto/finding"
findingmock "github.com/ca-risken/core/proto/finding/mocks"
"github.com/ca-risken/core/proto/iam"
iammock "github.com/ca-risken/core/proto/iam/mocks"
"github.com/ca-risken/core/proto/project"
projectmock "github.com/ca-risken/core/proto/project/mocks"
"github.com/jarcoal/httpmock"
)

Expand Down Expand Up @@ -327,3 +330,148 @@ func TestPutNotification(t *testing.T) {
})
}
}

func TestRequestProjectRoleNotification(t *testing.T) {
var ctx context.Context
type mockListNotification struct {
Resp *[]model.Notification
Err error
}
type mockListProject struct {
Resp *project.ListProjectResponse
Err error
}
type mockGetUser struct {
Resp *iam.GetUserResponse
Err error
}
now := time.Now()
cases := []struct {
name string
input *alert.RequestProjectRoleNotificationRequest
wantErr bool
listNotification mockListNotification
listProject mockListProject
getUser mockGetUser
}{
{
name: "OK Request project role",
input: &alert.RequestProjectRoleNotificationRequest{ProjectId: 1001, UserId: 1001},
wantErr: false,
listNotification: mockListNotification{
Resp: &[]model.Notification{
{ProjectID: 1001, Name: "name", Type: "slack", NotifySetting: `{"webhook_url": "https://example.com"}`, CreatedAt: now, UpdatedAt: now},
},
Err: nil,
},
getUser: mockGetUser{
Resp: &iam.GetUserResponse{
User: &iam.User{UserId: 1001, Name: "userName"},
},
Err: nil,
},
listProject: mockListProject{
Resp: &project.ListProjectResponse{
Project: []*project.Project{
{ProjectId: 1001, Name: "projectName"},
},
},
Err: nil,
},
},
{
name: "NG unimplemented notification type",
input: &alert.RequestProjectRoleNotificationRequest{ProjectId: 1001, UserId: 1001},
wantErr: true,
listNotification: mockListNotification{
Resp: &[]model.Notification{
{ProjectID: 1001, Name: "name", Type: "unimplemented", NotifySetting: `{"webhook_url": "https://example.com"}`, CreatedAt: now, UpdatedAt: now},
},
Err: nil,
},
listProject: mockListProject{
Resp: &project.ListProjectResponse{
Project: []*project.Project{
{ProjectId: 1001, Name: "projectName"},
},
},
Err: nil,
},
getUser: mockGetUser{
Resp: &iam.GetUserResponse{
User: &iam.User{UserId: 1001, Name: "userName"},
},
Err: nil,
},
},
{
name: "NG ListNotification (Notification Not Found)",
input: &alert.RequestProjectRoleNotificationRequest{ProjectId: 1001, UserId: 1001},
wantErr: true,
listNotification: mockListNotification{
Resp: &[]model.Notification{},
Err: gorm.ErrRecordNotFound,
},
},
{
name: "NG ListProject (API Error)",
input: &alert.RequestProjectRoleNotificationRequest{ProjectId: 1001, UserId: 1001},
wantErr: true,
listNotification: mockListNotification{
Resp: &[]model.Notification{
{ProjectID: 1001, Name: "name", Type: "slack", NotifySetting: `{"webhook_url": "https://example.com"}`, CreatedAt: now, UpdatedAt: now},
},
Err: nil,
},
listProject: mockListProject{
Resp: &project.ListProjectResponse{
Project: []*project.Project{
{ProjectId: 1001, Name: "projectName"},
},
},
Err: errors.New("api error"),
},
},
{
name: "NG GetUser (API Error)",
input: &alert.RequestProjectRoleNotificationRequest{ProjectId: 1001, UserId: 1001},
wantErr: true,
listNotification: mockListNotification{
Resp: &[]model.Notification{
{ProjectID: 1001, Name: "name", Type: "slack", NotifySetting: `{"webhook_url": "https://example.com"}`, CreatedAt: now, UpdatedAt: now},
},
Err: errors.New("api error"),
},
listProject: mockListProject{
Resp: &project.ListProjectResponse{
Project: []*project.Project{
{ProjectId: 1001, Name: "projectName"},
},
},
Err: nil,
},
getUser: mockGetUser{
Resp: &iam.GetUserResponse{
User: &iam.User{UserId: 1001, Name: "userName"},
},
Err: gorm.ErrRecordNotFound,
},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
mockDB := mocks.NewAlertRepository(t)
mockDB.On("ListNotification", test.RepeatMockAnything(5)...).Return(c.listNotification.Resp, c.listNotification.Err).Once()
mockProject := projectmock.ProjectServiceClient{}
mockProject.On("ListProject", mock.Anything, mock.Anything).Return(c.listProject.Resp, c.listProject.Err)
mockIAM := iammock.IAMServiceClient{}
mockIAM.On("GetUser", mock.Anything, mock.Anything).Return(c.getUser.Resp, c.getUser.Err)

svc := AlertService{projectClient: &mockProject, iamClient: &mockIAM, repository: mockDB, logger: logging.NewLogger()}
_, err := svc.RequestProjectRoleNotification(ctx, c.input)
if err != nil && !c.wantErr {
t.Fatalf("Unexpected error: %+v", err)
}
})
}
}
63 changes: 59 additions & 4 deletions pkg/server/alert/alert_slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,12 @@ const (
- Remove the root cause of the problem
- If it is an intentional setup/operation and the risk is small, archive it
- If the nature of the problem is not urgent and immediate action is difficult, set a target deadline and PEND`
slackNotificationAttachmentJa = "その他、%d件すべてのFindingは <%s/alert/alert?project_id=%d&from=slack|アラート画面> からご確認ください。"
slackNotificationAttachmentEn = "Please check all %d Findings from <%s/alert/alert?project_id=%d&from=slack|Alert screen>."
slackNotificationTestMessageJa = "RISKENからのテスト通知です"
slackNotificationTestMessageEn = "This is a test notification from RISKEN"
slackNotificationAttachmentJa = "その他、%d件すべてのFindingは <%s/alert/alert?project_id=%d&from=slack|アラート画面> からご確認ください。"
slackNotificationAttachmentEn = "Please check all %d Findings from <%s/alert/alert?project_id=%d&from=slack|Alert screen>."
slackNotificationTestMessageJa = "RISKENからのテスト通知です"
slackNotificationTestMessageEn = "This is a test notification from RISKEN"
slackRequestProjectRoleNotificationMessageJa = `<!here> %sさんがプロジェクト%sへのアクセスをリクエストしました。プロジェクト管理者は問題がなければ<%s/iam/user?project_id=%d|ユーザー一覧>から%sさんを招待してください。`
slackRequestProjectRoleNotificationMessageEn = `<!here> %s has requested access to your Project %s. If there are no issues, the project administrator should <%s/iam/user?project_id=%d|the user list> and invite %s.`
)

func (a *AlertService) sendSlackNotification(
Expand Down Expand Up @@ -111,6 +113,37 @@ func (a *AlertService) sendSlackTestNotification(ctx context.Context, url, notif
return nil
}

func (a *AlertService) sendSlackRequestProjectRoleNotification(ctx context.Context, url, notifySetting, defaultLocale, userName, projectName string, projectID uint32) error {
var setting slackNotifySetting
if err := json.Unmarshal([]byte(notifySetting), &setting); err != nil {
return err
}

var locale string
switch setting.Locale {
case LocaleJa:
locale = LocaleJa
case LocaleEn:
locale = LocaleEn
default:
locale = defaultLocale
}

if setting.WebhookURL != "" {
webhookMsg := getRequestProjectRoleWebhookMessage(setting.Data.Channel, locale, userName, projectName, url, projectID)
if err := slack.PostWebhook(setting.WebhookURL, webhookMsg); err != nil {
return fmt.Errorf("failed to send slack(webhookurl): %w", err)
}
} else if setting.ChannelID != "" {
if err := a.postMessageSlackWithRetry(ctx,
setting.ChannelID, slack.MsgOptionText(getRequestProjectRoleSlackMessageText(locale, userName, projectName, url, projectID), false)); err != nil {
return fmt.Errorf("failed to send slack(postmessage): %w", err)
}
}

return nil
}

func (a *AlertService) postMessageSlack(channelID string, msg ...slack.MsgOption) error {
if _, _, err := a.slackClient.PostMessage(channelID, msg...); err != nil {
var rateLimitError *slack.RateLimitedError
Expand Down Expand Up @@ -249,6 +282,28 @@ func getTestSlackMessageText(locale string) string {
return msgText
}

func getRequestProjectRoleWebhookMessage(channel, locale, projectName, userName, url string, projectID uint32) *slack.WebhookMessage {
msg := slack.WebhookMessage{
Text: getRequestProjectRoleSlackMessageText(locale, userName, projectName, url, projectID),
}
// override message
if channel != "" {
msg.Channel = channel
}
return &msg
}

func getRequestProjectRoleSlackMessageText(locale, projectName, userName, url string, projectID uint32) string {
var msgText string
switch locale {
case LocaleJa:
msgText = slackRequestProjectRoleNotificationMessageJa
default:
msgText = slackRequestProjectRoleNotificationMessageEn
}
return fmt.Sprintf(msgText, userName, projectName, url, projectID, userName)
}

func getColor(severity string) string {
switch severity {
case "high":
Expand Down
4 changes: 4 additions & 0 deletions pkg/server/alert/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/ca-risken/core/pkg/db"
"github.com/ca-risken/core/proto/alert"
"github.com/ca-risken/core/proto/finding"
"github.com/ca-risken/core/proto/iam"
"github.com/ca-risken/core/proto/project"
"github.com/cenkalti/backoff/v4"
"github.com/slack-go/slack"
Expand All @@ -19,6 +20,7 @@ type AlertService struct {
repository db.AlertRepository
findingClient finding.FindingServiceClient
projectClient project.ProjectServiceClient
iamClient iam.IAMServiceClient
maxAnalyzeAPICall int64
baseURL string
logger logging.Logger
Expand All @@ -32,6 +34,7 @@ func NewAlertService(
baseURL string,
findingClient finding.FindingServiceClient,
projectClient project.ProjectServiceClient,
iamClient iam.IAMServiceClient,
repository db.AlertRepository,
logger logging.Logger,
defaultLocale string,
Expand All @@ -41,6 +44,7 @@ func NewAlertService(
repository: repository,
findingClient: findingClient,
projectClient: projectClient,
iamClient: iamClient,
maxAnalyzeAPICall: maxAnalyzeAPICall,
baseURL: baseURL,
logger: logger,
Expand Down
1 change: 1 addition & 0 deletions pkg/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func (s *Server) Run(ctx context.Context) error {
s.config.BaseURL,
fc,
pc,
iamc,
s.db,
s.logger,
s.config.defaultLocale,
Expand Down
4 changes: 2 additions & 2 deletions proto/alert/entity.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit d9769d5

Please sign in to comment.