Skip to content

Commit b556c16

Browse files
authored
Concept: IDE extensions provide credentials to language server (#523)
This change demonstrates how we can let the IDE extension handle credentials configuration/resolution, and provide credentials into the language servers. This way, the language servers do not have to maintain an identical credential management implementation, which can drift over time. When the server is started with a specific command line argument, the server waits to receive the encryption key over stdin before listening for the LSP protocol. Extensions then pass a marker through the LSP Initialization message. When the server detects this marker, it registers a custom notification handler that will be used to receive credentials. The extension then pushes credentials (using custom notification messages) whenever the credential state changes. In this way, the server always has credentials available for use. At times when no credentials can be resolved, the server's credentials are "un-set". The sample S3 language server has been updated with this credentials concept. The sample VS and VS Code extensions have been updated with ways to simulate resolving credentials, and pushing them to the language server. This concept is subject to additional changes before its initial release.
1 parent 3c5bf40 commit b556c16

27 files changed

+3193
-302
lines changed

.prettierignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
telemetry/service/service-model.json
1+
telemetry/definitions/commonDefinitions.json
2+
telemetry/service/service-model.json

lsp/.vscode/launch.json

+3-2
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@
5151
"preLaunchTask": "npm: compile"
5252
},
5353
{
54-
"name": "S3 Server",
54+
"name": "S3 Server (with Credentials support)",
5555
"type": "extensionHost",
5656
"request": "launch",
5757
"runtimeExecutable": "${execPath}",
5858
"args": ["--extensionDevelopmentPath=${workspaceFolder}/client/vscode"],
5959
"outFiles": ["${workspaceFolder}/client/vscode/out/**/*.js"],
6060
"env": {
61-
"LSP_SERVER": "${workspaceFolder}/app/aws-lsp-s3-binary/out/index.js"
61+
"LSP_SERVER": "${workspaceFolder}/app/aws-lsp-s3-binary/out/index.js",
62+
"ENABLE_IAM_PROVIDER": "true"
6263
},
6364
"preLaunchTask": "npm: compile"
6465
},

lsp/README.md

+5
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ Monorepo
2929
── server - packages that contain Language Server implementations
3030
└── aws-lsp-buildspec - Language Server that wraps a JSON Schema for CodeBuild buildspec
3131
└── aws-lsp-cloudformation - Language Server that wraps a JSON Schema for CloudFormation
32+
└── aws-lsp-codewhisperer - Language Server that surfaces CodeWhisperer recommendations
33+
- experimental. Shows how recommendations can surface through
34+
completion lists and as ghost text
3235
└── aws-lsp-s3 - Example language server that provides S3 bucket names as completion items
36+
- Shows a concept where credentials can be provided from an IDE extension
37+
(See vscode and vs client readmes)
3338
```
3439

3540
## How To Contribute
+73-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,82 @@
1+
import {
2+
AwsInitializationOptions,
3+
EncryptionInitialization,
4+
IdeCredentialsProvider,
5+
shouldWaitForEncryptionKey,
6+
validateEncryptionDetails,
7+
} from '@lsp-placeholder/aws-lsp-core'
18
import { S3Server, S3ServerProps, S3ServiceProps, createS3ervice } from '@lsp-placeholder/aws-lsp-s3'
9+
import { Readable } from 'stream'
210
import { ProposedFeatures, createConnection } from 'vscode-languageserver/node'
311

4-
const connection = createConnection(ProposedFeatures.all)
12+
const lspConnection = createConnection(ProposedFeatures.all)
513

6-
const serviceProps: S3ServiceProps = {
7-
displayName: S3Server.serverId,
14+
if (shouldWaitForEncryptionKey()) {
15+
// Before starting the language server, accept encryption initialization details
16+
// directly from the host. This avoids writing the key to the same channel used
17+
// to send encrypted data.
18+
// Contract: Only read up to (and including) the first newline (\n).
19+
readLine(process.stdin).then(input => {
20+
const encryptionDetails = JSON.parse(input) as EncryptionInitialization
21+
22+
validateEncryptionDetails(encryptionDetails)
23+
24+
createServer(lspConnection, encryptionDetails.key)
25+
})
26+
} else {
27+
createServer(lspConnection)
828
}
929

10-
const cloudformationService = createS3ervice(serviceProps)
30+
/**
31+
* Read from the given stream, stopping after the first newline (\n).
32+
* Return the string consumed from the stream.
33+
*/
34+
function readLine(stream: Readable): Promise<string> {
35+
return new Promise<string>((resolve, reject) => {
36+
let contents = ''
37+
38+
// Fires when the stream has contents that can be read
39+
const onStreamIsReadable = () => {
40+
while (true) {
41+
const byteRead: Buffer = process.stdin.read(1)
42+
if (byteRead == null) {
43+
// wait for more content to arrive on the stream
44+
break
45+
}
46+
47+
const nextChar = byteRead.toString('utf-8')
48+
contents += nextChar
49+
50+
if (nextChar == '\n') {
51+
// Stop reading this stream, we have read a line from it
52+
stream.removeListener('readable', onStreamIsReadable)
53+
resolve(contents)
54+
break
55+
}
56+
}
57+
}
1158

12-
const props: S3ServerProps = {
13-
connection,
14-
s3Service: cloudformationService,
59+
stream.on('readable', onStreamIsReadable)
60+
})
1561
}
1662

17-
export const server = new S3Server(props)
63+
function createServer(connection: any, key?: string): S3Server {
64+
const credentialsProvider = new IdeCredentialsProvider(connection, key)
65+
66+
const serviceProps: S3ServiceProps = {
67+
displayName: S3Server.serverId,
68+
credentialsProvider,
69+
}
70+
71+
const service = createS3ervice(serviceProps)
72+
73+
const props: S3ServerProps = {
74+
connection,
75+
s3Service: service,
76+
onInitialize: (props: AwsInitializationOptions) => {
77+
credentialsProvider.initialize(props)
78+
},
79+
}
80+
81+
return new S3Server(props)
82+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using Newtonsoft.Json;
2+
3+
namespace IdesLspPoc.Credentials
4+
{
5+
/// <summary>
6+
// Request that the host uses when talking to custom notifications in
7+
// order to send updated credentials and bearer tokens to the language server.
8+
//
9+
// See credentialsProtocolMethodNames in core\aws-lsp-core\src\credentials\credentialsProvider.ts
10+
// for the custom notification names.
11+
//
12+
// While there are separate notifications for sending credentials and sending bearer tokens,
13+
// both notifications use this request.The `data` field is different for each notification.
14+
/// </summary>
15+
public class UpdateCredentialsRequest
16+
{
17+
/// <summary>
18+
/// Initialization vector for encrypted data, in base64
19+
/// </summary>
20+
[JsonProperty("iv")]
21+
public string Iv;
22+
23+
/// <summary>
24+
/// Encrypted data, in base64. The data contents will vary based on the request made.
25+
/// (eg: The payload is different when requesting IAM vs Bearer token)
26+
/// </summary>
27+
[JsonProperty("data")]
28+
public string Data;
29+
30+
/// <summary>
31+
/// Encrypted data's authTag - used for decryption validation
32+
/// </summary>
33+
[JsonProperty("authTag")]
34+
public string AuthTag;
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace IdesLspPoc.Credentials
2+
{
3+
public class UpdateIamCredentialsRequestData
4+
{
5+
public string AccessKeyId;
6+
public string SecretAccessKey;
7+
public string SessionToken;
8+
}
9+
}

lsp/client/visualStudio/IdesLspPoc/IdesLspPoc.csproj

+10
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,12 @@
4747
<ItemGroup>
4848
<Compile Include="ContentDefinitions\BuildSpec.cs" />
4949
<Compile Include="ContentDefinitions\JsonContentType.cs" />
50+
<Compile Include="Credentials\UpdateCredentialsRequest.cs" />
5051
<Compile Include="LspClient\BuildspecLspClient.cs" />
52+
<Compile Include="Credentials\UpdateIamCredentialsRequestData.cs" />
53+
<Compile Include="LspClient\S3LspClient.cs" />
5154
<Compile Include="LspClient\CloudFormationLspClient.cs" />
55+
<Compile Include="LspClient\S3\S3CredentialsUpdater.cs" />
5256
<Compile Include="LspClient\ToolkitLspClient.cs" />
5357
<Compile Include="Output\OutputWindow.cs" />
5458
<Compile Include="Properties\AssemblyInfo.cs" />
@@ -70,6 +74,12 @@
7074
<Reference Include="System" />
7175
</ItemGroup>
7276
<ItemGroup>
77+
<PackageReference Include="AWSSDK.Core">
78+
<Version>3.7.107.9</Version>
79+
</PackageReference>
80+
<PackageReference Include="BouncyCastle.Cryptography">
81+
<Version>2.2.1</Version>
82+
</PackageReference>
7383
<PackageReference Include="Microsoft.VisualStudio.LanguageServer.Client">
7484
<Version>17.0.5165</Version>
7585
</PackageReference>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
using Amazon.Runtime;
2+
using Amazon.Runtime.CredentialManagement;
3+
using IdesLspPoc.Credentials;
4+
using IdesLspPoc.Output;
5+
using Microsoft.VisualStudio.Threading;
6+
using Newtonsoft.Json;
7+
using Newtonsoft.Json.Serialization;
8+
using Org.BouncyCastle.Crypto.Engines;
9+
using Org.BouncyCastle.Crypto.Modes;
10+
using Org.BouncyCastle.Crypto.Parameters;
11+
using Org.BouncyCastle.Security;
12+
using StreamJsonRpc;
13+
using System;
14+
using System.Text;
15+
using System.Threading.Tasks;
16+
using System.Timers;
17+
18+
namespace IdesLspPoc.LspClient.S3
19+
{
20+
/// <summary>
21+
/// This class knows how to push credentials over to the language server (via SendIamCredentialsAsync)
22+
/// PROOF OF CONCEPT - this class would be used in a product like the AWS Toolkit
23+
/// to push credentials to the server whenever the credentials state changes (eg: user selects another profile, or
24+
/// credentials expire). For this concept, it is resolving and pushing credentials every 10 seconds as a
25+
/// simulation of the Toolkit sending credentials to the server.
26+
/// This class would also know how to clear credentials from the language server.
27+
/// </summary>
28+
internal class S3CredentialsUpdater
29+
{
30+
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings
31+
{
32+
ContractResolver = new DefaultContractResolver
33+
{
34+
NamingStrategy = new CamelCaseNamingStrategy(),
35+
}
36+
};
37+
38+
private Timer _resendTimer;
39+
private OutputWindow _outputWindow;
40+
private JsonRpc _rpc;
41+
private byte[] _aesKey;
42+
43+
public S3CredentialsUpdater(JsonRpc rpc, byte[] aesKey, OutputWindow outputWindow)
44+
{
45+
this._rpc = rpc;
46+
_aesKey = aesKey;
47+
_outputWindow = outputWindow;
48+
}
49+
50+
/// <summary>
51+
/// We start sending the lsp server credentials every 10 seconds as a simulation of the credentials state changing
52+
/// </summary>
53+
public void StartCredentialsRefreshSimulation()
54+
{
55+
_resendTimer?.Stop();
56+
57+
_resendTimer = new Timer()
58+
{
59+
AutoReset = true,
60+
Interval = 10_000,
61+
};
62+
63+
_resendTimer.Elapsed += OnRefreshCredentials;
64+
65+
_resendTimer.Start();
66+
}
67+
68+
private void OnRefreshCredentials(object sender, ElapsedEventArgs e)
69+
{
70+
// PROOF OF CONCEPT
71+
// We will resolve the default profile from the local system.
72+
// In a product, the host extension would know which profile it is configured to provide to the language server.
73+
var creds = new SharedCredentialsFile();
74+
if (!creds.TryGetProfile("default", out var profile))
75+
{
76+
_outputWindow.WriteLine("Client: Unable to get default profile");
77+
return;
78+
}
79+
80+
Task.Run(async () =>
81+
{
82+
AWSCredentials awsCredentials = profile.GetAWSCredentials(creds);
83+
var request = CreateUpdateCredentialsRequest(await awsCredentials.GetCredentialsAsync(), _aesKey);
84+
await SendIamCredentialsAsync(request);
85+
}).Forget();
86+
}
87+
88+
public async Task SendIamCredentialsAsync(UpdateCredentialsRequest request)
89+
{
90+
_outputWindow.WriteLine("Client: Sending (simulated) refreshed Credentials to the server");
91+
await this._rpc.NotifyAsync("$/aws/credentials/iam/update", request);
92+
}
93+
94+
private static UpdateCredentialsRequest CreateUpdateCredentialsRequest(ImmutableCredentials credentials, byte[] aesKey)
95+
{
96+
var requestData = new UpdateIamCredentialsRequestData
97+
{
98+
AccessKeyId = credentials.AccessKey,
99+
SecretAccessKey = credentials.SecretKey,
100+
SessionToken = credentials.Token,
101+
};
102+
103+
return CreateUpdateCredentialsRequest(requestData, aesKey);
104+
}
105+
106+
/// <summary>
107+
/// Creates an "update credentials" request that contains encrypted data
108+
/// </summary>
109+
private static UpdateCredentialsRequest CreateUpdateCredentialsRequest(object data, byte[] aesKey)
110+
{
111+
byte[] iv = CreateInitializationVector();
112+
113+
var aesEngine = new AesEngine();
114+
int macSize = 8 * aesEngine.GetBlockSize();
115+
116+
GcmBlockCipher cipher = new GcmBlockCipher(aesEngine);
117+
AeadParameters parameters = new AeadParameters(new KeyParameter(aesKey), macSize, iv);
118+
cipher.Init(true, parameters);
119+
120+
var json = JsonConvert.SerializeObject(data, _serializerSettings);
121+
122+
// Encrypt json
123+
byte[] cipherText = new byte[cipher.GetOutputSize(json.Length)];
124+
int len = cipher.ProcessBytes(Encoding.UTF8.GetBytes(json), 0, json.Length, cipherText, 0);
125+
cipher.DoFinal(cipherText, len);
126+
127+
// Get the authTag
128+
byte[] mac = cipher.GetMac();
129+
string authtag = Convert.ToBase64String(mac);
130+
var dataLength = cipherText.Length - mac.Length;
131+
132+
return new UpdateCredentialsRequest
133+
{
134+
Iv = Convert.ToBase64String(iv),
135+
// Remove Mac from end of cipherText
136+
Data = Convert.ToBase64String(cipherText, 0, dataLength),
137+
AuthTag = authtag,
138+
};
139+
}
140+
141+
private static byte[] CreateInitializationVector()
142+
{
143+
var iv = new byte[16];
144+
SecureRandom random = new SecureRandom();
145+
random.NextBytes(iv);
146+
return iv;
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)