We have identified a logic discrepancy between the Horde Dashboard (UI) and the Horde Backend (Horde.Server) regarding how JobAclAction.CreateJob is authorized. Our intention was to grant users view only permissions (ViewStream] and [ViewTemplate) in global config then selectively grant some users [CreateJob] permission for specific templates.
While the UI correctly evaluates effective permissions at the Template level - allowing a user to see and click the “Create Job” button if they have a Template-level override - the Backend API rejects the request with “User does not have permission for stream” error unless the user also has the permission at the Stream level. After investigation we found that root cause is this:
In JobsController.cs, the CreateJobAsync method performs a sequential check that requires a Stream-level grant before it ever evaluates the Template-level ACL. This early exit prevents the intended use case of granting a user the ability to run only specific templates while restricting them from the rest of the stream (see the code snippet below).
public async Task<ActionResult<CreateJobResponse>> CreateJobAsync([FromBody] CreateJobRequest create, CancellationToken cancellationToken = default)
{
StreamConfig? streamConfig;
if (!_buildConfig.Value.TryGetStream(create.StreamId, out streamConfig))
{
return NotFound(create.StreamId);
}
if (!streamConfig.Authorize(JobAclAction.CreateJob, User)) // is this necessary?
{
return Forbid(JobAclAction.CreateJob, streamConfig.Id);
}
if (create.RunAsScheduler == true && !_buildConfig.Value.Authorize(ServerAclAction.Debug, User))
{
return Forbid(ServerAclAction.Debug);
}
// Get the name of the template ref
TemplateId templateRefId = create.TemplateId;
// Augment the request with template properties
TemplateRefConfig? templateRefConfig;
if (!streamConfig.TryGetTemplate(templateRefId, out templateRefConfig))
{
return BadRequest($"Template {create.TemplateId} is not available for stream {streamConfig.Id}");
}
if (!templateRefConfig.Authorize(JobAclAction.CreateJob, User))
{
return Forbid(JobAclAction.CreateJob, streamConfig.Id);
}
We are therefore forced to grant [CreateJob] at the Stream level and then manually break inheritance on every other template to eliminate unauthorized access, which is difficult to maintain at scale.
To work around this for now we found that if we grant users CreateJob permission at the stream level then create a base template in which we rekove that permission and then extend this base for all templates and only grant the permission for the intended templates.
To summarize, intuitively you’d want:
- Stream: no [CreateJob]
- Template: grant [CreateJob] for specific jobs
But instead you have to:
- Stream: grant [CreateJob]
- Base template: revoke [CreateJob] (ACL override, no inheritance)
- Specific templates: re-grant [CreateJob]
It looks like removing the “if (!streamConfig.Authorize(JobAclAction.CreateJob, User))” block could achieve what we need but unsure if that condition is necessary for other purposes.
Can you get back to me with your thoughts on this? Is this intentional behaviour and we should use workarounds or is this an unintentional behaviour and we should attempt a fix in the Horde code?
Cheers, Daniel
[Attachment Removed]