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
- Authenticate to the API.
- 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.
- 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-Everywherescope is missing itsScopeId/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.