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

Support set up of SSO through commandline #151

Merged
merged 10 commits into from
Apr 16, 2024
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ Regardless of the method you use, if you don't already have one, the first step

SSO authentication uses the new [AWS Identity Center](https://us-east-1.console.aws.amazon.com/singlesignon/home). SSO is integrated with the `aws` CLI tool and is the method by which we can create time-limited session credentials.

1. Log into your root account (or super-admin account if you have one).
2. Click on the account name in the upper right-hand corner and select 'Security credentials'.
3. Under the 'Access keys' section, select 'Create access key'. You may get a warning; if you do, acknowledge and click next.
4. Execute:
```
aws configure
```
And copy+paste the access key ID and secret as prompted.




#### Set up the CloudsiteManager policy

1. Log into your AWS root account in the [AWS console](https://aws.amazon.com). Refer to [this section](#sign-up-for-your-aws-root-account) if you need to create a root account.
Expand Down Expand Up @@ -337,13 +349,31 @@ Command group for managing the Cloudsite CLI configuration.

##### Subcommands

- [`initialize`](#cloudsite-configuration-initialize): Runs the initialization wizard and updates all options.
- [`setup-local`](#cloudsite-configuration-setup-local): Runs the local setup wizard and updates all options. This should be used after the SSO account has been created (see 'cloudsite configuration setup-sso').
- [`setup-sso`](#cloudsite-configuration-setup-sso): Runs the SSO wizard and sets up the SSO user authentication in the IAM Identity Center.
- [`show`](#cloudsite-configuration-show): Displays the current configuration.

<span id="cloudsite-configuration-initialize"></span>
###### `cloudsite configuration initialize`
<span id="cloudsite-configuration-setup-local"></span>
###### `cloudsite configuration setup-local`

Runs the initialization wizard and updates all options.
Runs the local setup wizard and updates all options. This should be used after the SSO account has been created (see 'cloudsite configuration setup-sso').

<span id="cloudsite-configuration-setup-sso"></span>
###### `cloudsite configuration setup-sso <options>`

Runs the SSO wizard and sets up the SSO user authentication in the IAM Identity Center.

___`setup-sso` options___

|Option|Description|
|------|------|
|`--group-name`|The name of the group to create or reference. This group will be associated with the permission set and user.|
|`--instance-name`|The name to assign to the newly created identity center, if needed.|
|`--instance-region`|The region in which to set up the identity center if no identity center currently set up. Defaults to 'us-east-1'.|
|`--policy-name`|The name of the policy and permission set to create or reference.|
|`--sso-profile`|The name of the local SSO profile to create.|
|`--user-email`|The primary email to associate with the user.|
|`--user-name`|The name of the user account to create or reference.|

<span id="cloudsite-configuration-show"></span>
###### `cloudsite configuration show`
Expand Down
5,690 changes: 3,823 additions & 1,867 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@
},
"homepage": "https://github.com/liquid-labs/cloudsite#readme",
"dependencies": {
"@aws-sdk/client-account": "^3.554.0",
"@aws-sdk/client-acm": "^3.515.0",
"@aws-sdk/client-cloudformation": "^3.521.0",
"@aws-sdk/client-cloudfront": "^3.523.0",
"@aws-sdk/client-cost-explorer": "^3.535.0",
"@aws-sdk/client-iam": "^3.554.0",
"@aws-sdk/client-identitystore": "^3.554.0",
"@aws-sdk/client-lambda": "^3.533.0",
"@aws-sdk/client-route-53": "^3.523.0",
"@aws-sdk/client-s3": "^3.536.0",
"@aws-sdk/client-sso-admin": "^3.554.0",
"@aws-sdk/client-sts": "^3.521.0",
"@aws-sdk/credential-providers": "^3.515.0",
"@aws-sdk/signature-v4-crt": "^3.535.0",
Expand All @@ -74,6 +78,7 @@
"command-line-args": "^5.2.1",
"command-line-documentation": "^1.0.0-alpha.3",
"command-line-usage": "^7.0.1",
"config-ini-parser": "^1.6.1",
"json-to-plain-text": "^1.1.4",
"lodash": "^4.17.21",
"magic-print": "^1.0.0-alpha.4"
Expand Down
38 changes: 36 additions & 2 deletions src/cli/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,46 @@ const cliSpec = {
],
commands : [
{
name : 'initialize',
description : 'Runs the initialization wizard and updates all options.'
name : 'setup-local',
description : "Runs the local setup wizard and updates all options. This should be used after the SSO account has been created (see 'cloudsite configuration setup-sso')."
},
{
name : 'show',
description : 'Displays the current configuration.'
},
{
name : 'setup-sso',
description : 'Runs the SSO wizard and sets up the SSO user authentication in the IAM Identity Center.',
arguments : [
{
name : 'group-name',
description : 'The name of the group to create or reference. This group will be associated with the permission set and user.'
},
{
name : 'instance-name',
description : 'The name to assign to the newly created identity center, if needed.'
},
{
name : 'instance-region',
description : "The region in which to set up the identity center if no identity center currently set up. Defaults to 'us-east-1'."
},
{
name : 'policy-name',
description : 'The name of the policy and permission set to create or reference.'
},
{
name : 'sso-profile',
description : 'The name of the local SSO profile to create.'
},
{
name : 'user-email',
description : 'The primary email to associate with the user.'
},
{
name : 'user-name',
description : 'The name of the user account to create or reference.'
}
]
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions src/cli/lib/check-authentication.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { ListBucketsCommand, S3Client } from '@aws-sdk/client-s3'

import { getCredentials } from '../../lib/actions/lib/get-credentials' // move to shared

const checkAuthentication = async ({ db }) => {
const credentials = getCredentials(db.account.settings)
const checkAuthentication = async ({ db } = {}) => {
const credentials = getCredentials(db?.account?.settings) // passes in 'sso-profile'

const s3Client = new S3Client({ credentials })
const listBucketsCommand = new ListBucketsCommand({})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@ import { Questioner } from 'question-and-answer'

import { progressLogger } from '../../../lib/shared/progress-logger'

const handleConfigurationInitialize = async ({ db }) => {
const handleConfigurationSetupLocal = async ({ db }) => {
const defaultSSOProfile = db.account.localSettings['sso-profile']

const interrogationBundle = {
actions : [
{
prompt : "Which local AWS SSO profile should be used for authentication? Hit '<enter>' to use the default profile.",
parameter : 'sso-profile'
prompt : "Which local AWS SSO profile should be used for authentication? Enter '-' to use the configured 'default' account.",
parameter : 'sso-profile',
default : defaultSSOProfile
},
{
prompt : 'Which default format would you prefer?',
Expand All @@ -28,9 +31,9 @@ const handleConfigurationInitialize = async ({ db }) => {
const questioner = new Questioner({ interrogationBundle, output : progressLogger })
await questioner.question()

db.account.settings = questioner.values
db.account.localSettings = questioner.values

return { success : true, userMessage : 'Settings updated.' }
}

export { handleConfigurationInitialize }
export { handleConfigurationSetupLocal }
122 changes: 122 additions & 0 deletions src/cli/lib/configuration/handle-configuration-setup-sso.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as fs from 'node:fs/promises'
import * as fsPath from 'node:path'

import commandLineArgs from 'command-line-args'
import { ConfigIniParser } from 'config-ini-parser'
import { Questioner } from 'question-and-answer'

import { checkAuthentication } from '../check-authentication'
import { cliSpec } from '../../constants'
import { progressLogger } from '../../../lib/shared/progress-logger'
import { setupSSO } from '../../../lib/actions/setup-sso'

const handleConfigurationSetupSSO = async ({ argv, db }) => {
const ssoSetupOptionsSpec = cliSpec
.commands.find(({ name }) => name === 'configuration')
.commands.find(({ name }) => name === 'setup-sso')
.arguments || []
const ssoSetupOptions = commandLineArgs(ssoSetupOptionsSpec, { argv })
let {
'group-name': groupName,
'instance-name': instanceName,
'instance-region': instanceRegion,
'policy-name': policyName,
'sso-profile': ssoProfile,
'user-email': userEmail,
'user-name': userName
} = ssoSetupOptions

try {
await checkAuthentication()
} catch (e) {
let exitCode
if (e.name === 'CredentialsProviderError') {
progressLogger.write('<error>No credentials were found.<rst> Refer to cloudsite home instructions on how to configure API credentials for the SSO setup process.\n')
exitCode = 2
process.exit(exitCode) // eslint-disable-line no-process-exit
} else {
throw (e)
}
}

// TODO: process argv for options
const interrogationBundle = {
actions : [
{
prompt : 'Enter the preferred name for the identity store instance:',
parameter : 'instance-name'
},
{
prompt : 'Enter the preferred AWS region for the identity store instance:',
default : 'us-east-1',
parameter : 'instance-region'
},
{
prompt : 'Enter the name of the custom policy to create or reference:',
default : 'CloudsiteManager',
parameter : 'policy-name'
},
{
prompt : 'Enter the name of the Cloudsite managers group to create or reference:',
default : 'Cloudsite managers',
parameter : 'group-name'
},
{
prompt : 'Enter the name of the Cloudsite manager user account to create or reference:',
default : 'cloudsite-manager',
parameter : 'user-name'
},
{
prompt : 'Enter the email of the Cloudsite manager user:',
parameter : 'user-email'
},
{
prompt : "Enter the name of the SSO profile to create or reference (enter '-' to set the default profile):",
default : 'cloudsite-manager',
parameter : 'sso-profile'
},
{ review : 'questions' }
]
}

const questioner = new Questioner({ initialParameters : ssoSetupOptions, interrogationBundle, output : progressLogger })
await questioner.question();

({
'group-name': groupName,
'instance-name': instanceName,
'instance-region': instanceRegion,
'policy-name': policyName,
'sso-profile': ssoProfile,
'user-email': userEmail,
'user-name': userName
} = questioner.values)

const { ssoStartURL, ssoRegion } =
await setupSSO({ db, groupName, instanceName, instanceRegion, policyName, userEmail, userName })

progressLogger.write('Configuring local SSO profile...')
const configPath = fsPath.join(process.env.HOME, '.aws', 'config')
const configContents = await fs.readFile(configPath, { encoding : 'utf8' })
const config = new ConfigIniParser()
config.parse(configContents)
if (!config.isHaveSection('profile ' + ssoProfile)) {
config.addSection('profile ' + ssoProfile)
}
config.set('profile ' + ssoProfile, 'sso-session', ssoProfile)
const { accountID } = db.account
config.set('profile ' + ssoProfile, 'sso-account-id', accountID)
config.set('profile ' + ssoProfile, 'sso-role-name', policyName)
if (!config.isHaveSection('sso-session ' + ssoProfile)) {
config.addSection('sso-session ' + ssoProfile)
}
config.set('sso-session ' + ssoProfile, 'sso-start-url', ssoStartURL)
config.set('sso-session ' + ssoProfile, 'sso-region', ssoRegion)
config.set('sso-session ' + ssoProfile, 'sso-registration-scopes', 'sso:account:access')

await fs.writeFile(configPath, config.stringify())

return { success : true, userMessage : 'Settings updated.' }
}

export { handleConfigurationSetupSSO }
4 changes: 2 additions & 2 deletions src/cli/lib/configuration/handle-configuration-show.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const handleConfigurationShow = async ({ /* argv, */ db }) => {
const showConfigurationOptionsSpec = showConfigurationCLISpec.arguments
const showConfigurationOptions = commandLineArgs(showConfigurationOptionsSpec, { argv }) */

const accountSettings = db.account.settings || {}
return { success : true, data : accountSettings }
const localSettings = db.account.localSettings || {}
return { success : true, data : localSettings }
}

export { handleConfigurationShow }
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import { Questioner } from 'question-and-answer'

import { handleConfigurationInitialize } from '../handle-configuration-initialize'
import { handleConfigurationSetupLocal } from '../handle-configuration-setup-local'

jest.mock('node:fs/promises')

describe('handleConfigurationInitialize', () => {
const questionValues = { ssoProfile : 'some-profile' }
describe('handleConfigurationSetupLocal', () => {
const questionValues = { 'sso-profile' : 'some-profile' }
let db

afterEach(jest.clearAllMocks)

beforeAll(async () => {
jest.spyOn(Questioner.prototype, 'question').mockResolvedValue(undefined)
jest.spyOn(Questioner.prototype, 'values', 'get').mockReturnValue(questionValues)
db = { account : { settings : {} } }
await handleConfigurationInitialize({ db })
db = { account : { localSettings : {} } }
await handleConfigurationSetupLocal({ db })
})

test('questions the user', async () => expect(Questioner.prototype.question).toHaveBeenCalledTimes(1))

test('updates the account settings', async () =>
expect(db.account.settings).toEqual({ ssoProfile : questionValues.ssoProfile }))
expect(db.account.localSettings).toEqual({ 'sso-profile' : questionValues['sso-profile'] }))
})
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('handleConfigurationShow', () => {
})

test('prints the file contents', async () => {
const { data } = await handleConfigurationShow({ argv : [], db : { account : { settings : settingsVal } } })
const { data } = await handleConfigurationShow({ argv : [], db : { account : { localSettings : settingsVal } } })
expect(data).toBe(settingsVal)
})
})
9 changes: 6 additions & 3 deletions src/cli/lib/handle-configuration.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import commandLineArgs from 'command-line-args'

import { cliSpec } from '../constants'
import { getOptionsSpec } from './get-options-spec'
import { handleConfigurationInitialize } from './configuration/handle-configuration-initialize'
import { handleConfigurationSetupLocal } from './configuration/handle-configuration-setup-local'
import { handleConfigurationShow } from './configuration/handle-configuration-show'
import { handleConfigurationSetupSSO } from './configuration/handle-configuration-setup-sso'

const handleConfiguration = async ({ argv, db }) => {
const configurationOptionsSpec = getOptionsSpec({ cliSpec, name : 'configuration' })
Expand All @@ -12,10 +13,12 @@ const handleConfiguration = async ({ argv, db }) => {
argv = configurationOptions._unknown || []

switch (subcommand) {
case 'initialize':
return await handleConfigurationInitialize({ argv, db })
case 'setup-local':
return await handleConfigurationSetupLocal({ argv, db })
case 'show':
return await handleConfigurationShow({ argv, db })
case 'setup-sso':
return await handleConfigurationSetupSSO({ argv, db })
default:
throw new Error('Unknown configuration command: ' + subcommand)
}
Expand Down
Loading