Grants (or denies) a permission to a role, restricted to one specific target server, folder, job, or step -- instead of fleet-wide. This is the one endpoint behind every scope: which column it writes is decided by the ScopeType you send, not by calling a different route per scope.

Having trouble reaching the API, or logging in from another machine? See Network Access & Authentication Security -- by default the Api only answers localhost, and PowerShell needs an extra flag once it does answer elsewhere.

Order of Operations

  1. Authenticate to the API.
  2. Get the role's id (from List Roles), the permission key (from List All Permission Keys), and the id of the server/folder/job/step you're scoping to.
  3. Create the scoped grant.

The calls

POST   /api/security/permission-grants
GET    /api/security/permission-grants
DELETE /api/security/permission-grants/{grantId}

Permission needed: Security.Manage for all three.

C# example

var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web);

// Step 1: Authenticate to the API.
using var handler = new HttpClientHandler { UseDefaultCredentials = true };
using var client = new HttpClient(handler) { BaseAddress = new Uri("http://your-minion-agent-server:5443") };
client.DefaultRequestHeaders.Add("X-App-Name", "MyIntegration");

// Step 2: You already have the role's id, permission key, and the job's id to scope to.
var roleId = 9;
var jobId = 214;

// Step 3: Create the scoped grant -- this role can Job.Run only job 214, not every job.
var newGrant = new CreatePermissionGrantRequest(AppRoleId: roleId, PermissionKey: "Job.Run", ScopeType: "Job", ScopeId: jobId, IsDeny: false);
var response = await client.PostAsJsonAsync("/api/security/permission-grants", newGrant, jsonOptions);
response.EnsureSuccessStatusCode();
var created = await response.Content.ReadFromJsonAsync<CreatedId>(jsonOptions);
Console.WriteLine($"Created grant id: {created!.PermissionGrantID}");

// Look up every grant on that job.
var grants = await client.GetFromJsonAsync<PermissionGrantSummary[]>($"/api/security/permission-grants?scopeType=Job&scopeId={jobId}", jsonOptions);
foreach (var grant in grants!)
{
    Console.WriteLine($"{grant.PermissionGrantId}: {grant.RoleName} -- {grant.PermissionKey} on {grant.ScopeType} '{grant.ScopeName}', deny = {grant.IsDeny}");
}

// Revoke it later.
var deleteResponse = await client.DeleteAsync($"/api/security/permission-grants/{created.PermissionGrantID}");
deleteResponse.EnsureSuccessStatusCode();

record CreatePermissionGrantRequest(int AppRoleId, string PermissionKey, string ScopeType, int? ScopeId, bool IsDeny);
record CreatedId(int PermissionGrantID);
record PermissionGrantSummary(int PermissionGrantId, int AppRoleId, string RoleName, string PermissionKey, string ScopeType, int? ScopeId, string? ScopeName, bool IsDeny);

Not on a domain machine? Swap in the app-account login from Calling the API From Your Own Code.

PowerShell example

# Step 1: Authenticate to the API.
$headers = @{ "X-App-Name" = "MyIntegration" }

# Step 2: You already have the role's id, permission key, and the job's id to scope to.
$roleId = 9
$jobId = 214

# Step 3: Create the scoped grant. (-AllowUnencryptedAuthentication: PowerShell requires this for Windows auth over plain http to anything but localhost -- see "Network Access & Authentication Security".)
$newGrant = @{ AppRoleId = $roleId; PermissionKey = "Job.Run"; ScopeType = "Job"; ScopeId = $jobId; IsDeny = $false } | ConvertTo-Json
$created = Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/permission-grants" `
    -Method Post -Body $newGrant -ContentType "application/json" -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
"Created grant id: $($created.permissionGrantID)"

# Look up every grant on that job.
$grants = Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/permission-grants?scopeType=Job&scopeId=$jobId" -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers
$grants | ForEach-Object { "$($_.permissionGrantId): $($_.roleName) -- $($_.permissionKey) on $($_.scopeType) '$($_.scopeName)', deny = $($_.isDeny)" }

# Revoke it later.
Invoke-RestMethod -Uri "http://your-minion-agent-server:5443/api/security/permission-grants/$($created.permissionGrantID)" `
    -Method Delete -UseDefaultCredentials -AllowUnencryptedAuthentication -Headers $headers

ScopeType values

Everywhere, Server, Folder, Job, or Step. ScopeId is required for all but Everywhere -- it's the target server's, folder's, job's, or step's id respectively. (For a fleet-wide grant you can use this same endpoint with ScopeType: "Everywhere", or the narrower Set a Role's Fleet-Wide Permissions call -- both write the same place.)

What you get back

POST -- 201 Created:

{ "permissionGrantID": 55 }

GET -- 200 OK, an array (filterable by roleId, scopeType, scopeId query params, all optional):

[
    { "permissionGrantId": 55, "appRoleId": 9, "roleName": "Report Viewers", "permissionKey": "Job.Run", "scopeType": "Job", "scopeId": 214, "scopeName": "Nightly ETL", "isDeny": false }
]

DELETE -- 204 No Content -- no body.

Codes this call can return

See API Response Codes for what each one means in general. For this specific call:

  • 201 -- grant created.
  • 200 -- the list.
  • 204 -- grant revoked.
  • 400 -- unknown ScopeType/scopeType, or a non-Everywhere scope is missing its ScopeId/scopeId.
  • 403 -- you don't have Security.Manage.
  • 404 -- (DELETE) no grant exists with that id.

What gets audited

Create is recorded as PermissionGrant / Grant Permission with the full request in the after-state; delete as PermissionGrant / Revoke Permission with the grant's resolved label (role + permission + scope) in the before-state. The GET isn't audited (it's a read).

See also: List Roles, List All Permission Keys, Set a Role's Fleet-Wide Permissions, Check Effective Permissions.