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
22 changes: 19 additions & 3 deletions RockWeb/App_Code/SeccConnectGateHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,22 @@ public static bool IsPersonAuthorizedToConnect( Person currentPerson, Guid? safe
/// A null request is allowed (board add mode) so modal rendering isn't blocked.
/// </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 );
}

/// <summary>
/// Same gate, evaluated for an arbitrary status instead of a request. Lets callers ask
/// "could the person connect a request at this status?" (e.g. the board card action menu,
/// which must decide per status column). A null status means there is no request in context,
/// which is allowed (board add mode) so modal rendering isn't blocked.
/// </summary>
public static bool CanConnect( int? connectionStatusId, ConnectionState? connectionState, ConnectionOpportunity opportunity, Person currentPerson, Guid? safetySecurityRoleGuid )
{
if ( opportunity == null )
{
Expand Down Expand Up @@ -123,13 +139,13 @@ public static bool CanConnect( ConnectionRequest connectionRequest, ConnectionOp
return true;
}

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

return connectableStatuses.Contains( connectionRequest.ConnectionStatusId )
|| connectionRequest.ConnectionState == ConnectionState.Connected;
return connectableStatuses.Contains( connectionStatusId.Value )
|| connectionState == ConnectionState.Connected;
}
}
}
105 changes: 99 additions & 6 deletions RockWeb/Blocks/Connection/ConnectionRequestBoard.ascx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2866,13 +2866,102 @@ private bool CanUserEditConnectionRequest()
/// </summary>
private bool CanUserConnect()
{
var connectionRequest = GetConnectionRequest();

return SeccConnectGateHelper.CanConnect(
GetConnectionRequest(),
GetConnectionOpportunity(),
connectionRequest,
GetGateConnectionOpportunity( connectionRequest ),
Comment on lines +2872 to +2873
CurrentPerson,
GetAttributeValue( AttributeKey.SafetySecurityRole ).AsGuidOrNull() );
}

/// <summary>
/// SECC (ROCK-8640): Returns the opportunity whose connect rules apply to the given request.
/// The request identifier arrives from the client, so it can belong to an opportunity other than the
/// one currently selected on the board. The gate has to read SecurityToConnect and ConnectableStatuses
/// from the request's own opportunity - which is what ConnectionRequestDetail does - otherwise a user
/// on an opportunity that does not require security could connect a request in one that does.
/// Falls back to the selected opportunity when there is no request in context (modal add mode), which
/// is the opportunity the new request will be created in.
/// </summary>
/// <param name="connectionRequest">The connection request, or null in modal add mode.</param>
private ConnectionOpportunity GetGateConnectionOpportunity( ConnectionRequest connectionRequest )
{
var selectedConnectionOpportunity = GetConnectionOpportunity();

if ( connectionRequest == null )
{
return selectedConnectionOpportunity;
}

// Reuse the already loaded instance when it is the right one, which is the normal case.
if ( selectedConnectionOpportunity != null
&& selectedConnectionOpportunity.Id == connectionRequest.ConnectionOpportunityId )
{
return selectedConnectionOpportunity;
}

return new ConnectionOpportunityService( new RockContext() )
.Queryable()
.AsNoTracking()
.FirstOrDefault( co => co.Id == connectionRequest.ConnectionOpportunityId );
Comment on lines +2904 to +2907
}

/// <summary>
/// SECC (ROCK-8640): Runs the shared gate against every status on the selected opportunity, so the
/// board card action menu can hide its Connect item using exactly the same rule that hides the
/// Connect button on the request modal. Presentation only - the card menu's postback is enforced
/// separately in ProcessJavaScriptCommand.
/// </summary>
private List<int> GetUserConnectableStatusIds()
{
var statusIds = new List<int>();

/*
The modal's Connect button also requires edit rights, so apply the same check here.

Note that no request is in context at bind time, so when the connection type has
EnableRequestSecurity turned on this evaluates opportunity-level Edit rather than the
per-request Edit the modal evaluates, and the card menu can show Connect for a request
the modal would hide. The card menu's postback is still evaluated per-request in
ProcessJavaScriptCommand, so this is a presentation difference only. Matching the modal
exactly here would require evaluating the gate per request instead of per status.
*/
if ( !CanUserEditConnectionRequest() )
{
return statusIds;
}

var connectionOpportunity = GetConnectionOpportunity();

if ( connectionOpportunity == null
|| connectionOpportunity.ConnectionType == null
|| connectionOpportunity.ConnectionType.ConnectionStatuses == null )
{
return statusIds;
}

var safetySecurityRoleGuid = GetAttributeValue( AttributeKey.SafetySecurityRole ).AsGuidOrNull();

// Ordered so the same board state always produces the same list. The client stores these in its
// options object and re-renders the board when that object changes.
foreach ( var connectionStatus in connectionOpportunity.ConnectionType.ConnectionStatuses.OrderBy( cs => cs.Id ) )
{
/*
Each status is evaluated as Active. State only affects the gate when it is Connected,
and the client applies this list only to cards whose core CanConnect is already true --
which is false for both Connected and Inactive requests -- so the state passed here
cannot change the outcome.
*/
if ( SeccConnectGateHelper.CanConnect( connectionStatus.Id, ConnectionState.Active, connectionOpportunity, CurrentPerson, safetySecurityRoleGuid ) )
{
statusIds.Add( connectionStatus.Id );
}
}

return statusIds;
}

/// <summary>
/// Binds the modal activities grid.
/// </summary>
Expand Down Expand Up @@ -5744,7 +5833,8 @@ private void RefreshRequestCard()
statusIds: {8},
connectionStates: {9},
campusId: {10},
pastDueOnly: {11}
pastDueOnly: {11},
userConnectableStatusIds: {12}
}});",
ToJavaScript( ConnectionRequestId ), // 0
ToJavaScript( whitespaceRemovedTemplate ), // 1
Expand All @@ -5757,7 +5847,8 @@ private void RefreshRequestCard()
ToJavaScript( cblStatusFilter.SelectedValuesAsInt ), // 8
ToJavaScript( cblStateFilter.SelectedValues ), // 9
ToJavaScript( CampusId ), // 10
ToJavaScript( rcbPastDueOnly.Checked ) /* 11 */ );
ToJavaScript( rcbPastDueOnly.Checked ), // 11
ToJavaScript( GetUserConnectableStatusIds() ) /* 12 */ );

ScriptManager.RegisterStartupScript(
upnlJavaScript,
Expand Down Expand Up @@ -5793,7 +5884,8 @@ private void BindBoard()
lastActivityTypeIds: {11},
controlClientId: {12},
pastDueOnly: {13},
connectionRequestId: {14}
connectionRequestId: {14},
userConnectableStatusIds: {15}
}});",
ToJavaScript( ConnectionOpportunityId ), // 0
ToJavaScript( GetMaxCardsPerColumn() ), // 1
Expand All @@ -5809,7 +5901,8 @@ private void BindBoard()
ToJavaScript( cblLastActivityFilter.SelectedValuesAsInt ), // 11
ToJavaScript( lbJavaScriptCommand.ClientID ), // 12
ToJavaScript( rcbPastDueOnly.Checked ), //13
ToJavaScript( ConnectionRequestId ) /* 14 */ );
ToJavaScript( ConnectionRequestId ), // 14
ToJavaScript( GetUserConnectableStatusIds() ) /* 15 */ );

ScriptManager.RegisterStartupScript(
upnlJavaScript,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,34 @@
};
let _cardTemplate = '';

// ROCK-8640: the card action menu's Connect item is driven by the core CanConnect value, which knows
// nothing about the SECC Safety & Security gate. The server evaluates that gate (the same method that
// hides the Connect button on the request modal) for every status on the opportunity and sends the
// allowed status ids here, so this only has to check membership - no gate logic lives in JavaScript.
let _userConnectableStatusIds = null;

const captureConnectGate = function (options) {
if (options && options.userConnectableStatusIds) {
_userConnectableStatusIds = options.userConnectableStatusIds;
}
};

const applyConnectGate = function (viewModel) {
if (!viewModel || viewModel.CanConnect !== true || !_userConnectableStatusIds) {
return viewModel;
}

if (_userConnectableStatusIds.indexOf(viewModel.StatusId) !== -1) {
return viewModel;
}

const gated = $.extend({}, viewModel);
gated.CanConnect = false;
return gated;
};

const getCardHtml = function (requestViewModel) {
return resolveTemplateFields(getCardTemplate(), requestViewModel);
return resolveTemplateFields(getCardTemplate(), applyConnectGate(requestViewModel));
};

const getSentryTemplate = function () {
Expand Down Expand Up @@ -249,6 +275,8 @@
return
}

captureConnectGate(options);

fetchRequestViewModel(options, function (requestViewModel) {
refreshCard(options.connectionRequestId, requestViewModel);
});
Expand Down Expand Up @@ -644,6 +672,10 @@
throw 'A valid options object is required';
}

// ROCK-8640: capture before the early return below, so the gate is applied even when the board
// itself does not need re-rendering.
captureConnectGate(options);

// Check if this options object has already been initialized (no need to do it again)
const newOptionsHash = JSON.stringify(options);
const areColsRendered = $('.js-column-container > *').length > 0;
Expand Down