Compare commits

...

105 commits
v0.1.9 ... main

Author SHA1 Message Date
Calliope
43cc96a43d
Display Prompts on STDERR (#70)
Some checks failed
Go / build (push) Has been cancelled
Prompts should be on STDERR

https://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_05_03 (under PS1)
2025-03-04 17:25:56 +01:00
b1a82e06d6
Update go-passbolt and other deps (#71) 2025-03-04 17:15:48 +01:00
Nelson Isioma
1183813dcb
Feature: adding support for exposing secrets to subprocesses (#68)
Some checks failed
Go / build (push) Has been cancelled
* adding support for exposing secrets to subprocesses

* updating readme

* wip 3

* wip 4

* wip 56
2025-02-23 15:56:48 +01:00
6033d6bbb3
Merge pull request #64 from Tecnobutrul/allow-http-client-configuration
Some checks failed
Go / build (push) Has been cancelled
Added support for http client configuration via command arguments
2024-12-20 14:46:46 +01:00
Daniel Del Rio Figueira
78ed21f62b
Added flags for passing client cert and client private key as file contents instead of paths 2024-12-04 00:39:11 +01:00
Daniel Del Rio Figueira
0273cee2ba
Added File suffix on tlsClient command flags 2024-12-02 09:27:02 +01:00
Daniel Del Rio Figueira
72cfd79b77
Removed hardcoded paths and fixed typo 2024-12-02 09:26:25 +01:00
Daniel Del Rio Figueira
d5e2df49db
Added support for http client configuration via command arguments 2024-11-29 10:16:33 +01:00
d9703ff6fd
Merge pull request #63 from froque/add_get_resource_permission
Some checks failed
Go / build (push) Has been cancelled
Adds permission subcommand to get a list of permissions of a resource
2024-10-07 15:56:35 +02:00
Filipe Roque
9b2d47fecd Adds permission subcommand to get a list of permissions of a resource 2024-09-30 17:40:15 +01:00
c0dd780ca3
Merge pull request #61 from passbolt/update-readme-install
Some checks failed
Go / build (push) Has been cancelled
Update README.md Install Section
2024-08-13 12:18:24 +02:00
1fcbafa5b1
Merge pull request #62 from passbolt/update-deps
Update deps, fix import changes, update go-passbolt for mfa fix
2024-08-13 12:17:30 +02:00
9a9d06987a Update deps, fix import changes, update go-passbolt for mfa fix 2024-08-13 12:11:41 +02:00
647a063586
Update README.md Install Section 2024-08-13 10:57:41 +02:00
c454e733cf
Merge pull request #49 from kskarthik/main
rename go-passbolt-cli -> passbolt in README
2024-04-08 14:39:27 +02:00
Sai Karthik
a74c5e0550
rename go-passbolt-cli -> passbolt in README
Closes #29
2024-04-06 23:38:15 +05:30
b11702da24
Merge pull request #48 from passbolt/fix-version
Use goreleaser version if available, otherwise use buildinfo
2024-04-05 22:05:08 +02:00
1e09efcc5d fix Man and Completion CI 2024-04-05 21:49:53 +02:00
6cb52e23ca go mod tidy 2024-04-05 21:46:29 +02:00
b48f3274e6 Use goreleaser version if available, otherwise buildinfo 2024-04-05 21:45:57 +02:00
b269e8538d
Merge pull request #47 from passbolt/update-ci
CI Improvements
2024-04-05 21:42:09 +02:00
13da79adf6 update Deprecated field 2024-04-05 21:04:55 +02:00
effdbdb316 fix auto snapshot 2024-04-05 20:58:14 +02:00
3cbabb905a Add dist Artifact Uploading 2024-04-05 20:49:12 +02:00
eeaa987bb6 Add Run Version Step 2024-04-05 20:48:52 +02:00
c5e1dcbedc Update CI Action Versions 2024-04-05 20:48:24 +02:00
774fc8b54b
Merge pull request #44 from passbolt/version-flag
Implement Version flag
2024-03-26 11:50:04 +01:00
4a870ea137 Set Version info in rootcmd 2024-03-26 11:43:10 +01:00
4975d53b4d add versioninfo, update deps 2024-03-26 11:42:25 +01:00
b3e608e6c6
Merge pull request #43 from passbolt/basic-totp-support
Basic totp support, Export Improvements
2024-03-26 11:40:18 +01:00
e0469a14b0 Use old TOTP flags if the new ones are unset 2024-03-26 11:10:08 +01:00
1229ad7b59 Implement Custom TOTP URL Generation 2024-03-21 23:39:03 +01:00
869f33cfa3 remove otp library 2024-03-21 23:37:51 +01:00
6f4d6e92b7 Add More Algorithms 2024-03-21 23:12:30 +01:00
7da6aec339 Fix Incorrect Secret Parsing, Typos 2024-03-21 23:10:02 +01:00
c595eca741 export: skip resource on error 2024-03-21 22:42:36 +01:00
a535a832c2 Add TOTP Export Support 2024-03-21 22:34:54 +01:00
fafdd22d73 update deps 2024-03-21 19:45:32 +01:00
54d424390b update ci go version 2024-03-21 19:39:27 +01:00
52bdc35875 Update Readme about new Flags 2024-03-21 18:52:24 +01:00
c2506ed71a Add New Flags, Mark old ones as deprecated 2024-03-21 18:51:32 +01:00
2e1a46f5ce update deps 2024-03-21 18:49:59 +01:00
444fa17005
Fix Doc Link 2023-12-19 14:14:34 +01:00
abb8895df3
Merge pull request #39 from passbolt/speatzle-homebrew-readme
Add Hombrew Installation to README.md
2023-08-21 13:58:02 +02:00
4a15fe34ad
Add Hombrew Installation to README.md 2023-08-21 13:55:56 +02:00
c006252c66
Merge pull request #38 from passbolt/fix-goreleaser-homebrew-man
fix goreleaser homebrew man copy
2023-08-11 14:35:17 +02:00
2fd123692b
fix goreleaser man copy 2023-08-11 14:34:29 +02:00
61df03f10a
Merge pull request #37 from passbolt/fix-goreleaser-hombrew-completions
fix hombrew completions
2023-08-11 14:19:01 +02:00
a9ce43584a
fix hombrew completions 2023-08-11 14:18:27 +02:00
92a05e899e
Merge pull request #36 from passbolt/fix-goreleaser-homebrew
Fix goreleaser homebrew
2023-08-11 13:27:35 +02:00
d4c5ab93b8
Use --clean instead of --rm-dist 2023-08-11 13:26:02 +02:00
7883419856
Set Hombrew Folder 2023-08-11 13:25:26 +02:00
fb1e4e97fe
Merge pull request #35 from passbolt/goreleaser-homebrew
Homebrew Releases
2023-08-11 13:11:58 +02:00
12cb8bb643
Add TAP Github Token Secret 2023-08-11 09:54:46 +02:00
22c8648884
Add homebrew to .goreleaser.yml 2023-08-11 09:53:21 +02:00
82542147f2
Merge pull request #34 from passbolt/fix-goreleaser_deprecation
goreleaser remove/update old options
2023-08-10 21:20:29 +02:00
3b9406f0f0 remove/update old options 2023-08-10 21:12:35 +02:00
acd18bb0f7
Merge pull request #33 from passbolt/update-deps
update deps
2023-08-10 21:09:03 +02:00
1cd6ab33ac update deps 2023-08-10 21:00:18 +02:00
488bf7043a
Merge pull request #30 from lenforiee/fix-spelling-mistakes
Fix spelling mistakes in the code
2023-04-20 13:49:15 +02:00
lenforiee
ad51d8134f update the go-passbolt to v0.6.0 2023-04-19 23:49:03 +02:00
lenforiee
6169450ab5 challange -> challenge 2023-04-19 21:34:15 +02:00
Samuel Lorch
7fe89a34b3
Merge pull request #26 from PiMaDaum/feature/CEL-filter-implementation
feature/CEL filter implementation
2023-02-12 20:55:58 +01:00
PiMaDaum
0a542bfdba Initialize the CEL program in a util function 2023-02-05 22:19:54 +01:00
PiMaDaum
f30590588e Implement CEL filter in list user command 2023-02-05 21:57:01 +01:00
PiMaDaum
6562aeab95 Implement CEL filter in list group command 2023-02-05 21:42:32 +01:00
PiMaDaum
20e3a72c92 Add comment to filterFolders function 2023-02-05 21:30:48 +01:00
PiMaDaum
7db321f130 Implement CEL filter in list folder command 2023-02-05 21:27:40 +01:00
PiMaDaum
5ee20771a8 Define filter argument as persistent flag in list
command
2023-02-05 19:38:58 +01:00
PiMaDaum
4e5983dc43 Remove the overloaded parseTimestamp function 2023-02-01 21:16:28 +01:00
PiMaDaum
e5bb6eceff Outsource the resources filter function
to seperate file
2023-02-01 21:11:06 +01:00
PiMaDaum
9bdc4a96ca create filter flag description 2023-01-29 21:36:45 +01:00
PiMaDaum
3d395a11ed Implement an builtin function to parse timestamps
for filter expressions
2023-01-29 21:06:44 +01:00
PiMaDaum
48604aefd0 Make slice of cel environment options private 2023-01-29 20:14:21 +01:00
PiMaDaum
5a464f48da Implement filtering over CEL on list resources 2023-01-29 01:18:55 +01:00
PiMaDaum
a0b7b7daaf Set deendency of antlr/antlr4/runtime/Go/antlr 2023-01-29 01:18:07 +01:00
PiMaDaum
e9582729ad add google/cel-go as new dependency 2023-01-28 21:04:54 +01:00
Samuel Lorch
0aa392634b
Merge pull request #22 from passbolt/fix-ci
fix ci version
2022-12-30 17:59:39 +01:00
Samuel Lorch
34fedec4f2 fix ci version 2022-12-30 17:55:13 +01:00
Samuel Lorch
6d4879e8b4
Merge pull request #21 from passbolt/update-deps
update deps
2022-12-30 17:20:55 +01:00
Samuel Lorch
6f59446349 update deps 2022-12-30 17:20:02 +01:00
Samuel Lorch
9a2944c6fb
Merge pull request #20 from passbolt/feature-json-output
Json Output Support
2022-12-30 16:29:25 +01:00
Samuel Lorch
05dd936fbc Add Readme Section 2022-12-30 16:22:52 +01:00
Samuel Lorch
4dcb528f70 Add Group Json Output 2022-12-30 16:18:11 +01:00
Samuel Lorch
269e117fbf Add Resource Json Output 2022-12-30 16:08:46 +01:00
Samuel Lorch
e8bb791686 Add Folder Json Output 2022-12-30 16:08:29 +01:00
Samuel Lorch
c8dc067697 Add User Json Output 2022-12-30 16:08:14 +01:00
Samuel Lorch
a2257bc011 Add Json Output to Create Commands 2022-12-30 13:35:26 +01:00
Samuel Lorch
46ceb8176d Add Output Json Flag 2022-12-30 13:28:18 +01:00
Samuel Lorch
45fe21119b
Merge pull request #19 from passbolt/feature-add-created-modified-timestamp-columns
Add CreatedTimestamp and ModifiedTimestamp Columns to list commands
2022-12-30 11:50:02 +01:00
Samuel Lorch
d142f14ccc Add CreatedTimestamp and ModifiedTimestamp Columns to list commands 2022-12-30 11:39:59 +01:00
Samuel Lorch
423dfb020c
Merge pull request #18 from passbolt/fix-minor-issues
Fix minor issues
2022-12-30 11:36:27 +01:00
Samuel Lorch
cbf2771935 fix Double Prompting Issue when Reading Secret Input 2022-12-30 11:12:52 +01:00
Samuel Lorch
94165a0f3d Properly Name Groups in Code 2022-12-30 11:08:51 +01:00
Samuel Lorch
2b52c420c4
fix go install 2022-09-11 17:35:36 +02:00
Samuel Lorch
0804a1d9b1
Merge pull request #13 from bersace/patch-1
Lint and fix typos in README
2022-08-21 13:24:07 +02:00
Étienne BERSAC
3ee8f88bbb
Lint and fix typos in README 2022-08-20 12:06:41 +02:00
Samuel Lorch
89abb4f5a3 update deps 2022-05-30 20:00:29 +02:00
Samuel Lorch
6d5327cd95 use string instead of []byte 2022-05-30 19:50:43 +02:00
Samuel Lorch
fe7e6b2a22 fix windows build failing due to syscall 2022-05-30 19:49:02 +02:00
Samuel Lorch
e5c267da42
Merge pull request #12 from Tchoupinax/main
feat: allow password to be taken from pipe
2022-05-30 17:58:41 +02:00
Samuel Lorch
440f41a2af Replace usages of term.ReadPassword with util.ReadPassword 2022-05-30 17:52:17 +02:00
Samuel Lorch
bfbc15bd0d Make util ReadPassword Public 2022-05-30 17:51:54 +02:00
Samuel Lorch
a8b14c959f use golang.org/x/term instead of depricated golang.org/x/crypto/ssh/terminal 2022-05-30 17:51:10 +02:00
Tchoupinax
df187e8a5f feat: allow password to be taken from pipe 2022-05-25 19:00:09 +02:00
38 changed files with 1704 additions and 1100 deletions

View file

@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.17
go-version: 1.21
- name: Build
run: go build -o passbolt

View file

@ -13,32 +13,45 @@ jobs:
steps:
-
name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v4
with:
fetch-depth: 0
-
name: Set up Go
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: 1.17
go-version: 1.21
-
name: Generate Man and Completions
run: |
mkdir completion
go run main.go completion bash > completion/bash
go run main.go completion zsh > completion/zsh
go run main.go completion fish > completion/fish
go run main.go completion powershell > completion/powershell
go run *.go completion bash > completion/bash
go run *.go completion zsh > completion/zsh
go run *.go completion fish > completion/fish
go run *.go completion powershell > completion/powershell
mkdir man
go run main.go gendoc --type man
go run *.go gendoc --type man
pwd
ls
-
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
run: echo "flags=--snapshot" >> $GITHUB_ENV
-
name: Run GoReleaser
uses: goreleaser/goreleaser-action@v2
uses: goreleaser/goreleaser-action@v5
with:
distribution: goreleaser
version: latest
args: release --rm-dist
args: release --clean ${{ env.flags }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
-
name: Run Version
run: |
dist/go-passbolt-cli_linux_amd64_v1/passbolt -v
-
uses: actions/upload-artifact@v4
with:
name: go-passbolt-cli-artifacts
path: dist/

View file

@ -14,13 +14,7 @@ builds:
- arm64
binary: passbolt
archives:
- replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
files:
- files:
- completion/*
- man/*
format_overrides:
@ -31,7 +25,7 @@ release:
header: |
## Release {{ .Tag }} - ({{ .Date }})
nfpms:
- maintainer: Samuel Lorch <sam@lorch.net>
- maintainer: Samuel Lorch <sam@soontm.de>
description: A CLI for Passbolt.
homepage: https://github.com/passbolt/go-passbolt-cli
license: MIT
@ -49,3 +43,22 @@ nfpms:
formats:
- deb
- rpm
brews:
- homepage: https://github.com/passbolt/go-passbolt-cli
license: "MIT"
skip_upload: false
description: "A CLI tool to interact with Passbolt, a Open source Password Manager for Teams"
directory: Formula
install: |
bin.install "passbolt"
bash_completion.install "completion/bash" => "passbolt"
zsh_completion.install "completion/zsh" => "_passbolt"
fish_completion.install "completion/fish" => "passbolt.fish"
man1.install Dir["man/*"]
# ...
repository:
owner: passbolt
name: homebrew-tap
token: "{{ .Env.TAP_GITHUB_TOKEN }}"

View file

@ -1,22 +1,30 @@
# go-passbolt-cli
A CLI tool to interact with Passbolt, a Open source Password Manager for Teams.
A CLI tool to interact with Passbolt, an Open source Password Manager for teams.
If you want to do something more complicated: [this](https://github.com/passbolt/go-passbolt) Go Module to Interact with Passbolt from Go might intrest you.
If you want to do something more complicated: [this Go Module](https://github.com/passbolt/go-passbolt) to Interact with Passbolt from Go might intrest you.
Disclaimer: This project is community driven and not associated with Passbolt SA
# Install
## Via Package (Prefered):
Download the Package for your OS and architecture from the Latest Release.
## Via Repository (Prefered):
[![Packaging status](https://repology.org/badge/vertical-allrepos/go:passbolt-cli.svg)](https://repology.org/project/go:passbolt-cli/versions)
Use the package from your Distros Official Repository
## Via Package:
Download the deb/rpm Package for your Distro and architecture from the Latest Release.
Install via your Distros Package manager like `dpkg -i`
## Via Homebrew
brew install passbolt/tap/go-passbolt-cli
## Via Archive:
Download and Extract the Archive for your OS and architecture from the Latest Release.
Note: tab completion and manpages will need to be installed manually.
## Via Go:
go install github.com/passbolt/go-passbolt-cli
go install github.com/passbolt/go-passbolt-cli@latest
Note: this will install the binary as go-passbolt-cli, also tab completion and manpages will be missing.
# Getting Started
@ -30,35 +38,35 @@ or
```
passbolt configure --serverAddress https://passbolt.example.org --userPassword '1234' --userPrivateKey '-----BEGIN PGP PRIVATE KEY BLOCK-----'
```
- Setup Enviroment Variables
- Setup Environment Variables
- Provide the Flags manually every time
Notes:
- You can set the Private Key using the flags `--userPrivateKey` or `--userPrivateKeyFile` where `--userPrivateKey` takes the actual private key and `--userPrivateKeyFile` loads the content of a file as the PrivateKey, `--userPrivateKeyFile` overwrites the value of `--userPrivateKey`.
- You can also just store the serverAddress and your Private Key, if your Password is not set it will prompt you for it every time.
- Passwordless PrivateKeys are unsupported
- MFA settings can also be save permenantly this ways
- MFA settings can also be save permanently this ways
# Usage
Generally the Structure of Commands is like this:
```bash
go-passbolt-cli action entity [arguments]
passbolt action entity [arguments]
```
Action is the Action you want to perform like Creating, Updating or Deleting a Entity.
Entity is a Resource(Password), Folder, User or Group that you want to apply a action to.
Action is the Action you want to perform like Creating, Updating or Deleting an Entity.
Entity is a Resource(Password), Folder, User or Group that you want to apply an action to.
In Passbolt a Password is usually revert to as a Resource.
To Create a Resource you can do this, it will return the ID of the newly created Resource:
```bash
go-passbolt-cli create resource --name "Test Resource" --password "Strong Password"
passbolt create resource --name "Test Resource" --password "Strong Password"
```
You can then list all users:
```bash
go-passbolt-cli list user
passbolt list user
```
Note: you can adjust which columns should be listed using the flag `--column` or its short from `-c`, if you want multiple column then you need to specify this flag multiple times.
@ -74,25 +82,43 @@ For sharing we will need to know how we want to share, for that there are these
Now that we have a Resource ID, know the ID's of other Users and about know about Permission Types, we can share the Resource with them:
```bash
go-passbolt-cli share resource --id id_of_resource_to_share --type type_of_permission --user id_of_user_to_share_with
passbolt share resource --id id_of_resource_to_share --type type_of_permission --user id_of_user_to_share_with
```
Note: you can supply the the users argument multiple times to share with multiple users
For sharing with groups the `--group` argument exists.
# MFA
you can setup MFA also using the configuration sub command, only TOTP is supported, there are mulitple modes for MFA: `none`, `interactive-totp` and `noninteractive-totp`.
You can setup MFA also using the configuration sub command, only TOTP is supported, there are multiple modes for MFA: `none`, `interactive-totp` and `noninteractive-totp`.
| Mode | Description |
| --- | --- |
|`none`|just errors if challanged for MFA.
|`none`|just errors if challenged for MFA.
|`interactive-totp` | prompts for interactive entry of TOTP Codes.
|`noninteractive-totp` | automatically generates TOTP Codes when challenged, it requires the `totpToken` flag to be set to your totp Secret, you can configure the behavior using the `mfaDelay`, `mfaRetrys` and `totpOffset` flags
|`noninteractive-totp` | automatically generates TOTP Codes when challenged, it requires the `mfaTotpToken` flag to be set to your totp Secret, you can configure the behavior using the `mfaDelay`, `mfaRetrys` and `mfaTotpOffset` flags
# Server Verification
to enable Server Verification you need to run `passbolt verify` once, after that the server will always be verified if the same config is used
To enable Server Verification you need to run `passbolt verify` once, after that the server will always be verified if the same config is used
# Scripting
For Scripting we have a -j or --json flag to convert the Output for the create, get and list commands to JSON for easier Parsing in Scripts.
Note: The JSON Output does not cover Error Messages, you can detect Errors by checking if the Exitcode is not 0
# Exposing Secrets to Subprocesses
The `exec` command allows you to execute another command with environment variables that reference secrets stored in Passbolt.
Any environment variables containing `passbolt://` references are automatically resolved to their corresponding secret values
before the specified command is executed. This ensures that secrets are securely injected into the child process's environment
without exposing them to the parent shell.
For example:
```bash
export GITHUB_TOKEN=passbolt://<PASSBOLT_RESOURCE_ID_HERE>
passbolt exec -- gh auth login
```
This would resolve the passbolt:// reference in GITHUB_TOKEN to its actual secret value and pass it to the gh process.
# Documentation
Usage for all Subcommands is [here](https://github.com/passbolt/go-passbolt-cli/wiki/go-passbolt-cli).
Usage for all Subcommands is [here](https://github.com/passbolt/go-passbolt-cli/wiki/passbolt).
And is also available via `man passbolt`

View file

@ -18,6 +18,7 @@ var createCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(createCmd)
createCmd.PersistentFlags().BoolP("json", "j", false, "Output JSON")
createCmd.AddCommand(resource.ResourceCreateCmd)
createCmd.AddCommand(folder.FolderCreateCmd)
createCmd.AddCommand(group.GroupCreateCmd)

103
cmd/exec.go Normal file
View file

@ -0,0 +1,103 @@
package cmd
import (
"context"
"fmt"
"os"
"os/exec"
"strings"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/passbolt/go-passbolt/helper"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
const PassboltPrefix = "passbolt://"
// execCmd represents the exec command
var execCmd = &cobra.Command{
Use: "exec -- command [args...]",
Short: "Run a command with secrets injected into the environment.",
Long: `The command allows you to execute another command with environment variables that reference secrets stored in Passbolt.
Any environment variables containing passbolt:// references are automatically resolved to their corresponding secret values
before the specified command is executed. This ensures that secrets are securely injected into the child process's environment
without exposing them to the parent shell.
For example:
export GITHUB_TOKEN=passbolt://<PASSBOLT_RESOURCE_ID_HERE>
passbolt exec -- gh auth login
This would resolve the passbolt:// reference in GITHUB_TOKEN to its actual secret value and pass it to the gh process.
`,
Args: cobra.MinimumNArgs(1),
RunE: execAction,
}
func init() {
rootCmd.AddCommand(execCmd)
}
func execAction(_ *cobra.Command, args []string) error {
ctx, cancel := context.WithTimeout(context.Background(), viper.GetDuration("timeout"))
defer cancel()
client, err := util.GetClient(ctx)
if err != nil {
return fmt.Errorf("Creating client: %w", err)
}
envVars, err := resolveEnvironmentSecrets(ctx, client)
if err != nil {
return fmt.Errorf("Resolving secrets: %w", err)
}
if err = client.Logout(ctx); err != nil {
return fmt.Errorf("Logging out client: %w", err)
}
subCmd := exec.Command(args[0], args[1:]...)
subCmd.Stdin = os.Stdin
subCmd.Stdout = os.Stdout
subCmd.Stderr = os.Stderr
subCmd.Env = envVars
if err = subCmd.Run(); err != nil {
return fmt.Errorf("Running command: %w", err)
}
return nil
}
func resolveEnvironmentSecrets(ctx context.Context, client *api.Client) ([]string, error) {
envVars := os.Environ()
for i, envVar := range envVars {
splitIndex := strings.Index(envVar, "=")
if splitIndex == -1 {
continue
}
key := envVar[:splitIndex]
value := envVar[splitIndex+1:]
if !strings.HasPrefix(value, PassboltPrefix) {
continue
}
resourceId := strings.TrimPrefix(value, PassboltPrefix)
_, _, _, _, secret, _, err := helper.GetResource(ctx, client, resourceId)
if err != nil {
return nil, fmt.Errorf("Getting resource: %w", err)
}
envVars[i] = key + "=" + secret
if viper.GetBool("debug") {
fmt.Fprintf(os.Stdout, "%v env var populated with resource id %v\n", key, resourceId)
}
}
return envVars, nil
}

View file

@ -18,6 +18,7 @@ var getCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(getCmd)
getCmd.PersistentFlags().BoolP("json", "j", false, "Output JSON")
getCmd.AddCommand(resource.ResourceGetCmd)
getCmd.AddCommand(folder.FolderGetCmd)
getCmd.AddCommand(group.GroupGetCmd)

View file

@ -18,6 +18,13 @@ var listCmd = &cobra.Command{
func init() {
rootCmd.AddCommand(listCmd)
listCmd.PersistentFlags().BoolP("json", "j", false, "Output JSON")
listCmd.PersistentFlags().String("filter", "",
"Define a CEl expression as filter for any list commands. In the expression, all available columns of subcommand can be used (see -c/--column).\n"+
"See also CEl specifications under https://github.com/google/cel-spec.\n"+
"Examples:\n"+
"\t--filter '(Name == \"SomeName\" || matches(Name, \"RegExpr\")) && URI.startsWith(\"https://auth.\")'\n"+
"\t--filter 'Username == \"User\" && CreatedTimestamp > timestamp(\"2022-06-10T00:00:00.000-00:00\")'")
listCmd.AddCommand(resource.ResourceListCmd)
listCmd.AddCommand(folder.FolderListCmd)
listCmd.AddCommand(group.GroupListCmd)

View file

@ -2,7 +2,6 @@ package cmd
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"time"
@ -48,11 +47,24 @@ func init() {
rootCmd.PersistentFlags().String("userPrivateKeyFile", "", "Passbolt User Private Key File, if set then the userPrivateKey will be Overwritten with the File Content")
rootCmd.PersistentFlags().String("userPassword", "", "Passbolt User Password")
rootCmd.PersistentFlags().String("mfaMode", "interactive-totp", "How to Handle MFA, the following Modes exist: none, interactive-totp and noninteractive-totp")
rootCmd.PersistentFlags().String("totpToken", "", "Token to generate TOTP's, only used in nointeractive-totp mode")
rootCmd.PersistentFlags().MarkDeprecated("totpToken", "use --mfaTotpToken instead")
rootCmd.PersistentFlags().String("mfaTotpToken", "", "Token to generate TOTP's, only used in nointeractive-totp mode")
rootCmd.PersistentFlags().Duration("totpOffset", time.Duration(0), "TOTP Generation offset only used in noninteractive-totp mode")
rootCmd.PersistentFlags().MarkDeprecated("totpOffset", "use --mfaTotpOffset instead")
rootCmd.PersistentFlags().Duration("mfaTotpOffset", time.Duration(0), "TOTP Generation offset only used in noninteractive-totp mode")
rootCmd.PersistentFlags().Uint("mfaRetrys", 3, "How often to retry TOTP Auth, only used in nointeractive modes")
rootCmd.PersistentFlags().Duration("mfaDelay", time.Second*10, "Delay between MFA Attempts, only used in noninteractive modes")
rootCmd.PersistentFlags().Bool("tlsSkipVerify", false, "Allow servers with self-signed certificates")
rootCmd.PersistentFlags().String("tlsClientPrivateKeyFile", "", "Client private key path for mtls")
rootCmd.PersistentFlags().String("tlsClientCertFile", "", "Client certificate path for mtls")
rootCmd.PersistentFlags().String("tlsClientPrivateKey", "", "Client private key for mtls")
rootCmd.PersistentFlags().String("tlsClientCert", "", "Client certificate for mtls")
viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug"))
viper.BindPFlag("timeout", rootCmd.PersistentFlags().Lookup("timeout"))
viper.BindPFlag("serverAddress", rootCmd.PersistentFlags().Lookup("serverAddress"))
@ -60,9 +72,27 @@ func init() {
viper.BindPFlag("userPassword", rootCmd.PersistentFlags().Lookup("userPassword"))
viper.BindPFlag("mfaMode", rootCmd.PersistentFlags().Lookup("mfaMode"))
viper.BindPFlag("totpToken", rootCmd.PersistentFlags().Lookup("totpToken"))
viper.BindPFlag("mfaTotpToken", rootCmd.PersistentFlags().Lookup("mfaTotpToken"))
viper.BindPFlag("totpOffset", rootCmd.PersistentFlags().Lookup("totpOffset"))
viper.BindPFlag("mfaTotpOffset", rootCmd.PersistentFlags().Lookup("mfaTotpOffset"))
viper.BindPFlag("mfaRetrys", rootCmd.PersistentFlags().Lookup("mfaRetrys"))
viper.BindPFlag("mfaDelay", rootCmd.PersistentFlags().Lookup("mfaDelay"))
viper.BindPFlag("tlsSkipVerify", rootCmd.PersistentFlags().Lookup("tlsSkipVerify"))
viper.BindPFlag("tlsClientCert", rootCmd.PersistentFlags().Lookup("tlsClientCert"))
viper.BindPFlag("tlsClientPrivateKey", rootCmd.PersistentFlags().Lookup("tlsClientPrivateKey"))
}
func fileToContent(file, contentFlag string) {
if viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "Loading file:", file)
}
content, err := os.ReadFile(file)
if err != nil {
fmt.Fprintln(os.Stderr, "Error Loading File: ", err)
os.Exit(1)
}
viper.Set(contentFlag, string(content))
}
// initConfig reads in config file and ENV variables if set.
@ -98,16 +128,32 @@ func initConfig() {
// Read in Private Key from File if userprivatekeyfile is set
userprivatekeyfile, err := rootCmd.PersistentFlags().GetString("userPrivateKeyFile")
if err == nil && userprivatekeyfile != "" {
if viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "Loading Private Key from File:", userprivatekeyfile)
}
content, err := ioutil.ReadFile(userprivatekeyfile)
if err != nil {
fmt.Fprintln(os.Stderr, "Error Loading Private Key from File: ", err)
os.Exit(1)
}
viper.Set("userprivatekey", string(content))
fileToContent(userprivatekeyfile, "userPrivateKey")
} else if err != nil && viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "Getting Private Key File Flag:", err)
}
// Read in Client Certificate Private Key from File if tlsClientPrivateKeyFile is set
tlsclientprivatekeyfile, err := rootCmd.PersistentFlags().GetString("tlsClientPrivateKeyFile")
if err == nil && tlsclientprivatekeyfile != "" {
fileToContent(tlsclientprivatekeyfile, "tlsClientPrivateKey")
} else if err != nil && viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "Getting Client Certificate Private key File Flag:", err)
}
// Read in Client Certificate from File if tlsClientCertFile is set
tlsclientcertfile, err := rootCmd.PersistentFlags().GetString("tlsClientCertFile")
if err == nil && tlsclientcertfile != "" {
fileToContent(tlsclientcertfile, "tlsClientCert")
} else if err != nil && viper.GetBool("debug") {
fmt.Fprintln(os.Stderr, "Getting Client Certificate File Flag:", err)
}
}
func SetVersionInfo(version, commit, date string, dirty bool) {
v := fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit)
if dirty {
v = v + " dirty"
}
rootCmd.Version = v
}

View file

@ -2,13 +2,11 @@ package cmd
import (
"fmt"
"syscall"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"golang.org/x/term"
)
// verifyCMD represents the verify command
@ -34,17 +32,20 @@ var verifyCMD = &cobra.Command{
userPassword := viper.GetString("userPassword")
if userPassword == "" {
fmt.Print("Enter Password:")
bytepw, err := term.ReadPassword(int(syscall.Stdin))
pw, err := util.ReadPassword("Enter Password:")
if err != nil {
fmt.Println()
return fmt.Errorf("Reading Password: %w", err)
}
userPassword = string(bytepw)
userPassword = pw
fmt.Println()
}
client, err := api.NewClient(nil, "", serverAddress, userPrivateKey, userPassword)
httpClient, err := util.GetHttpClient()
if err != nil {
return err
}
client, err := api.NewClient(httpClient, "", serverAddress, userPrivateKey, userPassword)
if err != nil {
return fmt.Errorf("Creating Client: %w", err)
}

View file

@ -2,6 +2,7 @@ package folder
import (
"context"
"encoding/json"
"fmt"
"github.com/passbolt/go-passbolt-cli/util"
@ -33,6 +34,10 @@ func FolderCreate(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -53,6 +58,18 @@ func FolderCreate(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Creating Folder: %w", err)
}
fmt.Printf("FolderID: %v\n", id)
if jsonOutput {
jsonId, err := json.MarshalIndent(
map[string]string{"id": id},
"",
" ",
)
if err != nil {
return fmt.Errorf("Marshalling Json: %w", err)
}
fmt.Println(string(jsonId))
} else {
fmt.Printf("FolderID: %v\n", id)
}
return nil
}

56
folder/filter.go Normal file
View file

@ -0,0 +1,56 @@
package folder
import (
"context"
"fmt"
"github.com/google/cel-go/cel"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
)
// Environments for CEl
var celEnvOptions = []cel.EnvOption{
cel.Variable("ID", cel.StringType),
cel.Variable("FolderParentID", cel.StringType),
cel.Variable("Name", cel.StringType),
cel.Variable("CreatedTimestamp", cel.TimestampType),
cel.Variable("ModifiedTimestamp", cel.TimestampType),
}
// Filters the slice folders by invoke CEL program for each folder
func filterFolders(folders *[]api.Folder, celCmd string, ctx context.Context) ([]api.Folder, error) {
if celCmd == "" {
return *folders, nil
}
program, err := util.InitCELProgram(celCmd, celEnvOptions...)
if err != nil {
return nil, err
}
filteredFolders := []api.Folder{}
for _, folder := range *folders {
val, _, err := (*program).ContextEval(ctx, map[string]any{
"ID": folder.ID,
"FolderParentID": folder.FolderParentID,
"Name": folder.Name,
"CreatedTimestamp": folder.Created.Time,
"ModifiedTimestamp": folder.Modified.Time,
})
if err != nil {
return nil, err
}
if val.Value() == true {
filteredFolders = append(filteredFolders, folder)
}
}
if len(filteredFolders) == 0 {
return nil, fmt.Errorf("No such folders found with filter %v!", celCmd)
}
return filteredFolders, nil
}

View file

@ -2,11 +2,11 @@ package folder
import (
"context"
"encoding/json"
"fmt"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/helper"
"github.com/spf13/cobra"
)
@ -29,6 +29,10 @@ func FolderGet(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -39,15 +43,22 @@ func FolderGet(cmd *cobra.Command, args []string) error {
defer client.Logout(context.TODO())
cmd.SilenceUsage = true
folderParentID, name, err := helper.GetFolder(
ctx,
client,
id,
)
folder, err := client.GetFolder(ctx, id, nil)
if err != nil {
return fmt.Errorf("Getting Folder: %w", err)
}
fmt.Printf("FolderParentID: %v\n", folderParentID)
fmt.Printf("Name: %v\n", shellescape.StripUnsafe(name))
if jsonOutput {
jsonGroup, err := json.MarshalIndent(FolderJsonOutput{
FolderParentID: &folder.FolderParentID,
Name: &folder.Name,
}, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonGroup))
} else {
fmt.Printf("FolderParentID: %v\n", folder.FolderParentID)
fmt.Printf("Name: %v\n", shellescape.StripUnsafe(folder.Name))
}
return nil
}

11
folder/json.go Normal file
View file

@ -0,0 +1,11 @@
package folder
import "time"
type FolderJsonOutput struct {
ID *string `json:"id,omitempty"`
FolderParentID *string `json:"folder_parent_id,omitempty"`
Name *string `json:"name,omitempty"`
CreatedTimestamp *time.Time `json:"created_timestamp,omitempty"`
ModifiedTimestamp *time.Time `json:"modified_timestamp,omitempty"`
}

View file

@ -2,10 +2,12 @@ package folder
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/spf13/cobra"
@ -26,7 +28,7 @@ func init() {
FolderListCmd.Flags().StringP("search", "s", "", "Folders that have this in the Name")
FolderListCmd.Flags().StringArrayP("folder", "f", []string{}, "Folders that are in this Folder")
FolderListCmd.Flags().StringArrayP("group", "g", []string{}, "Folders that are shared with group")
FolderListCmd.Flags().StringArrayP("column", "c", []string{"ID", "FolderParentID", "Name"}, "Columns to return, possible Columns:\nID, FolderParentID, Name")
FolderListCmd.Flags().StringArrayP("column", "c", []string{"ID", "FolderParentID", "Name"}, "Columns to return, possible Columns:\nID, FolderParentID, Name, CreatedTimestamp, ModifiedTimestamp")
}
func FolderList(cmd *cobra.Command, args []string) error {
@ -45,6 +47,14 @@ func FolderList(cmd *cobra.Command, args []string) error {
if len(columns) == 0 {
return fmt.Errorf("You need to Specify atleast one column to return")
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
celFilter, err := cmd.Flags().GetString("filter")
if err != nil {
return err
}
ctx := util.GetContext()
cmd.SilenceUsage = true
@ -63,26 +73,53 @@ func FolderList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Listing Folder: %w", err)
}
data := pterm.TableData{columns}
for _, folder := range folders {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = folder.ID
case "folderparentid":
entry[i] = folder.FolderParentID
case "name":
entry[i] = shellescape.StripUnsafe(folder.Name)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
folders, err = filterFolders(&folders, celFilter, ctx)
if err != nil {
return err
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
if jsonOutput {
outputFolders := []FolderJsonOutput{}
for i := range folders {
outputFolders = append(outputFolders, FolderJsonOutput{
ID: &folders[i].ID,
FolderParentID: &folders[i].FolderParentID,
Name: &folders[i].Name,
CreatedTimestamp: &folders[i].Created.Time,
ModifiedTimestamp: &folders[i].Modified.Time,
})
}
jsonFolders, err := json.MarshalIndent(outputFolders, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonFolders))
} else {
data := pterm.TableData{columns}
for _, folder := range folders {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = folder.ID
case "folderparentid":
entry[i] = folder.FolderParentID
case "name":
entry[i] = shellescape.StripUnsafe(folder.Name)
case "createdtimestamp":
entry[i] = folder.Created.Format(time.RFC3339)
case "modifiedtimestamp":
entry[i] = folder.Modified.Format(time.RFC3339)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
return nil
}

76
go.mod
View file

@ -1,23 +1,67 @@
module github.com/passbolt/go-passbolt-cli
go 1.16
go 1.23.0
toolchain go1.23.6
require (
github.com/ProtonMail/go-crypto v0.0.0-20220407094043-a94812496cf5 // indirect
github.com/ProtonMail/gopenpgp/v2 v2.4.6 // indirect
github.com/alessio/shellescape v1.4.1
github.com/gookit/color v1.5.0 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/passbolt/go-passbolt v0.5.7
github.com/pterm/pterm v0.12.41
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cobra v1.4.0
github.com/spf13/viper v1.10.1
github.com/tobischo/gokeepasslib/v3 v3.2.4
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 // indirect
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad // indirect
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
gopkg.in/ini.v1 v1.66.4 // indirect
al.essio.dev/pkg/shellescape v1.5.1
github.com/google/cel-go v0.24.1
github.com/passbolt/go-passbolt v0.7.2
github.com/pterm/pterm v0.12.80
github.com/spf13/cobra v1.9.1
github.com/spf13/viper v1.19.0
github.com/tobischo/gokeepasslib/v3 v3.6.1
golang.org/x/term v0.29.0
)
require (
atomicgo.dev/cursor v0.2.0 // indirect
atomicgo.dev/keyboard v0.2.9 // indirect
atomicgo.dev/schedule v0.1.0 // indirect
cel.dev/expr v0.21.2 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
github.com/ProtonMail/gopenpgp/v2 v2.8.3 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
github.com/cloudflare/circl v1.6.0 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/lithammer/fuzzysearch v1.1.8 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sagikazarmark/locafero v0.7.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.12.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stoewer/go-strcase v1.3.0 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tobischo/argon2 v0.1.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.35.0 // indirect
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
// replace github.com/passbolt/go-passbolt => ../go-passbolt

1052
go.sum

File diff suppressed because it is too large Load diff

View file

@ -2,6 +2,7 @@ package group
import (
"context"
"encoding/json"
"fmt"
"github.com/passbolt/go-passbolt-cli/util"
@ -40,6 +41,10 @@ func GroupCreate(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ops := []helper.GroupMembershipOperation{}
for _, user := range users {
@ -74,6 +79,18 @@ func GroupCreate(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Creating Group: %w", err)
}
fmt.Printf("GroupID: %v\n", id)
if jsonOutput {
jsonId, err := json.MarshalIndent(
map[string]string{"id": id},
"",
" ",
)
if err != nil {
return fmt.Errorf("Marshalling Json: %w", err)
}
fmt.Println(string(jsonId))
} else {
fmt.Printf("GroupID: %v\n", id)
}
return nil
}

54
group/filter.go Normal file
View file

@ -0,0 +1,54 @@
package group
import (
"context"
"fmt"
"github.com/google/cel-go/cel"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
)
// Environments for CEl
var celEnvOptions = []cel.EnvOption{
cel.Variable("ID", cel.StringType),
cel.Variable("Name", cel.StringType),
cel.Variable("CreatedTimestamp", cel.TimestampType),
cel.Variable("ModifiedTimestamp", cel.TimestampType),
}
// Filters the slice groups by invoke CEL program for each group
func filterGroups(groups *[]api.Group, celCmd string, ctx context.Context) ([]api.Group, error) {
if celCmd == "" {
return *groups, nil
}
program, err := util.InitCELProgram(celCmd, celEnvOptions...)
if err != nil {
return nil, err
}
filteredGroups := []api.Group{}
for _, group := range *groups {
val, _, err := (*program).ContextEval(ctx, map[string]any{
"ID": group.ID,
"Name": group.Name,
"CreatedTimestamp": group.Created.Time,
"ModifiedTimestamp": group.Modified.Time,
})
if err != nil {
return nil, err
}
if val.Value() == true {
filteredGroups = append(filteredGroups, group)
}
}
if len(filteredGroups) == 0 {
return nil, fmt.Errorf("No such groups found with filter %v!", celCmd)
}
return filteredGroups, nil
}

View file

@ -2,10 +2,11 @@ package group
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/helper"
"github.com/pterm/pterm"
@ -37,6 +38,10 @@ func GroupGet(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -55,34 +60,58 @@ func GroupGet(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("Getting Group: %w", err)
}
fmt.Printf("Name: %v\n", name)
// Print Memberships
if len(columns) != 0 {
data := pterm.TableData{columns}
for _, membership := range memberships {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "userid":
entry[i] = membership.UserID
case "isgroupmanager":
entry[i] = fmt.Sprint(membership.IsGroupManager)
case "username":
entry[i] = shellescape.StripUnsafe(membership.Username)
case "userfirstname":
entry[i] = shellescape.StripUnsafe(membership.UserFirstName)
case "userlastname":
entry[i] = shellescape.StripUnsafe(membership.UserLastName)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
if jsonOutput {
groupUserMemberships := []GroupUserMembershipJsonOutput{}
for i := range memberships {
groupUserMemberships = append(groupUserMemberships, GroupUserMembershipJsonOutput{
ID: &memberships[i].UserID,
Username: &memberships[i].Username,
FirstName: &memberships[i].UserFirstName,
LastName: &memberships[i].UserLastName,
IsGroupManager: &memberships[i].IsGroupManager,
})
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
jsonGroup, err := json.MarshalIndent(GroupJsonOutput{
Name: &name,
Users: groupUserMemberships,
}, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonGroup))
} else {
fmt.Printf("Name: %v\n", name)
// Print Memberships
if len(columns) != 0 {
data := pterm.TableData{columns}
for _, membership := range memberships {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "userid":
entry[i] = membership.UserID
case "isgroupmanager":
entry[i] = fmt.Sprint(membership.IsGroupManager)
case "username":
entry[i] = shellescape.StripUnsafe(membership.Username)
case "userfirstname":
entry[i] = shellescape.StripUnsafe(membership.UserFirstName)
case "userlastname":
entry[i] = shellescape.StripUnsafe(membership.UserLastName)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
}
return nil
}

19
group/json.go Normal file
View file

@ -0,0 +1,19 @@
package group
import "time"
type GroupJsonOutput struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Users []GroupUserMembershipJsonOutput `json:"users,omitempty"`
CreatedTimestamp *time.Time `json:"created_timestamp,omitempty"`
ModifiedTimestamp *time.Time `json:"modified_timestamp,omitempty"`
}
type GroupUserMembershipJsonOutput struct {
ID *string `json:"id,omitempty"`
Username *string `json:"username,omitempty"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
IsGroupManager *bool `json:"is_group_manager,omitempty"`
}

View file

@ -2,10 +2,12 @@ package group
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/spf13/cobra"
@ -26,7 +28,7 @@ func init() {
GroupListCmd.Flags().StringArrayP("user", "u", []string{}, "Groups that are shared with group")
GroupListCmd.Flags().StringArrayP("manager", "m", []string{}, "Groups that are in folder")
GroupListCmd.Flags().StringArrayP("column", "c", []string{"ID", "Name"}, "Columns to return, possible Columns:\nID, Name")
GroupListCmd.Flags().StringArrayP("column", "c", []string{"ID", "Name"}, "Columns to return, possible Columns:\nID, Name, CreatedTimestamp, ModifiedTimestamp")
}
func GroupList(cmd *cobra.Command, args []string) error {
@ -45,6 +47,14 @@ func GroupList(cmd *cobra.Command, args []string) error {
if len(columns) == 0 {
return fmt.Errorf("You need to specify atleast one column to return")
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
celFilter, err := cmd.Flags().GetString("filter")
if err != nil {
return err
}
ctx := util.GetContext()
@ -55,7 +65,7 @@ func GroupList(cmd *cobra.Command, args []string) error {
defer client.Logout(context.TODO())
cmd.SilenceUsage = true
resources, err := client.GetGroups(ctx, &api.GetGroupsOptions{
groups, err := client.GetGroups(ctx, &api.GetGroupsOptions{
FilterHasUsers: users,
FilterHasManagers: managers,
})
@ -63,24 +73,50 @@ func GroupList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Listing Group: %w", err)
}
data := pterm.TableData{columns}
for _, resource := range resources {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = resource.ID
case "name":
entry[i] = shellescape.StripUnsafe(resource.Name)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
groups, err = filterGroups(&groups, celFilter, ctx)
if err != nil {
return err
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
if jsonOutput {
outputGroups := []GroupJsonOutput{}
for i := range groups {
outputGroups = append(outputGroups, GroupJsonOutput{
ID: &groups[i].ID,
Name: &groups[i].Name,
CreatedTimestamp: &groups[i].Created.Time,
ModifiedTimestamp: &groups[i].Modified.Time,
})
}
jsonGroups, err := json.MarshalIndent(outputGroups, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonGroups))
} else {
data := pterm.TableData{columns}
for _, group := range groups {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = group.ID
case "name":
entry[i] = shellescape.StripUnsafe(group.Name)
case "createdtimestamp":
entry[i] = group.Created.Format(time.RFC3339)
case "modifiedtimestamp":
entry[i] = group.Modified.Format(time.RFC3339)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
return nil
}

View file

@ -2,9 +2,13 @@ package keepass
import (
"context"
"encoding/json"
"fmt"
"net/url"
"os"
"syscall"
"sort"
"strconv"
"strings"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
@ -13,7 +17,6 @@ import (
"github.com/spf13/cobra"
"github.com/tobischo/gokeepasslib/v3"
w "github.com/tobischo/gokeepasslib/v3/wrappers"
"golang.org/x/term"
)
// KeepassExportCmd Exports a Passbolt Keepass
@ -55,13 +58,12 @@ func KeepassExport(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
if keepassPassword == "" {
fmt.Print("Enter Keepass Password:")
bytepw, err := term.ReadPassword(int(syscall.Stdin))
pw, err := util.ReadPassword("Enter Keepass Password:")
if err != nil {
fmt.Println()
return fmt.Errorf("Reading Keepass Password: %w", err)
}
keepassPassword = string(bytepw)
keepassPassword = pw
fmt.Println()
}
@ -91,22 +93,15 @@ func KeepassExport(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Progress: %w", err)
}
for i, resource := range resources {
_, _, _, _, pass, desc, err := helper.GetResourceFromData(client, resource, resource.Secrets[0], resource.ResourceType)
for _, resource := range resources {
entry, err := getKeepassEntry(client, resource, resource.Secrets[0], resource.ResourceType)
if err != nil {
return fmt.Errorf("Get Resource %v, %v %w", i, resource.ID, err)
fmt.Printf("\nSkipping Export of Resource %v %v Because of: %v\n", resource.ID, resource.Name, err)
progressbar.Increment()
continue
}
entry := gokeepasslib.NewEntry()
entry.Values = append(
entry.Values,
gokeepasslib.ValueData{Key: "Title", Value: gokeepasslib.V{Content: resource.Name}},
gokeepasslib.ValueData{Key: "UserName", Value: gokeepasslib.V{Content: resource.Username}},
gokeepasslib.ValueData{Key: "URL", Value: gokeepasslib.V{Content: resource.URI}},
gokeepasslib.ValueData{Key: "Password", Value: gokeepasslib.V{Content: pass, Protected: w.NewBoolWrapper(true)}},
gokeepasslib.ValueData{Key: "Notes", Value: gokeepasslib.V{Content: desc}},
)
rootGroup.Entries = append(rootGroup.Entries, entry)
rootGroup.Entries = append(rootGroup.Entries, *entry)
progressbar.Increment()
}
@ -133,3 +128,102 @@ func KeepassExport(cmd *cobra.Command, args []string) error {
return nil
}
func getKeepassEntry(client *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (*gokeepasslib.Entry, error) {
_, _, _, _, pass, desc, err := helper.GetResourceFromData(client, resource, resource.Secrets[0], resource.ResourceType)
if err != nil {
return nil, fmt.Errorf("Get Resource %v: %w", resource.ID, err)
}
entry := gokeepasslib.NewEntry()
entry.Values = append(
entry.Values,
gokeepasslib.ValueData{Key: "Title", Value: gokeepasslib.V{Content: resource.Name}},
gokeepasslib.ValueData{Key: "UserName", Value: gokeepasslib.V{Content: resource.Username}},
gokeepasslib.ValueData{Key: "URL", Value: gokeepasslib.V{Content: resource.URI}},
gokeepasslib.ValueData{Key: "Password", Value: gokeepasslib.V{Content: pass, Protected: w.NewBoolWrapper(true)}},
gokeepasslib.ValueData{Key: "Notes", Value: gokeepasslib.V{Content: desc}},
)
if resource.ResourceType.Slug == "password-description-totp" || resource.ResourceType.Slug == "totp" {
var totpData api.SecretDataTOTP
rawSecretData, err := client.DecryptMessage(resource.Secrets[0].Data)
if err != nil {
return nil, fmt.Errorf("Decrypting Secret Data: %w", err)
}
if resource.ResourceType.Slug == "password-description-totp" {
var secretData api.SecretDataTypePasswordDescriptionTOTP
err = json.Unmarshal([]byte(rawSecretData), &secretData)
if err != nil {
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
}
totpData = secretData.TOTP
} else {
var secretData api.SecretDataTypeTOTP
err = json.Unmarshal([]byte(rawSecretData), &secretData)
if err != nil {
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
}
totpData = secretData.TOTP
}
v := url.Values{}
v.Set("secret", totpData.SecretKey)
v.Set("period", strconv.FormatUint(uint64(totpData.Period), 10))
v.Set("algorithm", totpData.Algorithm)
v.Set("digits", fmt.Sprint(totpData.Digits))
issuer := resource.URI
if resource.URI == "" {
issuer = resource.Name
}
v.Set("issuer", issuer)
accountName := resource.Username
if resource.Username == "" {
accountName = resource.Name
}
u := url.URL{
Scheme: "otpauth",
Host: "totp",
Path: "/" + issuer + ":" + accountName,
RawQuery: encodeQuery(v),
}
entry.Values = append(entry.Values, gokeepasslib.ValueData{Key: "otp", Value: gokeepasslib.V{Content: u.String(), Protected: w.NewBoolWrapper(true)}})
}
return &entry, nil
}
// EncodeQuery is a copy-paste of url.Values.Encode, except it uses %20 instead
// of + to encode spaces. This is necessary to correctly render spaces in some
// authenticator apps, like Google Authenticator.
func encodeQuery(v url.Values) string {
if v == nil {
return ""
}
var buf strings.Builder
keys := make([]string, 0, len(v))
for k := range v {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
vs := v[k]
keyEscaped := url.PathEscape(k) // changed from url.QueryEscape
for _, v := range vs {
if buf.Len() > 0 {
buf.WriteByte('&')
}
buf.WriteString(keyEscaped)
buf.WriteByte('=')
buf.WriteString(url.PathEscape(v)) // changed from url.QueryEscape
}
}
return buf.String()
}

View file

@ -1,7 +1,10 @@
package main
import "github.com/passbolt/go-passbolt-cli/cmd"
import (
"github.com/passbolt/go-passbolt-cli/cmd"
)
func main() {
cmd.SetVersionInfo(version, commit, date, dirty)
cmd.Execute()
}

View file

@ -2,6 +2,7 @@ package resource
import (
"context"
"encoding/json"
"fmt"
"github.com/passbolt/go-passbolt-cli/util"
@ -54,6 +55,10 @@ func ResourceCreate(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -78,6 +83,18 @@ func ResourceCreate(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Creating Resource: %w", err)
}
fmt.Printf("ResourceID: %v\n", id)
if jsonOutput {
jsonId, err := json.MarshalIndent(
map[string]string{"id": id},
"",
" ",
)
if err != nil {
return fmt.Errorf("Marshalling Json: %w", err)
}
fmt.Println(string(jsonId))
} else {
fmt.Printf("ResourceID: %v\n", id)
}
return nil
}

80
resource/filter.go Normal file
View file

@ -0,0 +1,80 @@
package resource
import (
"context"
"fmt"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/passbolt/go-passbolt/helper"
)
// Environments for CEl
var celEnvOptions = []cel.EnvOption{
cel.Variable("ID", cel.StringType),
cel.Variable("FolderParentID", cel.StringType),
cel.Variable("Name", cel.StringType),
cel.Variable("Username", cel.StringType),
cel.Variable("URI", cel.StringType),
cel.Variable("Password", cel.StringType),
cel.Variable("Description", cel.StringType),
cel.Variable("CreatedTimestamp", cel.TimestampType),
cel.Variable("ModifiedTimestamp", cel.TimestampType),
}
// Filters the slice resources by invoke CEL program for each resource
func filterResources(resources *[]api.Resource, celCmd string, ctx context.Context, client *api.Client) ([]api.Resource, error) {
if celCmd == "" {
return *resources, nil
}
program, err := util.InitCELProgram(celCmd, celEnvOptions...)
if err != nil {
return nil, err
}
filteredResources := []api.Resource{}
for _, resource := range *resources {
val, _, err := (*program).ContextEval(ctx, map[string]any{
"Id": resource.ID,
"FolderParentID": resource.FolderParentID,
"Name": resource.Name,
"Username": resource.Username,
"URI": resource.URI,
"Password": func() ref.Val {
_, _, _, _, pass, _, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
fmt.Printf("Get Resource %v", err)
return types.String("")
}
return types.String(pass)
},
"Description": func() ref.Val {
_, _, _, _, _, descr, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
fmt.Printf("Get Resource %v", err)
return types.String("")
}
return types.String(descr)
},
"CreatedTimestamp": resource.Created.Time,
"ModifiedTimestamp": resource.Modified.Time,
})
if err != nil {
return nil, err
}
if val.Value() == true {
filteredResources = append(filteredResources, resource)
}
}
if len(filteredResources) == 0 {
return nil, fmt.Errorf("No such Resources found with filter %v!", celCmd)
}
return filteredResources, nil
}

View file

@ -2,11 +2,16 @@ package resource
import (
"context"
"encoding/json"
"fmt"
"strconv"
"strings"
"time"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/helper"
"github.com/pterm/pterm"
"github.com/spf13/cobra"
)
@ -18,10 +23,24 @@ var ResourceGetCmd = &cobra.Command{
RunE: ResourceGet,
}
// ResourcePermissionCmd Gets Permissions for Passbolt Resource
var ResourcePermissionCmd = &cobra.Command{
Use: "permission",
Short: "Gets Permissions for a Passbolt Resource",
Long: `Gets Permissions for a Passbolt Resource`,
Aliases: []string{"permissions"},
RunE: ResourcePermission,
}
func init() {
ResourceGetCmd.Flags().String("id", "", "id of Resource to Get")
ResourceGetCmd.MarkFlagRequired("id")
ResourceGetCmd.AddCommand(ResourcePermissionCmd)
ResourcePermissionCmd.Flags().String("id", "", "id of Resource to Get")
ResourcePermissionCmd.Flags().StringArrayP("column", "c", []string{"ID", "Aco", "AcoForeignKey", "Aro", "AroForeignKey", "Type"}, "Columns to return, possible Columns:\nID, Aco, AcoForeignKey, Aro, AroForeignKey, Type, CreatedTimestamp, ModifiedTimestamp")
}
func ResourceGet(cmd *cobra.Command, args []string) error {
@ -29,6 +48,10 @@ func ResourceGet(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -47,11 +70,114 @@ func ResourceGet(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("Getting Resource: %w", err)
}
fmt.Printf("FolderParentID: %v\n", folderParentID)
fmt.Printf("Name: %v\n", shellescape.StripUnsafe(name))
fmt.Printf("Username: %v\n", shellescape.StripUnsafe(username))
fmt.Printf("URI: %v\n", shellescape.StripUnsafe(uri))
fmt.Printf("Password: %v\n", shellescape.StripUnsafe(password))
fmt.Printf("Description: %v\n", shellescape.StripUnsafe(description))
if jsonOutput {
jsonResource, err := json.MarshalIndent(ResourceJsonOutput{
FolderParentID: &folderParentID,
Name: &name,
Username: &username,
URI: &uri,
Password: &password,
Description: &description,
}, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonResource))
} else {
fmt.Printf("FolderParentID: %v\n", folderParentID)
fmt.Printf("Name: %v\n", shellescape.StripUnsafe(name))
fmt.Printf("Username: %v\n", shellescape.StripUnsafe(username))
fmt.Printf("URI: %v\n", shellescape.StripUnsafe(uri))
fmt.Printf("Password: %v\n", shellescape.StripUnsafe(password))
fmt.Printf("Description: %v\n", shellescape.StripUnsafe(description))
}
return nil
}
func ResourcePermission(cmd *cobra.Command, args []string) error {
resource, err := cmd.Flags().GetString("id")
if err != nil {
return err
}
columns, err := cmd.Flags().GetStringArray("column")
if err != nil {
return err
}
if len(columns) == 0 {
return fmt.Errorf("You need to specify atleast one column to return")
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
client, err := util.GetClient(ctx)
if err != nil {
return err
}
defer client.Logout(context.TODO())
cmd.SilenceUsage = true
permissions, err := client.GetResourcePermissions(ctx, resource)
if err != nil {
return fmt.Errorf("Listing Permission: %w", err)
}
if jsonOutput {
outputPermissions := []PermissionJsonOutput{}
for i := range permissions {
outputPermissions = append(outputPermissions, PermissionJsonOutput{
ID: &permissions[i].ID,
Aco: &permissions[i].ACO,
AcoForeignKey: &permissions[i].ACOForeignKey,
Aro: &permissions[i].ARO,
AroForeignKey: &permissions[i].AROForeignKey,
Type: &permissions[i].Type,
CreatedTimestamp: &permissions[i].Created.Time,
ModifiedTimestamp: &permissions[i].Modified.Time,
})
}
jsonPermissions, err := json.MarshalIndent(outputPermissions, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonPermissions))
} else {
data := pterm.TableData{columns}
for _, permission := range permissions {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = permission.ID
case "aco":
entry[i] = permission.ACO
case "acoforeignkey":
entry[i] = permission.ACOForeignKey
case "aro":
entry[i] = permission.ARO
case "aroforeignkey":
entry[i] = permission.AROForeignKey
case "type":
entry[i] = strconv.Itoa(permission.Type)
case "createdtimestamp":
entry[i] = permission.Created.Format(time.RFC3339)
case "modifiedtimestamp":
entry[i] = permission.Modified.Format(time.RFC3339)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
return nil
}

26
resource/json.go Normal file
View file

@ -0,0 +1,26 @@
package resource
import "time"
type ResourceJsonOutput struct {
ID *string `json:"id,omitempty"`
FolderParentID *string `json:"folder_parent_id,omitempty"`
Name *string `json:"name,omitempty"`
Username *string `json:"username,omitempty"`
URI *string `json:"uri,omitempty"`
Password *string `json:"password,omitempty"`
Description *string `json:"description,omitempty"`
CreatedTimestamp *time.Time `json:"created_timestamp,omitempty"`
ModifiedTimestamp *time.Time `json:"modified_timestamp,omitempty"`
}
type PermissionJsonOutput struct {
ID *string `json:"id,omitempty"`
Aco *string `json:"aco,omitempty"`
AcoForeignKey *string `json:"aco_foreign_key,omitempty"`
Aro *string `json:"aro,omitempty"`
AroForeignKey *string `json:"aro_foreign_key,omitempty"`
Type *int `json:"type,omitempty"`
CreatedTimestamp *time.Time `json:"created_timestamp,omitempty"`
ModifiedTimestamp *time.Time `json:"modified_timestamp,omitempty"`
}

View file

@ -2,10 +2,12 @@ package resource
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/passbolt/go-passbolt/helper"
@ -26,11 +28,9 @@ var ResourceListCmd = &cobra.Command{
func init() {
ResourceListCmd.Flags().Bool("favorite", false, "Resources that are marked as favorite")
ResourceListCmd.Flags().Bool("own", false, "Resources that are owned by me")
ResourceListCmd.Flags().StringP("group", "g", "", "Resources that are shared with group")
ResourceListCmd.Flags().StringArrayP("folder", "f", []string{}, "Resources that are in folder")
ResourceListCmd.Flags().StringArrayP("column", "c", []string{"ID", "FolderParentID", "Name", "Username", "URI"}, "Columns to return, possible Columns:\nID, FolderParentID, Name, Username, URI, Password, Description")
ResourceListCmd.Flags().StringArrayP("column", "c", []string{"ID", "FolderParentID", "Name", "Username", "URI"}, "Columns to return, possible Columns:\nID, FolderParentID, Name, Username, URI, Password, Description, CreatedTimestamp, ModifiedTimestamp")
}
func ResourceList(cmd *cobra.Command, args []string) error {
@ -57,6 +57,14 @@ func ResourceList(cmd *cobra.Command, args []string) error {
if len(columns) == 0 {
return fmt.Errorf("You need to specify atleast one column to return")
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
celFilter, err := cmd.Flags().GetString("filter")
if err != nil {
return err
}
ctx := util.GetContext()
@ -77,42 +85,77 @@ func ResourceList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Listing Resource: %w", err)
}
data := pterm.TableData{columns}
for _, resource := range resources {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = resource.ID
case "folderparentid":
entry[i] = resource.FolderParentID
case "name":
entry[i] = shellescape.StripUnsafe(resource.Name)
case "username":
entry[i] = shellescape.StripUnsafe(resource.Username)
case "uri":
entry[i] = shellescape.StripUnsafe(resource.URI)
case "password":
_, _, _, _, pass, _, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
entry[i] = shellescape.StripUnsafe(pass)
case "description":
_, _, _, _, _, desc, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
entry[i] = shellescape.StripUnsafe(desc)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
resources, err = filterResources(&resources, celFilter, ctx, client)
if err != nil {
return err
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
if jsonOutput {
outputResources := []ResourceJsonOutput{}
for i := range resources {
_, _, _, _, pass, desc, err := helper.GetResource(ctx, client, resources[i].ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
outputResources = append(outputResources, ResourceJsonOutput{
ID: &resources[i].ID,
FolderParentID: &resources[i].FolderParentID,
Name: &resources[i].Name,
Username: &resources[i].Username,
URI: &resources[i].URI,
Password: &pass,
Description: &desc,
CreatedTimestamp: &resources[i].Created.Time,
ModifiedTimestamp: &resources[i].Modified.Time,
})
}
jsonResources, err := json.MarshalIndent(outputResources, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonResources))
} else {
data := pterm.TableData{columns}
for _, resource := range resources {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = resource.ID
case "folderparentid":
entry[i] = resource.FolderParentID
case "name":
entry[i] = shellescape.StripUnsafe(resource.Name)
case "username":
entry[i] = shellescape.StripUnsafe(resource.Username)
case "uri":
entry[i] = shellescape.StripUnsafe(resource.URI)
case "password":
_, _, _, _, pass, _, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
entry[i] = shellescape.StripUnsafe(pass)
case "description":
_, _, _, _, _, desc, err := helper.GetResource(ctx, client, resource.ID)
if err != nil {
return fmt.Errorf("Get Resource %w", err)
}
entry[i] = shellescape.StripUnsafe(desc)
case "createdtimestamp":
entry[i] = resource.Created.Format(time.RFC3339)
case "modifiedtimestamp":
entry[i] = resource.Modified.Format(time.RFC3339)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
return nil
}

View file

@ -2,6 +2,7 @@ package user
import (
"context"
"encoding/json"
"fmt"
"github.com/passbolt/go-passbolt-cli/util"
@ -45,6 +46,11 @@ func UserCreate(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
client, err := util.GetClient(ctx)
@ -66,6 +72,18 @@ func UserCreate(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Creating User: %w", err)
}
fmt.Printf("UserID: %v\n", id)
if jsonOutput {
jsonId, err := json.MarshalIndent(
map[string]string{"id": id},
"",
" ",
)
if err != nil {
return fmt.Errorf("Marshalling Json: %w", err)
}
fmt.Println(string(jsonId))
} else {
fmt.Printf("UserID: %v\n", id)
}
return nil
}

60
user/filter.go Normal file
View file

@ -0,0 +1,60 @@
package user
import (
"context"
"fmt"
"github.com/google/cel-go/cel"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
)
// Environments for CEl
var celEnvOptions = []cel.EnvOption{
cel.Variable("ID", cel.StringType),
cel.Variable("Username", cel.StringType),
cel.Variable("FirstName", cel.StringType),
cel.Variable("LastName", cel.StringType),
cel.Variable("Role", cel.StringType),
cel.Variable("CreatedTimestamp", cel.TimestampType),
cel.Variable("ModifiedTimestamp", cel.TimestampType),
}
// Filters the slice users by invoke CEL program for each user
func filterUsers(users *[]api.User, celCmd string, ctx context.Context) ([]api.User, error) {
if celCmd == "" {
return *users, nil
}
program, err := util.InitCELProgram(celCmd, celEnvOptions...)
if err != nil {
return nil, err
}
filteredUsers := []api.User{}
for _, user := range *users {
val, _, err := (*program).ContextEval(ctx, map[string]any{
"ID": user.ID,
"Username": user.Username,
"FirstName": user.Profile.FirstName,
"LastName": user.Profile.LastName,
"Role": user.Role.Name,
"CreatedTimestamp": user.Created.Time,
"ModifiedTimestamp": user.Modified.Time,
})
if err != nil {
return nil, err
}
if val.Value() == true {
filteredUsers = append(filteredUsers, user)
}
}
if len(filteredUsers) == 0 {
return nil, fmt.Errorf("No such users found with filter %v!", celCmd)
}
return filteredUsers, nil
}

View file

@ -2,9 +2,10 @@ package user
import (
"context"
"encoding/json"
"fmt"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/helper"
"github.com/spf13/cobra"
@ -29,6 +30,10 @@ func UserGet(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
ctx := util.GetContext()
@ -47,10 +52,22 @@ func UserGet(cmd *cobra.Command, args []string) error {
if err != nil {
return fmt.Errorf("Getting User: %w", err)
}
fmt.Printf("Username: %v\n", shellescape.StripUnsafe(username))
fmt.Printf("FirstName: %v\n", shellescape.StripUnsafe(firstname))
fmt.Printf("LastName: %v\n", shellescape.StripUnsafe(lastname))
fmt.Printf("Role: %v\n", shellescape.StripUnsafe(role))
if jsonOutput {
jsonUser, err := json.MarshalIndent(UserJsonOutput{
Username: &username,
FirstName: &firstname,
LastName: &lastname,
Role: &role,
}, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonUser))
} else {
fmt.Printf("Username: %v\n", shellescape.StripUnsafe(username))
fmt.Printf("FirstName: %v\n", shellescape.StripUnsafe(firstname))
fmt.Printf("LastName: %v\n", shellescape.StripUnsafe(lastname))
fmt.Printf("Role: %v\n", shellescape.StripUnsafe(role))
}
return nil
}

13
user/json.go Normal file
View file

@ -0,0 +1,13 @@
package user
import "time"
type UserJsonOutput struct {
ID *string `json:"id,omitempty"`
Username *string `json:"username,omitempty"`
FirstName *string `json:"first_name,omitempty"`
LastName *string `json:"last_name,omitempty"`
Role *string `json:"role,omitempty"`
CreatedTimestamp *time.Time `json:"created_timestamp,omitempty"`
ModifiedTimestamp *time.Time `json:"modified_timestamp,omitempty"`
}

View file

@ -2,10 +2,12 @@ package user
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/alessio/shellescape"
"al.essio.dev/pkg/shellescape"
"github.com/passbolt/go-passbolt-cli/util"
"github.com/passbolt/go-passbolt/api"
"github.com/spf13/cobra"
@ -29,7 +31,7 @@ func init() {
UserListCmd.Flags().StringP("search", "s", "", "Search for Users")
UserListCmd.Flags().BoolP("admin", "a", false, "Only show Admins")
UserListCmd.Flags().StringArrayP("column", "c", []string{"ID", "Username", "FirstName", "LastName", "Role"}, "Columns to return, possible Columns:\nID, Username, FirstName, LastName, Role")
UserListCmd.Flags().StringArrayP("column", "c", []string{"ID", "Username", "FirstName", "LastName", "Role"}, "Columns to return, possible Columns:\nID, Username, FirstName, LastName, Role, CreatedTimestamp, ModifiedTimestamp")
}
func UserList(cmd *cobra.Command, args []string) error {
@ -56,6 +58,14 @@ func UserList(cmd *cobra.Command, args []string) error {
if len(columns) == 0 {
return fmt.Errorf("You need to specify atleast one column to return")
}
jsonOutput, err := cmd.Flags().GetBool("json")
if err != nil {
return err
}
celFilter, err := cmd.Flags().GetString("filter")
if err != nil {
return err
}
ctx := util.GetContext()
@ -76,30 +86,59 @@ func UserList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("Listing User: %w", err)
}
data := pterm.TableData{columns}
for _, user := range users {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = user.ID
case "username":
entry[i] = shellescape.StripUnsafe(user.Username)
case "firstname":
entry[i] = shellescape.StripUnsafe(user.Profile.FirstName)
case "lastname":
entry[i] = shellescape.StripUnsafe(user.Profile.LastName)
case "role":
entry[i] = shellescape.StripUnsafe(user.Role.Name)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
users, err = filterUsers(&users, celFilter, ctx)
if err != nil {
return err
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
if jsonOutput {
outputUsers := []UserJsonOutput{}
for i := range users {
outputUsers = append(outputUsers, UserJsonOutput{
ID: &users[i].ID,
Username: &users[i].Username,
FirstName: &users[i].Profile.FirstName,
LastName: &users[i].Profile.LastName,
Role: &users[i].Role.Name,
CreatedTimestamp: &users[i].Created.Time,
ModifiedTimestamp: &users[i].Modified.Time,
})
}
jsonUsers, err := json.MarshalIndent(outputUsers, "", " ")
if err != nil {
return err
}
fmt.Println(string(jsonUsers))
} else {
data := pterm.TableData{columns}
for _, user := range users {
entry := make([]string, len(columns))
for i := range columns {
switch strings.ToLower(columns[i]) {
case "id":
entry[i] = user.ID
case "username":
entry[i] = shellescape.StripUnsafe(user.Username)
case "firstname":
entry[i] = shellescape.StripUnsafe(user.Profile.FirstName)
case "lastname":
entry[i] = shellescape.StripUnsafe(user.Profile.LastName)
case "role":
entry[i] = shellescape.StripUnsafe(user.Role.Name)
case "createdtimestamp":
entry[i] = user.Created.Format(time.RFC3339)
case "modifiedtimestamp":
entry[i] = user.Modified.Format(time.RFC3339)
default:
cmd.SilenceUsage = false
return fmt.Errorf("Unknown Column: %v", columns[i])
}
}
data = append(data, entry)
}
pterm.DefaultTable.WithHasHeader().WithData(data).Render()
}
return nil
}

23
util/cel.go Normal file
View file

@ -0,0 +1,23 @@
package util
import "github.com/google/cel-go/cel"
// InitCELProgram - Initialize a CEL program with given CEL command and a set of environments
func InitCELProgram(celCmd string, options ...cel.EnvOption) (*cel.Program, error) {
env, err := cel.NewEnv(options...)
if err != nil {
return nil, err
}
ast, issue := env.Compile(celCmd)
if issue.Err() != nil {
return nil, issue.Err()
}
program, err := env.Program(ast)
if err != nil {
return nil, err
}
return &program, nil
}

View file

@ -1,12 +1,15 @@
package util
import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"syscall"
"os"
"strings"
"time"
"github.com/passbolt/go-passbolt/api"
"github.com/passbolt/go-passbolt/helper"
@ -14,6 +17,30 @@ import (
"golang.org/x/term"
)
// ReadPassword reads a Password interactively or via Pipe
func ReadPassword(prompt string) (string, error) {
fd := int(os.Stdin.Fd())
var pass string
if term.IsTerminal(fd) {
fmt.Fprint(os.Stderr, prompt);
inputPass, err := term.ReadPassword(fd)
if err != nil {
return "", err
}
pass = string(inputPass)
} else {
reader := bufio.NewReader(os.Stdin)
s, err := reader.ReadString('\n')
if err != nil {
return "", err
}
pass = s
}
return strings.Replace(pass, "\n", "", 1), nil
}
// GetClient gets a Logged in Passbolt Client
func GetClient(ctx context.Context) (*api.Client, error) {
serverAddress := viper.GetString("serverAddress")
@ -28,17 +55,21 @@ func GetClient(ctx context.Context) (*api.Client, error) {
userPassword := viper.GetString("userPassword")
if userPassword == "" {
fmt.Print("Enter Password:")
bytepw, err := term.ReadPassword(int(syscall.Stdin))
cliPassword, err := ReadPassword("Enter Password:")
if err != nil {
fmt.Println()
return nil, fmt.Errorf("Reading Password: %w", err)
}
userPassword = string(bytepw)
userPassword = cliPassword
fmt.Println()
}
client, err := api.NewClient(nil, "", serverAddress, userPrivateKey, userPassword)
httpClient, err := GetHttpClient()
if err != nil {
return nil, err
}
client, err := api.NewClient(httpClient, "", serverAddress, userPrivateKey, userPassword)
if err != nil {
return nil, fmt.Errorf("Creating Client: %w", err)
}
@ -58,32 +89,30 @@ func GetClient(ctx context.Context) (*api.Client, error) {
switch viper.GetString("mfaMode") {
case "interactive-totp":
client.MFACallback = func(ctx context.Context, c *api.Client, res *api.APIResponse) (http.Cookie, error) {
challange := api.MFAChallange{}
err := json.Unmarshal(res.Body, &challange)
challenge := api.MFAChallenge{}
err := json.Unmarshal(res.Body, &challenge)
if err != nil {
return http.Cookie{}, fmt.Errorf("Parsing MFA Challange")
return http.Cookie{}, fmt.Errorf("Parsing MFA Challenge")
}
if challange.Provider.TOTP == "" {
if challenge.Provider.TOTP == "" {
return http.Cookie{}, fmt.Errorf("Server Provided no TOTP Provider")
}
for i := 0; i < 3; i++ {
var code string
fmt.Print("Enter TOTP:")
bytepw, err := term.ReadPassword(int(syscall.Stdin))
code, err := ReadPassword("Enter TOTP:")
if err != nil {
fmt.Printf("\n")
return http.Cookie{}, fmt.Errorf("Reading TOTP: %w", err)
}
code = string(bytepw)
fmt.Printf("\n")
req := api.MFAChallangeResponse{
req := api.MFAChallengeResponse{
TOTP: code,
}
var raw *http.Response
raw, _, err = c.DoCustomRequestAndReturnRawResponse(ctx, "POST", "mfa/verify/totp.json", "v2", req, nil)
if err != nil {
if errors.Unwrap(err) != api.ErrAPIResponseErrorStatusCode {
return http.Cookie{}, fmt.Errorf("Doing MFA Challange Response: %w", err)
return http.Cookie{}, fmt.Errorf("Doing MFA Challenge Response: %w", err)
}
fmt.Println("TOTP Verification Failed")
} else {
@ -96,10 +125,21 @@ func GetClient(ctx context.Context) (*api.Client, error) {
return http.Cookie{}, fmt.Errorf("Unable to find Passbolt MFA Cookie")
}
}
return http.Cookie{}, fmt.Errorf("Failed MFA Challange 3 times: %w", err)
return http.Cookie{}, fmt.Errorf("Failed MFA Challenge 3 times: %w", err)
}
case "noninteractive-totp":
helper.AddMFACallbackTOTP(client, viper.GetUint("mfaRetrys"), viper.GetDuration("mfaDelay"), viper.GetDuration("totpOffset"), viper.GetString("totpToken"))
// if new flag is unset, use old flag instead
totpToken := viper.GetString("mfaTotpToken")
if totpToken == "" {
totpToken = viper.GetString("totpToken")
}
totpOffset := viper.GetDuration("mfaTotpOffset")
if totpOffset == time.Duration(0) {
totpOffset = viper.GetDuration("totpOffset")
}
helper.AddMFACallbackTOTP(client, viper.GetUint("mfaRetrys"), viper.GetDuration("mfaDelay"), totpOffset, totpToken)
case "none":
default:
}

44
util/http.go Normal file
View file

@ -0,0 +1,44 @@
package util
import (
"crypto/tls"
"fmt"
"net/http"
"github.com/spf13/viper"
)
func GetClientCertificate() (tls.Certificate, error) {
cert := viper.GetString("tlsClientCert")
certExists := cert != ""
key := viper.GetString("tlsClientPrivateKey")
keyExists := key != ""
if !certExists && !keyExists {
return tls.Certificate{}, nil
}
if certExists && !keyExists {
return tls.Certificate{}, fmt.Errorf("Client TLS private key is empty, but client TLS cert was set.")
}
if !certExists && keyExists {
return tls.Certificate{}, fmt.Errorf("Client TLS cert is empty, but client TLS private key was set.")
}
return tls.X509KeyPair([]byte(cert), []byte(key))
}
func GetHttpClient() (*http.Client, error) {
tlsSkipVerify := viper.GetBool("tlsSkipVerify")
cert, err := GetClientCertificate()
if err != nil {
return nil, err
}
httpClient := http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
InsecureSkipVerify: tlsSkipVerify,
},
},
}
return &httpClient, nil
}

41
version.go Normal file
View file

@ -0,0 +1,41 @@
package main
import (
"runtime/debug"
"time"
)
var (
version = "unknown"
commit = "unknown"
date = "unknown"
dirty = false
)
func init() {
// if not set by goreleaser, use buildinfo instead
if version == "unknown" {
info, ok := debug.ReadBuildInfo()
if !ok {
return
}
if info.Main.Version != "" {
version = info.Main.Version
}
for _, kv := range info.Settings {
if kv.Value == "" {
continue
}
switch kv.Key {
case "vcs.revision":
commit = kv.Value
case "vcs.time":
d, _ := time.Parse(time.RFC3339, kv.Value)
date = d.String()
case "vcs.modified":
dirty = kv.Value == "true"
}
}
}
}