Skip to content

Commit aab45f7

Browse files
committed
merge in master
2 parents 8e281cd + c4f4462 commit aab45f7

31 files changed

+762
-332
lines changed

.changes/1.83.0.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"date": "2023-08-03",
3+
"version": "1.83.0",
4+
"entries": [
5+
{
6+
"type": "Feature",
7+
"description": "IAM Identity Center (SSO): log a warning if SSO user is not linked to an account"
8+
}
9+
]
10+
}

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 1.83.0 2023-08-03
2+
3+
- **Feature** IAM Identity Center (SSO): log a warning if SSO user is not linked to an account
4+
15
## 1.82.0 2023-07-26
26

37
- **Bug Fix** CodeWhisperer: issue with fetching enhanced file context

CONTRIBUTING.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,18 @@ Note therefore:
379379
2. `HEAD` implies that the URL depends on the current _default branch_ (i.e.
380380
`master`). Changes to other branches won't affect the marketplace page.
381381

382+
## Using new vscode APIs
383+
384+
The minimum required vscode version specified in [package.json](https://github.com/aws/aws-toolkit-vscode/blob/07119655109bb06105a3f53bbcd86b812b32cdbe/package.json#L16)
385+
is decided by the version of vscode running in Cloud9 and other vscode-compatible targets.
386+
387+
But you can still use the latest vscode APIs, by checking the current running vscode version. For example, to use a vscode 1.64 API:
388+
389+
1. Check the vscode version: `semver.gte(vscode.version, '1.64.0')`
390+
2. Disable the feature if is too old. That could mean just skipping the code entirely, or showing a different UI.
391+
392+
Full example: https://github.com/aws/aws-toolkit-vscode/blob/7cb97a2ef0a765862d21842693967070b0dcdd49/src/shared/credentials/defaultCredentialSelectionDataProvider.ts#L54-L76
393+
382394
## Importing icons from other open source repos
383395

384396
If you are contribuing visual assets from other open source repos, the source repo must have a compatible license (such as MIT), and we need to document the source of the images. Follow these steps:

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "aws-toolkit-vscode",
33
"displayName": "AWS Toolkit",
44
"description": "Including support for CodeWhisperer, CodeCatalyst, Lambda, S3, CloudWatch Logs, and many other services",
5-
"version": "1.83.0-SNAPSHOT",
5+
"version": "1.84.0-SNAPSHOT",
66
"extensionKind": [
77
"workspace"
88
],
@@ -1120,6 +1120,18 @@
11201120
"command": "aws.ec2.openTerminal",
11211121
"when": "aws.isDevMode"
11221122
},
1123+
{
1124+
"command": "aws.ec2.startInstance",
1125+
"when": "aws.isDevMode"
1126+
},
1127+
{
1128+
"command": "aws.ec2.stopInstance",
1129+
"whem": "aws.isDevMode"
1130+
},
1131+
{
1132+
"command": "aws.ec2.rebootInstance",
1133+
"whem": "aws.isDevMode"
1134+
},
11231135
{
11241136
"command": "aws.dev.openMenu",
11251137
"when": "aws.isDevMode || isCloud9"
@@ -1324,6 +1336,21 @@
13241336
"group": "inline@1",
13251337
"when": "viewItem == awsEc2Node"
13261338
},
1339+
{
1340+
"command": "aws.ec2.startInstance",
1341+
"group": "0@1",
1342+
"when": "viewItem == awsEc2Node"
1343+
},
1344+
{
1345+
"command": "aws.ec2.stopInstance",
1346+
"group": "0@1",
1347+
"when": "viewItem == awsEc2Node"
1348+
},
1349+
{
1350+
"command": "aws.ec2.rebootInstance",
1351+
"group": "0@1",
1352+
"when": "viewItem == awsEc2Node"
1353+
},
13271354
{
13281355
"command": "aws.ecr.createRepository",
13291356
"when": "view == aws.explorer && viewItem == awsEcrNode",
@@ -2091,6 +2118,36 @@
20912118
}
20922119
}
20932120
},
2121+
{
2122+
"command": "aws.ec2.startInstance",
2123+
"title": "%AWS.command.ec2.startInstance%",
2124+
"category": "%AWS.title%",
2125+
"cloud9": {
2126+
"cn": {
2127+
"category": "%AWS.title.cn%"
2128+
}
2129+
}
2130+
},
2131+
{
2132+
"command": "aws.ec2.stopInstance",
2133+
"title": "%AWS.command.ec2.stopInstance%",
2134+
"category": "%AWS.title%",
2135+
"cloud9": {
2136+
"cn": {
2137+
"category": "%AWS.title.cn%"
2138+
}
2139+
}
2140+
},
2141+
{
2142+
"command": "aws.ec2.rebootInstance",
2143+
"title": "%AWS.command.ec2.rebootInstance%",
2144+
"category": "%AWS.title%",
2145+
"cloud9": {
2146+
"cn": {
2147+
"category": "%AWS.title.cn%"
2148+
}
2149+
}
2150+
},
20942151
{
20952152
"command": "aws.ec2.copyInstanceId",
20962153
"title": "%AWS.command.ec2.copyInstanceId%",

package.nls.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@
110110
"AWS.command.cdk.help": "View CDK Documentation",
111111
"AWS.command.ec2.openTerminal": "Open terminal to EC2 instance...",
112112
"AWS.command.ec2.openRemoteConnection": "Connect to EC2 instance in New Window...",
113+
"AWS.command.ec2.startInstance": "Start EC2 Instance",
114+
"AWS.command.ec2.stopInstance": "Stop EC2 Instance",
115+
"AWS.command.ec2.rebootInstance": "Reboot EC2 Instance",
113116
"AWS.command.ec2.copyInstanceId": "Copy Instance Id",
114117
"AWS.command.ecr.copyTagUri": "Copy Tag URI",
115118
"AWS.command.ecr.copyRepositoryUri": "Copy Repository URI",

src/auth/auth.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export class Auth implements AuthService, ConnectionManager {
212212
}
213213

214214
/**
215-
* This method will gather all AWS accounts/roles that are associated with SSO connections.
215+
* Gathers all AWS accounts/roles associated with SSO ("IAM Identity Center", "IdC") connections.
216216
*
217217
* Use {@link Auth.listConnections} when you do not want to make extra API calls to the SSO service.
218218
*/
@@ -232,20 +232,22 @@ export class Auth implements AuthService, ConnectionManager {
232232
const linked = this.store
233233
.listProfiles()
234234
.filter(isLinkable)
235-
.map(([id, profile]) =>
236-
toCollection(() =>
235+
.map(([id, profile]) => {
236+
const startUrl = this.truncateStartUrl(profile.startUrl)
237+
return toCollection(() =>
237238
loadLinkedProfilesIntoStore(
238239
this.store,
239240
id,
240-
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile))
241+
this.createSsoClient(profile.ssoRegion, this.getTokenProvider(id, profile)),
242+
startUrl
241243
)
242244
)
243245
.catch(err => {
244246
getLogger().warn(`auth: failed to load linked profiles from "${id}": %s`, err)
245247
})
246248
.filter(isNonNullable)
247249
.map(entry => this.getConnectionFromStoreEntry(entry))
248-
)
250+
})
249251

250252
yield* linked.reduce(join, stream)
251253
}
@@ -748,8 +750,12 @@ export class Auth implements AuthService, ConnectionManager {
748750
return (this.#instance ??= new Auth(new ProfileStore(memento)))
749751
}
750752

753+
private truncateStartUrl(startUrl: string) {
754+
return startUrl.match(/https?:\/\/(.*)\.awsapps\.com\/start/)?.[1] ?? startUrl
755+
}
756+
751757
private getSsoProfileLabel(profile: SsoProfile) {
752-
const truncatedUrl = profile.startUrl.match(/https?:\/\/(.*)\.awsapps\.com\/start/)?.[1] ?? profile.startUrl
758+
const truncatedUrl = this.truncateStartUrl(profile.startUrl)
753759

754760
return profile.startUrl === builderIdStartUrl
755761
? localizedText.builderId()

src/auth/connection.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { builderIdStartUrl, SsoToken } from './sso/model'
99
import { SsoClient } from './sso/clients'
1010
import { CredentialsProviderManager } from './providers/credentialsProviderManager'
1111
import { fromString } from './providers/credentials'
12+
import { getLogger } from '../shared/logger/logger'
1213

1314
export const ssoScope = 'sso:account:access'
1415
export const codecatalystScopes = ['codecatalyst:read_write']
@@ -247,18 +248,27 @@ export async function loadIamProfilesIntoStore(store: ProfileStore, manager: Cre
247248
}
248249
}
249250

251+
/**
252+
* Fetches profiles from the given SSO ("IAM Identity Center", "IdC") connection.
253+
*/
250254
export async function* loadLinkedProfilesIntoStore(
251255
store: ProfileStore,
252256
source: SsoConnection['id'],
253-
client: SsoClient
257+
client: SsoClient,
258+
profileLabel: string
254259
) {
260+
const accounts = new Set<string>()
261+
const found = new Set<Connection['id']>()
262+
255263
const stream = client
256264
.listAccounts()
257265
.flatten()
258-
.map(resp => client.listAccountRoles({ accountId: resp.accountId }).flatten())
266+
.map(resp => {
267+
accounts.add(resp.accountId)
268+
return client.listAccountRoles({ accountId: resp.accountId }).flatten()
269+
})
259270
.flatten()
260271

261-
const found = new Set<Connection['id']>()
262272
for await (const info of stream) {
263273
const name = `${info.roleName}-${info.accountId}`
264274
const id = `sso:${source}#${name}`
@@ -280,6 +290,20 @@ export async function* loadLinkedProfilesIntoStore(
280290
yield [id, profile] as const
281291
}
282292

293+
if (accounts.size === 0) {
294+
// Possible causes:
295+
// - SSO org has no "Permission sets"
296+
// - user is not an "Assigned user" in any account in the SSO org
297+
// - user is an "Assigned user" but no "Permission sets"
298+
getLogger().warn('auth: SSO org (%s) returned no accounts', profileLabel)
299+
} else if (found.size === 0) {
300+
getLogger().warn(
301+
'auth: SSO org (%s) returned no IAM credentials for account: %s',
302+
profileLabel,
303+
Array.from(accounts).join()
304+
)
305+
}
306+
283307
// Clean-up stale references in case the user no longer has access
284308
for (const [id, profile] of store.listProfiles()) {
285309
if (profile.type === 'iam' && profile.subtype === 'linked' && profile.ssoSession === source && !found.has(id)) {

src/auth/sso/cache.ts

Lines changed: 3 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { hasProps, selectFrom } from '../../shared/utilities/tsUtils'
1212
import { SsoToken, ClientRegistration } from './model'
1313
import { SystemUtilities } from '../../shared/systemUtilities'
1414
import { DevSettings } from '../../shared/settings'
15-
import { statSync, Stats, readdirSync, unlinkSync } from 'fs'
1615

1716
interface RegistrationKey {
1817
readonly region: string
@@ -34,46 +33,13 @@ export interface SsoCache {
3433
const defaultCacheDir = path.join(SystemUtilities.getHomeDirectory(), '.aws', 'sso', 'cache')
3534
export const getCacheDir = () => DevSettings.instance.get('ssoCacheDirectory', defaultCacheDir)
3635

37-
export function getCache(directory = getCacheDir(), statFunc = getFileStats): SsoCache {
38-
try {
39-
deleteOldFiles(directory, statFunc)
40-
} catch (e) {
41-
getLogger().warn('auth: error deleting old files in sso cache: %s', e)
42-
}
43-
36+
export function getCache(directory = getCacheDir()): SsoCache {
4437
return {
4538
token: getTokenCache(directory),
4639
registration: getRegistrationCache(directory),
4740
}
4841
}
4942

50-
function deleteOldFiles(directory: string, statFunc: typeof getFileStats) {
51-
if (!isDirSafeToDeleteFrom(directory)) {
52-
getLogger().warn(`Skipped deleting files in directory: ${path.resolve(directory)}`)
53-
return
54-
}
55-
56-
const fileNames = readdirSync(directory)
57-
fileNames.forEach(fileName => {
58-
const filePath = path.join(directory, fileName)
59-
if (path.extname(filePath) === '.json' && isOldFile(filePath, statFunc)) {
60-
unlinkSync(filePath)
61-
getLogger().warn(`auth: removed old cache file: ${filePath}`)
62-
}
63-
})
64-
}
65-
66-
export function isDirSafeToDeleteFrom(dirPath: string): boolean {
67-
const resolvedPath = path.resolve(dirPath)
68-
const isRoot = resolvedPath === path.resolve('/')
69-
const isCwd = resolvedPath === path.resolve('.')
70-
const isAbsolute = path.isAbsolute(dirPath)
71-
const pathDepth = resolvedPath.split(path.sep).length
72-
73-
const isSafe = !isRoot && !isCwd && isAbsolute && pathDepth >= 5
74-
return isSafe
75-
}
76-
7743
export function getRegistrationCache(directory = getCacheDir()): KeyedCache<ClientRegistration, RegistrationKey> {
7844
// Compatability for older Toolkit versions (format on disk is unchanged)
7945
type StoredRegistration = Omit<ClientRegistration, 'expiresAt'> & { readonly expiresAt: string }
@@ -144,30 +110,7 @@ export function getTokenCache(directory = getCacheDir()): KeyedCache<SsoAccess>
144110
return mapCache(cache, read, write)
145111
}
146112

147-
function getFileStats(file: string): Stats {
148-
return statSync(file)
149-
}
150-
151-
const firstValidDate = new Date(2023, 3, 14) // April 14, 2023
152-
153-
/**
154-
* @returns true if file is older than the first valid date
155-
*/
156-
function isOldFile(file: string, statFunc: typeof getFileStats): boolean {
157-
try {
158-
const statResult = statFunc(file)
159-
// Depending on the Windows filesystem, birthtime may be 0, so we fall back to ctime (last time metadata was changed)
160-
// https://nodejs.org/api/fs.html#stat-time-values
161-
return statResult.birthtimeMs !== 0
162-
? statResult.birthtimeMs < firstValidDate.getTime()
163-
: statResult.ctime < firstValidDate
164-
} catch (err) {
165-
getLogger().debug(`SSO cache file age not be verified: ${file}: ${err}`)
166-
return false // Assume it is no old since we cannot validate
167-
}
168-
}
169-
170-
export function getTokenCacheFile(ssoCacheDir: string, ssoUrl: string): string {
113+
function getTokenCacheFile(ssoCacheDir: string, ssoUrl: string): string {
171114
const encoded = encodeURI(ssoUrl)
172115
// Per the spec: 'SSO Login Token Flow' the access token must be
173116
// cached as the SHA1 hash of the bytes of the UTF-8 encoded
@@ -183,7 +126,7 @@ export function getTokenCacheFile(ssoCacheDir: string, ssoUrl: string): string {
183126
return path.join(ssoCacheDir, `${hashedUrl}.json`)
184127
}
185128

186-
export function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationKey): string {
129+
function getRegistrationCacheFile(ssoCacheDir: string, key: RegistrationKey): string {
187130
const hashScopes = (scopes: string[]) => {
188131
const shasum = crypto.createHash('sha256')
189132
scopes.forEach(s => shasum.update(s))

src/cloudWatchLogs/commands/copyLogResource.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@ import { copyToClipboard } from '../../shared/utilities/messages'
1212

1313
export async function copyLogResource(uri?: vscode.Uri): Promise<void> {
1414
try {
15-
1615
if (!uri) {
1716
// No URI = used command palette as entrypoint, attempt to get URI from active editor
1817
// should work correctly under any normal circumstances since the action only appears in command palette when the editor is a CloudWatch Logs editor
1918
uri = vscode.window.activeTextEditor?.document.uri
2019
if (!uri) {
21-
throw new Error("no active text editor, or undefined URI")
20+
throw new Error('no active text editor, or undefined URI')
2221
}
23-
}
22+
}
2423
const parsedUri = parseCloudWatchLogsUri(uri)
2524
const resourceName = isLogStreamUri(uri) ? parsedUri.logGroupInfo.streamName : parsedUri.logGroupInfo.groupName
2625

0 commit comments

Comments
 (0)