Flow Studio
Access Policy Model
Access is stored in the Process_ProcessAccess table — one row per (actor, workflow, role) triple. The ProcessAccessChecker reads this table to evaluate every access request.
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:
- Looks up all direct user-level policies for (userId, processId).
- Looks up all groups the user belongs to (via IAM service call, cached).
- Looks up all group-level policies for those groups against the processId.
- 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.