Skip to content

Commit

Permalink
Rework permission rules schema
Browse files Browse the repository at this point in the history
  • Loading branch information
ezzatron committed Dec 12, 2024
1 parent 2a09de1 commit e196604
Show file tree
Hide file tree
Showing 17 changed files with 2,176 additions and 447 deletions.
76 changes: 61 additions & 15 deletions dist/main.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions dist/main.js.map

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/account.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export function normalizeAccountPattern(
definingAccount: string,
pattern: string,
): string {
return pattern === "." ? definingAccount : pattern;
}
11 changes: 7 additions & 4 deletions src/config/provider-config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { load } from "js-yaml";
import { normalizeAccountPattern } from "../account.js";
import { normalizeGitHubPattern } from "../github-pattern.js";
import type { ProviderConfig } from "../type/provider-config.js";
import { validateProvider } from "./validation.js";
Expand Down Expand Up @@ -34,10 +35,12 @@ function normalizeProviderConfig(
const rule = config.permissions.rules[i];

for (let j = 0; j < rule.resources.length; ++j) {
rule.resources[j] = normalizeGitHubPattern(
definingAccount,
rule.resources[j],
);
for (let k = 0; k < rule.resources[j].accounts.length; ++k) {
rule.resources[j].accounts[k] = normalizeAccountPattern(
definingAccount,
rule.resources[j].accounts[k],
);
}
}

for (let j = 0; j < rule.consumers.length; ++j) {
Expand Down
76 changes: 61 additions & 15 deletions src/schema/provider.v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,70 @@
"type": "string"
},
"resources": {
"description": "A list of patterns to match against resource repos when applying the rule.",
"description": "Sets of criteria that determine whether this rule matches the requested resources.",
"type": "array",
"minItems": 1,
"items": {
"description": "A pattern which matches repos.",
"type": "string",
"minLength": 1,
"pattern": "^(?:\\.|[*a-zA-Z](?:[*a-zA-Z-]*[*a-zA-Z])?)\\/[*a-zA-Z0-9-_.]+$",
"errorMessage": "must be a repo pattern in the form of \"account/repo\", or \"./repo\"",
"examples": [
"./repo-a",
"account-a/repo-a",
"./*",
"*/*",
"*/repo-a",
"account-a/*",
"prefix-*/*-suffix"
]
"description": "A set of criteria that determine whether this rule matches the requested resources.",
"type": "object",
"additionalProperties": false,
"required": ["accounts"],
"anyOf": [
{ "required": ["noRepos"] },
{ "required": ["allRepos"] },
{ "required": ["selectedRepos"] }
],
"properties": {
"accounts": {
"description": "A list of patterns to match against accounts when applying the rule.",
"type": "array",
"minItems": 1,
"items": {
"description": "A pattern which matches accounts.",
"type": "string",
"minLength": 1,
"pattern": "^(?:.|[*a-zA-Z](?:[*a-zA-Z-]*[*a-zA-Z])?)$",
"errorMessage": "must be a single period, or only contain alphanumeric characters, hyphens, or asterisks, and cannot begin or end with a hyphen",
"examples": [
".",
"account-a",
"*",
"with-prefix-*",
"*-with-suffix",
"with-*-infix"
]
}
},
"noRepos": {
"description": "Whether this rule should apply to requests for tokens that can't access any repos in the account(s). When true, this rule will apply when the token request is for account-only access.",
"type": "boolean",
"default": false
},
"allRepos": {
"description": "Whether this rule should apply to requests for tokens that can access all repos in the account(s). When true, this rule will match when the token request doesn't specify a selected set of repos, but instead asks for access to all current and future repos.",
"type": "boolean",
"default": false
},
"selectedRepos": {
"description": "A list of patterns to match against repos when applying the rule.",
"type": "array",
"default": [],
"items": {
"description": "A pattern which matches repos without their account prefix.",
"type": "string",
"minLength": 1,
"pattern": "^[*a-zA-Z0-9-_.]+$",
"errorMessage": "must only contain alphanumeric characters, hyphens, underscores, periods, or asterisks",
"examples": [
"repo-a",
"*",
"with-prefix-*",
"*-with-suffix",
"with-*-infix"
]
}
}
}
}
},
"consumers": {
Expand Down
99 changes: 60 additions & 39 deletions src/token-auth-explainer/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,63 @@ import type {
RepoTokenAuthorizationResourceResult,
RepoTokenAuthorizationResourceResultRuleResult,
RepoTokenAuthorizationResult,
RepoTokenAuthorizationResultAllRepos,
RepoTokenAuthorizationResultExplainer,
RepoTokenAuthorizationResultNoRepos,
RepoTokenAuthorizationResultSelectedRepos,
} from "../type/token-auth-result.js";

const ALLOWED_ICON = "✅";
const DENIED_ICON = "❌";

export function createTextRepoAuthExplainer(): RepoTokenAuthorizationResultExplainer<string> {
return (result) => {
const { resourceAccount, resources, want } = result;
const resourceEntries = Object.entries(resources).sort(([a], [b]) =>
a.localeCompare(b),
if (result.type === "ALL_REPOS") return explainAllRepos(result);
if (result.type === "NO_REPOS") return explainNoRepos(result);

return explainSelectedRepos(result);
};

function explainAllRepos(
result: RepoTokenAuthorizationResultAllRepos,
): string {
const { account, isAllowed, rules, want } = result;
const icon = isAllowed ? ALLOWED_ICON : DENIED_ICON;

return (
`${explainSummary(result)}\n` +
` ${icon} ${isAllowed ? "Sufficient" : "Insufficient"} ` +
`access to all repos in ${account} ${explainBasedOnRules(want, rules)}`
);
}

function explainNoRepos(result: RepoTokenAuthorizationResultNoRepos): string {
const { account, isAllowed, rules, want } = result;
const icon = isAllowed ? ALLOWED_ICON : DENIED_ICON;

return (
`${explainSummary(result)}\n` +
` ${icon} ${isAllowed ? "Sufficient" : "Insufficient"} ` +
`access to ${account} ${explainBasedOnRules(want, rules)}`
);
}

function explainSelectedRepos(
result: RepoTokenAuthorizationResultSelectedRepos,
): string {
const { results, want } = result;
const resourceEntries = Object.entries(results).sort(([a], [b]) =>
a.localeCompare(b),
);
let explainedResources = "";
for (const [resource, resourceResult] of resourceEntries) {

for (const [resourceRepo, resourceResult] of resourceEntries) {
explainedResources +=
"\n" + explainResource(resourceAccount, resource, want, resourceResult);
"\n" + explainResourceRepo(resourceRepo, want, resourceResult);
}

return explainSummary(result) + explainedResources;
};
}

function explainSummary({
consumer,
Expand All @@ -39,47 +75,38 @@ export function createTextRepoAuthExplainer(): RepoTokenAuthorizationResultExpla
);
}

function explainResource(
resourceAccount: string,
function explainResourceRepo(
resource: string,
want: InstallationPermissions,
resourceResult: RepoTokenAuthorizationResourceResult,
{ isAllowed, rules }: RepoTokenAuthorizationResourceResult,
): string {
const summary = explainResourceSummary(
resourceAccount,
resource,
resourceResult,
);
const ruleCount = resourceResult.rules.length;

if (ruleCount < 1) return summary;

let explainedRules = "";
for (const ruleResult of resourceResult.rules) {
explainedRules += "\n" + explainRule(want, ruleResult);
}
const icon = isAllowed ? ALLOWED_ICON : DENIED_ICON;

return `${summary}:${explainedRules}`;
return (
` ${icon} ${isAllowed ? "Sufficient" : "Insufficient"} ` +
`access to repo ${resource} ${explainBasedOnRules(want, rules)}`
);
}

function explainResourceSummary(
resourceAccount: string,
resource: string,
{ rules, isAllowed }: RepoTokenAuthorizationResourceResult,
function explainBasedOnRules(
want: InstallationPermissions,
rules: RepoTokenAuthorizationResourceResultRuleResult[],
): string {
const icon = isAllowed ? ALLOWED_ICON : DENIED_ICON;
const renderedResource = renderResource(resourceAccount, resource);
const ruleCount = rules.length;
const ruleOrRules = ruleCount === 1 ? "rule" : "rules";
const basedOn =
ruleCount < 1
? "(no matching rules)"
: `based on ${ruleCount} ${ruleOrRules}`;

return (
` ${icon} ${isAllowed ? "Sufficient" : "Insufficient"} ` +
`access to ${renderedResource} ${basedOn}`
);
if (ruleCount < 1) return basedOn;

let explainedRules = "";
for (const ruleResult of rules) {
explainedRules += "\n" + explainRule(want, ruleResult);
}

return `${basedOn}:${explainedRules}`;
}

function explainRule(
Expand All @@ -100,12 +127,6 @@ export function createTextRepoAuthExplainer(): RepoTokenAuthorizationResultExpla
);
}

function renderResource(resourceAccount: string, resource: string): string {
return resource === "*"
? `all repos in ${resourceAccount}`
: `repo ${resource}`;
}

function renderRule(index: number, { description }: PermissionsRule): string {
const n = `#${index + 1}`;

Expand Down
Loading

0 comments on commit e196604

Please sign in to comment.