Portal Community

Database Table: Process_ProcessAccess

CREATE TABLE Process_ProcessAccess (
    Id          UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),
    TenantId    NVARCHAR(100)    NOT NULL,
    ProcessId   NVARCHAR(100)    NOT NULL,
    ActorId     NVARCHAR(100)    NOT NULL,   -- userId or groupId
    ActorType   TINYINT          NOT NULL,   -- 1=User, 2=Group
    Role        TINYINT          NOT NULL,   -- 1=Owner,2=Editor,3=Viewer,4=Executor
    GrantedBy   NVARCHAR(100)    NOT NULL,   -- userId who granted
    GrantedAt   DATETIMEOFFSET   NOT NULL DEFAULT GETUTCDATE(),
    RevokedAt   DATETIMEOFFSET   NULL,       -- soft delete
    CONSTRAINT UQ_ProcessAccess UNIQUE (TenantId, ProcessId, ActorId, ActorType)
);

ProcessAccessPolicy C# Model

public class ProcessAccessPolicy
{
    public string             Id        { get; set; }
    public string             TenantId  { get; set; }
    public string             ProcessId { get; set; }
    public string             ActorId   { get; set; }
    public ProcessActorType   ActorType { get; set; }
    public ProcessAccessRole  Role      { get; set; }
    public string             GrantedBy { get; set; }
    public DateTimeOffset     GrantedAt { get; set; }
    public DateTimeOffset?    RevokedAt { get; set; }
    public bool               IsActive => RevokedAt == null;
}

IProcessAccessChecker Interface

public interface IProcessAccessChecker
{
    Task<bool> CanViewAsync(string userId, string processId, CancellationToken ct);
    Task<bool> CanEditAsync(string userId, string processId, CancellationToken ct);
    Task<bool> CanExecuteAsync(string userId, string processId, CancellationToken ct);
    Task<bool> CanShareAsync(string userId, string processId, CancellationToken ct);
    Task<bool> CanDeleteAsync(string userId, string processId, CancellationToken ct);

    /// General-purpose check — use when the specific action isn't covered above
    Task<bool> CheckAsync(string userId, string processId,
        ProcessAccessRole requiredRole, CancellationToken ct);
}

How Group Access Is Resolved

When evaluating access, the checker:

  1. Looks up all direct user-level policies for (userId, processId).
  2. Looks up all groups the user belongs to (via IAM service call, cached).
  3. Looks up all group-level policies for those groups against the processId.
  4. Takes the highest role across all matches — a user with direct Viewer role but group Editor role gets Editor.
// Effective role resolution pseudocode
var directRole  = GetDirectRole(userId, processId);
var groupRoles  = GetGroupRoles(userId, processId);   // via IAM groups
var effectiveRole = Max(directRole, groupRoles.Max()); // highest wins
Revoked policies: Soft-deleted rows (RevokedAt is set) are excluded from all access checks. Revocation is immediate — the cache is invalidated on revoke.