diff --git a/cmd/list.go b/cmd/list.go index 90124b2..7e94658 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -19,6 +19,12 @@ 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) diff --git a/folder/filter.go b/folder/filter.go new file mode 100644 index 0000000..afcd433 --- /dev/null +++ b/folder/filter.go @@ -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 +} diff --git a/folder/list.go b/folder/list.go index be92a52..6bea8f1 100644 --- a/folder/list.go +++ b/folder/list.go @@ -51,6 +51,10 @@ func FolderList(cmd *cobra.Command, args []string) error { if err != nil { return err } + celFilter, err := cmd.Flags().GetString("filter") + if err != nil { + return err + } ctx := util.GetContext() cmd.SilenceUsage = true @@ -69,6 +73,11 @@ func FolderList(cmd *cobra.Command, args []string) error { return fmt.Errorf("Listing Folder: %w", err) } + folders, err = filterFolders(&folders, celFilter, ctx) + if err != nil { + return err + } + if jsonOutput { outputFolders := []FolderJsonOutput{} for i := range folders { diff --git a/go.mod b/go.mod index d1da2e7..fc5efc2 100644 --- a/go.mod +++ b/go.mod @@ -20,10 +20,12 @@ require ( github.com/ProtonMail/gopenpgp/v2 v2.5.0 // indirect github.com/aead/argon2 v0.0.0-20180111183520-a87724528b07 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect + github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/cloudflare/circl v1.3.1 // indirect github.com/containerd/console v1.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect + github.com/google/cel-go v0.13.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/uuid v1.3.0 // indirect github.com/gookit/color v1.5.2 // indirect @@ -42,11 +44,14 @@ require ( github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/subosito/gotenv v1.4.1 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.4.0 // indirect golang.org/x/sys v0.3.0 // indirect golang.org/x/text v0.5.0 // indirect + google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect + google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index c950058..613842b 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmH github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 h1:yL7+Jz0jTC6yykIK/Wh74gnTJnrGr5AyrNMXuA0gves= +github.com/antlr/antlr4/runtime/Go/antlr v1.4.10/go.mod h1:F7bn7fEU90QkQ3tnmaTx3LTKLEDqnwWODIYppRQ5hnY= github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -123,8 +125,11 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU= +github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -134,6 +139,7 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -239,6 +245,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -550,6 +558,8 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo= +google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -576,6 +586,9 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= diff --git a/group/filter.go b/group/filter.go new file mode 100644 index 0000000..0b85d67 --- /dev/null +++ b/group/filter.go @@ -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 +} diff --git a/group/list.go b/group/list.go index 6faa9c5..96afe52 100644 --- a/group/list.go +++ b/group/list.go @@ -51,6 +51,10 @@ func GroupList(cmd *cobra.Command, args []string) error { if err != nil { return err } + celFilter, err := cmd.Flags().GetString("filter") + if err != nil { + return err + } ctx := util.GetContext() @@ -69,6 +73,11 @@ func GroupList(cmd *cobra.Command, args []string) error { return fmt.Errorf("Listing Group: %w", err) } + groups, err = filterGroups(&groups, celFilter, ctx) + if err != nil { + return err + } + if jsonOutput { outputGroups := []GroupJsonOutput{} for i := range groups { diff --git a/resource/filter.go b/resource/filter.go new file mode 100644 index 0000000..2cc1f31 --- /dev/null +++ b/resource/filter.go @@ -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 +} diff --git a/resource/list.go b/resource/list.go index ad1d9ac..a283d56 100644 --- a/resource/list.go +++ b/resource/list.go @@ -28,10 +28,8 @@ 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, CreatedTimestamp, ModifiedTimestamp") } @@ -63,6 +61,10 @@ func ResourceList(cmd *cobra.Command, args []string) error { if err != nil { return err } + celFilter, err := cmd.Flags().GetString("filter") + if err != nil { + return err + } ctx := util.GetContext() @@ -83,6 +85,11 @@ func ResourceList(cmd *cobra.Command, args []string) error { return fmt.Errorf("Listing Resource: %w", err) } + resources, err = filterResources(&resources, celFilter, ctx, client) + if err != nil { + return err + } + if jsonOutput { outputResources := []ResourceJsonOutput{} for i := range resources { diff --git a/user/filter.go b/user/filter.go new file mode 100644 index 0000000..973fce7 --- /dev/null +++ b/user/filter.go @@ -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 +} diff --git a/user/list.go b/user/list.go index 32ff273..c1f3eb1 100644 --- a/user/list.go +++ b/user/list.go @@ -62,6 +62,10 @@ func UserList(cmd *cobra.Command, args []string) error { if err != nil { return err } + celFilter, err := cmd.Flags().GetString("filter") + if err != nil { + return err + } ctx := util.GetContext() @@ -82,6 +86,11 @@ func UserList(cmd *cobra.Command, args []string) error { return fmt.Errorf("Listing User: %w", err) } + users, err = filterUsers(&users, celFilter, ctx) + if err != nil { + return err + } + if jsonOutput { outputUsers := []UserJsonOutput{} for i := range users { diff --git a/util/cel.go b/util/cel.go new file mode 100644 index 0000000..1e41f95 --- /dev/null +++ b/util/cel.go @@ -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 +}