Skip to content

Commit

Permalink
add support for copying role credentials to clipboard
Browse files Browse the repository at this point in the history
* also fixes a bug with toasts not showing sometimes
  • Loading branch information
abjrcode committed Nov 24, 2023
1 parent ad54a6d commit c3a8cf8
Show file tree
Hide file tree
Showing 10 changed files with 187 additions and 4 deletions.
28 changes: 28 additions & 0 deletions clients/awssso/aws-sso.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ type ListAccountsResponse struct {
Accounts []AwsAccount
}

type GetRoleCredentialsResponse struct {
AccessKeyId, SecretAccessKey, SessionToken string
Expiration int64
}

type AwsSsoOidcClient interface {
RegisterClient(ctx context.Context, friendlyClientName string) (*RegistrationResponse, error)

Expand All @@ -83,6 +88,8 @@ type AwsSsoOidcClient interface {
CreateToken(ctx context.Context, clientId, clientSecret, userCode, deviceCode string) (*GetTokenResponse, error)

ListAccounts(ctx context.Context, accessToken string) (*ListAccountsResponse, error)

GetRoleCredentials(ctx context.Context, accountId, roleName, accessToken string) (*GetRoleCredentialsResponse, error)
}

type awsSsoClientImpl struct {
Expand Down Expand Up @@ -226,3 +233,24 @@ func (c *awsSsoClientImpl) ListAccounts(ctx context.Context, accessToken string)
Accounts: accounts,
}, nil
}

func (c *awsSsoClientImpl) GetRoleCredentials(ctx context.Context, accountId, roleName, accessToken string) (*GetRoleCredentialsResponse, error) {
output, err := c.ssoClient.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{
AccountId: aws.String(accountId),
RoleName: aws.String(roleName),
AccessToken: aws.String(accessToken),
}, func(options *sso.Options) {
options.Region = ctx.Value(AwsRegion("awsRegion")).(string)
})

if err != nil {
return nil, err
}

return &GetRoleCredentialsResponse{
AccessKeyId: *output.RoleCredentials.AccessKeyId,
SecretAccessKey: *output.RoleCredentials.SecretAccessKey,
SessionToken: *output.RoleCredentials.SessionToken,
Expiration: output.RoleCredentials.Expiration,
}, nil
}
2 changes: 1 addition & 1 deletion frontend/src/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function Layout({ children }: { children: React.ReactNode }) {
const [toasts, setToasts] = useState<React.ReactElement[]>([])

function updateToasts(toasts: React.ReactElement[]) {
setToasts(toasts)
setToasts([...toasts])
}

useEffect(() => {
Expand Down
31 changes: 30 additions & 1 deletion frontend/src/routes/aws-iam-idc/aws-iam-idc-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from "react"
import { useFetcher, useNavigate, useRevalidator } from "react-router-dom"
import {
GetRoleCredentials,
MarkAsFavorite,
RefreshAccessToken,
UnmarkAsFavorite,
Expand All @@ -10,8 +11,12 @@ import {
AwsIamIdcCardDataError,
AwsIamIdcCardDataResult,
} from "./aws-iam-idc-card-data"
import { useWails } from "../../wails-provider/wails-context"
import { useToaster } from "../../toast-provider/toast-context"

export function AwsIamIdcCard({ instanceId }: { instanceId: string }) {
const wails = useWails()
const toaster = useToaster()
const navigate = useNavigate()
const fetcher = useFetcher()
const validator = useRevalidator()
Expand Down Expand Up @@ -61,6 +66,22 @@ export function AwsIamIdcCard({ instanceId }: { instanceId: string }) {
validator.revalidate()
}

async function copyCredentials(
instanceId: string,
accountId: string,
roleName: string,
) {
const output = await GetRoleCredentials(instanceId, accountId, roleName)
const credentialsProfile = `
[default]
aws_access_key_id = ${output.accessKeyId}
aws_secret_access_key = ${output.secretAccessKey}
aws_session_token = ${output.sessionToken}
`
await wails.runtime.ClipboardSetText(credentialsProfile)
toaster.showSuccess("Copied credentials to clipboard!")
}

const cardDataResult = fetcher.data as AwsIamIdcCardDataResult | undefined

if (cardDataResult === undefined) {
Expand Down Expand Up @@ -135,7 +156,15 @@ export function AwsIamIdcCard({ instanceId }: { instanceId: string }) {
className="inline-flex items-center gap-2">
<span>{role.roleName}</span>
<div className="inline-flex gap-2">
<button className="btn btn-accent btn-xs">
<button
onClick={() =>
copyCredentials(
instanceId,
account.accountId,
role.roleName,
)
}
className="btn btn-accent btn-xs">
copy credentials
</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/toast-provider/toast-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Toast } from "../components/toast"

export function ToastProvider({ children }: { children: React.ReactNode }) {
const MaxToasts = 3
const DefaultToastDuration = 3000
const DefaultToastDuration = 2000
const toastId = useRef(1)

const toastQueue = useRef<ToastSpec[]>([])
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/wails-provider/wails-context.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from "react"
import { ShowErrorDialog, ShowWarningDialog, CatchUnhandledError } from '../../wailsjs/go/main/AppController'
import { BrowserOpenURL } from '../../wailsjs/runtime'
import { BrowserOpenURL, ClipboardSetText } from '../../wailsjs/runtime'

export const ContextValue = {
runtime: {
BrowserOpenURL,
ClipboardSetText,
ShowWarningDialog,
ShowErrorDialog,
CatchUnhandledError,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ export function FinalizeSetup(arg1:string,arg2:string,arg3:string,arg4:string,ar

export function GetInstanceData(arg1:string,arg2:boolean):Promise<awsiamidc.AwsIdentityCenterCardData>;

export function GetRoleCredentials(arg1:string,arg2:string,arg3:string):Promise<awsiamidc.AwsIdentityCenterAccountRoleCredentials>;

export function Init(arg1:context.Context,arg2:logging.ErrorHandler):Promise<void>;

export function ListInstances():Promise<Array<string>>;
Expand Down
4 changes: 4 additions & 0 deletions frontend/wailsjs/go/awsiamidc/AwsIdentityCenterController.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export function GetInstanceData(arg1, arg2) {
return window['go']['awsiamidc']['AwsIdentityCenterController']['GetInstanceData'](arg1, arg2);
}

export function GetRoleCredentials(arg1, arg2, arg3) {
return window['go']['awsiamidc']['AwsIdentityCenterController']['GetRoleCredentials'](arg1, arg2, arg3);
}

export function Init(arg1, arg2) {
return window['go']['awsiamidc']['AwsIdentityCenterController']['Init'](arg1, arg2);
}
Expand Down
18 changes: 18 additions & 0 deletions frontend/wailsjs/go/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ export namespace awsiamidc {
}
}

export class AwsIdentityCenterAccountRoleCredentials {
accessKeyId: string;
secretAccessKey: string;
sessionToken: string;
expiration: number;

static createFrom(source: any = {}) {
return new AwsIdentityCenterAccountRoleCredentials(source);
}

constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.accessKeyId = source["accessKeyId"];
this.secretAccessKey = source["secretAccessKey"];
this.sessionToken = source["sessionToken"];
this.expiration = source["expiration"];
}
}
export class AwsIdentityCenterCardData {
instanceId: string;
enabled: boolean;
Expand Down
44 changes: 44 additions & 0 deletions providers/aws-iam-idc/aws-identity-center.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,13 @@ type AwsIdentityCenterAccount struct {
Roles []AwsIdentityCenterAccountRole `json:"roles"`
}

type AwsIdentityCenterAccountRoleCredentials struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
SessionToken string `json:"sessionToken"`
Expiration int64 `json:"expiration"`
}

type AwsIdentityCenterCardData struct {
InstanceId string `json:"instanceId"`
Enabled bool `json:"enabled"`
Expand Down Expand Up @@ -235,6 +242,43 @@ func (c *AwsIdentityCenterController) GetInstanceData(instanceId string, forceRe
}, nil
}

func (c *AwsIdentityCenterController) GetRoleCredentials(instanceId, accountId, roleName string) (*AwsIdentityCenterAccountRoleCredentials, error) {
row := c.db.QueryRowContext(c.ctx, "SELECT region, access_token_enc, access_token_created_at, access_token_expires_in, enc_key_id FROM aws_iam_idc_instances WHERE instance_id = ?", instanceId)

var region string
var accessTokenEnc string
var accessTokenCreatedAt int64
var accessTokenExpiresIn int64
var encKeyId string

if err := row.Scan(&region, &accessTokenEnc, &accessTokenCreatedAt, &accessTokenExpiresIn, &encKeyId); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrInstanceWasNotFound
}

c.errHandler.Catch(c.logger, err)
}

accessToken, err := c.encryptionService.Decrypt(accessTokenEnc, encKeyId)

c.errHandler.CatchWithMsg(c.logger, err, "failed to decrypt access token")

ctx := context.WithValue(c.ctx, awssso.AwsRegion("awsRegion"), region)
res, err := c.awsSsoClient.GetRoleCredentials(ctx, accountId, roleName, accessToken)

if err != nil {
c.logger.Error().Err(err).Msg("failed to get role credentials")
c.errHandler.Catch(c.logger, err)
}

return &AwsIdentityCenterAccountRoleCredentials{
AccessKeyId: res.AccessKeyId,
SecretAccessKey: res.SecretAccessKey,
SessionToken: res.SessionToken,
Expiration: res.Expiration,
}, nil
}

func (c *AwsIdentityCenterController) validateStartUrl(startUrl string) error {
_, err := url.ParseRequestURI(startUrl)

Expand Down
57 changes: 57 additions & 0 deletions providers/aws-iam-idc/aws-identity-center_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ func (m *mockAwsSsoOidcClient) ListAccounts(ctx context.Context, accessToken str
return res, args.Error(1)
}

func (m *mockAwsSsoOidcClient) GetRoleCredentials(ctx context.Context, accountId, roleName, accessToken string) (*awssso.GetRoleCredentialsResponse, error) {
args := m.Called(ctx, accountId, roleName, accessToken)
res, _ := args.Get(0).(*awssso.GetRoleCredentialsResponse)
return res, args.Error(1)
}

func initController(t *testing.T) (*AwsIdentityCenterController, *mockAwsSsoOidcClient, *testhelpers.MockClock) {
db, err := migrations.NewInMemoryMigratedDatabase(t, "aws-iam-idc-controller-tests.db")
require.NoError(t, err)
Expand Down Expand Up @@ -450,6 +456,57 @@ func TestGetInstanceData(t *testing.T) {
require.Equal(t, "test-account-name-2", instanceData.Accounts[1].AccountName)
}

func TestGetRoleCredentials(t *testing.T) {
startUrl := "https://test-start-url.aws-apps.com/start"
region := "eu-west-1"
label := "test_label"

roleName := "test-role-name"
accountId := "test-account-id"

controller, mockAws, mockTimeProvider := initController(t)

instanceId := simulateSuccessfulSetup(t, controller, mockAws, mockTimeProvider, startUrl, region, label)

mockTimeProvider.On("NowUnix").Return(3)

mockListAccountsRes := awssso.ListAccountsResponse{
Accounts: []awssso.AwsAccount{
{
AccountId: accountId,
AccountName: "test-account-name",
AccountEmail: "test-account-email",
Roles: []awssso.AwsAccountRole{
{
RoleName: roleName,
},
},
},
},
}

mockAws.On("ListAccounts", mock.Anything, mock.AnythingOfType("string")).Return(&mockListAccountsRes, nil)

mockGetRoleCredentialsRes := awssso.GetRoleCredentialsResponse{
AccessKeyId: "test-access-key-id",
SecretAccessKey: "test-secret-key",
SessionToken: "test-session-token",
Expiration: 100,
}

mockAws.On("GetRoleCredentials", mock.Anything, accountId, roleName, mock.AnythingOfType("string")).Return(&mockGetRoleCredentialsRes, nil)

roleCredentials, err := controller.GetRoleCredentials(instanceId, accountId, roleName)

require.NoError(t, err)
require.Equal(t, roleCredentials, &AwsIdentityCenterAccountRoleCredentials{
AccessKeyId: mockGetRoleCredentialsRes.AccessKeyId,
SecretAccessKey: mockGetRoleCredentialsRes.SecretAccessKey,
SessionToken: mockGetRoleCredentialsRes.SessionToken,
Expiration: mockGetRoleCredentialsRes.Expiration,
})
}

func TestGetInstance_AccessTokenExpired(t *testing.T) {
startUrl := "https://test-start-url.aws-apps.com/start"
region := "eu-west-1"
Expand Down

0 comments on commit c3a8cf8

Please sign in to comment.