[security] rpc: Do not transfer access/secret key. (#4857)

This is an improvement upon existing implementation
by avoiding transfer of access and secret keys over
the network. This change only exchanges JWT tokens
generated by an rpc client. Even if the JWT can be
traced over the network on a non-TLS connection, this
change makes sure that we never really expose the
secret key over the network.
This commit is contained in:
Harshavardhana 2017-09-19 12:37:56 -07:00 committed by Dee Koder
parent f680b8482f
commit f8024cadbb
14 changed files with 184 additions and 141 deletions

View File

@ -33,16 +33,19 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
} }
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
adminServer := adminCmd{}
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
adminServer := adminCmd{}
args := LoginRPCArgs{ args := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
reply := LoginRPCReply{} err = adminServer.Login(&args, &LoginRPCReply{})
err = adminServer.Login(&args, &reply)
if err != nil { if err != nil {
t.Fatalf("Failed to login to admin server - %v", err) t.Fatalf("Failed to login to admin server - %v", err)
} }
@ -52,7 +55,7 @@ func testAdminCmd(cmd cmdType, t *testing.T) {
<-globalServiceSignalCh <-globalServiceSignalCh
}() }()
ga := AuthRPCArgs{AuthToken: reply.AuthToken} ga := AuthRPCArgs{AuthToken: token}
genReply := AuthRPCReply{} genReply := AuthRPCReply{}
switch cmd { switch cmd {
case restartCmd: case restartCmd:
@ -91,21 +94,25 @@ func TestReInitDisks(t *testing.T) {
// Setup admin rpc server for an XL backend. // Setup admin rpc server for an XL backend.
globalIsXL = true globalIsXL = true
adminServer := adminCmd{} adminServer := adminCmd{}
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{ args := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
reply := LoginRPCReply{} err = adminServer.Login(&args, &LoginRPCReply{})
err = adminServer.Login(&args, &reply)
if err != nil { if err != nil {
t.Fatalf("Failed to login to admin server - %v", err) t.Fatalf("Failed to login to admin server - %v", err)
} }
authArgs := AuthRPCArgs{ authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken, AuthToken: token,
} }
authReply := AuthRPCReply{} authReply := AuthRPCReply{}
@ -114,12 +121,15 @@ func TestReInitDisks(t *testing.T) {
t.Errorf("Expected to pass, but failed with %v", err) t.Errorf("Expected to pass, but failed with %v", err)
} }
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Negative test case with admin rpc server setup for FS. // Negative test case with admin rpc server setup for FS.
globalIsXL = false globalIsXL = false
fsAdminServer := adminCmd{} fsAdminServer := adminCmd{}
fsArgs := LoginRPCArgs{ fsArgs := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
@ -130,7 +140,7 @@ func TestReInitDisks(t *testing.T) {
} }
authArgs = AuthRPCArgs{ authArgs = AuthRPCArgs{
AuthToken: fsReply.AuthToken, AuthToken: token,
} }
authReply = AuthRPCReply{} authReply = AuthRPCReply{}
// Attempt ReInitDisks service on a FS backend. // Attempt ReInitDisks service on a FS backend.
@ -154,9 +164,14 @@ func TestGetConfig(t *testing.T) {
adminServer := adminCmd{} adminServer := adminCmd{}
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{ args := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
@ -167,7 +182,7 @@ func TestGetConfig(t *testing.T) {
} }
authArgs := AuthRPCArgs{ authArgs := AuthRPCArgs{
AuthToken: reply.AuthToken, AuthToken: token,
} }
configReply := ConfigReply{} configReply := ConfigReply{}
@ -198,9 +213,12 @@ func TestWriteAndCommitConfig(t *testing.T) {
adminServer := adminCmd{} adminServer := adminCmd{}
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
args := LoginRPCArgs{ args := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
@ -215,7 +233,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
tmpFileName := mustGetUUID() tmpFileName := mustGetUUID()
wArgs := WriteConfigArgs{ wArgs := WriteConfigArgs{
AuthRPCArgs: AuthRPCArgs{ AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken, AuthToken: token,
}, },
TmpFileName: tmpFileName, TmpFileName: tmpFileName,
Buf: buf, Buf: buf,
@ -232,7 +250,7 @@ func TestWriteAndCommitConfig(t *testing.T) {
cArgs := CommitConfigArgs{ cArgs := CommitConfigArgs{
AuthRPCArgs: AuthRPCArgs{ AuthRPCArgs: AuthRPCArgs{
AuthToken: reply.AuthToken, AuthToken: token,
}, },
FileName: tmpFileName, FileName: tmpFileName,
} }

View File

@ -99,21 +99,22 @@ func (authClient *AuthRPCClient) Login() (err error) {
// Attempt to login if not logged in already. // Attempt to login if not logged in already.
if authClient.authToken == "" { if authClient.authToken == "" {
// Login to authenticate and acquire a new auth token. authClient.authToken, err = authenticateNode(authClient.config.accessKey, authClient.config.secretKey)
if err != nil {
return err
}
// Login to authenticate your token.
var ( var (
loginMethod = authClient.config.serviceName + loginMethodName loginMethod = authClient.config.serviceName + loginMethodName
loginArgs = LoginRPCArgs{ loginArgs = LoginRPCArgs{
Username: authClient.config.accessKey, AuthToken: authClient.authToken,
Password: authClient.config.secretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
loginReply = LoginRPCReply{}
) )
if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &loginReply); err != nil { if err = authClient.rpcClient.Call(loginMethod, &loginArgs, &LoginRPCReply{}); err != nil {
return err return err
} }
authClient.authToken = loginReply.AuthToken
} }
return nil return nil
} }

View File

@ -20,8 +20,7 @@ package cmd
const loginMethodName = ".Login" const loginMethodName = ".Login"
// AuthRPCServer RPC server authenticates using JWT. // AuthRPCServer RPC server authenticates using JWT.
type AuthRPCServer struct { type AuthRPCServer struct{}
}
// Login - Handles JWT based RPC login. // Login - Handles JWT based RPC login.
func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error { func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
@ -30,14 +29,10 @@ func (b AuthRPCServer) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
return err return err
} }
// Authenticate using JWT. // Return an error if token is not valid.
token, err := authenticateNode(args.Username, args.Password) if !isAuthTokenValid(args.AuthToken) {
if err != nil { return errAuthentication
return err
} }
// Return the token.
reply.AuthToken = token
return nil return nil
} }

View File

@ -29,6 +29,10 @@ func TestLogin(t *testing.T) {
} }
defer os.RemoveAll(rootPath) defer os.RemoveAll(rootPath)
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
ls := AuthRPCServer{} ls := AuthRPCServer{}
testCases := []struct { testCases := []struct {
args LoginRPCArgs args LoginRPCArgs
@ -38,9 +42,8 @@ func TestLogin(t *testing.T) {
// Valid case. // Valid case.
{ {
args: LoginRPCArgs{ args: LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey, Version: Version,
Version: Version,
}, },
skewTime: 0, skewTime: 0,
expectedErr: nil, expectedErr: nil,
@ -48,9 +51,8 @@ func TestLogin(t *testing.T) {
// Valid username, password and request time, not version. // Valid username, password and request time, not version.
{ {
args: LoginRPCArgs{ args: LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey, Version: "INVALID-" + Version,
Version: "INVALID-" + Version,
}, },
skewTime: 0, skewTime: 0,
expectedErr: errServerVersionMismatch, expectedErr: errServerVersionMismatch,
@ -58,49 +60,17 @@ func TestLogin(t *testing.T) {
// Valid username, password and version, not request time // Valid username, password and version, not request time
{ {
args: LoginRPCArgs{ args: LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey, Version: Version,
Version: Version,
}, },
skewTime: 20 * time.Minute, skewTime: 20 * time.Minute,
expectedErr: errServerTimeMismatch, expectedErr: errServerTimeMismatch,
}, },
// Invalid username length // Invalid token, fails with authentication error
{ {
args: LoginRPCArgs{ args: LoginRPCArgs{
Username: "aaa", AuthToken: "",
Password: "minio123", Version: Version,
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyLength,
},
// Invalid password length
{
args: LoginRPCArgs{
Username: "minio",
Password: "aaa",
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidSecretKeyLength,
},
// Invalid username
{
args: LoginRPCArgs{
Username: "aaaaa",
Password: creds.SecretKey,
Version: Version,
},
skewTime: 0,
expectedErr: errInvalidAccessKeyID,
},
// Invalid password
{
args: LoginRPCArgs{
Username: creds.AccessKey,
Password: "aaaaaaaa",
Version: Version,
}, },
skewTime: 0, skewTime: 0,
expectedErr: errAuthentication, expectedErr: errAuthentication,
@ -108,7 +78,7 @@ func TestLogin(t *testing.T) {
} }
for i, test := range testCases { for i, test := range testCases {
reply := LoginRPCReply{} reply := LoginRPCReply{}
test.args.RequestTime = time.Now().Add(test.skewTime).UTC() test.args.RequestTime = UTCNow().Add(test.skewTime)
err := ls.Login(&test.args, &reply) err := ls.Login(&test.args, &reply)
if err != test.expectedErr { if err != test.expectedErr {
t.Errorf("Test %d: Expected error %v but received %v", t.Errorf("Test %d: Expected error %v but received %v",

View File

@ -23,26 +23,6 @@ import (
"time" "time"
) )
// Login handler implements JWT login token generator, which upon login request
// along with username and password is generated.
func (br *browserPeerAPIHandlers) Login(args *LoginRPCArgs, reply *LoginRPCReply) error {
// Validate LoginRPCArgs
if err := args.IsValid(); err != nil {
return err
}
// Authenticate using JWT.
token, err := authenticateWeb(args.Username, args.Password)
if err != nil {
return err
}
// Return the token.
reply.AuthToken = token
return nil
}
// SetAuthPeerArgs - Arguments collection for SetAuth RPC call // SetAuthPeerArgs - Arguments collection for SetAuth RPC call
type SetAuthPeerArgs struct { type SetAuthPeerArgs struct {
// For Auth // For Auth

View File

@ -91,9 +91,12 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
// Validate for failure in login handler with previous credentials. // Validate for failure in login handler with previous credentials.
rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false) rclient = newRPCClient(s.testAuthConf.serverAddr, s.testAuthConf.serviceEndpoint, false)
defer rclient.Close() defer rclient.Close()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
rargs := &LoginRPCArgs{ rargs := &LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
@ -105,20 +108,18 @@ func (s *TestRPCBrowserPeerSuite) testBrowserPeerRPC(t *testing.T) {
} }
} }
token, err = authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
// Validate for success in loing handled with valid credetnails. // Validate for success in loing handled with valid credetnails.
rargs = &LoginRPCArgs{ rargs = &LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
rreply = &LoginRPCReply{} rreply = &LoginRPCReply{}
err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply) if err = rclient.Call("BrowserPeer"+loginMethodName, rargs, rreply); err != nil {
if err != nil {
t.Fatal(err) t.Fatal(err)
} }
// Validate all the replied fields after successful login.
if rreply.AuthToken == "" {
t.Fatalf("Generated token cannot be empty %s", errInvalidToken)
}
} }

View File

@ -35,7 +35,7 @@ type browserPeerAPIHandlers struct {
// Register RPC router // Register RPC router
func registerBrowserPeerRPCRouter(mux *router.Router) error { func registerBrowserPeerRPCRouter(mux *router.Router) error {
bpHandlers := &browserPeerAPIHandlers{} bpHandlers := &browserPeerAPIHandlers{AuthRPCServer{}}
bpRPCServer := newRPCServer() bpRPCServer := newRPCServer()
err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers) err := bpRPCServer.RegisterName("BrowserPeer", bpHandlers)

View File

@ -63,10 +63,10 @@ func authenticateJWT(accessKey, secretKey string, expiry time.Duration) (string,
} }
utcNow := UTCNow() utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.MapClaims{ token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
"exp": utcNow.Add(expiry).Unix(), ExpiresAt: utcNow.Add(expiry).Unix(),
"iat": utcNow.Unix(), IssuedAt: utcNow.Unix(),
"sub": accessKey, Subject: accessKey,
}) })
return token.SignedString([]byte(serverCred.SecretKey)) return token.SignedString([]byte(serverCred.SecretKey))
@ -93,13 +93,17 @@ func keyFuncCallback(jwtToken *jwtgo.Token) (interface{}, error) {
} }
func isAuthTokenValid(tokenString string) bool { func isAuthTokenValid(tokenString string) bool {
jwtToken, err := jwtgo.Parse(tokenString, keyFuncCallback) var claims jwtgo.StandardClaims
jwtToken, err := jwtgo.ParseWithClaims(tokenString, &claims, keyFuncCallback)
if err != nil { if err != nil {
errorIf(err, "Unable to parse JWT token string") errorIf(err, "Unable to parse JWT token string")
return false return false
} }
if err = claims.Valid(); err != nil {
return jwtToken.Valid errorIf(err, "Invalid claims in JWT token string")
return false
}
return jwtToken.Valid && claims.Subject == serverConfig.GetCredential().AccessKey
} }
func isHTTPRequestValid(req *http.Request) bool { func isHTTPRequestValid(req *http.Request) bool {
@ -110,14 +114,20 @@ func isHTTPRequestValid(req *http.Request) bool {
// Returns nil if the request is authenticated. errNoAuthToken if token missing. // Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors. // Returns errAuthentication for all other errors.
func webRequestAuthenticate(req *http.Request) error { func webRequestAuthenticate(req *http.Request) error {
jwtToken, err := jwtreq.ParseFromRequest(req, jwtreq.AuthorizationHeaderExtractor, keyFuncCallback) var claims jwtgo.StandardClaims
jwtToken, err := jwtreq.ParseFromRequestWithClaims(req, jwtreq.AuthorizationHeaderExtractor, &claims, keyFuncCallback)
if err != nil { if err != nil {
if err == jwtreq.ErrNoTokenInRequest { if err == jwtreq.ErrNoTokenInRequest {
return errNoAuthToken return errNoAuthToken
} }
return errAuthentication return errAuthentication
} }
if err = claims.Valid(); err != nil {
return err
}
if claims.Subject != serverConfig.GetCredential().AccessKey {
return errInvalidAccessKeyID
}
if !jwtToken.Valid { if !jwtToken.Valid {
return errAuthentication return errAuthentication
} }

View File

@ -17,6 +17,7 @@
package cmd package cmd
import ( import (
"net/http"
"os" "os"
"testing" "testing"
) )
@ -88,6 +89,58 @@ func TestAuthenticateURL(t *testing.T) {
testAuthenticate("url", t) testAuthenticate("url", t)
} }
// Tests web request authenticator.
func TestWebRequestAuthenticate(t *testing.T) {
testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil {
t.Fatalf("unable initialize config file, %s", err)
}
defer os.RemoveAll(testPath)
creds := serverConfig.GetCredential()
token, err := getTokenString(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatalf("unable get token %s", err)
}
testCases := []struct {
req *http.Request
expectedErr error
}{
// Set valid authorization header.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{token},
},
},
expectedErr: nil,
},
// No authorization header.
{
req: &http.Request{
Header: http.Header{},
},
expectedErr: errNoAuthToken,
},
// Invalid authorization token.
{
req: &http.Request{
Header: http.Header{
"Authorization": []string{"invalid-token"},
},
},
expectedErr: errAuthentication,
},
}
for i, testCase := range testCases {
gotErr := webRequestAuthenticate(testCase.req)
if testCase.expectedErr != gotErr {
t.Errorf("Test %d, expected err %s, got %s", i+1, testCase.expectedErr, gotErr)
}
}
}
func BenchmarkAuthenticateNode(b *testing.B) { func BenchmarkAuthenticateNode(b *testing.B) {
testPath, err := newTestConfig(globalMinioDefaultRegion) testPath, err := newTestConfig(globalMinioDefaultRegion)
if err != nil { if err != nil {

View File

@ -58,9 +58,12 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
}, },
} }
creds := serverConfig.GetCredential() creds := serverConfig.GetCredential()
token, err := authenticateNode(creds.AccessKey, creds.SecretKey)
if err != nil {
t.Fatal(err)
}
loginArgs := LoginRPCArgs{ loginArgs := LoginRPCArgs{
Username: creds.AccessKey, AuthToken: token,
Password: creds.SecretKey,
Version: Version, Version: Version,
RequestTime: UTCNow(), RequestTime: UTCNow(),
} }
@ -69,8 +72,6 @@ func createLockTestServer(t *testing.T) (string, *lockServer, string) {
if err != nil { if err != nil {
t.Fatalf("Failed to login to lock server - %v", err) t.Fatalf("Failed to login to lock server - %v", err)
} }
token := loginReply.AuthToken
return testPath, locker, token return testPath, locker, token
} }

View File

@ -235,10 +235,7 @@ func (n *notifier) Validate() error {
if err := n.MySQL.Validate(); err != nil { if err := n.MySQL.Validate(); err != nil {
return err return err
} }
if err := n.MQTT.Validate(); err != nil { return n.MQTT.Validate()
return err
}
return nil
} }
func (n *notifier) SetAMQPByID(accountID string, amqpn amqpNotify) { func (n *notifier) SetAMQPByID(accountID string, amqpn amqpNotify) {

View File

@ -60,8 +60,7 @@ type AuthRPCReply struct{}
// LoginRPCArgs - login username and password for RPC. // LoginRPCArgs - login username and password for RPC.
type LoginRPCArgs struct { type LoginRPCArgs struct {
Username string AuthToken string
Password string
Version string Version string
RequestTime time.Time RequestTime time.Time
} }
@ -80,11 +79,8 @@ func (args LoginRPCArgs) IsValid() error {
return nil return nil
} }
// LoginRPCReply - login reply provides generated token to be used // LoginRPCReply - login reply is a dummy struct perhaps for future use.
// with subsequent requests. type LoginRPCReply struct{}
type LoginRPCReply struct {
AuthToken string
}
// LockArgs represents arguments for any authenticated lock RPC call. // LockArgs represents arguments for any authenticated lock RPC call.
type LockArgs struct { type LockArgs struct {

View File

@ -34,6 +34,7 @@ import (
"strings" "strings"
"testing" "testing"
jwtgo "github.com/dgrijalva/jwt-go"
humanize "github.com/dustin/go-humanize" humanize "github.com/dustin/go-humanize"
"github.com/minio/minio-go/pkg/policy" "github.com/minio/minio-go/pkg/policy"
"github.com/minio/minio-go/pkg/set" "github.com/minio/minio-go/pkg/set"
@ -667,6 +668,16 @@ func TestWebCreateURLToken(t *testing.T) {
ExecObjectLayerTest(t, testCreateURLToken) ExecObjectLayerTest(t, testCreateURLToken)
} }
func getTokenString(accessKey, secretKey string) (string, error) {
utcNow := UTCNow()
token := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, jwtgo.StandardClaims{
ExpiresAt: utcNow.Add(defaultJWTExpiry).Unix(),
IssuedAt: utcNow.Unix(),
Subject: accessKey,
})
return token.SignedString([]byte(secretKey))
}
func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler) { func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler) {
apiRouter := initTestWebRPCEndPoint(obj) apiRouter := initTestWebRPCEndPoint(obj)
credentials := serverConfig.GetCredential() credentials := serverConfig.GetCredential()
@ -700,6 +711,21 @@ func testCreateURLToken(obj ObjectLayer, instanceType string, t TestErrHandler)
if !isAuthTokenValid(tokenReply.Token) { if !isAuthTokenValid(tokenReply.Token) {
t.Fatalf("token is not valid") t.Fatalf("token is not valid")
} }
// Token is invalid.
if isAuthTokenValid("") {
t.Fatalf("token shouldn't be valid, but it is")
}
token, err := getTokenString("invalid-access", credentials.SecretKey)
if err != nil {
t.Fatal(err)
}
// Token has invalid access key.
if isAuthTokenValid(token) {
t.Fatalf("token shouldn't be valid, but it is")
}
} }
// Wrapper for calling Upload Handler // Wrapper for calling Upload Handler

View File

@ -92,12 +92,7 @@ func (d config) Save(filename string) error {
func (d config) Load(filename string) error { func (d config) Load(filename string) error {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
return loadFileConfig(filename, d.data)
if err := loadFileConfig(filename, d.data); err != nil {
return err
}
return nil
} }
// Data - grab internal data map for reading // Data - grab internal data map for reading