Skip to content

Commit 8f099d4

Browse files
authored
Merge pull request #92 from passbolt/v5
V5 Support
2 parents 9c2c25a + f572b7b commit 8f099d4

File tree

8 files changed

+278
-231
lines changed

8 files changed

+278
-231
lines changed

go.mod

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,55 @@ go 1.23.0
55
toolchain go1.23.6
66

77
require (
8-
al.essio.dev/pkg/shellescape v1.5.1
9-
github.com/google/cel-go v0.24.1
10-
github.com/passbolt/go-passbolt v0.7.2
11-
github.com/pterm/pterm v0.12.80
8+
al.essio.dev/pkg/shellescape v1.6.0
9+
github.com/google/cel-go v0.26.0
10+
github.com/passbolt/go-passbolt v0.7.3-0.20250818130824-58240d4bc18c
11+
github.com/pterm/pterm v0.12.81
1212
github.com/spf13/cobra v1.9.1
13-
github.com/spf13/viper v1.19.0
13+
github.com/spf13/viper v1.20.1
1414
github.com/tobischo/gokeepasslib/v3 v3.6.1
15-
golang.org/x/term v0.29.0
15+
golang.org/x/term v0.34.0
1616
)
1717

1818
require (
1919
atomicgo.dev/cursor v0.2.0 // indirect
2020
atomicgo.dev/keyboard v0.2.9 // indirect
2121
atomicgo.dev/schedule v0.1.0 // indirect
22-
cel.dev/expr v0.21.2 // indirect
23-
github.com/ProtonMail/go-crypto v1.1.6 // indirect
24-
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f // indirect
25-
github.com/ProtonMail/gopenpgp/v2 v2.8.3 // indirect
22+
cel.dev/expr v0.24.0 // indirect
23+
github.com/ProtonMail/go-crypto v1.3.0 // indirect
24+
github.com/ProtonMail/gopenpgp/v3 v3.3.0 // indirect
2625
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
27-
github.com/cloudflare/circl v1.6.0 // indirect
28-
github.com/containerd/console v1.0.4 // indirect
29-
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
30-
github.com/fsnotify/fsnotify v1.8.0 // indirect
26+
github.com/cloudflare/circl v1.6.1 // indirect
27+
github.com/containerd/console v1.0.5 // indirect
28+
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
29+
github.com/fsnotify/fsnotify v1.9.0 // indirect
30+
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
3131
github.com/google/go-querystring v1.1.0 // indirect
3232
github.com/google/uuid v1.6.0 // indirect
3333
github.com/gookit/color v1.5.4 // indirect
34-
github.com/hashicorp/hcl v1.0.0 // indirect
3534
github.com/inconshreveable/mousetrap v1.1.0 // indirect
3635
github.com/lithammer/fuzzysearch v1.1.8 // indirect
37-
github.com/magiconair/properties v1.8.9 // indirect
3836
github.com/mattn/go-runewidth v0.0.16 // indirect
39-
github.com/mitchellh/mapstructure v1.5.0 // indirect
40-
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
41-
github.com/pkg/errors v0.9.1 // indirect
37+
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
4238
github.com/rivo/uniseg v0.4.7 // indirect
4339
github.com/russross/blackfriday/v2 v2.1.0 // indirect
44-
github.com/sagikazarmark/locafero v0.7.0 // indirect
45-
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
46-
github.com/santhosh-tekuri/jsonschema v1.2.4 // indirect
47-
github.com/sourcegraph/conc v0.3.0 // indirect
48-
github.com/spf13/afero v1.12.0 // indirect
49-
github.com/spf13/cast v1.7.1 // indirect
50-
github.com/spf13/pflag v1.0.6 // indirect
51-
github.com/stoewer/go-strcase v1.3.0 // indirect
40+
github.com/sagikazarmark/locafero v0.10.0 // indirect
41+
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
42+
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
43+
github.com/spf13/afero v1.14.0 // indirect
44+
github.com/spf13/cast v1.9.2 // indirect
45+
github.com/spf13/pflag v1.0.7 // indirect
46+
github.com/stoewer/go-strcase v1.3.1 // indirect
5247
github.com/subosito/gotenv v1.6.0 // indirect
5348
github.com/tobischo/argon2 v0.1.0 // indirect
5449
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
55-
go.uber.org/multierr v1.11.0 // indirect
56-
golang.org/x/crypto v0.35.0 // indirect
57-
golang.org/x/exp v0.0.0-20250228200357-dead58393ab7 // indirect
58-
golang.org/x/sys v0.30.0 // indirect
59-
golang.org/x/text v0.22.0 // indirect
60-
google.golang.org/genproto/googleapis/api v0.0.0-20250303144028-a0af3efb3deb // indirect
61-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250303144028-a0af3efb3deb // indirect
62-
google.golang.org/protobuf v1.36.5 // indirect
63-
gopkg.in/ini.v1 v1.67.0 // indirect
50+
golang.org/x/crypto v0.41.0 // indirect
51+
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
52+
golang.org/x/sys v0.35.0 // indirect
53+
golang.org/x/text v0.28.0 // indirect
54+
google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect
55+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
56+
google.golang.org/protobuf v1.36.6 // indirect
6457
gopkg.in/yaml.v3 v3.0.1 // indirect
6558
)
6659

go.sum

Lines changed: 65 additions & 142 deletions
Large diffs are not rendered by default.

keepass/export.go

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -130,43 +130,62 @@ func KeepassExport(cmd *cobra.Command, args []string) error {
130130
}
131131

132132
func getKeepassEntry(client *api.Client, resource api.Resource, secret api.Secret, rType api.ResourceType) (*gokeepasslib.Entry, error) {
133-
_, _, _, _, pass, desc, err := helper.GetResourceFromData(client, resource, resource.Secrets[0], resource.ResourceType)
133+
_, name, username, uri, pass, desc, err := helper.GetResourceFromData(client, resource, resource.Secrets[0], resource.ResourceType)
134134
if err != nil {
135135
return nil, fmt.Errorf("Get Resource %v: %w", resource.ID, err)
136136
}
137137

138138
entry := gokeepasslib.NewEntry()
139139
entry.Values = append(
140140
entry.Values,
141-
gokeepasslib.ValueData{Key: "Title", Value: gokeepasslib.V{Content: resource.Name}},
142-
gokeepasslib.ValueData{Key: "UserName", Value: gokeepasslib.V{Content: resource.Username}},
143-
gokeepasslib.ValueData{Key: "URL", Value: gokeepasslib.V{Content: resource.URI}},
141+
gokeepasslib.ValueData{Key: "Title", Value: gokeepasslib.V{Content: name}},
142+
gokeepasslib.ValueData{Key: "UserName", Value: gokeepasslib.V{Content: username}},
143+
gokeepasslib.ValueData{Key: "URL", Value: gokeepasslib.V{Content: uri}},
144144
gokeepasslib.ValueData{Key: "Password", Value: gokeepasslib.V{Content: pass, Protected: w.NewBoolWrapper(true)}},
145145
gokeepasslib.ValueData{Key: "Notes", Value: gokeepasslib.V{Content: desc}},
146146
)
147147

148-
if resource.ResourceType.Slug == "password-description-totp" || resource.ResourceType.Slug == "totp" {
148+
if resource.ResourceType.Slug == "password-description-totp" || resource.ResourceType.Slug == "totp" || resource.ResourceType.Slug == "v5-default-with-totp" || resource.ResourceType.Slug == "v5-totp-standalone" {
149149
var totpData api.SecretDataTOTP
150150

151151
rawSecretData, err := client.DecryptMessage(resource.Secrets[0].Data)
152152
if err != nil {
153153
return nil, fmt.Errorf("Decrypting Secret Data: %w", err)
154154
}
155155

156-
if resource.ResourceType.Slug == "password-description-totp" {
156+
switch resource.ResourceType.Slug {
157+
case "password-description-totp":
157158
var secretData api.SecretDataTypePasswordDescriptionTOTP
158159
err = json.Unmarshal([]byte(rawSecretData), &secretData)
159160
if err != nil {
160161
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
161162
}
162163
totpData = secretData.TOTP
163-
} else {
164+
break
165+
case "totp":
164166
var secretData api.SecretDataTypeTOTP
165167
err = json.Unmarshal([]byte(rawSecretData), &secretData)
166168
if err != nil {
167169
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
168170
}
169171
totpData = secretData.TOTP
172+
break
173+
case "v5-default-with-totp":
174+
var secretData api.SecretDataTypeV5DefaultWithTOTP
175+
err = json.Unmarshal([]byte(rawSecretData), &secretData)
176+
if err != nil {
177+
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
178+
}
179+
totpData = secretData.TOTP
180+
break
181+
case "v5-totp-standalone":
182+
var secretData api.SecretDataTypeV5TOTPStandalone
183+
err = json.Unmarshal([]byte(rawSecretData), &secretData)
184+
if err != nil {
185+
return nil, fmt.Errorf("Parsing Decrypted Secret Data: %w", err)
186+
}
187+
totpData = secretData.TOTP
188+
break
170189
}
171190

172191
v := url.Values{}
@@ -175,16 +194,16 @@ func getKeepassEntry(client *api.Client, resource api.Resource, secret api.Secre
175194
v.Set("algorithm", totpData.Algorithm)
176195
v.Set("digits", fmt.Sprint(totpData.Digits))
177196

178-
issuer := resource.URI
179-
if resource.URI == "" {
180-
issuer = resource.Name
197+
issuer := uri
198+
if uri == "" {
199+
issuer = name
181200

182201
}
183202
v.Set("issuer", issuer)
184203

185-
accountName := resource.Username
186-
if resource.Username == "" {
187-
accountName = resource.Name
204+
accountName := username
205+
if username == "" {
206+
accountName = name
188207
}
189208

190209
u := url.URL{

resource/create.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func init() {
2525
ResourceCreateCmd.Flags().StringP("password", "p", "", "Resource Password")
2626
ResourceCreateCmd.Flags().StringP("description", "d", "", "Resource Description")
2727
ResourceCreateCmd.Flags().StringP("folderParentID", "f", "", "Folder in which to create the Resource")
28-
28+
ResourceCreateCmd.Flags().String("expiry", "", "Expiry as RFC3339 (e.g. 2025-12-31T23:59:59Z) or Go duration (e.g. 48h, 30m)")
2929
ResourceCreateCmd.MarkFlagRequired("name")
3030
ResourceCreateCmd.MarkFlagRequired("password")
3131
}
@@ -55,6 +55,12 @@ func ResourceCreate(cmd *cobra.Command, args []string) error {
5555
if err != nil {
5656
return err
5757
}
58+
59+
expiry, err := cmd.Flags().GetString("expiry")
60+
if err != nil {
61+
return err
62+
}
63+
5864
jsonOutput, err := cmd.Flags().GetBool("json")
5965
if err != nil {
6066
return err
@@ -83,6 +89,13 @@ func ResourceCreate(cmd *cobra.Command, args []string) error {
8389
return fmt.Errorf("Creating Resource: %w", err)
8490
}
8591

92+
// TODO, Should be done by go-passbolt when the "new" Resource API is done
93+
if expiry != "" {
94+
if err := SetResourceExpiry(ctx, client, id, expiry); err != nil {
95+
return err
96+
}
97+
}
98+
8699
if jsonOutput {
87100
jsonId, err := json.MarshalIndent(
88101
map[string]string{"id": id},

resource/expiry.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
package resource
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"regexp"
7+
"strings"
8+
"time"
9+
10+
"github.com/passbolt/go-passbolt/api"
11+
)
12+
13+
// SetResourceExpiry updates only the expiry date of a resource.
14+
func SetResourceExpiry(ctx context.Context, client *api.Client, id string, expiryInput string) error {
15+
if expiryInput == "" {
16+
return nil
17+
}
18+
19+
// Safety: ensure the resource id is a UUID to avoid unsafe URL construction
20+
if !isUUID(id) {
21+
return fmt.Errorf("invalid resource id: %q", id)
22+
}
23+
24+
// allow a single keyword to clear expiry (no TrimSpace: flags shouldn't need quoting spaces)
25+
switch strings.ToLower(expiryInput) {
26+
case "none":
27+
// TODO: Should be handled in go-passbolt when the planned new Resource API is available
28+
_, _, err := client.DoCustomRequestAndReturnRawResponse(
29+
ctx,
30+
"PUT",
31+
fmt.Sprintf("resources/%s.json", id),
32+
"v2",
33+
map[string]*string{"expired": nil},
34+
nil,
35+
)
36+
if err != nil {
37+
return fmt.Errorf("Clearing expiry: %w", err)
38+
}
39+
return nil
40+
}
41+
42+
isoExpiry, err := ParseExpiry(expiryInput)
43+
if err != nil {
44+
return err
45+
}
46+
// TODO: Should be handled in go-passbolt when the planned new Resource API is available
47+
_, _, err = client.DoCustomRequestAndReturnRawResponse(
48+
ctx,
49+
"PUT",
50+
fmt.Sprintf("resources/%s.json", id),
51+
"v2",
52+
map[string]string{"expired": isoExpiry},
53+
nil,
54+
)
55+
if err != nil {
56+
return fmt.Errorf("Setting expiry: %w", err)
57+
}
58+
return nil
59+
}
60+
61+
// ParseExpiry accepts either an absolute time (ISO8601/RFC3339) or a human duration like "7d", "12h", "30m", or combinations like "1w2d3h".
62+
// It returns an ISO8601 (RFC3339) timestamp string in UTC suitable for the API.
63+
func ParseExpiry(input string) (string, error) {
64+
if input == "" {
65+
return "", nil
66+
}
67+
// Try absolute timestamp first
68+
if t, err := tryParseAbsoluteTime(input); err == nil {
69+
return t.UTC().Format(time.RFC3339), nil
70+
}
71+
// Fallback to human duration(s)
72+
d, err := time.ParseDuration(input)
73+
if err != nil {
74+
return "", fmt.Errorf("invalid expiry value %q: %w", input, err)
75+
}
76+
return time.Now().UTC().Add(d).Format(time.RFC3339), nil
77+
}
78+
79+
func tryParseAbsoluteTime(s string) (time.Time, error) {
80+
// Try RFC3339 variants only (avoid nonstandard timestamp formats)
81+
layouts := []string{
82+
time.RFC3339,
83+
time.RFC3339Nano,
84+
}
85+
var lastErr error
86+
for _, layout := range layouts {
87+
if t, err := time.Parse(layout, s); err == nil {
88+
return t, nil
89+
} else {
90+
lastErr = err
91+
}
92+
}
93+
return time.Time{}, lastErr
94+
}
95+
96+
// isUUID performs a basic UUID validation in canonical 8-4-4-4-12 hex format.
97+
func isUUID(s string) bool {
98+
re := regexp.MustCompile(`(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
99+
return re.MatchString(s)
100+
}

resource/filter.go

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import (
55
"fmt"
66

77
"github.com/google/cel-go/cel"
8-
"github.com/google/cel-go/common/types"
9-
"github.com/google/cel-go/common/types/ref"
108
"github.com/passbolt/go-passbolt-cli/util"
119
"github.com/passbolt/go-passbolt/api"
1210
"github.com/passbolt/go-passbolt/helper"
@@ -38,28 +36,20 @@ func filterResources(resources *[]api.Resource, celCmd string, ctx context.Conte
3836

3937
filteredResources := []api.Resource{}
4038
for _, resource := range *resources {
39+
// TODO We should decrypt the secret only when required for performance reasonse
40+
_, name, username, uri, pass, desc, err := helper.GetResource(ctx, client, resource.ID)
41+
if err != nil {
42+
return nil, fmt.Errorf("Get Resource %w", err)
43+
}
44+
4145
val, _, err := (*program).ContextEval(ctx, map[string]any{
42-
"Id": resource.ID,
43-
"FolderParentID": resource.FolderParentID,
44-
"Name": resource.Name,
45-
"Username": resource.Username,
46-
"URI": resource.URI,
47-
"Password": func() ref.Val {
48-
_, _, _, _, pass, _, err := helper.GetResource(ctx, client, resource.ID)
49-
if err != nil {
50-
fmt.Printf("Get Resource %v", err)
51-
return types.String("")
52-
}
53-
return types.String(pass)
54-
},
55-
"Description": func() ref.Val {
56-
_, _, _, _, _, descr, err := helper.GetResource(ctx, client, resource.ID)
57-
if err != nil {
58-
fmt.Printf("Get Resource %v", err)
59-
return types.String("")
60-
}
61-
return types.String(descr)
62-
},
46+
"Id": resource.ID,
47+
"FolderParentID": resource.FolderParentID,
48+
"Name": name,
49+
"Username": username,
50+
"URI": uri,
51+
"Password": pass,
52+
"Description": desc,
6353
"CreatedTimestamp": resource.Created.Time,
6454
"ModifiedTimestamp": resource.Modified.Time,
6555
})

0 commit comments

Comments
 (0)