Merge pull request #32 from mattermost/admin-local

Admin local unix socket server
This commit is contained in:
Chen-I Lim 2021-01-25 08:52:03 -08:00 committed by GitHub
commit fc8f9579eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 225 additions and 17 deletions

View File

@ -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"
}

48
server/api/admin.go Normal file
View File

@ -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)
}

View File

@ -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")

View File

@ -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
}
}

View File

@ -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 != "" {

28
server/context/context.go Normal file
View File

@ -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)
}

View File

@ -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=

View File

@ -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)
}
}

View File

@ -0,0 +1,8 @@
#!/bin/bash
if [[ $# < 2 ]] ; then
echo 'reset-password.sh <username> <new password>'
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'" }'

View File

@ -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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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)