Skip to content

Commit

Permalink
New Feature: Enable setting up Project visibility and Requesting acce…
Browse files Browse the repository at this point in the history
…ss to a project (#595)

* Adding ability to setup project visibility on project creation, ability for user to request access or cancel request, ability for admins to accept/reject access requests

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

* Notifications: Slack notification and SMTP Enhancement (#594)

* 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]>

* SMTP notifications when access request denied

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

* changed position of sortable icon

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

* Enabled smtp notification when requesting access to project

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

* Enable admins to update project's visibility, send slack notification for visibility changed, use badge to convey info

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

* Adding filter to display only discoverable projects on the list

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

* Project access request spec

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

* fixing spec

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

* Updated project's page filter and added request notification

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

* fix bug from merge

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

* Fix the filter for Show my projects to show all projects I am a member of regardless of wether it is discoverable or not

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

* Temp fix for new chrome update

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

---------

Signed-off-by: Vanessa Fotso <[email protected]>
  • Loading branch information
vanessuniq authored Aug 7, 2023
1 parent 1043403 commit a3c1abe
Show file tree
Hide file tree
Showing 29 changed files with 687 additions and 51 deletions.
10 changes: 5 additions & 5 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,6 @@ GEM
concurrent-ruby (~> 1.0)
chef-utils (17.9.46)
concurrent-ruby
childprocess (4.1.0)
coderay (1.1.3)
concurrent-ruby (1.2.2)
crass (1.0.6)
Expand Down Expand Up @@ -345,7 +344,7 @@ GEM
responders (3.0.1)
actionpack (>= 5.0)
railties (>= 5.0)
rexml (3.2.5)
rexml (3.2.6)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
Expand Down Expand Up @@ -398,10 +397,10 @@ GEM
ruh-roo (3.0.1)
nokogiri (~> 1)
rubyzip (>= 1.3.0, < 3.0.0)
selenium-webdriver (4.1.0)
childprocess (>= 0.5, < 5.0)
selenium-webdriver (4.9.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
semantic_range (3.0.0)
semverse (3.0.0)
settingslogic (2.0.9)
Expand Down Expand Up @@ -504,6 +503,7 @@ GEM
rack-proxy (>= 0.6.1)
railties (>= 5.2)
semantic_range (>= 2.3.0)
websocket (1.2.9)
websocket-driver (0.7.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
Expand Down
35 changes: 34 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class ApplicationController < ActionController::Base
include SlackNotificationsHelper

before_action :setup_navigation, :authenticate_user!
before_action :check_access_request_notifications

rescue_from NotAuthorizedError, with: :not_authorized

Expand Down Expand Up @@ -97,7 +98,20 @@ def send_slack_notification(notification_type, object, *args)
end

def slack_notification_params(notification_type, object, *args)
pattern = /^(approve|revoke|request_changes|request_review|assign|create|update|upload|rename|remove)/
pattern = /^(
approve|
revoke|
request_changes|
request_review|
assign|
create|
update|
upload|
rename|
remove|
change_visibility
)/x

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, *args)
Expand All @@ -111,6 +125,7 @@ def slack_notification_params(notification_type, object, *args)
def send_smtp_notification(mailer, action, *args)
mailer.membership_action(action, *args).deliver_now if membership_action?(action)
mailer.review_action(action, *args).deliver_now if review_action?(action)
mailer.project_access_action(action, *args).deliver_now if access_request_action?(action)
end

private
Expand Down Expand Up @@ -162,6 +177,10 @@ def review_action?(action)
%w[request_review approve revoke_review_request request_changes].include?(action)
end

def access_request_action?(action)
%w[request_access reject_access].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 Expand Up @@ -211,4 +230,18 @@ def setup_navigation
@navigation = []
@navigation += helpers.base_navigation if current_user
end

def check_access_request_notifications
@access_requests = []
return @access_requests unless user_signed_in?

# iterate over the user's projects and check if they are admin
# if they are admin on a project, retrieve the access requests if any
current_user.available_projects.each do |project|
if current_user.can_admin_project?(project)
@access_requests << project.access_requests.eager_load(:user, :project).as_json(methods: %i[user project])
end
end
@access_requests.flatten!
end
end
13 changes: 11 additions & 2 deletions app/controllers/memberships_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ def create
)
end

membership = Membership.new(membership_create_params)
filtered_params = membership_create_params.except(:access_request_id)
membership = Membership.new(filtered_params)
if membership.save
if membership_create_params[:access_request_id].present?
delete_access_request(membership_create_params[:access_request_id])
end
flash.notice = 'Successfully created membership.'
send_smtp_notification(UserMailer, 'welcome_user', current_user, membership) if Settings.smtp.enabled
case membership.membership_type
Expand Down Expand Up @@ -91,7 +95,7 @@ def authorize_admin_membership
end

def membership_create_params
params.require(:membership).permit(:user_id, :role, :membership_id, :membership_type)
params.require(:membership).permit(:user_id, :role, :membership_id, :membership_type, :access_request_id)
end

def membership_update_params
Expand All @@ -103,4 +107,9 @@ def send_membership_notification(notification_type, membership)

send_slack_notification(notification_type, membership)
end

def delete_access_request(access_request_id)
access_request = ProjectAccessRequest.find_by(id: access_request_id)
access_request&.destroy
end
end
39 changes: 39 additions & 0 deletions app/controllers/project_access_requests_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

##
# Controller for managing request to access a specific project.
#
class ProjectAccessRequestsController < ApplicationController
def create
@project = Project.find(params[:project_id])
@access_request = ProjectAccessRequest.new(user: current_user, project: @project)

if @access_request.save
flash.notice = 'Your request for access has been sent.'
if Settings.smtp.enabled
send_smtp_notification(UserMailer, 'request_access', @access_request.user, @access_request.project)
end
else
flash.alert = @access_request.errors.full_messages.to_sentence
end
redirect_to root_path
end

def destroy
@access_request = ProjectAccessRequest.find(params[:id])
if @access_request.destroy
if current_user.can_admin_project?(@access_request.project)
if Settings.smtp.enabled
send_smtp_notification(UserMailer, 'reject_access', @access_request.user, @access_request.project)
end
flash.notice = "Sucessfully denied #{@access_request.user.name}'s request to access project."
else
flash.notice = "Your request to access #{@access_request.project.name} has been cancelled."
end
else
flash.alert = @access_request.errors.full_messages.to_sentence
end

redirect_back(fallback_location: root_path)
end
end
45 changes: 30 additions & 15 deletions app/controllers/projects_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,18 @@ 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]
before_action :check_permission_to_update, only: %i[update]

def index
@projects = current_user.available_projects.eager_load(:memberships).alphabetical.as_json(methods: %i[memberships])
@projects.each do |project|
project['admin'] = project['memberships'].any? do |m|
m['role'] == PROJECT_MEMBER_ADMINS && m['user_id'] == current_user.id
end
project['is_member'] = project['memberships'].any? do |m|
m['user_id'] == current_user.id
end || current_user.admin
project['access_request_id'] = current_user.access_requests.find_by(project_id: project['id'])&.id
end
respond_to do |format|
format.html
Expand Down Expand Up @@ -47,7 +51,8 @@ def show
# projects that a user has permissions to access
@project.current_user = current_user
@project_json = @project.to_json(
methods: %i[histories memberships metadata components available_components available_members details users]
methods: %i[histories memberships metadata components available_components available_members details users
access_requests]
)
respond_to do |format|
format.html
Expand All @@ -60,7 +65,9 @@ def new; end
def create
project = Project.new(
name: new_project_params[:name],
memberships_attributes: [{ user: current_user, role: PROJECT_MEMBER_ADMINS }]
description: new_project_params[:description],
memberships_attributes: [{ user: current_user, role: PROJECT_MEMBER_ADMINS }],
visibility: new_project_params[:visibility]
)
if new_project_params[:slack_channel_id].present?
project.project_metadata_attributes = { data: { 'Slack Channel ID' => new_project_params[:slack_channel_id] } }
Expand All @@ -78,14 +85,16 @@ def create

# Update project and response with json
def update
current_project_name = @project.name
notification_types = []
notification_types << :rename_project if project_name_changed?(@project.name)
notification_types << :change_visibility if project_visibility_changed?(@project.visibility)
if @project.update(project_params)
if project_name_changed?(current_project_name, project_params)
render json: { toast: 'Project renamed successfully' }
send_slack_notification(:rename_project, @project) if Settings.slack.enabled
else
render json: { toast: 'Project updated successfully' }
if Settings.slack.enabled
notification_types.each do |type|
send_slack_notification(type, @project)
end
end
render json: { toast: 'Successfully updated project' }
else
render json: {
toast: {
Expand Down Expand Up @@ -160,23 +169,29 @@ def set_project
end

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

def project_params
params.require(:project).permit(
:name,
:description,
:visibility,
project_metadata_attributes: { data: {} }
)
end

def check_permission_to_update_slackchannel
return if project_params[:project_metadata_attributes]&.dig('data')&.dig('Slack Channel ID').blank?

authorize_admin_project
def check_permission_to_update
condition = (project_params[:project_metadata_attributes]&.dig('data')&.dig('Slack Channel ID').present? ||
project_params[:visibility].present?)
authorize_admin_project if condition
end

def project_name_changed?(current_project_name, project_params)
def project_name_changed?(current_project_name)
project_params['name'].present? && project_params['name'] != current_project_name
end

def project_visibility_changed?(current_project_visibility)
project_params[:visibility].present? && project_params[:visibility] != current_project_visibility
end
end
14 changes: 12 additions & 2 deletions app/helpers/notification_fields_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ def app_notification_field
def project_notification_fields(notif_prefix, project)
{
generate_project_label: {
label: generate_proj_comp_action_label(notif_prefix, 'Project'),
label: if notif_prefix == 'change_visibility'
'Project'
else
generate_proj_comp_action_label(notif_prefix,
'Project')
end,
value: notif_prefix == 'remove' ? project.name : generate_proj_or_comp_value(project, Project)
},
generate_initiated_by_label: {
label: generate_proj_comp_action_label(notif_prefix, 'By'),
value: "#{current_user.name} (#{current_user.email})"
},
generate_visibility_label: {
label: 'New Visibility',
value: "#{project.visibility} "
}
}.freeze
end
Expand Down Expand Up @@ -166,7 +175,8 @@ def generate_proj_comp_action_label(notification_type_prefix, action)
labels_hash = {
'create' => "Created #{action}",
'remove' => "Removed #{action}",
'rename' => "Renamed #{action}"
'rename' => "Renamed #{action}",
'change_visibility' => "Changed #{action}"
}
labels_hash[notification_type_prefix]
end
Expand Down
12 changes: 7 additions & 5 deletions app/helpers/slack_notification_fields_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,13 @@ def get_slack_notification_fields(object, notification_type, notification_type_p
]
when Project
notification_fields = project_notification_fields(notification_type_prefix, object)
fields = [
app_notification_field,
notification_fields[:generate_project_label],
notification_fields[:generate_initiated_by_label]
]
if notification_type_prefix == 'change_visibility'
fields = [
app_notification_field,
notification_fields[:generate_project_label],
notification_fields[:generate_initiated_by_label]
] << notification_fields[:generate_visibility_label]
end
when SecurityRequirementsGuide
notification_fields = srg_notification_fields(notification_type_prefix, object)
fields = [
Expand Down
3 changes: 2 additions & 1 deletion app/helpers/slack_notifications_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ def send_notification(channel, message_params)
def get_slack_headers_icons(notification_type, notification_type_prefix)
icon = case notification_type_prefix
when 'assign', 'upload', 'create', 'approve' then ':white_check_mark:'
when 'rename', 'update', 'request_review' then ':loudspeaker:'
when 'rename', 'update', 'request_review', 'change_visibility' then ':loudspeaker:'
when 'remove', 'revoke', 'request_changes' then ':x:'
end
header_map = {
create_component: 'Vulcan New Component Creation',
remove_component: 'Vulcan Component Removal',
create_project: 'Vulcan New Project Creation',
rename_project: 'Vulcan Project Renaming',
change_visibility: 'Vulcan Project Visibility Change',
remove_project: 'Vulcan Project Removal',
create_project_membership: 'New Members Added to the Project',
update_project_membership: 'Membership Updated on the Project',
Expand Down
Loading

0 comments on commit a3c1abe

Please sign in to comment.