diff --git a/README.md b/README.md index 4f7e251..744b814 100644 --- a/README.md +++ b/README.md @@ -15,25 +15,33 @@ The following steps should get you a working gmin installation - 3. Create a service account, create and download a JSON key for the service account and enable G Suite domain-wide delegation. 4. In the G Suite admin console of the domain with which you want to use gmin, go to Security > API Permissions > Manage Domain-wide Delegation and add a new record (or update an existing one). The scopes that you will need to add to the record for full functionality are as follows - ``` -Readonly scopes are needed for get and list functions. The other more permissive scopes are needed for create, delete, update -and undelete functions. +Readonly scopes are needed for get and list functions. The other more permissive scopes are needed for +create, delete, update and undelete functions. https://www.googleapis.com/auth/admin.directory.group https://www.googleapis.com/auth/admin.directory.group.member.readonly https://www.googleapis.com/auth/admin.directory.orgunit https://www.googleapis.com/auth/admin.directory.orgunit.readonly https://www.googleapis.com/auth/admin.directory.user +https://www.googleapis.com/auth/admin.directory.user.alias +https://www.googleapis.com/auth/admin.directory.user.alias.readonly https://www.googleapis.com/auth/admin.directory.user.readonly https://www.googleapis.com/auth/admin.directory.group.readonly https://www.googleapis.com/auth/admin.directory.group.member ``` -5. Copy/move the gmin binary to a convenient directory/folder and rename the JSON key file, downloaded earlier, to gmin_credentials and place in the same directory/folder as the gmin binary. -6. Run the command `gmin init` and enter the required information. The email address of the admin whose privileges will be used is mandatory. Customer ID and the path where you would like the config file, .gmin.yaml, to be written are optional. By default the config file is written to the current user's home directory and the string 'my_customer' is used for the customer ID. If you choose a different installation path for the config file then that path will need to be given with each gmin command by using the --config flag. +5. Copy/move the gmin binary to a convenient directory/folder and rename the JSON key file, downloaded earlier, to 'gmin_credentials'. Place in a directory/folder suitable for your environment. +6. Run the command `gmin init` and enter the required information. + +* Email address of the admin whose privileges will be used (mandatory). +* Path where config file, .gmin.yaml, will be written. Default is current user's home directory. If you choose a different installation path to the default for the config file then that path will need to be given with each gmin command by using the --config flag. +* Path where service account credentials json file is stored. File must be named 'gmin_credentials'. Default is current user's home directory. +* Customer ID. Default is 'my_customer'. + 7. To see the version number of your gmin binary, run the command `gmin -v` or `gmin --version`. 8. To get help from gmin itself, enter `gmin -h` or `gmin --help` and go from there. -If you already use GAM (https://github.com/jay0lee/GAM) then you will already have a service account and JSON credentials file. In this case you could use the same service account by copying the GAM credentials.json, rename the copy to gmin_credentials and place it in the same directory/folder as the gmin executable. +If you already use GAM (https://github.com/jay0lee/GAM) then you will already have a service account and JSON credentials file. In this case you could use the same service account by copying the GAM credentials.json, rename the copy to gmin_credentials and place it in the folder/directory that you set for the credentials path when running `gmin init`. ## Usage Commands usually take the form of `gmin `. Although there may be the odd exception like `gmin whoami`. @@ -139,11 +147,11 @@ https://developers.google.com/admin-sdk/directory/v1/get-start/getting-started i * Maybe there are other people who might benefit from gmin ## Project Status -gmin is pretty young which means that it is liable to rapid change, although the command syntax is unlikely to change much (if at all) over time. The functionality is currently limited to the Admin SDK API and the user, group, group member and organisation unit objects but additional functionality will be added when it is ready. +gmin is pretty young which means that it is liable to rapid change, although the command syntax is unlikely to change much (if at all) over time. The functionality is currently limited to the Admin SDK API and the group, group alias, group member, organisation unit, user and user alias objects but additional functionality will be added frequently when it is ready. All output is in JSON format apart from informational and error messages. Output in other formats such as CSV is on the roadmap, however, I have found the use of the jq utility (https://stedolan.github.io/jq/) can be a great help in working with JSON. -I will be publishing a roadmap and welcome any suggestions as to the most important features to add. A Wiki is on the list of tasks on the upcoming roadmap. +I have published a [development roadmap](https://github.com/plusworx/gmin/wiki/Development-Roadmap) and welcome any suggestions as to the most important features to add. A [wiki](https://github.com/plusworx/gmin/wiki) has also been started which contains the roadmap. ## Community diff --git a/cmd/create_group_alias.go b/cmd/create_group_alias.go new file mode 100644 index 0000000..ccd890b --- /dev/null +++ b/cmd/create_group_alias.go @@ -0,0 +1,78 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "errors" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var createGroupAliasCmd = &cobra.Command{ + Use: "group-alias -g ", + Aliases: []string{"galias", "ga"}, + Args: cobra.ExactArgs(1), + Short: "Creates a group alias", + Long: `Creates a group alias. + + Examples: gmin create group-alias group.alias@mycompany.com -g finance@mycompany.com + gmin crt ga group.alias@mycompany.com -g sales@mycompany.com`, + RunE: doCreateGroupAlias, +} + +func doCreateGroupAlias(cmd *cobra.Command, args []string) error { + var alias *admin.Alias + + alias = new(admin.Alias) + + alias.Alias = args[0] + + if group == "" { + err := errors.New("gmin: error - group must be provided") + return err + } + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryGroupScope) + if err != nil { + return err + } + + gaic := ds.Groups.Aliases.Insert(group, alias) + newAlias, err := gaic.Do() + if err != nil { + return err + } + + fmt.Println("**** group alias " + newAlias.Alias + " created for group " + group + " ****") + + return nil +} + +func init() { + createCmd.AddCommand(createGroupAliasCmd) + + createGroupAliasCmd.Flags().StringVarP(&group, "group", "g", "", "email address or id of group") +} diff --git a/cmd/create_test.go b/cmd/create_test.go index 7eaf316..551a7c9 100644 --- a/cmd/create_test.go +++ b/cmd/create_test.go @@ -26,6 +26,29 @@ import ( "testing" ) +func TestDoCreateGroupAlias(t *testing.T) { + cases := []struct { + args []string + expectedErr string + group string + }{ + { + args: []string{"group.alias@mycompany.org"}, + expectedErr: "gmin: error - group must be provided", + }, + } + + for _, c := range cases { + group = c.group + + got := doCreateGroupAlias(createGroupAliasCmd, c.args) + + if got.Error() != c.expectedErr { + t.Errorf("Expected error %v, got %v", c.expectedErr, got.Error()) + } + } +} + func TestDoCreateUser(t *testing.T) { cases := []struct { args []string @@ -109,3 +132,26 @@ func TestDoCreateMember(t *testing.T) { } } } + +func TestDoCreateUserAlias(t *testing.T) { + cases := []struct { + args []string + expectedErr string + userKey string + }{ + { + args: []string{"my.alias@mycompany.org"}, + expectedErr: "gmin: error - user must be provided", + }, + } + + for _, c := range cases { + userKey = c.userKey + + got := doCreateUserAlias(createUserAliasCmd, c.args) + + if got.Error() != c.expectedErr { + t.Errorf("Expected error %v, got %v", c.expectedErr, got.Error()) + } + } +} diff --git a/cmd/create_user_alias.go b/cmd/create_user_alias.go new file mode 100644 index 0000000..26cc3a7 --- /dev/null +++ b/cmd/create_user_alias.go @@ -0,0 +1,78 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "errors" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var createUserAliasCmd = &cobra.Command{ + Use: "user-alias -u ", + Aliases: []string{"ualias", "ua"}, + Args: cobra.ExactArgs(1), + Short: "Creates a user alias", + Long: `Creates a user alias. + + Examples: gmin create user-alias my.alias@mycompany.com -u brian.cox@mycompany.com + gmin crt ua my.alias@mycompany.com -u brian.cox@mycompany.com`, + RunE: doCreateUserAlias, +} + +func doCreateUserAlias(cmd *cobra.Command, args []string) error { + var alias *admin.Alias + + alias = new(admin.Alias) + + alias.Alias = args[0] + + if userKey == "" { + err := errors.New("gmin: error - user must be provided") + return err + } + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryUserAliasScope) + if err != nil { + return err + } + + uaic := ds.Users.Aliases.Insert(userKey, alias) + newAlias, err := uaic.Do() + if err != nil { + return err + } + + fmt.Println("**** user alias " + newAlias.Alias + " created for user " + userKey + " ****") + + return nil +} + +func init() { + createCmd.AddCommand(createUserAliasCmd) + + createUserAliasCmd.Flags().StringVarP(&userKey, "user", "u", "", "email address or id of user") +} diff --git a/cmd/delete_group_alias.go b/cmd/delete_group_alias.go new file mode 100644 index 0000000..bd5a177 --- /dev/null +++ b/cmd/delete_group_alias.go @@ -0,0 +1,70 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "errors" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var deleteGroupAliasCmd = &cobra.Command{ + Use: "group-alias -g ", + Aliases: []string{"galias", "ga"}, + Args: cobra.ExactArgs(1), + Short: "Deletes group alias", + Long: `Deletes group alias.`, + RunE: doDeleteGroupAlias, +} + +func doDeleteGroupAlias(cmd *cobra.Command, args []string) error { + if group == "" { + err := errors.New("gmin: error - group email address or id must be provided") + return err + } + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryGroupScope) + if err != nil { + return err + } + + gadc := ds.Groups.Aliases.Delete(group, args[0]) + + err = gadc.Do() + if err != nil { + return err + } + + fmt.Printf("**** gmin: group alias %s for group %s deleted ****\n", args[0], group) + + return nil +} + +func init() { + deleteCmd.AddCommand(deleteGroupAliasCmd) + + deleteGroupAliasCmd.Flags().StringVarP(&group, "group", "g", "", "email address or id of group") +} diff --git a/cmd/delete_test.go b/cmd/delete_test.go index df5e38e..9d32401 100644 --- a/cmd/delete_test.go +++ b/cmd/delete_test.go @@ -26,6 +26,29 @@ import ( "testing" ) +func TestDoDeleteGroupAlias(t *testing.T) { + cases := []struct { + args []string + expectedErr string + group string + }{ + { + args: []string{"group.alias@mycompany.org"}, + expectedErr: "gmin: error - group email address or id must be provided", + }, + } + + for _, c := range cases { + group = c.group + + got := doDeleteGroupAlias(deleteGroupAliasCmd, c.args) + + if got.Error() != c.expectedErr { + t.Errorf("Expected error %v, got %v", c.expectedErr, got.Error()) + } + } +} + func TestDoDeleteMember(t *testing.T) { cases := []struct { args []string @@ -48,3 +71,26 @@ func TestDoDeleteMember(t *testing.T) { } } } + +func TestDoDeleteUserAlias(t *testing.T) { + cases := []struct { + args []string + expectedErr string + userKey string + }{ + { + args: []string{"my.alias@mycompany.org"}, + expectedErr: "gmin: error - user email address or id must be provided", + }, + } + + for _, c := range cases { + userKey = c.userKey + + got := doDeleteUserAlias(deleteUserAliasCmd, c.args) + + if got.Error() != c.expectedErr { + t.Errorf("Expected error %v, got %v", c.expectedErr, got.Error()) + } + } +} diff --git a/cmd/delete_user_alias.go b/cmd/delete_user_alias.go new file mode 100644 index 0000000..c81ad7b --- /dev/null +++ b/cmd/delete_user_alias.go @@ -0,0 +1,70 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "errors" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var deleteUserAliasCmd = &cobra.Command{ + Use: "user-alias -u ", + Aliases: []string{"ualias", "ua"}, + Args: cobra.ExactArgs(1), + Short: "Deletes user alias", + Long: `Deletes user alias.`, + RunE: doDeleteUserAlias, +} + +func doDeleteUserAlias(cmd *cobra.Command, args []string) error { + if userKey == "" { + err := errors.New("gmin: error - user email address or id must be provided") + return err + } + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryUserAliasScope) + if err != nil { + return err + } + + uadc := ds.Users.Aliases.Delete(userKey, args[0]) + + err = uadc.Do() + if err != nil { + return err + } + + fmt.Printf("**** gmin: user alias %s for user %s deleted ****\n", args[0], userKey) + + return nil +} + +func init() { + deleteCmd.AddCommand(deleteUserAliasCmd) + + deleteUserAliasCmd.Flags().StringVarP(&userKey, "user", "u", "", "email address or id of user") +} diff --git a/cmd/init.go b/cmd/init.go index d465211..6a4b23e 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -50,6 +50,7 @@ func askForConfigPath() string { fmt.Print("Please enter a full config file path\n(Press for default value): ") _, err := fmt.Scanln(&response) + if response == "" { return response } @@ -62,12 +63,32 @@ func askForConfigPath() string { return response } +func askForCredentialPath() string { + var response string + + fmt.Print("Please enter a full credentials file path\n(Press for default value): ") + + _, err := fmt.Scanln(&response) + + if response == "" { + return response + } + + if err != nil { + fmt.Println(err) + return askForCredentialPath() + } + + return response +} + func askForCustomerID() string { var response string fmt.Print("Please enter customer ID\n(Press for default value): ") _, err := fmt.Scanln(&response) + if response == "" { return response } @@ -102,13 +123,15 @@ func askForEmail() string { func doInit(cmd *cobra.Command, args []string) error { answers := struct { - AdminEmail string - ConfigPath string - CustomerID string + AdminEmail string + ConfigPath string + CredentialPath string + CustomerID string }{} answers.AdminEmail = askForEmail() answers.ConfigPath = askForConfigPath() + answers.CredentialPath = askForCredentialPath() answers.CustomerID = askForCustomerID() if answers.ConfigPath == "" { @@ -120,13 +143,22 @@ func doInit(cmd *cobra.Command, args []string) error { answers.ConfigPath = hmDir } + if answers.CredentialPath == "" { + hmDir, err := homedir.Dir() + if err != nil { + log.Fatal(err) + } + + answers.CredentialPath = hmDir + } + if answers.CustomerID == "" { answers.CustomerID = "my_customer" } - cfgFile := cfg.File{Administrator: answers.AdminEmail, CustomerID: answers.CustomerID} + cfgFile := cfg.File{Administrator: answers.AdminEmail, CredentialPath: answers.CredentialPath, CustomerID: answers.CustomerID} - path := filepath.Join(filepath.ToSlash(answers.ConfigPath), cfg.FileName) + path := filepath.Join(filepath.ToSlash(answers.ConfigPath), cfg.ConfigFile) cfgYaml, err := yaml.Marshal(&cfgFile) if err != nil { diff --git a/cmd/list_group_aliases.go b/cmd/list_group_aliases.go new file mode 100644 index 0000000..13e9088 --- /dev/null +++ b/cmd/list_group_aliases.go @@ -0,0 +1,87 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + gas "github.com/plusworx/gmin/utils/groupaliases" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var listGroupAliasesCmd = &cobra.Command{ + Use: "group-aliases ", + Aliases: []string{"group-alias", "galiases", "galias", "gas", "ga"}, + Args: cobra.ExactArgs(1), + Short: "Outputs a list of group aliases", + Long: `Outputs a list of group aliases.`, + RunE: doListGroupAliases, +} + +func doListGroupAliases(cmd *cobra.Command, args []string) error { + var ( + formattedAttrs string + aliases *admin.Aliases + validAttrs []string + ) + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryGroupReadonlyScope) + if err != nil { + return err + } + + galc := ds.Groups.Aliases.List(args[0]) + + if attrs != "" { + validAttrs, err = cmn.ValidateArgs(attrs, gas.GroupAliasAttrMap, cmn.AttrStr) + if err != nil { + return err + } + + formattedAttrs = gas.FormatAttrs(validAttrs) + galc = gas.AddFields(galc, formattedAttrs) + } + + aliases, err = gas.DoList(galc) + if err != nil { + return err + } + + jsonData, err := json.MarshalIndent(aliases, "", " ") + if err != nil { + return err + } + + fmt.Println(string(jsonData)) + + return nil +} + +func init() { + listCmd.AddCommand(listGroupAliasesCmd) + + listGroupAliasesCmd.Flags().StringVarP(&attrs, "attributes", "a", "", "required group alias attributes (separated by ~)") +} diff --git a/cmd/list_user_aliases.go b/cmd/list_user_aliases.go new file mode 100644 index 0000000..57ff86e --- /dev/null +++ b/cmd/list_user_aliases.go @@ -0,0 +1,87 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "encoding/json" + "fmt" + + cmn "github.com/plusworx/gmin/utils/common" + uas "github.com/plusworx/gmin/utils/useraliases" + "github.com/spf13/cobra" + admin "google.golang.org/api/admin/directory/v1" +) + +var listUserAliasesCmd = &cobra.Command{ + Use: "user-aliases ", + Aliases: []string{"user-alias", "ualiases", "ualias", "uas", "ua"}, + Args: cobra.ExactArgs(1), + Short: "Outputs a list of user aliases", + Long: `Outputs a list of user aliases.`, + RunE: doListUserAliases, +} + +func doListUserAliases(cmd *cobra.Command, args []string) error { + var ( + formattedAttrs string + aliases *admin.Aliases + validAttrs []string + ) + + ds, err := cmn.CreateDirectoryService(admin.AdminDirectoryUserAliasReadonlyScope) + if err != nil { + return err + } + + ualc := ds.Users.Aliases.List(args[0]) + + if attrs != "" { + validAttrs, err = cmn.ValidateArgs(attrs, uas.UserAliasAttrMap, cmn.AttrStr) + if err != nil { + return err + } + + formattedAttrs = uas.FormatAttrs(validAttrs) + ualc = uas.AddFields(ualc, formattedAttrs) + } + + aliases, err = uas.DoList(ualc) + if err != nil { + return err + } + + jsonData, err := json.MarshalIndent(aliases, "", " ") + if err != nil { + return err + } + + fmt.Println(string(jsonData)) + + return nil +} + +func init() { + listCmd.AddCommand(listUserAliasesCmd) + + listUserAliasesCmd.Flags().StringVarP(&attrs, "attributes", "a", "", "required user alias attributes (separated by ~)") +} diff --git a/cmd/root.go b/cmd/root.go index 2ccee24..14af924 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -31,11 +31,14 @@ import ( ) var ( + adminEmail string attrs string archived bool blockInherit bool cfgFile string + credentialPath string changePassword bool + customerID string deleted bool deliverySetting string domain string @@ -78,7 +81,7 @@ var rootCmd = &cobra.Command{ Long: `gmin is a commandline interface (CLI) that enables the administration of G Suite domains. It aims to provide tools that make G Suite administration from the commandline more manageable.`, - Version: "v0.3.1", + Version: "v0.4.0", } // Execute adds all child commands to the root command and sets flags appropriately. diff --git a/cmd/set.go b/cmd/set.go new file mode 100644 index 0000000..c424ad2 --- /dev/null +++ b/cmd/set.go @@ -0,0 +1,79 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package cmd + +import ( + "fmt" + + valid "github.com/asaskevich/govalidator" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var setCmd = &cobra.Command{ + Use: "set", + Short: "Sets gmin configuration information", + RunE: doSet, +} + +func doSet(cmd *cobra.Command, args []string) error { + if customerID != "" { + viper.Set("customerid", customerID) + err := viper.WriteConfig() + if err != nil { + return err + } + fmt.Printf("**** gmin: customer id set to %v ****\n", customerID) + } + + if adminEmail != "" { + ok := valid.IsEmail(adminEmail) + if !ok { + return fmt.Errorf("gmin: error - %v is not a valid email address", adminEmail) + } + viper.Set("administrator", adminEmail) + err := viper.WriteConfig() + if err != nil { + return err + } + fmt.Printf("**** gmin: administrator set to %v ****\n", adminEmail) + } + + if credentialPath != "" { + viper.Set("credentialpath", credentialPath) + err := viper.WriteConfig() + if err != nil { + return err + } + fmt.Printf("**** gmin: service account credential path set to %v ****\n", credentialPath) + } + return nil +} + +func init() { + rootCmd.AddCommand(setCmd) + + setCmd.Flags().StringVarP(&adminEmail, "admin", "a", "", "administrator email address") + setCmd.Flags().StringVarP(&customerID, "customerid", "c", "", "customer id for domain") + setCmd.Flags().StringVarP(&credentialPath, "credentialpath", "p", "", "service account credential file path") +} diff --git a/go.mod b/go.mod index 397dd4e..8e34230 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,6 @@ require ( github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d - google.golang.org/api v0.28.0 + google.golang.org/api v0.29.0 gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index e20761f..2632a26 100644 --- a/go.sum +++ b/go.sum @@ -412,6 +412,8 @@ google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.28.0 h1:jMF5hhVfMkTZwHW1SDpKq5CkgWLXOb31Foaca9Zr3oM= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0 h1:BaiDisFir8O4IJxvAabCGGkQ6yCJegNQqSVoYUNAnbk= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/utils/common/common.go b/utils/common/common.go index 8036789..a77bdfb 100644 --- a/utils/common/common.go +++ b/utils/common/common.go @@ -27,6 +27,7 @@ import ( "encoding/hex" "fmt" "io/ioutil" + "path/filepath" "strings" "crypto/sha1" @@ -63,10 +64,15 @@ func CreateDirectoryService(scope ...string) (*admin.Service, error) { return nil, err } - var ServiceAccountFilePath = cfg.CredentialsFile + credentialPath, err := cfg.ReadConfigString("credentialpath") + if err != nil { + return nil, err + } ctx := context.Background() + ServiceAccountFilePath := filepath.Join(filepath.ToSlash(credentialPath), cfg.CredentialFile) + jsonCredentials, err := ioutil.ReadFile(ServiceAccountFilePath) if err != nil { return nil, err diff --git a/utils/config/config.go b/utils/config/config.go index f4d6aef..c5707d8 100644 --- a/utils/config/config.go +++ b/utils/config/config.go @@ -29,16 +29,17 @@ import ( ) const ( - // CredentialsFile holds service account credentials - CredentialsFile string = "gmin_credentials" - // FileName is configuration file name - FileName string = ".gmin.yaml" + // CredentialFile holds service account credentials + CredentialFile string = "gmin_credentials" + // ConfigFile is configuration file name + ConfigFile string = ".gmin.yaml" ) // File holds configuration data type File struct { - Administrator string `yaml:"administrator"` - CustomerID string `yaml:"customerid"` + Administrator string `yaml:"administrator"` + CredentialPath string `yaml:"credentialpath"` + CustomerID string `yaml:"customerid"` } // ReadConfigString gets a string item from config file diff --git a/utils/groupaliases/groupaliases.go b/utils/groupaliases/groupaliases.go new file mode 100644 index 0000000..2715c93 --- /dev/null +++ b/utils/groupaliases/groupaliases.go @@ -0,0 +1,80 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package groupaliases + +import ( + "strings" + + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" +) + +const ( + endField string = ")" + startAliasesField string = "aliases(" +) + +// GroupAliasAttrMap provides lowercase mappings to valid admin.Alias attributes +var GroupAliasAttrMap = map[string]string{ + "alias": "alias", + "etag": "etag", + "id": "id", + "kind": "kind", + "primaryemail": "primaryEmail", +} + +// AddFields adds Fields to admin calls +func AddFields(galc *admin.GroupsAliasesListCall, attrs string) *admin.GroupsAliasesListCall { + var fields googleapi.Field = googleapi.Field(attrs) + var newGALC *admin.GroupsAliasesListCall + + newGALC = galc.Fields(fields) + + return newGALC +} + +// DoList calls the .Do() function on the admin.GroupsAliasesListCall +func DoList(galc *admin.GroupsAliasesListCall) (*admin.Aliases, error) { + aliases, err := galc.Do() + if err != nil { + return nil, err + } + + return aliases, nil +} + +// FormatAttrs formats attributes for admin.GroupsAliasesListCall.Fields +func FormatAttrs(attrs []string) string { + var ( + outputStr string + aliasFields []string + ) + + for _, a := range attrs { + aliasFields = append(aliasFields, a) + } + + outputStr = startAliasesField + strings.Join(aliasFields, ",") + endField + + return outputStr +} diff --git a/utils/groupaliases/groupaliases_test.go b/utils/groupaliases/groupaliases_test.go new file mode 100644 index 0000000..8772194 --- /dev/null +++ b/utils/groupaliases/groupaliases_test.go @@ -0,0 +1,56 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package groupaliases + +import ( + "testing" + + tsts "github.com/plusworx/gmin/tests" + admin "google.golang.org/api/admin/directory/v1" +) + +func TestAddFields(t *testing.T) { + cases := []struct { + fields string + }{ + { + fields: "alias,id,primaryEmail", + }, + } + + ds, err := tsts.DummyDirectoryService(admin.AdminDirectoryGroupReadonlyScope) + if err != nil { + t.Error("Error: failed to create dummy admin.Service") + } + + galc := ds.Groups.Aliases.List("testgroup@company.org") + + for _, c := range cases { + + newGALC := AddFields(galc, c.fields) + + if newGALC == nil { + t.Error("Error: failed to add Fields to GroupsAliasesListCall") + } + } +} diff --git a/utils/useraliases/useraliases.go b/utils/useraliases/useraliases.go new file mode 100644 index 0000000..88dbb96 --- /dev/null +++ b/utils/useraliases/useraliases.go @@ -0,0 +1,80 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package useraliases + +import ( + "strings" + + admin "google.golang.org/api/admin/directory/v1" + "google.golang.org/api/googleapi" +) + +const ( + endField string = ")" + startAliasesField string = "aliases(" +) + +// UserAliasAttrMap provides lowercase mappings to valid admin.Alias attributes +var UserAliasAttrMap = map[string]string{ + "alias": "alias", + "etag": "etag", + "id": "id", + "kind": "kind", + "primaryemail": "primaryEmail", +} + +// AddFields adds Fields to admin calls +func AddFields(ualc *admin.UsersAliasesListCall, attrs string) *admin.UsersAliasesListCall { + var fields googleapi.Field = googleapi.Field(attrs) + var newUALC *admin.UsersAliasesListCall + + newUALC = ualc.Fields(fields) + + return newUALC +} + +// DoList calls the .Do() function on the admin.UsersAliasesListCall +func DoList(ualc *admin.UsersAliasesListCall) (*admin.Aliases, error) { + aliases, err := ualc.Do() + if err != nil { + return nil, err + } + + return aliases, nil +} + +// FormatAttrs formats attributes for admin.UsersAliasesListCall.Fields +func FormatAttrs(attrs []string) string { + var ( + outputStr string + aliasFields []string + ) + + for _, a := range attrs { + aliasFields = append(aliasFields, a) + } + + outputStr = startAliasesField + strings.Join(aliasFields, ",") + endField + + return outputStr +} diff --git a/utils/useraliases/useraliases_test.go b/utils/useraliases/useraliases_test.go new file mode 100644 index 0000000..81f2983 --- /dev/null +++ b/utils/useraliases/useraliases_test.go @@ -0,0 +1,56 @@ +/* +Copyright © 2020 Chris Duncan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +package useraliases + +import ( + "testing" + + tsts "github.com/plusworx/gmin/tests" + admin "google.golang.org/api/admin/directory/v1" +) + +func TestAddFields(t *testing.T) { + cases := []struct { + fields string + }{ + { + fields: "alias,id,primaryEmail", + }, + } + + ds, err := tsts.DummyDirectoryService(admin.AdminDirectoryUserAliasReadonlyScope) + if err != nil { + t.Error("Error: failed to create dummy admin.Service") + } + + ualc := ds.Users.Aliases.List("test.user@company.org") + + for _, c := range cases { + + newUALC := AddFields(ualc, c.fields) + + if newUALC == nil { + t.Error("Error: failed to add Fields to UsersAliasesListCall") + } + } +}