diff --git a/controllers/userHttpController.go b/controllers/userHttpController.go index b3a033a8..72332765 100644 --- a/controllers/userHttpController.go +++ b/controllers/userHttpController.go @@ -36,7 +36,6 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) { //Auth request consists of Mac Address and Password (from node that is authorizing //in case of Master, auth is ignored and mac is set to "mastermac" var authRequest models.UserAuthParams - var result models.User var errorResponse = models.ErrorResponse{ Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.", } @@ -44,74 +43,74 @@ func authenticateUser(response http.ResponseWriter, request *http.Request) { decoder := json.NewDecoder(request.Body) decoderErr := decoder.Decode(&authRequest) defer request.Body.Close() - if decoderErr != nil { returnErrorResponse(response, request, errorResponse) return - } else { - errorResponse.Code = http.StatusBadRequest - if authRequest.UserName == "" { - errorResponse.Message = "W1R3: Username can't be empty" - returnErrorResponse(response, request, errorResponse) - return - } else if authRequest.Password == "" { - errorResponse.Message = "W1R3: Password can't be empty" - returnErrorResponse(response, request, errorResponse) - return - } else { - - //Search DB for node with Mac Address. Ignore pending nodes (they should not be able to authenticate with API untill approved). - collection := mongoconn.Client.Database("netmaker").Collection("users") - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - var err = collection.FindOne(ctx, bson.M{"username": authRequest.UserName}).Decode(&result) - - defer cancel() - - if err != nil { - errorResponse.Message = "W1R3: User " + authRequest.UserName + " not found." - returnErrorResponse(response, request, errorResponse) - return - } - - //compare password from request to stored password in database - //might be able to have a common hash (certificates?) and compare those so that a password isn't passed in in plain text... - //TODO: Consider a way of hashing the password client side before sending, or using certificates - err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password)) - if err != nil { - errorResponse = models.ErrorResponse{ - Code: http.StatusUnauthorized, Message: "W1R3: Wrong Password.", - } - returnErrorResponse(response, request, errorResponse) - return - } else { - //Create a new JWT for the node - tokenString, _ := functions.CreateUserJWT(authRequest.UserName, result.IsAdmin) - - if tokenString == "" { - returnErrorResponse(response, request, errorResponse) - return - } - - var successResponse = models.SuccessResponse{ - Code: http.StatusOK, - Message: "W1R3: Device " + authRequest.UserName + " Authorized", - Response: models.SuccessfulUserLoginResponse{ - AuthToken: tokenString, - UserName: authRequest.UserName, - }, - } - //Send back the JWT - successJSONResponse, jsonError := json.Marshal(successResponse) - - if jsonError != nil { - returnErrorResponse(response, request, errorResponse) - return - } - response.Header().Set("Content-Type", "application/json") - response.Write(successJSONResponse) - } - } } + + jwt, err := VerifyAuthRequest(authRequest) + if err != nil { + errorResponse.Code = http.StatusBadRequest + errorResponse.Message = err.Error() + returnErrorResponse(response, request, errorResponse) + } + + if jwt == "" { + returnErrorResponse(response, request, errorResponse) + return + } + + var successResponse = models.SuccessResponse{ + Code: http.StatusOK, + Message: "W1R3: Device " + authRequest.UserName + " Authorized", + Response: models.SuccessfulUserLoginResponse{ + AuthToken: jwt, + UserName: authRequest.UserName, + }, + } + //Send back the JWT + successJSONResponse, jsonError := json.Marshal(successResponse) + + if jsonError != nil { + returnErrorResponse(response, request, errorResponse) + return + } + response.Header().Set("Content-Type", "application/json") + response.Write(successJSONResponse) +} + +func VerifyAuthRequest(authRequest models.UserAuthParams) (string, error) { + var result models.User + if authRequest.UserName == "" { + return "", errors.New("Username can't be empty") + } else if authRequest.Password == "" { + return "", errors.New("Password can't be empty") + } + //Search DB for node with Mac Address. Ignore pending nodes (they should not be able to authenticate with API untill approved). + collection := mongoconn.Client.Database("netmaker").Collection("users") + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + var err = collection.FindOne(ctx, bson.M{"username": authRequest.UserName}).Decode(&result) + + defer cancel() + if err != nil { + return "", errors.New("User " + authRequest.UserName + " not found") + } + // This is a a useless test as cannot create user that is not an an admin + if !result.IsAdmin { + return "", errors.New("User is not an admin") + } + + //compare password from request to stored password in database + //might be able to have a common hash (certificates?) and compare those so that a password isn't passed in in plain text... + //TODO: Consider a way of hashing the password client side before sending, or using certificates + err = bcrypt.CompareHashAndPassword([]byte(result.Password), []byte(authRequest.Password)) + if err != nil { + return "", errors.New("Wrong Password") + } + + //Create a new JWT for the node + tokenString, _ := functions.CreateUserJWT(authRequest.UserName, true) + return tokenString, nil } //The middleware for most requests to the API @@ -132,52 +131,45 @@ func authorizeUser(next http.Handler) http.HandlerFunc { //get the auth token bearerToken := r.Header.Get("Authorization") - - var tokenSplit = strings.Split(bearerToken, " ") - - //I put this in in case the user doesn't put in a token at all (in which case it's empty) - //There's probably a smarter way of handling this. - var authToken = "928rt238tghgwe@TY@$Y@#WQAEGB2FC#@HG#@$Hddd" - - if len(tokenSplit) > 1 { - authToken = tokenSplit[1] - } else { - errorResponse = models.ErrorResponse{ - Code: http.StatusUnauthorized, Message: "W1R3: Missing Auth Token.", - } - returnErrorResponse(w, r, errorResponse) - return - } - - //This checks if - //A: the token is the master password - //B: the token corresponds to a mac address, and if so, which one - //TODO: There's probably a better way of dealing with the "master token"/master password. Plz Halp. - username, _, err := functions.VerifyUserToken(authToken) - + err := ValidateToken(bearerToken) if err != nil { - errorResponse = models.ErrorResponse{ - Code: http.StatusUnauthorized, Message: "W1R3: Error Verifying Auth Token.", - } returnErrorResponse(w, r, errorResponse) return } - - isAuthorized := username != "" - - if !isAuthorized { - errorResponse = models.ErrorResponse{ - Code: http.StatusUnauthorized, Message: "W1R3: You are unauthorized to access this endpoint.", - } - returnErrorResponse(w, r, errorResponse) - return - } else { - //If authorized, this function passes along it's request and output to the appropriate route function. - next.ServeHTTP(w, r) - } + next.ServeHTTP(w, r) } } +func ValidateToken(token string) error { + var tokenSplit = strings.Split(token, " ") + + //I put this in in case the user doesn't put in a token at all (in which case it's empty) + //There's probably a smarter way of handling this. + var authToken = "928rt238tghgwe@TY@$Y@#WQAEGB2FC#@HG#@$Hddd" + + if len(tokenSplit) > 1 { + authToken = tokenSplit[1] + } else { + return errors.New("Missing Auth Token.") + } + + //This checks if + //A: the token is the master password + //B: the token corresponds to a mac address, and if so, which one + //TODO: There's probably a better way of dealing with the "master token"/master password. Plz Halp. + username, _, err := functions.VerifyUserToken(authToken) + if err != nil { + return errors.New("Error Verifying Auth Token") + } + + isAuthorized := username != "" + if !isAuthorized { + return errors.New("You are unauthorized to access this endpoint.") + } + + return nil +} + func HasAdmin() (bool, error) { collection := mongoconn.Client.Database("netmaker").Collection("users") @@ -249,10 +241,19 @@ func getUser(w http.ResponseWriter, r *http.Request) { } func CreateUser(user models.User) (models.User, error) { + hasadmin, err := HasAdmin() + if hasadmin { + return models.User{}, errors.New("Admin already Exists") + } + + user.IsAdmin = true + err = ValidateUser("create", user) + if err != nil { + return models.User{}, err + } //encrypt that password so we never see it again hash, err := bcrypt.GenerateFromPassword([]byte(user.Password), 5) - if err != nil { return user, err } @@ -271,9 +272,7 @@ func CreateUser(user models.User) (models.User, error) { ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) // insert our node to the node db. - result, err := collection.InsertOne(ctx, user) - _ = result - + _, err = collection.InsertOne(ctx, user) defer cancel() return user, err @@ -282,34 +281,11 @@ func CreateUser(user models.User) (models.User, error) { func createAdmin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") - var errorResponse = models.ErrorResponse{ - Code: http.StatusInternalServerError, Message: "W1R3: It's not you it's me.", - } - - hasadmin, err := HasAdmin() - - if hasadmin { - errorResponse = models.ErrorResponse{ - Code: http.StatusUnauthorized, Message: "W1R3: Admin already exists! ", - } - returnErrorResponse(w, r, errorResponse) - return - } - var admin models.User - //get node from body of request _ = json.NewDecoder(r.Body).Decode(&admin) - admin.IsAdmin = true - - err = ValidateUser("create", admin) - if err != nil { - json.NewEncoder(w).Encode(err) - return - } - - admin, err = CreateUser(admin) + admin, err := CreateUser(admin) if err != nil { json.NewEncoder(w).Encode(err) return @@ -320,6 +296,11 @@ func createAdmin(w http.ResponseWriter, r *http.Request) { func UpdateUser(userchange models.User, user models.User) (models.User, error) { + err := ValidateUser("update", userchange) + if err != nil { + return models.User{}, err + } + queryUser := user.UserName if userchange.UserName != "" { @@ -367,7 +348,7 @@ func UpdateUser(userchange models.User, user models.User) (models.User, error) { defer cancel() - return userupdate, errN + return user, errN } func updateUser(w http.ResponseWriter, r *http.Request) { @@ -393,15 +374,6 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } - userchange.IsAdmin = true - - err = ValidateUser("update", userchange) - - if err != nil { - json.NewEncoder(w).Encode(err) - return - } - user, err = UpdateUser(userchange, user) if err != nil { @@ -458,23 +430,6 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { func ValidateUser(operation string, user models.User) error { v := validator.New() - - _ = v.RegisterValidation("username_unique", func(fl validator.FieldLevel) bool { - _, err := GetUser(user.UserName) - return err == nil || operation == "update" - }) - - _ = v.RegisterValidation("username_valid", func(fl validator.FieldLevel) bool { - isvalid := functions.NameInNodeCharSet(user.UserName) - return isvalid - }) - - _ = v.RegisterValidation("password_check", func(fl validator.FieldLevel) bool { - notEmptyCheck := user.Password != "" - goodLength := len(user.Password) > 5 - return (notEmptyCheck && goodLength) || operation == "update" - }) - err := v.Struct(user) if err != nil { diff --git a/controllers/userHttpController_test.go b/controllers/userHttpController_test.go index 20420450..659b33a6 100644 --- a/controllers/userHttpController_test.go +++ b/controllers/userHttpController_test.go @@ -13,9 +13,6 @@ import ( func TestMain(m *testing.M) { mongoconn.ConnectDatabase() - //var waitgroup sync.WaitGroup - //waitgroup.Add(1) - //go controller.HandleRESTRequests(&waitgroup) var gconf models.GlobalConfig gconf.ServerGRPC = "localhost:8081" gconf.PortGRPC = "50051" @@ -28,17 +25,49 @@ func TestMain(m *testing.M) { if err != nil { panic("could not create config store") } - - //wait for http server to start - //time.Sleep(time.Second * 1) os.Exit(m.Run()) } +func TestHasAdmin(t *testing.T) { + _, err := DeleteUser("admin") + assert.Nil(t, err) + user := models.User{"admin", "admin", true} + _, err = CreateUser(user) + assert.Nil(t, err, err) + t.Run("AdminExists", func(t *testing.T) { + found, err := HasAdmin() + assert.Nil(t, err, err) + assert.True(t, found) + }) + t.Run("NoUser", func(t *testing.T) { + _, err := DeleteUser("admin") + assert.Nil(t, err, err) + found, err := HasAdmin() + assert.Nil(t, err, err) + assert.False(t, found) + }) +} + +func TestCreateUser(t *testing.T) { + user := models.User{"admin", "admin", true} + t.Run("NoUser", func(t *testing.T) { + _, err := DeleteUser("admin") + assert.Nil(t, err, err) + admin, err := CreateUser(user) + assert.Nil(t, err, err) + assert.Equal(t, user.UserName, admin.UserName) + }) + t.Run("AdminExists", func(t *testing.T) { + _, err := CreateUser(user) + assert.NotNil(t, err, err) + assert.Equal(t, "Admin already Exists", err.Error()) + }) +} + func TestDeleteUser(t *testing.T) { hasadmin, err := HasAdmin() assert.Nil(t, err, err) if !hasadmin { - //if !adminExists() { user := models.User{"admin", "admin", true} _, err := CreateUser(user) assert.Nil(t, err, err) @@ -47,6 +76,7 @@ func TestDeleteUser(t *testing.T) { deleted, err := DeleteUser("admin") assert.Nil(t, err, err) assert.True(t, deleted) + t.Log(deleted, err) }) t.Run("NonExistantUser", func(t *testing.T) { deleted, err := DeleteUser("admin") @@ -54,3 +84,155 @@ func TestDeleteUser(t *testing.T) { assert.False(t, deleted) }) } + +func TestValidateUser(t *testing.T) { + var user models.User + t.Run("ValidCreate", func(t *testing.T) { + user.UserName = "admin" + user.Password = "validpass" + err := ValidateUser("create", user) + assert.Nil(t, err, err) + }) + t.Run("ValidUpdate", func(t *testing.T) { + user.UserName = "admin" + user.Password = "admin" + err := ValidateUser("update", user) + assert.Nil(t, err, err) + }) + t.Run("InvalidUserName", func(t *testing.T) { + user.UserName = "invalid*" + err := ValidateUser("update", user) + assert.NotNil(t, err, err) + }) + t.Run("ShortUserName", func(t *testing.T) { + user.UserName = "12" + err := ValidateUser("create", user) + assert.NotNil(t, err, err) + }) + t.Run("EmptyPassword", func(t *testing.T) { + user.Password = "" + err := ValidateUser("create", user) + assert.NotNil(t, err, err) + }) + t.Run("ShortPassword", func(t *testing.T) { + user.Password = "123" + err := ValidateUser("create", user) + assert.NotNil(t, err, err) + }) +} + +func TestGetUser(t *testing.T) { + user := models.User{"admin", "admin", true} + t.Run("UserExisits", func(t *testing.T) { + _, err := CreateUser(user) + assert.Nil(t, err, err) + admin, err := GetUser("admin") + assert.Nil(t, err, err) + assert.Equal(t, user.UserName, admin.UserName) + }) + t.Run("NonExistantUser", func(t *testing.T) { + _, err := DeleteUser("admin") + assert.Nil(t, err, err) + admin, err := GetUser("admin") + assert.Equal(t, "mongo: no documents in result", err.Error()) + assert.Equal(t, "", admin.UserName) + }) +} + +func TestUpdateUser(t *testing.T) { + user := models.User{"admin", "admin", true} + newuser := models.User{"hello", "world", true} + t.Run("UserExisits", func(t *testing.T) { + _, err := DeleteUser("admin") + _, err = CreateUser(user) + assert.Nil(t, err, err) + admin, err := UpdateUser(newuser, user) + assert.Nil(t, err, err) + assert.Equal(t, newuser.UserName, admin.UserName) + }) + t.Run("NonExistantUser", func(t *testing.T) { + _, err := DeleteUser("hello") + assert.Nil(t, err, err) + _, err = UpdateUser(newuser, user) + assert.Equal(t, "mongo: no documents in result", err.Error()) + }) +} + +func TestValidateToken(t *testing.T) { + t.Run("EmptyToken", func(t *testing.T) { + err := ValidateToken("") + assert.NotNil(t, err, err) + assert.Equal(t, "Missing Auth Token.", err.Error()) + }) + t.Run("InvalidToken", func(t *testing.T) { + err := ValidateToken("Bearer: badtoken") + assert.NotNil(t, err, err) + assert.Equal(t, "Error Verifying Auth Token", err.Error()) + }) + t.Run("InvalidUser", func(t *testing.T) { + t.Skip() + //need authorization + }) + t.Run("ValidToken", func(t *testing.T) { + err := ValidateToken("Bearer: secretkey") + assert.Nil(t, err, err) + }) +} + +func TestVerifyAuthRequest(t *testing.T) { + var authRequest models.UserAuthParams + t.Run("EmptyUserName", func(t *testing.T) { + authRequest.UserName = "" + authRequest.Password = "Password" + jwt, err := VerifyAuthRequest(authRequest) + assert.NotNil(t, err, err) + assert.Equal(t, "", jwt) + assert.Equal(t, "Username can't be empty", err.Error()) + }) + t.Run("EmptyPassword", func(t *testing.T) { + authRequest.UserName = "admin" + authRequest.Password = "" + jwt, err := VerifyAuthRequest(authRequest) + assert.NotNil(t, err, err) + assert.Equal(t, "", jwt) + assert.Equal(t, "Password can't be empty", err.Error()) + }) + t.Run("NonExistantUser", func(t *testing.T) { + _, err := DeleteUser("admin") + authRequest.UserName = "admin" + authRequest.Password = "password" + jwt, err := VerifyAuthRequest(authRequest) + assert.NotNil(t, err, err) + assert.Equal(t, "", jwt) + assert.Equal(t, "User admin not found", err.Error()) + }) + t.Run("Non-Admin", func(t *testing.T) { + //can't create a user that is not a an admin + t.Skip() + user := models.User{"admin", "admin", false} + _, err := CreateUser(user) + assert.Nil(t, err) + authRequest := models.UserAuthParams{"admin", "admin"} + jwt, err := VerifyAuthRequest(authRequest) + assert.NotNil(t, err, err) + assert.Equal(t, "", jwt) + assert.Equal(t, "User is not an admin", err.Error()) + }) + t.Run("WrongPassword", func(t *testing.T) { + _, err := DeleteUser("admin") + user := models.User{"admin", "admin", true} + _, err = CreateUser(user) + assert.Nil(t, err) + authRequest := models.UserAuthParams{"admin", "badpass"} + jwt, err := VerifyAuthRequest(authRequest) + assert.NotNil(t, err, err) + assert.Equal(t, "", jwt) + assert.Equal(t, "Wrong Password", err.Error()) + }) + t.Run("Success", func(t *testing.T) { + authRequest := models.UserAuthParams{"admin", "admin"} + jwt, err := VerifyAuthRequest(authRequest) + assert.Nil(t, err, err) + assert.NotNil(t, jwt) + }) +} diff --git a/models/structs.go b/models/structs.go index acb7cbc6..8603720d 100644 --- a/models/structs.go +++ b/models/structs.go @@ -3,112 +3,109 @@ package models import jwt "github.com/dgrijalva/jwt-go" type AuthParams struct { - MacAddress string `json:"macaddress"` - Password string `json:"password"` + MacAddress string `json:"macaddress"` + Password string `json:"password"` } type User struct { - UserName string `json:"username" bson:"username" validate:username_valid,username_unique,min=3` - Password string `json:"password" bson:"password" validate:password_check` - IsAdmin bool `json:"isadmin" bson:"isadmin"` + UserName string `json:"username" bson:"username" validate:"alphanum,min=3"` + Password string `json:"password" bson:"password" validate:"required,min=5"` + IsAdmin bool `json:"isadmin" bson:"isadmin"` } type UserAuthParams struct { - UserName string `json:"username"` - Password string `json:"password"` + UserName string `json:"username"` + Password string `json:"password"` } type UserClaims struct { - IsAdmin bool - UserName string - jwt.StandardClaims + IsAdmin bool + UserName string + jwt.StandardClaims } type SuccessfulUserLoginResponse struct { - UserName string - AuthToken string + UserName string + AuthToken string } // Claims is a struct that will be encoded to a JWT. // jwt.StandardClaims is an embedded type to provide expiry time type Claims struct { - Network string - MacAddress string - jwt.StandardClaims + Network string + MacAddress string + jwt.StandardClaims } // SuccessfulLoginResponse is struct to send the request response type SuccessfulLoginResponse struct { - MacAddress string - AuthToken string + MacAddress string + AuthToken string } type ErrorResponse struct { - Code int - Message string + Code int + Message string } type NodeAuth struct { - Network string - Password string - MacAddress string + Network string + Password string + MacAddress string } // SuccessResponse is struct for sending error message with code. type SuccessResponse struct { - Code int - Message string - Response interface{} + Code int + Message string + Response interface{} } type AccessKey struct { - Name string `json:"name" bson:"name"` - Value string `json:"value" bson:"value"` - AccessString string `json:"accessstring" bson:"accessstring"` - Uses int `json:"uses" bson:"uses"` + Name string `json:"name" bson:"name"` + Value string `json:"value" bson:"value"` + AccessString string `json:"accessstring" bson:"accessstring"` + Uses int `json:"uses" bson:"uses"` } type DisplayKey struct { - Name string `json:"name" bson:"name"` - Uses int `json:"uses" bson:"uses"` + Name string `json:"name" bson:"name"` + Uses int `json:"uses" bson:"uses"` } type GlobalConfig struct { - Name string `json:"name" bson:"name"` - PortGRPC string `json:"portgrpc" bson:"portgrpc"` - ServerGRPC string `json:"servergrpc" bson:"servergrpc"` + Name string `json:"name" bson:"name"` + PortGRPC string `json:"portgrpc" bson:"portgrpc"` + ServerGRPC string `json:"servergrpc" bson:"servergrpc"` } - -type CheckInResponse struct{ - Success bool `json:"success" bson:"success"` - NeedPeerUpdate bool `json:"needpeerupdate" bson:"needpeerupdate"` - NeedConfigUpdate bool `json:"needconfigupdate" bson:"needconfigupdate"` - NeedKeyUpdate bool `json:"needkeyupdate" bson:"needkeyupdate"` - NeedDelete bool `json:"needdelete" bson:"needdelete"` - NodeMessage string `json:"nodemessage" bson:"nodemessage"` - IsPending bool `json:"ispending" bson:"ispending"` +type CheckInResponse struct { + Success bool `json:"success" bson:"success"` + NeedPeerUpdate bool `json:"needpeerupdate" bson:"needpeerupdate"` + NeedConfigUpdate bool `json:"needconfigupdate" bson:"needconfigupdate"` + NeedKeyUpdate bool `json:"needkeyupdate" bson:"needkeyupdate"` + NeedDelete bool `json:"needdelete" bson:"needdelete"` + NodeMessage string `json:"nodemessage" bson:"nodemessage"` + IsPending bool `json:"ispending" bson:"ispending"` } type PeersResponse struct { - PublicKey string `json:"publickey" bson:"publickey"` - Endpoint string `json:"endpoint" bson:"endpoint"` - Address string `json:"address" bson:"address"` - LocalAddress string `json:"localaddress" bson:"localaddress"` - IsGateway bool `json:"isgateway" bson:"isgateway"` - GatewayRange string `json:"gatewayrange" bson:"gatewayrange"` - ListenPort int32 `json:"listenport" bson:"listenport"` - KeepAlive int32 `json:"persistentkeepalive" bson:"persistentkeepalive"` + PublicKey string `json:"publickey" bson:"publickey"` + Endpoint string `json:"endpoint" bson:"endpoint"` + Address string `json:"address" bson:"address"` + LocalAddress string `json:"localaddress" bson:"localaddress"` + IsGateway bool `json:"isgateway" bson:"isgateway"` + GatewayRange string `json:"gatewayrange" bson:"gatewayrange"` + ListenPort int32 `json:"listenport" bson:"listenport"` + KeepAlive int32 `json:"persistentkeepalive" bson:"persistentkeepalive"` } type GatewayRequest struct { - NodeID string `json:"nodeid" bson:"nodeid"` - NetID string `json:"netid" bson:"netid"` - RangeString string `json:"rangestring" bson:"rangestring"` - Ranges []string `json:"ranges" bson:"ranges"` - Interface string `json:"interface" bson:"interface"` - PostUp string `json:"postup" bson:"postup"` - PostDown string `json:"postdown" bson:"postdown"` + NodeID string `json:"nodeid" bson:"nodeid"` + NetID string `json:"netid" bson:"netid"` + RangeString string `json:"rangestring" bson:"rangestring"` + Ranges []string `json:"ranges" bson:"ranges"` + Interface string `json:"interface" bson:"interface"` + PostUp string `json:"postup" bson:"postup"` + PostDown string `json:"postdown" bson:"postdown"` } - -