Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Typer CLI implementation with cognito device authorization flow #93

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
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
66 changes: 66 additions & 0 deletions .github/workflows/publish-to-test-pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Publish Python 🐍 distribution 📦 to PyPI

on:
push:
paths:
- 'scripts/sbt-aws-cli/pyproject.toml'

jobs:
build:
name: Build distribution 📦
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"

- name: Install Poetry
run: pip install poetry

- name: Install dependencies
run: poetry install --no-dev
working-directory: scripts/sbt-aws-cli

- name: Build the distribution
run: poetry build
working-directory: scripts/sbt-aws-cli

- name: List files in dist/
run: ls -la scripts/sbt-aws-cli/dist/

- name: Store the distribution packages
uses: actions/upload-artifact@v3
with:
name: python-package-distributions
path: scripts/sbt-aws-cli/dist/

publish-to-pypi:
name: Publish Python 🐍 distribution 📦 to PyPI
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/sbt-aws-cli
permissions:
id-token: write

steps:
- name: Download all the dists
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: scripts/sbt-aws-cli/dist/

- name: List files in dist/ after download
run: ls -la scripts/sbt-aws-cli/dist/

- name: Publish distribution 📦 to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
packages-dir: scripts/sbt-aws-cli/dist/
repository-url: https://upload.pypi.org/legacy/
skip-existing: true
2 changes: 2 additions & 0 deletions .mise.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[tools]
python = {version='3.9', virtualenv='.venv'}
13 changes: 13 additions & 0 deletions API.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions docs/public/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,43 @@ The application plane emits this event upon completion of offboarding. Similar t
- **Built in the open with the community** - SBT strives to meet developers where they are. We encourage PRs, not forks
- **Guide builders and make it approachable** - SBT will provide rich documentation and examples from which the community can derive inspiration and reference

## Command Line Interface (CLI)

SBT includes a Command Line Interface (CLI) to simplify the management of your SaaS environment on AWS. The CLI is designed to interact with the Control Plane of SBT, allowing users to configure, manage tenants and users, and perform other administrative tasks seamlessly from the command line.

### Key Features

- **Tenant Management:** Create, retrieve, update, and delete tenants within your SaaS environment. The CLI provides straightforward commands to manage tenant data and lifecycle operations.
- **User Management:** Handle user accounts associated with your SaaS tenants. You can create, retrieve, update, and delete users, ensuring that your SaaS environment’s access controls are easily managed.

### Installation

To install the CLI, ensure that you have `pip` installed, and then install the `sbt-aws-cli` package using the following command

```bash
pip install sbt-aws-cli
```

The CLI comes with a built-in `--help` mode that provides detailed information about each command and its options. This feature is particularly useful if you are unfamiliar with the available commands and arguments or need a quick reference.

#### Using `--help`

To access the help mode for the CLI, simply append the `--help` flag to any command. For example:

```bash
sbt-aws-cli --help
Copy link
Contributor

Choose a reason for hiding this comment

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

Does the actual executable have to be called sbt-aws-cli? Couldn't we make it just sbt-aws or even sbt?

```
```bash
sbt-aws-cli <command> --help
```

To get started, use the following command to configure the CLI with your Control Plane stack details. The arguments should be directly copied from the stack outputs. Remember you can use the `--help` flag to view the expected arguments.
```bash
sbt-aws-cli configure <CONTROL_PLANE_STACK> <DEVICE_CLIENT_ID> <FQDN> <CONTROL_PLANE_API_ENDPOINT> <COGNITO_DOMAIN> ```

Once you have configured the CLI, you have many commands available at your disposable, which you can view using `sbt-aws-cli --help`


## Additional documentation and resources

### Tenant management
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: MIT-0
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//Reference to Commnon library
const common = require( __dirname + '/common.js');

const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider");
const { DynamoDB } = require("@aws-sdk/client-dynamodb");

//Reference to Crypto library for PKCE challenge
const crypto = require('crypto');

var cognitoidentityserviceprovider = new CognitoIdentityProvider({
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
apiVersions: {
cognitoidentityserviceprovider: '2016-04-18',
},
});
var dynamodb = new DynamoDB({
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
apiVersions: {
dynamodb: '2012-08-10',
},
});

//Function that processes "Authorize" by an authenticated end user for a valid user code
// client_id: Client ID of the client application that initiated the Authorization request
// device_code: Primary key of the "Authorized" Authorization request in the DynamoDB table
// callback: Callback function to return the message
// dynamodb: Pointer to the DynamoDB SDK request handler
function processAllow(client_id, device_code, callback) {

//Generating a code verifier and challenge for the PKCE protection of the OAuth2 flow
var code_verifier = common.randomString(32, 'aA#');
var hash = crypto.createHash('sha256').update(code_verifier).digest();
var code_challenge = common.base6UurlEncode(hash);

//Generating a random state for preventing against CSRF attacks
var state = common.randomString(32, 'aA#');

//Updating the Authorization request with PKCE code verifier and State
var DynamoDBParams = {
ExpressionAttributeNames: {
"#AuthZ_State": "AuthZ_State",
"#AuthZ_Verif": "AuthZ_Verifier_code",
},
ExpressionAttributeValues: {
":authz_state": {
S: state
},
":authz_verif": {
S: code_verifier
}
},
Key: {
"Device_code": {
S: device_code
}
},
ReturnValues: "ALL_NEW",
TableName: process.env.DYNAMODB_TABLE,
UpdateExpression: "SET #AuthZ_State = :authz_state, #AuthZ_Verif = :authz_verif"
};
dynamodb.updateItem(DynamoDBParams, function(err, data) {
if (err) {
//There was an error updating the Authorization request
console.log("Unable to set Authorization State and Verifier Code for Device Code = " + device_code);
common.returnHTMLError(400, "<H1>Error, can't update status</H1>", callback);
}
else {
//Update was successful so triggering a standard Authorization Code Grant flow with PKCE to Cognito using the inial Client Application's Client ID
var response = {
statusCode: 302,
headers: {"location": "https://" + process.env.CUP_DOMAIN + ".auth." + process.env.CUP_REGION + ".amazoncognito.com/oauth2/authorize?response_type=code&client_id=" + client_id + "&redirect_uri=" + encodeURIComponent("https://" + process.env.CODE_VERIFICATION_URI + "/callback") + "&state=" + state + "&scope=" + data.Attributes.Scope.S + "&code_challenge_method=S256&code_challenge=" + code_challenge + "&identity_provider=COGNITO"},
};
callback(null, response);
}
});
}

module.exports = Object.assign({ processAllow });
119 changes: 119 additions & 0 deletions resources/functions/auth-device-grant-token/Modules/callback-path.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: MIT-0
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this
* software and associated documentation files (the "Software"), to deal in the Software
* without restriction, including without limitation the rights to use, copy, modify,
* merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
//Reference to Commnon library
const common = require( __dirname + '/common.js');

const { CognitoIdentityProvider } = require("@aws-sdk/client-cognito-identity-provider");
const { DynamoDB } = require("@aws-sdk/client-dynamodb");

var cognitoidentityserviceprovider = new CognitoIdentityProvider({
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
apiVersions: {
cognitoidentityserviceprovider: '2016-04-18',
},
});
var dynamodb = new DynamoDB({
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
// The transformation for apiVersions is not implemented.
// Refer to UPGRADING.md on aws-sdk-js-v3 for changes needed.
// Please create/upvote feature request on aws-sdk-js-codemod for apiVersions.
apiVersions: {
dynamodb: '2012-08-10',
},
});

//Function that processes Cognito callback after end user Authorization Code grant flow with PKCE request
// event: Full event trapped by the Lambda function
// callback: Callback function to return the message
function processAuthZCodeCallback(event, callback) {
console.log("An Authorization Code has been sent back as Callback");

//Search the DynamoDb table for Authorization Request with provided State
var DynamoDBParams = {
ExpressionAttributeValues: {
":authz_state": {
S: event.queryStringParameters.state
}
},
KeyConditionExpression: "AuthZ_State = :authz_state",
IndexName: process.env.DYNAMODB_AUTHZ_STATE_INDEX,
TableName: process.env.DYNAMODB_TABLE
};
dynamodb.query(DynamoDBParams, function(err, data) {
if (err) {
//There was an error retrieving the Authorization request
console.log("Authorization State can't be retrieved: " + event.queryStringParameters.state);
console.log(err, err.stack);
common.returnHTMLError(400, "<H1>Error, can't update status</H1>", callback);
} else {
console.log("Successful response");
//If there is no result set
if (data.Items.length == 0) {
console.log("No AuthZ State was returned");
common.returnHTMLError(400, "<H1>Error, can't update status</H1>", callback);
//If Result Set is more than 1 entry
} else if (data.Items.length > 1) {
console.log("Too much AuthZ State were returned");
common.returnHTMLError(400, "<H1>Error, can't update status</H1>", callback);
} else {
console.log("AuthZ State was returned");
// Updating the Authorization request with the Code returned through the Authorization Code grant flow with PKCE callback
DynamoDBParams = {
ExpressionAttributeNames: {
"#AuthZ_code": "AuthZ_code"
},
ExpressionAttributeValues: {
":value": {
S: event.queryStringParameters.code
}
},
Key: {
"Device_code": {
S: data.Items[0].Device_code.S
}
},
ReturnValues: "ALL_NEW",
TableName: process.env.DYNAMODB_TABLE,
UpdateExpression: "SET #AuthZ_code = :value"
};
dynamodb.updateItem(DynamoDBParams, function(err, data) {
if (err) {
//Update was not successful
console.log("Unable to set state to Authorization Code for Device Code");
console.log(err, err.stack);
common.returnHTMLError(400, "<H1>Error, can't update status</H1>", callback);
}
else {
//Update was successful
console.log("AuthZ Code updated");
common.returnHTMLSuccess("<H1>Thanks, Device has been Authorized. You can return to your device.</H1>", callback);
}
});
}
}
});
}

module.exports = Object.assign({ processAuthZCodeCallback });
Loading
Loading