├── .github ├── ISSUE_TEMPLATE │ ├── bug-report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── release.yml │ └── validate-pr-title.yml ├── .gitignore ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── assets ├── images │ ├── logo-targe-dark.png │ ├── logo-targe-light.png │ └── targe.gif └── windows │ └── targe.wxs ├── cmd └── targe │ └── targe.go ├── go.mod ├── go.sum ├── internal ├── ai │ ├── policy_generate.go │ └── user_promt.go ├── aws │ ├── find.go │ ├── list.go │ └── operations.go ├── config │ └── config.go └── requirements │ ├── aws │ ├── aws.go │ ├── managed_policies.go │ └── types.go │ └── requirements.go ├── pkg ├── aws │ ├── groups │ │ ├── controller.go │ │ ├── create_policy.go │ │ ├── group_list.go │ │ ├── operation_list.go │ │ ├── policy_list.go │ │ ├── policy_option_list.go │ │ ├── resource_list.go │ │ ├── result.go │ │ ├── service_list.go │ │ ├── state.go │ │ └── styles.go │ ├── models │ │ ├── group.go │ │ ├── operation.go │ │ ├── policy.go │ │ ├── policy_option.go │ │ ├── resource.go │ │ ├── role.go │ │ ├── service.go │ │ └── user.go │ ├── roles │ │ ├── controller.go │ │ ├── create_policy.go │ │ ├── operation_list.go │ │ ├── policy_list.go │ │ ├── policy_option_list.go │ │ ├── resource_list.go │ │ ├── result.go │ │ ├── role_list.go │ │ ├── service_list.go │ │ ├── state.go │ │ └── styles.go │ └── users │ │ ├── controller.go │ │ ├── create_policy.go │ │ ├── group_list.go │ │ ├── operation_list.go │ │ ├── policy_list.go │ │ ├── policy_option_list.go │ │ ├── resource_list.go │ │ ├── result.go │ │ ├── service_list.go │ │ ├── state.go │ │ ├── styles.go │ │ └── user_list.go └── cmd │ ├── aws │ ├── aws.go │ ├── flags.go │ ├── groups.go │ ├── roles.go │ ├── users.go │ └── utils.go │ ├── common │ └── requirements.go │ ├── config │ └── config.go │ ├── flags.go │ └── root.go └── requirements ├── managed_policies.json ├── policies.json └── types.json /.github/ISSUE_TEMPLATE/bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" 5 | directory: "/" 6 | schedule: 7 | interval: "monthly" 8 | 9 | - package-ecosystem: github-actions 10 | directory: / 11 | schedule: 12 | interval: daily 13 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Release 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | permissions: 8 | contents: write 9 | packages: write 10 | jobs: 11 | goreleaser: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4.2.2 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v5.4.0 21 | with: 22 | go-version: ~1.23 23 | 24 | - name: Install msitools 25 | run: sudo apt-get install -y wixl 26 | 27 | - name: Run GoReleaser 28 | uses: goreleaser/goreleaser-action@v6.2.1 29 | with: 30 | distribution: goreleaser-pro 31 | version: ~> v2 32 | args: release --clean 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 35 | HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_GITHUB_TOKEN }} 36 | GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }} 37 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Conventional Commits" 3 | on: 4 | pull_request_target: 5 | types: 6 | - opened 7 | - edited 8 | - synchronize 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | validate: 14 | name: Validate PR title 15 | runs-on: ubuntu-latest 16 | timeout-minutes: 5 17 | steps: 18 | - uses: amannn/action-semantic-pull-request@v5.5.3 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 21 | with: 22 | types: | 23 | feat 24 | fix 25 | chore 26 | docs 27 | enhancement 28 | revert 29 | build 30 | ci 31 | style 32 | refactor 33 | perf 34 | test 35 | requireScope: false 36 | subjectPattern: ^(?![A-Z]).+$ 37 | subjectPatternError: | 38 | The subject "{subject}" found in the pull request title "{title}" 39 | didn't match the configured pattern. Please ensure that the subject 40 | doesn't start with an uppercase character. 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea 3 | 4 | # Config 5 | .env 6 | 7 | # Binaries for programs and plugins 8 | *.exe 9 | *.exe~ 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | # Test binary, built with `go test -c` 15 | *.test 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | # Dependency directories (remove the comment below to include it) 21 | vendor/dist/ 22 | /dist 23 | /config 24 | /tmp 25 | node_modules 26 | 27 | # macOS 28 | *.DS_Store 29 | targe 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | project_name: targe 4 | universal_binaries: 5 | - replace: true 6 | builds: 7 | - 8 | env: 9 | - CGO_ENABLED=0 10 | goarch: 11 | - amd64 12 | - arm64 13 | goos: 14 | - linux 15 | - windows 16 | - darwin 17 | ldflags: 18 | - "-s -w" 19 | main: ./cmd/targe 20 | mod_timestamp: "{{ .CommitTimestamp }}" 21 | brews: 22 | - 23 | dependencies: 24 | - 25 | name: go 26 | type: build 27 | description: "" 28 | download_strategy: CurlDownloadStrategy 29 | directory: Formula 30 | homepage: "https://github.com/Permify/targe" 31 | license: "Apache-2.0" 32 | custom_block: | 33 | head "https://github.com/Permify/targe.git", :branch => "master" 34 | install: |- 35 | bin.install "targe" 36 | repository: 37 | name: homebrew-tap-targe 38 | owner: Permify 39 | token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}" 40 | commit_author: 41 | name: permify-bot 42 | email: hello@permify.co 43 | url_template: "https://github.com/Permify/targe/releases/download/{{ .Tag }}/{{ .ArtifactName }}" 44 | nfpms: 45 | - 46 | description: "" 47 | formats: 48 | - deb 49 | - rpm 50 | - apk 51 | homepage: "https://github.com/Permify" 52 | license: "MIT" 53 | maintainer: "permify " 54 | vendor: "permify inc." 55 | msi: 56 | - id: targe 57 | name: "targe-{{.MsiArch}}" 58 | wxs: ./assets/windows/targe.wxs 59 | ids: 60 | - targe 61 | goamd64: v1 62 | extra_files: 63 | - ./assets/images/logo-iam-copilot-light.png 64 | replace: true 65 | mod_timestamp: "{{ .CommitTimestamp }}" 66 | snapshot: 67 | version_template: "{{ incpatch .Version }}-next" 68 | changelog: 69 | sort: asc 70 | filters: 71 | exclude: 72 | - "^docs:" 73 | - "^test:" 74 | checksum: 75 | name_template: checksums.txt 76 | release: 77 | draft: true 78 | mode: replace 79 | prerelease: auto 80 | footer: | 81 | This is an automated release. 82 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | ege@permify.co. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Welcome to Targe contribution guidelines, happy to see you here :blush: 4 | 5 | Before participating in the community, we must specify that all of the contributions must follow our [Code of Conduct](https://github.com/Permify/targe/blob/master/CODE_OF_CONDUCT.md). Please read it before you make any contributions. 6 | 7 | If you need any help or want to talk about about a specific issue, you can always reach out to me from my mail:ege@permify.co. 8 | 9 | You're always more than welcome to our other communication channels. 10 | 11 | ## Communication Channels 12 | 13 |

14 | 15 | permify | Discord 16 | 17 | 18 | permify | Twitter 19 | 20 | 21 | permify | Linkedin 22 | 23 |

24 | 25 | ## Ways to contribute 26 | 27 | * **Contribute to codebase:** We're collaboratively working with our community to make Targe the best it can be! You can develop new features, fix existing issues or make third-party integrations/packages. 28 | 29 | ### Contribution Steps 30 | 31 | - Fork this repository. 32 | - Clone the repository you forked. 33 | - Create a branch with specified name. It's better to relate it with your issue title. 34 | - Make necessary changes and commit those changes. Make sure to test your changes. 35 | - Push changes to your branch. 36 | - Submit your changes for review. 37 | 38 | ## Commit convention 39 | 40 | We use [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to keep our commit messages consistent and easy to understand. Here is the applied form of a commit message. 41 | 42 | ``` 43 | (optional scope): 44 | ``` 45 | 46 | **Examples:** 47 | 48 | - `feat: added multi tenant authentication support` 49 | - `fix: fixed welcomeServer duplicated syntax` 50 | - `docs: update the deployment options on set up section` 51 | 52 | ### Types 53 | 54 | `fix:`,  `feat:`, `build:`, `chore:`, `ci:`, `docs:`, `style:`, `refactor:`, `perf:`, `test:` 55 | 56 | ## Running Tests 57 | 58 | In order to contribute and test in our codebase you need to have Go version 1.19 or higher. 59 | 60 | ```go test -v ./...``` 61 | 62 | ### Adding dependencies 63 | Targe is not using anything other than the standard Go modules toolchain to manage dependencies. 64 | 65 | ```go get github.com/org/newdependency@version``` 66 | 67 | ## Issues 68 | 69 | If you found any bug, have feature request or just want to improve our code base, docs or other resources; you can open an issue about it to let us know. If you plan to work on an existing issue, mention us on the issue page before you start working on it so we can assign you to it. 70 | 71 | ### When opening a issue 72 | 73 | - If you plan to work on a problem, please check that same problem or topic does not already exist. 74 | - If you plan to work on a new feature, our advise is to discuss it with other community members/maintainers who might give you an idea or support. 75 | - If you're stuck anywhere, ask for help in our discord community. 76 | - Please relate one bug with one issue, do not use issues as bug lists. 77 | 78 | You can create an issue and contribute to anything you want, but please ensure to follow the steps above. We will definitely ease your work and help on anything when needed. 79 | 80 | 81 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export 2 | 3 | # HELP ================================================================================================================= 4 | # This will output the help for each task 5 | # thanks to https://marmelab.com/blog/2016/02/29/auto-documented-makefile.html 6 | .PHONY: help 7 | help: ## Display this help screen 8 | @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) 9 | 10 | .PHONY: download 11 | download: 12 | @cd tools/ && go mod download 13 | 14 | .PHONY: linter-golangci 15 | linter-golangci: ### check by golangci linter 16 | golangci-lint run 17 | 18 | .PHONY: linter-hadolint 19 | linter-hadolint: ### check by hadolint linter 20 | git ls-files --exclude='Dockerfile*' --ignored | xargs hadolint 21 | 22 | .PHONY: linter-dotenv 23 | linter-dotenv: ### check by dotenv linter 24 | dotenv-linter 25 | 26 | .PHONY: integration-test 27 | integration-test: ### run integration-test 28 | go clean -testcache && go test -v ./integration-test/... 29 | 30 | .PHONY: build 31 | build: ## Build/compile the Combo service 32 | go build -o ./targe ./cmd/targe 33 | 34 | .PHONY: format 35 | format: ## Auto-format the code 36 | gofumpt -l -w -extra . 37 | 38 | .PHONY: lint-all 39 | lint-all: linter-golangci linter-hadolint linter-dotenv ## Run all linters 40 | 41 | .PHONY: security-scan 42 | security-scan: ## Scan code for security vulnerabilities using Gosec 43 | gosec -exclude-dir=sdk -exclude-dir=playground -exclude-dir=docs -exclude-dir=assets ./... 44 | 45 | .PHONY: coverage 46 | coverage: ## Generate global code coverage report 47 | go test -coverprofile=covprofile ./cmd/... ./internal/... ./pkg/... 48 | go tool cover -html=covprofile -o coverage.html 49 | 50 | .PHONY: clean 51 | clean: ## Remove temporary and generated files 52 | rm -f ./targe 53 | rm -f covprofile coverage.html 54 | 55 | .PHONY: release 56 | release: format security-scan clean ## Prepare for release 57 | 58 | # Serve 59 | 60 | .PHONY: users 61 | users: build 62 | ./targe users 63 | 64 | .PHONY: groups 65 | groups: build 66 | ./targe groups 67 | 68 | .PHONY: roles 69 | roles: build 70 | ./targe roles -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | Targe logo 5 | 6 |

7 | Targe - Open Source IAM Copilot 8 |

9 |
10 |

11 | Targe is an open-source CLI for managing IAM (Identity and Access Management) operations with AI assistance. 12 | 13 | DevOps engineers use Targe to configure how employees in their organization access infrastructure resources. Targe simplifies and accelerates granting and revoking access, while supporting custom policy creation — eliminating the need for tedious back-and-forth UI work. 14 |

15 | 16 |

17 | Permify Go Version  18 | Targe Go Report Card  19 | Targe Licence  20 | Permify Discord Channel  21 | Targe Release  22 | Targe Commit Activity  23 | GitHub Workflow Status  24 |

25 | 26 | ![Targe Demo](assets/images/targe.gif) 27 | 28 | ## How it Works? 29 | 30 | 1. Configure your cloud credentials to enable Targe to access resources in your infrastructure. Currently, Targe supports only AWS. 31 | 2. Start an access flow or use AI to create an access command to fulfill an access request. 32 | 3. Preview the access action and complete the access request. 33 | 34 | ### Create an Access Command with AI 35 | 36 | Describe the access action you want to perform. For example, "give S3 read-only access to user Omer." 37 | 38 | Targe analyzes the request and generates the necessary access command using AI. 39 | 40 | ![targe-ai-flow](https://github.com/user-attachments/assets/0a2ea874-b6b6-47ec-b792-1602137f23e7) 41 | 42 | ### Start an Access Flow Manually 43 | 44 | You can also manually start any flow to complete an access action. 45 | 46 | There are three main flows: 47 | - `~ % targe aws users` | Grant or revoke access to/from a user. 48 | - `~ % targe aws groups` | Attach or detach a policy to/from a group. 49 | - `~ % targe aws roles` | Attach or detach a policy to/from a role. 50 | 51 | Let's repeat the example above of granting s3 read-only access to user Omer. 52 | 53 | We will use following command to start **user** flow: `~ % targe aws users`. 54 | 55 | The user access flow begins by listing the users in the system. Select the user to take action on. 56 | 57 | ![select-user](https://github.com/user-attachments/assets/d99327e8-3c74-42b4-9615-2afe6f0bde0b) 58 | 59 | After selecting the user, choose the operation to perform. Let’s attach a policy to user Omer. 60 | 61 | ![select-operation](https://github.com/user-attachments/assets/bfa67375-0cd1-4bcf-9d73-dcb1a7a88dc4) 62 | 63 | In the next step, select the policy you want to attach. You can use "filters" in each section to search what you need. 64 | 65 | ![select-policy](https://github.com/user-attachments/assets/af918b77-7e45-4c43-9d4b-f8971b1ece47) 66 | 67 | Finally, preview the access action. 68 | 69 | ![preview-access-action](https://github.com/user-attachments/assets/d843bd92-db6d-4907-ab39-0344e4986da8) 70 | 71 | ## Installation Steps 72 | 73 | 1. **Install Targe CLI:** 74 | ```shell 75 | brew tap permify/tap-targe 76 | brew install targe 77 | ``` 78 | 79 | 2. **Set Up AWS Credentials:** 80 | 81 | Targe requires AWS credentials to be configured in the file `~/.aws/credentials`. Follow these steps: 82 | 83 | - Create or open the `~/.aws/credentials` file using a text editor: 84 | ```shell 85 | nano ~/.aws/credentials 86 | ``` 87 | 88 | - Add your AWS credentials in the following format: 89 | ```plaintext 90 | [default] 91 | aws_access_key_id = your_access_key 92 | aws_secret_access_key = your_secret_key 93 | ``` 94 | 95 | - Save the file and exit (in nano, press `CTRL + O` to save, then `CTRL + X` to exit). 96 | 97 | 3. **Verify the Configuration:** 98 | 99 | Run the following command to confirm the credentials are set correctly: 100 | ```shell 101 | aws sts get-caller-identity 102 | ``` 103 | This should return information about your AWS account. If it fails, double-check the credentials file for accuracy. 104 | 105 | 4. **Configure OpenAI API Key** 106 | 107 | Run the following command to configure your OpenAI API Key: 108 | ```shell 109 | targe config set openai_api_key [your_api_key] 110 | ``` 111 | 112 | 5. **Set the Default Region (Optional):** 113 | 114 | If your tool requires a specific AWS region, you can set it in the `~/.aws/config` file: 115 | ```shell 116 | nano ~/.aws/config 117 | ``` 118 | Add: 119 | ```plaintext 120 | [default] 121 | region = us-east-1 122 | ``` 123 | Replace `us-east-1` with your desired region. 124 | 125 | ## Communication Channels 126 | 127 | If you like Targe, please consider giving us a :star: 128 | 129 |

130 | 131 | permify | Discord 132 | 133 | 134 | permify | Twitter 135 | 136 | 137 | permify | Linkedin 138 | 139 |

140 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | If you discover any security related issues, please email tolga@permify.co or ege@permify.co instead of using the issue tracker. 4 | -------------------------------------------------------------------------------- /assets/images/logo-targe-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Permify/targe/3b51e92ad886def0b27f8224c1f4f1eef0c3ad15/assets/images/logo-targe-dark.png -------------------------------------------------------------------------------- /assets/images/logo-targe-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Permify/targe/3b51e92ad886def0b27f8224c1f4f1eef0c3ad15/assets/images/logo-targe-light.png -------------------------------------------------------------------------------- /assets/images/targe.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Permify/targe/3b51e92ad886def0b27f8224c1f4f1eef0c3ad15/assets/images/targe.gif -------------------------------------------------------------------------------- /assets/windows/targe.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | 12 | 23 | 24 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 43 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /cmd/targe/targe.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/Permify/targe/pkg/cmd" 7 | ) 8 | 9 | func main() { 10 | root := cmd.NewRootCommand() 11 | 12 | if err := root.Execute(); err != nil { 13 | os.Exit(1) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Permify/targe 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.36.3 7 | github.com/aws/aws-sdk-go-v2/config v1.29.9 8 | github.com/aws/aws-sdk-go-v2/service/cloudformation v1.56.2 9 | github.com/aws/aws-sdk-go-v2/service/iam v1.38.3 10 | github.com/charmbracelet/bubbles v0.20.0 11 | github.com/charmbracelet/bubbletea v1.2.4 12 | github.com/charmbracelet/huh v0.6.0 13 | github.com/charmbracelet/lipgloss v1.0.0 14 | github.com/spf13/cobra v1.8.1 15 | github.com/spf13/pflag v1.0.5 16 | github.com/spf13/viper v1.19.0 17 | ) 18 | 19 | require ( 20 | github.com/atotto/clipboard v0.1.4 // indirect 21 | github.com/aws/aws-sdk-go-v2/credentials v1.17.62 // indirect 22 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.30 // indirect 23 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect 24 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect 25 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect 26 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect 27 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.25.1 // indirect 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.29.1 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/sts v1.33.17 // indirect 31 | github.com/aws/smithy-go v1.22.2 // indirect 32 | github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect 33 | github.com/catppuccin/go v0.2.0 // indirect 34 | github.com/charmbracelet/harmonica v0.2.0 // indirect 35 | github.com/charmbracelet/x/ansi v0.4.5 // indirect 36 | github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect 37 | github.com/charmbracelet/x/term v0.2.1 // indirect 38 | github.com/dustin/go-humanize v1.0.1 // indirect 39 | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect 40 | github.com/fsnotify/fsnotify v1.7.0 // indirect 41 | github.com/google/go-cmp v0.6.0 // indirect 42 | github.com/hashicorp/hcl v1.0.0 // indirect 43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 44 | github.com/jmespath/go-jmespath v0.4.0 // indirect 45 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 46 | github.com/magiconair/properties v1.8.7 // indirect 47 | github.com/mattn/go-isatty v0.0.20 // indirect 48 | github.com/mattn/go-localereader v0.0.1 // indirect 49 | github.com/mattn/go-runewidth v0.0.16 // indirect 50 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 51 | github.com/mitchellh/mapstructure v1.5.0 // indirect 52 | github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect 53 | github.com/muesli/cancelreader v0.2.2 // indirect 54 | github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect 55 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 56 | github.com/rivo/uniseg v0.4.7 // indirect 57 | github.com/sagikazarmark/locafero v0.4.0 // indirect 58 | github.com/sagikazarmark/slog-shim v0.1.0 // indirect 59 | github.com/sahilm/fuzzy v0.1.1 // indirect 60 | github.com/sourcegraph/conc v0.3.0 // indirect 61 | github.com/spf13/afero v1.11.0 // indirect 62 | github.com/spf13/cast v1.6.0 // indirect 63 | github.com/subosito/gotenv v1.6.0 // indirect 64 | go.uber.org/atomic v1.9.0 // indirect 65 | go.uber.org/multierr v1.9.0 // indirect 66 | golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect 67 | golang.org/x/sync v0.10.0 // indirect 68 | golang.org/x/sys v0.28.0 // indirect 69 | golang.org/x/text v0.21.0 // indirect 70 | gopkg.in/ini.v1 v1.67.0 // indirect 71 | gopkg.in/yaml.v3 v3.0.1 // indirect 72 | ) 73 | -------------------------------------------------------------------------------- /internal/ai/policy_generate.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | type IAMPolicy struct { 13 | Version string `json:"Version"` 14 | Id string `json:"Id,omitempty"` 15 | Statement []IAMStatement `json:"Statement"` 16 | } 17 | 18 | type IAMStatement struct { 19 | Sid string `json:"Sid,omitempty"` 20 | Effect string `json:"Effect"` 21 | Principal *IAMPrincipal `json:"Principal,omitempty"` 22 | NotPrincipal *IAMPrincipal `json:"NotPrincipal,omitempty"` 23 | Action *IAMActionResource `json:"Action,omitempty"` 24 | NotAction *IAMActionResource `json:"NotAction,omitempty"` 25 | Resource *IAMActionResource `json:"Resource,omitempty"` 26 | NotResource *IAMActionResource `json:"NotResource,omitempty"` 27 | Condition map[string]interface{} `json:"Condition,omitempty"` 28 | } 29 | 30 | type IAMPrincipal struct { 31 | Star bool 32 | AWS []string 33 | Federated []string 34 | Service []string 35 | CanonicalUser []string 36 | } 37 | 38 | type IAMActionResource struct { 39 | IsWildcard bool 40 | Resources []string 41 | } 42 | 43 | var IAMPolicySchema = map[string]interface{}{ 44 | "name": "IAMPolicy", 45 | "schema": map[string]interface{}{ 46 | "type": "object", 47 | "properties": map[string]interface{}{ 48 | "Version": map[string]interface{}{ 49 | "type": "string", 50 | "enum": []string{"2012-10-17"}, 51 | }, 52 | "Id": map[string]interface{}{ 53 | "type": "string", 54 | }, 55 | "Statement": map[string]interface{}{ 56 | "type": "array", 57 | "items": map[string]interface{}{ 58 | "type": "object", 59 | "properties": map[string]interface{}{ 60 | "Sid": map[string]interface{}{ 61 | "type": "string", 62 | }, 63 | "Effect": map[string]interface{}{ 64 | "type": "string", 65 | "enum": []string{"Allow", "Deny"}, 66 | }, 67 | "Action": map[string]interface{}{ 68 | "oneOf": []interface{}{ 69 | map[string]interface{}{ 70 | "type": "string", 71 | "enum": []string{"*"}, 72 | }, 73 | map[string]interface{}{ 74 | "type": "array", 75 | "items": map[string]interface{}{"type": "string"}, 76 | }, 77 | }, 78 | }, 79 | "Resource": map[string]interface{}{ 80 | "oneOf": []interface{}{ 81 | map[string]interface{}{ 82 | "type": "string", 83 | "enum": []string{"*"}, 84 | }, 85 | map[string]interface{}{ 86 | "type": "array", 87 | "items": map[string]interface{}{"type": "string"}, 88 | }, 89 | }, 90 | }, 91 | }, 92 | "required": []string{"Effect"}, 93 | }, 94 | }, 95 | }, 96 | "required": []string{"Version", "Statement"}, 97 | }, 98 | } 99 | 100 | func (p *IAMPrincipal) UnmarshalJSON(data []byte) error { 101 | var str string 102 | if err := json.Unmarshal(data, &str); err == nil { 103 | if str == "*" { 104 | p.Star = true 105 | return nil 106 | } 107 | p.AWS = []string{str} 108 | return nil 109 | } 110 | var m map[string]interface{} 111 | if err := json.Unmarshal(data, &m); err != nil { 112 | return err 113 | } 114 | for k, v := range m { 115 | switch k { 116 | case "AWS": 117 | p.AWS = toStringSlice(v) 118 | case "Federated": 119 | p.Federated = toStringSlice(v) 120 | case "Service": 121 | p.Service = toStringSlice(v) 122 | case "CanonicalUser": 123 | p.CanonicalUser = toStringSlice(v) 124 | } 125 | } 126 | return nil 127 | } 128 | 129 | func (ar *IAMActionResource) UnmarshalJSON(data []byte) error { 130 | var str string 131 | if err := json.Unmarshal(data, &str); err == nil { 132 | if str == "*" { 133 | ar.IsWildcard = true 134 | return nil 135 | } 136 | ar.Resources = []string{str} 137 | return nil 138 | } 139 | 140 | var arr []string 141 | if err := json.Unmarshal(data, &arr); err == nil { 142 | ar.Resources = arr 143 | return nil 144 | } 145 | 146 | return fmt.Errorf("invalid action/resource format") 147 | } 148 | 149 | func (p IAMPrincipal) MarshalJSON() ([]byte, error) { 150 | if p.Star { 151 | return json.Marshal("*") 152 | } 153 | 154 | obj := map[string]interface{}{} 155 | 156 | if len(p.AWS) == 1 { 157 | obj["AWS"] = p.AWS[0] 158 | } else if len(p.AWS) > 1 { 159 | obj["AWS"] = p.AWS 160 | } 161 | 162 | if len(p.Federated) == 1 { 163 | obj["Federated"] = p.Federated[0] 164 | } else if len(p.Federated) > 1 { 165 | obj["Federated"] = p.Federated 166 | } 167 | 168 | if len(p.Service) == 1 { 169 | obj["Service"] = p.Service[0] 170 | } else if len(p.Service) > 1 { 171 | obj["Service"] = p.Service 172 | } 173 | 174 | if len(p.CanonicalUser) == 1 { 175 | obj["CanonicalUser"] = p.CanonicalUser[0] 176 | } else if len(p.CanonicalUser) > 1 { 177 | obj["CanonicalUser"] = p.CanonicalUser 178 | } 179 | 180 | if len(obj) == 0 { 181 | return json.Marshal("*") 182 | } 183 | 184 | return json.Marshal(obj) 185 | } 186 | 187 | func (ar IAMActionResource) MarshalJSON() ([]byte, error) { 188 | if ar.IsWildcard { 189 | return json.Marshal("*") 190 | } 191 | if len(ar.Resources) == 1 { 192 | return json.Marshal(ar.Resources[0]) 193 | } 194 | return json.Marshal(ar.Resources) 195 | } 196 | 197 | func toStringSlice(v interface{}) []string { 198 | switch val := v.(type) { 199 | case string: 200 | return []string{val} 201 | case []interface{}: 202 | var out []string 203 | for _, item := range val { 204 | if s, ok := item.(string); ok { 205 | out = append(out, s) 206 | } 207 | } 208 | return out 209 | default: 210 | return nil 211 | } 212 | } 213 | 214 | func GeneratePolicy(apiKey, prompt string, serviceName, resourceArn *string) (IAMPolicy, error) { 215 | url := "https://api.openai.com/v1/chat/completions" 216 | 217 | // Build detailed information for the service and resource. 218 | serviceAndResourceDetails := "" 219 | if resourceArn != nil { 220 | if serviceName != nil { 221 | serviceAndResourceDetails = fmt.Sprintf("The service name is: %s\nThe resource ARN is: %s", *serviceName, *resourceArn) 222 | } else { 223 | serviceAndResourceDetails = fmt.Sprintf("The service name is: all services\nThe resource ARN is: %s", *resourceArn) 224 | } 225 | } else { 226 | if serviceName != nil { 227 | serviceAndResourceDetails = fmt.Sprintf("The service name is: %s\nNo specific resource ARN provided.", *serviceName) 228 | } else { 229 | serviceAndResourceDetails = "The service name is: all services\nNo specific resource ARN provided." 230 | } 231 | } 232 | 233 | payload := map[string]interface{}{ 234 | "model": "gpt-4o", 235 | "temperature": 0.1, 236 | "messages": []map[string]string{ 237 | {"role": "system", "content": "You are an assistant that produces IAM policies as JSON."}, 238 | {"role": "user", "content": fmt.Sprintf("%s%s", prompt, serviceAndResourceDetails)}, 239 | }, 240 | "response_format": map[string]interface{}{ 241 | "type": "json_schema", 242 | "json_schema": IAMPolicySchema, 243 | }, 244 | } 245 | 246 | payloadBytes, err := json.Marshal(payload) 247 | if err != nil { 248 | return IAMPolicy{}, fmt.Errorf("failed to marshal payload: %w", err) 249 | } 250 | 251 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) 252 | if err != nil { 253 | return IAMPolicy{}, fmt.Errorf("failed to create request: %w", err) 254 | } 255 | 256 | req.Header.Set("Content-Type", "application/json") 257 | req.Header.Set("Authorization", "Bearer "+apiKey) 258 | 259 | client := &http.Client{} 260 | resp, err := client.Do(req) 261 | if err != nil { 262 | return IAMPolicy{}, fmt.Errorf("request error: %w", err) 263 | } 264 | defer resp.Body.Close() 265 | 266 | if resp.StatusCode != http.StatusOK { 267 | body, _ := io.ReadAll(resp.Body) 268 | return IAMPolicy{}, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 269 | } 270 | 271 | var intermediateResponse struct { 272 | Choices []struct { 273 | Message struct { 274 | Content string `json:"content"` 275 | } `json:"message"` 276 | } `json:"choices"` 277 | } 278 | bodyBytes, _ := io.ReadAll(resp.Body) 279 | err = json.Unmarshal(bodyBytes, &intermediateResponse) 280 | if err != nil { 281 | return IAMPolicy{}, fmt.Errorf("failed to parse intermediate response: %w", err) 282 | } 283 | 284 | if len(intermediateResponse.Choices) == 0 || intermediateResponse.Choices[0].Message.Content == "" { 285 | return IAMPolicy{}, fmt.Errorf("no content found in response") 286 | } 287 | 288 | content := strings.TrimSpace(intermediateResponse.Choices[0].Message.Content) 289 | 290 | var policy IAMPolicy 291 | err = json.Unmarshal([]byte(content), &policy) 292 | if err != nil { 293 | return IAMPolicy{}, fmt.Errorf("failed to parse structured content: %w", err) 294 | } 295 | 296 | return policy, nil 297 | } 298 | -------------------------------------------------------------------------------- /internal/ai/user_promt.go: -------------------------------------------------------------------------------- 1 | package ai 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | type GPTResponse struct { 13 | Action string `json:"action"` 14 | Principal map[string]string `json:"principal"` 15 | RequestedResourceType string `json:"requested_resource_type"` 16 | RequestedResource string `json:"requested_resource"` 17 | IsManagedPolicy bool `json:"is_managed_policy"` 18 | Policy string `json:"policy"` 19 | Error bool `json:"error"` 20 | Confidence int `json:"confidence"` 21 | } 22 | 23 | var UserPromptSchema = map[string]interface{}{ 24 | "name": "iam_request", 25 | "schema": map[string]interface{}{ 26 | "type": "object", 27 | "properties": map[string]interface{}{ 28 | "action": map[string]interface{}{ 29 | "type": []string{"string", "null"}, 30 | "description": "The action type. If undetermined, return null.", 31 | "enum": []string{ 32 | "attach_policy", 33 | "detach_policy", 34 | "add_to_group", 35 | "remove_from_group", 36 | "attach_custom_policy", 37 | }, 38 | }, 39 | "error": map[string]interface{}{ 40 | "type": "boolean", 41 | "description": "Indicates if the command cannot be managed by the specified actions.", 42 | }, 43 | "principal": map[string]interface{}{ 44 | "type": "object", 45 | "description": "The target entity to which the policy will be attached.", 46 | "properties": map[string]interface{}{ 47 | "type": map[string]interface{}{ 48 | "type": []string{"string", "null"}, 49 | "enum": []string{"users", "groups", "roles"}, 50 | }, 51 | "name": map[string]interface{}{ 52 | "type": []string{"string", "null"}, 53 | "description": "The name of the target entity.", 54 | }, 55 | }, 56 | "required": []string{"type", "name"}, 57 | "additionalProperties": false, 58 | }, 59 | "requested_resource_type": map[string]interface{}{ 60 | "type": []string{"string", "null"}, 61 | "description": "The type of aws resource user wants access for. AWS::S3::Bucket, AWS::RDS::DBCluster, AWS::RDS::DBInstance, AWS::EC2::Instance etc", 62 | }, 63 | "requested_resource": map[string]interface{}{ 64 | "type": []string{"string", "null"}, 65 | "description": "The name of the resource user wants access for.", 66 | }, 67 | "is_managed_policy": map[string]interface{}{ 68 | "type": "boolean", 69 | "description": "Indicates if the provided policy is an exact AWS managed policy.", 70 | }, 71 | "policy": map[string]interface{}{ 72 | "type": []string{"string", "null"}, 73 | "description": "The name of the policy. If it's too vague, return null. If the user input does not provide any meaningful context, the model must not guess a policy. For Managed policies, use the arn foe example: arn:aws:iam::aws:policy/AdministratorAccess.", 74 | }, 75 | "confidence": map[string]interface{}{ 76 | "type": "integer", 77 | "description": "Confidence level from 1 to 10 about the policy name.", 78 | }, 79 | }, 80 | "required": []string{ 81 | "error", 82 | }, 83 | "additionalProperties": false, 84 | }, 85 | } 86 | 87 | func UserPrompt(apiKey, prompt string) (GPTResponse, error) { 88 | url := "https://api.openai.com/v1/chat/completions" 89 | schema := UserPromptSchema 90 | payload := map[string]interface{}{ 91 | "model": "gpt-4o", 92 | "temperature": 0.1, 93 | "messages": []map[string]string{ 94 | {"role": "system", "content": ` 95 | You are an assistant designed to interpret IAM-related requests and convert them into structured JSON objects. 96 | 97 | Your task is to: 98 | 1. Analyze the user's input. 99 | 2. Only provide a field if you are very certain (confidence >= 8). If you are not sure, return null for that field. 100 | 3. Identify the requested action, target entity, and resource details. 101 | 4. If input is vauge return null for specified section. 102 | 5. If target is a specific resource use custom policies. "Example: For 'Allow access to bucket production-data', identify the required resource-specific policy and set isManagedPolicy = false. 103 | 6. Identify policy. If target is a service try getting aws managed policies first. Be certain about aws managed policy names if its wrong correct it. 104 | 7. If the identified policy name has low confidence, set confidence < 5." 105 | 8. If the user input does not provide any meaningful context, the model must not guess a policy. 106 | `}, 107 | {"role": "user", "content": prompt}, 108 | }, 109 | "response_format": map[string]interface{}{ 110 | "type": "json_schema", 111 | "json_schema": schema, 112 | }, 113 | } 114 | 115 | payloadBytes, err := json.Marshal(payload) 116 | if err != nil { 117 | return GPTResponse{}, fmt.Errorf("failed to marshal payload: %w", err) 118 | } 119 | 120 | req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes)) 121 | if err != nil { 122 | return GPTResponse{}, fmt.Errorf("failed to create request: %w", err) 123 | } 124 | 125 | req.Header.Set("Content-Type", "application/json") 126 | req.Header.Set("Authorization", "Bearer "+apiKey) 127 | 128 | client := &http.Client{} 129 | resp, err := client.Do(req) 130 | if err != nil { 131 | return GPTResponse{}, fmt.Errorf("request error: %w", err) 132 | } 133 | defer resp.Body.Close() 134 | 135 | if resp.StatusCode != http.StatusOK { 136 | body, _ := io.ReadAll(resp.Body) 137 | return GPTResponse{}, fmt.Errorf("unexpected status code: %d, body: %s", resp.StatusCode, string(body)) 138 | } 139 | 140 | var intermediateResponse struct { 141 | Choices []struct { 142 | Message struct { 143 | Content string `json:"content"` 144 | } `json:"message"` 145 | } `json:"choices"` 146 | } 147 | bodyBytes, _ := io.ReadAll(resp.Body) 148 | err = json.Unmarshal(bodyBytes, &intermediateResponse) 149 | if err != nil { 150 | return GPTResponse{}, fmt.Errorf("failed to parse intermediate response: %w", err) 151 | } 152 | 153 | if len(intermediateResponse.Choices) == 0 || intermediateResponse.Choices[0].Message.Content == "" { 154 | return GPTResponse{}, fmt.Errorf("no content found in response") 155 | } 156 | 157 | content := intermediateResponse.Choices[0].Message.Content 158 | content = strings.TrimSpace(content) 159 | 160 | var gptResponse GPTResponse 161 | err = json.Unmarshal([]byte(content), &gptResponse) 162 | if err != nil { 163 | return GPTResponse{}, fmt.Errorf("failed to parse structured content: %w", err) 164 | } 165 | 166 | return gptResponse, nil 167 | } 168 | 169 | func GenerateCLICommand(response GPTResponse) string { 170 | var flags []string 171 | 172 | if response.Principal != nil { 173 | if val, ok := response.Principal["type"]; ok && val != "" { 174 | flags = append(flags, val) 175 | if val == "users" { 176 | flags = append(flags, "--user") 177 | } else if val == "groups" { 178 | flags = append(flags, "--group") 179 | } else if val == "roles" { 180 | flags = append(flags, "--role") 181 | } 182 | } 183 | if val, ok := response.Principal["name"]; ok && val != "" { 184 | flags = append(flags, val) 185 | } 186 | } 187 | 188 | if response.Action != "" { 189 | flags = append(flags, fmt.Sprintf("--operation %s", response.Action)) 190 | } 191 | 192 | if response.Policy != "" { 193 | flags = append(flags, fmt.Sprintf("--policy %s", response.Policy)) 194 | } 195 | 196 | if response.RequestedResource != "" { 197 | flags = append(flags, fmt.Sprintf("--resource %s", response.RequestedResource)) 198 | } 199 | 200 | if response.RequestedResourceType != "" { 201 | flags = append(flags, fmt.Sprintf("--service %s", response.RequestedResourceType)) 202 | } 203 | 204 | if len(flags) == 0 { 205 | return "No valid flags generated from GPT response." 206 | } 207 | 208 | return fmt.Sprintf("aws %s", strings.Join(flags, " ")) 209 | } 210 | -------------------------------------------------------------------------------- /internal/aws/find.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/iam" 8 | ) 9 | 10 | func (op *Api) FindUser(ctx context.Context, username string) (*iam.GetUserOutput, error) { 11 | return op.client.GetUser(ctx, &iam.GetUserInput{ 12 | UserName: aws.String(username), 13 | }) 14 | } 15 | 16 | func (op *Api) FindPolicy(ctx context.Context, arn string) (*iam.GetPolicyOutput, error) { 17 | return op.client.GetPolicy(ctx, &iam.GetPolicyInput{ 18 | PolicyArn: aws.String(arn), 19 | }) 20 | } 21 | 22 | func (op *Api) FindGroup(ctx context.Context, groupname string) (*iam.GetGroupOutput, error) { 23 | return op.client.GetGroup(ctx, &iam.GetGroupInput{ 24 | GroupName: aws.String(groupname), 25 | }) 26 | } 27 | 28 | func (op *Api) FindRole(ctx context.Context, rolename string) (*iam.GetRoleOutput, error) { 29 | return op.client.GetRole(ctx, &iam.GetRoleInput{ 30 | RoleName: aws.String(rolename), 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /internal/aws/list.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "net/http" 11 | "regexp" 12 | "time" 13 | 14 | "github.com/aws/aws-sdk-go-v2/service/iam/types" 15 | 16 | "github.com/aws/aws-sdk-go-v2/service/iam" 17 | 18 | "github.com/aws/aws-sdk-go-v2/aws" 19 | v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" 20 | ) 21 | 22 | // Resource represents a single AWS resource. 23 | type Resource struct { 24 | Name string 25 | Arn string 26 | } 27 | 28 | // ListResources fetches resources of a given type from the AWS Resource Explorer API. 29 | func (op *Api) ListResources(resourceType string) ([]Resource, error) { 30 | const service = "resource-explorer" 31 | const endpoint = "https://resource-explorer.%s.amazonaws.com/resources-list" 32 | 33 | // Validate inputs 34 | if resourceType == "" { 35 | return nil, fmt.Errorf("resourceType cannot be empty") 36 | } 37 | 38 | // Format the endpoint with the region 39 | url := fmt.Sprintf(endpoint, op.config.Region) 40 | 41 | // Retrieve actual credentials 42 | creds, err := op.config.Credentials.Retrieve(context.TODO()) 43 | if err != nil { 44 | return nil, fmt.Errorf("failed to retrieve AWS credentials: %w", err) 45 | } 46 | 47 | // Prepare the request payload 48 | payload := fmt.Sprintf(`{"ResourceType":"%s"}`, resourceType) 49 | 50 | // Hash the payload 51 | hash := sha256.Sum256([]byte(payload)) 52 | payloadHash := hex.EncodeToString(hash[:]) 53 | 54 | // Create the HTTP request 55 | req, err := http.NewRequest("POST", url, bytes.NewReader([]byte(payload))) 56 | if err != nil { 57 | return nil, fmt.Errorf("failed to create HTTP request: %w", err) 58 | } 59 | 60 | // Add required headers 61 | addHeaders(req, payloadHash) 62 | 63 | // Sign the request using AWS Signature Version 4 64 | err = signRequest(req, payloadHash, creds, service, op.config.Region) 65 | if err != nil { 66 | return nil, fmt.Errorf("failed to sign request: %w", err) 67 | } 68 | 69 | // Send the HTTP request 70 | client := &http.Client{} 71 | resp, err := client.Do(req) 72 | if err != nil { 73 | return nil, fmt.Errorf("failed to send HTTP request: %w", err) 74 | } 75 | defer resp.Body.Close() 76 | 77 | // Check for a successful response 78 | if resp.StatusCode != http.StatusOK { 79 | return nil, fmt.Errorf("API call failed with status %s", resp.Status) 80 | } 81 | 82 | // Parse the response body 83 | var listResourcesResponse map[string]interface{} 84 | if err := json.NewDecoder(resp.Body).Decode(&listResourcesResponse); err != nil { 85 | return nil, fmt.Errorf("failed to decode response body: %w", err) 86 | } 87 | 88 | // Extract resources 89 | resources, err := extractResources(listResourcesResponse) 90 | if err != nil { 91 | return nil, fmt.Errorf("failed to extract resources: %w", err) 92 | } 93 | 94 | return resources, nil 95 | } 96 | 97 | // addHeaders adds required headers to the HTTP request. 98 | func addHeaders(req *http.Request, payloadHash string) { 99 | req.Header.Add("accept", "*/*") 100 | req.Header.Add("content-type", "application/json") 101 | req.Header.Add("x-amz-content-sha256", payloadHash) 102 | req.Header.Add("x-amz-date", time.Now().UTC().Format("20060102T150405Z")) 103 | req.Header.Add("user-agent", "Custom-Client/1.0") 104 | } 105 | 106 | // signRequest signs the HTTP request using AWS Signature Version 4 and the loaded configuration. 107 | func signRequest(req *http.Request, payloadHash string, creds aws.Credentials, service, region string) error { 108 | signer := v4.NewSigner() 109 | return signer.SignHTTP(context.TODO(), creds, req, payloadHash, service, region, time.Now()) 110 | } 111 | 112 | // extractResources extracts resources from the API response. 113 | func extractResources(response map[string]interface{}) ([]Resource, error) { 114 | resourceArns, ok := response["ResourceArns"].([]interface{}) 115 | if !ok { 116 | return nil, fmt.Errorf("invalid response format: 'ResourceArns' missing or invalid") 117 | } 118 | 119 | var resources []Resource 120 | for _, arnInterface := range resourceArns { 121 | arn, ok := arnInterface.(string) 122 | if !ok { 123 | return nil, fmt.Errorf("invalid ARN format in response") 124 | } 125 | 126 | resources = append(resources, Resource{ 127 | Name: extractResourceName(arn), 128 | Arn: arn, 129 | }) 130 | } 131 | return resources, nil 132 | } 133 | 134 | // extractResourceName extracts the last component of an ARN. 135 | func extractResourceName(arn string) string { 136 | re := regexp.MustCompile(`([^/:]+)$`) 137 | matches := re.FindStringSubmatch(arn) 138 | if len(matches) > 1 { 139 | return matches[1] 140 | } 141 | return arn // Fallback to the full ARN if no match 142 | } 143 | 144 | func (op *Api) ListGroupsForUser(ctx context.Context, username string) ([]string, error) { 145 | var groups []string 146 | 147 | input := &iam.ListGroupsForUserInput{ 148 | UserName: aws.String(username), 149 | } 150 | 151 | resp, err := op.client.ListGroupsForUser(ctx, input) 152 | if err != nil { 153 | return nil, err 154 | } 155 | 156 | for _, group := range resp.Groups { 157 | groups = append(groups, *group.GroupName) 158 | } 159 | 160 | return groups, nil 161 | } 162 | 163 | func (op *Api) ListUsers(ctx context.Context) (*iam.ListUsersOutput, error) { 164 | input := &iam.ListUsersInput{} 165 | return op.client.ListUsers(ctx, input) 166 | } 167 | 168 | func (op *Api) ListPolicies(ctx context.Context) (*iam.ListPoliciesOutput, error) { 169 | input := &iam.ListPoliciesInput{ 170 | Scope: types.PolicyScopeTypeLocal, 171 | MaxItems: aws.Int32(500), 172 | } 173 | 174 | resp, err := op.client.ListPolicies(ctx, input) 175 | if err != nil { 176 | return nil, err 177 | } 178 | 179 | return resp, nil 180 | } 181 | 182 | func (op *Api) ListGroups(ctx context.Context) (*iam.ListGroupsOutput, error) { 183 | input := &iam.ListGroupsInput{} 184 | return op.client.ListGroups(ctx, input) 185 | } 186 | 187 | func (op *Api) ListRoles(ctx context.Context) (*iam.ListRolesOutput, error) { 188 | input := &iam.ListRolesInput{} 189 | return op.client.ListRoles(ctx, input) 190 | } 191 | 192 | func (op *Api) ListAttachedUserPolicies(ctx context.Context, username string) ([]string, error) { 193 | var names []string 194 | 195 | input := &iam.ListAttachedUserPoliciesInput{ 196 | UserName: aws.String(username), 197 | } 198 | 199 | resp, err := op.client.ListAttachedUserPolicies(ctx, input) 200 | if err != nil { 201 | return nil, err 202 | } 203 | 204 | for _, p := range resp.AttachedPolicies { 205 | names = append(names, *p.PolicyName) 206 | } 207 | 208 | return names, nil 209 | } 210 | 211 | func (op *Api) ListUserInlinePolicies(ctx context.Context, username string) ([]string, error) { 212 | input := &iam.ListUserPoliciesInput{ 213 | UserName: aws.String(username), 214 | } 215 | 216 | resp, err := op.client.ListUserPolicies(ctx, input) 217 | if err != nil { 218 | return nil, err 219 | } 220 | 221 | return resp.PolicyNames, nil 222 | } 223 | 224 | func (op *Api) ListGroupInlinePolicies(ctx context.Context, groupname string) ([]string, error) { 225 | input := &iam.ListGroupPoliciesInput{ 226 | GroupName: aws.String(groupname), 227 | } 228 | 229 | resp, err := op.client.ListGroupPolicies(ctx, input) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | return resp.PolicyNames, nil 235 | } 236 | 237 | func (op *Api) ListAttachedGroupPolicies(ctx context.Context, groupname string) ([]string, error) { 238 | var names []string 239 | 240 | input := &iam.ListAttachedGroupPoliciesInput{ 241 | GroupName: aws.String(groupname), 242 | } 243 | 244 | resp, err := op.client.ListAttachedGroupPolicies(ctx, input) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | for _, p := range resp.AttachedPolicies { 250 | names = append(names, *p.PolicyName) 251 | } 252 | 253 | return names, nil 254 | } 255 | 256 | func (op *Api) ListRoleInlinePolicies(ctx context.Context, rolename string) ([]string, error) { 257 | input := &iam.ListRolePoliciesInput{ 258 | RoleName: aws.String(rolename), 259 | } 260 | 261 | resp, err := op.client.ListRolePolicies(ctx, input) 262 | if err != nil { 263 | return nil, err 264 | } 265 | 266 | return resp.PolicyNames, nil 267 | } 268 | 269 | func (op *Api) ListAttachedRolePolicies(ctx context.Context, rolename string) ([]string, error) { 270 | var names []string 271 | input := &iam.ListAttachedRolePoliciesInput{ 272 | RoleName: aws.String(rolename), 273 | } 274 | resp, err := op.client.ListAttachedRolePolicies(ctx, input) 275 | if err != nil { 276 | return nil, err 277 | } 278 | for _, p := range resp.AttachedPolicies { 279 | names = append(names, *p.PolicyName) 280 | } 281 | return names, nil 282 | } 283 | -------------------------------------------------------------------------------- /internal/aws/operations.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/iam" 8 | ) 9 | 10 | type Api struct { 11 | client *iam.Client 12 | config aws.Config 13 | } 14 | 15 | func NewApi(config aws.Config) *Api { 16 | return &Api{ 17 | client: iam.NewFromConfig(config), 18 | config: config, 19 | } 20 | } 21 | 22 | func (op *Api) CreatePolicy(ctx context.Context, name, document string) (*iam.CreatePolicyOutput, error) { 23 | return op.client.CreatePolicy(ctx, &iam.CreatePolicyInput{ 24 | Description: aws.String("created by targe"), 25 | PolicyName: aws.String(name), 26 | PolicyDocument: aws.String(document), 27 | }) 28 | } 29 | 30 | func (op *Api) AttachPolicyToUser(ctx context.Context, policyArn, username string) error { 31 | _, err := op.client.AttachUserPolicy(ctx, &iam.AttachUserPolicyInput{ 32 | PolicyArn: aws.String(policyArn), 33 | UserName: aws.String(username), 34 | }) 35 | return err 36 | } 37 | 38 | func (op *Api) DetachPolicyFromUser(ctx context.Context, policyArn, username string) error { 39 | _, err := op.client.DetachUserPolicy(ctx, &iam.DetachUserPolicyInput{ 40 | PolicyArn: aws.String(policyArn), 41 | UserName: aws.String(username), 42 | }) 43 | return err 44 | } 45 | 46 | func (op *Api) AttachPolicyToGroup(ctx context.Context, policyArn, groupname string) error { 47 | _, err := op.client.AttachGroupPolicy(ctx, &iam.AttachGroupPolicyInput{ 48 | PolicyArn: aws.String(policyArn), 49 | GroupName: aws.String(groupname), 50 | }) 51 | return err 52 | } 53 | 54 | func (op *Api) DetachPolicyFromGroup(ctx context.Context, policyArn, groupname string) error { 55 | _, err := op.client.DetachGroupPolicy(ctx, &iam.DetachGroupPolicyInput{ 56 | PolicyArn: aws.String(policyArn), 57 | GroupName: aws.String(groupname), 58 | }) 59 | return err 60 | } 61 | 62 | func (op *Api) AttachPolicyToRole(ctx context.Context, policyArn, rolename string) error { 63 | _, err := op.client.AttachRolePolicy(ctx, &iam.AttachRolePolicyInput{ 64 | PolicyArn: aws.String(policyArn), 65 | RoleName: aws.String(rolename), 66 | }) 67 | return err 68 | } 69 | 70 | func (op *Api) DetachPolicyFromRole(ctx context.Context, policyArn, rolename string) error { 71 | _, err := op.client.DetachRolePolicy(ctx, &iam.DetachRolePolicyInput{ 72 | PolicyArn: aws.String(policyArn), 73 | RoleName: aws.String(rolename), 74 | }) 75 | return err 76 | } 77 | 78 | func (op *Api) PutInlinePolicyToUser(ctx context.Context, policyname, policyDocument, username string) error { 79 | _, err := op.client.PutUserPolicy(ctx, &iam.PutUserPolicyInput{ 80 | PolicyName: aws.String(policyname), 81 | PolicyDocument: aws.String(policyDocument), 82 | UserName: aws.String(username), 83 | }) 84 | return err 85 | } 86 | 87 | func (op *Api) DeleteInlinePolicyFromUser(ctx context.Context, policyname, username string) error { 88 | _, err := op.client.DeleteUserPolicy(ctx, &iam.DeleteUserPolicyInput{ 89 | PolicyName: aws.String(policyname), 90 | UserName: aws.String(username), 91 | }) 92 | return err 93 | } 94 | 95 | func (op *Api) PutInlinePolicyToGroup(ctx context.Context, policyname, policyDocument, groupname string) error { 96 | _, err := op.client.PutGroupPolicy(ctx, &iam.PutGroupPolicyInput{ 97 | PolicyName: aws.String(policyname), 98 | PolicyDocument: aws.String(policyDocument), 99 | GroupName: aws.String(groupname), 100 | }) 101 | return err 102 | } 103 | 104 | func (op *Api) DeleteInlinePolicyFromGroup(ctx context.Context, policyname, groupname string) error { 105 | _, err := op.client.DeleteGroupPolicy(ctx, &iam.DeleteGroupPolicyInput{ 106 | PolicyName: aws.String(policyname), 107 | GroupName: aws.String(groupname), 108 | }) 109 | return err 110 | } 111 | 112 | func (op *Api) PutInlinePolicyToRole(ctx context.Context, policyname, policyDocument, rolename string) error { 113 | _, err := op.client.PutRolePolicy(ctx, &iam.PutRolePolicyInput{ 114 | PolicyName: aws.String(policyname), 115 | PolicyDocument: aws.String(policyDocument), 116 | RoleName: aws.String(rolename), 117 | }) 118 | return err 119 | } 120 | 121 | func (op *Api) DeleteInlinePolicyFromRole(ctx context.Context, policyname, rolename string) error { 122 | _, err := op.client.DeleteRolePolicy(ctx, &iam.DeleteRolePolicyInput{ 123 | PolicyName: aws.String(policyname), 124 | RoleName: aws.String(rolename), 125 | }) 126 | return err 127 | } 128 | 129 | func (op *Api) AddUserToGroup(ctx context.Context, username, groupname string) error { 130 | _, err := op.client.AddUserToGroup(ctx, &iam.AddUserToGroupInput{ 131 | GroupName: aws.String(groupname), 132 | UserName: aws.String(username), 133 | }) 134 | return err 135 | } 136 | 137 | func (op *Api) RemoveUserFromGroup(ctx context.Context, username, groupname string) error { 138 | _, err := op.client.RemoveUserFromGroup(ctx, &iam.RemoveUserFromGroupInput{ 139 | GroupName: aws.String(groupname), 140 | UserName: aws.String(username), 141 | }) 142 | return err 143 | } 144 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/spf13/viper" 10 | ) 11 | 12 | type ( 13 | Config struct { 14 | OpenaiApiKey string `mapstructure:"openai_api_key"` 15 | } 16 | ) 17 | 18 | // NewConfig initializes and returns a new Config object by reading and unmarshalling 19 | // the configuration file from the given path. It falls back to the DefaultConfig if the 20 | // file is not found. If there's an error during the process, it returns the error. 21 | func NewConfig() (*Config, error) { 22 | // Start with the default configuration values 23 | cfg := DefaultConfig() 24 | 25 | // Set the name and type of the config file to be read 26 | viper.SetConfigName("config") 27 | viper.SetConfigType("toml") 28 | 29 | // Add the path where the config file is located 30 | configPath := os.ExpandEnv("$HOME/.targe/") 31 | viper.AddConfigPath(configPath) 32 | 33 | // Ensure the directory exists 34 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 35 | if err := os.MkdirAll(configPath, 0o755); err != nil { 36 | return nil, fmt.Errorf("failed to create config directory: %w", err) 37 | } 38 | } 39 | 40 | // Read the config file 41 | err := viper.ReadInConfig() 42 | if err != nil { 43 | // If the error is due to the file not being found, create a new one 44 | var configFileNotFoundError viper.ConfigFileNotFoundError 45 | if errors.As(err, &configFileNotFoundError) { 46 | filePath := filepath.Join(configPath, "config.toml") 47 | if err := writeDefaultConfig(filePath, cfg); err != nil { 48 | return nil, fmt.Errorf("failed to create config file: %w", err) 49 | } 50 | } 51 | } 52 | 53 | // Unmarshal the configuration data into the Config struct 54 | if err = viper.Unmarshal(cfg); err != nil { 55 | // If there's an error during unmarshalling, return the error with a message 56 | return nil, fmt.Errorf("failed to unmarshal server config: %w", err) 57 | } 58 | 59 | // Return the populated Config object 60 | return cfg, nil 61 | } 62 | 63 | func writeDefaultConfig(filePath string, cfg *Config) error { 64 | // Use viper to write the default configuration to a file 65 | viper.Set("openai_api_key", cfg.OpenaiApiKey) 66 | file, err := os.Create(filePath) 67 | if err != nil { 68 | return err 69 | } 70 | defer file.Close() 71 | 72 | if err := viper.WriteConfigAs(filePath); err != nil { 73 | return fmt.Errorf("failed to write default config: %w", err) 74 | } 75 | return nil 76 | } 77 | 78 | // DefaultConfig - Creates default config. 79 | func DefaultConfig() *Config { 80 | return &Config{ 81 | OpenaiApiKey: "", 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /internal/requirements/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | var Folder = "requirements" 10 | 11 | // WriteServicesToJSONFile writes the services slice to a JSON file 12 | func writeServicesToJSONFile(folder, filename string, services interface{}) error { 13 | // Ensure the folder exists 14 | if err := os.MkdirAll(folder, os.ModePerm); err != nil { 15 | return fmt.Errorf("failed to create folder: %w", err) 16 | } 17 | 18 | // Combine folder and filename to get full file path 19 | filePath := folder + "/" + filename 20 | 21 | // Create the file 22 | file, err := os.Create(filePath) 23 | if err != nil { 24 | return fmt.Errorf("failed to create file: %w", err) 25 | } 26 | defer file.Close() 27 | 28 | // Write JSON data to the file 29 | encoder := json.NewEncoder(file) 30 | encoder.SetIndent("", " ") // Pretty print with indentation 31 | if err := encoder.Encode(services); err != nil { 32 | return fmt.Errorf("failed to encode JSON: %w", err) 33 | } 34 | 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/requirements/aws/managed_policies.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | ) 10 | 11 | type ManagedPolicies struct{} 12 | 13 | func (p ManagedPolicies) GetName() string { 14 | return "aws::managed_policies" 15 | } 16 | 17 | func (p ManagedPolicies) GetFileName() string { 18 | return "managed_policies.json" 19 | } 20 | 21 | func (p ManagedPolicies) Install() error { 22 | policies, err := p.getManagedPolicies() 23 | if err != nil { 24 | } 25 | return writeServicesToJSONFile(Folder, p.GetFileName(), policies) 26 | } 27 | 28 | type ManagedPolicy struct { 29 | Name string `json:"name"` 30 | Arn string `json:"arn"` 31 | } 32 | 33 | // getManagedPolicies . 34 | func (p ManagedPolicies) getManagedPolicies() ([]ManagedPolicy, error) { 35 | // URL of the JSON file 36 | url := "https://aws-managed-policies-list.s3.eu-central-1.amazonaws.com/policies.json" 37 | 38 | // Fetch the JSON file 39 | resp, err := http.Get(url) 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer resp.Body.Close() 44 | 45 | // Read the response body 46 | body, err := ioutil.ReadAll(resp.Body) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | // Parse the JSON 52 | var policies []ManagedPolicy 53 | err = json.Unmarshal(body, &policies) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return policies, nil 59 | } 60 | 61 | // GetPolicies reads the services from a JSON file in a specified folder 62 | func (p ManagedPolicies) GetPolicies() ([]ManagedPolicy, error) { 63 | // Ensure the folder exists 64 | if _, err := os.Stat(Folder); os.IsNotExist(err) { 65 | return nil, fmt.Errorf("folder does not exist: %s", Folder) 66 | } 67 | 68 | // Construct the full file path 69 | filePath := Folder + "/" + p.GetFileName() 70 | 71 | // Open the JSON file 72 | file, err := os.Open(filePath) 73 | if err != nil { 74 | return nil, err 75 | } 76 | defer file.Close() 77 | 78 | // Decode the policies 79 | var policies []ManagedPolicy 80 | decoder := json.NewDecoder(file) 81 | if err := decoder.Decode(&policies); err != nil { 82 | return nil, err 83 | } 84 | 85 | return policies, nil 86 | } 87 | -------------------------------------------------------------------------------- /internal/requirements/aws/types.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/config" 12 | "github.com/aws/aws-sdk-go-v2/service/cloudformation" 13 | "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" 14 | ) 15 | 16 | type Types struct{} 17 | 18 | func (t Types) GetName() string { 19 | return "aws::types" 20 | } 21 | 22 | func (t Types) GetFileName() string { 23 | return "types.json" 24 | } 25 | 26 | func (t Types) Install() error { 27 | services := t.getServices() 28 | return writeServicesToJSONFile(Folder, t.GetFileName(), services) 29 | } 30 | 31 | type ListTypesResponse struct { 32 | TypeSummaries []types.TypeSummary `json:"TypeSummaries"` 33 | NextToken *string `json:"NextToken,omitempty"` 34 | } 35 | 36 | type Service struct { 37 | Name string `json:"name"` 38 | Description string `json:"description"` 39 | } 40 | 41 | // GetServices retrieves all CloudFormation resource types and binds them to a slice of Service structs 42 | func (t Types) getServices() []Service { 43 | // Load the AWS configuration 44 | cfg, err := config.LoadDefaultConfig(context.TODO(), config.WithRegion("us-east-1")) 45 | if err != nil { 46 | log.Fatalf("failed to load configuration, %v", err) 47 | } 48 | 49 | // Create a CloudFormation client 50 | client := cloudformation.NewFromConfig(cfg) 51 | 52 | var services []Service 53 | var nextToken *string 54 | 55 | for { 56 | // Create the input for the ListTypes API call 57 | input := &cloudformation.ListTypesInput{ 58 | Type: types.RegistryTypeResource, 59 | Visibility: types.VisibilityPublic, 60 | ProvisioningType: types.ProvisioningTypeFullyMutable, 61 | MaxResults: aws.Int32(100), 62 | NextToken: nextToken, 63 | } 64 | 65 | // Call the ListTypes API 66 | resp, err := client.ListTypes(context.TODO(), input) 67 | if err != nil { 68 | log.Fatalf("failed to list types, %v", err) 69 | } 70 | 71 | // Append results to the services slice 72 | for _, t := range resp.TypeSummaries { 73 | service := Service{ 74 | Name: aws.ToString(t.TypeName), 75 | Description: aws.ToString(t.Description), 76 | } 77 | services = append(services, service) 78 | } 79 | 80 | // Check if there is a next page 81 | if resp.NextToken == nil { 82 | break 83 | } 84 | nextToken = resp.NextToken 85 | } 86 | 87 | return services 88 | } 89 | 90 | // GetServices reads the services from a JSON file in a specified folder 91 | func (t Types) GetServices() ([]Service, error) { 92 | // Ensure the folder exists 93 | if _, err := os.Stat(Folder); os.IsNotExist(err) { 94 | return nil, fmt.Errorf("folder does not exist: %s", Folder) 95 | } 96 | 97 | // Construct the full file path 98 | filePath := Folder + "/" + t.GetFileName() 99 | 100 | // Open the JSON file 101 | file, err := os.Open(filePath) 102 | if err != nil { 103 | return nil, err 104 | } 105 | defer file.Close() 106 | 107 | // Decode the services 108 | var services []Service 109 | decoder := json.NewDecoder(file) 110 | if err := decoder.Decode(&services); err != nil { 111 | return nil, err 112 | } 113 | 114 | return services, nil 115 | } 116 | -------------------------------------------------------------------------------- /internal/requirements/requirements.go: -------------------------------------------------------------------------------- 1 | package requirements 2 | 3 | import ( 4 | "github.com/Permify/targe/internal/requirements/aws" 5 | ) 6 | 7 | type Requirement interface { 8 | GetFileName() string 9 | GetName() string 10 | Install() error 11 | } 12 | 13 | var requirements = []Requirement{ 14 | aws.Types{}, 15 | aws.ManagedPolicies{}, 16 | } 17 | 18 | func GetRequirements() []Requirement { 19 | return requirements 20 | } 21 | -------------------------------------------------------------------------------- /pkg/aws/groups/create_policy.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/charmbracelet/huh" 9 | 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | 13 | "github.com/Permify/targe/internal/ai" 14 | "github.com/Permify/targe/pkg/aws/models" 15 | ) 16 | 17 | type CreatePolicy struct { 18 | controller *Controller 19 | lg *lipgloss.Renderer 20 | styles *Styles 21 | form *huh.Form 22 | senderStyle lipgloss.Style 23 | err error 24 | width int 25 | message *string 26 | done *bool 27 | result string 28 | } 29 | 30 | func NewCreatePolicy(controller *Controller) CreatePolicy { 31 | m := CreatePolicy{controller: controller, width: maxWidth} 32 | m.lg = lipgloss.DefaultRenderer() 33 | m.styles = NewStyles(m.lg) 34 | 35 | doneInitialValue := false 36 | m.done = &doneInitialValue 37 | 38 | messageInitialValue := "" 39 | m.message = &messageInitialValue 40 | 41 | m.form = huh.NewForm( 42 | huh.NewGroup( 43 | huh.NewText().Key("message"). 44 | Title("Describe Your Policy"). 45 | Value(m.message), 46 | 47 | huh.NewConfirm(). 48 | Key("done"). 49 | Title("All done?"). 50 | Value(m.done). 51 | Affirmative("Yes"). 52 | Negative("Refresh"), 53 | ), 54 | ). 55 | WithWidth(45). 56 | WithShowHelp(false). 57 | WithShowErrors(false) 58 | 59 | return m 60 | } 61 | 62 | func (m CreatePolicy) Init() tea.Cmd { 63 | return m.form.Init() 64 | } 65 | 66 | func (m CreatePolicy) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 67 | switch msg := msg.(type) { 68 | case tea.WindowSizeMsg: 69 | m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize() 70 | 71 | case tea.KeyMsg: 72 | 73 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 74 | return m, tea.Quit 75 | } 76 | 77 | // Check if the "Refresh" or "Done" button was selected 78 | if msg.String() == "enter" { 79 | if m.done != nil && *m.done { 80 | return Switch(m.controller.Next(), 0, 0) 81 | } else { 82 | var resourceArn *string = nil 83 | if m.controller.State.GetResource() != nil { 84 | resourceArn = &m.controller.State.GetResource().Arn 85 | } 86 | 87 | var serviceName *string = nil 88 | if m.controller.State.GetService() != nil { 89 | serviceName = &m.controller.State.GetService().Name 90 | } 91 | 92 | if m.message == nil { 93 | m.err = errors.New("Please provide a message") 94 | } 95 | 96 | policy, err := ai.GeneratePolicy(m.controller.openAiApiKey, *m.message, serviceName, resourceArn) 97 | 98 | policyJson, err := json.MarshalIndent(policy, "", "\t") 99 | if err != nil { 100 | m.err = err 101 | } 102 | 103 | m.result = string(policyJson) 104 | 105 | m.controller.State.SetPolicy(&models.Policy{ 106 | Arn: "new", 107 | Name: policy.Id, 108 | Document: string(policyJson), 109 | }) 110 | 111 | m.reinitializeForm() 112 | } 113 | } 114 | } 115 | 116 | var cmds []tea.Cmd 117 | 118 | // Process the form 119 | form, cmd := m.form.Update(msg) 120 | if f, ok := form.(*huh.Form); ok { 121 | m.form = f 122 | cmds = append(cmds, cmd) 123 | } 124 | 125 | return m, tea.Batch(cmds...) 126 | } 127 | 128 | func (m CreatePolicy) View() string { 129 | s := m.styles 130 | 131 | v := strings.TrimSuffix(m.form.View(), "\n\n") 132 | form := m.lg.NewStyle().Margin(1, 0).Render(v) 133 | 134 | var titles []string 135 | var title string 136 | 137 | if m.controller.State.GetGroup() != nil { 138 | titles = append(titles, 139 | s.StateHeader.Render("Group Name: "+m.controller.State.GetGroup().Name), 140 | s.StateHeader.Render("Group ARN: "+m.controller.State.GetGroup().Arn), 141 | ) 142 | } 143 | 144 | if m.controller.State.GetService() != nil && m.controller.State.GetResource() != nil { 145 | titles = append(titles, 146 | s.StateHeader.Render("Service Name: "+m.controller.State.GetService().Name), 147 | s.StateHeader.Render("Resource ARN: "+m.controller.State.GetResource().Arn), 148 | ) 149 | } 150 | 151 | if len(titles) > 0 { 152 | // Join the titles vertically 153 | title = lipgloss.JoinVertical(lipgloss.Left, titles...) 154 | 155 | // Apply margin-top to the entire title block 156 | title = lipgloss.NewStyle(). 157 | MarginTop(1). // Set the margin top to 2 lines 158 | Render(title) 159 | } 160 | 161 | // Status (right side) 162 | var status string 163 | { 164 | buildInfo := "(None)" 165 | 166 | if m.result != "" { 167 | buildInfo = m.result 168 | } 169 | 170 | const statusWidth = 60 171 | statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() 172 | status = s.Status. 173 | Height(lipgloss.Height(form)). 174 | Width(statusWidth). 175 | MarginLeft(statusMarginLeft). 176 | Render(s.StatusHeader.Render("Policy") + "\n" + 177 | buildInfo) 178 | } 179 | 180 | errors := m.form.Errors() 181 | header := lipgloss.JoinVertical(lipgloss.Top, 182 | m.appBoundaryView("Custom Policy Generator"), 183 | title, 184 | ) 185 | if len(errors) > 0 { 186 | header = m.appErrorBoundaryView(m.errorView()) 187 | } 188 | body := lipgloss.JoinHorizontal(lipgloss.Top, form, status) 189 | 190 | footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 191 | if len(errors) > 0 { 192 | footer = m.appErrorBoundaryView("") 193 | } 194 | 195 | return s.Base.Render(header + "\n" + body + "\n\n" + footer) 196 | } 197 | 198 | func (m CreatePolicy) errorView() string { 199 | var s string 200 | for _, err := range m.form.Errors() { 201 | s += err.Error() 202 | } 203 | return s 204 | } 205 | 206 | func (m CreatePolicy) appBoundaryView(text string) string { 207 | return lipgloss.PlaceHorizontal( 208 | m.width, 209 | lipgloss.Left, 210 | m.styles.HeaderText.Render(text), 211 | lipgloss.WithWhitespaceChars("/"), 212 | lipgloss.WithWhitespaceForeground(indigo), 213 | ) 214 | } 215 | 216 | func (m CreatePolicy) appErrorBoundaryView(text string) string { 217 | return lipgloss.PlaceHorizontal( 218 | m.width, 219 | lipgloss.Left, 220 | m.styles.ErrorHeaderText.Render(text), 221 | lipgloss.WithWhitespaceChars("/"), 222 | lipgloss.WithWhitespaceForeground(red), 223 | ) 224 | } 225 | 226 | func (m *CreatePolicy) reinitializeForm() { 227 | doneInitialValue := false 228 | m.done = &doneInitialValue 229 | 230 | // Preserve the current message value 231 | m.form = huh.NewForm( 232 | huh.NewGroup( 233 | huh.NewText(). 234 | Key("message"). 235 | Title("Describe Your Policy").Value(m.message), 236 | huh.NewConfirm(). 237 | Key("done"). 238 | Title("All done?"). 239 | Value(m.done). 240 | Affirmative("Yes"). 241 | Negative("Refresh"), 242 | ), 243 | ). 244 | WithWidth(45). 245 | WithShowHelp(false). 246 | WithShowErrors(false) 247 | } 248 | -------------------------------------------------------------------------------- /pkg/aws/groups/group_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type GroupList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewGroupList(controller *Controller) GroupList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := GroupList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Groups" 32 | return view 33 | } 34 | 35 | func (m GroupList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadGroups()) 37 | } 38 | 39 | func (m GroupList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | group := m.list.SelectedItem().(models.Group) 50 | m.controller.State.SetGroup(&group) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case GroupLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m GroupList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/aws/groups/operation_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type OperationList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewOperationList(controller *Controller) OperationList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := OperationList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Operations" 32 | return view 33 | } 34 | 35 | func (m OperationList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadOperations()) 37 | } 38 | 39 | func (m OperationList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.Operation) 50 | m.controller.State.SetOperation(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case OperationLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m OperationList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/groups/policy_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyList(controller *Controller) PolicyList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Policies" 32 | return view 33 | } 34 | 35 | func (m PolicyList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicies()) 37 | } 38 | 39 | func (m PolicyList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | policy := m.list.SelectedItem().(models.Policy) 50 | m.controller.State.SetPolicy(&policy) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/groups/policy_option_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyOptionList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyOptionList(controller *Controller) PolicyOptionList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyOptionList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Options" 32 | return view 33 | } 34 | 35 | func (m PolicyOptionList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicyOptions()) 37 | } 38 | 39 | func (m PolicyOptionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.PolicyOption) 50 | m.controller.State.SetPolicyOption(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyOptionLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyOptionList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/groups/resource_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ResourceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewResourceList(controller *Controller) ResourceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ResourceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Resources" 32 | return view 33 | } 34 | 35 | func (m ResourceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadResources()) 37 | } 38 | 39 | func (m ResourceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | resource := m.list.SelectedItem().(models.Resource) 50 | m.controller.State.SetResource(&resource) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ResourceLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m ResourceList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/aws/groups/result.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/huh" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/lipgloss/table" 11 | 12 | "github.com/Permify/targe/pkg/aws/models" 13 | ) 14 | 15 | type Result struct { 16 | controller *Controller 17 | lg *lipgloss.Renderer 18 | styles *Styles 19 | form *huh.Form 20 | width int 21 | value *bool 22 | error error 23 | } 24 | 25 | func NewResult(controller *Controller) Result { 26 | // Initialize the Result with default values 27 | result := Result{ 28 | width: maxWidth, 29 | lg: lipgloss.DefaultRenderer(), 30 | controller: controller, 31 | } 32 | 33 | // Initialize styles 34 | result.styles = NewStyles(result.lg) 35 | 36 | // Initialize value pointer 37 | initialValue := false 38 | result.value = &initialValue 39 | 40 | // Configure the form 41 | result.form = createForm(result.value) 42 | 43 | return result 44 | } 45 | 46 | func (m Result) Init() tea.Cmd { 47 | return m.form.Init() 48 | } 49 | 50 | func min(x, y int) int { 51 | if x > y { 52 | return y 53 | } 54 | return x 55 | } 56 | 57 | func (m Result) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 58 | switch msg := msg.(type) { 59 | case tea.WindowSizeMsg: 60 | m.width = min(msg.Width, 80) - m.styles.Base.GetHorizontalFrameSize() 61 | case tea.KeyMsg: 62 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 63 | return m, tea.Quit 64 | } 65 | 66 | // Handle "Enter" for exit confirmation 67 | if m.form.State == huh.StateCompleted && msg.String() == "enter" { 68 | return m, tea.Quit 69 | } 70 | } 71 | 72 | var cmds []tea.Cmd 73 | 74 | // Process the form 75 | form, cmd := m.form.Update(msg) 76 | if f, ok := form.(*huh.Form); ok { 77 | m.form = f 78 | cmds = append(cmds, cmd) 79 | } 80 | 81 | // Handle form completion 82 | if m.form.State == huh.StateCompleted { 83 | if *m.value { 84 | if err := m.controller.Done(); err != nil { 85 | // Handle error without quitting 86 | m.error = err 87 | return m, nil // Return updated model without quitting 88 | } 89 | } else { 90 | cmds = append(cmds, tea.Quit) 91 | } 92 | } 93 | 94 | return m, tea.Batch(cmds...) 95 | } 96 | 97 | func (m Result) View() string { 98 | if m.form.State == huh.StateCompleted && m.error == nil { 99 | // Success Message with Exit Footer 100 | successMessage := fmt.Sprintf( 101 | "\n%s\n\n%s\n", 102 | lipgloss.NewStyle(). 103 | Bold(true). 104 | Foreground(lipgloss.Color("10")). 105 | Render("✔ Operation executed successfully!"), 106 | lipgloss.NewStyle(). 107 | Foreground(lipgloss.Color("7")). 108 | Italic(true). 109 | Render("The requested AWS IAM operation has been completed."), 110 | ) 111 | 112 | exitFooter := lipgloss.NewStyle(). 113 | Foreground(lipgloss.Color("8")). 114 | Italic(true). 115 | Render("Press Enter to exit.") 116 | 117 | return successMessage + "\n" + exitFooter 118 | } 119 | 120 | // When not in completed state, display other UI elements 121 | rows := m.collectOverviewRows() 122 | t := m.createTable(rows) 123 | formView := m.lg.NewStyle().Margin(1, 0).Render(strings.TrimSuffix(m.form.View(), "\n\n")) 124 | header := m.renderHeader() 125 | footer := m.renderFooter() 126 | 127 | body := lipgloss.JoinVertical(lipgloss.Top, t.Render(), formView) 128 | 129 | // Add error message if present 130 | if m.error != nil { 131 | errorView := fmt.Sprintf( 132 | "\n%s\n\n%s\n", 133 | lipgloss.NewStyle(). 134 | Bold(true). 135 | Foreground(lipgloss.Color("9")). 136 | Render("✖ An error occurred"), 137 | lipgloss.NewStyle(). 138 | Foreground(lipgloss.Color("1")). 139 | Italic(true). 140 | Render(m.error.Error()), 141 | ) 142 | body = lipgloss.JoinVertical(lipgloss.Top, body, errorView) 143 | } 144 | 145 | return m.styles.Base.Render(header + "\n" + body + "\n\n" + footer) 146 | } 147 | 148 | func (m Result) collectOverviewRows() [][]string { 149 | var rows [][]string 150 | state := m.controller.State 151 | 152 | if state.group != nil { 153 | rows = append(rows, []string{"Group", state.group.Name, state.group.Arn}) 154 | } 155 | if state.operation != nil { 156 | rows = append(rows, []string{"Operation", state.operation.Name, state.operation.Desc}) 157 | } 158 | if state.group != nil { 159 | rows = append(rows, []string{"Group", state.group.Name, state.group.Arn}) 160 | } 161 | if state.service != nil { 162 | rows = append(rows, []string{"Service", state.service.Name, state.service.Desc}) 163 | } 164 | if state.resource != nil { 165 | rows = append(rows, []string{"Resource", state.resource.Name, state.resource.Arn}) 166 | } 167 | if state.policy != nil { 168 | rows = append(rows, m.formatPolicyRow(state.policy)) 169 | } 170 | 171 | return rows 172 | } 173 | 174 | func (m Result) formatPolicyRow(policy *models.Policy) []string { 175 | if len(policy.Document) > 0 { 176 | return []string{"Policy", policy.Name, "new"} 177 | } 178 | return []string{"Policy", policy.Name, policy.Arn} 179 | } 180 | 181 | func (m Result) createTable(rows [][]string) *table.Table { 182 | return table.New(). 183 | Border(lipgloss.HiddenBorder()). 184 | BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). 185 | StyleFunc(func(row, col int) lipgloss.Style { 186 | if col == 0 { 187 | return m.styles.Base.Foreground(lipgloss.Color("205")).Bold(true) 188 | } 189 | return m.styles.Base 190 | }). 191 | Rows(rows...) 192 | } 193 | 194 | func (m Result) renderHeader() string { 195 | errors := m.form.Errors() 196 | if len(errors) > 0 { 197 | return m.appErrorBoundaryView(m.errorView()) 198 | } 199 | return m.appBoundaryView("Overview") 200 | } 201 | 202 | func (m Result) renderFooter() string { 203 | errors := m.form.Errors() 204 | if len(errors) > 0 { 205 | return m.appErrorBoundaryView("") 206 | } 207 | return m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 208 | } 209 | 210 | func (m Result) errorView() string { 211 | var s string 212 | for _, err := range m.form.Errors() { 213 | s += err.Error() + "\n" 214 | } 215 | return s 216 | } 217 | 218 | func (m Result) appBoundaryView(text string) string { 219 | return lipgloss.PlaceHorizontal( 220 | m.width, 221 | lipgloss.Left, 222 | m.styles.HeaderText.Render(text), 223 | lipgloss.WithWhitespaceChars("/"), 224 | lipgloss.WithWhitespaceForeground(indigo), 225 | ) 226 | } 227 | 228 | func (m Result) appErrorBoundaryView(text string) string { 229 | return lipgloss.PlaceHorizontal( 230 | m.width, 231 | lipgloss.Left, 232 | m.styles.ErrorHeaderText.Render(text), 233 | lipgloss.WithWhitespaceChars("/"), 234 | lipgloss.WithWhitespaceForeground(indigo), 235 | ) 236 | } 237 | 238 | func createForm(value *bool) *huh.Form { 239 | confirm := huh.NewConfirm(). 240 | Key("done"). 241 | Title("All done?"). 242 | Validate(func(v bool) error { 243 | return nil 244 | }). 245 | Affirmative("Yes"). 246 | Negative("No"). 247 | Value(value) 248 | 249 | return huh.NewForm( 250 | huh.NewGroup(confirm), 251 | ). 252 | WithWidth(45). 253 | WithShowHelp(false). 254 | WithShowErrors(false) 255 | } 256 | -------------------------------------------------------------------------------- /pkg/aws/groups/service_list.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ServiceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewServiceList(controller *Controller) ServiceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ServiceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Services" 32 | return view 33 | } 34 | 35 | func (m ServiceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadServices()) 37 | } 38 | 39 | func (m ServiceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | service := m.list.SelectedItem().(models.Service) 50 | m.controller.State.SetService(&service) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ServiceLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m ServiceList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/groups/state.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/Permify/targe/pkg/aws/models" 5 | ) 6 | 7 | // State represents the groups flow state. 8 | type State struct { 9 | group *models.Group 10 | operation *models.Operation 11 | policyOption *models.PolicyOption 12 | service *models.Service 13 | resource *models.Resource 14 | policy *models.Policy 15 | } 16 | 17 | // Getters 18 | 19 | // GetGroup retrieves the group from the state. 20 | func (s *State) GetGroup() *models.Group { 21 | return s.group 22 | } 23 | 24 | // GetOperation retrieves the operation from the state. 25 | func (s *State) GetOperation() *models.Operation { 26 | return s.operation 27 | } 28 | 29 | // GetPolicyOption retrieves the policy option from the state. 30 | func (s *State) GetPolicyOption() *models.PolicyOption { 31 | return s.policyOption 32 | } 33 | 34 | // GetService retrieves the service from the state. 35 | func (s *State) GetService() *models.Service { 36 | return s.service 37 | } 38 | 39 | // GetResource retrieves the resource from the state. 40 | func (s *State) GetResource() *models.Resource { 41 | return s.resource 42 | } 43 | 44 | // GetPolicy retrieves the policy from the state. 45 | func (s *State) GetPolicy() *models.Policy { 46 | return s.policy 47 | } 48 | 49 | // Setters 50 | 51 | // SetGroup updates the group in the state. 52 | func (s *State) SetGroup(group *models.Group) { 53 | s.group = group 54 | } 55 | 56 | // SetOperation updates the action in the state. 57 | func (s *State) SetOperation(operation *models.Operation) { 58 | s.operation = operation 59 | } 60 | 61 | // SetPolicyOption updates the policy option in the state. 62 | func (s *State) SetPolicyOption(policyOption *models.PolicyOption) { 63 | s.policyOption = policyOption 64 | } 65 | 66 | // SetService updates the service in the state. 67 | func (s *State) SetService(service *models.Service) { 68 | s.service = service 69 | } 70 | 71 | // SetResource updates the resource in the state. 72 | func (s *State) SetResource(resource *models.Resource) { 73 | s.resource = resource 74 | } 75 | 76 | // SetPolicy updates the policy in the state. 77 | func (s *State) SetPolicy(policy *models.Policy) { 78 | s.policy = policy 79 | } 80 | -------------------------------------------------------------------------------- /pkg/aws/groups/styles.go: -------------------------------------------------------------------------------- 1 | package groups 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var listStyle = lipgloss.NewStyle().Margin(1, 2) 8 | 9 | var spinnerStyle = lipgloss.NewStyle(). 10 | Foreground(lipgloss.Color("205")). 11 | Bold(true) 12 | 13 | const maxWidth = 100 14 | 15 | var ( 16 | red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} 17 | purple = lipgloss.Color("212") 18 | indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} 19 | green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} 20 | ) 21 | 22 | type Styles struct { 23 | Base, 24 | HeaderText, 25 | Status, 26 | StatusHeader, 27 | StateHeader, 28 | Highlight, 29 | ErrorHeaderText, 30 | Help lipgloss.Style 31 | } 32 | 33 | func NewStyles(lg *lipgloss.Renderer) *Styles { 34 | s := Styles{} 35 | s.Base = lg.NewStyle(). 36 | Padding(1, 4, 0, 1) 37 | s.HeaderText = lg.NewStyle(). 38 | Foreground(purple). 39 | Bold(true). 40 | Padding(0, 1, 0, 2) 41 | s.Status = lg.NewStyle(). 42 | Border(lipgloss.RoundedBorder()). 43 | BorderForeground(purple). 44 | PaddingLeft(1). 45 | MarginTop(1) 46 | s.StateHeader = lipgloss.NewStyle(). 47 | Bold(true). 48 | Foreground(green).MarginLeft(2).MarginTop(0).MarginLeft(2) 49 | s.StatusHeader = lg.NewStyle(). 50 | Foreground(green). 51 | Bold(true) 52 | s.Highlight = lg.NewStyle(). 53 | Foreground(lipgloss.Color("212")) 54 | s.ErrorHeaderText = s.HeaderText. 55 | Foreground(red) 56 | s.Help = lg.NewStyle(). 57 | Foreground(lipgloss.Color("240")) 58 | return &s 59 | } 60 | -------------------------------------------------------------------------------- /pkg/aws/models/group.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Group struct { 4 | Arn string 5 | Name string 6 | } 7 | 8 | func (i Group) Title() string { return i.Name } 9 | func (i Group) Description() string { return i.Arn } 10 | func (i Group) FilterValue() string { return i.Name } 11 | -------------------------------------------------------------------------------- /pkg/aws/models/operation.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Operation struct { 4 | Id string 5 | Name string 6 | Desc string 7 | } 8 | 9 | func (i Operation) Title() string { return i.Name } 10 | func (i Operation) Description() string { return i.Desc } 11 | func (i Operation) FilterValue() string { return i.Name } 12 | -------------------------------------------------------------------------------- /pkg/aws/models/policy.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Policy struct { 4 | Arn string 5 | Name string 6 | Document string 7 | } 8 | 9 | func (i Policy) Title() string { return i.Name } 10 | func (i Policy) Description() string { return i.Arn } 11 | func (i Policy) FilterValue() string { return i.Name } 12 | -------------------------------------------------------------------------------- /pkg/aws/models/policy_option.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type PolicyOption struct { 4 | Id string 5 | Name string 6 | Desc string 7 | } 8 | 9 | func (i PolicyOption) Title() string { return i.Name } 10 | func (i PolicyOption) Description() string { return i.Desc } 11 | func (i PolicyOption) FilterValue() string { return i.Name } 12 | -------------------------------------------------------------------------------- /pkg/aws/models/resource.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Resource struct { 4 | Arn string 5 | Name string 6 | } 7 | 8 | func (i Resource) Title() string { return i.Name } 9 | func (i Resource) Description() string { return i.Arn } 10 | func (i Resource) FilterValue() string { return i.Arn } 11 | -------------------------------------------------------------------------------- /pkg/aws/models/role.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Role struct { 4 | Arn string 5 | Name string 6 | } 7 | 8 | func (i Role) Title() string { return i.Name } 9 | func (i Role) Description() string { return i.Arn } 10 | func (i Role) FilterValue() string { return i.Name } 11 | -------------------------------------------------------------------------------- /pkg/aws/models/service.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type Service struct { 4 | Name string 5 | Desc string 6 | } 7 | 8 | func (i Service) Title() string { return i.Name } 9 | func (i Service) Description() string { return i.Desc } 10 | func (i Service) FilterValue() string { return i.Name } 11 | -------------------------------------------------------------------------------- /pkg/aws/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type User struct { 4 | Arn string 5 | Name string 6 | } 7 | 8 | func (i User) Title() string { return i.Name } 9 | func (i User) Description() string { return i.Arn } 10 | func (i User) FilterValue() string { return i.Name } 11 | -------------------------------------------------------------------------------- /pkg/aws/roles/controller.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "slices" 7 | 8 | "github.com/charmbracelet/bubbles/list" 9 | tea "github.com/charmbracelet/bubbletea" 10 | 11 | "github.com/Permify/targe/internal/aws" 12 | requirements "github.com/Permify/targe/internal/requirements/aws" 13 | "github.com/Permify/targe/pkg/aws/models" 14 | ) 15 | 16 | type Controller struct { 17 | api *aws.Api 18 | openAiApiKey string 19 | State *State 20 | } 21 | 22 | func NewController(api *aws.Api, openaiApiKey string, state *State) *Controller { 23 | return &Controller{ 24 | api: api, 25 | openAiApiKey: openaiApiKey, 26 | State: state, 27 | } 28 | } 29 | 30 | // FailedMsg represents a failure operation. 31 | type FailedMsg struct { 32 | Err error 33 | } 34 | 35 | type RoleLoadedMsg struct{ List []list.Item } 36 | 37 | // LoadRoles loads roles from the AWS API. 38 | func (c *Controller) LoadRoles() tea.Cmd { 39 | return func() tea.Msg { 40 | var items []list.Item 41 | 42 | output, err := c.api.ListRoles(context.Background()) 43 | if err != nil { 44 | return FailedMsg{Err: err} 45 | } 46 | 47 | for _, role := range output.Roles { 48 | items = append(items, models.Role{ 49 | Name: *role.RoleName, 50 | Arn: *role.Arn, 51 | }) 52 | } 53 | 54 | return RoleLoadedMsg{List: items} 55 | } 56 | } 57 | 58 | type OperationLoadedMsg struct{ List []list.Item } 59 | 60 | // LoadOperations loads operations. 61 | func (c *Controller) LoadOperations() tea.Cmd { 62 | return func() tea.Msg { 63 | items := []list.Item{ 64 | models.Operation{Id: AttachPolicySlug.String(), Name: ReachableOperations[AttachPolicySlug].Name, Desc: ReachableOperations[AttachPolicySlug].Desc}, 65 | models.Operation{Id: DetachPolicySlug.String(), Name: ReachableOperations[DetachPolicySlug].Name, Desc: ReachableOperations[DetachPolicySlug].Desc}, 66 | models.Operation{Id: AttachCustomPolicySlug.String(), Name: ReachableOperations[AttachCustomPolicySlug].Name, Desc: ReachableOperations[AttachCustomPolicySlug].Desc}, 67 | } 68 | return OperationLoadedMsg{List: items} 69 | } 70 | } 71 | 72 | type ServiceLoadedMsg struct{ List []list.Item } 73 | 74 | // LoadServices loads services. 75 | func (c *Controller) LoadServices() tea.Cmd { 76 | return func() tea.Msg { 77 | t := requirements.Types{} 78 | services, err := t.GetServices() 79 | if err != nil { 80 | return FailedMsg{Err: err} 81 | } 82 | 83 | var items []list.Item 84 | 85 | for _, service := range services { 86 | items = append(items, models.Service{ 87 | Name: service.Name, 88 | Desc: service.Description, 89 | }) 90 | } 91 | return ServiceLoadedMsg{List: items} 92 | } 93 | } 94 | 95 | type ResourceLoadedMsg struct{ List []list.Item } 96 | 97 | // LoadResources loads resources. 98 | func (c *Controller) LoadResources() tea.Cmd { 99 | return func() tea.Msg { 100 | items := []list.Item{ 101 | models.Resource{Name: "All Resources", Arn: "*"}, 102 | } 103 | 104 | resources, err := c.api.ListResources(c.State.GetService().Name) 105 | if err != nil { 106 | return FailedMsg{Err: err} 107 | } 108 | 109 | for _, resource := range resources { 110 | items = append(items, models.Resource{ 111 | Name: resource.Name, 112 | Arn: resource.Arn, 113 | }) 114 | } 115 | 116 | return ResourceLoadedMsg{List: items} 117 | } 118 | } 119 | 120 | type PolicyLoadedMsg struct{ List []list.Item } 121 | 122 | // LoadPolicies loads policies. 123 | func (c *Controller) LoadPolicies() tea.Cmd { 124 | return func() tea.Msg { 125 | var items []list.Item 126 | 127 | policies, err := c.api.ListPolicies(context.Background()) 128 | if err != nil { 129 | return FailedMsg{Err: err} 130 | } 131 | 132 | mp := requirements.ManagedPolicies{} 133 | managedPolicies, err := mp.GetPolicies() 134 | if err != nil { 135 | return FailedMsg{Err: err} 136 | } 137 | 138 | attachedPolicies, err := c.api.ListAttachedRolePolicies(context.Background(), c.State.role.Name) 139 | if err != nil { 140 | return FailedMsg{Err: err} 141 | } 142 | 143 | switch c.State.operation.Id { 144 | case AttachPolicySlug.String(): 145 | for _, policy := range policies.Policies { 146 | if !slices.Contains(attachedPolicies, *policy.PolicyName) { 147 | items = append(items, models.Policy{ 148 | Name: *policy.PolicyName, 149 | Arn: *policy.Arn, 150 | }) 151 | } 152 | } 153 | 154 | for _, policy := range managedPolicies { 155 | if !slices.Contains(attachedPolicies, policy.Name) { 156 | items = append(items, models.Policy{ 157 | Name: policy.Name, 158 | Arn: policy.Arn, 159 | }) 160 | } 161 | } 162 | case DetachPolicySlug.String(): 163 | inlinePolicies, err := c.api.ListRoleInlinePolicies(context.Background(), c.State.role.Name) 164 | if err != nil { 165 | return FailedMsg{Err: err} 166 | } 167 | 168 | for _, name := range inlinePolicies { 169 | items = append(items, models.Policy{ 170 | Name: name, 171 | Arn: "inline", 172 | }) 173 | } 174 | 175 | for _, policy := range policies.Policies { 176 | if slices.Contains(attachedPolicies, *policy.PolicyName) { 177 | items = append(items, models.Policy{ 178 | Name: *policy.PolicyName, 179 | Arn: *policy.Arn, 180 | }) 181 | } 182 | } 183 | 184 | for _, policy := range managedPolicies { 185 | if slices.Contains(attachedPolicies, policy.Name) { 186 | items = append(items, models.Policy{ 187 | Name: policy.Name, 188 | Arn: policy.Arn, 189 | }) 190 | } 191 | } 192 | } 193 | 194 | return PolicyLoadedMsg{List: items} 195 | } 196 | } 197 | 198 | type PolicyOptionLoadedMsg struct{ List []list.Item } 199 | 200 | // LoadPolicyOptions loads operations. 201 | func (c *Controller) LoadPolicyOptions() tea.Cmd { 202 | return func() tea.Msg { 203 | items := []list.Item{ 204 | models.PolicyOption{Id: WithoutResourceSlug.String(), Name: ReachablePolicyOptions[WithoutResourceSlug].Name, Desc: ReachablePolicyOptions[WithoutResourceSlug].Desc}, 205 | models.PolicyOption{Id: WithResourceSlug.String(), Name: ReachablePolicyOptions[WithResourceSlug].Name, Desc: ReachablePolicyOptions[WithResourceSlug].Desc}, 206 | } 207 | return PolicyOptionLoadedMsg{List: items} 208 | } 209 | } 210 | 211 | type OperationType string 212 | 213 | // Constants representing role actions and their slugs 214 | const ( 215 | AttachPolicySlug OperationType = "attach_policy" 216 | DetachPolicySlug OperationType = "detach_policy" 217 | AttachCustomPolicySlug OperationType = "attach_custom_policy" 218 | ) 219 | 220 | func (o OperationType) String() string { 221 | return string(o) 222 | } 223 | 224 | // ReachableOperations Predefined list of actions with their names and descriptions 225 | var ReachableOperations = map[OperationType]models.Operation{ 226 | AttachPolicySlug: { 227 | Id: AttachPolicySlug.String(), 228 | Name: "Attach Policy (attach_policy)", 229 | Desc: "Assign a policy to the role.", 230 | }, 231 | DetachPolicySlug: { 232 | Id: DetachPolicySlug.String(), 233 | Name: "Detach Policy (detach_policy)", 234 | Desc: "Remove a policy from the role.", 235 | }, 236 | AttachCustomPolicySlug: { 237 | Id: AttachCustomPolicySlug.String(), 238 | Name: "Attach Custom Policy (attach_custom_policy)", 239 | Desc: "Create and attach a custom policy.", 240 | }, 241 | } 242 | 243 | type PolicyOptionType string 244 | 245 | // Constants representing custom policy options and their slugs 246 | const ( 247 | WithoutResourceSlug PolicyOptionType = "without_resource" 248 | WithResourceSlug PolicyOptionType = "with_resource" 249 | ) 250 | 251 | func (o PolicyOptionType) String() string { 252 | return string(o) 253 | } 254 | 255 | // ReachablePolicyOptions Predefined list of custom policy options with their names and descriptions 256 | var ReachablePolicyOptions = map[PolicyOptionType]models.PolicyOption{ 257 | WithoutResourceSlug: { 258 | Id: WithoutResourceSlug.String(), 259 | Name: "Without Resource (without_resource)", 260 | Desc: "Applies globally without a resource.", 261 | }, 262 | WithResourceSlug: { 263 | Id: WithResourceSlug.String(), 264 | Name: "With Resource (with_resource)", 265 | Desc: "Scoped to a specific resource.", 266 | }, 267 | } 268 | 269 | // Next determines the next step based on the current state. 270 | func (c *Controller) Next() tea.Model { 271 | // Handle case where role is not defined 272 | if c.State.role == nil { 273 | return NewRoleList(c) 274 | } 275 | 276 | // Handle case where action is not defined 277 | if c.State.operation == nil { 278 | return NewOperationList(c) 279 | } 280 | 281 | // Handle specific action: AttachCustomPolicySlug 282 | if c.State.operation.Id == AttachCustomPolicySlug.String() { 283 | 284 | if c.State.policy != nil { 285 | return NewResult(c) 286 | } 287 | 288 | // Handle case where a policy option is selected 289 | if c.State.policyOption != nil { 290 | switch c.State.policyOption.Id { 291 | case WithoutResourceSlug.String(): 292 | return NewCreatePolicy(c) 293 | 294 | case WithResourceSlug.String(): 295 | // Handle case where resource is defined 296 | if c.State.resource != nil { 297 | return NewCreatePolicy(c) 298 | } 299 | 300 | // Handle case where service is defined 301 | if c.State.service != nil { 302 | return NewResourceList(c) 303 | } 304 | // If service is not defined 305 | return NewServiceList(c) 306 | } 307 | } else { 308 | // Handle case where resource is defined 309 | if c.State.resource != nil { 310 | return NewCreatePolicy(c) 311 | } 312 | 313 | // Handle case where service is defined 314 | if c.State.service != nil { 315 | return NewResourceList(c) 316 | } 317 | // If no policy option is selected 318 | return NewPolicyOptionList(c) 319 | } 320 | } 321 | 322 | // Handle case where no policy is selected 323 | if c.State.policy == nil { 324 | return NewPolicyList(c) 325 | } 326 | 327 | // Default fallback 328 | return NewResult(c) 329 | } 330 | 331 | func (c *Controller) Done() error { 332 | switch c.State.operation.Id { 333 | case AttachPolicySlug.String(): 334 | return c.api.AttachPolicyToRole(context.Background(), c.State.GetPolicy().Arn, c.State.GetRole().Name) 335 | case DetachPolicySlug.String(): 336 | return c.api.DetachPolicyFromRole(context.Background(), c.State.GetPolicy().Arn, c.State.GetRole().Name) 337 | case AttachCustomPolicySlug.String(): 338 | output, err := c.api.CreatePolicy(context.Background(), c.State.GetPolicy().Name, c.State.GetPolicy().Document) 339 | if err != nil { 340 | return err 341 | } 342 | return c.api.AttachPolicyToRole(context.Background(), *output.Policy.Arn, c.State.GetRole().Name) 343 | default: 344 | return errors.New("operation not supported") 345 | } 346 | 347 | return nil 348 | } 349 | 350 | // Switch handles window size changes and updates the model accordingly. 351 | func Switch(model tea.Model, width, height int) (tea.Model, tea.Cmd) { 352 | // Always initialize the model 353 | initCmd := model.Init() 354 | 355 | // Handle window size updates 356 | if width == 0 && height == 0 { 357 | return model, initCmd 358 | } 359 | 360 | updateModel, updateCmd := model.Update(tea.WindowSizeMsg{ 361 | Width: width, 362 | Height: height, 363 | }) 364 | 365 | // Combine initialization and update commands 366 | return updateModel, tea.Batch(initCmd, updateCmd) 367 | } 368 | -------------------------------------------------------------------------------- /pkg/aws/roles/create_policy.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | "github.com/charmbracelet/huh" 9 | 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/charmbracelet/lipgloss" 12 | 13 | "github.com/Permify/targe/internal/ai" 14 | "github.com/Permify/targe/pkg/aws/models" 15 | ) 16 | 17 | type CreatePolicy struct { 18 | controller *Controller 19 | lg *lipgloss.Renderer 20 | styles *Styles 21 | form *huh.Form 22 | senderStyle lipgloss.Style 23 | err error 24 | width int 25 | message *string 26 | done *bool 27 | result string 28 | } 29 | 30 | func NewCreatePolicy(controller *Controller) CreatePolicy { 31 | m := CreatePolicy{controller: controller, width: maxWidth} 32 | m.lg = lipgloss.DefaultRenderer() 33 | m.styles = NewStyles(m.lg) 34 | 35 | doneInitialValue := false 36 | m.done = &doneInitialValue 37 | 38 | messageInitialValue := "" 39 | m.message = &messageInitialValue 40 | 41 | m.form = huh.NewForm( 42 | huh.NewGroup( 43 | huh.NewText().Key("message"). 44 | Title("Describe Your Policy"). 45 | Value(m.message), 46 | 47 | huh.NewConfirm(). 48 | Key("done"). 49 | Title("All done?"). 50 | Value(m.done). 51 | Affirmative("Yes"). 52 | Negative("Refresh"), 53 | ), 54 | ). 55 | WithWidth(45). 56 | WithShowHelp(false). 57 | WithShowErrors(false) 58 | 59 | return m 60 | } 61 | 62 | func (m CreatePolicy) Init() tea.Cmd { 63 | return m.form.Init() 64 | } 65 | 66 | func (m CreatePolicy) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 67 | switch msg := msg.(type) { 68 | case tea.WindowSizeMsg: 69 | m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize() 70 | 71 | case tea.KeyMsg: 72 | 73 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 74 | return m, tea.Quit 75 | } 76 | 77 | // Check if the "Refresh" or "Done" button was selected 78 | if msg.String() == "enter" { 79 | if m.done != nil && *m.done { 80 | return Switch(m.controller.Next(), 0, 0) 81 | } else { 82 | var resourceArn *string = nil 83 | if m.controller.State.GetResource() != nil { 84 | resourceArn = &m.controller.State.GetResource().Arn 85 | } 86 | 87 | var serviceName *string = nil 88 | if m.controller.State.GetService() != nil { 89 | serviceName = &m.controller.State.GetService().Name 90 | } 91 | 92 | if m.message == nil { 93 | m.err = errors.New("Please provide a message") 94 | } 95 | 96 | policy, err := ai.GeneratePolicy(m.controller.openAiApiKey, *m.message, serviceName, resourceArn) 97 | if err != nil { 98 | m.err = err 99 | } 100 | 101 | policyJson, err := json.MarshalIndent(policy, "", "\t") 102 | if err != nil { 103 | m.err = err 104 | } 105 | 106 | m.result = string(policyJson) 107 | 108 | m.controller.State.SetPolicy(&models.Policy{ 109 | Arn: "new", 110 | Name: policy.Id, 111 | Document: string(policyJson), 112 | }) 113 | 114 | m.reinitializeForm() 115 | } 116 | } 117 | } 118 | 119 | var cmds []tea.Cmd 120 | 121 | // Process the form 122 | form, cmd := m.form.Update(msg) 123 | if f, ok := form.(*huh.Form); ok { 124 | m.form = f 125 | cmds = append(cmds, cmd) 126 | } 127 | 128 | return m, tea.Batch(cmds...) 129 | } 130 | 131 | func (m CreatePolicy) View() string { 132 | s := m.styles 133 | 134 | v := strings.TrimSuffix(m.form.View(), "\n\n") 135 | form := m.lg.NewStyle().Margin(1, 0).Render(v) 136 | 137 | var titles []string 138 | var title string 139 | 140 | if m.controller.State.GetRole() != nil { 141 | titles = append(titles, 142 | s.StateHeader.Render("Role Name: "+m.controller.State.GetRole().Name), 143 | s.StateHeader.Render("Role ARN: "+m.controller.State.GetRole().Arn), 144 | ) 145 | } 146 | 147 | if m.controller.State.GetService() != nil && m.controller.State.GetResource() != nil { 148 | titles = append(titles, 149 | s.StateHeader.Render("Service Name: "+m.controller.State.GetService().Name), 150 | s.StateHeader.Render("Resource ARN: "+m.controller.State.GetResource().Arn), 151 | ) 152 | } 153 | 154 | if len(titles) > 0 { 155 | // Join the titles vertically 156 | title = lipgloss.JoinVertical(lipgloss.Left, titles...) 157 | 158 | // Apply margin-top to the entire title block 159 | title = lipgloss.NewStyle(). 160 | MarginTop(1). // Set the margin top to 2 lines 161 | Render(title) 162 | } 163 | 164 | // Status (right side) 165 | var status string 166 | { 167 | buildInfo := "(None)" 168 | 169 | if m.result != "" { 170 | buildInfo = m.result 171 | } 172 | 173 | const statusWidth = 60 174 | statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() 175 | status = s.Status. 176 | Height(lipgloss.Height(form)). 177 | Width(statusWidth). 178 | MarginLeft(statusMarginLeft). 179 | Render(s.StatusHeader.Render("Policy") + "\n" + 180 | buildInfo) 181 | } 182 | 183 | errors := m.form.Errors() 184 | header := lipgloss.JoinVertical(lipgloss.Top, 185 | m.appBoundaryView("Custom Policy Generator"), 186 | title, 187 | ) 188 | if len(errors) > 0 { 189 | header = m.appErrorBoundaryView(m.errorView()) 190 | } 191 | body := lipgloss.JoinHorizontal(lipgloss.Top, form, status) 192 | 193 | footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 194 | if len(errors) > 0 { 195 | footer = m.appErrorBoundaryView("") 196 | } 197 | 198 | return s.Base.Render(header + "\n" + body + "\n\n" + footer) 199 | } 200 | 201 | func (m CreatePolicy) errorView() string { 202 | var s string 203 | for _, err := range m.form.Errors() { 204 | s += err.Error() 205 | } 206 | return s 207 | } 208 | 209 | func (m CreatePolicy) appBoundaryView(text string) string { 210 | return lipgloss.PlaceHorizontal( 211 | m.width, 212 | lipgloss.Left, 213 | m.styles.HeaderText.Render(text), 214 | lipgloss.WithWhitespaceChars("/"), 215 | lipgloss.WithWhitespaceForeground(indigo), 216 | ) 217 | } 218 | 219 | func (m CreatePolicy) appErrorBoundaryView(text string) string { 220 | return lipgloss.PlaceHorizontal( 221 | m.width, 222 | lipgloss.Left, 223 | m.styles.ErrorHeaderText.Render(text), 224 | lipgloss.WithWhitespaceChars("/"), 225 | lipgloss.WithWhitespaceForeground(red), 226 | ) 227 | } 228 | 229 | func (m *CreatePolicy) reinitializeForm() { 230 | doneInitialValue := false 231 | m.done = &doneInitialValue 232 | 233 | // Preserve the current message value 234 | m.form = huh.NewForm( 235 | huh.NewGroup( 236 | huh.NewText(). 237 | Key("message"). 238 | Title("Describe Your Policy").Value(m.message), 239 | huh.NewConfirm(). 240 | Key("done"). 241 | Title("All done?"). 242 | Value(m.done). 243 | Affirmative("Yes"). 244 | Negative("Refresh"), 245 | ), 246 | ). 247 | WithWidth(45). 248 | WithShowHelp(false). 249 | WithShowErrors(false) 250 | } 251 | -------------------------------------------------------------------------------- /pkg/aws/roles/operation_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type OperationList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewOperationList(controller *Controller) OperationList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := OperationList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Operations" 32 | return view 33 | } 34 | 35 | func (m OperationList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadOperations()) 37 | } 38 | 39 | func (m OperationList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.Operation) 50 | m.controller.State.SetOperation(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case OperationLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m OperationList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/roles/policy_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyList(controller *Controller) PolicyList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Policies" 32 | return view 33 | } 34 | 35 | func (m PolicyList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicies()) 37 | } 38 | 39 | func (m PolicyList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | policy := m.list.SelectedItem().(models.Policy) 50 | m.controller.State.SetPolicy(&policy) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/roles/policy_option_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyOptionList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyOptionList(controller *Controller) PolicyOptionList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyOptionList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Options" 32 | return view 33 | } 34 | 35 | func (m PolicyOptionList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicyOptions()) 37 | } 38 | 39 | func (m PolicyOptionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.PolicyOption) 50 | m.controller.State.SetPolicyOption(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyOptionLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyOptionList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/roles/resource_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ResourceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewResourceList(controller *Controller) ResourceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ResourceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Resources" 32 | return view 33 | } 34 | 35 | func (m ResourceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadResources()) 37 | } 38 | 39 | func (m ResourceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | resource := m.list.SelectedItem().(models.Resource) 50 | m.controller.State.SetResource(&resource) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ResourceLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m ResourceList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/aws/roles/result.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/huh" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/lipgloss/table" 11 | 12 | "github.com/Permify/targe/pkg/aws/models" 13 | ) 14 | 15 | type Result struct { 16 | controller *Controller 17 | lg *lipgloss.Renderer 18 | styles *Styles 19 | form *huh.Form 20 | width int 21 | value *bool 22 | error error 23 | } 24 | 25 | func NewResult(controller *Controller) Result { 26 | // Initialize the Result with default values 27 | result := Result{ 28 | width: maxWidth, 29 | lg: lipgloss.DefaultRenderer(), 30 | controller: controller, 31 | } 32 | 33 | // Initialize styles 34 | result.styles = NewStyles(result.lg) 35 | 36 | // Initialize value pointer 37 | initialValue := false 38 | result.value = &initialValue 39 | 40 | // Configure the form 41 | result.form = createForm(result.value) 42 | 43 | return result 44 | } 45 | 46 | func (m Result) Init() tea.Cmd { 47 | return m.form.Init() 48 | } 49 | 50 | func min(x, y int) int { 51 | if x > y { 52 | return y 53 | } 54 | return x 55 | } 56 | 57 | func (m Result) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 58 | switch msg := msg.(type) { 59 | case tea.WindowSizeMsg: 60 | m.width = min(msg.Width, 80) - m.styles.Base.GetHorizontalFrameSize() 61 | case tea.KeyMsg: 62 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 63 | return m, tea.Quit 64 | } 65 | 66 | // Handle "Enter" for exit confirmation 67 | if m.form.State == huh.StateCompleted && msg.String() == "enter" { 68 | return m, tea.Quit 69 | } 70 | } 71 | 72 | var cmds []tea.Cmd 73 | 74 | // Process the form 75 | form, cmd := m.form.Update(msg) 76 | if f, ok := form.(*huh.Form); ok { 77 | m.form = f 78 | cmds = append(cmds, cmd) 79 | } 80 | 81 | // Handle form completion 82 | if m.form.State == huh.StateCompleted { 83 | if *m.value { 84 | if err := m.controller.Done(); err != nil { 85 | // Handle error without quitting 86 | m.error = err 87 | return m, nil // Return updated model without quitting 88 | } 89 | } else { 90 | cmds = append(cmds, tea.Quit) 91 | } 92 | } 93 | 94 | return m, tea.Batch(cmds...) 95 | } 96 | 97 | func (m Result) View() string { 98 | if m.form.State == huh.StateCompleted && m.error == nil { 99 | // Success Message with Exit Footer 100 | successMessage := fmt.Sprintf( 101 | "\n%s\n\n%s\n", 102 | lipgloss.NewStyle(). 103 | Bold(true). 104 | Foreground(lipgloss.Color("10")). 105 | Render("✔ Operation executed successfully!"), 106 | lipgloss.NewStyle(). 107 | Foreground(lipgloss.Color("7")). 108 | Italic(true). 109 | Render("The requested AWS IAM operation has been completed."), 110 | ) 111 | 112 | exitFooter := lipgloss.NewStyle(). 113 | Foreground(lipgloss.Color("8")). 114 | Italic(true). 115 | Render("Press Enter to exit.") 116 | 117 | return successMessage + "\n" + exitFooter 118 | } 119 | 120 | // When not in completed state, display other UI elements 121 | rows := m.collectOverviewRows() 122 | t := m.createTable(rows) 123 | formView := m.lg.NewStyle().Margin(1, 0).Render(strings.TrimSuffix(m.form.View(), "\n\n")) 124 | header := m.renderHeader() 125 | footer := m.renderFooter() 126 | 127 | body := lipgloss.JoinVertical(lipgloss.Top, t.Render(), formView) 128 | 129 | // Add error message if present 130 | if m.error != nil { 131 | errorView := fmt.Sprintf( 132 | "\n%s\n\n%s\n", 133 | lipgloss.NewStyle(). 134 | Bold(true). 135 | Foreground(lipgloss.Color("9")). 136 | Render("✖ An error occurred"), 137 | lipgloss.NewStyle(). 138 | Foreground(lipgloss.Color("1")). 139 | Italic(true). 140 | Render(m.error.Error()), 141 | ) 142 | body = lipgloss.JoinVertical(lipgloss.Top, body, errorView) 143 | } 144 | 145 | return m.styles.Base.Render(header + "\n" + body + "\n\n" + footer) 146 | } 147 | 148 | func (m Result) collectOverviewRows() [][]string { 149 | var rows [][]string 150 | state := m.controller.State 151 | 152 | if state.role != nil { 153 | rows = append(rows, []string{"Role", state.role.Name, state.role.Arn}) 154 | } 155 | if state.operation != nil { 156 | rows = append(rows, []string{"Operation", state.operation.Name, state.operation.Desc}) 157 | } 158 | if state.service != nil { 159 | rows = append(rows, []string{"Service", state.service.Name, state.service.Desc}) 160 | } 161 | if state.resource != nil { 162 | rows = append(rows, []string{"Resource", state.resource.Name, state.resource.Arn}) 163 | } 164 | if state.policy != nil { 165 | rows = append(rows, m.formatPolicyRow(state.policy)) 166 | } 167 | 168 | return rows 169 | } 170 | 171 | func (m Result) formatPolicyRow(policy *models.Policy) []string { 172 | if len(policy.Document) > 0 { 173 | return []string{"Policy", policy.Name, "new"} 174 | } 175 | return []string{"Policy", policy.Name, policy.Arn} 176 | } 177 | 178 | func (m Result) createTable(rows [][]string) *table.Table { 179 | return table.New(). 180 | Border(lipgloss.HiddenBorder()). 181 | BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). 182 | StyleFunc(func(row, col int) lipgloss.Style { 183 | if col == 0 { 184 | return m.styles.Base.Foreground(lipgloss.Color("205")).Bold(true) 185 | } 186 | return m.styles.Base 187 | }). 188 | Rows(rows...) 189 | } 190 | 191 | func (m Result) renderHeader() string { 192 | errors := m.form.Errors() 193 | if len(errors) > 0 { 194 | return m.appErrorBoundaryView(m.errorView()) 195 | } 196 | return m.appBoundaryView("Overview") 197 | } 198 | 199 | func (m Result) renderFooter() string { 200 | errors := m.form.Errors() 201 | if len(errors) > 0 { 202 | return m.appErrorBoundaryView("") 203 | } 204 | return m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 205 | } 206 | 207 | func (m Result) errorView() string { 208 | var s string 209 | for _, err := range m.form.Errors() { 210 | s += err.Error() + "\n" 211 | } 212 | return s 213 | } 214 | 215 | func (m Result) appBoundaryView(text string) string { 216 | return lipgloss.PlaceHorizontal( 217 | m.width, 218 | lipgloss.Left, 219 | m.styles.HeaderText.Render(text), 220 | lipgloss.WithWhitespaceChars("/"), 221 | lipgloss.WithWhitespaceForeground(indigo), 222 | ) 223 | } 224 | 225 | func (m Result) appErrorBoundaryView(text string) string { 226 | return lipgloss.PlaceHorizontal( 227 | m.width, 228 | lipgloss.Left, 229 | m.styles.ErrorHeaderText.Render(text), 230 | lipgloss.WithWhitespaceChars("/"), 231 | lipgloss.WithWhitespaceForeground(red), 232 | ) 233 | } 234 | 235 | func createForm(value *bool) *huh.Form { 236 | confirm := huh.NewConfirm(). 237 | Key("done"). 238 | Title("All done?"). 239 | Validate(func(v bool) error { 240 | return nil 241 | }). 242 | Affirmative("Yes"). 243 | Negative("No"). 244 | Value(value) 245 | 246 | return huh.NewForm( 247 | huh.NewGroup(confirm), 248 | ). 249 | WithWidth(45). 250 | WithShowHelp(false). 251 | WithShowErrors(false) 252 | } 253 | -------------------------------------------------------------------------------- /pkg/aws/roles/role_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type RoleList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewRoleList(controller *Controller) RoleList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := RoleList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Roles" 32 | return view 33 | } 34 | 35 | func (m RoleList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadRoles()) 37 | } 38 | 39 | func (m RoleList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | role := m.list.SelectedItem().(models.Role) 50 | m.controller.State.SetRole(&role) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case RoleLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m RoleList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/aws/roles/service_list.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ServiceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewServiceList(controller *Controller) ServiceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ServiceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Services" 32 | return view 33 | } 34 | 35 | func (m ServiceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadServices()) 37 | } 38 | 39 | func (m ServiceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | service := m.list.SelectedItem().(models.Service) 50 | m.controller.State.SetService(&service) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ServiceLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m ServiceList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/roles/state.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/Permify/targe/pkg/aws/models" 5 | ) 6 | 7 | // State represents the roles flow state. 8 | type State struct { 9 | role *models.Role 10 | operation *models.Operation 11 | policyOption *models.PolicyOption 12 | service *models.Service 13 | resource *models.Resource 14 | policy *models.Policy 15 | } 16 | 17 | // Getters 18 | 19 | // GetRole retrieves the role from the state. 20 | func (s *State) GetRole() *models.Role { 21 | return s.role 22 | } 23 | 24 | // GetOperation retrieves the operation from the state. 25 | func (s *State) GetOperation() *models.Operation { 26 | return s.operation 27 | } 28 | 29 | // GetPolicyOption retrieves the policy option from the state. 30 | func (s *State) GetPolicyOption() *models.PolicyOption { 31 | return s.policyOption 32 | } 33 | 34 | // GetService retrieves the service from the state. 35 | func (s *State) GetService() *models.Service { 36 | return s.service 37 | } 38 | 39 | // GetResource retrieves the resource from the state. 40 | func (s *State) GetResource() *models.Resource { 41 | return s.resource 42 | } 43 | 44 | // GetPolicy retrieves the policy from the state. 45 | func (s *State) GetPolicy() *models.Policy { 46 | return s.policy 47 | } 48 | 49 | // Setters 50 | 51 | // SetRole updates the role in the state. 52 | func (s *State) SetRole(role *models.Role) { 53 | s.role = role 54 | } 55 | 56 | // SetOperation updates the action in the state. 57 | func (s *State) SetOperation(operation *models.Operation) { 58 | s.operation = operation 59 | } 60 | 61 | // SetPolicyOption updates the policy option in the state. 62 | func (s *State) SetPolicyOption(policyOption *models.PolicyOption) { 63 | s.policyOption = policyOption 64 | } 65 | 66 | // SetService updates the service in the state. 67 | func (s *State) SetService(service *models.Service) { 68 | s.service = service 69 | } 70 | 71 | // SetResource updates the resource in the state. 72 | func (s *State) SetResource(resource *models.Resource) { 73 | s.resource = resource 74 | } 75 | 76 | // SetPolicy updates the policy in the state. 77 | func (s *State) SetPolicy(policy *models.Policy) { 78 | s.policy = policy 79 | } 80 | -------------------------------------------------------------------------------- /pkg/aws/roles/styles.go: -------------------------------------------------------------------------------- 1 | package roles 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var listStyle = lipgloss.NewStyle().Margin(1, 2) 8 | 9 | var spinnerStyle = lipgloss.NewStyle(). 10 | Foreground(lipgloss.Color("205")). 11 | Bold(true) 12 | 13 | const maxWidth = 100 14 | 15 | var ( 16 | red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} 17 | purple = lipgloss.Color("212") 18 | indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} 19 | green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} 20 | ) 21 | 22 | type Styles struct { 23 | Base, 24 | HeaderText, 25 | Status, 26 | StatusHeader, 27 | StateHeader, 28 | Highlight, 29 | ErrorHeaderText, 30 | Help lipgloss.Style 31 | } 32 | 33 | func NewStyles(lg *lipgloss.Renderer) *Styles { 34 | s := Styles{} 35 | s.Base = lg.NewStyle(). 36 | Padding(1, 4, 0, 1) 37 | s.HeaderText = lg.NewStyle(). 38 | Foreground(purple). 39 | Bold(true). 40 | Padding(0, 1, 0, 2) 41 | s.Status = lg.NewStyle(). 42 | Border(lipgloss.RoundedBorder()). 43 | BorderForeground(purple). 44 | PaddingLeft(1). 45 | MarginTop(1) 46 | s.StateHeader = lipgloss.NewStyle(). 47 | Bold(true). 48 | Foreground(green).MarginLeft(2).MarginTop(0).MarginLeft(2) 49 | s.StatusHeader = lg.NewStyle(). 50 | Foreground(green). 51 | Bold(true) 52 | s.Highlight = lg.NewStyle(). 53 | Foreground(lipgloss.Color("212")) 54 | s.ErrorHeaderText = s.HeaderText. 55 | Foreground(red) 56 | s.Help = lg.NewStyle(). 57 | Foreground(lipgloss.Color("240")) 58 | return &s 59 | } 60 | -------------------------------------------------------------------------------- /pkg/aws/users/create_policy.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strings" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/huh" 10 | "github.com/charmbracelet/lipgloss" 11 | 12 | "github.com/Permify/targe/internal/ai" 13 | "github.com/Permify/targe/pkg/aws/models" 14 | ) 15 | 16 | type CreatePolicy struct { 17 | controller *Controller 18 | lg *lipgloss.Renderer 19 | styles *Styles 20 | form *huh.Form 21 | senderStyle lipgloss.Style 22 | err error 23 | width int 24 | message *string 25 | done *bool 26 | result string 27 | } 28 | 29 | func NewCreatePolicy(controller *Controller) CreatePolicy { 30 | m := CreatePolicy{controller: controller, width: maxWidth} 31 | m.lg = lipgloss.DefaultRenderer() 32 | m.styles = NewStyles(m.lg) 33 | 34 | doneInitialValue := false 35 | m.done = &doneInitialValue 36 | 37 | messageInitialValue := "" 38 | m.message = &messageInitialValue 39 | 40 | m.form = huh.NewForm( 41 | huh.NewGroup( 42 | huh.NewText().Key("message"). 43 | Title("Describe Your Policy"). 44 | Value(m.message), 45 | 46 | huh.NewConfirm(). 47 | Key("done"). 48 | Title("All done?"). 49 | Value(m.done). 50 | Affirmative("Yes"). 51 | Negative("Refresh"), 52 | ), 53 | ). 54 | WithWidth(45). 55 | WithShowHelp(false). 56 | WithShowErrors(false) 57 | 58 | return m 59 | } 60 | 61 | func (m CreatePolicy) Init() tea.Cmd { 62 | return m.form.Init() 63 | } 64 | 65 | func (m CreatePolicy) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 66 | switch msg := msg.(type) { 67 | case tea.WindowSizeMsg: 68 | m.width = min(msg.Width, maxWidth) - m.styles.Base.GetHorizontalFrameSize() 69 | 70 | case tea.KeyMsg: 71 | 72 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 73 | return m, tea.Quit 74 | } 75 | 76 | // Check if the "Refresh" or "Done" button was selected 77 | if msg.String() == "enter" { 78 | if m.done != nil && *m.done { 79 | return Switch(m.controller.Next(), 0, 0) 80 | } else { 81 | var resourceArn *string = nil 82 | if m.controller.State.GetResource() != nil { 83 | resourceArn = &m.controller.State.GetResource().Arn 84 | } 85 | 86 | var serviceName *string = nil 87 | if m.controller.State.GetService() != nil { 88 | serviceName = &m.controller.State.GetService().Name 89 | } 90 | 91 | if m.message == nil { 92 | m.err = errors.New("Please provide a message") 93 | } 94 | 95 | policy, err := ai.GeneratePolicy(m.controller.openAiApiKey, *m.message, serviceName, resourceArn) 96 | 97 | policyJson, err := json.MarshalIndent(policy, "", "\t") 98 | if err != nil { 99 | m.err = err 100 | } 101 | 102 | m.result = string(policyJson) 103 | 104 | m.controller.State.SetPolicy(&models.Policy{ 105 | Arn: "new", 106 | Name: policy.Id, 107 | Document: string(policyJson), 108 | }) 109 | 110 | m.reinitializeForm() 111 | } 112 | } 113 | } 114 | 115 | var cmds []tea.Cmd 116 | 117 | // Process the form 118 | form, cmd := m.form.Update(msg) 119 | if f, ok := form.(*huh.Form); ok { 120 | m.form = f 121 | cmds = append(cmds, cmd) 122 | } 123 | 124 | return m, tea.Batch(cmds...) 125 | } 126 | 127 | func (m CreatePolicy) View() string { 128 | s := m.styles 129 | 130 | v := strings.TrimSuffix(m.form.View(), "\n\n") 131 | form := m.lg.NewStyle().Margin(1, 0).Render(v) 132 | 133 | var titles []string 134 | var title string 135 | 136 | if m.controller.State.GetUser() != nil { 137 | titles = append(titles, 138 | s.StateHeader.Render("User Name: "+m.controller.State.GetUser().Name), 139 | s.StateHeader.Render("User ARN: "+m.controller.State.GetUser().Arn), 140 | ) 141 | } 142 | 143 | if m.controller.State.GetService() != nil && m.controller.State.GetResource() != nil { 144 | titles = append(titles, 145 | s.StateHeader.Render("Service Name: "+m.controller.State.GetService().Name), 146 | s.StateHeader.Render("Resource ARN: "+m.controller.State.GetResource().Arn), 147 | ) 148 | } 149 | 150 | if len(titles) > 0 { 151 | // Join the titles vertically 152 | title = lipgloss.JoinVertical(lipgloss.Left, titles...) 153 | 154 | // Apply margin-top to the entire title block 155 | title = lipgloss.NewStyle(). 156 | MarginTop(1). // Set the margin top to 2 lines 157 | Render(title) 158 | } 159 | 160 | // Status (right side) 161 | var status string 162 | { 163 | buildInfo := "(None)" 164 | 165 | if m.result != "" { 166 | buildInfo = m.result 167 | } 168 | 169 | const statusWidth = 60 170 | statusMarginLeft := m.width - statusWidth - lipgloss.Width(form) - s.Status.GetMarginRight() 171 | status = s.Status. 172 | Height(lipgloss.Height(form)). 173 | Width(statusWidth). 174 | MarginLeft(statusMarginLeft). 175 | Render(s.StatusHeader.Render("Policy") + "\n" + 176 | buildInfo) 177 | } 178 | 179 | errors := m.form.Errors() 180 | header := lipgloss.JoinVertical(lipgloss.Top, 181 | m.appBoundaryView("Custom Policy Generator"), 182 | title, 183 | ) 184 | if len(errors) > 0 { 185 | header = m.appErrorBoundaryView(m.errorView()) 186 | } 187 | body := lipgloss.JoinHorizontal(lipgloss.Top, form, status) 188 | 189 | footer := m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 190 | if len(errors) > 0 { 191 | footer = m.appErrorBoundaryView("") 192 | } 193 | 194 | return s.Base.Render(header + "\n" + body + "\n\n" + footer) 195 | } 196 | 197 | func (m CreatePolicy) errorView() string { 198 | var s string 199 | for _, err := range m.form.Errors() { 200 | s += err.Error() 201 | } 202 | return s 203 | } 204 | 205 | func (m CreatePolicy) appBoundaryView(text string) string { 206 | return lipgloss.PlaceHorizontal( 207 | m.width, 208 | lipgloss.Left, 209 | m.styles.HeaderText.Render(text), 210 | lipgloss.WithWhitespaceChars("/"), 211 | lipgloss.WithWhitespaceForeground(indigo), 212 | ) 213 | } 214 | 215 | func (m CreatePolicy) appErrorBoundaryView(text string) string { 216 | return lipgloss.PlaceHorizontal( 217 | m.width, 218 | lipgloss.Left, 219 | m.styles.ErrorHeaderText.Render(text), 220 | lipgloss.WithWhitespaceChars("/"), 221 | lipgloss.WithWhitespaceForeground(red), 222 | ) 223 | } 224 | 225 | func (m *CreatePolicy) reinitializeForm() { 226 | doneInitialValue := false 227 | m.done = &doneInitialValue 228 | 229 | // Preserve the current message value 230 | m.form = huh.NewForm( 231 | huh.NewGroup( 232 | huh.NewText(). 233 | Key("message"). 234 | Title("Describe Your Policy").Value(m.message), 235 | huh.NewConfirm(). 236 | Key("done"). 237 | Title("All done?"). 238 | Value(m.done). 239 | Affirmative("Yes"). 240 | Negative("Refresh"), 241 | ), 242 | ). 243 | WithWidth(45). 244 | WithShowHelp(false). 245 | WithShowErrors(false) 246 | } 247 | -------------------------------------------------------------------------------- /pkg/aws/users/group_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type GroupList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewGroupList(controller *Controller) GroupList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := GroupList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Groups" 32 | return view 33 | } 34 | 35 | func (m GroupList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadGroups()) 37 | } 38 | 39 | func (m GroupList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | group := m.list.SelectedItem().(models.Group) 50 | m.controller.State.SetGroup(&group) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case GroupLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m GroupList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/users/operation_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type OperationList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewOperationList(controller *Controller) OperationList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := OperationList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Operations" 32 | return view 33 | } 34 | 35 | func (m OperationList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadOperations()) 37 | } 38 | 39 | func (m OperationList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.Operation) 50 | m.controller.State.SetOperation(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case OperationLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m OperationList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/users/policy_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyList(controller *Controller) PolicyList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Policies" 32 | return view 33 | } 34 | 35 | func (m PolicyList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicies()) 37 | } 38 | 39 | func (m PolicyList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | policy := m.list.SelectedItem().(models.Policy) 50 | m.controller.State.SetPolicy(&policy) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/users/policy_option_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type PolicyOptionList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewPolicyOptionList(controller *Controller) PolicyOptionList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := PolicyOptionList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Options" 32 | return view 33 | } 34 | 35 | func (m PolicyOptionList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadPolicyOptions()) 37 | } 38 | 39 | func (m PolicyOptionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | option := m.list.SelectedItem().(models.PolicyOption) 50 | m.controller.State.SetPolicyOption(&option) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case PolicyOptionLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m PolicyOptionList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/users/resource_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ResourceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewResourceList(controller *Controller) ResourceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ResourceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Resources" 32 | return view 33 | } 34 | 35 | func (m ResourceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadResources()) 37 | } 38 | 39 | func (m ResourceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | resource := m.list.SelectedItem().(models.Resource) 50 | m.controller.State.SetResource(&resource) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ResourceLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m ResourceList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/aws/users/result.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | "github.com/charmbracelet/huh" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/charmbracelet/lipgloss/table" 11 | 12 | "github.com/Permify/targe/pkg/aws/models" 13 | ) 14 | 15 | type Result struct { 16 | controller *Controller 17 | lg *lipgloss.Renderer 18 | styles *Styles 19 | form *huh.Form 20 | width int 21 | value *bool 22 | error error 23 | } 24 | 25 | func NewResult(controller *Controller) Result { 26 | // Initialize the Result with default values 27 | result := Result{ 28 | width: maxWidth, 29 | lg: lipgloss.DefaultRenderer(), 30 | controller: controller, 31 | } 32 | 33 | // Initialize styles 34 | result.styles = NewStyles(result.lg) 35 | 36 | // Initialize value pointer 37 | initialValue := false 38 | result.value = &initialValue 39 | 40 | // Configure the form 41 | result.form = createForm(result.value) 42 | 43 | return result 44 | } 45 | 46 | func (m Result) Init() tea.Cmd { 47 | return m.form.Init() 48 | } 49 | 50 | func min(x, y int) int { 51 | if x > y { 52 | return y 53 | } 54 | return x 55 | } 56 | 57 | func (m Result) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 58 | switch msg := msg.(type) { 59 | case tea.WindowSizeMsg: 60 | m.width = min(msg.Width, 80) - m.styles.Base.GetHorizontalFrameSize() 61 | case tea.KeyMsg: 62 | if msg.String() == "esc" || msg.String() == "ctrl+c" || msg.String() == "q" { 63 | return m, tea.Quit 64 | } 65 | 66 | // Handle "Enter" for exit confirmation 67 | if m.form.State == huh.StateCompleted && msg.String() == "enter" { 68 | return m, tea.Quit 69 | } 70 | } 71 | 72 | var cmds []tea.Cmd 73 | 74 | // Process the form 75 | form, cmd := m.form.Update(msg) 76 | if f, ok := form.(*huh.Form); ok { 77 | m.form = f 78 | cmds = append(cmds, cmd) 79 | } 80 | 81 | // Handle form completion 82 | if m.form.State == huh.StateCompleted { 83 | if *m.value { 84 | if err := m.controller.Done(); err != nil { 85 | // Handle error without quitting 86 | m.error = err 87 | return m, nil // Return updated model without quitting 88 | } 89 | } else { 90 | cmds = append(cmds, tea.Quit) 91 | } 92 | } 93 | 94 | return m, tea.Batch(cmds...) 95 | } 96 | 97 | func (m Result) View() string { 98 | if m.form.State == huh.StateCompleted && m.error == nil { 99 | // Success Message with Exit Footer 100 | successMessage := fmt.Sprintf( 101 | "\n%s\n\n%s\n", 102 | lipgloss.NewStyle(). 103 | Bold(true). 104 | Foreground(lipgloss.Color("10")). 105 | Render("✔ Operation executed successfully!"), 106 | lipgloss.NewStyle(). 107 | Foreground(lipgloss.Color("7")). 108 | Italic(true). 109 | Render("The requested AWS IAM operation has been completed."), 110 | ) 111 | 112 | exitFooter := lipgloss.NewStyle(). 113 | Foreground(lipgloss.Color("8")). 114 | Italic(true). 115 | Render("Press Enter to exit.") 116 | 117 | return successMessage + "\n" + exitFooter 118 | } 119 | 120 | // When not in completed state, display other UI elements 121 | rows := m.collectOverviewRows() 122 | t := m.createTable(rows) 123 | formView := m.lg.NewStyle().Margin(1, 0).Render(strings.TrimSuffix(m.form.View(), "\n\n")) 124 | header := m.renderHeader() 125 | footer := m.renderFooter() 126 | 127 | body := lipgloss.JoinVertical(lipgloss.Top, t.Render(), formView) 128 | 129 | // Add error message if present 130 | if m.error != nil { 131 | errorView := fmt.Sprintf( 132 | "\n%s\n\n%s\n", 133 | lipgloss.NewStyle(). 134 | Bold(true). 135 | Foreground(lipgloss.Color("9")). 136 | Render("✖ An error occurred"), 137 | lipgloss.NewStyle(). 138 | Foreground(lipgloss.Color("1")). 139 | Italic(true). 140 | Render(m.error.Error()), 141 | ) 142 | body = lipgloss.JoinVertical(lipgloss.Top, body, errorView) 143 | } 144 | 145 | return m.styles.Base.Render(header + "\n" + body + "\n\n" + footer) 146 | } 147 | 148 | func (m Result) collectOverviewRows() [][]string { 149 | var rows [][]string 150 | state := m.controller.State 151 | 152 | if state.user != nil { 153 | rows = append(rows, []string{"User", state.user.Name, state.user.Arn}) 154 | } 155 | if state.operation != nil { 156 | rows = append(rows, []string{"Operation", state.operation.Name, state.operation.Desc}) 157 | } 158 | if state.group != nil { 159 | rows = append(rows, []string{"Group", state.group.Name, state.group.Arn}) 160 | } 161 | if state.service != nil { 162 | rows = append(rows, []string{"Service", state.service.Name, state.service.Desc}) 163 | } 164 | if state.resource != nil { 165 | rows = append(rows, []string{"Resource", state.resource.Name, state.resource.Arn}) 166 | } 167 | if state.policy != nil { 168 | rows = append(rows, m.formatPolicyRow(state.policy)) 169 | } 170 | 171 | return rows 172 | } 173 | 174 | func (m Result) formatPolicyRow(policy *models.Policy) []string { 175 | if len(policy.Document) > 0 { 176 | return []string{"Policy", policy.Name, "new"} 177 | } 178 | return []string{"Policy", policy.Name, policy.Arn} 179 | } 180 | 181 | func (m Result) createTable(rows [][]string) *table.Table { 182 | return table.New(). 183 | Border(lipgloss.HiddenBorder()). 184 | BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color("99"))). 185 | StyleFunc(func(row, col int) lipgloss.Style { 186 | if col == 0 { 187 | return m.styles.Base.Foreground(lipgloss.Color("205")).Bold(true) 188 | } 189 | return m.styles.Base 190 | }). 191 | Rows(rows...) 192 | } 193 | 194 | func (m Result) renderHeader() string { 195 | errors := m.form.Errors() 196 | if len(errors) > 0 { 197 | return m.appErrorBoundaryView(m.errorView()) 198 | } 199 | return m.appBoundaryView("Overview") 200 | } 201 | 202 | func (m Result) renderFooter() string { 203 | errors := m.form.Errors() 204 | if len(errors) > 0 { 205 | return m.appErrorBoundaryView("") 206 | } 207 | return m.appBoundaryView(m.form.Help().ShortHelpView(m.form.KeyBinds())) 208 | } 209 | 210 | func (m Result) errorView() string { 211 | var s string 212 | for _, err := range m.form.Errors() { 213 | s += err.Error() + "\n" 214 | } 215 | return s 216 | } 217 | 218 | func (m Result) appBoundaryView(text string) string { 219 | return lipgloss.PlaceHorizontal( 220 | m.width, 221 | lipgloss.Left, 222 | m.styles.HeaderText.Render(text), 223 | lipgloss.WithWhitespaceChars("/"), 224 | lipgloss.WithWhitespaceForeground(indigo), 225 | ) 226 | } 227 | 228 | func (m Result) appErrorBoundaryView(text string) string { 229 | return lipgloss.PlaceHorizontal( 230 | m.width, 231 | lipgloss.Left, 232 | m.styles.ErrorHeaderText.Render(text), 233 | lipgloss.WithWhitespaceChars("/"), 234 | lipgloss.WithWhitespaceForeground(red), 235 | ) 236 | } 237 | 238 | func createForm(value *bool) *huh.Form { 239 | confirm := huh.NewConfirm(). 240 | Key("done"). 241 | Title("All done?"). 242 | Validate(func(v bool) error { 243 | return nil 244 | }). 245 | Affirmative("Yes"). 246 | Negative("No"). 247 | Value(value) 248 | 249 | return huh.NewForm( 250 | huh.NewGroup(confirm), 251 | ). 252 | WithWidth(45). 253 | WithShowHelp(false). 254 | WithShowErrors(false) 255 | } 256 | -------------------------------------------------------------------------------- /pkg/aws/users/service_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type ServiceList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewServiceList(controller *Controller) ServiceList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := ServiceList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Services" 32 | return view 33 | } 34 | 35 | func (m ServiceList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadServices()) 37 | } 38 | 39 | func (m ServiceList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | service := m.list.SelectedItem().(models.Service) 50 | m.controller.State.SetService(&service) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case ServiceLoadedMsg: 58 | m.loading = false 59 | m.list.SetItems(msg.List) 60 | case FailedMsg: 61 | // Handle error 62 | m.loading = false 63 | m.err = msg.Err 64 | } 65 | 66 | // Update spinner if loading 67 | if m.loading { 68 | m.spinner, cmd = m.spinner.Update(msg) 69 | return m, cmd 70 | } 71 | 72 | // Update list if not loading 73 | m.list, cmd = m.list.Update(msg) 74 | return m, cmd 75 | } 76 | 77 | func (m ServiceList) View() string { 78 | if m.err != nil { 79 | return listStyle.Render(m.err.Error()) 80 | } 81 | 82 | if m.loading { 83 | return listStyle.Render(m.spinner.View() + " Loading...") 84 | } 85 | 86 | return listStyle.Render(m.list.View()) 87 | } 88 | -------------------------------------------------------------------------------- /pkg/aws/users/state.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/Permify/targe/pkg/aws/models" 5 | ) 6 | 7 | type State struct { 8 | user *models.User 9 | operation *models.Operation 10 | group *models.Group 11 | policyOption *models.PolicyOption 12 | service *models.Service 13 | resource *models.Resource 14 | policy *models.Policy 15 | } 16 | 17 | // Getters 18 | 19 | // GetUser retrieves the user from the state. 20 | func (s *State) GetUser() *models.User { 21 | return s.user 22 | } 23 | 24 | // GetOperation retrieves the operation from the state. 25 | func (s *State) GetOperation() *models.Operation { 26 | return s.operation 27 | } 28 | 29 | // GetGroup retrieves the group from the state. 30 | func (s *State) GetGroup() *models.Group { 31 | return s.group 32 | } 33 | 34 | // GetPolicyOption retrieves the policy option from the state. 35 | func (s *State) GetPolicyOption() *models.PolicyOption { 36 | return s.policyOption 37 | } 38 | 39 | // GetService retrieves the service from the state. 40 | func (s *State) GetService() *models.Service { 41 | return s.service 42 | } 43 | 44 | // GetResource retrieves the resource from the state. 45 | func (s *State) GetResource() *models.Resource { 46 | return s.resource 47 | } 48 | 49 | // GetPolicy retrieves the policy from the state. 50 | func (s *State) GetPolicy() *models.Policy { 51 | return s.policy 52 | } 53 | 54 | // Setters 55 | 56 | // SetUser updates the user in the state. 57 | func (s *State) SetUser(user *models.User) { 58 | s.user = user 59 | } 60 | 61 | // SetOperation updates the action in the state. 62 | func (s *State) SetOperation(operation *models.Operation) { 63 | s.operation = operation 64 | } 65 | 66 | // SetGroup updates the group in the state. 67 | func (s *State) SetGroup(group *models.Group) { 68 | s.group = group 69 | } 70 | 71 | // SetPolicyOption updates the policy option in the state. 72 | func (s *State) SetPolicyOption(policyOption *models.PolicyOption) { 73 | s.policyOption = policyOption 74 | } 75 | 76 | // SetService updates the service in the state. 77 | func (s *State) SetService(service *models.Service) { 78 | s.service = service 79 | } 80 | 81 | // SetResource updates the resource in the state. 82 | func (s *State) SetResource(resource *models.Resource) { 83 | s.resource = resource 84 | } 85 | 86 | // SetPolicy updates the policy in the state. 87 | func (s *State) SetPolicy(policy *models.Policy) { 88 | s.policy = policy 89 | } 90 | -------------------------------------------------------------------------------- /pkg/aws/users/styles.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/lipgloss" 5 | ) 6 | 7 | var listStyle = lipgloss.NewStyle().Margin(1, 2) 8 | 9 | var spinnerStyle = lipgloss.NewStyle(). 10 | Foreground(lipgloss.Color("205")). 11 | Bold(true) 12 | 13 | const maxWidth = 100 14 | 15 | var ( 16 | red = lipgloss.AdaptiveColor{Light: "#FE5F86", Dark: "#FE5F86"} 17 | purple = lipgloss.Color("212") 18 | indigo = lipgloss.AdaptiveColor{Light: "#5A56E0", Dark: "#7571F9"} 19 | green = lipgloss.AdaptiveColor{Light: "#02BA84", Dark: "#02BF87"} 20 | ) 21 | 22 | type Styles struct { 23 | Base, 24 | HeaderText, 25 | Status, 26 | StatusHeader, 27 | StateHeader, 28 | Highlight, 29 | ErrorHeaderText, 30 | Help lipgloss.Style 31 | } 32 | 33 | func NewStyles(lg *lipgloss.Renderer) *Styles { 34 | s := Styles{} 35 | s.Base = lg.NewStyle(). 36 | Padding(1, 4, 0, 1) 37 | s.HeaderText = lg.NewStyle(). 38 | Foreground(purple). 39 | Bold(true). 40 | Padding(0, 1, 0, 2) 41 | s.Status = lg.NewStyle(). 42 | Border(lipgloss.RoundedBorder()). 43 | BorderForeground(purple). 44 | PaddingLeft(1). 45 | MarginTop(1) 46 | s.StateHeader = lipgloss.NewStyle(). 47 | Bold(true). 48 | Foreground(green).MarginLeft(2).MarginTop(0).MarginLeft(2) 49 | s.StatusHeader = lg.NewStyle(). 50 | Foreground(green). 51 | Bold(true) 52 | s.Highlight = lg.NewStyle(). 53 | Foreground(lipgloss.Color("212")) 54 | s.ErrorHeaderText = s.HeaderText. 55 | Foreground(red) 56 | s.Help = lg.NewStyle(). 57 | Foreground(lipgloss.Color("240")) 58 | return &s 59 | } 60 | -------------------------------------------------------------------------------- /pkg/aws/users/user_list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/charmbracelet/bubbles/list" 5 | "github.com/charmbracelet/bubbles/spinner" 6 | tea "github.com/charmbracelet/bubbletea" 7 | 8 | "github.com/Permify/targe/pkg/aws/models" 9 | ) 10 | 11 | type UserList struct { 12 | controller *Controller 13 | spinner spinner.Model 14 | loading bool 15 | list list.Model 16 | err error 17 | } 18 | 19 | func NewUserList(controller *Controller) UserList { 20 | sp := spinner.New() 21 | sp.Style = spinnerStyle 22 | sp.Spinner = spinner.Pulse 23 | 24 | view := UserList{ 25 | controller: controller, 26 | spinner: sp, 27 | loading: true, 28 | } 29 | 30 | view.list = list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0) 31 | view.list.Title = "Users" 32 | return view 33 | } 34 | 35 | func (m UserList) Init() tea.Cmd { 36 | return tea.Batch(m.spinner.Tick, m.controller.LoadUsers()) 37 | } 38 | 39 | func (m UserList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 40 | var cmd tea.Cmd 41 | 42 | switch msg := msg.(type) { 43 | case tea.KeyMsg: 44 | switch msg.String() { 45 | case "ctrl+c": 46 | return m, tea.Quit 47 | case "enter": 48 | if !m.loading { 49 | user := m.list.SelectedItem().(models.User) 50 | m.controller.State.SetUser(&user) 51 | return Switch(m.controller.Next(), m.list.Width(), m.list.Height()) 52 | } 53 | } 54 | case tea.WindowSizeMsg: 55 | h, v := listStyle.GetFrameSize() 56 | m.list.SetSize(msg.Width-h, msg.Height-v) 57 | case UserLoadedMsg: 58 | // Update list with loaded users 59 | m.loading = false 60 | m.list.SetItems(msg.List) 61 | case FailedMsg: 62 | // Handle error 63 | m.loading = false 64 | m.err = msg.Err 65 | } 66 | 67 | // Update spinner if loading 68 | if m.loading { 69 | m.spinner, cmd = m.spinner.Update(msg) 70 | return m, cmd 71 | } 72 | 73 | // Update list if not loading 74 | m.list, cmd = m.list.Update(msg) 75 | return m, cmd 76 | } 77 | 78 | func (m UserList) View() string { 79 | if m.err != nil { 80 | return listStyle.Render(m.err.Error()) 81 | } 82 | 83 | if m.loading { 84 | return listStyle.Render(m.spinner.View() + " Loading...") 85 | } 86 | 87 | return listStyle.Render(m.list.View()) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/cmd/aws/aws.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/spf13/cobra" 5 | 6 | "github.com/Permify/targe/internal/config" 7 | ) 8 | 9 | // NewAwsCommand - 10 | func NewAwsCommand(cfg *config.Config) *cobra.Command { 11 | command := &cobra.Command{ 12 | Use: "aws", 13 | Short: "", 14 | Long: ``, 15 | } 16 | 17 | command.AddCommand(NewUsersCommand(cfg)) 18 | command.AddCommand(NewRolesCommand(cfg)) 19 | command.AddCommand(NewGroupsCommand(cfg)) 20 | 21 | return command 22 | } 23 | -------------------------------------------------------------------------------- /pkg/cmd/aws/flags.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func RegisterUsersFlags(flags *pflag.FlagSet) { 9 | var err error 10 | if err = viper.BindPFlag("user", flags.Lookup("user")); err != nil { 11 | panic(err) 12 | } 13 | if err = viper.BindPFlag("operation", flags.Lookup("operation")); err != nil { 14 | panic(err) 15 | } 16 | if err = viper.BindPFlag("group", flags.Lookup("group")); err != nil { 17 | panic(err) 18 | } 19 | if err = viper.BindPFlag("policy", flags.Lookup("policy")); err != nil { 20 | panic(err) 21 | } 22 | if err = viper.BindPFlag("resource", flags.Lookup("resource")); err != nil { 23 | panic(err) 24 | } 25 | if err = viper.BindPFlag("service", flags.Lookup("service")); err != nil { 26 | panic(err) 27 | } 28 | if err = viper.BindPFlag("policy_option", flags.Lookup("policy-option")); err != nil { 29 | panic(err) 30 | } 31 | } 32 | 33 | func RegisterRolesFlags(flags *pflag.FlagSet) { 34 | var err error 35 | if err = viper.BindPFlag("role", flags.Lookup("role")); err != nil { 36 | panic(err) 37 | } 38 | if err = viper.BindPFlag("operation", flags.Lookup("operation")); err != nil { 39 | panic(err) 40 | } 41 | if err = viper.BindPFlag("policy", flags.Lookup("policy")); err != nil { 42 | panic(err) 43 | } 44 | if err = viper.BindPFlag("resource", flags.Lookup("resource")); err != nil { 45 | panic(err) 46 | } 47 | if err = viper.BindPFlag("service", flags.Lookup("service")); err != nil { 48 | panic(err) 49 | } 50 | if err = viper.BindPFlag("policy_option", flags.Lookup("policy-option")); err != nil { 51 | panic(err) 52 | } 53 | } 54 | 55 | func RegisterGroupsFlags(flags *pflag.FlagSet) { 56 | var err error 57 | if err = viper.BindPFlag("group", flags.Lookup("group")); err != nil { 58 | panic(err) 59 | } 60 | if err = viper.BindPFlag("operation", flags.Lookup("operation")); err != nil { 61 | panic(err) 62 | } 63 | if err = viper.BindPFlag("policy", flags.Lookup("policy")); err != nil { 64 | panic(err) 65 | } 66 | if err = viper.BindPFlag("resource", flags.Lookup("resource")); err != nil { 67 | panic(err) 68 | } 69 | if err = viper.BindPFlag("service", flags.Lookup("service")); err != nil { 70 | panic(err) 71 | } 72 | if err = viper.BindPFlag("policy_option", flags.Lookup("policy-option")); err != nil { 73 | panic(err) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/cmd/aws/groups.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 9 | 10 | tea "github.com/charmbracelet/bubbletea" 11 | "github.com/spf13/cobra" 12 | "github.com/spf13/viper" 13 | 14 | internalaws "github.com/Permify/targe/internal/aws" 15 | "github.com/Permify/targe/internal/config" 16 | pkggroups "github.com/Permify/targe/pkg/aws/groups" 17 | "github.com/Permify/targe/pkg/aws/models" 18 | ) 19 | 20 | type Groups struct { 21 | model tea.Model 22 | } 23 | 24 | func (m Groups) Init() tea.Cmd { 25 | return m.model.Init() // rest methods are just wrappers for the model's methods 26 | } 27 | 28 | func (m Groups) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 29 | return m.model.Update(msg) 30 | } 31 | 32 | func (m Groups) View() string { 33 | return m.model.View() 34 | } 35 | 36 | // NewGroupsCommand - 37 | func NewGroupsCommand(cfg *config.Config) *cobra.Command { 38 | command := &cobra.Command{ 39 | Use: "groups", 40 | Short: "", 41 | RunE: groups(cfg), 42 | } 43 | 44 | f := command.Flags() 45 | 46 | f.String("group", "", "group") 47 | f.String("operation", "", "operation") 48 | f.String("policy", "", "policy") 49 | f.String("resource", "", "resource") 50 | f.String("service", "", "service") 51 | f.String("policy-option", "", "policy option") 52 | 53 | // SilenceUsage is set to true to suppress usage when an error occurs 54 | command.SilenceUsage = true 55 | 56 | command.PreRun = func(cmd *cobra.Command, args []string) { 57 | RegisterGroupsFlags(f) 58 | } 59 | 60 | return command 61 | } 62 | 63 | func groups(cfg *config.Config) func(cmd *cobra.Command, args []string) error { 64 | return func(cmd *cobra.Command, args []string) error { 65 | // get min coverage from viper 66 | group := viper.GetString("group") 67 | operation := viper.GetString("operation") 68 | policy := viper.GetString("policy") 69 | resource := viper.GetString("resource") 70 | service := viper.GetString("service") 71 | policyOption := viper.GetString("policy-option") 72 | 73 | // Load the AWS configuration 74 | awscfg, err := awsconfig.LoadDefaultConfig(context.Background()) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | api := internalaws.NewApi(awscfg) 80 | state := &pkggroups.State{} 81 | 82 | if group != "" { 83 | awsgroup, err := api.FindGroup(context.Background(), group) 84 | if err != nil { 85 | return err 86 | } 87 | 88 | state.SetGroup(&models.Group{ 89 | Name: *awsgroup.Group.GroupName, 90 | Arn: *awsgroup.Group.Arn, 91 | }) 92 | } 93 | 94 | if operation != "" { 95 | // Check if the operation exists in the ReachableOperations map 96 | op, exists := pkggroups.ReachableOperations[pkggroups.OperationType(operation)] 97 | if !exists { 98 | return fmt.Errorf("Operation '%s' does not exist in ReachableOperations\n", operation) 99 | } 100 | 101 | state.SetOperation(&op) 102 | } 103 | 104 | if policy != "" { 105 | awspolicy, err := api.FindPolicy(context.Background(), policy) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | state.SetPolicy(&models.Policy{ 111 | Name: *awspolicy.Policy.PolicyName, 112 | Arn: *awspolicy.Policy.Arn, 113 | }) 114 | } 115 | 116 | if resource != "" { 117 | resourceName := parseResourceNameFromArn(resource) 118 | state.SetResource(&models.Resource{ 119 | Name: resourceName, 120 | Arn: resource, 121 | }) 122 | } 123 | 124 | if service != "" { 125 | state.SetService(&models.Service{ 126 | Name: service, 127 | }) 128 | } 129 | 130 | if policyOption != "" { 131 | // Check if the operation exists in the ReachableOperations map 132 | op, exists := pkggroups.ReachablePolicyOptions[pkggroups.PolicyOptionType(policyOption)] 133 | if !exists { 134 | return fmt.Errorf("Policy options '%s' does not exist in ReachableCustomPolicyOptions\n", policyOption) 135 | } 136 | 137 | state.SetPolicyOption(&op) 138 | } 139 | 140 | controller := pkggroups.NewController(api, cfg.OpenaiApiKey, state) 141 | 142 | p := tea.NewProgram(RootModel(controller.Next()), tea.WithAltScreen()) 143 | if _, err := p.Run(); err != nil { 144 | fmt.Println("Error running program:", err) 145 | os.Exit(1) 146 | } 147 | 148 | return nil 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /pkg/cmd/aws/roles.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 10 | 11 | tea "github.com/charmbracelet/bubbletea" 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | 15 | internalaws "github.com/Permify/targe/internal/aws" 16 | "github.com/Permify/targe/internal/config" 17 | "github.com/Permify/targe/pkg/aws/models" 18 | pkgroles "github.com/Permify/targe/pkg/aws/roles" 19 | ) 20 | 21 | type Roles struct { 22 | model tea.Model 23 | } 24 | 25 | func (m Roles) Init() tea.Cmd { 26 | return m.model.Init() // rest methods are just wrappers for the model's methods 27 | } 28 | 29 | func (m Roles) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 30 | return m.model.Update(msg) 31 | } 32 | 33 | func (m Roles) View() string { 34 | return m.model.View() 35 | } 36 | 37 | // NewRolesCommand - 38 | func NewRolesCommand(cfg *config.Config) *cobra.Command { 39 | command := &cobra.Command{ 40 | Use: "roles", 41 | Short: "", 42 | RunE: roles(cfg), 43 | } 44 | 45 | f := command.Flags() 46 | 47 | f.String("role", "", "role") 48 | f.String("operation", "", "operation") 49 | f.String("policy", "", "policy") 50 | f.String("resource", "", "resource") 51 | f.String("service", "", "service") 52 | f.String("policy-option", "", "policy option") 53 | 54 | // SilenceUsage is set to true to suppress usage when an error occurs 55 | command.SilenceUsage = true 56 | 57 | command.PreRun = func(cmd *cobra.Command, args []string) { 58 | RegisterRolesFlags(f) 59 | } 60 | 61 | return command 62 | } 63 | 64 | func roles(cfg *config.Config) func(cmd *cobra.Command, args []string) error { 65 | return func(cmd *cobra.Command, args []string) error { 66 | // get min coverage from viper 67 | role := viper.GetString("role") 68 | operation := viper.GetString("operation") 69 | policy := viper.GetString("policy") 70 | resource := viper.GetString("resource") 71 | service := viper.GetString("service") 72 | policyOption := viper.GetString("policy-option") 73 | 74 | // Load the AWS configuration 75 | awscfg, err := awsconfig.LoadDefaultConfig(context.Background()) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | api := internalaws.NewApi(awscfg) 81 | state := &pkgroles.State{} 82 | 83 | if role != "" { 84 | awsrole, err := api.FindRole(context.Background(), role) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | state.SetRole(&models.Role{ 90 | Name: aws.ToString(awsrole.Role.RoleName), 91 | Arn: aws.ToString(awsrole.Role.Arn), 92 | }) 93 | } 94 | 95 | if operation != "" { 96 | // Check if the operation exists in the ReachableOperations map 97 | op, exists := pkgroles.ReachableOperations[pkgroles.OperationType(operation)] 98 | if !exists { 99 | return fmt.Errorf("Operation '%s' does not exist in ReachableOperations\n", operation) 100 | } 101 | 102 | state.SetOperation(&op) 103 | } 104 | 105 | if policy != "" { 106 | awspolicy, err := api.FindPolicy(context.Background(), policy) 107 | if err != nil { 108 | return err 109 | } 110 | 111 | state.SetPolicy(&models.Policy{ 112 | Name: aws.ToString(awspolicy.Policy.PolicyName), 113 | Arn: aws.ToString(awspolicy.Policy.Arn), 114 | }) 115 | } 116 | 117 | if service != "" { 118 | state.SetService(&models.Service{ 119 | Name: service, 120 | }) 121 | } 122 | 123 | if resource != "" { 124 | resourceName := parseResourceNameFromArn(resource) 125 | state.SetResource(&models.Resource{ 126 | Name: resourceName, 127 | Arn: resource, 128 | }) 129 | } 130 | 131 | if policyOption != "" { 132 | // Check if the operation exists in the ReachableOperations map 133 | op, exists := pkgroles.ReachablePolicyOptions[pkgroles.PolicyOptionType(policyOption)] 134 | if !exists { 135 | return fmt.Errorf("Policy options '%s' does not exist in ReachableCustomPolicyOptions\n", policyOption) 136 | } 137 | 138 | state.SetPolicyOption(&op) 139 | } 140 | 141 | controller := pkgroles.NewController(api, cfg.OpenaiApiKey, state) 142 | 143 | p := tea.NewProgram(RootModel(controller.Next()), tea.WithAltScreen()) 144 | if _, err := p.Run(); err != nil { 145 | fmt.Println("Error running program:", err) 146 | os.Exit(1) 147 | } 148 | 149 | return nil 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /pkg/cmd/aws/users.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 9 | 10 | "github.com/spf13/viper" 11 | 12 | tea "github.com/charmbracelet/bubbletea" 13 | "github.com/spf13/cobra" 14 | 15 | "github.com/Permify/targe/pkg/aws/models" 16 | 17 | internalaws "github.com/Permify/targe/internal/aws" 18 | "github.com/Permify/targe/internal/config" 19 | pkgusers "github.com/Permify/targe/pkg/aws/users" 20 | "github.com/Permify/targe/pkg/cmd/common" 21 | ) 22 | 23 | type Users struct { 24 | model tea.Model 25 | } 26 | 27 | func (m Users) Init() tea.Cmd { 28 | return m.model.Init() // rest methods are just wrappers for the model's methods 29 | } 30 | 31 | func (m Users) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | return m.model.Update(msg) 33 | } 34 | 35 | func (m Users) View() string { 36 | return m.model.View() 37 | } 38 | 39 | // NewUsersCommand - 40 | func NewUsersCommand(cfg *config.Config) *cobra.Command { 41 | command := &cobra.Command{ 42 | Use: "users", 43 | Short: "", 44 | RunE: users(cfg), 45 | } 46 | 47 | f := command.Flags() 48 | 49 | f.String("user", "", "user") 50 | f.String("operation", "", "operation") 51 | f.String("group", "", "group") 52 | f.String("policy", "", "policy") 53 | f.String("resource", "", "resource") 54 | f.String("service", "", "service") 55 | f.String("policy-option", "", "policy option") 56 | 57 | // SilenceUsage is set to true to suppress usage when an error occurs 58 | command.SilenceUsage = true 59 | 60 | command.PreRun = func(cmd *cobra.Command, args []string) { 61 | RegisterUsersFlags(f) 62 | } 63 | 64 | return command 65 | } 66 | 67 | func users(cfg *config.Config) func(cmd *cobra.Command, args []string) error { 68 | return func(cmd *cobra.Command, args []string) error { 69 | // Replace "requirements" with the actual path to your folder 70 | requirementsPath := "requirements" 71 | 72 | // Check if the requirements folder exists 73 | if !folderExists(requirementsPath) { 74 | if _, err := tea.NewProgram(common.NewRequirements()).Run(); err != nil { 75 | fmt.Println("Error running program:", err) 76 | os.Exit(1) 77 | } 78 | } 79 | 80 | user := viper.GetString("user") 81 | operation := viper.GetString("operation") 82 | group := viper.GetString("group") 83 | policy := viper.GetString("policy") 84 | resource := viper.GetString("resource") 85 | service := viper.GetString("service") 86 | policyOption := viper.GetString("policy-option") 87 | 88 | // Load the AWS configuration 89 | awscfg, err := awsconfig.LoadDefaultConfig(context.Background()) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | api := internalaws.NewApi(awscfg) 95 | state := &pkgusers.State{} 96 | 97 | if user != "" { 98 | awsuser, err := api.FindUser(context.Background(), user) 99 | if err != nil { 100 | return err 101 | } 102 | state.SetUser(&models.User{ 103 | Name: *awsuser.User.UserName, 104 | Arn: *awsuser.User.Arn, 105 | }) 106 | } 107 | 108 | if operation != "" { 109 | // Check if the operation exists in the ReachableOperations map 110 | op, exists := pkgusers.ReachableOperations[pkgusers.OperationType(operation)] 111 | if !exists { 112 | return fmt.Errorf("Operation '%s' does not exist in ReachableOperations\n", operation) 113 | } 114 | 115 | state.SetOperation(&op) 116 | } 117 | 118 | if policy != "" { 119 | awspolicy, err := api.FindPolicy(context.Background(), policy) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | state.SetPolicy(&models.Policy{ 125 | Name: *awspolicy.Policy.PolicyName, 126 | Arn: *awspolicy.Policy.Arn, 127 | }) 128 | } 129 | 130 | if group != "" { 131 | awsgroup, err := api.FindGroup(context.Background(), group) 132 | if err != nil { 133 | return err 134 | } 135 | 136 | state.SetGroup(&models.Group{ 137 | Name: *awsgroup.Group.GroupName, 138 | Arn: *awsgroup.Group.Arn, 139 | }) 140 | } 141 | 142 | if service != "" { 143 | state.SetService(&models.Service{ 144 | Name: service, 145 | }) 146 | } 147 | 148 | if resource != "" { 149 | resourceName := parseResourceNameFromArn(resource) 150 | state.SetResource(&models.Resource{ 151 | Name: resourceName, 152 | Arn: resource, 153 | }) 154 | } 155 | 156 | if policyOption != "" { 157 | op, exists := pkgusers.ReachablePolicyOptions[pkgusers.PolicyOptionType(policyOption)] 158 | if !exists { 159 | return fmt.Errorf("Policy options '%s' does not exist in ReachableCustomPolicyOptions\n", policyOption) 160 | } 161 | 162 | state.SetPolicyOption(&op) 163 | } 164 | 165 | controller := pkgusers.NewController(api, cfg.OpenaiApiKey, state) 166 | 167 | p := tea.NewProgram(RootModel(controller.Next()), tea.WithAltScreen()) 168 | if _, err := p.Run(); err != nil { 169 | fmt.Println("Error running program:", err) 170 | os.Exit(1) 171 | } 172 | 173 | return nil 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /pkg/cmd/aws/utils.go: -------------------------------------------------------------------------------- 1 | package aws 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | 7 | tea "github.com/charmbracelet/bubbletea" 8 | ) 9 | 10 | func RootModel(m tea.Model) Users { 11 | return Users{ 12 | model: m, 13 | } 14 | } 15 | 16 | func folderExists(folderPath string) bool { 17 | info, err := os.Stat(folderPath) 18 | if os.IsNotExist(err) { 19 | return false 20 | } 21 | return info.IsDir() 22 | } 23 | 24 | // Helper function to extract the resource name from the ARN 25 | func parseResourceNameFromArn(arn string) string { 26 | parts := strings.Split(arn, "/") 27 | if len(parts) > 1 { 28 | return parts[len(parts)-1] // Return the last part of the ARN 29 | } 30 | return arn 31 | } 32 | -------------------------------------------------------------------------------- /pkg/cmd/common/requirements.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/charmbracelet/bubbles/progress" 8 | "github.com/charmbracelet/bubbles/spinner" 9 | tea "github.com/charmbracelet/bubbletea" 10 | "github.com/charmbracelet/lipgloss" 11 | 12 | "github.com/Permify/targe/internal/requirements" 13 | ) 14 | 15 | type RequirementsManager struct { 16 | requirements []requirements.Requirement 17 | index int 18 | width int 19 | height int 20 | spinner spinner.Model 21 | progress progress.Model 22 | done bool 23 | } 24 | 25 | var ( 26 | currentPkgNameStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("211")) 27 | doneStyle = lipgloss.NewStyle().Margin(1, 2) 28 | checkMark = lipgloss.NewStyle().Foreground(lipgloss.Color("42")).SetString("✓") 29 | ) 30 | 31 | func NewRequirements() RequirementsManager { 32 | p := progress.New( 33 | progress.WithDefaultGradient(), 34 | progress.WithWidth(40), 35 | progress.WithoutPercentage(), 36 | ) 37 | s := spinner.New() 38 | s.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("63")) 39 | return RequirementsManager{ 40 | requirements: requirements.GetRequirements(), 41 | spinner: s, 42 | progress: p, 43 | } 44 | } 45 | 46 | func (m RequirementsManager) Init() tea.Cmd { 47 | return tea.Batch(install(m.requirements[m.index]), m.spinner.Tick) 48 | } 49 | 50 | func (m RequirementsManager) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 51 | switch msg := msg.(type) { 52 | case tea.WindowSizeMsg: 53 | m.width, m.height = msg.Width, msg.Height 54 | case tea.KeyMsg: 55 | switch msg.String() { 56 | case "ctrl+c", "esc", "q": 57 | return m, tea.Quit 58 | } 59 | case installedPkgMsg: 60 | pkg := m.requirements[m.index] 61 | if m.index >= len(m.requirements)-1 { 62 | // Everything's been installed. We're done! 63 | m.done = true 64 | return m, tea.Sequence( 65 | tea.Printf("%s %s", checkMark, pkg.GetName()), // print the last success message 66 | tea.Quit, // exit the program 67 | ) 68 | } 69 | 70 | // Update progress bar 71 | m.index++ 72 | progressCmd := m.progress.SetPercent(float64(m.index) / float64(len(m.requirements))) 73 | 74 | return m, tea.Batch( 75 | progressCmd, 76 | tea.Printf("%s %s", checkMark, pkg), // print success message above our program 77 | install(m.requirements[m.index]), // download the next package 78 | ) 79 | case installErrorMsg: 80 | // Update state for errors 81 | tea.Printf("Error installing package: %v", msg.Err) 82 | return m, nil 83 | case spinner.TickMsg: 84 | var cmd tea.Cmd 85 | m.spinner, cmd = m.spinner.Update(msg) 86 | return m, cmd 87 | case progress.FrameMsg: 88 | newModel, cmd := m.progress.Update(msg) 89 | if newModel, ok := newModel.(progress.Model); ok { 90 | m.progress = newModel 91 | } 92 | return m, cmd 93 | } 94 | return m, nil 95 | } 96 | 97 | func (m RequirementsManager) View() string { 98 | n := len(m.requirements) 99 | w := lipgloss.Width(fmt.Sprintf("%d", n)) 100 | 101 | if m.done { 102 | return doneStyle.Render(fmt.Sprintf("Done! Installed %d requirements.\n", n)) 103 | } 104 | 105 | pkgCount := fmt.Sprintf(" %*d/%*d", w, m.index, w, n) 106 | 107 | spin := m.spinner.View() + " " 108 | prog := m.progress.View() 109 | cellsAvail := max(0, m.width-lipgloss.Width(spin+prog+pkgCount)) 110 | 111 | pkgName := currentPkgNameStyle.Render(m.requirements[m.index].GetName()) 112 | info := lipgloss.NewStyle().MaxWidth(cellsAvail).Render("Installing " + pkgName) 113 | 114 | cellsRemaining := max(0, m.width-lipgloss.Width(spin+info+prog+pkgCount)) 115 | gap := strings.Repeat(" ", cellsRemaining) 116 | 117 | return spin + info + gap + prog + pkgCount 118 | } 119 | 120 | // Message types 121 | type installedPkgMsg struct { 122 | Name string 123 | } 124 | 125 | type installErrorMsg struct { 126 | Err error 127 | } 128 | 129 | // downloadAndInstall asynchronously downloads and installs a requirement 130 | func install(requirement requirements.Requirement) tea.Cmd { 131 | return func() tea.Msg { 132 | // Simulate installation 133 | err := requirement.Install() 134 | if err != nil { 135 | return installErrorMsg{Err: err} 136 | } 137 | 138 | // Return a success message 139 | return installedPkgMsg{Name: requirement.GetName()} 140 | } 141 | } 142 | 143 | func max(a, b int) int { 144 | if a > b { 145 | return a 146 | } 147 | return b 148 | } 149 | -------------------------------------------------------------------------------- /pkg/cmd/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/spf13/cobra" 10 | "github.com/spf13/viper" 11 | 12 | "github.com/Permify/targe/internal/config" 13 | ) 14 | 15 | // NewConfigCommand - returns a new cobra command for config 16 | func NewConfigCommand() *cobra.Command { 17 | command := &cobra.Command{ 18 | Use: "config", 19 | Short: "Manage targe configuration", 20 | } 21 | 22 | // Add subcommands 23 | command.AddCommand(newConfigSetCommand()) 24 | command.AddCommand(newConfigGetCommand()) 25 | 26 | return command 27 | } 28 | 29 | // newConfigSetCommand - returns a cobra command for setting config 30 | func newConfigSetCommand() *cobra.Command { 31 | return &cobra.Command{ 32 | Use: "set [key] [value]", 33 | Short: "Set a configuration key-value pair", 34 | Args: cobra.ExactArgs(2), // Requires exactly 2 arguments: key and value 35 | RunE: func(cmd *cobra.Command, args []string) error { 36 | key := args[0] 37 | value := args[1] 38 | 39 | if key != "openai_api_key" { 40 | return fmt.Errorf("invalid key: %s", key) 41 | } 42 | 43 | // Start with the default configuration values 44 | cfg := config.DefaultConfig() 45 | 46 | // Set the name and type of the config file to be read 47 | viper.SetConfigName("config") 48 | viper.SetConfigType("toml") 49 | 50 | // Add the path where the config file is located 51 | configPath := os.ExpandEnv("$HOME/.targe/") 52 | viper.AddConfigPath(configPath) 53 | 54 | // Ensure the directory exists 55 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 56 | if err := os.MkdirAll(configPath, 0o755); err != nil { 57 | return fmt.Errorf("failed to create config directory: %w", err) 58 | } 59 | } 60 | 61 | // Update the key-value pair 62 | cfg.OpenaiApiKey = value 63 | 64 | // Read the config file 65 | err := viper.ReadInConfig() 66 | if err != nil { 67 | // If the error is due to the file not being found, create a new one 68 | var configFileNotFoundError viper.ConfigFileNotFoundError 69 | if errors.As(err, &configFileNotFoundError) { 70 | filePath := filepath.Join(configPath, "config.toml") 71 | if err := writeConfig(filePath, cfg); err != nil { 72 | return fmt.Errorf("failed to create config file: %w", err) 73 | } 74 | } 75 | } 76 | 77 | filePath := filepath.Join(configPath, "config.toml") 78 | if err := writeConfig(filePath, cfg); err != nil { 79 | return fmt.Errorf("failed to create config file: %w", err) 80 | } 81 | 82 | fmt.Printf("Configuration set: %s=%s\n", key, value) 83 | return nil 84 | }, 85 | } 86 | } 87 | 88 | // newConfigGetCommand - returns a cobra command for getting config values 89 | func newConfigGetCommand() *cobra.Command { 90 | return &cobra.Command{ 91 | Use: "get [key]", 92 | Short: "Get a configuration value by key", 93 | Args: cobra.ExactArgs(1), // Requires exactly 1 argument: key 94 | RunE: func(cmd *cobra.Command, args []string) error { 95 | key := args[0] 96 | 97 | if key != "openai_api_key" { 98 | return fmt.Errorf("invalid key: %s", key) 99 | } 100 | 101 | // Start with the default configuration values 102 | cfg := config.DefaultConfig() 103 | 104 | // Set the name and type of the config file to be read 105 | viper.SetConfigName("config") 106 | viper.SetConfigType("toml") 107 | 108 | // Add the path where the config file is located 109 | configPath := os.ExpandEnv("$HOME/.targe/") 110 | viper.AddConfigPath(configPath) 111 | 112 | // Ensure the directory exists 113 | if _, err := os.Stat(configPath); os.IsNotExist(err) { 114 | if err := os.MkdirAll(configPath, 0o755); err != nil { 115 | return fmt.Errorf("failed to create config directory: %w", err) 116 | } 117 | } 118 | 119 | // Read the config file 120 | err := viper.ReadInConfig() 121 | if err != nil { 122 | // If the error is due to the file not being found, create a new one 123 | var configFileNotFoundError viper.ConfigFileNotFoundError 124 | if errors.As(err, &configFileNotFoundError) { 125 | filePath := filepath.Join(configPath, "config.toml") 126 | if err := writeConfig(filePath, cfg); err != nil { 127 | return fmt.Errorf("failed to create config file: %w", err) 128 | } 129 | } 130 | } 131 | 132 | // Unmarshal the configuration data into the Config struct 133 | if err = viper.Unmarshal(cfg); err != nil { 134 | // If there's an error during unmarshalling, return the error with a message 135 | return fmt.Errorf("failed to unmarshal server config: %w", err) 136 | } 137 | 138 | fmt.Printf("%s=%s\n", key, cfg.OpenaiApiKey) 139 | return nil 140 | }, 141 | } 142 | } 143 | 144 | func writeConfig(filePath string, cfg *config.Config) error { 145 | // Use viper to write the default configuration to a file 146 | viper.Set("openai_api_key", cfg.OpenaiApiKey) 147 | file, err := os.Create(filePath) 148 | if err != nil { 149 | return err 150 | } 151 | defer file.Close() 152 | 153 | if err := viper.WriteConfigAs(filePath); err != nil { 154 | return fmt.Errorf("failed to write default config: %w", err) 155 | } 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /pkg/cmd/flags.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/spf13/pflag" 5 | "github.com/spf13/viper" 6 | ) 7 | 8 | func RegisterRootFlags(flags *pflag.FlagSet) { 9 | var err error 10 | if err = viper.BindPFlag("m", flags.Lookup("m")); err != nil { 11 | panic(err) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /pkg/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | tea "github.com/charmbracelet/bubbletea" 9 | "github.com/charmbracelet/lipgloss" 10 | "github.com/spf13/viper" 11 | 12 | "github.com/spf13/cobra" 13 | 14 | "github.com/Permify/targe/internal/ai" 15 | configc "github.com/Permify/targe/pkg/cmd/config" 16 | 17 | "github.com/Permify/targe/internal/config" 18 | "github.com/Permify/targe/pkg/cmd/aws" 19 | ) 20 | 21 | type RootModel struct { 22 | command string 23 | choice string 24 | quitting bool 25 | } 26 | 27 | func (m RootModel) Init() tea.Cmd { 28 | return nil 29 | } 30 | 31 | func (m RootModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 32 | switch msg := msg.(type) { 33 | case tea.KeyMsg: 34 | switch msg.String() { 35 | case "y", "Y", "", tea.KeyEnter.String(): 36 | return RootModel{command: m.command, choice: "yes", quitting: true}, tea.Quit 37 | case "n", "N": 38 | return RootModel{command: m.command, choice: "no", quitting: true}, tea.Quit 39 | case tea.KeyCtrlC.String(), tea.KeyEsc.String(): 40 | return m, tea.Quit 41 | } 42 | } 43 | return m, nil 44 | } 45 | 46 | func (m RootModel) View() string { 47 | if m.quitting { 48 | if m.choice == "yes" { 49 | return "" 50 | } 51 | return fmt.Sprintf( 52 | "%s\n\n", 53 | lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("9")).Render("✘ Command aborted."), 54 | ) 55 | } 56 | 57 | // Define styles 58 | brandStyle := lipgloss.NewStyle(). 59 | Foreground(lipgloss.Color("7")). 60 | Padding(0, 0) 61 | 62 | headerStyle := lipgloss.NewStyle(). 63 | Foreground(lipgloss.Color("6")). 64 | Underline(true) 65 | 66 | messageStyle := lipgloss.NewStyle(). 67 | Foreground(lipgloss.Color("2")). 68 | Italic(true). 69 | PaddingLeft(2) 70 | 71 | promptStyle := lipgloss.NewStyle(). 72 | Foreground(lipgloss.Color("3")). 73 | PaddingTop(1) 74 | 75 | // Render sections 76 | brand := brandStyle.Render("Generating your command...") 77 | header := headerStyle.Render("Here’s your command:") 78 | 79 | // Format the command 80 | formattedCommand := formatCommand(m.command, 2) 81 | message := messageStyle.Render(fmt.Sprintf("➤ targe %s", formattedCommand)) 82 | prompt := promptStyle.Render("Would you like to use this command? (Y/n):") 83 | 84 | // Combine output 85 | return fmt.Sprintf("%s\n\n%s\n\n%s\n%s", brand, header, message, prompt) 86 | } 87 | 88 | // NewRootCommand - Creates new root command 89 | func NewRootCommand() *cobra.Command { 90 | // Load the configuration 91 | cfg, err := config.NewConfig() 92 | if err != nil { 93 | fmt.Println("Failed to load configuration:", err) 94 | os.Exit(1) 95 | } 96 | 97 | root := &cobra.Command{ 98 | Use: "targe", 99 | Short: "", 100 | Long: ``, 101 | RunE: r(cfg), 102 | } 103 | 104 | f := root.Flags() 105 | 106 | f.String("m", "", "message") 107 | 108 | // SilenceUsage is set to true to suppress usage when an error occurs 109 | root.SilenceUsage = true 110 | 111 | root.PreRun = func(cmd *cobra.Command, args []string) { 112 | RegisterRootFlags(f) 113 | } 114 | 115 | configCommand := configc.NewConfigCommand() 116 | awsCommand := aws.NewAwsCommand(cfg) 117 | 118 | root.AddCommand(awsCommand, configCommand) 119 | 120 | return root 121 | } 122 | 123 | func r(cfg *config.Config) func(cmd *cobra.Command, args []string) error { 124 | return func(cmd *cobra.Command, args []string) error { 125 | message := viper.GetString("m") 126 | 127 | gptResponse, err := ai.UserPrompt(cfg.OpenaiApiKey, message) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | command := ai.GenerateCLICommand(gptResponse) 133 | 134 | // Bubble Tea program setup 135 | program := tea.NewProgram(&RootModel{command: command}) 136 | mod, err := program.Run() 137 | if err != nil { 138 | return fmt.Errorf("program encountered an error: %w", err) 139 | } 140 | 141 | // Check user choice 142 | if result, ok := mod.(RootModel); ok && result.choice == "yes" { 143 | var args []string 144 | args = append(args, strings.Split(command, " ")...) 145 | cmd.SetArgs(args) 146 | return cmd.Root().Execute() 147 | } 148 | 149 | return nil 150 | } 151 | } 152 | 153 | // Helper function to format a command 154 | func formatCommand(command string, maxWordsPerLine int) string { 155 | parts := strings.Fields(command) // Split command into words 156 | var result []string 157 | var line []string 158 | 159 | for _, part := range parts { 160 | line = append(line, part) 161 | if len(line) >= maxWordsPerLine { 162 | result = append(result, strings.Join(line, " ")+" \\") 163 | line = []string{} 164 | } 165 | } 166 | 167 | // Add the remaining words 168 | if len(line) > 0 { 169 | result = append(result, strings.Join(line, " ")) 170 | } 171 | 172 | return strings.Join(result, "\n ") 173 | } 174 | -------------------------------------------------------------------------------- /requirements/policies.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "AWSLambdaVPCAccessExecutionRole-a3caa621-31a1-4452-8af0-b83697fa951d", 4 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaVPCAccessExecutionRole-a3caa621-31a1-4452-8af0-b83697fa951d" 5 | }, 6 | { 7 | "name": "KarpenterControllerPolicy-permify-karpenter-ohio", 8 | "arn": "arn:aws:iam::107288898794:policy/KarpenterControllerPolicy-permify-karpenter-ohio" 9 | }, 10 | { 11 | "name": "AWSLambdaBasicExecutionRole-9fceaa3f-d0a9-4e0f-ac0a-e92304e03ebd", 12 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-9fceaa3f-d0a9-4e0f-ac0a-e92304e03ebd" 13 | }, 14 | { 15 | "name": "AWSLambdaBasicExecutionRole-4e492876-5b6c-4cf9-af76-9a03c69322e3", 16 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-4e492876-5b6c-4cf9-af76-9a03c69322e3" 17 | }, 18 | { 19 | "name": "AWSLoadBalancerControllerIAMPolicy", 20 | "arn": "arn:aws:iam::107288898794:policy/AWSLoadBalancerControllerIAMPolicy" 21 | }, 22 | { 23 | "name": "AWSLambdaVPCAccessExecutionRole-6b111e5b-af13-4674-b3fb-fc7a8e872e52", 24 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaVPCAccessExecutionRole-6b111e5b-af13-4674-b3fb-fc7a8e872e52" 25 | }, 26 | { 27 | "name": "rds-proxy-policy-1714770444306", 28 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714770444306" 29 | }, 30 | { 31 | "name": "AWSLambdaVPCAccessExecutionRole-51a3724d-423b-4995-8486-8659ea785368", 32 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaVPCAccessExecutionRole-51a3724d-423b-4995-8486-8659ea785368" 33 | }, 34 | { 35 | "name": "EKSFullManager", 36 | "arn": "arn:aws:iam::107288898794:policy/EKSFullManager" 37 | }, 38 | { 39 | "name": "AWSLambdaBasicExecutionRole-0a4d4885-f8cf-491c-8a91-80c20835e3a2", 40 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-0a4d4885-f8cf-491c-8a91-80c20835e3a2" 41 | }, 42 | { 43 | "name": "AWSLambdaBasicExecutionRole-671512d2-d370-4675-828a-65f444d4958a", 44 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-671512d2-d370-4675-828a-65f444d4958a" 45 | }, 46 | { 47 | "name": "rds-proxy-policy-1714780672173", 48 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714780672173" 49 | }, 50 | { 51 | "name": "KarpenterControllerPolicy-cloud", 52 | "arn": "arn:aws:iam::107288898794:policy/KarpenterControllerPolicy-cloud" 53 | }, 54 | { 55 | "name": "rds-proxy-policy-1714739686273", 56 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714739686273" 57 | }, 58 | { 59 | "name": "rds-proxy-policy-1726507887904", 60 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1726507887904" 61 | }, 62 | { 63 | "name": "playground-access-policy", 64 | "arn": "arn:aws:iam::107288898794:policy/playground-access-policy" 65 | }, 66 | { 67 | "name": "rds-proxy-policy-1714645466490", 68 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714645466490" 69 | }, 70 | { 71 | "name": "AWSLambdaBasicExecutionRole-825c5f94-9c81-423c-b5a1-dc52a7409019", 72 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-825c5f94-9c81-423c-b5a1-dc52a7409019" 73 | }, 74 | { 75 | "name": "AWSLambdaBasicExecutionRole-88a65331-cd14-4561-a68e-8ac2e81bbb56", 76 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-88a65331-cd14-4561-a68e-8ac2e81bbb56" 77 | }, 78 | { 79 | "name": "rds-proxy-policy-1713858779135", 80 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1713858779135" 81 | }, 82 | { 83 | "name": "Cloudtrail-CW-access-policy-all-region-events-7976965f-d2f6-4cf0-9762-a3abb63e0f2d", 84 | "arn": "arn:aws:iam::107288898794:policy/service-role/Cloudtrail-CW-access-policy-all-region-events-7976965f-d2f6-4cf0-9762-a3abb63e0f2d" 85 | }, 86 | { 87 | "name": "rds-proxy-policy-1726504301442", 88 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1726504301442" 89 | }, 90 | { 91 | "name": "KarpenterControllerPolicy-permify-karpenter", 92 | "arn": "arn:aws:iam::107288898794:policy/KarpenterControllerPolicy-permify-karpenter" 93 | }, 94 | { 95 | "name": "CastEKSPolicy", 96 | "arn": "arn:aws:iam::107288898794:policy/CastEKSPolicy" 97 | }, 98 | { 99 | "name": "AWSLambdaBasicExecutionRole-d8ed2b6a-27d3-4ee8-bb6f-65929a70349a", 100 | "arn": "arn:aws:iam::107288898794:policy/service-role/AWSLambdaBasicExecutionRole-d8ed2b6a-27d3-4ee8-bb6f-65929a70349a" 101 | }, 102 | { 103 | "name": "rds-proxy-policy-1716399922162", 104 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1716399922162" 105 | }, 106 | { 107 | "name": "AmazonEKS_EFS_CSI_Driver_Policy", 108 | "arn": "arn:aws:iam::107288898794:policy/AmazonEKS_EFS_CSI_Driver_Policy" 109 | }, 110 | { 111 | "name": "rds-proxy-policy-1714764375474", 112 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714764375474" 113 | }, 114 | { 115 | "name": "KarpenterControllerPolicy-permify-cluster-karpenter", 116 | "arn": "arn:aws:iam::107288898794:policy/KarpenterControllerPolicy-permify-cluster-karpenter" 117 | }, 118 | { 119 | "name": "rds-proxy-policy-1726507530925", 120 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1726507530925" 121 | }, 122 | { 123 | "name": "rds-proxy-policy-1714751580124", 124 | "arn": "arn:aws:iam::107288898794:policy/service-role/rds-proxy-policy-1714751580124" 125 | } 126 | ] 127 | --------------------------------------------------------------------------------