Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Separate CSP-specific functionality from generic SSH code #122

Merged
merged 1 commit into from
Oct 1, 2024
Merged
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
86 changes: 61 additions & 25 deletions src/plugins/aws/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,34 +31,38 @@ const MAX_SSH_RETRIES = 30;
/** The name of the SessionManager port forwarding document. This document is managed by AWS. */
const START_SSH_SESSION_DOCUMENT_NAME = "AWS-StartSSHSession";

/**There are 2 cases of unprovisioned access in AWS
* 1. SSM:StartSession action is missing either on the SSM document (AWS-StartSSHSession) or the EC2 instance
* 2. Temporary error when issuing an SCP command
*
* 1: results in UNAUTHORIZED_START_SESSION_MESSAGE
* 2: results in CONNECTION_CLOSED_MESSAGE
*/
const unprovisionedAccessPatterns = [
/** Matches the error message that AWS SSM prints when access is not propagated */
// Note that the resource will randomly be either the SSM document or the EC2 instance
{
pattern:
/An error occurred \(AccessDeniedException\) when calling the StartSession operation: User: arn:aws:sts::.*:assumed-role\/P0GrantsRole.* is not authorized to perform: ssm:StartSession on resource: arn:aws:.*:.*:.* because no identity-based policy allows the ssm:StartSession action/,
},
/**
* Matches the following error messages that AWS SSM pints when ssh authorized
* key access hasn't propagated to the instance yet.
* - Connection closed by UNKNOWN port 65535
* - scp: Connection closed
* - kex_exchange_identification: Connection closed by remote host
*/
{
pattern: /\bConnection closed\b.*\b(?:by UNKNOWN port \d+|by remote host)?/,
},
] as const;

export const awsSshProvider: SshProvider<
AwsSshPermissionSpec,
undefined,
AwsSshRequest,
AwsCredentials
> = {
requestToSsh: (request) => {
const { permission, generated } = request;
const { instanceId, accountId, region } = permission.spec;
const { idc, ssh, name } = generated;
const { linuxUserName } = ssh;
const common = { linuxUserName, accountId, region, id: instanceId };
return !idc
? { ...common, role: name, type: "aws", access: "role" }
: {
...common,
idc,
permissionSet: name,
type: "aws",
access: "idc",
};
},
toCliRequest: async (request) => ({ ...request, cliLocalData: undefined }),
ensureInstall: async () => {
if (!(await ensureSsmInstall())) {
throw "Please try again after installing the required AWS utilities";
}
},
cloudProviderLogin: async (authn, request) => {
const { config } = await getAwsConfig(authn, request.accountId);
if (!config.login?.type || config.login?.type === "iam") {
Expand All @@ -71,6 +75,19 @@ export const awsSshProvider: SshProvider<
? await assumeRoleWithOktaSaml(authn, request as AwsSshRoleRequest)
: throwAssertNever(config.login);
},

ensureInstall: async () => {
if (!(await ensureSsmInstall())) {
throw "Please try again after installing the required AWS utilities";
}
},

friendlyName: "AWS",

maxRetries: MAX_SSH_RETRIES,

preTestAccessPropagationArgs: () => undefined,

proxyCommand: (request) => {
return [
"aws",
Expand All @@ -86,6 +103,7 @@ export const awsSshProvider: SshProvider<
'"portNumber=%p"',
];
},

reproCommands: (request) => {
// TODO: Add manual commands for IDC login
if (request.access !== "idc") {
Expand All @@ -95,7 +113,25 @@ export const awsSshProvider: SshProvider<
}
return undefined;
},
preTestAccessPropagationArgs: () => undefined,
maxRetries: MAX_SSH_RETRIES,
friendlyName: "AWS",

requestToSsh: (request) => {
const { permission, generated } = request;
const { instanceId, accountId, region } = permission.spec;
const { idc, ssh, name } = generated;
const { linuxUserName } = ssh;
const common = { linuxUserName, accountId, region, id: instanceId };
return !idc
? { ...common, role: name, type: "aws", access: "role" }
: {
...common,
idc,
permissionSet: name,
type: "aws",
access: "idc",
};
},

toCliRequest: async (request) => ({ ...request, cliLocalData: undefined }),

unprovisionedAccessPatterns,
};
115 changes: 82 additions & 33 deletions src/plugins/google/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,75 @@ import { GcpSshPermissionSpec, GcpSshRequest } from "./types";
*/
const MAX_SSH_RETRIES = 120;

/**
* There are 7 cases of unprovisioned access in Google Cloud.
* These are all potentially subject to propagation delays.
* 1. The linux user name is not present in the user's Google Workspace profile `posixAccounts` attribute
* 2. The public key is not present in the user's Google Workspace profile `sshPublicKeys` attribute
* 3. The user cannot act as the service account of the compute instance
* 4. The user cannot tunnel through the IAP tunnel to the instance
* 5. The user doesn't have osLogin or osAdminLogin role to the instance
* 5.a. compute.instances.get permission is missing
* 5.b. compute.instances.osLogin permission is missing
* 6. compute.instances.osAdminLogin is not provisioned but compute.instances.osLogin is - happens when a user upgrades existing access to sudo
* 7: Rare occurrence, the exact conditions so far undetermined (together with CONNECTION_CLOSED_MESSAGE)
*
* 1, 2, 3 (yes!), 5b: result in PUBLIC_KEY_DENIED_MESSAGE
* 4: results in UNAUTHORIZED_TUNNEL_USER_MESSAGE and also CONNECTION_CLOSED_MESSAGE
* 5a: results in UNAUTHORIZED_INSTANCES_GET_MESSAGE
* 6: results in SUDO_MESSAGE
* 7: results in DESTINATION_READ_ERROR and also CONNECTION_CLOSED_MESSAGE
*/
const unprovisionedAccessPatterns = [
{ pattern: /Permission denied \(publickey\)/ },
{
// The output of `sudo -v` when the user is not allowed to run sudo
pattern: /Sorry, user .+ may not run sudo on .+/,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useful to keep the existing constants I think, as they may be used elsewhere. I am making a change which relies on this string for example.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's introduce that in the PR that actually reuses it?

},
{ pattern: /Error while connecting \[4033: 'not authorized'\]/ },
{
pattern: /Required 'compute\.instances\.get' permission/,
validationWindowMs: 30e3,
},
{ pattern: /Error while connecting \[4010: 'destination read failed'\]/ },
] as const;

export const gcpSshProvider: SshProvider<
GcpSshPermissionSpec,
{ linuxUserName: string },
GcpSshRequest
> = {
requestToSsh: (request) => {
return {
id: request.permission.spec.instanceName,
projectId: request.permission.spec.projectId,
zone: request.permission.spec.zone,
linuxUserName: request.cliLocalData.linuxUserName,
type: "gcloud",
};
},
toCliRequest: async (request, options) => ({
...request,
cliLocalData: {
linuxUserName: await importSshKey(
request.permission.spec.publicKey,
options
),
},
}),
// TODO support login with Google Cloud
cloudProviderLogin: async () => undefined,

ensureInstall: async () => {
if (!(await ensureGcpSshInstall())) {
throw "Please try again after installing the required GCP utilities";
}
},
cloudProviderLogin: async () => undefined, // TODO @ENG-2284 support login with Google Cloud

friendlyName: "Google Cloud",

loginRequiredMessage:
"Please login to Google Cloud CLI with 'gcloud auth login'",

loginRequiredPattern: /You do not currently have an active account selected/,

maxRetries: MAX_SSH_RETRIES,

preTestAccessPropagationArgs: (cmdArgs) => {
if (isSudoCommand(cmdArgs)) {
return {
...cmdArgs,
// `sudo -v` prints `Sorry, user <user> may not run sudo on <hostname>.` to stderr when user is not a sudoer.
// It prints nothing to stdout when user is a sudoer - which is important because we don't want any output from the pre-test.
command: "sudo",
arguments: ["-v"],
};
}
return undefined;
},

proxyCommand: (request) => {
return [
"gcloud",
Expand All @@ -65,19 +105,28 @@ export const gcpSshProvider: SshProvider<
`--project=${request.projectId}`,
];
},
reproCommands: () => undefined, // TODO @ENG-2284 support login with Google Cloud
preTestAccessPropagationArgs: (cmdArgs) => {
if (isSudoCommand(cmdArgs)) {
return {
...cmdArgs,
// `sudo -v` prints `Sorry, user <user> may not run sudo on <hostname>.` to stderr when user is not a sudoer.
// It prints nothing to stdout when user is a sudoer - which is important because we don't want any output from the pre-test.
command: "sudo",
arguments: ["-v"],
};
}
return undefined;

reproCommands: () => undefined,

requestToSsh: (request) => {
return {
id: request.permission.spec.instanceName,
projectId: request.permission.spec.projectId,
zone: request.permission.spec.zone,
linuxUserName: request.cliLocalData.linuxUserName,
type: "gcloud",
};
},
maxRetries: MAX_SSH_RETRIES,
friendlyName: "Google Cloud",

unprovisionedAccessPatterns,

toCliRequest: async (request, options) => ({
...request,
cliLocalData: {
linuxUserName: await importSshKey(
request.permission.spec.publicKey,
options
),
},
}),
};
Loading
Loading