Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
81 changes: 64 additions & 17 deletions RockWeb/App_Code/SeccConnectGateHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// </copyright>
//
using System;
using System.Collections.Generic;
using System.Linq;

using Rock;
Expand Down Expand Up @@ -95,12 +96,7 @@ public static bool IsPersonAuthorizedToConnect( Person currentPerson, Guid? safe
/// </summary>
public static bool CanConnect( ConnectionRequest connectionRequest, ConnectionOpportunity opportunity, Person currentPerson, Guid? safetySecurityRoleGuid )
{
if ( connectionRequest == null )
{
return CanConnect( ( int? ) null, null, opportunity, currentPerson, safetySecurityRoleGuid );
}

return CanConnect( connectionRequest.ConnectionStatusId, connectionRequest.ConnectionState, opportunity, currentPerson, safetySecurityRoleGuid );
return CanConnect( connectionRequest?.ConnectionStatusId, connectionRequest?.ConnectionState, opportunity, currentPerson, safetySecurityRoleGuid );
}

/// <summary>
Expand All @@ -111,12 +107,68 @@ public static bool CanConnect( ConnectionRequest connectionRequest, ConnectionOp
/// </summary>
public static bool CanConnect( int? connectionStatusId, ConnectionState? connectionState, ConnectionOpportunity opportunity, Person currentPerson, Guid? safetySecurityRoleGuid )
{
List<int> connectableStatuses;
if ( !TryGetStatusRestriction( opportunity, currentPerson, safetySecurityRoleGuid, out connectableStatuses ) )
{
return false;
}

if ( connectableStatuses == null || !connectionStatusId.HasValue )
{
return true;
}

return connectableStatuses.Contains( connectionStatusId.Value )
|| connectionState == ConnectionState.Connected;
}

/// <summary>
/// Returns the subset of <paramref name="connectionStatusIds"/> at which the person could connect a
/// (not yet connected) request on the opportunity. Evaluates the opportunity-level half of the gate
/// once instead of once per status, which is what the board needs when it builds its card action menu.
/// Fails closed: returns an empty list if the opportunity cannot be resolved.
/// </summary>
public static List<int> GetConnectableStatusIds( IEnumerable<int> connectionStatusIds, ConnectionOpportunity opportunity, Person currentPerson, Guid? safetySecurityRoleGuid )
{
var statusIds = new List<int>();

if ( connectionStatusIds == null )
{
return statusIds;
}

List<int> connectableStatuses;
if ( !TryGetStatusRestriction( opportunity, currentPerson, safetySecurityRoleGuid, out connectableStatuses ) )
{
return statusIds;
}

statusIds.AddRange( connectableStatuses == null
? connectionStatusIds
: connectionStatusIds.Where( connectableStatuses.Contains ) );

return statusIds;
}

/// <summary>
/// Evaluates the status-independent half of the gate. Returns false when the person may not connect
/// anything on the opportunity. Returns true otherwise, with <paramref name="connectableStatuses"/>
/// set to the status ids the opportunity restricts connecting to, or null when there is no restriction.
/// </summary>
private static bool TryGetStatusRestriction( ConnectionOpportunity opportunity, Person currentPerson, Guid? safetySecurityRoleGuid, out List<int> connectableStatuses )
{
connectableStatuses = null;

if ( opportunity == null )
{
return false;
}

opportunity.LoadAttributes();
if ( opportunity.Attributes == null )
{
opportunity.LoadAttributes();
}

var requiresSecurityToConnect = GetRequiresSecurityToConnect( opportunity );

if ( !requiresSecurityToConnect.HasValue || !requiresSecurityToConnect.Value )
Expand All @@ -129,23 +181,18 @@ public static bool CanConnect( int? connectionStatusId, ConnectionState? connect
return false;
}

var connectableStatuses = opportunity.GetAttributeValue( "ConnectableStatuses" ).SplitDelimitedValues()
var statuses = opportunity.GetAttributeValue( "ConnectableStatuses" ).SplitDelimitedValues()
.Select( v => v.AsIntegerOrNull() )
.Where( v => v.HasValue )
.Select( v => v.Value )
.ToList();

if ( connectableStatuses.Count == 0 )
{
return true;
}

if ( !connectionStatusId.HasValue )
if ( statuses.Count > 0 )
{
return true;
connectableStatuses = statuses;
}

return connectableStatuses.Contains( connectionStatusId.Value )
|| connectionState == ConnectionState.Connected;
return true;
}
}
}
49 changes: 49 additions & 0 deletions RockWeb/Blocks/Connection/BulkUpdateRequests.ascx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ namespace RockWeb.Blocks.Connection
Order = 1,
DefaultValue = Rock.SystemGuid.Page.CONNECTIONS_BOARD )]

[SecurityRoleField(
"Safety & Security Role",
Description = "Members of this security role (plus Rock Administrators) can bulk update requests into the Connected state on opportunities that require security to connect. If an opportunity requires security to connect and no role is set here, only Rock Administrators can.",
IsRequired = false,
Order = 2,
Key = AttributeKeys.SafetySecurityRole )]

#endregion

[Rock.SystemGuid.BlockTypeGuid( "175158F8-F10E-476F-809E-A76825E0AC5D" )]
Expand All @@ -61,6 +68,7 @@ public partial class BulkUpdateRequests : RockBlock
private static class AttributeKeys
{
public const string PreviousPage = "PreviousPage";
public const string SafetySecurityRole = "SafetySecurityRole";
}

#endregion AttributeKeys
Expand Down Expand Up @@ -403,6 +411,47 @@ protected void btnConfirm_Click( object sender, EventArgs e )
.Where( cr => RequestIdsState.Contains( cr.Id ) && ( ( includeNoCampus && !cr.CampusId.HasValue ) || selectedCampusIds.Contains( cr.CampusId.Value ) ) )
.ToList();

// SECC (ROCK-9044): nothing has been modified yet, so returning from either guard below persists nothing.
if ( connectionOpportunity == null )
{
mdConfirmUpdateRequests.Hide();
ShowNotification( NotificationBoxType.Danger, "The selected connection opportunity could not be found. No requests were updated." );
return;
}

// SECC (ROCK-9044): the State list offers Connected, which makes this the one board path that can move
// requests into Connected without the Safety & Security connect gate the board and detail blocks enforce.
// Apply the same gate here, against the opportunity and status the requests are being moved to. A request
// enters Connected on the target opportunity when it ends up Connected (State set to Connected, or left
// unchanged on an already Connected request) and was not already Connected on that opportunity. Each one
// is gated as a new connect (no state passed), so the target's ConnectableStatuses always apply.
var targetState = ddlState.SelectedValueAsEnumOrNull<ConnectionState>();
var targetOpportunityId = connectionOpportunity.Id;
var targetStatusId = ddlStatus.SelectedValue.AsIntegerOrNull();
var safetySecurityRoleGuid = GetAttributeValue( AttributeKeys.SafetySecurityRole ).AsGuidOrNull();

var blockedCount = connectionRequests.Count( cr =>
( targetState ?? cr.ConnectionState ) == ConnectionState.Connected
&& !( cr.ConnectionState == ConnectionState.Connected && cr.ConnectionOpportunityId == targetOpportunityId )
&& !SeccConnectGateHelper.CanConnect(
targetStatusId ?? cr.ConnectionStatusId,
null,
connectionOpportunity,
CurrentPerson,
safetySecurityRoleGuid ) );

if ( blockedCount > 0 )
{
mdConfirmUpdateRequests.Hide();
ShowNotification(
NotificationBoxType.Danger,
string.Format(
"You are not authorized to connect {0} of the selected connection requests on {1}. No requests were updated.",
blockedCount.ToString( "N0" ),
connectionOpportunity.Name.EncodeHtml() ) );
return;
}

foreach ( var connectionRequest in connectionRequests )
{
connectionRequest.ConnectionOpportunityId = ddlOpportunity.SelectedValue.AsInteger();
Expand Down
Loading