From 0122537872565c1f1eb92ed5077166ed623ba975 Mon Sep 17 00:00:00 2001 From: Fabian Seidl <fabian.b.seidl@stud.h-da.de> Date: Tue, 24 May 2022 08:50:51 +0000 Subject: [PATCH] Improvements for RBAC See merge request danet/gosdn!320 Co-authored-by: Andre Sterba <andre.sterba@stud.h-da.de> --- cli/cmd/login.go | 8 ++ cli/cmd/logout.go | 19 ++- cli/cmd/userCreate.go | 2 - cli/cmd/userUpdate.go | 2 - controller/api/apiUtil_test.go | 57 +++++++- controller/api/auth_test.go | 9 +- controller/api/initialise_test.go | 1 + controller/api/user_test.go | 8 +- controller/cmd/root.go | 1 + controller/config/config.go | 36 ++++- controller/configs/gosdn.toml | 3 + controller/controller.go | 22 +-- controller/interfaces/rbac/rbacService.go | 1 + controller/interfaces/rbac/user.go | 1 + controller/northbound/server/auth.go | 91 +++++++++--- .../northbound/server/auth_interceptor.go | 26 ++-- .../server/auth_interceptor_test.go | 4 - controller/northbound/server/auth_test.go | 130 +++++++++++++++++- controller/northbound/server/nbi.go | 8 +- controller/northbound/server/pnd_test.go | 4 +- controller/northbound/server/role.go | 19 ++- .../northbound/server/test_util_test.go | 41 ++++-- controller/northbound/server/user.go | 48 ++++--- controller/northbound/server/user_test.go | 13 +- controller/rbac/jwtManager.go | 10 ++ controller/rbac/rbacService.go | 2 +- controller/rbac/user.go | 19 ++- 27 files changed, 461 insertions(+), 124 deletions(-) diff --git a/cli/cmd/login.go b/cli/cmd/login.go index 19c48e60a..77e655f81 100644 --- a/cli/cmd/login.go +++ b/cli/cmd/login.go @@ -57,6 +57,14 @@ var loginCmd = &cobra.Command{ pterm.Info.Println("New controller address: ", viper.GetString("controllerAPIEndpoint")) } + // log out to remove active session in case an user is already logged in + if userToken != "" { + _, err := api.Logout(createContextWithAuthorization(), viper.GetString("controllerAPIEndpoint"), nbUserName) + if err != nil { + pterm.Error.Println("error logging out active user", err) + } + } + // TODO: maybe add credentials in context instead of context.TODO() resp, err := api.Login(context.TODO(), viper.GetString("controllerAPIEndpoint"), nbUserName, nbUserPwd) if err != nil { diff --git a/cli/cmd/logout.go b/cli/cmd/logout.go index a797c80e5..a8268b764 100644 --- a/cli/cmd/logout.go +++ b/cli/cmd/logout.go @@ -32,8 +32,10 @@ POSSIBILITY OF SUCH DAMAGE. package cmd import ( + "time" + "code.fbi.h-da.de/danet/gosdn/controller/api" - log "github.com/sirupsen/logrus" + "github.com/pterm/pterm" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -45,13 +47,24 @@ var logoutCmd = &cobra.Command{ Long: `Logs the current user out. Further actions on the controller are not permitted after this.`, RunE: func(cmd *cobra.Command, args []string) error { + spinner, _ := pterm.DefaultSpinner.Start("Logout attempt for user: ", nbUserName) + resp, err := api.Logout(createContextWithAuthorization(), viper.GetString("controllerAPIEndpoint"), nbUserName) if err != nil { + spinner.Fail("Logout failed: ", err) + return err + } + + userToken = "" + + viper.Set("USER_TOKEN", userToken) + err = viper.WriteConfig() + if err != nil { + pterm.Error.Println(err) return err } - // TODO: unset session proof here - log.Info("LogoutResponse: " + resp.Status.String()) + spinner.Success("User ", nbUserName, " successfully logged out at ", time.Unix((resp.Timestamp/1000000000), 0), ".") return nil }, diff --git a/cli/cmd/userCreate.go b/cli/cmd/userCreate.go index 0b64cad0f..bd0b87888 100644 --- a/cli/cmd/userCreate.go +++ b/cli/cmd/userCreate.go @@ -53,8 +53,6 @@ var userCreateCmd = &cobra.Command{ // only active pnd for now, add option for additional param later roles[viper.GetString("CLI_PND")] = nbUserRole - //TODO(faseid): hash password - // only one for now, add option to add more users at once later users := []*apb.User{ { diff --git a/cli/cmd/userUpdate.go b/cli/cmd/userUpdate.go index a85365a5b..7288d9e4d 100644 --- a/cli/cmd/userUpdate.go +++ b/cli/cmd/userUpdate.go @@ -53,8 +53,6 @@ var userUpdateCmd = &cobra.Command{ // only active pnd for now, add option for additional param later roles[viper.GetString("CLI_PND")] = nbUserRole - //TODO(faseid): hash password - // only one for now, add option to update more users at once later users := []*apb.User{ { diff --git a/controller/api/apiUtil_test.go b/controller/api/apiUtil_test.go index bea1ab205..cc01aebb9 100644 --- a/controller/api/apiUtil_test.go +++ b/controller/api/apiUtil_test.go @@ -1,8 +1,15 @@ package api import ( + "encoding/base64" + "time" + + "code.fbi.h-da.de/danet/gosdn/controller/rbac" rbacImpl "code.fbi.h-da.de/danet/gosdn/controller/rbac" + "code.fbi.h-da.de/danet/gosdn/controller/store" "github.com/google/uuid" + "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/argon2" ) // Name of this file requires _test at the end, because of how the availability of varibales is handled in test files of go packages. @@ -54,18 +61,28 @@ func clearAndCreateAuthTestSetup() error { return nil } -//TODO(faseid): change password to hashed/encrypted one func createTestUsers() error { randomRoleMap := map[string]string{pndID: randomRoleName} + // Generate a salt that is 16 characters long with 3 digits, 0 symbols, + // allowing upper and lower case letters, disallowing repeat characters. + salt, err := password.Generate(16, 3, 0, true, false) + if err != nil { + return err + } + + testAdminPWD := createHashedAndSaltedPassword("admin", salt) + testUserPWD := createHashedAndSaltedPassword("user", salt) + testRandPWD := createHashedAndSaltedPassword("aurelius", salt) + users := []rbacImpl.User{ - {UserID: uuid.MustParse(adminID), UserName: "testAdmin", Roles: adminRoleMap, Password: "admin"}, - {UserID: uuid.MustParse(userID), UserName: "testUser", Roles: userRoleMap, Password: "user"}, - {UserID: uuid.New(), UserName: "testRandom", Roles: randomRoleMap, Password: "aurelius", Token: "wrong token"}, + {UserID: uuid.MustParse(adminID), UserName: "testAdmin", Roles: adminRoleMap, Password: testAdminPWD}, + {UserID: uuid.MustParse(userID), UserName: "testUser", Roles: userRoleMap, Password: testUserPWD}, + {UserID: uuid.New(), UserName: "testRandom", Roles: randomRoleMap, Password: testRandPWD, Token: "wrong token"}, } for _, u := range users { - err := userService.Add(rbacImpl.NewUser(u.ID(), u.Name(), u.Roles, u.Password, "")) + err := userService.Add(rbacImpl.NewUser(u.ID(), u.Name(), u.Roles, u.Password, "", salt)) if err != nil { return err } @@ -112,3 +129,33 @@ func createTestRoles() error { return nil } + +// Creates a token to be used in auth interceptor tests. If validTokenRequired is set as true, the generated token will also +// be attached to the provided user. Else the user won't have the token and can not be authorized. +func createTestUserToken(userName string, validTokenRequired bool) (string, error) { + jwt := rbacImpl.NewJWTManager("", 1*time.Minute) + + token, err := jwt.GenerateToken(rbac.User{UserName: userName}) + if err != nil { + return token, err + } + + if validTokenRequired { + user, err := userService.Get(store.Query{Name: userName}) + if err != nil { + return token, err + } + user.SetToken(token) + + err = userService.Update(user) + if err != nil { + return token, err + } + } + + return token, nil +} + +func createHashedAndSaltedPassword(plainPWD, salt string) string { + return base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(plainPWD), []byte(salt), 1, 64*1024, 4, 32)) +} diff --git a/controller/api/auth_test.go b/controller/api/auth_test.go index 8012d03cb..1c7cdb5af 100644 --- a/controller/api/auth_test.go +++ b/controller/api/auth_test.go @@ -5,6 +5,8 @@ import ( "testing" apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" + log "github.com/sirupsen/logrus" + "google.golang.org/grpc/metadata" ) func TestLogin(t *testing.T) { @@ -64,6 +66,11 @@ func TestLogin(t *testing.T) { } func TestLogout(t *testing.T) { + validToken, err := createTestUserToken("testAdmin", true) + if err != nil { + log.Fatal(err) + } + type args struct { ctx context.Context addr string @@ -78,7 +85,7 @@ func TestLogout(t *testing.T) { { name: "default log out", args: args{ - ctx: context.TODO(), + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)), addr: testAPIEndpoint, username: "testAdmin", }, diff --git a/controller/api/initialise_test.go b/controller/api/initialise_test.go index 20982ef78..8638d200c 100644 --- a/controller/api/initialise_test.go +++ b/controller/api/initialise_test.go @@ -144,6 +144,7 @@ func bootstrapUnitTest() { northbound := nbi.NewNBI(pndStore, userService, roleService) northbound.Auth = nbi.NewAuthServer(jwtManager) + northbound.User = nbi.NewUserServer(jwtManager) cpb.RegisterCoreServiceServer(s, northbound.Core) ppb.RegisterPndServiceServer(s, northbound.Pnd) diff --git a/controller/api/user_test.go b/controller/api/user_test.go index edfb703d3..ab8cf858e 100644 --- a/controller/api/user_test.go +++ b/controller/api/user_test.go @@ -7,9 +7,15 @@ import ( apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" "github.com/google/uuid" + "google.golang.org/grpc/metadata" ) func TestCreateUsers(t *testing.T) { + token, err := createTestUserToken("testAdmin", true) + if err != nil { + t.Fatalf("%v", err) + } + type args struct { ctx context.Context addr string @@ -24,7 +30,7 @@ func TestCreateUsers(t *testing.T) { { name: "default create users", args: args{ - ctx: context.TODO(), + ctx: metadata.NewOutgoingContext(context.Background(), metadata.Pairs("authorize", token)), addr: testAPIEndpoint, users: []*apb.User{ { diff --git a/controller/cmd/root.go b/controller/cmd/root.go index c1dad9408..6149c2aff 100644 --- a/controller/cmd/root.go +++ b/controller/cmd/root.go @@ -126,6 +126,7 @@ func initConfig() { viper.SetDefault("csbi-orchestrator", "localhost:55056") viper.SetDefault("plugin-folder", "plugins") viper.SetDefault("security", "secure") + viper.SetDefault("defaultJWTDuration", 24) ll := viper.GetString("GOSDN_LOG") if ll != "" { diff --git a/controller/config/config.go b/controller/config/config.go index ac47b9541..059821cf7 100644 --- a/controller/config/config.go +++ b/controller/config/config.go @@ -18,6 +18,9 @@ const ( changeTimeoutKey = "GOSDN_CHANGE_TIMEOUT" databaseConnectionKey = "databaseConnection" filesystemPathToStores = "filesystemPathToStores" + jwtDurationKey = "defaultJWTDuration" + defaultJWTDuration = time.Hour * 24 + jwtSecretKey = "jwtSecret" ) // BasePndUUID is an uuid for the base PND @@ -41,6 +44,12 @@ var DatabaseConnection string // FilesystemPathToStores determines in which folder the stores should be saved var FilesystemPathToStores = "stores_testing" +// JWTDuration determines how long a jwt is valid +var JWTDuration time.Duration + +// JWTSecret determines the scret that is used to sign tokens +var JWTSecret string + // Init gets called on module import func Init() { err := InitializeConfig() @@ -71,9 +80,6 @@ func InitializeConfig() error { if BaseSouthBoundType != 1 { BaseSouthBoundType = 1 viper.Set(baseSouthBoundTypeKey, 1) - if err := viper.WriteConfig(); err != nil { - return err - } } err = setChangeTimeout() @@ -90,6 +96,17 @@ func InitializeConfig() error { FilesystemPathToStores = "stores" } + JWTDuration, err = getDurationFromViper(jwtDurationKey, time.Hour) + if err != nil { + JWTDuration = defaultJWTDuration + } + + JWTSecret = viper.GetString(jwtSecretKey) + + if err := viper.WriteConfig(); err != nil { + return err + } + return nil } @@ -104,9 +121,6 @@ func getUUIDFromViper(viperKey string) (uuid.UUID, error) { if UUIDAsString == "" { newUUID := uuid.New() viper.Set(viperKey, newUUID.String()) - if err := viper.WriteConfig(); err != nil { - return uuid.Nil, err - } return newUUID, nil } @@ -147,3 +161,13 @@ func setLogLevel() { LogLevel = logrus.InfoLevel } } + +func getDurationFromViper(viperKey string, unit time.Duration) (time.Duration, error) { + duration := viper.GetDuration(viperKey) + + if duration > 0 { + return duration * unit, nil + } + + return 0, viper.ConfigParseError{} +} diff --git a/controller/configs/gosdn.toml b/controller/configs/gosdn.toml index dc3060edb..d215797b3 100644 --- a/controller/configs/gosdn.toml +++ b/controller/configs/gosdn.toml @@ -1,3 +1,6 @@ +# This is used as an example config or when the controller gets started using the config path flag. +# If the controller is started from gosdn/controller directory the config file that gets used can +# can be found in controller/cmd/gosdn/configs/gosdn.toml basepnduuid = "5f20f34b-cbd0-4511-9ddc-c50cf6a3b49d" basesouthboundtype = 1 basesouthbounduuid = "ca29311a-3b17-4385-96f8-515b602a97ac" diff --git a/controller/controller.go b/controller/controller.go index c3c49d2cc..eebc8ae8c 100644 --- a/controller/controller.go +++ b/controller/controller.go @@ -2,6 +2,7 @@ package controller import ( "context" + "encoding/base64" "fmt" "net" "net/http" @@ -9,12 +10,12 @@ import ( "os/signal" "sync" "syscall" - "time" "github.com/google/uuid" "github.com/sethvargo/go-password/password" log "github.com/sirupsen/logrus" "github.com/spf13/viper" + "golang.org/x/crypto/argon2" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" @@ -78,11 +79,6 @@ func initialize() error { startHttpServer() coreLock.Unlock() - err = config.InitializeConfig() - if err != nil { - return err - } - err = createPrincipalNetworkDomain() if err != nil { return err @@ -109,11 +105,12 @@ func startGrpc() error { } log.Infof("listening to %v", lislisten.Addr()) - jwtManager := rbacImpl.NewJWTManager("", (10000 * time.Hour)) //TODO(faseid): add real secret and proper duration data here! + jwtManager := rbacImpl.NewJWTManager(config.JWTSecret, config.JWTDuration) setupGRPCServerWithCorrectSecurityLevel(jwtManager) c.nbi = nbi.NewNBI(c.pndStore, c.userService, c.roleService) c.nbi.Auth = nbi.NewAuthServer(jwtManager) + c.nbi.User = nbi.NewUserServer(jwtManager) pb.RegisterCoreServiceServer(c.grpcServer, c.nbi.Core) ppb.RegisterPndServiceServer(c.grpcServer, c.nbi.Pnd) @@ -226,12 +223,19 @@ func ensureDefaultUserExists() error { log.Fatal(err) } - fmt.Printf("########\n Generated admin password: %s\n########\n", generatedPassword) + salt, err := password.Generate(16, 3, 0, true, false) + if err != nil { + log.Fatal(err) + } + + hashedPassword := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(generatedPassword), []byte(salt), 1, 64*1024, 4, 32)) - err = c.userService.Add(rbacImpl.NewUser(uuid.New(), defaultUserName, map[string]string{config.BasePndUUID.String(): "admin"}, generatedPassword, "")) + err = c.userService.Add(rbacImpl.NewUser(uuid.New(), defaultUserName, map[string]string{config.BasePndUUID.String(): "admin"}, string(hashedPassword), "", salt)) if err != nil { return err } + + fmt.Printf("########\n Generated admin password: %s\n########\n", generatedPassword) } return nil diff --git a/controller/interfaces/rbac/rbacService.go b/controller/interfaces/rbac/rbacService.go index e5a543e8c..74c938ac2 100644 --- a/controller/interfaces/rbac/rbacService.go +++ b/controller/interfaces/rbac/rbacService.go @@ -20,6 +20,7 @@ type LoadedUser struct { Roles map[string]string `json:"roles,omitempty"` Password string `json:"password"` Token string `json:"token,omitempty"` + Salt string `json:"salt" bson:"salt"` } // RoleService describes an interface for role service implementations. diff --git a/controller/interfaces/rbac/user.go b/controller/interfaces/rbac/user.go index 8ed0fc254..37b4f9021 100644 --- a/controller/interfaces/rbac/user.go +++ b/controller/interfaces/rbac/user.go @@ -10,4 +10,5 @@ type User interface { GetPassword() string GetToken() string SetToken(string) + GetSalt() string } diff --git a/controller/northbound/server/auth.go b/controller/northbound/server/auth.go index 4507b5f1d..bf0096873 100644 --- a/controller/northbound/server/auth.go +++ b/controller/northbound/server/auth.go @@ -2,6 +2,7 @@ package server import ( "context" + "encoding/base64" "time" apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" @@ -9,7 +10,9 @@ import ( "code.fbi.h-da.de/danet/gosdn/controller/rbac" "code.fbi.h-da.de/danet/gosdn/controller/store" "github.com/prometheus/client_golang/prometheus" + "golang.org/x/crypto/argon2" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -37,11 +40,9 @@ func (s Auth) Login(ctx context.Context, request *apb.LoginRequest) (*apb.LoginR Password: request.Pwd, } - //TODO: add check if user is logged in with session handling - // validation of credentials - validCredentials, err := s.isValidUser(user) - if (err != nil) || !validCredentials { + err := s.isValidUser(user) + if err != nil { return nil, err } @@ -51,14 +52,14 @@ func (s Auth) Login(ctx context.Context, request *apb.LoginRequest) (*apb.LoginR return nil, err } - userToUpdate, err := userc.Get(store.Query{Name: user.UserName}) + userToUpdate, err := userService.Get(store.Query{Name: user.UserName}) if err != nil { return nil, err } userToUpdate.SetToken(token) - err = userc.Update(userToUpdate) + err = userService.Update(userToUpdate) if err != nil { return nil, err } @@ -76,8 +77,10 @@ func (s Auth) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.Logo start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - // not implemented yet - // TODO: add session handling and logout + err := s.handleLogout(ctx, request.Username) + if err != nil { + return nil, err + } return &apb.LogoutResponse{ Timestamp: time.Now().UnixNano(), @@ -85,19 +88,75 @@ func (s Auth) Logout(ctx context.Context, request *apb.LogoutRequest) (*apb.Logo }, nil } -func (s Auth) isValidUser(user rbac.User) (bool, error) { - storedUser, err := userc.Get(store.Query{Name: user.Name()}) +// isValidUser checks if the provided user name fits to a stored one and then checks if the provided password is correct. +func (s Auth) isValidUser(user rbac.User) error { + storedUser, err := userService.Get(store.Query{Name: user.Name()}) if err != nil { - return false, err - } else if storedUser == nil { - return false, status.Errorf(codes.Aborted, "no user object") + return err } if storedUser.Name() == user.Name() { - if storedUser.GetPassword() == user.GetPassword() { - return true, nil + err := s.isCorrectPassword(storedUser.GetPassword(), storedUser.GetSalt(), user.Password) + if err != nil { + return err + } + } + + return nil +} + +// isCorrectPassword checks if the provided password fits with the hashed user password taken from the storage. +func (s Auth) isCorrectPassword(storedPassword, salt, loginPassword string) error { + hashedPasswordFromLogin := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(loginPassword), []byte(salt), 1, 64*1024, 4, 32)) + + if storedPassword == hashedPasswordFromLogin { + return nil + } + + return status.Errorf(codes.Unauthenticated, "incorrect user name or password") +} + +// handleLogout checks if the provided user name matches with the one associated with token and +// replaces the stored token of the user with an empty string. +func (s Auth) handleLogout(ctx context.Context, userName string) error { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return status.Errorf(codes.Aborted, "metadata is not provided") + } + + if len(md["authorize"]) > 0 { + token := md["authorize"][0] + + claims, err := s.jwtManager.GetClaimsFromToken(token) + if err != nil { + return err + } + + if claims.Username != userName { + return status.Errorf(codes.Aborted, "missing match of user associated to token and provided user name") + } + + storedUser, err := userService.Get(store.Query{Name: userName}) + if err != nil { + return err + } + + if token != storedUser.GetToken() { + return status.Errorf(codes.Aborted, "missing match of token provied for user") + } + + err = userService.Update(&rbac.User{UserID: storedUser.ID(), + UserName: storedUser.Name(), + Roles: storedUser.GetRoles(), + Password: storedUser.GetPassword(), + Token: " ", + Salt: storedUser.GetSalt(), + }) + + if err != nil { + return err } } - return false, status.Errorf(codes.Unauthenticated, "incorrect user name or password") + return nil } diff --git a/controller/northbound/server/auth_interceptor.go b/controller/northbound/server/auth_interceptor.go index 47c80e023..9c5f7a746 100644 --- a/controller/northbound/server/auth_interceptor.go +++ b/controller/northbound/server/auth_interceptor.go @@ -2,6 +2,7 @@ package server import ( "context" + "time" csbipb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/csbi" apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" @@ -28,20 +29,11 @@ func NewAuthInterceptor(jwtManager *rbac.JWTManager) *AuthInterceptor { // Unary returns a unary interceptor function to authenticate and authorize unary RPC calls func (auth *AuthInterceptor) Unary() grpc.UnaryServerInterceptor { return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { - switch r := req.(type) { + switch req.(type) { case *apb.LoginRequest: return handler(ctx, req) case *apb.LogoutRequest: return handler(ctx, req) - case *apb.CreateUsersRequest: - if len(r.User) < 2 { - return handler(ctx, req) - } - - err := auth.authorize(ctx, info.FullMethod) - if err != nil { - return nil, err - } case *csbipb.Syn: return handler(ctx, req) default: @@ -79,16 +71,18 @@ func (auth *AuthInterceptor) authorize(ctx context.Context, method string) error } // validate token and check permission here - token := "" if len(md["authorize"]) > 0 { - token = md["authorize"][0] - - claims, err := auth.jwtManager.VerifyToken(token) + token := md["authorize"][0] + claims, err := auth.jwtManager.GetClaimsFromToken(token) if err != nil { return err } - user, err := userc.Get(store.Query{Name: claims.Username}) + if time.Now().Unix() > claims.ExpiresAt { + return status.Errorf(codes.PermissionDenied, "token expired at %v, please login", time.Unix(claims.ExpiresAt, 0)) + } + + user, err := userService.Get(store.Query{Name: claims.Username}) if err != nil { return err } @@ -120,7 +114,7 @@ func (auth *AuthInterceptor) verifyPermisisonForRequestedCall(userRoles map[stri } func (auth *AuthInterceptor) verifyUserRoleAndRequestedCall(userRole, requestedMethod string) error { - storedRoles, err := rolec.GetAll() + storedRoles, err := roleService.GetAll() if err != nil { return err } diff --git a/controller/northbound/server/auth_interceptor_test.go b/controller/northbound/server/auth_interceptor_test.go index 2d975d919..499a63b2c 100644 --- a/controller/northbound/server/auth_interceptor_test.go +++ b/controller/northbound/server/auth_interceptor_test.go @@ -2,7 +2,6 @@ package server import ( "context" - "fmt" "log" "net" "testing" @@ -197,9 +196,6 @@ func TestAuthInterceptor_authorize(t *testing.T) { log.Fatal(err) } - md := metadata.Pairs("authorize", validToken) - fmt.Println(md.Get("authorize")) - type args struct { ctx context.Context method string diff --git a/controller/northbound/server/auth_test.go b/controller/northbound/server/auth_test.go index 0c2577624..9a875ec84 100644 --- a/controller/northbound/server/auth_test.go +++ b/controller/northbound/server/auth_test.go @@ -2,9 +2,12 @@ package server import ( "context" + "log" "testing" apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" + "code.fbi.h-da.de/danet/gosdn/controller/rbac" + "google.golang.org/grpc/metadata" ) func TestAuth_Login(t *testing.T) { @@ -64,6 +67,11 @@ func TestAuth_Login(t *testing.T) { } func TestAuth_Logout(t *testing.T) { + validToken, err := createTestUserToken("testAdmin", true) + if err != nil { + log.Fatal(err) + } + type args struct { ctx context.Context request *apb.LogoutRequest @@ -77,7 +85,7 @@ func TestAuth_Logout(t *testing.T) { { name: "default log out", args: args{ - ctx: context.TODO(), + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)), request: &apb.LogoutRequest{ Username: "testAdmin", }, @@ -91,7 +99,9 @@ func TestAuth_Logout(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := Auth{} + s := Auth{ + jwtManager: jwt, + } got, err := s.Logout(tt.args.ctx, tt.args.request) if (err != nil) != tt.wantErr { t.Errorf("Auth.Logout() error = %v, wantErr %v", err, tt.wantErr) @@ -104,3 +114,119 @@ func TestAuth_Logout(t *testing.T) { }) } } + +func TestAuth_isValidUser(t *testing.T) { + type args struct { + user rbac.User + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default valid user", + args: args{ + user: rbac.User{ + UserName: "testAdmin", + Password: "admin", + }, + }, + wantErr: false, + }, + { + name: "error wrong user name", + args: args{ + user: rbac.User{ + UserName: "foo", + Password: "admin", + }, + }, + wantErr: true, + }, + { + name: "error wrong password", + args: args{ + user: rbac.User{ + UserName: "testAdmin", + Password: "foo", + }, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Auth{} + if err := s.isValidUser(tt.args.user); (err != nil) != tt.wantErr { + t.Errorf("Auth.isValidUser() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestAuth_handleLogout(t *testing.T) { + validToken, err := createTestUserToken("testAdmin", true) + if err != nil { + log.Fatal(err) + } + + invalidToken, err := createTestUserToken("testAdmin", false) + if err != nil { + log.Fatal(err) + } + + type args struct { + ctx context.Context + userName string + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "default handle logout", + args: args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)), + userName: "testAdmin", + }, + wantErr: false, + }, + { + name: "fail no metadata", + args: args{ + ctx: context.TODO(), + userName: "testAdmin", + }, + wantErr: true, + }, + { + name: "fail invalid token for user", + args: args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", invalidToken)), + userName: "testAdmin", + }, + wantErr: true, + }, + { + name: "fail invalid user for token", + args: args{ + ctx: metadata.NewIncomingContext(context.Background(), metadata.Pairs("authorize", validToken)), + userName: "testUser", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Auth{ + jwtManager: jwt, + } + if err := s.handleLogout(tt.args.ctx, tt.args.userName); (err != nil) != tt.wantErr { + t.Errorf("Auth.handleLogout() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/controller/northbound/server/nbi.go b/controller/northbound/server/nbi.go index 8df59768e..040407e32 100644 --- a/controller/northbound/server/nbi.go +++ b/controller/northbound/server/nbi.go @@ -11,8 +11,8 @@ import ( ) var pndc networkdomain.PndStore -var userc rbac.UserService -var rolec rbac.RoleService +var userService rbac.UserService +var roleService rbac.RoleService // NorthboundInterface is the representation of the // gRPC services used provided. @@ -29,8 +29,8 @@ type NorthboundInterface struct { // NewNBI receives a PndStore and returns a new gRPC *NorthboundInterface func NewNBI(pnds networkdomain.PndStore, users rbac.UserService, roles rbac.RoleService) *NorthboundInterface { pndc = pnds - userc = users - rolec = roles + userService = users + roleService = roles return &NorthboundInterface{ Pnd: &pndServer{}, Core: &core{}, diff --git a/controller/northbound/server/pnd_test.go b/controller/northbound/server/pnd_test.go index 2aabdce13..58771744d 100644 --- a/controller/northbound/server/pnd_test.go +++ b/controller/northbound/server/pnd_test.go @@ -121,8 +121,8 @@ func TestMain(m *testing.M) { } // everyting auth related - userc = rbac.NewUserService(rbac.NewMemoryUserStore()) - rolec = rbac.NewRoleService(rbac.NewMemoryRoleStore()) + userService = rbac.NewUserService(rbac.NewMemoryUserStore()) + roleService = rbac.NewRoleService(rbac.NewMemoryRoleStore()) err = clearAndCreateAuthTestSetup() if err != nil { log.Fatal(err) diff --git a/controller/northbound/server/role.go b/controller/northbound/server/role.go index df2f3a0f8..4f4990d0f 100644 --- a/controller/northbound/server/role.go +++ b/controller/northbound/server/role.go @@ -37,7 +37,7 @@ func (r Role) CreateRoles(ctx context.Context, request *apb.CreateRolesRequest) for _, r := range request.Roles { role := rbac.NewRole(uuid.New(), r.Name, r.Description, r.Permissions) - err := rolec.Add(role) + err := roleService.Add(role) if err != nil { log.Error(err) return nil, status.Errorf(codes.Aborted, "%v", err) @@ -56,7 +56,7 @@ func (r Role) GetRole(ctx context.Context, request *apb.GetRoleRequest) (*apb.Ge start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - roleData, err := rolec.Get(store.Query{Name: request.RoleName}) + roleData, err := roleService.Get(store.Query{Name: request.RoleName}) if err != nil { return nil, err } @@ -81,7 +81,7 @@ func (r Role) GetRoles(ctx context.Context, request *apb.GetRolesRequest) (*apb. start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - roleList, err := rolec.GetAll() + roleList, err := roleService.GetAll() if err != nil { return nil, err } @@ -109,20 +109,19 @@ func (r Role) UpdateRoles(ctx context.Context, request *apb.UpdateRolesRequest) start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - // TODO: check if current user is allowed to update the role they try to update; only their own if not admin for _, r := range request.Roles { rid, err := uuid.Parse(r.Id) if err != nil { return nil, handleRPCError(labels, err) } - _, err = rolec.Get(store.Query{ID: rid}) + _, err = roleService.Get(store.Query{ID: rid}) if err != nil { return nil, status.Errorf(codes.Canceled, "role not found %v", err) } roleToUpdate := rbac.NewRole(rid, r.Name, r.Description, r.Permissions) - err = rolec.Update(roleToUpdate) + err = roleService.Update(roleToUpdate) if err != nil { return nil, status.Errorf(codes.Aborted, "could not update role %v", err) } @@ -140,7 +139,7 @@ func (r Role) DeletePermissionsForRole(ctx context.Context, request *apb.DeleteP start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - roleToUpdate, err := rolec.Get(store.Query{Name: request.RoleName}) + roleToUpdate, err := roleService.Get(store.Query{Name: request.RoleName}) if err != nil { return nil, status.Errorf(codes.Canceled, "role not found %v", err) } @@ -165,7 +164,7 @@ func (r Role) DeletePermissionsForRole(ctx context.Context, request *apb.DeleteP // updates the existing role with the trimmed set of permissions roleToUpdate.RemovePermissionsFromRole(request.PermissionsToDelete) - err = rolec.Update(roleToUpdate) + err = roleService.Update(roleToUpdate) if err != nil { return nil, status.Errorf(codes.Aborted, "could not update role %v", err) } @@ -183,12 +182,12 @@ func (r Role) DeleteRoles(ctx context.Context, request *apb.DeleteRolesRequest) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) for _, r := range request.RoleName { - roleToDelete, err := rolec.Get(store.Query{Name: r}) + roleToDelete, err := roleService.Get(store.Query{Name: r}) if err != nil { return nil, status.Errorf(codes.Canceled, "role not found") } - err = rolec.Delete(roleToDelete) + err = roleService.Delete(roleToDelete) if err != nil { return nil, status.Errorf(codes.Aborted, "error deleting role %v", err) } diff --git a/controller/northbound/server/test_util_test.go b/controller/northbound/server/test_util_test.go index 96992775c..6eb5c982b 100644 --- a/controller/northbound/server/test_util_test.go +++ b/controller/northbound/server/test_util_test.go @@ -2,12 +2,15 @@ package server import ( "bytes" + "encoding/base64" "log" "testing" "code.fbi.h-da.de/danet/gosdn/controller/rbac" "code.fbi.h-da.de/danet/gosdn/controller/store" "github.com/google/uuid" + "github.com/sethvargo/go-password/password" + "golang.org/x/crypto/argon2" ) // Name of this file requires _test at the end, because of how the availability of varibales is handled in test files of go packages. @@ -24,23 +27,23 @@ var jwt *rbac.JWTManager func clearAndCreateAuthTestSetup() error { //clear setup if changed - storedUsers, err := userc.GetAll() + storedUsers, err := userService.GetAll() if err != nil { return err } for _, u := range storedUsers { - err = userc.Delete(u) + err = userService.Delete(u) if err != nil { return err } } - storedRoles, err := rolec.GetAll() + storedRoles, err := roleService.GetAll() if err != nil { return err } for _, r := range storedRoles { - err = rolec.Delete(r) + err = roleService.Delete(r) if err != nil { return err } @@ -60,18 +63,28 @@ func clearAndCreateAuthTestSetup() error { return nil } -//TODO(faseid): change password to hashed/encrypted one func createTestUsers() error { randomRoleMap := map[string]string{pndID: randomRoleName} + // Generate a salt that is 16 characters long with 3 digits, 0 symbols, + // allowing upper and lower case letters, disallowing repeat characters. + salt, err := password.Generate(16, 3, 0, true, false) + if err != nil { + return err + } + + testAdminPWD := createHashedAndSaltedPassword("admin", salt) + testUserPWD := createHashedAndSaltedPassword("user", salt) + testRandPWD := createHashedAndSaltedPassword("aurelius", salt) + users := []rbac.User{ - {UserID: uuid.MustParse(adminID), UserName: "testAdmin", Roles: adminRoleMap, Password: "admin"}, - {UserID: uuid.MustParse(userID), UserName: "testUser", Roles: userRoleMap, Password: "user"}, - {UserID: uuid.New(), UserName: "testRandom", Roles: randomRoleMap, Password: "aurelius", Token: "wrong token"}, + {UserID: uuid.MustParse(adminID), UserName: "testAdmin", Roles: adminRoleMap, Password: testAdminPWD}, + {UserID: uuid.MustParse(userID), UserName: "testUser", Roles: userRoleMap, Password: testUserPWD}, + {UserID: uuid.New(), UserName: "testRandom", Roles: randomRoleMap, Password: testRandPWD, Token: "wrong token"}, } for _, u := range users { - err := userc.Add(rbac.NewUser(u.ID(), u.Name(), u.Roles, u.Password, "")) + err := userService.Add(rbac.NewUser(u.ID(), u.Name(), u.Roles, u.Password, "", salt)) if err != nil { return err } @@ -112,7 +125,7 @@ func createTestRoles() error { } for _, r := range roles { - err := rolec.Add(rbac.NewRole(r.ID(), r.Name(), r.Description, r.Permissions)) + err := roleService.Add(rbac.NewRole(r.ID(), r.Name(), r.Description, r.Permissions)) if err != nil { return err } @@ -147,13 +160,13 @@ func createTestUserToken(userName string, validTokenRequired bool) (string, erro } if validTokenRequired { - user, err := userc.Get(store.Query{Name: userName}) + user, err := userService.Get(store.Query{Name: userName}) if err != nil { return token, err } user.SetToken(token) - err = userc.Update(user) + err = userService.Update(user) if err != nil { return token, err } @@ -161,3 +174,7 @@ func createTestUserToken(userName string, validTokenRequired bool) (string, erro return token, nil } + +func createHashedAndSaltedPassword(plainPWD, salt string) string { + return base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(plainPWD), []byte(salt), 1, 64*1024, 4, 32)) +} diff --git a/controller/northbound/server/user.go b/controller/northbound/server/user.go index e5f5e4a73..f15f34124 100644 --- a/controller/northbound/server/user.go +++ b/controller/northbound/server/user.go @@ -2,7 +2,7 @@ package server import ( "context" - "fmt" + "encoding/base64" "time" apb "code.fbi.h-da.de/danet/gosdn/api/go/gosdn/rbac" @@ -11,9 +11,12 @@ import ( "code.fbi.h-da.de/danet/gosdn/controller/store" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" + "github.com/sethvargo/go-password/password" log "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + + "golang.org/x/crypto/argon2" ) // User holds a JWTManager and represents a UserServiceServer. @@ -35,12 +38,9 @@ func (u User) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest) start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - // TODO: implement check if user is allowed to create users with this role - // e.g. non-admin shouldn't be allowed to create admin users for _, u := range request.User { roles := map[string]string{} for key, elem := range u.Roles { - fmt.Printf("k: %v v: %v\n", key, elem) _, err := uuid.Parse(key) if err != nil { return nil, handleRPCError(labels, err) @@ -48,8 +48,18 @@ func (u User) CreateUsers(ctx context.Context, request *apb.CreateUsersRequest) roles[key] = elem } - user := rbac.NewUser(uuid.New(), u.Name, roles, u.Password, u.Token) - err := userc.Add(user) + // Generate a salt that is 16 characters long with 3 digits, 0 symbols, + // allowing upper and lower case letters, disallowing repeat characters. + salt, err := password.Generate(16, 3, 0, true, false) + if err != nil { + log.Error(err) + return nil, status.Errorf(codes.Aborted, "%v", err) + } + + hashedPassword := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(u.Password), []byte(salt), 1, 64*1024, 4, 32)) + + user := rbac.NewUser(uuid.New(), u.Name, roles, string(hashedPassword), u.Token, salt) + err = userService.Add(user) if err != nil { log.Error(err) return nil, status.Errorf(codes.Aborted, "%v", err) @@ -68,8 +78,7 @@ func (u User) GetUser(ctx context.Context, request *apb.GetUserRequest) (*apb.Ge start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - // TODO: implement check if user is allowed to get this user data; only their own if not admin - userData, err := userc.Get(store.Query{Name: request.Name}) + userData, err := userService.Get(store.Query{Name: request.Name}) if err != nil { return nil, err } @@ -93,7 +102,7 @@ func (u User) GetUsers(ctx context.Context, request *apb.GetUsersRequest) (*apb. start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - userList, err := userc.GetAll() + userList, err := userService.GetAll() if err != nil { return nil, err } @@ -120,21 +129,22 @@ func (u User) UpdateUsers(ctx context.Context, request *apb.UpdateUsersRequest) start := metrics.StartHook(labels, grpcRequestsTotal) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) - // TODO: check if current user is allowed to update the user they try to update; only their own if not admin for _, u := range request.User { uid, err := uuid.Parse(u.Id) if err != nil { return nil, handleRPCError(labels, err) } - _, err = userc.Get(store.Query{ID: uid}) + storedUser, err := userService.Get(store.Query{ID: uid}) if err != nil { return nil, status.Errorf(codes.Canceled, "user not found %v", err) } - userToUpdate := rbac.NewUser(uid, u.Name, u.Roles, u.Password, u.Token) + hashedPassword := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(u.Password), []byte(storedUser.GetSalt()), 1, 64*1024, 4, 32)) + + userToUpdate := rbac.NewUser(uid, u.Name, u.Roles, string(hashedPassword), u.Token, storedUser.GetSalt()) - err = userc.Update(userToUpdate) + err = userService.Update(userToUpdate) if err != nil { return nil, status.Errorf(codes.Aborted, "could not update user %v", err) } @@ -153,12 +163,12 @@ func (u User) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest) defer metrics.FinishHook(labels, start, grpcRequestDurationSecondsTotal, grpcRequestDurationSeconds) for _, u := range request.Username { - userToDelete, err := userc.Get(store.Query{Name: u}) + userToDelete, err := userService.Get(store.Query{Name: u}) if err != nil { return nil, status.Errorf(codes.Canceled, "user not found %v", err) } - err = userc.Delete(userToDelete) + err = userService.Delete(userToDelete) if err != nil { return nil, status.Errorf(codes.Aborted, "error deleting user %v", err) } @@ -170,7 +180,7 @@ func (u User) DeleteUsers(ctx context.Context, request *apb.DeleteUsersRequest) } func (u User) isValidUser(user rbac.User) (bool, error) { - storedUser, err := userc.Get(store.Query{Name: user.Name()}) + storedUser, err := userService.Get(store.Query{Name: user.Name()}) if err != nil { return false, err } else if storedUser == nil { @@ -178,7 +188,11 @@ func (u User) isValidUser(user rbac.User) (bool, error) { } if storedUser.Name() == user.Name() { - if storedUser.GetPassword() == user.GetPassword() { + salt := storedUser.GetSalt() + + hashedPasswordFromLogin := base64.RawStdEncoding.EncodeToString(argon2.IDKey([]byte(user.GetPassword()), []byte(salt), 1, 64*1024, 4, 32)) + + if storedUser.GetPassword() == hashedPasswordFromLogin { return true, nil } } diff --git a/controller/northbound/server/user_test.go b/controller/northbound/server/user_test.go index 6611486e1..a5d2d25f2 100644 --- a/controller/northbound/server/user_test.go +++ b/controller/northbound/server/user_test.go @@ -22,13 +22,14 @@ func TestUser_CreateUsers(t *testing.T) { }{ { name: "default create users", - args: args{ctx: context.TODO(), + args: args{ + ctx: context.TODO(), request: &apb.CreateUsersRequest{ User: []*apb.User{ { - Name: "asdf", - Roles: map[string]string{pndID: "asdf"}, - Password: "asdf", + Name: "someUser", + Roles: map[string]string{pndID: "userTestRole"}, + Password: "pass", Token: "", }, }, @@ -40,7 +41,9 @@ func TestUser_CreateUsers(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := User{} + s := User{ + jwtManager: jwt, + } got, err := s.CreateUsers(tt.args.ctx, tt.args.request) if (err != nil) != tt.wantErr { t.Errorf("User.CreateUsers() error = %v, wantErr %v", err, tt.wantErr) diff --git a/controller/rbac/jwtManager.go b/controller/rbac/jwtManager.go index 981310821..3e76ec038 100644 --- a/controller/rbac/jwtManager.go +++ b/controller/rbac/jwtManager.go @@ -64,3 +64,13 @@ func (man *JWTManager) VerifyToken(accessToken string) (*UserClaims, error) { return claims, nil } + +// GetClaimsFromToken returns the UserClaims associated to the provided token. +func (man *JWTManager) GetClaimsFromToken(token string) (*UserClaims, error) { + claims, err := man.VerifyToken(token) + if err != nil { + return nil, err + } + + return claims, nil +} diff --git a/controller/rbac/rbacService.go b/controller/rbac/rbacService.go index e6f000c3d..1cbb6f94d 100644 --- a/controller/rbac/rbacService.go +++ b/controller/rbac/rbacService.go @@ -75,7 +75,7 @@ func (s *UserService) GetAll() ([]rbac.User, error) { } func (s *UserService) createUserFromStore(loadedUser rbac.LoadedUser) rbac.User { - return NewUser(uuid.MustParse(loadedUser.ID), loadedUser.UserName, loadedUser.Roles, loadedUser.Password, loadedUser.Token) + return NewUser(uuid.MustParse(loadedUser.ID), loadedUser.UserName, loadedUser.Roles, loadedUser.Password, loadedUser.Token, loadedUser.Salt) } //RoleService provides a role service implementation. diff --git a/controller/rbac/user.go b/controller/rbac/user.go index 0ff1ecd3c..52514adc5 100644 --- a/controller/rbac/user.go +++ b/controller/rbac/user.go @@ -15,6 +15,7 @@ type User struct { Roles map[string]string `json:"roles,omitempty"` Password string `json:"password"` Token string `json:"token,omitempty"` + Salt string `json:"salt"` } // NewUser creates a new user. @@ -22,13 +23,15 @@ func NewUser(id uuid.UUID, name string, roles map[string]string, pw string, - token string) rbac.User { + token string, + salt string) rbac.User { return &User{ UserID: id, UserName: name, Roles: roles, Password: pw, Token: token, + Salt: salt, } } @@ -42,11 +45,6 @@ func (u *User) Name() string { return u.UserName } -// IsCorrectPassword compares the provided with the stored password of a user. -func (u *User) IsCorrectPassword(pwd string) bool { - return pwd == u.Password -} - // GetPassword returns the password of the user. func (u *User) GetPassword() string { return u.Password @@ -72,6 +70,11 @@ func (u *User) SetToken(token string) { u.Token = token } +// GetSalt returns the salt of the user. +func (u *User) GetSalt() string { + return u.Salt +} + // MarshalJSON implements the MarshalJSON interface to store a user as JSON func (u *User) MarshalJSON() ([]byte, error) { return json.Marshal(&struct { @@ -80,12 +83,14 @@ func (u *User) MarshalJSON() ([]byte, error) { Roles map[string]string `json:"roles,omitempty"` Password string `json:"password"` Token string `json:"token,omitempty"` + Salt string `json:"salt"` }{ UserID: u.ID(), UserName: u.Name(), Roles: u.Roles, Password: u.Password, Token: u.Token, + Salt: u.Salt, }) } @@ -97,11 +102,13 @@ func (u *User) MarshalBSON() ([]byte, error) { Roles map[string]string `bson:"roles,omitempty"` Password string `bson:"password"` Token string `bson:"token,omitempty"` + Salt string `bson:"salt"` }{ UserID: u.ID().String(), UserName: u.Name(), Roles: u.Roles, Password: u.Password, Token: u.Token, + Salt: u.Salt, }) } -- GitLab