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

Add RequestProjectRole API #353

Merged
merged 12 commits into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
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())
gassara-kys marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
Copy link
Contributor Author

@senk8 senk8 Mar 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ここの書き方は他の部分では

	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return &empty.Empty{}, nil
		}
		return nil, err
	}

となっていますが、

  • このAPIは管理者に権限リクエストを送るもので、何らかのバグで送ることができなかった場合にはエラーをそのまま返してしまうのがよいと考えた

ので、errをそのまま返却するようにしました。

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&from=slack|ユーザー一覧>から%sさんを招待してください。`
iiiidaaa marked this conversation as resolved.
Show resolved Hide resolved
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&from=slack|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