diff --git a/config.json b/config.json index 0ad2c57c4..6b4d8fc2e 100644 --- a/config.json +++ b/config.json @@ -12,5 +12,7 @@ "webhook_update": [], "secret": "this-is-a-secret-string", "session_expire_time": 2592000, - "session_refresh_time": 18000 + "session_refresh_time": 18000, + "enableLocalMode": true, + "localModeSocketLocation": "/var/tmp/octo_local.socket" } diff --git a/server/api/admin.go b/server/api/admin.go new file mode 100644 index 000000000..fd080ea0e --- /dev/null +++ b/server/api/admin.go @@ -0,0 +1,48 @@ +package api + +import ( + "encoding/json" + "io/ioutil" + "log" + "net/http" + "strings" + + "github.com/gorilla/mux" +) + +type AdminSetPasswordData struct { + Password string `json:"password"` +} + +func (a *API) handleAdminSetPassword(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + username := vars["username"] + + requestBody, err := ioutil.ReadAll(r.Body) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil, err) + return + } + + var requestData AdminSetPasswordData + err = json.Unmarshal(requestBody, &requestData) + if err != nil { + errorResponse(w, http.StatusInternalServerError, nil, err) + return + } + + if !strings.Contains(requestData.Password, "") { + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": "password is required"}, err) + return + } + + err = a.app().UpdateUserPassword(username, requestData.Password) + if err != nil { + errorResponse(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}, err) + return + } + + log.Printf("AdminSetPassword, username: %s", username) + + jsonBytesResponse(w, http.StatusOK, nil) +} diff --git a/server/api/api.go b/server/api/api.go index 206a6fa2c..9d1e70a04 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -60,6 +60,10 @@ func (a *API) RegisterRoutes(r *mux.Router) { r.HandleFunc("/api/v1/workspace/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST") } +func (a *API) RegisterAdminRoutes(r *mux.Router) { + r.HandleFunc("/api/v1/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST") +} + func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() parentID := query.Get("parent_id") diff --git a/server/api/auth.go b/server/api/auth.go index d4d6a146e..33abc4e2c 100644 --- a/server/api/auth.go +++ b/server/api/auth.go @@ -6,11 +6,13 @@ import ( "errors" "io/ioutil" "log" + "net" "net/http" "strings" "time" "github.com/gorilla/mux" + serverContext "github.com/mattermost/mattermost-octo-tasks/server/context" "github.com/mattermost/mattermost-octo-tasks/server/model" "github.com/mattermost/mattermost-octo-tasks/server/services/auth" ) @@ -226,3 +228,17 @@ func (a *API) attachSession(handler func(w http.ResponseWriter, r *http.Request) handler(w, r.WithContext(ctx)) } } + +func (a *API) adminRequired(handler func(w http.ResponseWriter, r *http.Request)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + // Currently, admin APIs require local unix connections + conn := serverContext.GetContextConn(r) + if _, isUnix := conn.(*net.UnixConn); !isUnix { + errorResponse(w, http.StatusUnauthorized, nil, nil) + return + } + + handler(w, r) + return + } +} diff --git a/server/app/auth.go b/server/app/auth.go index 8210cb4dc..ab5bfeb2a 100644 --- a/server/app/auth.go +++ b/server/app/auth.go @@ -133,6 +133,15 @@ func (a *App) RegisterUser(username string, email string, password string) error return nil } +func (a *App) UpdateUserPassword(username string, password string) error { + err := a.store.UpdateUserPassword(username, auth.HashPassword(password)) + if err != nil { + return err + } + + return nil +} + func (a *App) ChangePassword(userID string, oldPassword string, newPassword string) error { var user *model.User if userID != "" { diff --git a/server/context/context.go b/server/context/context.go new file mode 100644 index 000000000..0a877488c --- /dev/null +++ b/server/context/context.go @@ -0,0 +1,28 @@ +package context + +import ( + "context" + "net" + "net/http" +) + +type contextKey struct { + key string +} + +var connContextKey = &contextKey{"http-conn"} + +// SetContextConn stores the connection in the request context +func SetContextConn(ctx context.Context, c net.Conn) context.Context { + return context.WithValue(ctx, connContextKey, c) +} + +// GetContextConn gets the stored connection from the request context +func GetContextConn(r *http.Request) net.Conn { + value := r.Context().Value(connContextKey) + if value == nil { + return nil + } + + return value.(net.Conn) +} diff --git a/server/go.sum b/server/go.sum index e734aa074..487be97f6 100644 --- a/server/go.sum +++ b/server/go.sum @@ -1029,6 +1029,7 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de h1:ikNHVSjEfnvz6sxdSPCaPt572qowuyMDMJLLm3Db3ig= golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= diff --git a/server/main/main.go b/server/main/main.go index d6db3997c..4afd64a5a 100644 --- a/server/main/main.go +++ b/server/main/main.go @@ -83,10 +83,10 @@ func main() { server, err := server.New(config, singleUser) if err != nil { - log.Fatal("ListenAndServeTLS: ", err) + log.Fatal("server.New ERROR: ", err) } if err := server.Start(); err != nil { - log.Fatal("ListenAndServeTLS: ", err) + log.Fatal("server.Start ERROR: ", err) } } diff --git a/server/scripts/reset-password.sh b/server/scripts/reset-password.sh new file mode 100755 index 000000000..6b4891ad9 --- /dev/null +++ b/server/scripts/reset-password.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +if [[ $# < 2 ]] ; then + echo 'reset-password.sh ' + exit 1 +fi + +curl --unix-socket /var/tmp/octo_local.socket http://localhost/api/v1/admin/users/$1/password -X POST -H 'Content-Type: application/json' -d '{ "password": "'$2'" }' diff --git a/server/server/server.go b/server/server/server.go index f6e26a014..3fc05b192 100644 --- a/server/server/server.go +++ b/server/server/server.go @@ -1,19 +1,23 @@ package server import ( - "errors" "log" + "net" + "net/http" "os" "os/signal" "runtime" "sync" + "syscall" "time" "github.com/google/uuid" + "github.com/gorilla/mux" "go.uber.org/zap" "github.com/mattermost/mattermost-octo-tasks/server/api" "github.com/mattermost/mattermost-octo-tasks/server/app" + "github.com/mattermost/mattermost-octo-tasks/server/context" appModel "github.com/mattermost/mattermost-octo-tasks/server/model" "github.com/mattermost/mattermost-octo-tasks/server/services/config" "github.com/mattermost/mattermost-octo-tasks/server/services/scheduler" @@ -26,6 +30,8 @@ import ( "github.com/mattermost/mattermost-server/utils" "github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost-server/v5/services/filesstore" + + "github.com/pkg/errors" ) type Server struct { @@ -37,6 +43,9 @@ type Server struct { telemetry *telemetry.Service logger *zap.Logger cleanUpSessionsTask *scheduler.ScheduledTask + + localRouter *mux.Router + localModeServer *http.Server } func New(cfg *config.Configuration, singleUser bool) (*Server, error) { @@ -68,6 +77,10 @@ func New(cfg *config.Configuration, singleUser bool) (*Server, error) { appBuilder := func() *app.App { return app.New(cfg, store, wsServer, filesBackend, webhookClient) } api := api.NewAPI(appBuilder, singleUser) + // Local router for admin APIs + localRouter := mux.NewRouter() + api.RegisterAdminRoutes(localRouter) + // Init workspace appBuilder().GetRootWorkspace() @@ -132,6 +145,7 @@ func New(cfg *config.Configuration, singleUser bool) (*Server, error) { filesBackend: filesBackend, telemetry: telemetryService, logger: logger, + localRouter: localRouter, }, nil } @@ -141,6 +155,12 @@ func (s *Server) Start() error { s.webServer.Start(httpServerExitDone) + if s.config.EnableLocalMode { + if err := s.startLocalModeServer(); err != nil { + return err + } + } + s.cleanUpSessionsTask = scheduler.CreateRecurringTask("cleanUpSessions", func() { if err := s.store.CleanUpSessions(s.config.SessionExpireTime); err != nil { s.logger.Error("Unable to clean up the sessions", zap.Error(err)) @@ -162,6 +182,8 @@ func (s *Server) Shutdown() error { return err } + s.stopLocalModeServer() + if s.cleanUpSessionsTask != nil { s.cleanUpSessionsTask.Cancel() } @@ -174,3 +196,41 @@ func (s *Server) Shutdown() error { func (s *Server) Config() *config.Configuration { return s.config } + +// Local server + +func (s *Server) startLocalModeServer() error { + s.localModeServer = &http.Server{ + Handler: s.localRouter, + ConnContext: context.SetContextConn, + } + + // TODO: Close and delete socket file on shutdown + syscall.Unlink(s.config.LocalModeSocketLocation) + + socket := s.config.LocalModeSocketLocation + unixListener, err := net.Listen("unix", socket) + if err != nil { + return err + } + if err = os.Chmod(socket, 0600); err != nil { + return err + } + + go func() { + log.Println("Starting unix socket server") + err = s.localModeServer.Serve(unixListener) + if err != nil && err != http.ErrServerClosed { + log.Fatalf("Error starting unix socket server: %v", err) + } + }() + + return nil +} + +func (s *Server) stopLocalModeServer() { + if s.localModeServer != nil { + s.localModeServer.Close() + s.localModeServer = nil + } +} diff --git a/server/services/config/config.go b/server/services/config/config.go index 95a2adfba..d15ac32b7 100644 --- a/server/services/config/config.go +++ b/server/services/config/config.go @@ -13,18 +13,20 @@ const ( // Configuration is the app configuration stored in a json file. type Configuration struct { - ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"` - Port int `json:"port" mapstructure:"port"` - DBType string `json:"dbtype" mapstructure:"dbtype"` - DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"` - UseSSL bool `json:"useSSL" mapstructure:"useSSL"` - WebPath string `json:"webpath" mapstructure:"webpath"` - FilesPath string `json:"filespath" mapstructure:"filespath"` - Telemetry bool `json:"telemetry" mapstructure:"telemetry"` - WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"` - Secret string `json:"secret" mapstructure:"secret"` - SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"` - SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"` + ServerRoot string `json:"serverRoot" mapstructure:"serverRoot"` + Port int `json:"port" mapstructure:"port"` + DBType string `json:"dbtype" mapstructure:"dbtype"` + DBConfigString string `json:"dbconfig" mapstructure:"dbconfig"` + UseSSL bool `json:"useSSL" mapstructure:"useSSL"` + WebPath string `json:"webpath" mapstructure:"webpath"` + FilesPath string `json:"filespath" mapstructure:"filespath"` + Telemetry bool `json:"telemetry" mapstructure:"telemetry"` + WebhookUpdate []string `json:"webhook_update" mapstructure:"webhook_update"` + Secret string `json:"secret" mapstructure:"secret"` + SessionExpireTime int64 `json:"session_expire_time" mapstructure:"session_expire_time"` + SessionRefreshTime int64 `json:"session_refresh_time" mapstructure:"session_refresh_time"` + EnableLocalMode bool `json:"enableLocalMode" mapstructure:"enableLocalMode"` + LocalModeSocketLocation string `json:"localModeSocketLocation" mapstructure:"localModeSocketLocation"` } // ReadConfigFile read the configuration from the filesystem. @@ -41,6 +43,8 @@ func ReadConfigFile() (*Configuration, error) { viper.SetDefault("WebhookUpdate", nil) viper.SetDefault("SessionExpireTime", 60*60*24*30) // 30 days session lifetime viper.SetDefault("SessionRefreshTime", 60*60*5) // 5 minutes session refresh + viper.SetDefault("EnableLocalMode", false) + viper.SetDefault("LocalModeSocketLocation", "/var/tmp/octo_local.socket") err := viper.ReadInConfig() // Find and read the config file if err != nil { // Handle errors reading the config file diff --git a/server/services/store/mockstore/mockstore.go b/server/services/store/mockstore/mockstore.go index 77b613326..0bd25b76c 100644 --- a/server/services/store/mockstore/mockstore.go +++ b/server/services/store/mockstore/mockstore.go @@ -427,6 +427,20 @@ func (mr *MockStoreMockRecorder) UpdateUser(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUser", reflect.TypeOf((*MockStore)(nil).UpdateUser), arg0) } +// UpdateUserPassword mocks base method +func (m *MockStore) UpdateUserPassword(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateUserPassword", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateUserPassword indicates an expected call of UpdateUserPassword +func (mr *MockStoreMockRecorder) UpdateUserPassword(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateUserPassword", reflect.TypeOf((*MockStore)(nil).UpdateUserPassword), arg0, arg1) +} + // UpdateUserPasswordByID mocks base method func (m *MockStore) UpdateUserPasswordByID(arg0, arg1 string) error { m.ctrl.T.Helper() diff --git a/server/services/store/sqlstore/user.go b/server/services/store/sqlstore/user.go index 7c7c78d81..01593e1e5 100644 --- a/server/services/store/sqlstore/user.go +++ b/server/services/store/sqlstore/user.go @@ -88,12 +88,25 @@ func (s *SQLStore) UpdateUser(user *model.User) error { Set("username", user.Username). Set("email", user.Email). Set("props", propsBytes). - Set("update_at", now) + Set("update_at", now). + Where(sq.Eq{"id": user.ID}) _, err = query.Exec() return err } +func (s *SQLStore) UpdateUserPassword(username string, password string) error { + now := time.Now().Unix() + + query := s.getQueryBuilder().Update("users"). + Set("password", password). + Set("update_at", now). + Where(sq.Eq{"username": username}) + + _, err := query.Exec() + return err +} + func (s *SQLStore) UpdateUserPasswordByID(userID string, password string) error { now := time.Now().Unix() diff --git a/server/services/store/store.go b/server/services/store/store.go index 51405e673..e7bf73acd 100644 --- a/server/services/store/store.go +++ b/server/services/store/store.go @@ -27,6 +27,7 @@ type Store interface { GetUserByUsername(username string) (*model.User, error) CreateUser(user *model.User) error UpdateUser(user *model.User) error + UpdateUserPassword(username string, password string) error UpdateUserPasswordByID(userID string, password string) error GetSession(token string, expireTime int64) (*model.Session, error)