Skip to content

Commit

Permalink
Notifications: Slack notification and SMTP Enhancement (#594)
Browse files Browse the repository at this point in the history
* Enable users with permissions to set up private slack channel for their projects/components and enable each user to set up direct slack notifications

Signed-off-by: Vanessa Fotso <[email protected]>

* Switch notifying users about error delivering slack notifications to logging the errors in the app logs

Signed-off-by: Vanessa Fotso <[email protected]>

* optimize user_mailer and associated views code

Signed-off-by: Vanessa Fotso <[email protected]>

* optimized the smtp notification workflow and updated to also notify users when membership updated or revoked

Signed-off-by: Vanessa Fotso <[email protected]>

* Refactored the slack workflow and updated to cover the review use case

Signed-off-by: Vanessa Fotso <[email protected]>

* Only project/component admins should be able to add/edit slackchannels

Signed-off-by: Vanessa Fotso <[email protected]>

* logic to determine which slack channel(s) should be notified

Signed-off-by: Vanessa Fotso <[email protected]>

* Setting default url for action mailer

Signed-off-by: Vanessa Fotso <[email protected]>

---------

Signed-off-by: Vanessa Fotso <[email protected]>
  • Loading branch information
vanessuniq authored Jun 24, 2023
1 parent f197e3e commit 2eec4d0
Show file tree
Hide file tree
Showing 29 changed files with 549 additions and 415 deletions.
69 changes: 58 additions & 11 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,18 @@ def authorize_viewer_component
raise(NotAuthorizedError, 'You are not authorized to perform viewer actions on this component')
end

def send_slack_notification(notification_type, object)
send_notification(Settings.slack.channel_id, slack_notification_params(notification_type, object))
def send_slack_notification(notification_type, object, *args)
channels = find_slack_channel(object, notification_type)
channels.each do |channel|
send_notification(channel, slack_notification_params(notification_type, object, *args))
end
end

def slack_notification_params(notification_type, object)
notification_type_prefix = notification_type.to_s.match(/^(assign|create|update|upload|rename|remove)/)[1]
def slack_notification_params(notification_type, object, *args)
pattern = /^(approve|revoke|request_changes|request_review|assign|create|update|upload|rename|remove)/
notification_type_prefix = notification_type.to_s.match(pattern)[1]
icon, header = get_slack_headers_icons(notification_type, notification_type_prefix)
fields = get_slack_notification_fields(object, notification_type, notification_type_prefix)
fields = get_slack_notification_fields(object, notification_type, notification_type_prefix, *args)
{
icon: icon,
header: header,
Expand All @@ -105,16 +109,59 @@ def slack_notification_params(notification_type, object)
end

def send_smtp_notification(mailer, action, *args)
mailer.request_review(*args).deliver_now if action == 'request_review'
mailer.approve_review(*args).deliver_now if action == 'approve'
mailer.revoke_review(*args).deliver_now if action == 'revoke_review_request'
mailer.request_review_changes(*args).deliver_now if action == 'request_changes'
mailer.welcome_project_member(*args).deliver_now if action == 'project_user'
mailer.welcome_component_member(*args).deliver_now if action == 'component_user'
mailer.membership_action(action, *args).deliver_now if membership_action?(action)
mailer.review_action(action, *args).deliver_now if review_action?(action)
end

private

# Determine the slack channel(s) and user id to which the slack notification should be sent.
def find_slack_channel(object, notification_type)
channels = []
# In all case except for review request, the general channel
# (default configured with the Vulcan instance) will be notified
channels << Settings.slack.channel_id unless object.is_a?(Rule)
# Usecase: requesting a review, revoking review request, approving or requesting changes on a control
case object
when Rule
# Getting the component or project slack channel
comp = object.component
channels << (comp.metadata&.dig('Slack Channel ID') || comp.project.metadata&.dig('Slack Channel ID'))
# Getting the slack user id of the user who initially requested the review
channels << latest_reviewer_slack_id(object) unless notification_type.to_s == 'request_review'
when Membership
# Usecase: updating project/component membership role
channels << object.user.slack_user_id
when User
# Usecase: updating Vulcan role (admin/user)
channels << object.slack_user_id
when Project
# Usecase: Project creation, removal, & renaming
channels << object.metadata&.dig('Slack Channel ID')
when Component
# Usecase: Component creation and removal
channels << (object.metadata&.dig('Slack Channel ID') || object.project.metadata&.dig('Slack Channel ID'))
end

channels.compact.uniq
end

def latest_reviewer_slack_id(rule)
latest_review = Review.where(
rule_id: Rule.find_by(rule_id: rule.rule_id.to_s, component_id: rule.component_id).id,
action: 'request_review'
).order(updated_at: :desc).first
latest_review&.user&.slack_user_id
end

def membership_action?(action)
%w[welcome_user update_membership remove_membership].include?(action)
end

def review_action?(action)
%w[request_review approve revoke_review_request request_changes].include?(action)
end

def helpful_errors(exception)
# Based on the accepted response type, either send a JSON response with the
# alert message, or redirect to home and display the alert.
Expand Down
17 changes: 15 additions & 2 deletions app/controllers/components_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ComponentsController < ApplicationController
before_action :authorize_admin_project, only: %i[create]
before_action :authorize_admin_component, only: %i[destroy]
before_action :authorize_author_component, only: %i[update]
before_action :check_permission_to_update_slackchannel, only: %i[update]
before_action :authorize_admin_component, only: %i[update], if: (lambda {
params
.require(:component)
Expand Down Expand Up @@ -71,6 +72,11 @@ def create
component.duplicate_reviews_and_history(component_create_params[:id])
component.create_rule_satisfactions if component_create_params[:file]
component.rules_count = component.rules.where(deleted_at: nil).size
if component_create_params[:slack_channel_id].present?
component.component_metadata_attributes = { data: {
'Slack Channel ID' => component_create_params[:slack_channel_id]
} }
end
component.save
send_slack_notification(:create_component, component) if Settings.slack.enabled
render json: { toast: 'Successfully added component to project.' }
Expand Down Expand Up @@ -227,12 +233,12 @@ def find
'LOWER(vuln_discussion) LIKE ? OR LOWER(mitigations) LIKE ?', "%#{find_param}%", "%#{find_param}%"
)
rules = rules.where(
'LOWER(title) LIKE ? OR
"LOWER(title) LIKE ? OR
LOWER(fixtext) LIKE ? OR
LOWER(vendor_comments) LIKE ? OR
LOWER(status_justification) LIKE ? OR
LOWER(artifact_description) LIKE ? OR
id IN (?) ', "%#{find_param}%", "%#{find_param}%", "%#{find_param}%", "%#{find_param}%",
id IN (?) ", "%#{find_param}%", "%#{find_param}%", "%#{find_param}%", "%#{find_param}%",
"%#{find_param}%", (checks.pluck(:base_rule_id) | descriptions.pluck(:base_rule_id))
)
.order(:rule_id)
Expand Down Expand Up @@ -344,6 +350,12 @@ def set_project
@project = Project.find(params[:project_id] || @component.project_id)
end

def check_permission_to_update_slackchannel
return if component_update_params[:component_metadata_attributes][:data]['Slack Channel ID'].blank?

authorize_admin_component
end

def component_update_params
params.require(:component).permit(
:released,
Expand Down Expand Up @@ -374,6 +386,7 @@ def component_create_params
:title,
:description,
:file,
:slack_channel_id,
file: {}
)
end
Expand Down
5 changes: 3 additions & 2 deletions app/controllers/memberships_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,11 @@ def create
membership = Membership.new(membership_create_params)
if membership.save
flash.notice = 'Successfully created membership.'
send_smtp_notification(UserMailer, 'welcome_user', current_user, membership) if Settings.smtp.enabled
case membership.membership_type
when 'Project'
send_smtp_notification(UserMailer, 'project_user', current_user, membership) if Settings.smtp.enabled
send_membership_notification(:create_project_membership, membership)
when 'Component'
send_smtp_notification(UserMailer, 'component_user', current_user, membership) if Settings.smtp.enabled
send_membership_notification(:create_component_membership, membership)
end
redirect_to membership.membership
Expand All @@ -46,6 +45,7 @@ def create
def update
if @membership.update(membership_update_params)
flash.notice = 'Successfully updated membership.'
send_smtp_notification(UserMailer, 'update_membership', current_user, @membership) if Settings.smtp.enabled
case @membership.membership_type
when 'Project'
send_membership_notification(:update_project_membership, @membership)
Expand All @@ -61,6 +61,7 @@ def update
def destroy
if @membership.destroy
flash.notice = 'Successfully removed membership.'
send_smtp_notification(UserMailer, 'remove_membership', current_user, @membership) if Settings.smtp.enabled
case @membership.membership_type
when 'Project'
send_membership_notification(:remove_project_membership, @membership)
Expand Down
10 changes: 9 additions & 1 deletion app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class ProjectsController < ApplicationController
before_action :authorize_viewer_project, only: %i[show]
before_action :authorize_logged_in, only: %i[index new search]
before_action :authorize_admin_or_create_permission_enabled, only: %i[create]
before_action :check_permission_to_update_slackchannel, only: %i[update]

def index
@projects = current_user.available_projects.eager_load(:memberships).alphabetical.as_json(methods: %i[memberships])
Expand Down Expand Up @@ -61,6 +62,9 @@ def create
name: new_project_params[:name],
memberships_attributes: [{ user: current_user, role: PROJECT_MEMBER_ADMINS }]
)
if new_project_params[:slack_channel_id].present?
project.project_metadata_attributes = { data: { 'Slack Channel ID' => new_project_params[:slack_channel_id] } }
end

# First save ensures base Project is acceptable.
if project.save
Expand Down Expand Up @@ -156,7 +160,7 @@ def set_project
end

def new_project_params
params.require(:project).permit(:name)
params.require(:project).permit(:name, :slack_channel_id)
end

def project_params
Expand All @@ -166,6 +170,10 @@ def project_params
)
end

def check_permission_to_update_slackchannel
authorize_admin_project if project_params[:project_metadata_attributes][:data]['Slack Channel ID'].present?
end

def project_name_changed?(current_project_name, project_params)
project_params['name'].present? && project_params['name'] != current_project_name
end
Expand Down
9 changes: 9 additions & 0 deletions app/controllers/reviews_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ def create
@rule
)
end

if Settings.slack.enabled
send_slack_notification(
review_params[:action].to_sym,
@rule,
review_params[:comment]
)
end

render json: { toast: 'Successfully added review.' }
else
render json: {
Expand Down
4 changes: 2 additions & 2 deletions app/controllers/users/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ def update_resource(resource, params)
end

def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:name])
devise_parameter_sanitizer.permit(:account_update, keys: [:name])
devise_parameter_sanitizer.permit(:sign_up, keys: %i[name slack_user_id])
devise_parameter_sanitizer.permit(:account_update, keys: %i[name slack_user_id])
end
end
end
Loading

0 comments on commit 2eec4d0

Please sign in to comment.