Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,44 @@ npm install -g gulp-cli
gulp ci
```

### Concurrent PAC requests

For interactive users, use a stable profile key per customer, tenant, and identity. The persistent helper retains PAC's
authentication cache across host sessions and serializes only callers sharing that key; different keys can run concurrently:

```typescript
await withPersistentPacRuntimeParameters("customer-tenant-user", runnerParameters, scopedRunnerParameters =>
actions.whoAmI(parameters, scopedRunnerParameters, host)
);
```

Authenticate inside the persistent runtime only when `pac auth list` shows no stored profile. Do not run `pac auth clear`
or delete the persistent root at the end of each operation. Explicit logout or reauthentication remains a host decision.

For ephemeral CI or service-principal work, create a disposable runtime for each request and pass its environment to
`RunnerParameters.pacEnvironment`. Use `withPacRuntimeEnvironment` so the root is removed on both success and failure:

```typescript
await withPacRuntimeEnvironment(async runtime => {
await actions.whoAmI(parameters, {
...runnerParameters,
pacEnvironment: runtime.environment,
}, host);
});
```

When the operation already owns `RunnerParameters`, `withPacRuntimeParameters` performs the merge for you:

```typescript
await withPacRuntimeParameters(runnerParameters, scopedRunnerParameters =>
actions.whoAmI(parameters, scopedRunnerParameters, host)
);
```

The profile-directory environment variables are runtime-validated rather than a documented PAC isolation contract. Hosts
must keep a serialized fallback and validate the PAC version they deploy. If isolation is not honored, serialize the complete
PAC transaction globally rather than relying on the per-profile lock.

### How to make GitHub Actions and Build Tools compatible with latest PAC CLI?

After adding any new functionality in PAC CLI, support for relevant parameters/actions needs to be considered on all three repositories.
Expand Down
24 changes: 20 additions & 4 deletions src/CommandRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,21 @@ export function createCommandRunner(
agent: string,
options?: SpawnOptionsWithoutStdio
): CommandRunner {
return async function run(...args: string[]): Promise<string[]> {
const spawnOptions = options ?? {};
const scopedEnvironment: NodeJS.ProcessEnv = { ...(spawnOptions.env ?? {}) };
const run: CommandRunner = async function run(...args: string[]): Promise<string[]> {
return new Promise((resolve, reject) => {
logInitialization(...args);

const allOutput: string[] = [];

const cp = spawn(commandPath, args, {
cwd: workingDir,
...spawnOptions,
env: Object.assign({
PATH: env.PATH,
"PP_TOOLS_AUTOMATION_AGENT": agent
}, process.env),
...options,
}, process.env, spawnOptions.env, scopedEnvironment),
});

const outputLineReader = readline.createInterface({ input: cp.stdout });
Expand Down Expand Up @@ -64,6 +66,18 @@ export function createCommandRunner(
});
};

run.setEnvironment = (environment: NodeJS.ProcessEnv) => {
for (const [name, value] of Object.entries(environment)) {
if (value === undefined) {
delete scopedEnvironment[name];
} else {
scopedEnvironment[name] = value;
}
}
};

return run;

function closeAllReaders(outputLineReader?: readline.Interface | undefined, errorLineReader?: readline.Interface | undefined): void {
outputLineReader?.close();
errorLineReader?.close();
Expand All @@ -83,7 +97,9 @@ export function createCommandRunner(
}
}

export type CommandRunner = (...args: string[]) => Promise<string[]>;
export type CommandRunner = ((...args: string[]) => Promise<string[]>) & {
setEnvironment?: (environment: NodeJS.ProcessEnv) => void;
};

export class RunnerError extends Error {
public constructor(public exitCode: number, message: string) {
Expand Down
4 changes: 4 additions & 0 deletions src/Parameters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface RunnerParameters extends LoggerParameters, TelemetryParameters
{
workingDir: string;

// Optional environment overrides applied only to PAC child processes.
// Hosts can use this to provide a request-scoped PAC profile/cache root.
pacEnvironment?: NodeJS.ProcessEnv;

// Directory containing unzipped Windows and Linux PAC Nuget Packages.
// Expectation is that, both versions have been renamed such that
// linux PAC executable's path is <runnersDir>/pac_linux/tools/pac
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./CommandRunner";
export * from "./Parameters";
export * from "./pac/auth/authParameters";
export * from "./pac/runtimeEnvironment";
export * from "./Logger";

import * as actions from "./actions";
Expand Down
12 changes: 9 additions & 3 deletions src/pac/auth/authenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,25 @@ import { ClientCredentials, AuthCredentials, UsernamePassword, FederatedCredenti

export function authenticateAdmin(pac: CommandRunner, credentials: AuthCredentials, logger: Logger): Promise<string[]> {
logger.log(`authN to admin API: authType=${isUsernamePassword(credentials) ? 'UserPass' : 'SPN'}; cloudInstance: ${credentials.cloudInstance || '<not set>'}`);
setClientSecretEnvironment(pac, credentials);
return pac("auth", "create", ...addCredentials(credentials), ...addCloudInstance(credentials));
}

export function authenticateEnvironment(pac: CommandRunner, credentials: AuthCredentials, environmentUrl: string, logger: Logger): Promise<string[]> {

logger.log(`authN to env. authType:${isUsernamePassword(credentials) ? 'UserPass' : 'SPN'} authScheme:${isUsernamePassword(credentials) ? '' : `${credentials.scheme}`}; cloudInstance: ${credentials.cloudInstance || '<not set>'}; envUrl: ${environmentUrl}`);
setClientSecretEnvironment(pac, credentials);
return pac("auth", "create", ...addEnvironment(environmentUrl), ...addCredentials(credentials), ...addCloudInstance(credentials));
}

export function clearAuthentication(pac: CommandRunner): Promise<string[]> {
delete process.env.PAC_CLI_SPN_SECRET; // Will be cleaned up anyway by closing of the node process
return pac("auth", "clear");
return pac("auth", "clear").finally(() => pac.setEnvironment?.({ PAC_CLI_SPN_SECRET: undefined }));
}

function setClientSecretEnvironment(pac: CommandRunner, credentials: AuthCredentials): void {
if (!isUsernamePassword(credentials) && !isFederatedCredentials(credentials) && credentials.scheme !== "ManagedServiceIdentity") {
pac.setEnvironment?.({ PAC_CLI_SPN_SECRET: credentials.clientSecret });
}
}

function addEnvironment(env: string) {
Expand Down Expand Up @@ -54,7 +61,6 @@ function addClientCredentials(parameters: ClientCredentials) {
return ["--managedIdentity"];
}

process.env.PAC_CLI_SPN_SECRET = parameters.clientSecret;
const clientSecret = parameters.encodeSecret ? `data:text/plain;base64,${Buffer.from(parameters.clientSecret, 'binary').toString('base64')}` : parameters.clientSecret;

return [
Expand Down
4 changes: 2 additions & 2 deletions src/pac/createPacRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { resolve } from "path";
import { CommandRunner, createCommandRunner } from "../CommandRunner";
import { RunnerParameters } from "../Parameters";

export default function createPacRunner({workingDir, runnersDir, pacPath, logger, agent}: RunnerParameters): CommandRunner
export default function createPacRunner({workingDir, runnersDir, pacPath, logger, agent, pacEnvironment}: RunnerParameters): CommandRunner
{
return createCommandRunner(
workingDir,
Expand All @@ -12,6 +12,6 @@ export default function createPacRunner({workingDir, runnersDir, pacPath, logger
: resolve(runnersDir, "pac_linux", "tools", "pac")),
logger,
agent,
undefined,
pacEnvironment ? { env: pacEnvironment } : undefined,
);
}
Loading