From c4e95b4cb4c8f471b10d07fa3ee93a674f41bc8b Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 19 Aug 2024 06:04:00 +0100 Subject: [PATCH 01/13] macOS Support (#40) ## New features - Automatic HTTPS prefix on Okta Domain during Okta Setup - Automatic copy RDS pwd to clipboard (#36) - MacOS Support (#35) - New Open Logs Command (#39) ## Bug Fixes - Error: Unable to assume AWS role bug (#29) - Error message for incorrect configuration is not a user-friendly bug (#38) - Unable to retrieve OKTA apps automatically bug (#37) - Improves Push MFA, challenge message --- .github/ISSUE_TEMPLATE/bug_report.md | 32 +++-- .github/ISSUE_TEMPLATE/feature_request.md | 14 +- .github/workflows/build.yml | 5 +- .github/workflows/release.yml | 66 +++++++-- .vscode/launch.json | 21 +++ .vscode/tasks.json | 38 ++++- Directory.Build.props | 9 +- Ellosoft.AwsCredentialsManager.sln | 9 ++ ...soft.AwsCredentialsManager.sln.DotSettings | 1 + README.md | 21 ++- config/sample-config.toml | 57 ++++++++ config/sample-config.yml | 62 ++++++++ .../Commands/CommandException.cs | 7 +- .../Commands/Config/OpenAwsConfig.cs | 2 +- .../Commands/Config/OpenConfig.cs | 2 +- .../Credentials/CreateCredentialsProfile.cs | 64 ++++----- .../Commands/Credentials/GetCredentials.cs | 4 +- .../Credentials/ListCredentialsProfiles.cs | 1 - .../Commands/Okta/SetupOkta.cs | 19 +-- .../Commands/RDS/GetRdsPassword.cs | 52 +++---- .../Commands/RDS/ListRdsProfiles.cs | 1 - .../Commands/Utils/OpenLogs.cs | 22 +++ .../Ellosoft.AwsCredentialsManager.csproj | 28 ++-- .../GlobalSuppressions.cs | 5 +- .../GlobalUsings.cs | 2 + .../Infrastructure/Cli/TypeRegistrar.cs | 1 - .../Infrastructure/FileDownloadService.cs | 9 +- .../Logging/ConfigInterceptor.cs | 16 +++ .../Infrastructure/Logging/LogRegistration.cs | 3 +- .../Infrastructure/SemanticVersion.cs | 2 - .../Infrastructure/Upgrade/UpgradeService.cs | 75 +++------- src/Ellosoft.AwsCredentialsManager/Program.cs | 15 +- .../ServiceRegistration.cs | 61 ++++++-- .../Services/AWS/AwsCredentialsService.cs | 77 ++++++---- .../Services/AWS/AwsSamlService.cs | 23 +-- .../AWS/Interactive/AwsOktaSessionManager.cs | 25 ++-- .../Services/AWS/RDSTokenGenerator.cs | 13 +- .../Services/AppDataDirectory.cs | 18 ++- .../Services/AppMetadata.cs | 3 + .../Services/Configuration/ConfigManager.cs | 12 +- .../Services/Configuration/ConfigReader.cs | 25 +++- .../Services/Configuration/ConfigWriter.cs | 18 ++- .../Interactive/CredentialsManager.cs | 12 +- .../Interactive/EnvironmentManager.cs | 26 ++-- .../Configuration/Models/AppConfig.cs | 6 +- .../Configuration/Models/ToolConfiguration.cs | 17 +++ .../Configuration/UserCredentialsManager.cs | 58 -------- .../Services/Encryption/DataProtection.cs | 24 ---- .../Encryption/WindowsDataProtection.cs | 14 -- .../Services/IO/FileManager.cs | 38 ++--- .../Okta/Interactive/OktaLoginService.cs | 91 ++++-------- .../Okta/MfaHandlers/MfaHandlerProvider.cs | 7 +- .../Okta/MfaHandlers/OktaPushFactorHandler.cs | 33 +++-- .../Okta/Models/AuthenticationResult.cs | 3 + .../Models/HttpModels/CreateSessionRequest.cs | 5 + .../Models/HttpModels/CreateSessionResult.cs | 16 +++ .../HttpModels/OktaSourceGenerationContext.cs | 2 + .../Okta/OktaClassicAccessTokenProvider.cs | 135 ------------------ .../Services/Okta/OktaClassicAuthenticator.cs | 52 +++++-- .../Services/Okta/OktaHttpClientFactory.cs | 10 +- .../Services/Okta/OktaSamlService.cs | 7 +- .../Services/PlatformServiceSlim.cs | 32 +++++ .../Platforms/MacOS/IntPtrExtensions.cs | 22 +++ .../Platforms/MacOS/NSTypes/NSData.cs | 23 +++ .../MacOS/NSTypes/NSMutableDictionary.cs | 42 ++++++ .../Platforms/MacOS/NSTypes/NSNumber.cs | 19 +++ .../Platforms/MacOS/NSTypes/NSObject.cs | 27 ++++ .../Platforms/MacOS/NSTypes/NSString.cs | 13 ++ .../MacOS/ObjectiveCRuntimeInterop.cs | 68 +++++++++ .../MacOS/Security/KeychainConstants.cs | 17 +++ .../MacOS/Security/KeychainService.cs | 68 +++++++++ .../MacOS/Security/MacOsKeychainInterop.cs | 36 +++++ .../MacOS/Security/SecurityItemResult.cs | 13 ++ .../Windows/Security/ProtectedDataService.cs | 21 +++ .../Services/Security/SecureStorage.cs | 26 ++++ .../Services/Security/SecureStorageMacOS.cs | 32 +++++ .../Services/Security/SecureStorageWindows.cs | 42 ++++++ .../Security/UserCredentialsManager.cs | 43 ++++++ .../Services/Utilities/ClipboardManager.cs | 57 ++++++++ ...llosoft.AwsCredentialsManager.Tests.csproj | 26 ++-- .../ServiceRegistrationTests.cs | 95 ++++++++++++ 81 files changed, 1563 insertions(+), 655 deletions(-) create mode 100644 config/sample-config.toml create mode 100644 config/sample-config.yml create mode 100644 src/Ellosoft.AwsCredentialsManager/Commands/Utils/OpenLogs.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/ConfigInterceptor.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/ToolConfiguration.cs delete mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Configuration/UserCredentialsManager.cs delete mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Encryption/DataProtection.cs delete mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Encryption/WindowsDataProtection.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionRequest.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionResult.cs delete mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAccessTokenProvider.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/PlatformServiceSlim.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/IntPtrExtensions.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSData.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSMutableDictionary.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSNumber.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSObject.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSString.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/ObjectiveCRuntimeInterop.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainConstants.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainService.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/MacOsKeychainInterop.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/SecurityItemResult.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Platforms/Windows/Security/ProtectedDataService.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorage.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageMacOS.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageWindows.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Security/UserCredentialsManager.cs create mode 100644 src/Ellosoft.AwsCredentialsManager/Services/Utilities/ClipboardManager.cs create mode 100644 test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index dc23b33..fd972de 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,45 +1,49 @@ --- name: Bug report about: Create a report to help us improve -title: "[BUG]" +title: "Brief description" labels: bug -assignees: vgmello-ellosoft +assignees: vgmello --- -**Describe the Bug** +## Describe the Bug -A clear and concise description of what the bug is. +Description of the bug is. -**Expected Behavior** +## Expected Behavior -A clear and concise description of what you expected to happen. +Add the expected behavior. -**Screenshots** +## Screenshots If applicable, add screenshots to help explain your problem. -**Environment (please complete the following information):** +## Environment (please complete the following information): -- OS: [e.g. Windows, macOS, Linux] -- aws-cred-mgr Version: [e.g. 1.0.0] (use `aws-cred-mgr --version`) +``` +OS: [e.g. Windows, macOS, Linux] +aws-cred-mgr Version: [e.g. 1.0.0] (use `aws-cred-mgr --version`) + +etc... +``` -**To Reproduce** +## To Reproduce ```sh # Provide the exact command(s) run when the issue occurred # (please exclude any sensitive information). ``` -**Configuration File** +## Configuration File -Please include the configuration file (WITHOUT any sensitive data) that relates to the issue. +Include the configuration file (WITHOUT any sensitive data) that relates to the issue, if applicable. ```yaml # Your configuration here ``` -**Log** +## Log Include the exception from the `~\.aws_cred_mgr\aws-cred-mgr.log` if applicable (please make sure there is no sensitive data in the exception). diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index e5f8cbe..cb0d144 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,17 +1,17 @@ --- name: Feature request about: Suggest an idea for this project -title: "[FEATURE]" +title: "Feature name" labels: enhancement -assignees: vgmello-ellosoft +assignees: vgmello --- -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +## Is your feature request related to a problem? Please describe. +Describe the problem. -**Describe the solution you'd like** -A clear and concise description of what you want to happen. +## Describe the solution you'd like +Describe the solution, how you'd like to see it implemented. -**Additional context** +## Additional context Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 998051d..65a5347 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: .NET +name: .NET Build & Test Workflow on: push: branches: ["main", "develop"] @@ -6,7 +6,8 @@ on: branches: ["main", "develop"] jobs: build: - runs-on: ubuntu-latest + name: MacOS Build + runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Setup .NET diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ba43926..523e405 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,16 @@ -name: .NET Release +name: .NET Release Workflow on: push: tags: - "*" +permissions: + id-token: write + contents: write + attestations: write jobs: - build: - runs-on: ubuntu-latest + release: + name: Build & Release + runs-on: macos-latest steps: - uses: actions/checkout@v4 - name: Setup .NET @@ -14,18 +19,63 @@ jobs: dotnet-version: 8.0.x - name: Restore dependencies run: dotnet restore - - name: Build & Publish - run: dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} -r win-x64 -o win-output - - name: Zip Output + - name: Build & Publish Windows + run: | + dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ + -r win-x64 -o output-win + - name: Build & Publish MacOS x64 + run: | + dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ + -r osx-x64 -o output-osx + - name: Build & Publish MacOS ARM + run: | + dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ + -r osx-arm64 -o output-osxarm + +# Start - Temporary steps to enable the migration of zip to binary + + - name: Zip Output Windows uses: thedoctor0/zip-release@0.7.6 with: type: "zip" - directory: win-output/ + directory: output-win/ path: aws-cred-mgr.exe filename: aws-cred-mgr-${{ github.ref_name }}-win-x64.zip + - name: Zip Output MacOS x64 + uses: thedoctor0/zip-release@0.7.6 + with: + type: "zip" + directory: output-osx/ + path: aws-cred-mgr + filename: aws-cred-mgr-${{ github.ref_name }}-osx-x64.zip + - name: Zip Output MacOS ARM + uses: thedoctor0/zip-release@0.7.6 + with: + type: "zip" + directory: output-osxarm/ + path: aws-cred-mgr + filename: aws-cred-mgr-${{ github.ref_name }}-osx-arm64.zip + +# End - Temporary steps to enable the migration of zip to binary + + - name: Rename Executables + run: | + mv output-win/aws-cred-mgr.exe output-win/aws-cred-mgr-win-x64.exe + mv output-osx/aws-cred-mgr output-osx/aws-cred-mgr-osx-x64 + mv output-osxarm/aws-cred-mgr output-osxarm/aws-cred-mgr-osx-arm64 + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + output-win/aws-cred-mgr-win-x64.exe + output-osx/aws-cred-mgr-osx-x64 + output-osxarm/aws-cred-mgr-osx-arm64 - name: Create Release uses: softprops/action-gh-release@v2 with: generate_release_notes: true prerelease: ${{ contains(github.ref_name, 'beta') }} - files: win-output/aws-cred-mgr-${{ github.ref_name }}-win-x64.zip + files: | + output-win/aws-cred-mgr-win-x64.exe + output-osx/aws-cred-mgr-osx-x64 + output-osxarm/aws-cred-mgr-osx-arm64 diff --git a/.vscode/launch.json b/.vscode/launch.json index 8a7ebe2..a65edc9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,5 +4,26 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/dotnet/vscode-csharp/blob/main/debugger-launchjson.md + "name": ".NET Core Launch (console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/src/Ellosoft.AwsCredentialsManager/bin/Debug/net8.0/osx-arm64/aws-cred-mgr.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Ellosoft.AwsCredentialsManager", + // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console + "console": "internalConsole", + "stopAtEntry": false + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index a8ec961..1a7f734 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,6 +10,42 @@ }, "problemMatcher": [], "label": "dotnet: build" - } + }, + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Ellosoft.AwsCredentialsManager.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/Ellosoft.AwsCredentialsManager.sln", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary;ForceNoAlign" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/Ellosoft.AwsCredentialsManager.sln" + ], + "problemMatcher": "$msCompile" + } ] } diff --git a/Directory.Build.props b/Directory.Build.props index 7acb831..8feb496 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,9 +1,11 @@ - latest enable enable + true + true ..\..\ellosoft.ruleset + $(NoWarn);CS1591 @@ -13,4 +15,9 @@ + + $(DefineConstants);MACOS + $(DefineConstants);WINDOWS + + diff --git a/Ellosoft.AwsCredentialsManager.sln b/Ellosoft.AwsCredentialsManager.sln index a924f01..0436ac2 100644 --- a/Ellosoft.AwsCredentialsManager.sln +++ b/Ellosoft.AwsCredentialsManager.sln @@ -18,6 +18,12 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ellosoft.AwsCredentialsManager.Tests", "test\Ellosoft.AwsCredentialsManager.Tests\Ellosoft.AwsCredentialsManager.Tests.csproj", "{17AC9083-C7C0-4315-82E9-E51913C88702}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Configs", "Configs", "{BE5D4D7F-D3F1-4A43-A88E-1EB889C697EE}" + ProjectSection(SolutionItems) = preProject + config\sample-config.toml = config\sample-config.toml + config\sample-config.yml = config\sample-config.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -39,4 +45,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {49500883-1CA1-4B19-9746-91C708E418AC} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {BE5D4D7F-D3F1-4A43-A88E-1EB889C697EE} = {EF1AF96A-3E6B-41D7-868D-80D55107BF04} + EndGlobalSection EndGlobal diff --git a/Ellosoft.AwsCredentialsManager.sln.DotSettings b/Ellosoft.AwsCredentialsManager.sln.DotSettings index 94e4a61..bf851a2 100644 --- a/Ellosoft.AwsCredentialsManager.sln.DotSettings +++ b/Ellosoft.AwsCredentialsManager.sln.DotSettings @@ -2,6 +2,7 @@ DO_NOT_SHOW HINT DO_NOT_SHOW + HINT <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /></Policy> True diff --git a/README.md b/README.md index 52eef9b..58fc116 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ AWS Credential Manager (`aws-cred-mgr`) is a command-line interface (CLI) tool d - **Credential Management**: Create and list AWS credentials, manage profiles with ease. - **RDS Token Management**: Obtain RDS passwords for your databases securely. +### [Request new features here](https://github.com/ellosoft/aws-cred-mgr/issues/new?assignees=vgmello-ellosoft&labels=enhancement&projects=&template=feature_request.md&title=%5BFEATURE%5D) + ## Installation To install aws-cred-mgr, download the latest version from the [GitHub Release](https://github.com/ellosoft/aws-cred-mgr/releases) page @@ -66,11 +68,14 @@ aws-cred-mgr config - Open user config: `aws-cred-mgr config` - Open AWS credentials file: `aws-cred-mgr config aws` -## Security Note for Windows Users +## Security Note for Windows and macOS Users + +On Windows systems, `aws-cred-mgr` securely stores your Okta credentials using the Data Protection API (DPAPI). +This ensures that your sensitive information is encrypted and can only be accessed by your user account on your computer. -On Windows systems, `aws-cred-mgr` securely stores your Okta credentials using the Data Protection API (DPAPI). This ensures that your sensitive information is encrypted and can only be accessed by your user account on your computer. +On macOs systems, `aws-cred-mgr` securely stores your Okta credentials using the native Keychain API. -MacOS support is still under development +Linux support is still under development ## Full Configuration Example @@ -158,6 +163,16 @@ This project has adopted the code of conduct defined by the Contributor Covenant Each of these libraries may be licensed differently, so we recommend you to review their licenses if you plan to use `aws-cred-mgr` in your own projects. +## Trademarks + +This repository makes use of libraries and technologies related to AWS (Amazon Web Services) and Okta. +Please note that “AWS” and “Amazon Web Services” are trademarks or registered trademarks of Amazon.com, Inc. or its affiliates. +Similarly, “Okta” is a trademark or registered trademark of Okta, Inc. All other trademarks and registered trademarks are the property +of their respective owners. + +This repository is not affiliated with, endorsed by, or sponsored by Amazon.com, Inc., Okta, Inc., +or any of their subsidiaries or affiliates. The use of these names is solely for descriptive purposes to identify the relevant technologies. + ## License This project is licensed under the terms of the MIT license. diff --git a/config/sample-config.toml b/config/sample-config.toml new file mode 100644 index 0000000..5e0ded2 --- /dev/null +++ b/config/sample-config.toml @@ -0,0 +1,57 @@ +[variables] +rds_username = "my.user" +default_pwd_lifetime = 15 + +[authentication.okta.default] +okta_domain = "https://xyz.okta.com/" +preferred_mfa_type = "push" +auth_type = "classic" + +[credentials.my_aws_dev_account] +role_arn = "arn:aws:iam::123:role:/my_aws_role_arn" +aws_profile = "default" +okta_app_url = "https://xyz.okta.com/home/amazon_aws/abc/272" +okta_profile = "default" + +[credentials.my_aws_prod_account] +role_arn = "arn:aws:iam::456:role:/my_aws_prod_role_arn" +aws_profile = "prod" +okta_app_url = "https://xyz.okta.com/home/amazon_aws/def/789" +okta_profile = "default" + +[templates.rds.orders_db] +hostname = "rds-hostname.aws.endpoint" +port = 5432 +username = "${rds_username}" +region = "us-east-2" + +[templates.rds.products_db] +hostname = "products-rds.aws.endpoint" +port = 5432 +username = "${rds_username}" +region = "us-west-2" + +[environments.dev] +credential = "my_aws_dev_account" + +[environments.dev.rds.orders_db] +hostname = "dev-orders.rds.amazonaws.com" +template = "orders_db" + +[environments.dev.rds.products_db] +hostname = "dev-products.rds.amazonaws.com" +port = 5432 +username = "${rds_username}" +ttl = "${default_pwd_lifetime}" +region = "us-west-2" + +[environments.prod] +credential = "my_aws_prod_account" + +[environments.prod.rds.orders_db] +hostname = "prod-orders.rds.amazonaws.com" +template = "orders_db" + +[environments.prod.rds.products_db] +hostname = "prod-products.rds.amazonaws.com" +template = "products_db" diff --git a/config/sample-config.yml b/config/sample-config.yml new file mode 100644 index 0000000..5ba8bbf --- /dev/null +++ b/config/sample-config.yml @@ -0,0 +1,62 @@ +variables: + rds_username: my.user + default_pwd_lifetime: 15 + +authentication: + okta: + default: + okta_domain: https://xyz.okta.com/ + preferred_mfa_type: push + auth_type: classic + +credentials: + my_aws_dev_account: + role_arn: arn:aws:iam::123:role:/my_aws_role_arn + aws_profile: default + okta_app_url: https://xyz.okta.com/home/amazon_aws/abc/272 + okta_profile: default + my_aws_prod_account: + role_arn: arn:aws:iam::456:role:/my_aws_prod_role_arn + aws_profile: prod + okta_app_url: https://xyz.okta.com/home/amazon_aws/def/789 + okta_profile: default + +templates: + rds: + orders_db: + hostname: rds-hostname.aws.endpoint + port: 5432 + username: ${rds_username} + region: us-east-2 + products_db: + hostname: products-rds.aws.endpoint + port: 5432 + username: ${rds_username} + region: us-west-2 + +environments: + dev: + credential: my_aws_dev_account + rds: + orders_db: + hostname: dev-orders.rds.amazonaws.com + template: orders_db + products_db: + hostname: dev-products.rds.amazonaws.com + port: 5433 + username: some diferent user + ttl: ${default_pwd_lifetime} + region: us-east-2 + prod: + credential: my_aws_prod_account + rds: + orders_db: + hostname: prod-orders.rds.amazonaws.com + template: orders_db + products_db: + hostname: prod-products.rds.amazonaws.com + template: products_db + +config: + aws_ignore_configured_endpoints: false # default value is true + copy_to_clipboard: false # default value is true diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/CommandException.cs b/src/Ellosoft.AwsCredentialsManager/Commands/CommandException.cs index 3ad22d0..47b66b8 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/CommandException.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/CommandException.cs @@ -2,9 +2,4 @@ namespace Ellosoft.AwsCredentialsManager.Commands; -public class CommandException : Exception -{ - public CommandException(string message) : base(message) - { - } -} +public class CommandException(string message) : Exception(message); diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs index c7b07dc..40a4e58 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenAwsConfig.cs @@ -17,7 +17,7 @@ public override int Execute(CommandContext context) if (!File.Exists(awsCredentialsPath)) throw new CommandException($"The file {awsCredentialsPath} does not exist"); - fileManager.OpenFile(awsCredentialsPath); + fileManager.OpenFileUsingDefaultApp(awsCredentialsPath); return 0; } diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs index 9447a81..2e179c8 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Config/OpenConfig.cs @@ -15,7 +15,7 @@ public override int Execute(CommandContext context) if (!File.Exists(configManager.AppConfigPath)) configManager.SaveConfig(); - fileManager.OpenFile(configManager.AppConfigPath); + fileManager.OpenFileUsingDefaultApp(configManager.AppConfigPath); return 0; } diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs index 18e1519..ae3b924 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/CreateCredentialsProfile.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Net.Http.Headers; using System.Net.Http.Json; using Ellosoft.AwsCredentialsManager.Commands.AWS; using Ellosoft.AwsCredentialsManager.Services.AWS; @@ -15,7 +14,12 @@ namespace Ellosoft.AwsCredentialsManager.Commands.Credentials; [Name("new")] [Description("Create new credential profile")] [Examples("new prod")] -public class CreateCredentialsProfile : AsyncCommand +public class CreateCredentialsProfile( + ICredentialsManager credentialsManager, + IOktaLoginService oktaLogin, + IOktaSamlService oktaSamlService, + IAwsSamlService awsSamlService) + : AsyncCommand { private const string DEFAULT_AWS_PROFILE_VALUE = "[credential name]"; @@ -44,30 +48,13 @@ public class Settings : AwsSettings public string OktaUserProfile { get; set; } = OktaConfiguration.DefaultProfileName; } - private readonly CredentialsManager _credentialsManager; - private readonly IOktaLoginService _oktaLogin; - private readonly OktaSamlService _oktaSamlService; - private readonly AwsSamlService _awsSamlService; - - public CreateCredentialsProfile( - CredentialsManager credentialsManager, - IOktaLoginService oktaLogin, - OktaSamlService oktaSamlService, - AwsSamlService awsSamlService) - { - _credentialsManager = credentialsManager; - _oktaLogin = oktaLogin; - _oktaSamlService = oktaSamlService; - _awsSamlService = awsSamlService; - } - public override async Task ExecuteAsync(CommandContext context, Settings settings) { var oktaAppUrl = settings.OktaAppUrl ?? await GetAwsAppUrl(settings.OktaUserProfile); var awsRole = settings.AwsRoleArn ?? await GetAwsRoleArn(settings.OktaUserProfile, oktaAppUrl); var awsProfile = settings.AwsProfile is null or DEFAULT_AWS_PROFILE_VALUE ? null : settings.AwsProfile; - _credentialsManager.CreateCredential( + credentialsManager.CreateCredential( name: settings.Name, awsProfile: awsProfile, awsRole: awsRole, @@ -83,12 +70,12 @@ private async Task GetAwsAppUrl(string oktaUserProfile) { AnsiConsole.MarkupLine("Retrieving AWS Apps from OKTA..."); - var accessTokenResult = await _oktaLogin.InteractiveGetAccessToken(oktaUserProfile); + var authenticationResult = await oktaLogin.InteractiveLogin(oktaUserProfile, createSession: true); - if (accessTokenResult is null) + if (authenticationResult?.SessionId is null) throw new CommandException("Unable to retrieve OKTA apps, please try again or use the '--okta-app-url' option to specify an app URL manually"); - var awsAppLinks = await GetAwsLinks(accessTokenResult.AuthResult.OktaDomain, accessTokenResult.AccessToken); + var awsAppLinks = await GetAwsLinks(authenticationResult.OktaDomain, authenticationResult.SessionId); if (awsAppLinks.Count == 0) throw new CommandException("No AWS apps found in Okta, please use the '--okta-app-url' option to specify an app URL manually"); @@ -102,34 +89,41 @@ private async Task GetAwsAppUrl(string oktaUserProfile) .AddChoices(awsAppLinks)); return appLink.LinkUrl; + } + + private static async Task> GetAwsLinks(Uri oktaDomain, string sessionId) + { + // TODO: Replace with HttpClientFactory client + using var httpClient = new HttpClient(); + + var request = new HttpRequestMessage(HttpMethod.Get, new Uri(oktaDomain, "/api/v1/users/me/appLinks")); + request.Headers.Add("Cookie", $"sid={sessionId}"); - static async Task> GetAwsLinks(Uri oktaDomain, string accessToken) - { - using var httpClient = new HttpClient(); - httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + var httpResponse = await httpClient.SendAsync(request); + httpResponse.EnsureSuccessStatusCode(); - var appLinks = await httpClient.GetFromJsonAsync(new Uri(oktaDomain, "/api/v1/users/me/appLinks"), OktaSourceGenerationContext.Default.ListAppLink); + var appLinks = await httpResponse.Content.ReadFromJsonAsync( + OktaSourceGenerationContext.Default.ListAppLink); - if (appLinks is not null) - return appLinks.Where(app => app.AppName == "amazon_aws").ToList(); + if (appLinks is not null) + return appLinks.Where(app => app.AppName == "amazon_aws").ToList(); - throw new InvalidOperationException("Invalid Okta AppLinks response"); - } + throw new InvalidOperationException("Invalid Okta AppLinks response"); } private async Task GetAwsRoleArn(string oktaUserProfile, string oktaAppUrl) { AnsiConsole.MarkupLine("Retrieving AWS roles..."); - var sessionTokenResult = await _oktaLogin.InteractiveLogin(oktaUserProfile); + var sessionTokenResult = await oktaLogin.InteractiveLogin(oktaUserProfile); if (sessionTokenResult?.SessionToken is null) throw new CommandException("Unable to create AWS credential profile, please try again"); - var samlData = await _oktaSamlService.GetAppSamlDataAsync(sessionTokenResult.OktaDomain, oktaAppUrl, + var samlData = await oktaSamlService.GetAppSamlDataAsync(sessionTokenResult.OktaDomain, oktaAppUrl, sessionTokenResult.SessionToken); - var awsRoles = await _awsSamlService.GetAwsRolesWithAccountName(samlData); + var awsRoles = await awsSamlService.GetAwsRolesWithAccountName(samlData); if (awsRoles.Count == 0) throw new CommandException("Unable to load AWS roles, please use the '--aws-role' option to specify a role manually"); diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/GetCredentials.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/GetCredentials.cs index b76d8dd..f1336f5 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/GetCredentials.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/GetCredentials.cs @@ -12,8 +12,8 @@ namespace Ellosoft.AwsCredentialsManager.Commands.Credentials; "get prod", "get prod --aws-profile default")] public class GetCredentials( - CredentialsManager credentialsManager, - AwsOktaSessionManager sessionManager + ICredentialsManager credentialsManager, + IAwsOktaSessionManager sessionManager ) : AsyncCommand { public class Settings : AwsSettings diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/ListCredentialsProfiles.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/ListCredentialsProfiles.cs index 637da03..c1e957a 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/ListCredentialsProfiles.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Credentials/ListCredentialsProfiles.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Ellosoft.AwsCredentialsManager.Commands.AWS; using Ellosoft.AwsCredentialsManager.Services.Configuration; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Okta/SetupOkta.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Okta/SetupOkta.cs index 95a96e8..ae8d079 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/Okta/SetupOkta.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Okta/SetupOkta.cs @@ -12,7 +12,7 @@ namespace Ellosoft.AwsCredentialsManager.Commands.Okta; "setup", "setup -d https://xyz.okta.com -u john --mfa push", "setup xyz_profile -d https://xyz.okta.com -u john --mfa push")] -public class SetupOkta : AsyncCommand +public class SetupOkta(IOktaLoginService loginService, IConfigManager configManager) : AsyncCommand { public class Settings : CommonSettings { @@ -34,15 +34,6 @@ public class Settings : CommonSettings public string? PreferredMfaType { get; set; } } - private readonly IOktaLoginService _loginService; - private readonly IConfigManager _configManager; - - public SetupOkta(IOktaLoginService loginService, IConfigManager configManager) - { - _loginService = loginService; - _configManager = configManager; - } - public override async Task ExecuteAsync(CommandContext context, Settings settings) { AnsiConsole.MarkupLine("Okta Setup"); @@ -56,7 +47,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se var credentials = new UserCredentials(username, password); var preferredMfaType = settings.PreferredMfaType is not null ? OktaMfaFactorSelector.GetOktaMfaFactorCode(settings.PreferredMfaType) : null; - var authResult = await _loginService.Login(oktaDomain, credentials, preferredMfaType, userProfileKey: settings.Profile); + var authResult = await loginService.Login(oktaDomain, credentials, preferredMfaType, userProfileKey: settings.Profile); if (!authResult.Authenticated) throw new CommandException("Unable to create profile, please try again"); @@ -70,7 +61,7 @@ public override async Task ExecuteAsync(CommandContext context, Settings se private static Uri GetOktaDomainUrl(Settings settings) { - const string URL_MESSAGE = "Enter your [green]Okta[/] domain URL (e.g. https://xyz.okta.com):"; + const string URL_MESSAGE = "Enter your [green]Okta[/] domain URL (e.g. https://xyz.okta.com): [grey85][[https://]][/]"; var oktaDomain = settings.OktaDomain ?? AnsiConsole.Ask(URL_MESSAGE); @@ -88,7 +79,7 @@ private static Uri GetOktaDomainUrl(Settings settings) private void CreateOktaProfile(string profileName, string oktaDomain, string? preferredMfaType) { - var appConfig = _configManager.AppConfig; + var appConfig = configManager.AppConfig; appConfig.Authentication ??= new AppConfig.AuthenticationSection(); appConfig.Authentication.Okta[profileName] = new OktaConfiguration @@ -97,6 +88,6 @@ private void CreateOktaProfile(string profileName, string oktaDomain, string? pr PreferredMfaType = preferredMfaType }; - _configManager.SaveConfig(); + configManager.SaveConfig(); } } diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/RDS/GetRdsPassword.cs b/src/Ellosoft.AwsCredentialsManager/Commands/RDS/GetRdsPassword.cs index 50510fe..a199f5d 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/RDS/GetRdsPassword.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/RDS/GetRdsPassword.cs @@ -7,6 +7,7 @@ using Ellosoft.AwsCredentialsManager.Services.Configuration; using Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; +using Ellosoft.AwsCredentialsManager.Services.Utilities; namespace Ellosoft.AwsCredentialsManager.Commands.RDS; @@ -15,7 +16,14 @@ namespace Ellosoft.AwsCredentialsManager.Commands.RDS; [Examples( "pwd prod_db", "pwd -h localhost -p 5432 -u john")] -public class GetRdsPassword : AsyncCommand +public class GetRdsPassword( + IConfigManager configManager, + IEnvironmentManager envManager, + ICredentialsManager credentialsManager, + IAwsOktaSessionManager awsSessionManager, + IRdsTokenGenerator rdsTokenGenerator, + IClipboardManager clipboardManager) + : AsyncCommand { public class Settings : AwsSettings { @@ -45,34 +53,14 @@ public class Settings : AwsSettings public string? Environment { get; set; } } - private readonly IConfigManager _configManager; - private readonly EnvironmentManager _envManager; - private readonly CredentialsManager _credentialsManager; - private readonly AwsOktaSessionManager _awsSessionManager; - private readonly RdsTokenGenerator _rdsTokenGenerator; - - public GetRdsPassword( - IConfigManager configManager, - EnvironmentManager envManager, - CredentialsManager credentialsManager, - AwsOktaSessionManager awsSessionManager, - RdsTokenGenerator rdsTokenGenerator) - { - _configManager = configManager; - _envManager = envManager; - _credentialsManager = credentialsManager; - _awsSessionManager = awsSessionManager; - _rdsTokenGenerator = rdsTokenGenerator; - } - public override async Task ExecuteAsync(CommandContext context, Settings settings) { if (settings.Profile is null) return await HandleAdHocRequest(settings); - var dbConfig = GetDbConfig(_configManager.AppConfig, settings.Profile, settings.Environment); + var dbConfig = GetDbConfig(configManager.AppConfig, settings.Profile, settings.Environment); - ApplyTemplateValues(dbConfig, _configManager.AppConfig.Templates); + ApplyTemplateValues(dbConfig, configManager.AppConfig.Templates); await GenerateDbPassword( dbConfig.Credential, @@ -88,7 +76,7 @@ await GenerateDbPassword( private async Task HandleAdHocRequest(Settings settings) { - var credentialName = _credentialsManager.GetCredentialNameFromUser(); + var credentialName = credentialsManager.GetCredentialNameFromUser(); AnsiConsole.MarkupLine($"Getting RDS password using [green i]{credentialName}[/] credential profile"); @@ -114,16 +102,20 @@ private async Task GenerateDbPassword(string? credential, string? hostname, int? ArgumentNullException.ThrowIfNull(username); ArgumentNullException.ThrowIfNull(region); - var awsCredentials = await _awsSessionManager.CreateOrResumeSessionAsync(credential, null); + var awsCredentials = await awsSessionManager.CreateOrResumeSessionAsync(credential, null); if (awsCredentials is null) throw new CommandException($"Unable to resume or create AWS session for credential '{credential}'"); var regionEndpoint = RegionEndpoint.GetBySystemName(region); - var dbPassword = _rdsTokenGenerator.GenerateDbPassword(awsCredentials, regionEndpoint, hostname, port.Value, username, ttl); + var dbPassword = rdsTokenGenerator.GenerateDbPassword(awsCredentials, regionEndpoint, hostname, port.Value, username, ttl); - AnsiConsole.MarkupLine("\r\n[green]DB Password:[/]"); + AnsiConsole.MarkupLine("[green]DB Password:[/]"); Console.WriteLine(dbPassword); + Console.WriteLine(); + + if (configManager.ToolConfig.CopyToClipboard && clipboardManager.SetClipboardText(dbPassword)) + AnsiConsole.MarkupLine("[green]DB Password copied to clipboard[/]"); } catch (ArgumentNullException e) { @@ -140,7 +132,7 @@ private void CreateNewRdsProfile(string credential, string hostname, int port, s return; } - var environment = _envManager.GetOrCreateEnvironment(environmentName); + var environment = envManager.GetOrCreateEnvironment(environmentName); var dbConfig = new DatabaseConfiguration { @@ -163,7 +155,7 @@ private void CreateNewRdsProfile(string credential, string hostname, int port, s profileName = AnsiConsole.Ask("There is already a RDS profile with that name, please choose another one:"); } - _configManager.SaveConfig(); + configManager.SaveConfig(); AnsiConsole.MarkupLine($"[bold green]'{profileName}' RDS profile created[/]"); } @@ -172,7 +164,7 @@ private DatabaseConfiguration GetDbConfig(AppConfig appConfig, string rdsProfile { if (environmentName is not null) { - var env = _envManager.GetEnvironment(environmentName); + var env = envManager.GetEnvironment(environmentName); if (env is not null && env.Rds.TryGetValue(rdsProfile, out var dbConfig)) { diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/RDS/ListRdsProfiles.cs b/src/Ellosoft.AwsCredentialsManager/Commands/RDS/ListRdsProfiles.cs index 7378387..a72ed88 100644 --- a/src/Ellosoft.AwsCredentialsManager/Commands/RDS/ListRdsProfiles.cs +++ b/src/Ellosoft.AwsCredentialsManager/Commands/RDS/ListRdsProfiles.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Ellosoft.AwsCredentialsManager.Services.Configuration; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; diff --git a/src/Ellosoft.AwsCredentialsManager/Commands/Utils/OpenLogs.cs b/src/Ellosoft.AwsCredentialsManager/Commands/Utils/OpenLogs.cs new file mode 100644 index 0000000..7d0d37d --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Commands/Utils/OpenLogs.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Infrastructure.Logging; +using Ellosoft.AwsCredentialsManager.Services.IO; + +namespace Ellosoft.AwsCredentialsManager.Commands.Utils; + +[Name("logs")] +[Description("Open log file")] +[Examples("logs")] +public class OpenLogs(IFileManager fileManager) : Command +{ + public override int Execute(CommandContext context) + { + if (!File.Exists(LogRegistration.LogFileName)) + AnsiConsole.MarkupLine("[yellow]Unable to find log file[/]"); + + fileManager.OpenFileUsingDefaultApp(LogRegistration.LogFileName); + + return 0; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj index 66d9148..ef9119f 100644 --- a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj +++ b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj @@ -4,6 +4,7 @@ Exe net8.0 AnyCPU + true @@ -13,12 +14,11 @@ true full true - 12.0 false - + @@ -43,20 +43,20 @@ - - - - - - - - - + + + + + + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + diff --git a/src/Ellosoft.AwsCredentialsManager/GlobalSuppressions.cs b/src/Ellosoft.AwsCredentialsManager/GlobalSuppressions.cs index 4f8026f..61c7e85 100644 --- a/src/Ellosoft.AwsCredentialsManager/GlobalSuppressions.cs +++ b/src/Ellosoft.AwsCredentialsManager/GlobalSuppressions.cs @@ -1,6 +1,5 @@ -using System.Diagnostics.CodeAnalysis; - [assembly: SuppressMessage("Usage", "Spectre1000:Use AnsiConsole instead of System.Console", Justification = "Required for non-wrapped text output", Scope = "member", Target = "~M:Ellosoft.AwsCredentialsManager.Commands.RDS.GetRdsPassword.GenerateDbPassword" + - "(System.String,System.String,System.Nullable{System.Int32},System.String,System.String,System.Int32)~System.Threading.Tasks.Task")] + "(System.String,System.String,System.Nullable{System.Int32}," + + "System.String,System.String,System.Int32)~System.Threading.Tasks.Task")] diff --git a/src/Ellosoft.AwsCredentialsManager/GlobalUsings.cs b/src/Ellosoft.AwsCredentialsManager/GlobalUsings.cs index 20bc3e5..4e2d994 100644 --- a/src/Ellosoft.AwsCredentialsManager/GlobalUsings.cs +++ b/src/Ellosoft.AwsCredentialsManager/GlobalUsings.cs @@ -1,6 +1,8 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. global using System.ComponentModel; +global using System.Diagnostics.CodeAnalysis; +global using System.Runtime.Versioning; global using Ellosoft.AwsCredentialsManager.Infrastructure.Cli.Attributes; global using Spectre.Console; global using Spectre.Console.Cli; diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Cli/TypeRegistrar.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Cli/TypeRegistrar.cs index 0358dde..1c8d16c 100644 --- a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Cli/TypeRegistrar.cs +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Cli/TypeRegistrar.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; namespace Ellosoft.AwsCredentialsManager.Infrastructure.Cli; diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs index 3de4faa..4dff344 100644 --- a/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/FileDownloadService.cs @@ -4,14 +4,16 @@ namespace Ellosoft.AwsCredentialsManager.Infrastructure; public interface IFileDownloadService { - Task DownloadFileAsync(HttpClient httpClient, string downloadUrl, Stream destinationStream); + Task DownloadAsync(HttpClient httpClient, string downloadUrl, string destinationFilePath); } public class FileDownloadService : IFileDownloadService { - public Task DownloadFileAsync(HttpClient httpClient, string downloadUrl, Stream destinationStream) + public async Task DownloadAsync(HttpClient httpClient, string downloadUrl, string destinationFilePath) { - return AnsiConsole.Progress() + await using var destinationStream = new FileStream(destinationFilePath, FileMode.Create, FileAccess.Write, FileShare.None); + + await AnsiConsole.Progress() .HideCompleted(true) .AutoClear(true) .StartAsync(async ctx => @@ -40,7 +42,6 @@ public Task DownloadFileAsync(HttpClient httpClient, string downloadUrl, Stream } await destinationStream.FlushAsync(); - destinationStream.Position = 0; downloadTask.StopTask(); }); diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/ConfigInterceptor.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/ConfigInterceptor.cs new file mode 100644 index 0000000..02daa9f --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/ConfigInterceptor.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Services.Configuration; + +namespace Ellosoft.AwsCredentialsManager.Infrastructure.Logging; + +public class ConfigInterceptor(IConfigManager configManager) : ICommandInterceptor +{ + public void Intercept(CommandContext context, CommandSettings settings) + { + if (configManager.ToolConfig.AwsIgnoreConfiguredEndpoints) + { + Environment.SetEnvironmentVariable("AWS_IGNORE_CONFIGURED_ENDPOINTS", "true"); + } + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/LogRegistration.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/LogRegistration.cs index 91d64a2..8b5a43a 100644 --- a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/LogRegistration.cs +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Logging/LogRegistration.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Reflection; using Ellosoft.AwsCredentialsManager.Services; using Microsoft.Extensions.DependencyInjection; using Serilog; @@ -11,7 +10,7 @@ internal static class LogRegistration { private const long MAX_LOG_FILE_SIZE = 20 * (1024 ^ 2); - private static readonly string LogFileName = AppDataDirectory.GetPath($"{Assembly.GetExecutingAssembly().GetName().Name}.log"); + public static readonly string LogFileName = AppDataDirectory.GetPath($"{AppMetadata.AppName}.log"); public static IServiceCollection SetupLogging(this IServiceCollection services, ILogger logger) => services.AddLogging(config => config.AddSerilog(logger)); diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs index 6c6430f..04f1c6e 100644 --- a/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/SemanticVersion.cs @@ -1,7 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; - namespace Ellosoft.AwsCredentialsManager.Infrastructure; public record SemanticVersion : IComparable diff --git a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs index 05c86dc..14ff9be 100644 --- a/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Infrastructure/Upgrade/UpgradeService.cs @@ -1,7 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; -using System.IO.Compression; using System.Net.Http.Json; using System.Runtime.InteropServices; using Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade.Models; @@ -12,16 +10,17 @@ namespace Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade; #pragma warning disable S1075 // URIs should not be hardcoded -public class UpgradeService +public class UpgradeService( + ILogger logger, + IAnsiConsole console, + IAppMetadata appMetadata, + IFileDownloadService downloadService, + HttpClient httpClient) { private const string GITHUB_RELEASES_URL = "https://api.github.com/repos/ellosoft/aws-cred-mgr/releases"; private const string GITHUB_LATEST_RELEASE_URL = "https://api.github.com/repos/ellosoft/aws-cred-mgr/releases/latest"; - private readonly ILogger _logger; - private readonly IAnsiConsole _console; - private readonly IAppMetadata _appMetadata; - private readonly IFileDownloadService _downloadService; - private readonly HttpClient _httpClient; + private readonly ILogger _logger = logger.ForContext(); public UpgradeService(ILogger logger) : this( logger, @@ -32,20 +31,6 @@ public UpgradeService(ILogger logger) : this( { } - public UpgradeService( - ILogger logger, - IAnsiConsole console, - IAppMetadata appMetadata, - IFileDownloadService downloadService, - HttpClient httpClient) - { - _logger = logger.ForContext(); - _console = console; - _appMetadata = appMetadata; - _downloadService = downloadService; - _httpClient = httpClient; - } - public async Task TryUpgradeApp() { try @@ -53,7 +38,7 @@ public async Task TryUpgradeApp() if (!ShouldCheckForUpgrade()) return; - var currentAppVersion = _appMetadata.GetAppVersion(); + var currentAppVersion = appMetadata.GetAppVersion(); if (currentAppVersion is null) return; @@ -68,26 +53,26 @@ public async Task TryUpgradeApp() if (!shouldUpdate) return; - using var zipStream = new MemoryStream(); - await _downloadService.DownloadFileAsync(_httpClient, downloadUrl, zipStream); - - var (executablePath, appFolder) = _appMetadata.GetExecutablePath(); + var (executablePath, appFolder) = appMetadata.GetExecutablePath(); var executableName = Path.GetFileName(executablePath); - var newFile = Path.Combine(appFolder, executableName + ".new"); + var newFilePath = Path.Combine(appFolder, executableName + ".new"); var archivePath = Path.Combine(Path.GetTempPath(), executableName + ".old"); - _console.MarkupLine("\r\nInstalling upgrade..."); + await downloadService.DownloadAsync(httpClient, downloadUrl, newFilePath); + + console.MarkupLine("\r\nInstalling upgrade..."); - ExtractApp(zipStream, newFile); - UpgradeApp(executablePath, archivePath, newFile); + UpgradeApp(executablePath, archivePath, newFilePath); - _console.MarkupLine("[green]Upgrade complete! The changes will reflect next time you execute the application.\r\n[/]"); + console.MarkupLine("Upgrade complete! The changes will reflect next time you execute the application.\r\n"); + console.MarkupLine("If you like this tool, consider giving it a :star: Star on GitHub, it's free and only takes 2 minutes!\r\n" + + "Link: https://github.com/ellosoft/aws-cred-mgr\r\n"); } catch (Exception e) { _logger.Error(e, "Unable to upgrade app"); - _console.MarkupLine("[yellow]Unable to upgrade app, try again later or " + + console.MarkupLine("[yellow]Unable to upgrade app, try again later or " + "download the new version from https://github.com/ellosoft/aws-cred-mgr/releases [/]"); } } @@ -95,9 +80,9 @@ public async Task TryUpgradeApp() private async Task GetLatestRelease(SemanticVersion currentAppVersion) { if (!currentAppVersion.IsPreRelease) - return await _httpClient.GetFromJsonAsync(GITHUB_LATEST_RELEASE_URL, GithubSourceGenerationContext.Default.GitHubRelease); + return await httpClient.GetFromJsonAsync(GITHUB_LATEST_RELEASE_URL, GithubSourceGenerationContext.Default.GitHubRelease); - var releases = await _httpClient.GetFromJsonAsync(GITHUB_RELEASES_URL, GithubSourceGenerationContext.Default.ListGitHubRelease); + var releases = await httpClient.GetFromJsonAsync(GITHUB_RELEASES_URL, GithubSourceGenerationContext.Default.ListGitHubRelease); return releases?.Find(r => r.PreRelease); } @@ -106,7 +91,7 @@ private bool CheckIfUserWantsToUpdate(SemanticVersion currentAppVersion, GitHubR { var preReleaseTag = latestRelease.PreRelease ? "(Pre-release)" : String.Empty; - _console.MarkupLine( + console.MarkupLine( $""" New version available: [b]Current Version:[/] {currentAppVersion} @@ -115,7 +100,7 @@ private bool CheckIfUserWantsToUpdate(SemanticVersion currentAppVersion, GitHubR """); - return _console.Confirm("[yellow]Do you want to upgrade now ?[/]"); + return console.Confirm("[yellow]Do you want to upgrade now ?[/]"); } private static bool TryGetDownloadAssetAndVersion( @@ -132,26 +117,12 @@ private static bool TryGetDownloadAssetAndVersion( if (!SemanticVersion.TryParse(latestRelease.Name, out version)) return false; - var fileSuffix = $"{RuntimeInformation.RuntimeIdentifier}.zip"; - downloadUrl = latestRelease - .Assets.FirstOrDefault(a => a.DownloadUrl.EndsWith(fileSuffix))?.DownloadUrl; + .Assets.FirstOrDefault(a => a.DownloadUrl.Contains(RuntimeInformation.RuntimeIdentifier))?.DownloadUrl; return downloadUrl is not null; } - private static void ExtractApp(Stream zipStream, string newFile) - { - using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read); - - var mainAppEntry = archive.Entries.FirstOrDefault(e => e.Name == "aws-cred-mgr.exe"); - - if (mainAppEntry is null) - throw new InvalidOperationException("Unable to find application file in release archive"); - - mainAppEntry.ExtractToFile(newFile, overwrite: true); - } - private static void UpgradeApp(string executablePath, string archivePath, string newFile) { if (File.Exists(archivePath)) diff --git a/src/Ellosoft.AwsCredentialsManager/Program.cs b/src/Ellosoft.AwsCredentialsManager/Program.cs index 622618c..7a7d494 100644 --- a/src/Ellosoft.AwsCredentialsManager/Program.cs +++ b/src/Ellosoft.AwsCredentialsManager/Program.cs @@ -7,9 +7,11 @@ using Ellosoft.AwsCredentialsManager.Commands.Credentials; using Ellosoft.AwsCredentialsManager.Commands.Okta; using Ellosoft.AwsCredentialsManager.Commands.RDS; +using Ellosoft.AwsCredentialsManager.Commands.Utils; using Ellosoft.AwsCredentialsManager.Infrastructure.Cli; using Ellosoft.AwsCredentialsManager.Infrastructure.Logging; using Ellosoft.AwsCredentialsManager.Infrastructure.Upgrade; +using Ellosoft.AwsCredentialsManager.Services; using Microsoft.Extensions.DependencyInjection; using Serilog.Events; @@ -27,8 +29,7 @@ app.Configure(config => { - config.SetApplicationName("aws-cred-mgr"); - config.SetInterceptor(new LogInterceptor()); + config.SetApplicationName(AppMetadata.AppName); config .AddBranch(okta => @@ -53,13 +54,16 @@ cfg.AddCommand(); }); + // root commands + config.AddCommand(); + config.PropagateExceptions(); #if DEBUG config.ValidateExamples(); if (Debugger.IsAttached) - args = "rds pwd test_db".Split(' '); + args = "rds pwd local".Split(' '); #endif }); @@ -75,6 +79,11 @@ { logger.Error(e, "Unexpected error"); + if (e is CommandRuntimeException && e.InnerException is not null) + { + e = e.InnerException; + } + if (logger.IsEnabled(LogEventLevel.Debug)) AnsiConsole.WriteException(e); else diff --git a/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs b/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs index 61716f7..d56de1a 100644 --- a/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs +++ b/src/Ellosoft.AwsCredentialsManager/ServiceRegistration.cs @@ -1,5 +1,6 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. +using Ellosoft.AwsCredentialsManager.Infrastructure.Logging; using Ellosoft.AwsCredentialsManager.Services.AWS; using Ellosoft.AwsCredentialsManager.Services.AWS.Interactive; using Ellosoft.AwsCredentialsManager.Services.Configuration; @@ -7,7 +8,12 @@ using Ellosoft.AwsCredentialsManager.Services.IO; using Ellosoft.AwsCredentialsManager.Services.Okta; using Ellosoft.AwsCredentialsManager.Services.Okta.Interactive; +using Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; +using Ellosoft.AwsCredentialsManager.Services.Platforms.Windows.Security; +using Ellosoft.AwsCredentialsManager.Services.Security; +using Ellosoft.AwsCredentialsManager.Services.Utilities; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; namespace Ellosoft.AwsCredentialsManager; @@ -15,29 +21,68 @@ public static class ServiceRegistration { public static IServiceCollection RegisterAppServices(this IServiceCollection services) { + // core services services .AddSingleton() + .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); // okta related services services - .AddSingleton() - .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton(); // aws related services services - .AddSingleton() - .AddSingleton(); + .AddSingleton() + .AddSingleton() + .AddSingleton(); services .AddKeyedSingleton(nameof(OktaHttpClientFactory), OktaHttpClientFactory.CreateHttpClient()); + services + .AddSingleton() + .AddSingleton(); + + if (OperatingSystem.IsMacOS()) + { + RegisterMacOSServices(services); + } + + if (OperatingSystem.IsWindows()) + { + RegisterWindowsServices(services); + } + + // fallback implementations + services.TryAddSingleton(); + return services; } + + [SupportedOSPlatform("windows")] + private static void RegisterWindowsServices(IServiceCollection services) + { + services.AddSingleton(); + + // platform services + services.AddSingleton(); + } + + [SupportedOSPlatform("macos")] + private static void RegisterMacOSServices(IServiceCollection services) + { + services.AddSingleton(); + + // platform services + services.AddSingleton(); + services.AddSingleton(); + } } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs index 8d25b5b..332d094 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsCredentialsService.cs @@ -6,15 +6,14 @@ using Amazon.Runtime.CredentialManagement; using Amazon.SecurityToken; using Amazon.SecurityToken.Model; +using Microsoft.Extensions.Logging; namespace Ellosoft.AwsCredentialsManager.Services.AWS; public record AwsCredentialsData(string AccessKeyId, string SecretAccessKey, string SessionToken, DateTime ExpirationDateTime, string RoleArn); -public class AwsCredentialsService +public interface IAwsCredentialsService { - internal sealed record ProfileMetadata(string RoleArn, string AccessKey, DateTime Expiration); - /// /// Assume AWS role and retrieve its credentials by using the SAML authentication assertion /// @@ -31,6 +30,34 @@ internal sealed record ProfileMetadata(string RoleArn, string AccessKey, DateTim /// The AssumeRoleWithSAMLAsync issues an HTTP POST request to https://sts.amazonaws.com, which does not require a region, /// however the region is still required as part of the AmazonSecurityTokenServiceClient constructor validation, therefore USEast2 is being used. /// + Task GetAwsCredentials( + string samlAssertion, + string roleArn, + string idp, + int expirationInMinutes = 120); + + /// + /// Store AWS credentials for a specific profile in the AWS credentials file. + /// + /// The name of the AWS profile where the credentials should be stored. + /// AWS credentials to be stored. + void StoreCredentials(string awsProfileName, AwsCredentialsData credentials); + + /// + /// Retrieve AWS credentials for a specific profile from the AWS credentials file. + /// + /// The name of the AWS profile where the credentials should be retrieved. + /// + /// AwsCredentialsData containing retrieved AWS credentials or + /// null if the profile isn't found or if the credential is about expire (15 min threshold). + /// + AwsCredentialsData? GetCredentialsFromStore(string awsProfileName); +} + +public class AwsCredentialsService(ILogger logger) : IAwsCredentialsService +{ + internal sealed record ProfileMetadata(string RoleArn, string AccessKey, DateTime Expiration); + public async Task GetAwsCredentials( string samlAssertion, string roleArn, @@ -64,11 +91,6 @@ public async Task GetAwsCredentials( } } - /// - /// Store AWS credentials for a specific profile in the AWS credentials file. - /// - /// The name of the AWS profile where the credentials should be stored. - /// AWS credentials to be stored. public void StoreCredentials(string awsProfileName, AwsCredentialsData credentials) { var options = new CredentialProfileOptions @@ -86,14 +108,6 @@ public void StoreCredentials(string awsProfileName, AwsCredentialsData credentia SaveProfileMetadata(awsProfileName, new ProfileMetadata(credentials.RoleArn, credentials.AccessKeyId, credentials.ExpirationDateTime)); } - /// - /// Retrieve AWS credentials for a specific profile from the AWS credentials file. - /// - /// The name of the AWS profile where the credentials should be retrieved. - /// - /// AwsCredentialsData containing retrieved AWS credentials or - /// null if the profile isn't found or if the credential is about expire (15 min threshold). - /// public AwsCredentialsData? GetCredentialsFromStore(string awsProfileName) { var sharedFile = new CredentialProfileStoreChain(); @@ -117,27 +131,36 @@ public void StoreCredentials(string awsProfileName, AwsCredentialsData credentia profileMetadata.RoleArn); } - private static void SaveProfileMetadata(string profileName, ProfileMetadata metadata) + private ProfileMetadata? GetProfileMetadata(string profileName) { var profileMetadataPath = GetProfileMetadataFilePath(profileName); - Directory.CreateDirectory(Path.GetDirectoryName(profileMetadataPath)!); + if (!File.Exists(profileMetadataPath)) + return null; - File.WriteAllBytes(profileMetadataPath, - JsonSerializer.SerializeToUtf8Bytes(metadata, SourceGenerationContext.Default.ProfileMetadata)); + var bytes = File.ReadAllBytes(profileMetadataPath); + + try + { + return JsonSerializer.Deserialize(bytes, SourceGenerationContext.Default.ProfileMetadata); + } + catch (Exception e) + { + logger.LogError(e, "Unable to read AWS credentials profile metadata"); + + return null; + } } - private static ProfileMetadata? GetProfileMetadata(string profileName) + private static void SaveProfileMetadata(string profileName, ProfileMetadata metadata) { var profileMetadataPath = GetProfileMetadataFilePath(profileName); - if (!File.Exists(profileMetadataPath)) - return null; - - var bytes = File.ReadAllBytes(profileMetadataPath); + Directory.CreateDirectory(Path.GetDirectoryName(profileMetadataPath)!); - return JsonSerializer.Deserialize(bytes, SourceGenerationContext.Default.ProfileMetadata); + File.WriteAllBytes(profileMetadataPath, + JsonSerializer.SerializeToUtf8Bytes(metadata, SourceGenerationContext.Default.ProfileMetadata)); } - private static string GetProfileMetadataFilePath(string profileName) => AppDataDirectory.GetPath($"aws_profiles\\{profileName}"); + private static string GetProfileMetadataFilePath(string profileName) => AppDataDirectory.GetPath($"aws_profiles/{profileName}"); } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsSamlService.cs b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsSamlService.cs index be01855..af46fe4 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsSamlService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AWS/AwsSamlService.cs @@ -9,7 +9,7 @@ namespace Ellosoft.AwsCredentialsManager.Services.AWS; public record AwsRole(string RoleName, string AccountName); -public class AwsSamlService +public interface IAwsSamlService { /// /// Extracts AWS roles and IDP from an encoded SAML assertion and @@ -18,6 +18,20 @@ public class AwsSamlService /// Encoded SAML assertion /// Dictionary with AWS role ARN as the key and the IDP as the value /// + Dictionary GetAwsRolesAndIdpFromSamlAssertion(string encodedSamlAssertion); + + /// + /// Retrieves the AWS account names and roles for the current user + /// based on the provided SAML data. + /// + /// The SAML data containing the SAML assertion and sign-in URL + /// A list of AWS roles + /// + Task> GetAwsRolesWithAccountName(SamlData samlData); +} + +public class AwsSamlService : IAwsSamlService +{ public Dictionary GetAwsRolesAndIdpFromSamlAssertion(string encodedSamlAssertion) { var samlAssertion = Encoding.UTF8.GetString(Convert.FromBase64String(encodedSamlAssertion)); @@ -37,13 +51,6 @@ public Dictionary GetAwsRolesAndIdpFromSamlAssertion(string enco .ToDictionary(roleInfo => roleInfo[1], roleInfo => roleInfo[0]); } - /// - /// Retrieves the AWS account names and roles for the current user - /// based on the provided SAML data. - /// - /// The SAML data containing the SAML assertion and sign-in URL - /// A list of AWS roles - /// public async Task> GetAwsRolesWithAccountName(SamlData samlData) { if (samlData.SamlAssertion is null || samlData.SignInUrl is null) diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AWS/Interactive/AwsOktaSessionManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/AWS/Interactive/AwsOktaSessionManager.cs index 3b8bf12..1e4f7a0 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AWS/Interactive/AwsOktaSessionManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AWS/Interactive/AwsOktaSessionManager.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Amazon.Runtime; using Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; @@ -9,14 +8,18 @@ namespace Ellosoft.AwsCredentialsManager.Services.AWS.Interactive; +public interface IAwsOktaSessionManager +{ + Task CreateOrResumeSessionAsync(string credentialProfile, string? outputAwsProfile); +} + public class AwsOktaSessionManager( - CredentialsManager credentialsManager, + ICredentialsManager credentialsManager, IOktaLoginService loginService, - OktaSamlService oktaSamlService) + IOktaSamlService oktaSamlService, + IAwsCredentialsService awsCredentialsService, + IAwsSamlService awsSamlService) : IAwsOktaSessionManager { - private readonly AwsCredentialsService _awsCredentialsService = new(); - private readonly AwsSamlService _awsSamlService = new(); - public async Task CreateOrResumeSessionAsync(string credentialProfile, string? outputAwsProfile) { if (!credentialsManager.TryGetCredential(credentialProfile, out var credentialsConfig)) @@ -34,7 +37,7 @@ public class AwsOktaSessionManager( private bool TryResumeSession(string awsProfile, string roleArn, [NotNullWhen(true)] out AwsCredentialsData? credentialsData) { - credentialsData = _awsCredentialsService.GetCredentialsFromStore(awsProfile); + credentialsData = awsCredentialsService.GetCredentialsFromStore(awsProfile); if (credentialsData is null || credentialsData.RoleArn != roleArn) return false; @@ -72,16 +75,16 @@ Do you want renew the credentials now ?[/] if (idp is null) return null; - var awsCredentialsData = await _awsCredentialsService.GetAwsCredentials(samlData.SamlAssertion, credentialsConfig.RoleArn, idp); + var awsCredentialsData = await awsCredentialsService.GetAwsCredentials(samlData.SamlAssertion, credentialsConfig.RoleArn, idp); - _awsCredentialsService.StoreCredentials(awsProfile, awsCredentialsData); + awsCredentialsService.StoreCredentials(awsProfile, awsCredentialsData); return awsCredentialsData; } private string? GetRoleIdp(string credentialProfile, string roleArn, string samlAssertion) { - var roles = _awsSamlService.GetAwsRolesAndIdpFromSamlAssertion(samlAssertion); + var roles = awsSamlService.GetAwsRolesAndIdpFromSamlAssertion(samlAssertion); if (roles.TryGetValue(roleArn, out var idp)) return idp; @@ -103,7 +106,7 @@ private SessionAWSCredentials CreateAwsCredentials(AwsCredentialsData credential { if (outputAwsProfile is not null && outputAwsProfile != credentialProfile) { - _awsCredentialsService.StoreCredentials(outputAwsProfile, credentialsData); + awsCredentialsService.StoreCredentials(outputAwsProfile, credentialsData); } return new SessionAWSCredentials(credentialsData.AccessKeyId, credentialsData.SecretAccessKey, credentialsData.SessionToken); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AWS/RDSTokenGenerator.cs b/src/Ellosoft.AwsCredentialsManager/Services/AWS/RDSTokenGenerator.cs index bb5c06a..5ce75e5 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AWS/RDSTokenGenerator.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AWS/RDSTokenGenerator.cs @@ -10,7 +10,7 @@ namespace Ellosoft.AwsCredentialsManager.Services.AWS; -public class RdsTokenGenerator +public interface IRdsTokenGenerator { /// /// Generates an RDS DB password using AWS credentials @@ -26,6 +26,17 @@ public class RdsTokenGenerator /// This method is based on the , /// however it allows the DB password lifetime to be changed from the hard code 15 minutes. /// + string GenerateDbPassword( + AWSCredentials awsCredentials, + RegionEndpoint region, + string hostname, + int port, + string dbUser, + int ttlInMinutes); +} + +public class RdsTokenGenerator : IRdsTokenGenerator +{ public string GenerateDbPassword( AWSCredentials awsCredentials, RegionEndpoint region, diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs b/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs index 8ed30c2..4ba773f 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AppDataDirectory.cs @@ -23,8 +23,24 @@ public static class AppDataDirectory /// Gets the full path to the specified file in the application's data directory. /// /// The name of the file to get the path for. + /// If true, the directory will be created if it doesn't exist. Default is true. /// The full path to the specified file in the application's data directory. - public static string GetPath(string fileName) => IOPath.Combine(Path, fileName); + public static string GetPath(string fileName, bool createDirectory = true) + { + var path = IOPath.Combine(Path, fileName); + + if (!createDirectory) + return path; + + var directory = IOPath.GetDirectoryName(path); + + if (directory is not null) + { + Directory.CreateDirectory(directory); + } + + return path; + } private static string GetOrCreateAppDataDirectory() { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs b/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs index 8bef6ba..20eb93c 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/AppMetadata.cs @@ -7,6 +7,7 @@ namespace Ellosoft.AwsCredentialsManager.Services; public interface IAppMetadata { + public SemanticVersion? GetAppVersion(); (string executablePath, string appFolder) GetExecutablePath(); @@ -14,6 +15,8 @@ public interface IAppMetadata public class AppMetadata : IAppMetadata { + public const string AppName = "aws-cred-mgr"; + public SemanticVersion? GetAppVersion() { var versionValue = Assembly.GetEntryAssembly()? diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs index 8ed75d9..a844e19 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigManager.cs @@ -8,6 +8,8 @@ public interface IConfigManager { AppConfig AppConfig { get; } + ActiveToolConfiguration ToolConfig { get; } + string AppConfigPath { get; } /// @@ -24,12 +26,18 @@ public class ConfigManager : IConfigManager private readonly ConfigReader _configReader = new(); private readonly ConfigWriter _configWriter = new(); - public string AppConfigPath => InternalAppConfigPath; + public ConfigManager() + { + AppConfig = GetConfiguration(); + ToolConfig = new ActiveToolConfiguration(AppConfig.Config ?? new()); + } - public ConfigManager() => AppConfig = GetConfiguration(); + public string AppConfigPath => InternalAppConfigPath; public AppConfig AppConfig { get; } + public ActiveToolConfiguration ToolConfig { get; } + public void SaveConfig() => _configWriter.Write(AppConfigPath, AppConfig); private AppConfig GetConfiguration() => File.Exists(AppConfigPath) ? _configReader.Read(AppConfigPath) : new AppConfig(); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs index 02af5cc..2657a1d 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigReader.cs @@ -4,12 +4,18 @@ using System.Reflection; using System.Text; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; +using YamlDotNet.Core; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace Ellosoft.AwsCredentialsManager.Services.Configuration; -public class ConfigReader +public interface IConfigReader +{ + AppConfig Read(string filePath); +} + +public class ConfigReader : IConfigReader { private static readonly IDeserializer Deserializer = CreateDeserializer(); @@ -45,9 +51,6 @@ public AppConfig Read(string filePath) var rawConfigContent = yamlContent.ToString(); - static AppConfig DeserializeAppConfig(string content) - => Deserializer.Deserialize(content) ?? new AppConfig(); - if (!hasVariables) return DeserializeAppConfig(rawConfigContent); @@ -164,5 +167,19 @@ private static IDeserializer CreateDeserializer() => .WithNamingConvention(UnderscoredNamingConvention.Instance) .Build(); + private static AppConfig DeserializeAppConfig(string content) + { + try + { + return Deserializer.Deserialize(content) ?? new AppConfig(); + } + catch (YamlException e) + { + throw new InvalidOperationException($"Invalid configuration found at line {e.Start.Line} column {e.Start.Column}. " + + $"Update your configuration and try again." + Environment.NewLine + + $"[green]Tip[/]: Use the [green i]aws-cred-mgr config[/] command to open the configuration file"); + } + } + private static string GetYamlName(string value) => UnderscoredNamingConvention.Instance.Apply(value); } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigWriter.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigWriter.cs index 7a2944a..d3a5521 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigWriter.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/ConfigWriter.cs @@ -9,7 +9,17 @@ namespace Ellosoft.AwsCredentialsManager.Services.Configuration; -public class ConfigWriter +public interface IConfigWriter +{ + /// + /// Serializes an AppConfig into a output file, replacing variable values with variable placeholders + /// + /// Output file name + /// AppConfig object + void Write(string fileName, AppConfig config); +} + +public class ConfigWriter : IConfigWriter { private static readonly ISerializer Serializer = new SerializerBuilder() .ConfigureDefaultValuesHandling(DefaultValuesHandling.OmitNull) @@ -17,11 +27,6 @@ public class ConfigWriter .WithTypeInspector(inner => new ResourceConfigurationInspector(inner)) .Build(); - /// - /// Serializes an AppConfig into a output file, replacing variable values with variable placeholders - /// - /// Output file name - /// AppConfig object public void Write(string fileName, AppConfig config) { var writer = new StringBuilder(); @@ -37,6 +42,7 @@ public void Write(string fileName, AppConfig config) WriteProperty(writer, nameof(AppConfig.Templates), config.Templates); WriteProperty(writer, nameof(AppConfig.Credentials), config.Credentials); WriteProperty(writer, nameof(AppConfig.Environments), config.Environments); + WriteProperty(writer, nameof(AppConfig.Config), config.Config); CreateBackupFile(fileName); File.WriteAllText(fileName, writer.ToString(), Encoding.UTF8); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs index 8371c13..59bedda 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/CredentialsManager.cs @@ -1,12 +1,20 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics.CodeAnalysis; using Ellosoft.AwsCredentialsManager.Commands; using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; namespace Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; -public class CredentialsManager(IConfigManager configManager) +public interface ICredentialsManager +{ + string GetCredentialNameFromUser(); + + bool TryGetCredential(string credentialProfile, [NotNullWhen(true)] out CredentialsConfiguration? credentialsConfig); + + void CreateCredential(string name, string? awsProfile, string awsRole, string oktaAppUrl, string oktaProfile); +} + +public class CredentialsManager(IConfigManager configManager) : ICredentialsManager { public string GetCredentialNameFromUser() { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/EnvironmentManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/EnvironmentManager.cs index 72b2408..449e950 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/EnvironmentManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Interactive/EnvironmentManager.cs @@ -4,22 +4,20 @@ namespace Ellosoft.AwsCredentialsManager.Services.Configuration.Interactive; -public class EnvironmentManager +public interface IEnvironmentManager { - private sealed record Environment(string EnvironmentName, EnvironmentConfiguration? Configuration); + EnvironmentConfiguration GetOrCreateEnvironment(string? environment); - private readonly IConfigManager _configManager; - private readonly CredentialsManager _credentialsManager; + EnvironmentConfiguration? GetEnvironment(string environment); +} - public EnvironmentManager(IConfigManager configManager, CredentialsManager credentialsManager) - { - _configManager = configManager; - _credentialsManager = credentialsManager; - } +public class EnvironmentManager(IConfigManager configManager, ICredentialsManager credentialsManager) : IEnvironmentManager +{ + private sealed record Environment(string EnvironmentName, EnvironmentConfiguration? Configuration); public EnvironmentConfiguration GetOrCreateEnvironment(string? environment) { - var appConfig = _configManager.AppConfig; + var appConfig = configManager.AppConfig; if (environment is not null && appConfig.Environments.TryGetValue(environment, out var existingEnv)) return existingEnv; @@ -46,7 +44,7 @@ public EnvironmentConfiguration GetOrCreateEnvironment(string? environment) } public EnvironmentConfiguration? GetEnvironment(string environment) => - _configManager.AppConfig.Environments.TryGetValue(environment, out var env) ? env : null; + configManager.AppConfig.Environments.GetValueOrDefault(environment); private EnvironmentConfiguration CreateNewEnvironment(string? environment = null) { @@ -54,15 +52,15 @@ private EnvironmentConfiguration CreateNewEnvironment(string? environment = null AnsiConsole.MarkupLine($"Creating environment [green i]{environmentName}[/]"); - var credentialName = _credentialsManager.GetCredentialNameFromUser(); + var credentialName = credentialsManager.GetCredentialNameFromUser(); var environmentConfig = new EnvironmentConfiguration { Credential = credentialName }; - while (!_configManager.AppConfig.Environments.TryAdd(environmentName, environmentConfig)) + while (!configManager.AppConfig.Environments.TryAdd(environmentName, environmentConfig)) { environmentName = AnsiConsole.Ask("This environment already exists, please choose another name:"); } - _configManager.SaveConfig(); + configManager.SaveConfig(); AnsiConsole.MarkupLine("Environment created"); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/AppConfig.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/AppConfig.cs index d85fd8b..f347910 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/AppConfig.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/AppConfig.cs @@ -17,6 +17,10 @@ public class AppConfig public Dictionary Environments { get; set; } = new(); + public ToolConfiguration? Config { get; set; } = new(); + + internal VariablesSection? Variables { get; set; } + public class AuthenticationSection { public Dictionary Okta { get; set; } = new(); @@ -26,6 +30,4 @@ public class TemplatesSection { public Dictionary Rds { get; set; } = new(); } - - internal VariablesSection? Variables { get; set; } } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/ToolConfiguration.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/ToolConfiguration.cs new file mode 100644 index 0000000..a5f5531 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/Models/ToolConfiguration.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services.Configuration.Models; + +public class ToolConfiguration : ResourceConfiguration +{ + public bool? CopyToClipboard { get; set; } + + public bool? AwsIgnoreConfiguredEndpoints { get; set; } +} + +public class ActiveToolConfiguration(ToolConfiguration config) +{ + public bool CopyToClipboard => config.CopyToClipboard ?? true; + + public bool AwsIgnoreConfiguredEndpoints => config.AwsIgnoreConfiguredEndpoints ?? true; +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/UserCredentialsManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Configuration/UserCredentialsManager.cs deleted file mode 100644 index 15bede5..0000000 --- a/src/Ellosoft.AwsCredentialsManager/Services/Configuration/UserCredentialsManager.cs +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) 2023 Ellosoft Limited. All rights reserved. - -using System.Runtime.InteropServices; -using System.Text.Json; -using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; -using Ellosoft.AwsCredentialsManager.Services.Encryption; - -namespace Ellosoft.AwsCredentialsManager.Services.Configuration; - -public class UserCredentialsManager -{ - public bool SupportCredentialsStore { get; } = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - /// - /// Encrypt and save user credentials to the app data directory - /// - /// Credentials key - /// User credentials - /// If the file already exists it will be overwritten - public void SaveUserCredentials(string key, UserCredentials userCredentials) - { - var encryptedData = DataProtection.Encrypt(JsonSerializer.SerializeToUtf8Bytes(userCredentials, SourceGenerationContext.Default.UserCredentials)); - - File.WriteAllBytes(GetUserCredentialsFilePath(key), encryptedData); - } - - /// - /// Delete user credentials - /// - /// Credentials key - public void DeleteUserCredentials(string key) - { - var userProfileFilePath = GetUserCredentialsFilePath(key); - - if (File.Exists(userProfileFilePath)) - File.Delete(userProfileFilePath); - } - - /// - /// Read encrypted user credentials from app data directory - /// - /// Credentials key - /// This method will return null if no credentials file is found - public UserCredentials? GetUserCredentials(string key) - { - var userProfileFilePath = GetUserCredentialsFilePath(key); - - if (!File.Exists(userProfileFilePath)) - return null; - - var data = File.ReadAllBytes(userProfileFilePath); - var decryptedData = DataProtection.Decrypt(data); - - return JsonSerializer.Deserialize(decryptedData, SourceGenerationContext.Default.UserCredentials); - } - - private static string GetUserCredentialsFilePath(string key) => AppDataDirectory.GetPath($"{key}_profile.bin"); -} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Encryption/DataProtection.cs b/src/Ellosoft.AwsCredentialsManager/Services/Encryption/DataProtection.cs deleted file mode 100644 index 17dcddb..0000000 --- a/src/Ellosoft.AwsCredentialsManager/Services/Encryption/DataProtection.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2023 Ellosoft Limited. All rights reserved. - -using System.Runtime.InteropServices; - -namespace Ellosoft.AwsCredentialsManager.Services.Encryption; - -public static class DataProtection -{ - public static byte[] Encrypt(byte[] data) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException("Saving passwords is only supported on Windows at the moment"); - - return WindowsDataProtection.Protect(data); - } - - public static byte[] Decrypt(byte[] data) - { - if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - throw new PlatformNotSupportedException("Saving passwords is only supported on Windows at the moment"); - - return WindowsDataProtection.Unprotect(data); - } -} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Encryption/WindowsDataProtection.cs b/src/Ellosoft.AwsCredentialsManager/Services/Encryption/WindowsDataProtection.cs deleted file mode 100644 index dc8db44..0000000 --- a/src/Ellosoft.AwsCredentialsManager/Services/Encryption/WindowsDataProtection.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) 2023 Ellosoft Limited. All rights reserved. - -using System.Runtime.Versioning; -using System.Security.Cryptography; - -namespace Ellosoft.AwsCredentialsManager.Services.Encryption; - -[SupportedOSPlatform("windows")] -public static class WindowsDataProtection -{ - public static byte[] Protect(byte[] data) => ProtectedData.Protect(data, null, DataProtectionScope.CurrentUser); - - public static byte[] Unprotect(byte[] data) => ProtectedData.Unprotect(data, null, DataProtectionScope.CurrentUser); -} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs index 17914f6..94f407e 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/IO/FileManager.cs @@ -1,33 +1,37 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. using System.Diagnostics; -using System.Runtime.InteropServices; namespace Ellosoft.AwsCredentialsManager.Services.IO; public interface IFileManager { - void OpenFile(string filePath); + void OpenFileUsingDefaultApp(string filePath); + + bool FileExists(string filePath); + + public byte[] ReadFile(string filePath); + + void SaveFile(string filePath, byte[] data); + + void DeleteFile(string filePath); } -public class FileManager : IFileManager +[ExcludeFromCodeCoverage] +public class FileManager : PlatformServiceSlim, IFileManager { - public void OpenFile(string filePath) - { - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - Process.Start("explorer", $"\"{filePath}\""); + private readonly string _openCommand = ExecuteMultiPlatformCommand( + win: () => "explorer", + macos: () => "open", + linux: () => "xdg-open"); + + public void OpenFileUsingDefaultApp(string filePath) => Process.Start(_openCommand, $"\"{filePath}\""); - return; - } + public bool FileExists(string filePath) => File.Exists(filePath); - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) - { - Process.Start("open", $"\"{filePath}\""); + public byte[] ReadFile(string filePath) => File.ReadAllBytes(filePath); - return; - } + public void SaveFile(string filePath, byte[] data) => File.WriteAllBytes(filePath, data); - throw new InvalidOperationException("Unsupported operating system."); - } + public void DeleteFile(string filePath) => File.Delete(filePath); } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Interactive/OktaLoginService.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Interactive/OktaLoginService.cs index 8d48dca..d5f9f9d 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Interactive/OktaLoginService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Interactive/OktaLoginService.cs @@ -4,6 +4,7 @@ using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; using Ellosoft.AwsCredentialsManager.Services.Okta.Exceptions; using Ellosoft.AwsCredentialsManager.Services.Okta.Models; +using Ellosoft.AwsCredentialsManager.Services.Security; namespace Ellosoft.AwsCredentialsManager.Services.Okta.Interactive; @@ -13,71 +14,40 @@ public interface IOktaLoginService /// Execute an interactive Okta user login /// /// Okta profile + /// If true, a new Okta session will be created (default: false) /// Authentication result - Task InteractiveLogin(string oktaProfile); - - /// - /// Execute an interactive Okta user login returning an OKTA API access token as result - /// - /// Okta profile - /// Access token result - Task InteractiveGetAccessToken(string oktaProfile); + Task InteractiveLogin(string oktaProfile, bool createSession = false); Task Login(Uri oktaDomain, UserCredentials userCredentials, string? preferredMfaType = null, bool savedCredentials = false, string userProfileKey = OktaConfiguration.DefaultProfileName); } -public class OktaLoginService : IOktaLoginService +public class OktaLoginService( + IConfigManager configManager, + IUserCredentialsManager userCredentialsManager, + IOktaClassicAuthenticator classicAuthenticator) + : IOktaLoginService { - private readonly IConfigManager _configManager; - private readonly OktaClassicAuthenticator _oktaAuth; - private readonly OktaClassicAccessTokenProvider _oktaAccessTokenProvider; - - private readonly UserCredentialsManager _userCredentialsManager = new(); - - public OktaLoginService( - IConfigManager configManager, - OktaClassicAuthenticator classicAuthenticator, - OktaClassicAccessTokenProvider oktaClassicAccessTokenProvider) - { - _configManager = configManager; - _oktaAuth = classicAuthenticator; - _oktaAccessTokenProvider = oktaClassicAccessTokenProvider; - } - - public async Task InteractiveLogin(string oktaProfile) + public async Task InteractiveLogin(string oktaProfile, bool createSession = false) { var oktaConfig = GetOktaConfig(oktaProfile); var userCredentials = GetUserCredentials(oktaProfile, out var savedCredentials); var preferredMfa = GetOktaMfaFactorCode(oktaConfig.PreferredMfaType); - var authResult = await Login(new Uri(oktaConfig.OktaDomain), userCredentials, preferredMfa, savedCredentials, oktaProfile); + var oktaDomain = new Uri(oktaConfig.OktaDomain); + var authResult = await Login(oktaDomain, userCredentials, preferredMfa, savedCredentials, oktaProfile); - return authResult; - } + if (!createSession || authResult is not { Authenticated: true, SessionToken: not null }) + return authResult; - public async Task InteractiveGetAccessToken(string oktaProfile) - { - var oktaConfig = GetOktaConfig(oktaProfile); - var userCredentials = GetUserCredentials(oktaProfile, out var savedCredentials); - var preferredMfa = GetOktaMfaFactorCode(oktaConfig.PreferredMfaType); + var sessionResult = await classicAuthenticator.CreateSessionAsync(oktaDomain, authResult.SessionToken); - try + if (sessionResult is not null) { - var authResult = await _oktaAccessTokenProvider - .GetAccessTokenAsync(new Uri(oktaConfig.OktaDomain), userCredentials.Username, userCredentials.Password, preferredMfa); - - if (authResult is not null) - SaveUserCredentials(oktaProfile, userCredentials, savedCredentials); - - return authResult; + authResult = authResult with { SessionId = sessionResult.Id }; } - catch (Exception e) when (e is InvalidUsernameOrPasswordException or PasswordExpiredException) - { - ClearStoredPassword(oktaProfile, userCredentials); - return null; - } + return authResult; } public async Task Login(Uri oktaDomain, UserCredentials userCredentials, @@ -85,7 +55,7 @@ public async Task Login(Uri oktaDomain, UserCredentials us { try { - var authResult = await _oktaAuth.AuthenticateAsync(oktaDomain, userCredentials.Username, userCredentials.Password, preferredMfaType); + var authResult = await classicAuthenticator.AuthenticateAsync(oktaDomain, userCredentials.Username, userCredentials.Password, preferredMfaType); SaveUserCredentials(userProfileKey, userCredentials, savedCredentials); @@ -101,12 +71,12 @@ public async Task Login(Uri oktaDomain, UserCredentials us private void SaveUserCredentials(string userProfileKey, UserCredentials userCredentials, bool savedCredentials) { - if (savedCredentials || !_userCredentialsManager.SupportCredentialsStore) + if (savedCredentials || !userCredentialsManager.SupportCredentialsStore) return; if (AnsiConsole.Confirm("Do you want to save your Okta username and password for future logins ?")) { - _userCredentialsManager.SaveUserCredentials(userProfileKey, userCredentials); + userCredentialsManager.SaveUserCredentials(userProfileKey, userCredentials); return; } @@ -116,19 +86,15 @@ private void SaveUserCredentials(string userProfileKey, UserCredentials userCred private UserCredentials GetUserCredentials(string userProfileKey, out bool savedCredentials) { - UserCredentials? user = null; savedCredentials = false; - if (_userCredentialsManager.SupportCredentialsStore) - { - user = _userCredentialsManager.GetUserCredentials(userProfileKey); + var user = userCredentialsManager.GetUserCredentials(userProfileKey); - if (!string.IsNullOrWhiteSpace(user?.Password)) - { - savedCredentials = true; + if (!string.IsNullOrWhiteSpace(user?.Password)) + { + savedCredentials = true; - return user; - } + return user; } AnsiConsole.MarkupLine("Let's get you logged in !"); @@ -144,16 +110,13 @@ private UserCredentials GetUserCredentials(string userProfileKey, out bool saved private void ClearStoredPassword(string profileKey, UserCredentials userCredentials) { - if (!_userCredentialsManager.SupportCredentialsStore) - return; - var credentialsWithoutPasswords = userCredentials with { Password = string.Empty }; - _userCredentialsManager.SaveUserCredentials(profileKey, credentialsWithoutPasswords); + userCredentialsManager.SaveUserCredentials(profileKey, credentialsWithoutPasswords); } private OktaConfiguration GetOktaConfig(string profile) { - if (_configManager.AppConfig.Authentication?.Okta.TryGetValue(profile, out var config) == true) + if (configManager.AppConfig.Authentication?.Okta.TryGetValue(profile, out var config) == true) return config; throw new OktaProfileNotFoundException(profile); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/MfaHandlerProvider.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/MfaHandlerProvider.cs index 5fdbe20..f1a8ccd 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/MfaHandlerProvider.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/MfaHandlerProvider.cs @@ -2,7 +2,12 @@ namespace Ellosoft.AwsCredentialsManager.Services.Okta.MfaHandlers; -public class MfaHandlerProvider +public interface IMfaHandlerProvider +{ + IOktaMfaHandler GetOktaFactorHandler(HttpClient httpClient, string factorType); +} + +public class MfaHandlerProvider : IMfaHandlerProvider { public IOktaMfaHandler GetOktaFactorHandler(HttpClient httpClient, string factorType) { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/OktaPushFactorHandler.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/OktaPushFactorHandler.cs index c6a4313..5a498bf 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/OktaPushFactorHandler.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/MfaHandlers/OktaPushFactorHandler.cs @@ -5,12 +5,8 @@ namespace Ellosoft.AwsCredentialsManager.Services.Okta.MfaHandlers; -public class OktaPushFactorHandler : OktaFactorHandler +public class OktaPushFactorHandler(HttpClient httpClient) : OktaFactorHandler(httpClient) { - public OktaPushFactorHandler(HttpClient httpClient) : base(httpClient) - { - } - public override async Task VerifyFactorAsync(Uri oktaDomain, OktaFactor factor, string stateToken) { var verifyFactorRequest = new VerifyPushFactorRequest { StateToken = stateToken }; @@ -19,33 +15,46 @@ public override async Task VerifyFactorAsync(Uri okt Default.FactorVerificationResponsePushOktaFactor); var factorResult = mfaAuthResponse.FactorResult; + var challengeMsgShown = false; AnsiConsole.WriteLine("Okta push sent... Please check your phone"); AnsiConsole.WriteLine("Waiting response..."); while (factorResult == FactorResult.Waiting) { - await Task.Delay(2000); + await Task.Delay(5_000); mfaAuthResponse = await VerifyFactorAsync(oktaDomain, factor.Id, verifyFactorRequest, Default.VerifyPushFactorRequest, Default.FactorVerificationResponsePushOktaFactor); factorResult = mfaAuthResponse.FactorResult; - Verify3NumberPushMfaChallenge(mfaAuthResponse); + if (!challengeMsgShown && ChallengeRequired(mfaAuthResponse, out var factorChallenge)) + { + Show3NumberPushMfaChallengeMessage(factorChallenge); + challengeMsgShown = true; + } } return mfaAuthResponse; } - private static void Verify3NumberPushMfaChallenge(FactorVerificationResponse authResponse) + private static void Show3NumberPushMfaChallengeMessage(long factorChallenge) { - var factorChallenge = authResponse.Embedded.Factor?.Embedded?.Challenge.CorrectAnswer; + AnsiConsole.MarkupLine( + $"[yellow][[Extra Verification Required]][/] Please select the following number in your Okta Verify App: [bold yellow]{factorChallenge}[/]"); + } - if (factorChallenge is not null) + private static bool ChallengeRequired(FactorVerificationResponse authResponse, out long factorChallenge) + { + var correctAnswer = authResponse.Embedded.Factor?.Embedded?.Challenge.CorrectAnswer; + if (correctAnswer is not null) { - AnsiConsole.MarkupLine( - $"[yellow][[Extra Verification Required]][/] Please select the following number in your Okta Verify App: [bold yellow]{factorChallenge}[/]"); + factorChallenge = correctAnswer.Value; + return true; } + + factorChallenge = 0; + return false; } } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/AuthenticationResult.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/AuthenticationResult.cs index 8e4b5df..8558506 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/AuthenticationResult.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/AuthenticationResult.cs @@ -10,7 +10,10 @@ public record AuthenticationResult public string? StateToken { get; init; } + public string? SessionId { get; init; } + public string? SessionToken { get; init; } public bool Authenticated { get; init; } } + diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionRequest.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionRequest.cs new file mode 100644 index 0000000..7ff68d3 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionRequest.cs @@ -0,0 +1,5 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services.Okta.Models.HttpModels; + +public record CreateSessionRequest(string SessionToken); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionResult.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionResult.cs new file mode 100644 index 0000000..fdf398f --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/CreateSessionResult.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services.Okta.Models.HttpModels; + +public record CreateSessionResult +{ + public string Id { get; set; } = string.Empty; + + public string UserId { get; set; } = string.Empty; + + public string Login { get; set; } = string.Empty; + + public DateTime ExpiresAt { get; set; } + + public string Status { get; set; } = string.Empty; +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/OktaSourceGenerationContext.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/OktaSourceGenerationContext.cs index 1cf87ce..8be3ff4 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/OktaSourceGenerationContext.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/Models/HttpModels/OktaSourceGenerationContext.cs @@ -15,6 +15,8 @@ namespace Ellosoft.AwsCredentialsManager.Services.Okta.Models.HttpModels; [JsonSerializable(typeof(OktaApiError))] [JsonSerializable(typeof(TokenResponse))] [JsonSerializable(typeof(List))] +[JsonSerializable(typeof(CreateSessionRequest))] +[JsonSerializable(typeof(CreateSessionResult))] internal partial class OktaSourceGenerationContext : JsonSerializerContext { } diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAccessTokenProvider.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAccessTokenProvider.cs deleted file mode 100644 index a2d41c5..0000000 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAccessTokenProvider.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) 2023 Ellosoft Limited. All rights reserved. - -using System.Net; -using System.Net.Http.Json; -using System.Security.Cryptography; -using System.Text; -using System.Web; -using Ellosoft.AwsCredentialsManager.Services.Okta.Models; -using Ellosoft.AwsCredentialsManager.Services.Okta.Models.HttpModels; -using Microsoft.Extensions.DependencyInjection; - -namespace Ellosoft.AwsCredentialsManager.Services.Okta; - -public record AccessTokenResult(string AccessToken, AuthenticationResult AuthResult); - -public class OktaClassicAccessTokenProvider -{ - private const string OKTA_UI_CLIENT_ID = "okta.2b1959c8-bcc0-56eb-a589-cfcfb7422f26"; - - private readonly HttpClient _httpClient; - private readonly OktaClassicAuthenticator _authenticator; - - public OktaClassicAccessTokenProvider( - [FromKeyedServices(nameof(OktaHttpClientFactory))] HttpClient httpClient, - OktaClassicAuthenticator authenticator) - { - _httpClient = httpClient; - _authenticator = authenticator; - } - - /// - /// Get Okta API access token using Okta PKCE auth flow - /// - /// - /// - /// - /// - /// - public async Task GetAccessTokenAsync(Uri oktaDomain, string username, string password, string? preferredMfa) - { - var (codeVerifier, codeChallenge) = CreatePkceCodes(); - - // this call creates cookies needed by the token_redirect. - await AuthorizeAsync(oktaDomain, codeChallenge); - - var authResult = await _authenticator.AuthenticateAsync(oktaDomain, username, password, preferredMfa); - - await TokenRedirectAsync(oktaDomain, authResult.StateToken!); - - var authCode = await AuthorizeAsync(oktaDomain, codeChallenge); - - if (authCode is null) - return null; - - var accessToken = await GetAccessToken(oktaDomain, authCode, codeVerifier); - - return new AccessTokenResult(accessToken, authResult); - } - - private async Task AuthorizeAsync(Uri oktaDomain, string codeChallenge) - { - var parameters = new Dictionary - { - { "client_id", OKTA_UI_CLIENT_ID }, - { "code_challenge", codeChallenge }, - { "code_challenge_method", "S256" }, - { "nonce", GetRandomString() }, - { "redirect_uri", new Uri(oktaDomain, "/enduser/callback").ToString() }, - { "response_type", "code" }, - { "state", GetRandomString() }, - { "scope", "openid profile email okta.users.read.self okta.enduser.dashboard.read" } - }; - - var url = new Uri(oktaDomain, "/oauth2/v1/authorize?" + GetQueryParams(parameters)); - using var httpResponse = await _httpClient.GetAsync(url); - - if (httpResponse.IsSuccessStatusCode) - return null; - - if (httpResponse is { StatusCode: HttpStatusCode.Redirect or HttpStatusCode.TemporaryRedirect }) - { - var queryParameters = HttpUtility.ParseQueryString(httpResponse.Headers.Location?.Query ?? String.Empty); - - return queryParameters.Get("code"); - } - - throw new InvalidOperationException($"Invalid PKCE authorization response. Status Code: {httpResponse.StatusCode}"); - } - - private async Task TokenRedirectAsync(Uri oktaDomain, string stateToken) - { - var url = new Uri(oktaDomain, "/login/token/redirect?stateToken=" + stateToken); - using var httpResponse = await _httpClient.GetAsync(url); - - if (httpResponse is { StatusCode: HttpStatusCode.Redirect or HttpStatusCode.TemporaryRedirect }) - return; - - httpResponse.EnsureSuccessStatusCode(); - } - - private async Task GetAccessToken(Uri oktaDomain, string authCode, string codeVerifier) - { - var parameters = new Dictionary - { - { "client_id", OKTA_UI_CLIENT_ID }, - { "redirect_uri", new Uri(oktaDomain, "/enduser/callback").ToString() }, - { "grant_type", "authorization_code" }, - { "code_verifier", codeVerifier }, - { "code", authCode } - }; - - using var httpResponse = await _httpClient.PostAsync(new Uri(oktaDomain, "/oauth2/v1/token"), new FormUrlEncodedContent(parameters)); - var tokenResponse = await httpResponse.Content.ReadFromJsonAsync(OktaSourceGenerationContext.Default.TokenResponse); - - return tokenResponse?.AccessToken ?? throw new InvalidOperationException($"Invalid OAuth token response. Status Code: {httpResponse.StatusCode}"); - } - - private static string GetQueryParams(Dictionary parameters) => string.Join("&", parameters.Select(kv => $"{kv.Key}={kv.Value}")); - - private static (string codeVerifier, string codeChallenge) CreatePkceCodes() - { - var codeVerifier = GetRandomString() + GetRandomString(); - - var hashedCodeChallenge = SHA256.HashData(Encoding.UTF8.GetBytes(codeVerifier).ToArray()); - - var codeChallenge = Convert.ToBase64String(hashedCodeChallenge) - .Replace("+", "-") - .Replace("/", "_") - .TrimEnd('='); - - return (codeVerifier, codeChallenge); - } - - private static string GetRandomString() => Guid.NewGuid().ToString("N"); -} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAuthenticator.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAuthenticator.cs index a92e834..d792229 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAuthenticator.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaClassicAuthenticator.cs @@ -8,22 +8,23 @@ using Ellosoft.AwsCredentialsManager.Services.Okta.Models; using Ellosoft.AwsCredentialsManager.Services.Okta.Models.HttpModels; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; namespace Ellosoft.AwsCredentialsManager.Services.Okta; -public class OktaClassicAuthenticator +public interface IOktaClassicAuthenticator { - private readonly HttpClient _httpClient; - private readonly MfaHandlerProvider _mfaHandlerProvider = new(); - private readonly IOktaMfaFactorSelector _mfaFactorSelector; + Task AuthenticateAsync(Uri oktaDomain, string username, string password, string? preferredMfa); - public OktaClassicAuthenticator( - [FromKeyedServices(nameof(OktaHttpClientFactory))] HttpClient httpClient, - IOktaMfaFactorSelector mfaFactorSelector) - { - _httpClient = httpClient; - _mfaFactorSelector = mfaFactorSelector; - } + Task CreateSessionAsync(Uri oktaDomain, string sessionToken); +} + +public class OktaClassicAuthenticator( + ILogger logger, + [FromKeyedServices(nameof(OktaHttpClientFactory))] HttpClient httpClient, + IOktaMfaFactorSelector mfaFactorSelector) : IOktaClassicAuthenticator +{ + private readonly MfaHandlerProvider _mfaHandlerProvider = new(); public async Task AuthenticateAsync(Uri oktaDomain, string username, string password, string? preferredMfa) { @@ -52,6 +53,29 @@ public async Task AuthenticateAsync(Uri oktaDomain, string return HandleFailedAuthenticationResponse(oktaDomain, authResponse); } + public async Task CreateSessionAsync(Uri oktaDomain, string sessionToken) + { + var sessionUrl = new Uri(oktaDomain, "/api/v1/sessions"); + var sessionRequest = new CreateSessionRequest(sessionToken); + + var sessionResponse = await httpClient.PostAsJsonAsync( + sessionUrl, sessionRequest, OktaSourceGenerationContext.Default.CreateSessionRequest); + + if (sessionResponse.IsSuccessStatusCode) + { + var response = await sessionResponse.Content.ReadFromJsonAsync(OktaSourceGenerationContext.Default.CreateSessionResult); + + return response?.Status == "ACTIVE" ? response : null; + } + + var responseBody = await sessionResponse.Content.ReadAsStringAsync(); + + logger.LogError("Unable to create Okta session. Status Code: {StatusCode}. Response Body: {ResponseBody}", + sessionResponse.StatusCode, responseBody); + + return null; + } + private async Task AuthenticateWithUsernameAndPassword(Uri oktaDomain, string username, string password) { var authUrl = new Uri(oktaDomain, "/api/v1/authn"); @@ -62,7 +86,7 @@ private async Task AuthenticateWithUsernameAndPassword(U Password = password }; - using var httpResponse = await _httpClient.PostAsJsonAsync(authUrl, authRequest, OktaSourceGenerationContext.Default.AuthenticationRequest); + using var httpResponse = await httpClient.PostAsJsonAsync(authUrl, authRequest, OktaSourceGenerationContext.Default.AuthenticationRequest); if (httpResponse.IsSuccessStatusCode) { @@ -104,7 +128,7 @@ private async Task AuthenticateWithUsernameAndPassword(U } } - var selectedFactor = _mfaFactorSelector.GetMfaFactor(preferredMfaType, availableMfaFactors); + var selectedFactor = mfaFactorSelector.GetMfaFactor(preferredMfaType, availableMfaFactors); factorResponse = await ExecuteMfaFactorHandler(oktaDomain, selectedFactor, authResponse.StateToken); @@ -113,7 +137,7 @@ private async Task AuthenticateWithUsernameAndPassword(U private async Task ExecuteMfaFactorHandler(Uri oktaDomain, OktaFactor factor, string stateToken) { - var handler = _mfaHandlerProvider.GetOktaFactorHandler(_httpClient, factor.FactorType); + var handler = _mfaHandlerProvider.GetOktaFactorHandler(httpClient, factor.FactorType); var mfaVerificationResponse = await handler.VerifyFactorAsync(oktaDomain, factor, stateToken); diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaHttpClientFactory.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaHttpClientFactory.cs index 9ac425d..bc46e05 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaHttpClientFactory.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaHttpClientFactory.cs @@ -1,7 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Net; - namespace Ellosoft.AwsCredentialsManager.Services.Okta; public static class OktaHttpClientFactory @@ -14,13 +12,7 @@ public static HttpClient CreateHttpClient() { const string USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0"; - var handler = new HttpClientHandler - { - CookieContainer = new CookieContainer(), - AllowAutoRedirect = false - }; - - var httpClient = new HttpClient(handler); + var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("User-Agent", USER_AGENT); return httpClient; diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaSamlService.cs b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaSamlService.cs index f858e6b..e5481ab 100644 --- a/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaSamlService.cs +++ b/src/Ellosoft.AwsCredentialsManager/Services/Okta/OktaSamlService.cs @@ -6,7 +6,12 @@ namespace Ellosoft.AwsCredentialsManager.Services.Okta; public record SamlData(string SamlAssertion, string SignInUrl, string RelayState); -public class OktaSamlService +public interface IOktaSamlService +{ + Task GetAppSamlDataAsync(Uri oktaDomain, string oktaAppUrl, string sessionToken); +} + +public class OktaSamlService : IOktaSamlService { public async Task GetAppSamlDataAsync(Uri oktaDomain, string oktaAppUrl, string sessionToken) { diff --git a/src/Ellosoft.AwsCredentialsManager/Services/PlatformServiceSlim.cs b/src/Ellosoft.AwsCredentialsManager/Services/PlatformServiceSlim.cs new file mode 100644 index 0000000..07c68ed --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/PlatformServiceSlim.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services; + +public abstract class PlatformServiceSlim +{ + protected static T ExecuteMultiPlatformCommand( + Func? win = null, + Func? macos = null, + Func? linux = null, + Func? nonSupported = null) + { + if (OperatingSystem.IsWindows() && win is not null) + return win(); + + if (OperatingSystem.IsMacOS() && macos is not null) + return macos(); + + if (OperatingSystem.IsLinux() && linux is not null) + return linux(); + + return HandleNonSupportedPlatformException(nonSupported); + } + + private static T HandleNonSupportedPlatformException(Func? nonSupported) + { + if (nonSupported is not null) + return nonSupported(); + + throw new PlatformNotSupportedException("This operation is not supported on this platform."); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/IntPtrExtensions.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/IntPtrExtensions.cs new file mode 100644 index 0000000..f8bacf0 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/IntPtrExtensions.cs @@ -0,0 +1,22 @@ +using System.Runtime.InteropServices; +using Serilog; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS; + +public static class IntPtrExtensions +{ + public static void SafeReleaseIntPrtMem(this IntPtr handle) + { + try + { + if (handle == IntPtr.Zero) + return; + + Marshal.FreeHGlobal(handle); + } + catch (Exception e) + { + Log.Logger.Error(e, "Unable to release memory"); + } + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSData.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSData.cs new file mode 100644 index 0000000..2127786 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSData.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +[SupportedOSPlatform("macos")] +public class NSData : NSObject +{ + private static readonly IntPtr LengthSelector = GetSelector("length"); + private static readonly IntPtr BytesSelector = GetSelector("bytes"); + + public NSData(IntPtr handle) => Handle = handle; + + public override string ToString() + { + var length = (int)ObjectiveCRuntimeInterop.Instance.SendMessage(Handle, LengthSelector); + var bytes = ObjectiveCRuntimeInterop.Instance.SendMessage(Handle, BytesSelector); + + var buffer = new byte[length]; + Marshal.Copy(bytes, buffer, 0, length); + return Encoding.UTF8.GetString(buffer); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSMutableDictionary.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSMutableDictionary.cs new file mode 100644 index 0000000..5c73223 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSMutableDictionary.cs @@ -0,0 +1,42 @@ +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +[SupportedOSPlatform("macos")] +public class NSMutableDictionary : NSObject +{ + private static readonly IntPtr NSClassType = GetClass("NSMutableDictionary"); + private static readonly IntPtr CreateDictionarySelector = GetSelector("dictionary"); + private static readonly IntPtr AddKeyValueSelector = GetSelector("setObject:forKey:"); + + public NSMutableDictionary() + { + Handle = ObjectiveCRuntimeInterop.Instance.SendMessage(NSClassType, CreateDictionarySelector); + } + + public void Add(string key, object value) + { + var nsKey = new NSString(key); + var nsValue = ConvertManagedTypeToNSObject(value); + + ObjectiveCRuntimeInterop.Instance.SendMessage(Handle, AddKeyValueSelector, nsValue.Handle, nsKey.Handle); + } + + private static NSObject ConvertManagedTypeToNSObject(object value) => value switch + { + string s => new NSString(s), + bool b => new NSNumber(b), + int i => new NSNumber(i), + _ => throw new NotSupportedException($"Unsupported type: {value.GetType()}") + }; + + public static NSMutableDictionary Create(Dictionary dictionary) + { + var nsMutableDictionary = new NSMutableDictionary(); + + foreach (var (key, value) in dictionary) + { + nsMutableDictionary.Add(key, value); + } + + return nsMutableDictionary; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSNumber.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSNumber.cs new file mode 100644 index 0000000..e88d680 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSNumber.cs @@ -0,0 +1,19 @@ +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +[SupportedOSPlatform("macos")] +public class NSNumber : NSObject +{ + private static readonly IntPtr NSClassType = GetClass("NSNumber"); + private static readonly IntPtr CreateBoolSelector = GetSelector("numberWithBool:"); + private static readonly Lazy CreateIntSelector = GetSelectorLazy("numberWithInt:"); + + public NSNumber(bool value) + { + Handle = ObjectiveCRuntimeInterop.Instance.SendMessage(NSClassType, CreateBoolSelector, value); + } + + public NSNumber(int value) + { + Handle = ObjectiveCRuntimeInterop.Instance.SendMessage(NSClassType, CreateIntSelector.Value, value); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSObject.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSObject.cs new file mode 100644 index 0000000..b03f414 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSObject.cs @@ -0,0 +1,27 @@ +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +[SupportedOSPlatform("macos")] +public abstract class NSObject : IDisposable +{ + public IntPtr Handle { get; protected init; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposing) return; + + Handle.SafeReleaseIntPrtMem(); + } + + protected static IntPtr GetClass(string name) => ObjectiveCRuntimeInterop.Instance.GetClass(name); + protected static IntPtr GetSelector(string name) => ObjectiveCRuntimeInterop.Instance.RegisterSelector(name); + + protected static Lazy GetClassLazy(string name) => new(() => GetClass(name)); + + protected static Lazy GetSelectorLazy(string name) => new(() => GetSelector(name)); +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSString.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSString.cs new file mode 100644 index 0000000..85edeae --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/NSTypes/NSString.cs @@ -0,0 +1,13 @@ +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +[SupportedOSPlatform("macos")] +public class NSString : NSObject +{ + private static readonly IntPtr NSClassType = GetClass("NSString"); + private static readonly IntPtr CreateStringSelector = GetSelector("stringWithUTF8String:"); + + public NSString(string value) + { + Handle = ObjectiveCRuntimeInterop.Instance.SendMessage(NSClassType, CreateStringSelector, value); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/ObjectiveCRuntimeInterop.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/ObjectiveCRuntimeInterop.cs new file mode 100644 index 0000000..7f02069 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/ObjectiveCRuntimeInterop.cs @@ -0,0 +1,68 @@ +using System.Runtime.InteropServices; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS; + +public interface IObjectiveCRuntimeInterop +{ + IntPtr SendMessage(IntPtr receiver, IntPtr selector); + + IntPtr SendMessage(IntPtr receiver, IntPtr selector, IntPtr arg1); + + IntPtr SendMessage(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2); + + IntPtr SendMessage(IntPtr receiver, IntPtr selector, string arg1); + + IntPtr SendMessage(IntPtr receiver, IntPtr selector, bool arg1); + + IntPtr GetClass(string name); + + IntPtr RegisterSelector(string name); +} + +#pragma warning disable S4200 + +[SupportedOSPlatform("macos")] +[ExcludeFromCodeCoverage] +public class ObjectiveCRuntimeInterop : IObjectiveCRuntimeInterop +{ + private ObjectiveCRuntimeInterop() { } + + private const string LIBRARY_NAME = "/usr/lib/libobjc.dylib"; + + public static IObjectiveCRuntimeInterop Instance { get; set; } = new ObjectiveCRuntimeInterop(); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_msgSend")] + private static extern IntPtr SendMessageNative(IntPtr receiver, IntPtr selector); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_msgSend")] + private static extern IntPtr SendMessageNative(IntPtr receiver, IntPtr selector, IntPtr arg1); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_msgSend")] + private static extern IntPtr SendMessageNative(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_msgSend")] + private static extern IntPtr SendMessageNative(IntPtr receiver, IntPtr selector, string arg1); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_msgSend")] + private static extern IntPtr SendMessageNative(IntPtr receiver, IntPtr selector, bool arg1); + + [DllImport(LIBRARY_NAME, EntryPoint = "objc_getClass")] + private static extern IntPtr GetClassNative(string name); + + [DllImport(LIBRARY_NAME, EntryPoint = "sel_registerName")] + private static extern IntPtr RegisterSelectorNative(string name); + + public IntPtr SendMessage(IntPtr receiver, IntPtr selector) => SendMessageNative(receiver, selector); + + public IntPtr SendMessage(IntPtr receiver, IntPtr selector, IntPtr arg1) => SendMessageNative(receiver, selector, arg1); + + public IntPtr SendMessage(IntPtr receiver, IntPtr selector, IntPtr arg1, IntPtr arg2) => SendMessageNative(receiver, selector, arg1, arg2); + + public IntPtr SendMessage(IntPtr receiver, IntPtr selector, string arg1) => SendMessageNative(receiver, selector, arg1); + + public IntPtr SendMessage(IntPtr receiver, IntPtr selector, bool arg1) => SendMessageNative(receiver, selector, arg1); + + public IntPtr GetClass(string name) => GetClassNative(name); + + public IntPtr RegisterSelector(string name) => RegisterSelectorNative(name); +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainConstants.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainConstants.cs new file mode 100644 index 0000000..558f5f3 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainConstants.cs @@ -0,0 +1,17 @@ +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; + +// ReSharper disable InconsistentNaming +public static class KeychainConstants +{ + public const string kSecClassGenericPassword = "genp"; + + public static class QueryKeys + { + public const string kSecClass = "class"; + public const string kSecAttrAccount = "acct"; + public const string kSecAttrService = "svce"; + public const string kSecValueData = "v_Data"; + + public const string kSecReturnData = "r_Data"; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainService.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainService.cs new file mode 100644 index 0000000..3b30a12 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/KeychainService.cs @@ -0,0 +1,68 @@ +using Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.NSTypes; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; + +public interface IKeychainService +{ + SecurityItemResult AddGenericPassword(string account, string service, string password); + + string? GetGenericPassword(string account, string service); + + SecurityItemResult DeleteItem(string account, string service); +} + +[SupportedOSPlatform("macos")] +public class KeychainService(IMacOsKeychainInterop keychainInterop) : IKeychainService +{ + public SecurityItemResult AddGenericPassword(string account, string service, string password) + { + var query = new Dictionary + { + { KeychainConstants.QueryKeys.kSecClass, KeychainConstants.kSecClassGenericPassword }, + { KeychainConstants.QueryKeys.kSecAttrAccount, account }, + { KeychainConstants.QueryKeys.kSecAttrService, service }, + { KeychainConstants.QueryKeys.kSecValueData, password }, + }; + + using var nsQuery = NSMutableDictionary.Create(query); + + return keychainInterop.SecItemAdd(nsQuery.Handle, IntPtr.Zero); + } + + public string? GetGenericPassword(string account, string service) + { + var query = new Dictionary + { + { KeychainConstants.QueryKeys.kSecClass, KeychainConstants.kSecClassGenericPassword }, + { KeychainConstants.QueryKeys.kSecAttrAccount, account }, + { KeychainConstants.QueryKeys.kSecAttrService, service }, + { KeychainConstants.QueryKeys.kSecReturnData, true } + }; + + using var nsQuery = NSMutableDictionary.Create(query); + var result = keychainInterop.SecItemCopyMatching(nsQuery.Handle, out var resultPtr); + + if (result == 0 && resultPtr != IntPtr.Zero) + { + using var nsData = new NSData(resultPtr); + + return nsData.ToString(); + } + + return null; + } + + public SecurityItemResult DeleteItem(string account, string service) + { + var query = new Dictionary + { + { KeychainConstants.QueryKeys.kSecClass, KeychainConstants.kSecClassGenericPassword }, + { KeychainConstants.QueryKeys.kSecAttrAccount, account }, + { KeychainConstants.QueryKeys.kSecAttrService, service }, + }; + + using var nsQuery = NSMutableDictionary.Create(query); + + return keychainInterop.SecItemDelete(nsQuery.Handle); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/MacOsKeychainInterop.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/MacOsKeychainInterop.cs new file mode 100644 index 0000000..52b39d7 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/MacOsKeychainInterop.cs @@ -0,0 +1,36 @@ +using System.Runtime.InteropServices; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; + +public interface IMacOsKeychainInterop +{ + SecurityItemResult SecItemAdd(IntPtr query, IntPtr result); + + SecurityItemResult SecItemCopyMatching(IntPtr query, out IntPtr result); + + SecurityItemResult SecItemDelete(IntPtr query); +} + +#pragma warning disable S4200 + +[SupportedOSPlatform("macos")] +[ExcludeFromCodeCoverage] +public class MacOsKeychainInterop : IMacOsKeychainInterop +{ + private const string SECURITY_LIBRARY = "/System/Library/Frameworks/Security.framework/Security"; + + [DllImport(SECURITY_LIBRARY, EntryPoint = "SecItemAdd")] + private static extern SecurityItemResult SecItemAddNative(IntPtr query, IntPtr result); + + [DllImport(SECURITY_LIBRARY, EntryPoint = "SecItemCopyMatching")] + private static extern SecurityItemResult SecItemCopyMatchingNative(IntPtr query, out IntPtr result); + + [DllImport(SECURITY_LIBRARY, EntryPoint = "SecItemDelete")] + private static extern SecurityItemResult SecItemDeleteNative(IntPtr query); + + public SecurityItemResult SecItemAdd(IntPtr query, IntPtr result) => SecItemAddNative(query, result); + + public SecurityItemResult SecItemCopyMatching(IntPtr query, out IntPtr result) => SecItemCopyMatchingNative(query, out result); + + public SecurityItemResult SecItemDelete(IntPtr query) => SecItemDeleteNative(query); +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/SecurityItemResult.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/SecurityItemResult.cs new file mode 100644 index 0000000..1624f58 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/MacOS/Security/SecurityItemResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; + +public enum SecurityItemResult +{ + Success = 0, + DuplicateItem = -25299, + ItemNotFound = -25300, + AuthFailed = -25293, + Param = -50, + Allocate = -108 +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Platforms/Windows/Security/ProtectedDataService.cs b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/Windows/Security/ProtectedDataService.cs new file mode 100644 index 0000000..0f94a78 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Platforms/Windows/Security/ProtectedDataService.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using System.Security.Cryptography; + +namespace Ellosoft.AwsCredentialsManager.Services.Platforms.Windows.Security; + +public interface IProtectedDataService +{ + byte[] Protect(byte[] data, byte[] scope); + + byte[] Unprotect(byte[] data, byte[] scope); +} + +[SupportedOSPlatform("windows")] +[ExcludeFromCodeCoverage] +public class ProtectedDataService : IProtectedDataService +{ + public byte[] Protect(byte[] data, byte[] scope) => ProtectedData.Protect(data, scope, DataProtectionScope.CurrentUser); + + public byte[] Unprotect(byte[] data, byte[] scope) => ProtectedData.Unprotect(data, scope, DataProtectionScope.CurrentUser); +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorage.cs b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorage.cs new file mode 100644 index 0000000..ede7cf8 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorage.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +namespace Ellosoft.AwsCredentialsManager.Services.Security; + +public interface ISecureStorage +{ + void StoreSecret(string key, string data); + + void DeleteSecret(string key); + + bool TryRetrieveSecret(string key, [NotNullWhen(true)] out string? data); +} + +public class SecureStorage : ISecureStorage +{ + public virtual void StoreSecret(string key, string data) {} + + public virtual void DeleteSecret(string key) { } + + public virtual bool TryRetrieveSecret(string key, [NotNullWhen(true)] out string? data) + { + data = null; + + return false; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageMacOS.cs b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageMacOS.cs new file mode 100644 index 0000000..608ebc2 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageMacOS.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +using Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; + +namespace Ellosoft.AwsCredentialsManager.Services.Security; + +[SupportedOSPlatform("macos")] +public class SecureStorageMacOS(IKeychainService keychainService) : SecureStorage +{ + public override void StoreSecret(string key, string data) + { + var result = keychainService.AddGenericPassword(key, AppMetadata.AppName, data); + + if (result == SecurityItemResult.Success) + return; + + if (result == SecurityItemResult.DuplicateItem) + { + keychainService.DeleteItem(key, AppMetadata.AppName); + keychainService.AddGenericPassword(key, AppMetadata.AppName, data); + } + } + + public override void DeleteSecret(string key) => keychainService.DeleteItem(key, AppMetadata.AppName); + + public override bool TryRetrieveSecret(string key, [NotNullWhen(true)] out string? data) + { + data = keychainService.GetGenericPassword(key, AppMetadata.AppName); + + return !string.IsNullOrEmpty(data); + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageWindows.cs b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageWindows.cs new file mode 100644 index 0000000..9798592 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Security/SecureStorageWindows.cs @@ -0,0 +1,42 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +using System.Text; +using Ellosoft.AwsCredentialsManager.Services.IO; +using Ellosoft.AwsCredentialsManager.Services.Platforms.Windows.Security; + +namespace Ellosoft.AwsCredentialsManager.Services.Security; + +[SupportedOSPlatform("windows")] +public class SecureStorageWindows( + IProtectedDataService protectedDataService, + IFileManager fileManager) : SecureStorage +{ + private static readonly byte[] Scope = Encoding.UTF8.GetBytes(AppMetadata.AppName); + + public override void StoreSecret(string key, string data) + { + var protectedData = protectedDataService.Protect(Encoding.UTF8.GetBytes(data), Scope); + var credentialPath = GetCredentialPath(key); + + fileManager.SaveFile(credentialPath, protectedData); + } + + public override void DeleteSecret(string key) => fileManager.DeleteFile(GetCredentialPath(key)); + + public override bool TryRetrieveSecret(string key, [NotNullWhen(true)] out string? data) + { + data = null; + var credentialPath = GetCredentialPath(key); + + if (!fileManager.FileExists(credentialPath)) + return false; + + var protectedData = fileManager.ReadFile(credentialPath); + var unprotectedData = protectedDataService.Unprotect(protectedData, Scope); + data = Encoding.UTF8.GetString(unprotectedData); + + return !string.IsNullOrEmpty(data); + } + + private static string GetCredentialPath(string key) => AppDataDirectory.GetPath($"credentials/{key}.bin"); +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Security/UserCredentialsManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Security/UserCredentialsManager.cs new file mode 100644 index 0000000..7f9e3aa --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Security/UserCredentialsManager.cs @@ -0,0 +1,43 @@ +// Copyright (c) 2023 Ellosoft Limited. All rights reserved. + +using System.Text.Json; +using Ellosoft.AwsCredentialsManager.Services.Configuration.Models; + +namespace Ellosoft.AwsCredentialsManager.Services.Security; + +public interface IUserCredentialsManager +{ + bool SupportCredentialsStore { get; } + + /// + /// Encrypt and save user credentials to the app data directory + /// + /// Credentials key + /// User credentials + /// If the file already exists it will be overwritten + void SaveUserCredentials(string key, UserCredentials userCredentials); + + /// + /// Read encrypted user credentials from app data directory + /// + /// Credentials key + /// This method will return null if no credentials file is found + UserCredentials? GetUserCredentials(string key); +} + +public class UserCredentialsManager(ISecureStorage secureStorage) : IUserCredentialsManager +{ + public bool SupportCredentialsStore => OperatingSystem.IsWindows() || OperatingSystem.IsMacOS(); + + public void SaveUserCredentials(string key, UserCredentials userCredentials) + { + var credentialsJson = JsonSerializer.Serialize(userCredentials, SourceGenerationContext.Default.UserCredentials); + secureStorage.StoreSecret(key, credentialsJson); + } + + public UserCredentials? GetUserCredentials(string key) + { + return secureStorage.TryRetrieveSecret(key, out var data) ? + JsonSerializer.Deserialize(data, SourceGenerationContext.Default.UserCredentials) : null; + } +} diff --git a/src/Ellosoft.AwsCredentialsManager/Services/Utilities/ClipboardManager.cs b/src/Ellosoft.AwsCredentialsManager/Services/Utilities/ClipboardManager.cs new file mode 100644 index 0000000..ab5e710 --- /dev/null +++ b/src/Ellosoft.AwsCredentialsManager/Services/Utilities/ClipboardManager.cs @@ -0,0 +1,57 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +using System.Diagnostics; + +namespace Ellosoft.AwsCredentialsManager.Services.Utilities; + +public interface IClipboardManager +{ + /// + /// Set the clipboard text + /// + /// Text to copy to the clipboard + /// True if the operation was successful, false otherwise + bool SetClipboardText(string text); +} + +public class ClipboardManager : PlatformServiceSlim, IClipboardManager +{ + private readonly string _command = ExecuteMultiPlatformCommand( + win: () => "clip", + macos: () => "/usr/bin/pbcopy", + linux: () => "xclip"); + + public bool SetClipboardText(string text) => ExecuteCliCommand(_command, text) == 0; + + private static int ExecuteCliCommand(string command, string? stdin = null) + { + using var process = new Process(); + + process.StartInfo = new ProcessStartInfo + { + FileName = command, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + process.Start(); + + if (stdin is not null) + { + using var writer = process.StandardInput; + + if (writer.BaseStream.CanWrite) + { + writer.Write(stdin); + writer.Flush(); + } + + writer.Close(); + } + + process.WaitForExit(); + + return process.ExitCode; + } +} diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj index 1b73163..02ac85f 100644 --- a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj +++ b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj @@ -4,30 +4,32 @@ net8.0 false true + true - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all - + - + diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs b/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs new file mode 100644 index 0000000..0adce7e --- /dev/null +++ b/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) 2024 Ellosoft Limited. All rights reserved. + +#pragma warning disable IDE0005 + +using Ellosoft.AwsCredentialsManager.Commands.Config; +using Ellosoft.AwsCredentialsManager.Commands.Credentials; +using Ellosoft.AwsCredentialsManager.Commands.Okta; +using Ellosoft.AwsCredentialsManager.Commands.RDS; +using Ellosoft.AwsCredentialsManager.Infrastructure.Cli; +using Microsoft.Extensions.DependencyInjection; +using Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; +using Ellosoft.AwsCredentialsManager.Services.Security; +using FluentAssertions; +using Spectre.Console.Cli; +using Spectre.Console.Testing; + +namespace Ellosoft.AwsCredentialsManager.Tests; + +public class ServiceRegistrationTests +{ + private readonly IServiceProvider _serviceProvider; + private readonly IServiceCollection _services; + + public ServiceRegistrationTests() + { + _services = new ServiceCollection(); + + _services.AddLogging(); + _services.RegisterAppServices(); + + _serviceProvider = _services.BuildServiceProvider(); + } + + [Theory] + // okta + [InlineData(typeof(SetupOkta))] + // config + [InlineData(typeof(OpenAwsConfig))] + [InlineData(typeof(OpenConfig))] + // credentials + [InlineData(typeof(CreateCredentialsProfile))] + [InlineData(typeof(GetCredentials))] + [InlineData(typeof(ListCredentialsProfiles))] + // rds + [InlineData(typeof(GetRdsPassword))] + [InlineData(typeof(ListRdsProfiles))] + public void GetService_UsingCommands_ShouldResolveDependentServices(Type commandType) + { + var registrar = new TypeRegistrar(_services); + + var app = new CommandAppTester(registrar); + app.Configure(config => config.PropagateExceptions()); + + var setDefaultCommandMethod = app.GetType().GetMethod(nameof(CommandAppTester.SetDefaultCommand)); + var genericMethod = setDefaultCommandMethod!.MakeGenericMethod(commandType); + genericMethod.Invoke(app, [null, null]); + + try + { + _ = app.Run("__default_command"); + } + catch (Exception ex) + { + ex.InnerException?.Message.Should().NotContain("Unable to resolve service for type"); + } + } + +#if MACOS + [Theory] + [InlineData(typeof(ISecureStorage), typeof(SecureStorageMacOS))] + [InlineData(typeof(IKeychainService), typeof(KeychainService))] + [InlineData(typeof(IMacOsKeychainInterop), typeof(MacOsKeychainInterop))] + public void MacOS_GetService_ShouldResolveService(Type serviceType, Type implementationType) + { + TestServiceResolution(serviceType, implementationType); + } +#endif + +#if WINDOWS + [Theory] + [InlineData(typeof(ISecureStorage), typeof(SecureStorageWindows))] + [InlineData(typeof(IProtectedDataService), typeof(ProtectedDataService))] + public void Windows_GetService_ShouldResolveService(Type serviceType, Type implementationType) + { + TestServiceResolution(serviceType, implementationType); + } +#endif + + private void TestServiceResolution(Type serviceType, Type implementationType) + { + var resolvedServices = _serviceProvider.GetServices(serviceType).ToList(); + + resolvedServices.Should().Contain(s => s!.GetType().IsAssignableTo(implementationType)); + } +} From 9ef4477ccb72a9afc244da2adab18c6dc1afac57 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 19 Aug 2024 06:10:02 +0100 Subject: [PATCH 02/13] macOS Support (#40) ## New features - Automatic HTTPS prefix on Okta Domain during Okta Setup - Automatic copy RDS pwd to clipboard (#36) - MacOS Support (#35) - New Open Logs Command (#39) ## Bug Fixes - Error: Unable to assume AWS role bug (#29) - Error message for incorrect configuration is not a user-friendly bug (#38) - Unable to retrieve OKTA apps automatically bug (#37) - Improves Push MFA, challenge message --- src/Ellosoft.AwsCredentialsManager/Program.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Ellosoft.AwsCredentialsManager/Program.cs b/src/Ellosoft.AwsCredentialsManager/Program.cs index 7a7d494..43f7a5b 100644 --- a/src/Ellosoft.AwsCredentialsManager/Program.cs +++ b/src/Ellosoft.AwsCredentialsManager/Program.cs @@ -1,6 +1,5 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. -using System.Diagnostics; using Ellosoft.AwsCredentialsManager; using Ellosoft.AwsCredentialsManager.Commands; using Ellosoft.AwsCredentialsManager.Commands.Config; @@ -62,7 +61,7 @@ #if DEBUG config.ValidateExamples(); - if (Debugger.IsAttached) + if (System.Diagnostics.Debugger.IsAttached) args = "rds pwd local".Split(' '); #endif }); From 64822777c89d6d12fcba4d09ad37f8f3a421a83c Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 19 Aug 2024 06:19:05 +0100 Subject: [PATCH 03/13] Adding win zip upload --- .github/workflows/release.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 523e405..4342b82 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -41,20 +41,6 @@ jobs: directory: output-win/ path: aws-cred-mgr.exe filename: aws-cred-mgr-${{ github.ref_name }}-win-x64.zip - - name: Zip Output MacOS x64 - uses: thedoctor0/zip-release@0.7.6 - with: - type: "zip" - directory: output-osx/ - path: aws-cred-mgr - filename: aws-cred-mgr-${{ github.ref_name }}-osx-x64.zip - - name: Zip Output MacOS ARM - uses: thedoctor0/zip-release@0.7.6 - with: - type: "zip" - directory: output-osxarm/ - path: aws-cred-mgr - filename: aws-cred-mgr-${{ github.ref_name }}-osx-arm64.zip # End - Temporary steps to enable the migration of zip to binary @@ -76,6 +62,7 @@ jobs: generate_release_notes: true prerelease: ${{ contains(github.ref_name, 'beta') }} files: | + output-win/aws-cred-mgr-win-x64.zip output-win/aws-cred-mgr-win-x64.exe output-osx/aws-cred-mgr-osx-x64 output-osxarm/aws-cred-mgr-osx-arm64 From a836a3429830fa426c66f9a109e5fd03fcfe9e7e Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 19 Aug 2024 06:24:58 +0100 Subject: [PATCH 04/13] Fixing release --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4342b82..a3fdb9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: generate_release_notes: true prerelease: ${{ contains(github.ref_name, 'beta') }} files: | - output-win/aws-cred-mgr-win-x64.zip + output-win/aws-cred-mgr-${{ github.ref_name }}-win-x64.zip output-win/aws-cred-mgr-win-x64.exe output-osx/aws-cred-mgr-osx-x64 output-osxarm/aws-cred-mgr-osx-arm64 From 25040b337d149881f64f37359c7a9af819be6997 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:20:10 +0100 Subject: [PATCH 05/13] Adding extra tests Fixing code signing --- .github/workflows/release.yml | 48 +++++++++++++++- .gitignore | 3 + Ellosoft.AwsCredentialsManager.entitlements | 14 +++++ .../Ellosoft.AwsCredentialsManager.csproj | 4 +- src/Ellosoft.AwsCredentialsManager/Program.cs | 4 ++ ...llosoft.AwsCredentialsManager.Tests.csproj | 4 +- .../ServiceRegistrationTests.cs | 56 ++++++++----------- 7 files changed, 93 insertions(+), 40 deletions(-) create mode 100644 Ellosoft.AwsCredentialsManager.entitlements diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a3fdb9d..60b12f9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,22 +17,26 @@ jobs: uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x + - name: Restore dependencies run: dotnet restore + - name: Build & Publish Windows run: | dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ -r win-x64 -o output-win + - name: Build & Publish MacOS x64 run: | dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ -r osx-x64 -o output-osx + - name: Build & Publish MacOS ARM run: | dotnet publish src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj -c Release -p:Version=${{ github.ref_name }} \ -r osx-arm64 -o output-osxarm -# Start - Temporary steps to enable the migration of zip to binary + # Start - Temporary steps to enable the migration of zip to binary - name: Zip Output Windows uses: thedoctor0/zip-release@0.7.6 @@ -42,13 +46,52 @@ jobs: path: aws-cred-mgr.exe filename: aws-cred-mgr-${{ github.ref_name }}-win-x64.zip -# End - Temporary steps to enable the migration of zip to binary + # End - Temporary steps to enable the migration of zip to binary - name: Rename Executables run: | mv output-win/aws-cred-mgr.exe output-win/aws-cred-mgr-win-x64.exe mv output-osx/aws-cred-mgr output-osx/aws-cred-mgr-osx-x64 mv output-osxarm/aws-cred-mgr output-osxarm/aws-cred-mgr-osx-arm64 + + - name: Change MacOs Permissions + run: | + chmod +x output-osx/aws-cred-mgr-osx-x64 + chmod +x output-osxarm/aws-cred-mgr-osx-arm64 + + - name: Import Apple Certificate and Key + env: + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + P12_PASSWORD: ${{ secrets.P12_PASSWORD }} + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # create variables + CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificate and provisioning profile from secrets + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $CERTIFICATE_PATH -P "$P12_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + + - name: Sign MacOS Binaries + run: | + codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr + codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr + + # - name: Notarize MacOS Binaries + # run: | + # xcrun altool --notarize-app --primary-bundle-id "com.ellosoft.aws-cred-mgr" --username "${{ secrets.APPLE_ID_USERNAME }}" --password "${{ secrets.APPLE_ID_PASSWORD }}" --file output-osxarm/aws-cred-mgr + # xcrun altool --notarize-app --primary-bundle-id "com.ellosoft.aws-cred-mgr" --username "${{ secrets.APPLE_ID_USERNAME }}" --password "${{ secrets.APPLE_ID_PASSWORD }}" --file output-osx/aws-cred-mgr + - name: Generate artifact attestation uses: actions/attest-build-provenance@v1 with: @@ -56,6 +99,7 @@ jobs: output-win/aws-cred-mgr-win-x64.exe output-osx/aws-cred-mgr-osx-x64 output-osxarm/aws-cred-mgr-osx-arm64 + - name: Create Release uses: softprops/action-gh-release@v2 with: diff --git a/.gitignore b/.gitignore index 8a30d25..62f0a0f 100644 --- a/.gitignore +++ b/.gitignore @@ -396,3 +396,6 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +#Output folder for release +output-*/* diff --git a/Ellosoft.AwsCredentialsManager.entitlements b/Ellosoft.AwsCredentialsManager.entitlements new file mode 100644 index 0000000..ef1b82e --- /dev/null +++ b/Ellosoft.AwsCredentialsManager.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.cs.allow-dyld-environment-variables + + com.apple.security.cs.disable-library-validation + + + diff --git a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj index ef9119f..83e8bec 100644 --- a/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj +++ b/src/Ellosoft.AwsCredentialsManager/Ellosoft.AwsCredentialsManager.csproj @@ -44,8 +44,8 @@ - - + + diff --git a/src/Ellosoft.AwsCredentialsManager/Program.cs b/src/Ellosoft.AwsCredentialsManager/Program.cs index 43f7a5b..de2217c 100644 --- a/src/Ellosoft.AwsCredentialsManager/Program.cs +++ b/src/Ellosoft.AwsCredentialsManager/Program.cs @@ -1,5 +1,6 @@ // Copyright (c) 2023 Ellosoft Limited. All rights reserved. +using System.Text; using Ellosoft.AwsCredentialsManager; using Ellosoft.AwsCredentialsManager.Commands; using Ellosoft.AwsCredentialsManager.Commands.Config; @@ -14,6 +15,8 @@ using Microsoft.Extensions.DependencyInjection; using Serilog.Events; +Console.OutputEncoding = Encoding.UTF8; + var logger = LogRegistration.CreateNewLogger(); var upgradeService = new UpgradeService(logger); @@ -29,6 +32,7 @@ app.Configure(config => { config.SetApplicationName(AppMetadata.AppName); + config.UseAssemblyInformationalVersion(); config .AddBranch(okta => diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj index 02ac85f..4d3b4d7 100644 --- a/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj +++ b/test/Ellosoft.AwsCredentialsManager.Tests/Ellosoft.AwsCredentialsManager.Tests.csproj @@ -10,7 +10,7 @@ - + all @@ -27,7 +27,7 @@ all - + diff --git a/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs b/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs index 0adce7e..544f1d6 100644 --- a/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs +++ b/test/Ellosoft.AwsCredentialsManager.Tests/ServiceRegistrationTests.cs @@ -2,17 +2,11 @@ #pragma warning disable IDE0005 -using Ellosoft.AwsCredentialsManager.Commands.Config; -using Ellosoft.AwsCredentialsManager.Commands.Credentials; -using Ellosoft.AwsCredentialsManager.Commands.Okta; -using Ellosoft.AwsCredentialsManager.Commands.RDS; -using Ellosoft.AwsCredentialsManager.Infrastructure.Cli; using Microsoft.Extensions.DependencyInjection; using Ellosoft.AwsCredentialsManager.Services.Platforms.MacOS.Security; using Ellosoft.AwsCredentialsManager.Services.Security; using FluentAssertions; using Spectre.Console.Cli; -using Spectre.Console.Testing; namespace Ellosoft.AwsCredentialsManager.Tests; @@ -31,38 +25,20 @@ public ServiceRegistrationTests() _serviceProvider = _services.BuildServiceProvider(); } - [Theory] - // okta - [InlineData(typeof(SetupOkta))] - // config - [InlineData(typeof(OpenAwsConfig))] - [InlineData(typeof(OpenConfig))] - // credentials - [InlineData(typeof(CreateCredentialsProfile))] - [InlineData(typeof(GetCredentials))] - [InlineData(typeof(ListCredentialsProfiles))] - // rds - [InlineData(typeof(GetRdsPassword))] - [InlineData(typeof(ListRdsProfiles))] - public void GetService_UsingCommands_ShouldResolveDependentServices(Type commandType) + [Fact] + public void GetService_UsingCommands_ShouldResolveDependentServices() { - var registrar = new TypeRegistrar(_services); + var commandTypes = typeof(ServiceRegistration).Assembly.GetTypes().Where(type => + type is { IsAbstract: false, IsInterface: false } && + typeof(ICommand).IsAssignableFrom(type)).ToList(); - var app = new CommandAppTester(registrar); - app.Configure(config => config.PropagateExceptions()); + var serviceProvider = _services.BuildServiceProvider(); - var setDefaultCommandMethod = app.GetType().GetMethod(nameof(CommandAppTester.SetDefaultCommand)); - var genericMethod = setDefaultCommandMethod!.MakeGenericMethod(commandType); - genericMethod.Invoke(app, [null, null]); + var commands = commandTypes + .Select(t => ActivatorUtilities.CreateInstance(serviceProvider, t)) + .ToList(); - try - { - _ = app.Run("__default_command"); - } - catch (Exception ex) - { - ex.InnerException?.Message.Should().NotContain("Unable to resolve service for type"); - } + commands.Should().NotBeEmpty(); } #if MACOS @@ -92,4 +68,16 @@ private void TestServiceResolution(Type serviceType, Type implementationType) resolvedServices.Should().Contain(s => s!.GetType().IsAssignableTo(implementationType)); } + + public static class CommandExecutePatch + { + // ReSharper disable once InconsistentNaming + // ReSharper disable once UnusedMember.Local + public static bool Prefix(ref object __result) + { + __result = Task.FromResult(0); + + return false; + } + } } From b1b52ea6efcafb5085831608e7b4eab8e084bdbb Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 26 Aug 2024 18:29:14 +0100 Subject: [PATCH 06/13] Fixing path name --- .github/workflows/release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60b12f9..9ffe224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,8 +84,8 @@ jobs: - name: Sign MacOS Binaries run: | - codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr - codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr + codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 + codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 # - name: Notarize MacOS Binaries # run: | From cabd8446901085f4a6f2eb2ecfc44d8aea2ee462 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Mon, 26 Aug 2024 22:02:46 +0100 Subject: [PATCH 07/13] Adding app notorization --- .github/workflows/release.yml | 51 ++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9ffe224..cf7e770 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,13 +38,10 @@ jobs: # Start - Temporary steps to enable the migration of zip to binary - - name: Zip Output Windows - uses: thedoctor0/zip-release@0.7.6 - with: - type: "zip" - directory: output-win/ - path: aws-cred-mgr.exe - filename: aws-cred-mgr-${{ github.ref_name }}-win-x64.zip + - name: Zip Windows Output + run: | + cd output-win + zip aws-cred-mgr-win-x64.zip aws-cred-mgr.exe # End - Temporary steps to enable the migration of zip to binary @@ -64,6 +61,9 @@ jobs: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} run: | # create variables CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 @@ -84,21 +84,30 @@ jobs: - name: Sign MacOS Binaries run: | - codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 - codesign --deep --force -v --timestamp --sign "Developer ID Application: Ellosoft Limited (W8X5JTYQ6D)" --options=runtime --no-strict --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 + codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 + codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 - # - name: Notarize MacOS Binaries - # run: | - # xcrun altool --notarize-app --primary-bundle-id "com.ellosoft.aws-cred-mgr" --username "${{ secrets.APPLE_ID_USERNAME }}" --password "${{ secrets.APPLE_ID_PASSWORD }}" --file output-osxarm/aws-cred-mgr - # xcrun altool --notarize-app --primary-bundle-id "com.ellosoft.aws-cred-mgr" --username "${{ secrets.APPLE_ID_USERNAME }}" --password "${{ secrets.APPLE_ID_PASSWORD }}" --file output-osx/aws-cred-mgr + - name: Notarize MacOS ARM Binaries + run: | + cd output-osxarm + zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 + xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + xcrun stapler staple aws-cred-mgr-osx-arm64 - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v1 - with: - subject-path: | - output-win/aws-cred-mgr-win-x64.exe - output-osx/aws-cred-mgr-osx-x64 - output-osxarm/aws-cred-mgr-osx-arm64 + - name: Notarize MacOS x64 Binaries + run: | + cd output-osx + zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 + xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + xcrun stapler staple aws-cred-mgr-osx-x64 + + # - name: Generate artifact attestation + # uses: actions/attest-build-provenance@v1 + # with: + # subject-path: | + # output-win/aws-cred-mgr-win-x64.exe + # output-osx/aws-cred-mgr-osx-x64 + # output-osxarm/aws-cred-mgr-osx-arm64 - name: Create Release uses: softprops/action-gh-release@v2 @@ -106,7 +115,7 @@ jobs: generate_release_notes: true prerelease: ${{ contains(github.ref_name, 'beta') }} files: | - output-win/aws-cred-mgr-${{ github.ref_name }}-win-x64.zip + output-win/aws-cred-mgr-win-x64.zip output-win/aws-cred-mgr-win-x64.exe output-osx/aws-cred-mgr-osx-x64 output-osxarm/aws-cred-mgr-osx-arm64 From 6495708b25d3227d2cee9f2405327d25e724cee9 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 12:15:36 +0100 Subject: [PATCH 08/13] Fixing env variables --- .github/workflows/release.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf7e770..eb3c660 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -61,9 +61,6 @@ jobs: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} P12_PASSWORD: ${{ secrets.P12_PASSWORD }} KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} - APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} run: | # create variables CERTIFICATE_PATH=$RUNNER_TEMP/certificate.p12 @@ -83,11 +80,17 @@ jobs: security list-keychain -d user -s $KEYCHAIN_PATH - name: Sign MacOS Binaries + env: + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 - name: Notarize MacOS ARM Binaries + env: + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | cd output-osxarm zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 @@ -95,6 +98,10 @@ jobs: xcrun stapler staple aws-cred-mgr-osx-arm64 - name: Notarize MacOS x64 Binaries + env: + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | cd output-osx zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 From 7e0d2ee319aa8ce148d5d1aa48348c350b51e61a Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:23:52 +0100 Subject: [PATCH 09/13] Fixing file permissions --- .github/workflows/release.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb3c660..6d13d2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,11 +51,6 @@ jobs: mv output-osx/aws-cred-mgr output-osx/aws-cred-mgr-osx-x64 mv output-osxarm/aws-cred-mgr output-osxarm/aws-cred-mgr-osx-arm64 - - name: Change MacOs Permissions - run: | - chmod +x output-osx/aws-cred-mgr-osx-x64 - chmod +x output-osxarm/aws-cred-mgr-osx-arm64 - - name: Import Apple Certificate and Key env: BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} @@ -95,6 +90,7 @@ jobs: cd output-osxarm zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + chmod 755 aws-cred-mgr-osx-arm64 xcrun stapler staple aws-cred-mgr-osx-arm64 - name: Notarize MacOS x64 Binaries @@ -106,6 +102,7 @@ jobs: cd output-osx zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + chmod 755 aws-cred-mgr-osx-x64 xcrun stapler staple aws-cred-mgr-osx-x64 # - name: Generate artifact attestation From 6ef5ac73adcc2406204f9c56a64c77f0e732673f Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:33:03 +0100 Subject: [PATCH 10/13] MacOS x86 --- .github/workflows/release.yml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d13d2c..7246c88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,9 @@ name: .NET Release Workflow on: push: - tags: - - "*" + # tags: + # - "*" + branches: ["*"] permissions: id-token: write contents: write @@ -81,17 +82,17 @@ jobs: codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 - - name: Notarize MacOS ARM Binaries - env: - APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} - APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - cd output-osxarm - zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 - xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait - chmod 755 aws-cred-mgr-osx-arm64 - xcrun stapler staple aws-cred-mgr-osx-arm64 + # - name: Notarize MacOS ARM Binaries + # env: + # APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + # APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} + # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # run: | + # cd output-osxarm + # zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 + # xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + # chmod 755 aws-cred-mgr-osx-arm64 + # xcrun stapler staple aws-cred-mgr-osx-arm64 - name: Notarize MacOS x64 Binaries env: From e4e2b74325557e22a8d14908c951c119359cccc5 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:37:32 +0100 Subject: [PATCH 11/13] Test --- .github/workflows/release.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7246c88..2a8d104 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,8 @@ on: push: # tags: # - "*" - branches: ["*"] + branches: + - "**" permissions: id-token: write contents: write From 902b14a09cc4e716b187338745e2a2dd4f0b134b Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:44:38 +0100 Subject: [PATCH 12/13] Removing stampling --- .github/workflows/release.yml | 44 +++++++++++++++++------------------ 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a8d104..31cce8b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,10 +1,8 @@ name: .NET Release Workflow on: push: - # tags: - # - "*" - branches: - - "**" + tags: + - "*" permissions: id-token: write contents: write @@ -83,29 +81,29 @@ jobs: codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osxarm/aws-cred-mgr-osx-arm64 codesign --force -v --timestamp --options runtime --no-strict --prefix com.ellosoft. --sign "Developer ID Application: Ellosoft Limited ($APPLE_TEAM_ID)" --entitlements ./Ellosoft.AwsCredentialsManager.entitlements ./output-osx/aws-cred-mgr-osx-x64 - # - name: Notarize MacOS ARM Binaries - # env: - # APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} - # APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - # run: | - # cd output-osxarm - # zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 - # xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait - # chmod 755 aws-cred-mgr-osx-arm64 - # xcrun stapler staple aws-cred-mgr-osx-arm64 - - - name: Notarize MacOS x64 Binaries + - name: Notarize MacOS ARM Binaries env: APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} run: | - cd output-osx - zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 - xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait - chmod 755 aws-cred-mgr-osx-x64 - xcrun stapler staple aws-cred-mgr-osx-x64 + cd output-osxarm + zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 + xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + chmod +x aws-cred-mgr-osx-arm64 + # xcrun stapler staple aws-cred-mgr-osx-arm64 + + # - name: Notarize MacOS x64 Binaries + # env: + # APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + # APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} + # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + # run: | + # cd output-osx + # zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 + # xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + # chmod +x aws-cred-mgr-osx-x64 + # # xcrun stapler staple aws-cred-mgr-osx-x64 # - name: Generate artifact attestation # uses: actions/attest-build-provenance@v1 @@ -123,5 +121,5 @@ jobs: files: | output-win/aws-cred-mgr-win-x64.zip output-win/aws-cred-mgr-win-x64.exe - output-osx/aws-cred-mgr-osx-x64 output-osxarm/aws-cred-mgr-osx-arm64 + # output-osx/aws-cred-mgr-osx-x64 From b3a10717412c74d7769de21b312367ac8e5ba518 Mon Sep 17 00:00:00 2001 From: Vitor M <4777793+vgmello@users.noreply.github.com> Date: Tue, 27 Aug 2024 21:51:36 +0100 Subject: [PATCH 13/13] Complete --- .github/workflows/release.yml | 42 ++++++++++++++++------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31cce8b..269f405 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,28 +90,24 @@ jobs: cd output-osxarm zip aws-cred-mgr-osx-arm64.zip aws-cred-mgr-osx-arm64 xcrun notarytool submit aws-cred-mgr-osx-arm64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait - chmod +x aws-cred-mgr-osx-arm64 - # xcrun stapler staple aws-cred-mgr-osx-arm64 - - # - name: Notarize MacOS x64 Binaries - # env: - # APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} - # APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} - # APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - # run: | - # cd output-osx - # zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 - # xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait - # chmod +x aws-cred-mgr-osx-x64 - # # xcrun stapler staple aws-cred-mgr-osx-x64 - - # - name: Generate artifact attestation - # uses: actions/attest-build-provenance@v1 - # with: - # subject-path: | - # output-win/aws-cred-mgr-win-x64.exe - # output-osx/aws-cred-mgr-osx-x64 - # output-osxarm/aws-cred-mgr-osx-arm64 + + - name: Notarize MacOS x64 Binaries + env: + APPLE_DEV_ID: ${{ secrets.APPLE_DEV_ID }} + APPLE_DEV_PASSWORD: ${{ secrets.APPLE_DEV_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + run: | + cd output-osx + zip aws-cred-mgr-osx-x64.zip aws-cred-mgr-osx-x64 + xcrun notarytool submit aws-cred-mgr-osx-x64.zip --apple-id $APPLE_DEV_ID --password $APPLE_DEV_PASSWORD --team-id $APPLE_TEAM_ID --wait + + - name: Generate artifact attestation + uses: actions/attest-build-provenance@v1 + with: + subject-path: | + output-win/aws-cred-mgr-win-x64.exe + output-osx/aws-cred-mgr-osx-x64 + output-osxarm/aws-cred-mgr-osx-arm64 - name: Create Release uses: softprops/action-gh-release@v2 @@ -122,4 +118,4 @@ jobs: output-win/aws-cred-mgr-win-x64.zip output-win/aws-cred-mgr-win-x64.exe output-osxarm/aws-cred-mgr-osx-arm64 - # output-osx/aws-cred-mgr-osx-x64 + output-osx/aws-cred-mgr-osx-x64