Skip to content

Commit c53bee5

Browse files
committed
Implement security
1 parent 5b9b01a commit c53bee5

File tree

11 files changed

+179
-15
lines changed

11 files changed

+179
-15
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Usage:
1313
Available Commands:
1414
account Account commands
1515
alias Alias commands
16+
auth Authentication commands
1617
domain Domain commands
1718
help Help about any command
1819
recipient-bcc recipient-bcc commands
@@ -24,6 +25,7 @@ Flags:
2425
-h, --help help for emailctl
2526
2627
Use "emailctl [command] --help" for more information about a command.
28+
2729
```
2830

2931
## Installation
@@ -66,6 +68,20 @@ The above values are the defaults. You can omit options that don't change the de
6668
6769
Below are a few usage examples:
6870
71+
### Authentication
72+
73+
* Log in
74+
75+
```
76+
emailctl auth login admin@example.com
77+
```
78+
79+
* Log out
80+
81+
```
82+
emailctl auth logout
83+
```
84+
6985
### Domain API
7086

7187
* List all domains on your server:

auth.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package emailctl
2+
3+
import (
4+
"github.com/lyubenblagoev/goprsc"
5+
)
6+
7+
// AuthResponse is a wrapper for goprsc.AuthResponse
8+
type AuthResponse struct {
9+
*goprsc.AuthResponse
10+
}
11+
12+
// AuthService handles communication with the authentication API of the Postfix REST Server.
13+
type AuthService service
14+
15+
// Login authenticates the user credential and returnes the tokens provided by the Postfix REST Server.
16+
func (s *AuthService) Login(login, password string) (*AuthResponse, error) {
17+
response, err := s.client.Auth.Login(login, password)
18+
if err != nil {
19+
return nil, err
20+
}
21+
return &AuthResponse{AuthResponse: response}, nil
22+
}
23+
24+
// Logout logs out the user, if the provided login and refreshToken are valid.
25+
func (s *AuthService) Logout(login, refreshToken string) error {
26+
err := s.client.Auth.Logout(login, refreshToken)
27+
return err
28+
}

client.go

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,43 @@ import (
1111
type Client struct {
1212
client *goprsc.Client
1313

14+
Auth *AuthService
1415
Domains *DomainService
1516
Accounts *AccountService
1617
Aliases *AliasService
1718
InputBccs *InputBccService
1819
OutputBccs *OutputBccService
1920
}
2021

22+
// GetLogin returns the user login associated with the client
23+
func (c *Client) GetLogin() string {
24+
return c.client.Login
25+
}
26+
27+
// GetAuthToken returns the authentication token associated with the client
28+
func (c *Client) GetAuthToken() string {
29+
return c.client.AuthToken
30+
}
31+
32+
// GetRefreshToken returns the refresh token associated with the client
33+
func (c *Client) GetRefreshToken() string {
34+
return c.client.RefreshToken
35+
}
36+
2137
type service struct {
2238
client *goprsc.Client
2339
}
2440

2541
// NewClient creates an instance of Client.
2642
func NewClient() (*Client, error) {
27-
goprscClient, err := getGoprscClient()
43+
goprscClient, err := newGoprscClient()
2844
if err != nil {
2945
return nil, fmt.Errorf("unable to initialize Postfix REST Server API client: %s", err)
3046
}
3147

3248
c := &Client{client: goprscClient}
3349
s := service{client: goprscClient} // Reuse the same structure instead of allocating one for each service
50+
c.Auth = (*AuthService)(&s)
3451
c.Domains = (*DomainService)(&s)
3552
c.Accounts = (*AccountService)(&s)
3653
c.Aliases = (*AliasService)(&s)
@@ -40,17 +57,25 @@ func NewClient() (*Client, error) {
4057
return c, nil
4158
}
4259

43-
func getGoprscClient() (*goprsc.Client, error) {
60+
func newGoprscClient() (*goprsc.Client, error) {
4461
host := viper.GetString("host")
4562
port := viper.GetString("port")
4663
useHTTPS := viper.GetBool("https")
4764

65+
login := viper.GetString("login")
66+
authToken := viper.GetString("authToken")
67+
refreshToken := viper.GetString("refreshToken")
68+
4869
var options []goprsc.ClientOption
70+
options = append(options, goprsc.UserAgentOption("emailctl"))
4971
options = append(options, goprsc.HostOption(host))
5072
options = append(options, goprsc.PortOption(port))
5173
if useHTTPS {
5274
options = append(options, goprsc.HTTPSProtocolOption())
5375
}
76+
if len(authToken) > 0 {
77+
options = append(options, goprsc.AuthOption(login, authToken, refreshToken))
78+
}
5479

5580
return goprsc.NewClientWithOptions(nil, options...)
5681
}

commands/account.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func showAccount(client *emailctl.Client, args []string) error {
6464

6565
func addAccount(client *emailctl.Client, args []string) error {
6666
domain, username := args[0], args[1]
67-
password, err := emailctl.ReadPassword()
67+
password, err := emailctl.ReadAndConfirmPassword()
6868
if err != nil {
6969
return err
7070
}
@@ -93,7 +93,7 @@ func renameAccount(client *emailctl.Client, args []string) error {
9393

9494
func changeAccountPassword(client *emailctl.Client, args []string) error {
9595
domain, username := args[0], args[1]
96-
password, err := emailctl.ReadPassword()
96+
password, err := emailctl.ReadAndConfirmPassword()
9797
if err != nil {
9898
return err
9999
}

commands/auth.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/lyubenblagoev/emailctl"
7+
"github.com/spf13/cobra"
8+
"github.com/spf13/viper"
9+
)
10+
11+
// CreateAuthCommand creates an auth command with its subcommands.
12+
func CreateAuthCommand() *Command {
13+
c := &Command{
14+
Command: &cobra.Command{
15+
Use: "auth",
16+
Short: "Authentication commands",
17+
Long: "Auth is used to access authentication commands",
18+
},
19+
}
20+
BuildCommand(c, login, "login <email-address>", "Log in using the given email address", ArgsOption(1), AliasOption("l"))
21+
BuildCommand(c, logout, "logout", "Log in using the given email address", AliasOption("l"))
22+
return c
23+
}
24+
25+
func login(client *emailctl.Client, args []string) error {
26+
login := args[0]
27+
password, err := emailctl.ReadPassword("Password: ")
28+
if err != nil {
29+
return err
30+
}
31+
auth, err := client.Auth.Login(login, password)
32+
if err != nil {
33+
return err
34+
}
35+
fmt.Printf("Logged in successfully.\n")
36+
err = SaveAuth(login, auth.AuthToken, auth.RefreshToken)
37+
return err
38+
}
39+
40+
func logout(client *emailctl.Client, args []string) error {
41+
login := viper.GetString("login")
42+
refreshToken := viper.GetString("refreshToken")
43+
CleanAuth()
44+
return client.Auth.Logout(login, refreshToken)
45+
}
46+
47+
// SaveAuth writes active authentication tokens to the configuration file
48+
func SaveAuth(login, token, refreshToken string) error {
49+
if login != "" && token != "" && refreshToken != "" {
50+
viper.Set("login", login)
51+
viper.Set("authToken", token)
52+
viper.Set("refreshToken", refreshToken)
53+
return viper.WriteConfig()
54+
}
55+
return nil
56+
}
57+
58+
// CleanAuth removes saved authentication tokens
59+
func CleanAuth() {
60+
viper.Set("login", "")
61+
viper.Set("authToken", "")
62+
viper.Set("refreshToken", "")
63+
viper.WriteConfig()
64+
}

commands/command.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,8 @@ func BuildCommand(parent *Command, runner CommandRunner, usage, description stri
2525
Short: description,
2626
Long: description,
2727
Run: func(cmd *cobra.Command, args []string) {
28-
client, err := emailctl.NewClient()
29-
checkErr(err)
3028
checkErr(runner(client, args))
29+
SaveAuth(client.GetLogin(), client.GetAuthToken(), client.GetRefreshToken())
3130
},
3231
}
3332

commands/emailctl.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package commands
22

33
import (
4+
"log"
5+
"os"
6+
47
"github.com/lyubenblagoev/emailctl"
58
"github.com/spf13/cobra"
69
"github.com/spf13/viper"
@@ -9,11 +12,12 @@ import (
912
// EmailctlVersion is emailctl's version.
1013
var EmailctlVersion = emailctl.Version{
1114
Major: 0,
12-
Minor: 1,
15+
Minor: 2,
1316
Patch: 0,
1417
}
1518

1619
var cfgFile string
20+
var client *emailctl.Client
1721

1822
// emailctlCommand represents the base command when called without any subcommands
1923
var emailctlCommand = &Command{
@@ -38,24 +42,39 @@ func init() {
3842
func initConfig() {
3943
if cfgFile != "" {
4044
viper.SetConfigFile(cfgFile)
45+
} else {
46+
viper.SetConfigName(".emailctl")
47+
viper.SetConfigType("yaml")
4148
}
4249

43-
viper.SetConfigName(".emailctl")
44-
viper.AddConfigPath("$HOME")
50+
home, err := os.UserHomeDir()
51+
if err != nil {
52+
log.Fatal("Failed to determine user's home directory.", err)
53+
}
54+
viper.AddConfigPath(home)
4555
viper.AutomaticEnv()
4656

4757
viper.SetDefault("host", "localhost")
4858
viper.SetDefault("port", "8080")
4959
viper.SetDefault("https", false)
5060

5161
checkErr(viper.ReadInConfig())
62+
63+
initClient()
5264
}
5365

5466
func initCommands() {
67+
emailctlCommand.AddCommand(CreateAuthCommand())
5568
emailctlCommand.AddCommand(CreateDomainCommand())
5669
emailctlCommand.AddCommand(CreateAccountCommand())
5770
emailctlCommand.AddCommand(CreateAliasCommand())
5871
emailctlCommand.AddCommand(CreateVersionCommand())
5972
emailctlCommand.AddCommand(CreateSenderBccCommand())
6073
emailctlCommand.AddCommand(CreateRecipientBccCommand())
6174
}
75+
76+
func initClient() {
77+
var err error
78+
client, err = emailctl.NewClient()
79+
checkErr(err)
80+
}

commands/error.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@ package commands
22

33
import (
44
"fmt"
5+
"net/http"
56
"os"
7+
8+
"github.com/lyubenblagoev/goprsc"
69
)
710

811
func checkErr(err error) {
912
if err != nil {
13+
if e, ok := err.(*goprsc.ErrorResponse); ok {
14+
statusCode := e.Response.StatusCode
15+
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
16+
CleanAuth()
17+
}
18+
}
1019
fmt.Println(err.Error())
1120
os.Exit(-1)
1221
}

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/lyubenblagoev/emailctl
33
go 1.15
44

55
require (
6-
github.com/lyubenblagoev/goprsc v0.0.0-20200821145540-97dc088fd222
6+
github.com/lyubenblagoev/goprsc v0.2.0
77
github.com/spf13/cobra v0.0.3
88
github.com/spf13/viper v1.2.1
99
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ github.com/lyubenblagoev/goprsc v0.0.0-20181029073722-795bb40c88dc h1:fzzJn+uM3P
77
github.com/lyubenblagoev/goprsc v0.0.0-20181029073722-795bb40c88dc/go.mod h1:tLux+5WiiwdKkaSKpzqbDzp7sWVxtIFPdUvdRb/U05U=
88
github.com/lyubenblagoev/goprsc v0.0.0-20200821145540-97dc088fd222 h1:Ue7MouIj6amVqR8X2PDfKTO5d5Uq5H/uBlcGiSs1AlU=
99
github.com/lyubenblagoev/goprsc v0.0.0-20200821145540-97dc088fd222/go.mod h1:w1nNEnn97AvUNOlyQ+y7wE48xxAp/yhsQrk0rmufEyc=
10+
github.com/lyubenblagoev/goprsc v0.2.0 h1:rrvFeGNx+OpbAj9eov0V6oDKoLLnL9a24X8tAqxGH5E=
11+
github.com/lyubenblagoev/goprsc v0.2.0/go.mod h1:w1nNEnn97AvUNOlyQ+y7wE48xxAp/yhsQrk0rmufEyc=
1012
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
1113
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
1214
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=

password.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ const (
1313
maxPromptRetries = 3
1414
)
1515

16-
// ReadPassword reads a password and confirmation from the terminal.
17-
func ReadPassword() (string, error) {
16+
// ReadAndConfirmPassword reads a password and confirmation from the terminal.
17+
// Retries three times if the passwords do not match.
18+
func ReadAndConfirmPassword() (string, error) {
1819
for i := 0; i < maxPromptRetries; i++ {
19-
pass, err := readPass("Password: ")
20+
pass, err := ReadPassword("Password: ")
2021
if err != nil {
2122
return "", err
2223
}
2324

24-
confirmPass, err := readPass("Confirm password: ")
25+
confirmPass, err := ReadPassword("Confirm password: ")
2526
if err != nil {
2627
return "", err
2728
}
@@ -39,7 +40,8 @@ func ReadPassword() (string, error) {
3940
return "", errors.New("Passwords don't match")
4041
}
4142

42-
func readPass(prompt string) (string, error) {
43+
// ReadPassword reads a password from the terminal
44+
func ReadPassword(prompt string) (string, error) {
4345
fmt.Print(prompt)
4446
b, err := terminal.ReadPassword(syscall.Stdin)
4547
fmt.Print("\n")

0 commit comments

Comments
 (0)