Skip to content
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
1,619 changes: 1,347 additions & 272 deletions package-lock.json

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions server/aws-lsp-identity/src/iam/iamProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { AwsErrorCodes, IamCredentials, Profile, ProfileKind } from '@aws/language-server-runtimes/server-interface'
import { AwsError, Observability } from '@aws/lsp-core'
import { ProfileStore } from '../language-server/profiles/profileService'

export class IamProvider {
constructor(
private readonly observability: Observability, // In case we need telemetry and logging in the future
private readonly profileStore: ProfileStore // Will be used when assuming role with source_profile
) {}

async getCredential(profile: Profile, callStsOnInvalidIamCredential: boolean): Promise<IamCredentials> {
let credentials: IamCredentials
// Get the credentials directly from the profile
if (profile.kinds.includes(ProfileKind.IamCredentialsProfile)) {
credentials = {
accessKeyId: profile.settings!.aws_access_key_id!,
secretAccessKey: profile.settings!.aws_secret_access_key!,
sessionToken: profile.settings!.aws_session_token!,
}
} else {
throw new AwsError(
'Credentials could not be found for provided profile kind',
AwsErrorCodes.E_INVALID_PROFILE
)
}

return credentials
}
}
38 changes: 38 additions & 0 deletions server/aws-lsp-identity/src/iam/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { IAMClient, SimulatePrincipalPolicyCommand, SimulatePrincipalPolicyCommandOutput } from '@aws-sdk/client-iam'
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts'
import {
AwsErrorCodes,
GetMfaCodeParams,
GetMfaCodeResult,
IamCredentials,
} from '@aws/language-server-runtimes/server-interface'
import { AwsError } from '@aws/lsp-core'

// Simulate permissions on the identity associated with the credentials
export async function simulatePermissions(
credentials: IamCredentials,
permissions: string[],
region?: string
): Promise<SimulatePrincipalPolicyCommandOutput> {
// Convert the credentials into an identity
const stsClient = new STSClient({ region: region || 'us-east-1', credentials: credentials })
const identity = await stsClient.send(new GetCallerIdentityCommand({}))
if (!identity.Arn) {
throw new AwsError('Caller identity ARN not found.', AwsErrorCodes.E_INVALID_PROFILE)
}

// Simulate permissions on the identity
const iamClient = new IAMClient({ region: region || 'us-east-1', credentials: credentials })
return await iamClient.send(
new SimulatePrincipalPolicyCommand({
PolicySourceArn: identity.Arn,
ActionNames: permissions,
})
)
}

export type SendGetMfaCode = (params: GetMfaCodeParams) => Promise<GetMfaCodeResult>

export type IamHandlers = {
sendGetMfaCode: SendGetMfaCode
}
19 changes: 18 additions & 1 deletion server/aws-lsp-identity/src/language-server/identityServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
InitializeParams,
PartialInitializeResult,
ShowMessageRequestParams,
GetIamCredentialParams,
GetMfaCodeParams,
} from '@aws/language-server-runtimes/server-interface'
import { SharedConfigProfileStore } from './profiles/sharedConfigProfileStore'
import { IdentityService } from './identityService'
Expand All @@ -18,6 +20,8 @@ import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher'
import { AwsError, ServerBase } from '@aws/lsp-core'
import { Features } from '@aws/language-server-runtimes/server-interface/server'
import { ShowUrl, ShowMessageRequest, ShowProgress } from '../sso/utils'
import { SendGetMfaCode } from '../iam/utils'
import { IamProvider } from '../iam/iamProvider'

export class IdentityServer extends ServerBase {
constructor(features: Features) {
Expand All @@ -38,6 +42,8 @@ export class IdentityServer extends ServerBase {
const showMessageRequest: ShowMessageRequest = (params: ShowMessageRequestParams) =>
this.features.lsp.window.showMessageRequest(params)
const showProgress: ShowProgress = this.features.lsp.sendProgress
const sendGetMfaCode: SendGetMfaCode = (params: GetMfaCodeParams) =>
this.features.identityManagement.sendGetMfaCode(params)

// Initialize dependencies
const profileStore = new SharedConfigProfileStore(this.observability)
Expand All @@ -50,11 +56,14 @@ export class IdentityServer extends ServerBase {

const autoRefresher = new SsoTokenAutoRefresher(ssoCache, this.observability)

const iamProvider = new IamProvider(this.observability, profileStore)

const identityService = new IdentityService(
profileStore,
ssoCache,
autoRefresher,
{ showUrl, showMessageRequest, showProgress },
iamProvider,
{ showUrl, showMessageRequest, showProgress, sendGetMfaCode },
this.getClientName(params),
this.observability
)
Expand All @@ -70,6 +79,14 @@ export class IdentityServer extends ServerBase {
})
)

this.features.identityManagement.onGetIamCredential(
async (params: GetIamCredentialParams, token: CancellationToken) =>
await identityService.getIamCredential(params, token).catch(reason => {
this.observability.logging.log(`GetIamCredential failed. ${reason}`)
throw awsResponseErrorWrap(reason)
})
)

this.features.identityManagement.onInvalidateSsoToken(
async (params: InvalidateSsoTokenParams, token: CancellationToken) =>
await identityService.invalidateSsoToken(params, token).catch(reason => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { awsBuilderIdReservedName, SsoCache, SsoClientRegistration } from '../ss
import { IdentityService } from './identityService'
import { ProfileData, ProfileStore } from './profiles/profileService'
import { SsoTokenAutoRefresher } from './ssoTokenAutoRefresher'
import { createStubInstance, restore, spy, SinonSpy } from 'sinon'
import { createStubInstance, restore, spy, SinonSpy, stub } from 'sinon'
import {
AuthorizationFlowKind,
CancellationToken,
Expand All @@ -14,6 +14,9 @@ import {
import { SSOToken } from '@smithy/shared-ini-file-loader'
import { Logging, Telemetry } from '@aws/language-server-runtimes/server-interface'
import { Observability } from '@aws/lsp-core'
import { STSClient } from '@aws-sdk/client-sts'
import { IAMClient } from '@aws-sdk/client-iam'
import { IamProvider } from '../iam/iamProvider'

// eslint-disable-next-line
use(require('chai-as-promised'))
Expand All @@ -23,6 +26,7 @@ let sut: IdentityService
let profileStore: StubbedInstance<ProfileStore>
let ssoCache: StubbedInstance<SsoCache>
let autoRefresher: StubbedInstance<SsoTokenAutoRefresher>
let iamProvider: StubbedInstance<IamProvider>
let observability: StubbedInstance<Observability>
let authFlowFn: SinonSpy

Expand All @@ -33,11 +37,52 @@ describe('IdentityService', () => {
profiles: [
{
kinds: [ProfileKind.SsoTokenProfile],
name: 'my-profile',
name: 'my-sso-profile',
settings: {
sso_session: 'my-sso-session',
},
},
{
kinds: [ProfileKind.IamCredentialsProfile],
name: 'my-iam-profile',
settings: {
aws_access_key_id: 'my-access-key',
aws_secret_access_key: 'my-secret-key',
},
},
{
kinds: [ProfileKind.IamCredentialsProfile],
name: 'my-sts-profile',
settings: {
aws_access_key_id: 'my-access-key',
aws_secret_access_key: 'my-secret-key',
aws_session_token: 'my-session-token',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-role-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
},
},
{
kinds: [ProfileKind.IamSourceProfileProfile],
name: 'my-mfa-profile',
settings: {
role_arn: 'my-role-arn',
source_profile: 'my-iam-profile',
mfa_serial: 'my-device-arn',
},
},
{
kinds: [ProfileKind.IamCredentialProcessProfile],
name: 'my-process-profile',
settings: {
credential_process: 'my-process',
},
},
],
ssoSessions: [
{
Expand Down Expand Up @@ -70,6 +115,14 @@ describe('IdentityService', () => {
unwatch: undefined,
}) as StubbedInstance<SsoTokenAutoRefresher>

iamProvider = stubInterface<IamProvider>({
getCredential: Promise.resolve({
accessKeyId: 'my-access-key',
secretAccessKey: 'my-secret-key',
sessionToken: 'my-session-token',
}),
})

authFlowFn = spy(() =>
Promise.resolve({
accessToken: 'my-access-token',
Expand All @@ -85,10 +138,12 @@ describe('IdentityService', () => {
profileStore,
ssoCache,
autoRefresher,
iamProvider,
{
showUrl: _ => {},
showMessageRequest: _ => Promise.resolve({ title: 'client-response' }),
showProgress: _ => Promise.resolve(),
sendGetMfaCode: () => Promise.resolve({ code: 'mfa-code' }),
},
'My Client',
observability,
Expand All @@ -97,6 +152,24 @@ describe('IdentityService', () => {
[AuthorizationFlowKind.DeviceCode]: authFlowFn,
}
)

stub(STSClient.prototype, 'send').resolves({
Credentials: {
AccessKeyId: 'role-access-key',
SecretAccessKey: 'role-secret-key',
SessionToken: 'role-session-token',
Expiration: new Date('2024-09-25T18:09:20.455Z'),
},
AssumedRoleUser: {
Arn: 'role-arn',
AssumedRoleId: 'role-id',
},
Arn: 'role-arn',
})

stub(IAMClient.prototype, 'send').resolves({
EvaluationResults: [],
})
})

afterEach(() => {
Expand All @@ -122,7 +195,7 @@ describe('IdentityService', () => {
const actual = await sut.getSsoToken(
{
clientName: 'my-client',
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' },
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' },
},
CancellationToken.None
)
Expand All @@ -136,7 +209,7 @@ describe('IdentityService', () => {
await sut.getSsoToken(
{
clientName: 'my-client',
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' },
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' },
options: {
authorizationFlow: 'DeviceCode',
},
Expand All @@ -148,7 +221,7 @@ describe('IdentityService', () => {
await sut.getSsoToken(
{
clientName: 'my-client',
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' },
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' },
options: {
authorizationFlow: 'Pkce',
},
Expand Down Expand Up @@ -185,7 +258,7 @@ describe('IdentityService', () => {
const actual = await sut.getSsoToken(
{
clientName: 'my-client',
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' },
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' },
},
CancellationToken.None
)
Expand All @@ -203,7 +276,7 @@ describe('IdentityService', () => {
const actual = await sut.getSsoToken(
{
clientName: 'my-client',
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-profile' },
source: { kind: SsoTokenSourceKind.IamIdentityCenter, profileName: 'my-sso-profile' },
},
CancellationToken.None
)
Expand Down Expand Up @@ -255,6 +328,16 @@ describe('IdentityService', () => {
})
})

describe('getIamCredential', () => {
it('Can login with access key, secret key, and session token.', async () => {
const actual = await sut.getIamCredential({ profileName: 'my-sts-profile' }, CancellationToken.None)

expect(actual.credential.credentials.accessKeyId).to.equal('my-access-key')
expect(actual.credential.credentials.secretAccessKey).to.equal('my-secret-key')
expect(actual.credential.credentials.sessionToken).to.equal('my-session-token')
})
})

describe('invalidateSsoToken', () => {
it('removeToken removes on valid SSO session name', async () => {
await sut.invalidateSsoToken({ ssoTokenId: 'my-sso-session' }, CancellationToken.None)
Expand Down
Loading