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
307 changes: 179 additions & 128 deletions packages/cli/README.md

Large diffs are not rendered by default.

35 changes: 32 additions & 3 deletions packages/cli/src/commands/apps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { flags } from '@oclif/command'

import { App } from '@smartthings/core-sdk'
import { App, AppType, AppClassification, AppListOptions } from '@smartthings/core-sdk'

import { ListingOutputAPICommand, TableFieldDefinition } from '@smartthings/cli-lib'

Expand Down Expand Up @@ -40,6 +40,19 @@ export default class AppsList extends ListingOutputAPICommand<App, App> {

static flags = {
...ListingOutputAPICommand.flags,
type: flags.string({
description: 'filter results by appType, WEBHOOK_SMART_APP, LAMBDA_SMART_APP, API_ONLY',
multiple: false,
}),
classification: flags.string({
description: 'filter results by one or more classifications, AUTOMATION, SERVICE, DEVICE, CONNECTED_SERVICE',
multiple: true,
}),
// TODO -- uncomment when implemented
// tag: flags.string({
// description: 'filter results by one or more tags, e.g. --tag=industry:energy',
// multiple: true,
// }),
verbose: flags.boolean({
description: 'include URLs and ARNs in table output',
char: 'v',
Expand All @@ -66,8 +79,24 @@ export default class AppsList extends ListingOutputAPICommand<App, App> {
}

const listApps = async (): Promise<App[]> => {
const appListOptions: AppListOptions = {}
if (flags.type) {
appListOptions.appType = AppType[flags.type as keyof typeof AppType]
}
if (flags.classification) {
appListOptions.classification = flags.classification.map(it => AppClassification[it as keyof typeof AppClassification])
}
// TODO -- uncomment when implemented
// if (flags.tag) {
// appListOptions.tag = flags.tag.reduce((map: {[key: string]: string}, it) => {
// const pos = it.indexOf(':')
// map[it.slice(0, pos)] = it.slice(pos+1)
// return map
// }, {})
// }
this.log(JSON.stringify(appListOptions, null, 2))
if (flags.verbose) {
return this.client.apps.list().then(list => {
return this.client.apps.list(appListOptions).then(list => {
const objects = list.map(it => {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return this.client.apps.get((it.appId)!) // TODO appId should not be optional
Expand All @@ -86,7 +115,7 @@ export default class AppsList extends ListingOutputAPICommand<App, App> {
})
})
}
return this.client.apps.list()
return this.client.apps.list(appListOptions)
}

this.processNormally(args.id, listApps, id => this.client.apps.get(id))
Expand Down
28 changes: 15 additions & 13 deletions packages/cli/src/commands/apps/authorize.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import { flags } from '@oclif/command'
import { APICommand } from '@smartthings/cli-lib'
import {addPermission} from '../../lib/aws-utils'
import { addPermission } from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


interface AuthorizeResponse {
error?: string
functions?: { [key: string]: string }
}

export default class AppAuthorize extends APICommand {
static description = 'authorize calls to your AWS Lambda function from SmartThings'

static flags = APICommand.flags
static flags = {
...APICommand.flags,
...lambdaAuthFlags,
}

static args = [{
name: 'arn',
description: 'the ARN of the AWS Lambda function',
required: true,
}]
static args = [
{
name: 'arn',
description: 'the ARN of the AWS Lambda function',
required: true,
},
]

static examples = [
'$ smartthings apps:authorize arn:aws:lambda:us-east-1:1234567890:function:your-test-app',
Expand All @@ -34,7 +36,7 @@ export default class AppAuthorize extends APICommand {
const { args, argv, flags } = this.parse(AppAuthorize)
await super.setup(args, argv, flags)

addPermission(args.arn).then(async (message) => {
addPermission(args.arn, flags.principal, flags['statement-id']).then(async (message) => {
this.log(message)
}).catch(err => {
this.log(`caught error ${err}`)
Expand Down
7 changes: 5 additions & 2 deletions packages/cli/src/commands/apps/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { InputOutputAPICommand } from '@smartthings/cli-lib'

import { tableFieldDefinitions } from '../apps'
import { addPermission } from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


export default class AppCreateCommand extends InputOutputAPICommand<AppRequest, AppCreationResponse> {
Expand All @@ -15,7 +16,9 @@ export default class AppCreateCommand extends InputOutputAPICommand<AppRequest,
...InputOutputAPICommand.flags,
authorize: flags.boolean({
description: 'authorize Lambda functions to be called by SmartThings',
})}
}),
...lambdaAuthFlags,
}

protected buildTableOutput(data: AppCreationResponse): string {
return this.tableGenerator.buildTableFromItem(data.app, tableFieldDefinitions)
Expand All @@ -30,7 +33,7 @@ export default class AppCreateCommand extends InputOutputAPICommand<AppRequest,
if (data.lambdaSmartApp) {
if (data.lambdaSmartApp.functions) {
const requests = data.lambdaSmartApp.functions.map((it) => {
return addPermission(it)
return addPermission(it, flags.principal, flags['statement-id'])
})
await Promise.all(requests)
}
Expand Down
14 changes: 10 additions & 4 deletions packages/cli/src/commands/apps/register.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { App } from '@smartthings/core-sdk'
import _ from 'lodash'

import {App, AppType} from '@smartthings/core-sdk'

import {SelectingAPICommand} from '@smartthings/cli-lib'


export default class AppRegisterCommand extends SelectingAPICommand<App> {
static description = 'register the app'
static description = 'Send request to app target URL to confirm existence and authorize lifecycle events'

static flags = SelectingAPICommand.flags

Expand All @@ -15,14 +17,18 @@ export default class AppRegisterCommand extends SelectingAPICommand<App> {

primaryKeyName = 'appId'
sortKeyName = 'displayName'
listTableFieldDefinitions = ['displayName', 'appType', 'appId']

async run(): Promise<void> {
const { args, argv, flags } = this.parse(AppRegisterCommand)
await super.setup(args, argv, flags)

this.processNormally(args.id,
async () => await this.client.apps.list(),
async () => _.flatten(await Promise.all([
this.client.apps.list({appType: AppType.WEBHOOK_SMART_APP}),
this.client.apps.list({appType: AppType.API_ONLY}),
])),
async (id) => { await this.client.apps.register(id) },
'app {{id}} registered')
'Registration request sent to app {{id}}. Check server log for confirmation URL')
}
}
7 changes: 5 additions & 2 deletions packages/cli/src/commands/apps/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { SelectingInputOutputAPICommand } from '@smartthings/cli-lib'

import { tableFieldDefinitions } from '../apps'
import { addPermission } from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


export default class AppUpdateCommand extends SelectingInputOutputAPICommand<AppRequest, App, App> {
Expand All @@ -15,7 +16,9 @@ export default class AppUpdateCommand extends SelectingInputOutputAPICommand<App
...SelectingInputOutputAPICommand.flags,
authorize: flags.boolean({
description: 'authorize Lambda functions to be called by SmartThings',
})}
}),
...lambdaAuthFlags,
}

static args = [{
name: 'id',
Expand All @@ -38,7 +41,7 @@ export default class AppUpdateCommand extends SelectingInputOutputAPICommand<App
if (data.lambdaSmartApp) {
if (data.lambdaSmartApp.functions) {
const requests = data.lambdaSmartApp.functions.map((it) => {
return addPermission(it)
return addPermission(it, flags.principal, flags['statement-id'])
})
await Promise.all(requests)
}
Expand Down
44 changes: 44 additions & 0 deletions packages/cli/src/commands/schema/authorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { flags } from '@oclif/command'
import { APICommand } from '@smartthings/cli-lib'
import { addPermission } from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


export default class SchemaAppAuthorize extends APICommand {
static description = 'authorize calls to your ST Schema Lambda function from SmartThings'

static flags = {
...APICommand.flags,
...lambdaAuthFlags,
}
static args = [
{
name: 'arn',
description: 'the ARN of the AWS Lambda function',
required: true,
},
]

static examples = [
'$ smartthings apps:authorize arn:aws:lambda:us-east-1:1234567890:function:your-test-app',
'',
'Note that this command is the same as running the following with the AWS CLI:',
'',
'$ aws lambda add-permission --region us-east-1 \\',
' --function-name arn:aws:lambda:us-east-1:1234567890:function:your-test-app \\',
' --statement-id smartthings --principal 148790070172 --action lambda:InvokeFunction',
'',
'It requires your machine to be configured to run the AWS CLI',
]
Comment on lines +22 to +32
Copy link
Collaborator

Choose a reason for hiding this comment

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

I love that you're including these examples. I need to do that more often.


async run(): Promise<void> {
const { args, argv, flags } = this.parse(SchemaAppAuthorize)
await super.setup(args, argv, flags)

addPermission(args.arn, flags.principal, flags['statement-id']).then(async (message) => {
this.log(message)
}).catch(err => {
this.log(`caught error ${err}`)
})
}
}
10 changes: 6 additions & 4 deletions packages/cli/src/commands/schema/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SchemaAppRequest, SchemaCreateResponse } from '@smartthings/core-sdk'
import { InputOutputAPICommand } from '@smartthings/cli-lib'

import { addSchemaPermission } from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


export default class SchemaAppCreateCommand extends InputOutputAPICommand<SchemaAppRequest, SchemaCreateResponse> {
Expand All @@ -15,6 +16,7 @@ export default class SchemaAppCreateCommand extends InputOutputAPICommand<Schema
authorize: flags.boolean({
description: 'authorize connector\'s Lambda functions to be called by SmartThings',
}),
...lambdaAuthFlags,
}

protected tableFieldDefinitions = ['endpointAppId', 'stClientId', 'stClientSecret']
Expand All @@ -27,16 +29,16 @@ export default class SchemaAppCreateCommand extends InputOutputAPICommand<Schema
if (flags.authorize) {
if (data.hostingType === 'lambda') {
if (data.lambdaArn) {
addSchemaPermission(data.lambdaArn)
addSchemaPermission(data.lambdaArn, flags.principal, flags['statement-id'])
}
if (data.lambdaArnAP) {
addSchemaPermission(data.lambdaArnAP)
addSchemaPermission(data.lambdaArnAP, flags.principal, flags['statement-id'])
}
if (data.lambdaArnCN) {
addSchemaPermission(data.lambdaArnCN)
addSchemaPermission(data.lambdaArnCN, flags.principal, flags['statement-id'])
}
if (data.lambdaArnEU) {
addSchemaPermission(data.lambdaArnEU)
addSchemaPermission(data.lambdaArnEU, flags.principal, flags['statement-id'])
}
} else {
this.logger.error('Authorization is not applicable to web-hook schema connectors')
Expand Down
16 changes: 9 additions & 7 deletions packages/cli/src/commands/schema/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SchemaApp, SchemaAppRequest, Status } from '@smartthings/core-sdk'
import { SelectingInputOutputAPICommand } from '@smartthings/cli-lib'

import { addSchemaPermission} from '../../lib/aws-utils'
import { lambdaAuthFlags } from '../../lib/common-flags'


export default class SchemaUpdateCommand extends SelectingInputOutputAPICommand<SchemaAppRequest, Status, SchemaApp> {
Expand All @@ -15,43 +16,44 @@ export default class SchemaUpdateCommand extends SelectingInputOutputAPICommand<
authorize: flags.boolean({
description: 'authorize Lambda functions to be called by SmartThings',
}),
...lambdaAuthFlags,
}

static args = [{
name: 'id',
description: 'the app id',
required: true,
}]

primaryKeyName = 'endpointAppId'
sortKeyName = 'appName'
listTableFieldDefinitions = ['appName', 'endpointAppId', 'hostingType']

async run(): Promise<void> {
const { args, argv, flags } = this.parse(SchemaUpdateCommand)
await super.setup(args, argv, flags)

this.processNormally(args.id,
() => { return this.client.apps.list() },
() => { return this.client.schema.list() },
async (id, data) => {
if (flags.authorize) {
if (data.hostingType === 'lambda') {
if (data.lambdaArn) {
await addSchemaPermission(data.lambdaArn)
await addSchemaPermission(data.lambdaArn, flags.principal, flags['statement-id'])
}
if (data.lambdaArnAP) {
await addSchemaPermission(data.lambdaArnAP)
await addSchemaPermission(data.lambdaArnAP, flags.principal, flags['statement-id'])
}
if (data.lambdaArnCN) {
await addSchemaPermission(data.lambdaArnCN)
await addSchemaPermission(data.lambdaArnCN, flags.principal, flags['statement-id'])
}
if (data.lambdaArnEU) {
await addSchemaPermission(data.lambdaArnEU)
await addSchemaPermission(data.lambdaArnEU, flags.principal, flags['statement-id'])
}
} else {
throw Error('Authorization is not applicable to web-hook schema connectors')
}
}
return this.client.schema.update(args.id, data)
return this.client.schema.update(id, data)
})
}
}
10 changes: 5 additions & 5 deletions packages/cli/src/lib/aws-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import AWS from 'aws-sdk'
import {AddPermissionRequest} from 'aws-sdk/clients/lambda'


export async function addPermission(arn: string, principal = '906037444270'): Promise<string> {
export async function addPermission(arn: string, principal = '906037444270', statementId = 'smartthings'): Promise<string> {
const segs = arn.split(':')
if (segs.length < 7) {
return 'Invalid Lambda ARN'
Expand All @@ -16,8 +16,8 @@ export async function addPermission(arn: string, principal = '906037444270'): Pr
const params: AddPermissionRequest = {
Action: 'lambda:InvokeFunction',
FunctionName: arn,
Principal: principal, // TODO environment dependent
StatementId: 'smartthings',
Principal: principal,
StatementId: statementId,
}

await lambda.addPermission(params).promise()
Expand All @@ -30,6 +30,6 @@ export async function addPermission(arn: string, principal = '906037444270'): Pr
}
}

export function addSchemaPermission(arn: string): Promise<string> {
return addPermission(arn, '148790070172')
export function addSchemaPermission(arn: string, principal = '148790070172', statementId = 'smartthings'): Promise<string> {
return addPermission(arn, principal, statementId)
}
11 changes: 11 additions & 0 deletions packages/cli/src/lib/common-flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { flags } from '@oclif/command'


export const lambdaAuthFlags = {
principal: flags.string({
description: 'use this principal instead of the default when authorizing lambda functions',
}),
'statement-id': flags.string({
description: 'use this statement id instead of the default when authorizing lambda functions',
}),
}