diff --git a/cmd/securityprofiles/export.go b/cmd/securityprofiles/export.go new file mode 100644 index 000000000..c0e4c9556 --- /dev/null +++ b/cmd/securityprofiles/export.go @@ -0,0 +1,58 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package securityprofiles + +import ( + "internal/apiclient" + "os" + + "internal/client/securityprofiles" + + "github.com/spf13/cobra" +) + +// ExpCmd to export sec profiles +var ExpCmd = &cobra.Command{ + Use: "export", + Short: "Export Security Profiles to a file", + Long: "Export Security Profiles to a file", + Args: func(cmd *cobra.Command, args []string) (err error) { + return apiclient.SetApigeeOrg(org) + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + if folder == "" { + folder, _ = os.Getwd() + } + if err = apiclient.FolderExists(folder); err != nil { + return err + } + return securityprofiles.Export(conn, folder, allRevisions) + }, +} + +var ( + conn int + folder string + allRevisions bool +) + +func init() { + ExpCmd.Flags().IntVarP(&conn, "conn", "c", + 4, "Number of connections") + ExpCmd.Flags().StringVarP(&folder, "folder", "f", + "", "folder to export API proxy bundles") + ExpCmd.Flags().BoolVarP(&allRevisions, "all", "", + false, "Export all proxy revisions") +} diff --git a/cmd/securityprofiles/get.go b/cmd/securityprofiles/get.go index 4b3fc4cd1..93a23a78b 100644 --- a/cmd/securityprofiles/get.go +++ b/cmd/securityprofiles/get.go @@ -30,7 +30,7 @@ var GetCmd = &cobra.Command{ return apiclient.SetApigeeOrg(org) }, RunE: func(cmd *cobra.Command, args []string) (err error) { - _, err = securityprofiles.Get(name) + _, err = securityprofiles.Get(name, revision) return }, } @@ -40,5 +40,7 @@ var name string func init() { GetCmd.Flags().StringVarP(&name, "name", "n", "", "Name of the security profile") + GetCmd.Flags().StringVarP(&revision, "revision", "r", + "", "Revision of the security profile") _ = GetCmd.MarkFlagRequired("name") } diff --git a/cmd/securityprofiles/securityprofiles.go b/cmd/securityprofiles/securityprofiles.go index f0d358d9f..99477f05e 100644 --- a/cmd/securityprofiles/securityprofiles.go +++ b/cmd/securityprofiles/securityprofiles.go @@ -39,6 +39,8 @@ func init() { Cmd.AddCommand(DetachCmd) Cmd.AddCommand(CreateCmd) Cmd.AddCommand(ListRevisionsCmd) + Cmd.AddCommand(UpdateCmd) + Cmd.AddCommand(ExpCmd) _ = Cmd.MarkFlagRequired("org") } diff --git a/cmd/securityprofiles/update.go b/cmd/securityprofiles/update.go new file mode 100644 index 000000000..ac1a3cd0a --- /dev/null +++ b/cmd/securityprofiles/update.go @@ -0,0 +1,50 @@ +// Copyright 2023 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package securityprofiles + +import ( + "internal/apiclient" + "internal/client/securityprofiles" + + "github.com/apigee/apigeecli/cmd/utils" + "github.com/spf13/cobra" +) + +// UpdateCmd to update a security profile +var UpdateCmd = &cobra.Command{ + Use: "update", + Short: "Update an existing Security Profile", + Long: "Update an existing Security Profile", + Args: func(cmd *cobra.Command, args []string) (err error) { + return apiclient.SetApigeeOrg(org) + }, + RunE: func(cmd *cobra.Command, args []string) (err error) { + content, err := utils.ReadFile(securityActionFile) + if err != nil { + return err + } + _, err = securityprofiles.Update(name, content) + return + }, +} + +func init() { + UpdateCmd.Flags().StringVarP(&name, "name", "n", + "", "Name of the security profile") + UpdateCmd.Flags().StringVarP(&securityActionFile, "file", "f", + "", "Path to a file containing Security Profile content") + _ = UpdateCmd.MarkFlagRequired("name") + _ = UpdateCmd.MarkFlagRequired("file") +} diff --git a/internal/client/securityprofiles/securityprofiles.go b/internal/client/securityprofiles/securityprofiles.go index 03290cfba..6f080ea45 100644 --- a/internal/client/securityprofiles/securityprofiles.go +++ b/internal/client/securityprofiles/securityprofiles.go @@ -16,20 +16,30 @@ package securityprofiles import ( "encoding/json" + "errors" + "fmt" "net/url" "path" "strconv" "strings" + "sync" "internal/apiclient" + "internal/clilog" ) +type secprofiles struct { + SecurityProfiles []secprofile `json:"securityProfiles,omitempty"` + NextPageToken string `json:"nextPageToken,omitempty"` +} + type secprofile struct { Name string `json:"name"` DisplayName string `json:"displayName"` Description string `json:"description,omitempty"` + RevisionID string `json:"revisionId,omitempty"` ProfileConfig profileConfig `json:"profileConfig"` - ScoreConfigs []scoreConfig `json:"scoreConfigs,omitempty"` + ScoreConfigs []scoreConfig `json:"scoringConfigs,omitempty"` } type profileConfig struct { @@ -51,6 +61,8 @@ type category struct { Cors interface{} `json:"cors,omitempty"` } +const maxPageSize = 50 + // Create func Create(name string, content []byte) (respBody []byte, err error) { sc := secprofile{} @@ -96,14 +108,18 @@ func Delete(name string) (respBody []byte, err error) { } // Get -func Get(name string) (respBody []byte, err error) { +func Get(name string, revision string) (respBody []byte, err error) { u, _ := url.Parse(apiclient.BaseURL) - u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "securityProfiles", name) + if revision != "" { + u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "securityProfiles", name+"@"+revision) + } else { + u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "securityProfiles", name) + } respBody, err = apiclient.HttpClient(u.String()) return respBody, err } -// ListVersions +// ListRevisions func ListRevisions(name string) (respBody []byte, err error) { u, _ := url.Parse(apiclient.BaseURL) u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "securityProfiles", name+":listRevisions") @@ -127,3 +143,128 @@ func List(pageSize int, pageToken string) (respBody []byte, err error) { respBody, err = apiclient.HttpClient(u.String()) return respBody, err } + +// Update +func Update(name string, content []byte) (respBody []byte, err error) { + sc := secprofile{} + if err = json.Unmarshal(content, &sc); err != nil { + return nil, err + } + u, _ := url.Parse(apiclient.BaseURL) + u.Path = path.Join(u.Path, apiclient.GetApigeeOrg(), "securityProfiles", name) + q := u.Query() + q.Set("updateMask", "description,profileConfig") + u.RawQuery = q.Encode() + + respBody, err = apiclient.HttpClient(u.String(), string(content), "PATCH") + return respBody, err +} + +// Export +func Export(conn int, folder string, allRevisions bool) (err error) { + apiclient.ClientPrintHttpResponse.Set(false) + defer apiclient.ClientPrintHttpResponse.Set(apiclient.GetCmdPrintHttpResponseSetting()) + + pageToken := "" + listsecprofiles := secprofiles{} + + for { + l := secprofiles{} + listRespBytes, err := List(maxPageSize, pageToken) + if err != nil { + return fmt.Errorf("failed to fetch security profiles: %w", err) + } + err = json.Unmarshal(listRespBytes, &l) + if err != nil { + return fmt.Errorf("failed to unmarshall: %w", err) + } + listsecprofiles.SecurityProfiles = append(listsecprofiles.SecurityProfiles, l.SecurityProfiles...) + if l.NextPageToken == "" { + break + } + } + + errChan := make(chan error) + workChan := make(chan secprofile, len(listsecprofiles.SecurityProfiles)) + + fanOutWg := sync.WaitGroup{} + fanInWg := sync.WaitGroup{} + + errs := []string{} + fanInWg.Add(1) + + go func() { + defer fanInWg.Done() + for { + newErr, ok := <-errChan + if !ok { + return + } + errs = append(errs, newErr.Error()) + } + }() + + for i := 0; i < conn; i++ { + fanOutWg.Add(1) + go exportWorker(&fanOutWg, workChan, folder, allRevisions, errChan) + } + + for _, i := range listsecprofiles.SecurityProfiles { + workChan <- i + } + + close(workChan) + fanOutWg.Wait() + close(errChan) + fanInWg.Wait() + + if len(errs) > 0 { + return errors.New(strings.Join(errs, "\n")) + } + + return nil +} + +func exportWorker(wg *sync.WaitGroup, workCh <-chan secprofile, folder string, allRevisions bool, errs chan<- error) { + defer wg.Done() + for { + var respBody []byte + var err error + + listsecprofiles := secprofiles{} + + work, ok := <-workCh + if !ok { + return + } + + if allRevisions { + securityProfileName := work.Name[strings.LastIndex(work.Name, "/")+1:] + clilog.Info.Printf("Exporting all the revisions for Security Profile %s\n", securityProfileName) + + if respBody, err = ListRevisions(securityProfileName); err != nil { + errs <- err + } + err = json.Unmarshal(respBody, &listsecprofiles) + if err != nil { + errs <- err + } + for _, s := range listsecprofiles.SecurityProfiles { + payload, err := json.Marshal(s) + if err != nil { + errs <- err + } + payload, _ = apiclient.PrettifyJSON(payload) + apiclient.WriteByteArrayToFile(path.Join(folder, s.Name), false, payload) + } + } else { + clilog.Info.Printf("Exporting Security Profile %s\n", work.Name) + payload, err := json.Marshal(work) + if err != nil { + errs <- err + } + payload, _ = apiclient.PrettifyJSON(payload) + apiclient.WriteByteArrayToFile(path.Join(folder, work.Name+".json"), false, payload) + } + } +}