diff --git a/api/command.go b/api/command.go index 449483bbf9..aedbe07cc1 100644 --- a/api/command.go +++ b/api/command.go @@ -9,6 +9,8 @@ import ( "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" "net/http" + "reflect" + "runtime" "strconv" "strings" ) @@ -19,16 +21,13 @@ var commands = []commandHandler{ logoutCommand, joinCommand, loadTestCommand, + echoCommand, } func InitCommand(r *mux.Router) { l4g.Debug("Initializing command api routes") r.Handle("/command", ApiUserRequired(command)).Methods("POST") - if utils.Cfg.TeamSettings.AllowValet { - commands = append(commands, echoCommand) - } - hub.Start() } @@ -59,6 +58,8 @@ func checkCommand(c *Context, command *model.Command) bool { return false } + tchan := Srv.Store.Team().Get(c.Session.TeamId) + if len(command.ChannelId) > 0 { cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, command.ChannelId, c.Session.UserId) @@ -67,7 +68,21 @@ func checkCommand(c *Context, command *model.Command) bool { } } + allowValet := false + if tResult := <-tchan; tResult.Err != nil { + c.Err = model.NewAppError("checkCommand", "Could not find the team for this session, team_id="+c.Session.TeamId, "") + return false + } else { + allowValet = tResult.Data.(*model.Team).AllowValet + } + + ec := runtime.FuncForPC(reflect.ValueOf(echoCommand).Pointer()).Name() + for _, v := range commands { + if !allowValet && ec == runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() { + continue + } + if v(c, command) { return true } else if c.Err != nil { diff --git a/api/post.go b/api/post.go index 3acc955515..99cbdcb852 100644 --- a/api/post.go +++ b/api/post.go @@ -58,11 +58,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) { - if !utils.Cfg.TeamSettings.AllowValet { - c.Err = model.NewAppError("createValetPost", "The valet feature is currently turned off. Please contact your system administrator for details.", "") - c.Err.StatusCode = http.StatusNotImplemented - return - } + tchan := Srv.Store.Team().Get(c.Session.TeamId) post := model.PostFromJson(r.Body) if post == nil { @@ -70,13 +66,25 @@ func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) { return } - // Any one with access to the team can post as valet to any open channel cchan := Srv.Store.Channel().CheckOpenChannelPermissions(c.Session.TeamId, post.ChannelId) + // Any one with access to the team can post as valet to any open channel if !c.HasPermissionsToChannel(cchan, "createValetPost") { return } + // Make sure this team has the valet feature enabled + if tResult := <-tchan; tResult.Err != nil { + c.Err = model.NewAppError("createValetPost", "Could not find the team for this session, team_id="+c.Session.TeamId, "") + return + } else { + if !tResult.Data.(*model.Team).AllowValet { + c.Err = model.NewAppError("createValetPost", "The valet feature is currently turned off. Please contact your team administrator for details.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + } + if rp, err := CreateValetPost(c, post); err != nil { c.Err = err diff --git a/api/post_test.go b/api/post_test.go index b322a5017d..03f70bff7d 100644 --- a/api/post_test.go +++ b/api/post_test.go @@ -147,7 +147,7 @@ func TestCreateValetPost(t *testing.T) { channel2 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id} channel2 = Client.Must(Client.CreateChannel(channel2)).Data.(*model.Channel) - if utils.Cfg.TeamSettings.AllowValet { + if utils.Cfg.TeamSettings.AllowValetDefault { post1 := &model.Post{ChannelId: channel1.Id, Message: "#hashtag a" + model.NewId() + "a"} rpost1, err := Client.CreateValetPost(post1) if err != nil { diff --git a/api/team.go b/api/team.go index cb60602c68..775bc29ae2 100644 --- a/api/team.go +++ b/api/team.go @@ -29,6 +29,8 @@ func InitTeam(r *mux.Router) { sr.Handle("/email_teams", ApiAppHandler(emailTeams)).Methods("POST") sr.Handle("/invite_members", ApiUserRequired(inviteMembers)).Methods("POST") sr.Handle("/update_name", ApiUserRequired(updateTeamName)).Methods("POST") + sr.Handle("/update_valet_feature", ApiUserRequired(updateValetFeature)).Methods("POST") + sr.Handle("/me", ApiUserRequired(getMyTeam)).Methods("GET") } func signupTeam(c *Context, w http.ResponseWriter, r *http.Request) { @@ -136,6 +138,8 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { } } + teamSignup.Team.AllowValet = utils.Cfg.TeamSettings.AllowValetDefault + if result := <-Srv.Store.Team().Save(&teamSignup.Team); result.Err != nil { c.Err = result.Err return @@ -157,7 +161,7 @@ func createTeamFromSignup(c *Context, w http.ResponseWriter, r *http.Request) { return } - if utils.Cfg.TeamSettings.AllowValet { + if teamSignup.Team.AllowValet { CreateValet(c, rteam) if c.Err != nil { return @@ -200,6 +204,13 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } + if rteam.AllowValet { + CreateValet(c, rteam) + if c.Err != nil { + return + } + } + w.Write([]byte(rteam.ToJson())) } } @@ -542,3 +553,72 @@ func updateTeamName(c *Context, w http.ResponseWriter, r *http.Request) { w.Write([]byte(model.MapToJson(props))) } + +func updateValetFeature(c *Context, w http.ResponseWriter, r *http.Request) { + + props := model.MapFromJson(r.Body) + + allowValetStr := props["allow_valet"] + if len(allowValetStr) == 0 { + c.SetInvalidParam("updateValetFeature", "allow_valet") + return + } + + allowValet := allowValetStr == "true" + + teamId := props["team_id"] + if len(teamId) > 0 && len(teamId) != 26 { + c.SetInvalidParam("updateValetFeature", "team_id") + return + } else if len(teamId) == 0 { + teamId = c.Session.TeamId + } + + tchan := Srv.Store.Team().Get(teamId) + + if !c.HasPermissionsToTeam(teamId, "updateValetFeature") { + return + } + + if !strings.Contains(c.Session.Roles, model.ROLE_ADMIN) { + c.Err = model.NewAppError("updateValetFeature", "You do not have the appropriate permissions", "userId="+c.Session.UserId) + c.Err.StatusCode = http.StatusForbidden + return + } + + var team *model.Team + if tResult := <-tchan; tResult.Err != nil { + c.Err = tResult.Err + return + } else { + team = tResult.Data.(*model.Team) + } + + team.AllowValet = allowValet + + if result := <-Srv.Store.Team().Update(team); result.Err != nil { + c.Err = result.Err + return + } + + w.Write([]byte(model.MapToJson(props))) +} + +func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) { + + if len(c.Session.TeamId) == 0 { + return + } + + if result := <-Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else if HandleEtag(result.Data.(*model.Team).Etag(), w, r) { + return + } else { + w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.Team).Etag()) + w.Header().Set("Expires", "-1") + w.Write([]byte(result.Data.(*model.Team).ToJson())) + return + } +} diff --git a/api/team_test.go b/api/team_test.go index 74a1846347..042c0a2e95 100644 --- a/api/team_test.go +++ b/api/team_test.go @@ -286,3 +286,106 @@ func TestFuzzyTeamCreate(t *testing.T) { } } } + +func TestGetMyTeam(t *testing.T) { + Setup() + + team := model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + rteam, _ := Client.CreateTeam(&team) + + user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + ruser, _ := Client.CreateUser(&user, "") + Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id) + + Client.LoginByEmail(team.Domain, user.Email, user.Password) + + if result, err := Client.GetMyTeam(""); err != nil { + t.Fatal("Failed to get user") + } else { + if result.Data.(*model.Team).Name != team.Name { + t.Fatal("team names did not match") + } + if result.Data.(*model.Team).Domain != team.Domain { + t.Fatal("team domains did not match") + } + if result.Data.(*model.Team).Type != team.Type { + t.Fatal("team types did not match") + } + } +} + +func TestUpdateValetFeature(t *testing.T) { + Setup() + + team := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team) + + user := &model.User{TeamId: team.Id, Email: "test@nowhere.com", FullName: "Corey Hulen", Password: "pwd"} + user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user.Id) + + user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user2.Id) + + team2 := &model.Team{Name: "Name", Domain: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN} + team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team) + + user3 := &model.User{TeamId: team2.Id, Email: model.NewId() + "corey@test.com", FullName: "Corey Hulen", Password: "pwd"} + user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User) + Srv.Store.User().VerifyEmail(user3.Id) + + Client.LoginByEmail(team.Domain, user2.Email, "pwd") + + data := make(map[string]string) + data["allow_valet"] = "true" + if _, err := Client.UpdateValetFeature(data); err == nil { + t.Fatal("Should have errored, not admin") + } + + Client.LoginByEmail(team.Domain, user.Email, "pwd") + + data["allow_valet"] = "" + if _, err := Client.UpdateValetFeature(data); err == nil { + t.Fatal("Should have errored, empty allow_valet field") + } + + data["allow_valet"] = "true" + if _, err := Client.UpdateValetFeature(data); err != nil { + t.Fatal(err) + } + + rteam := Client.Must(Client.GetMyTeam("")).Data.(*model.Team) + if rteam.AllowValet != true { + t.Fatal("Should have errored - allow valet property not updated") + } + + data["team_id"] = "junk" + if _, err := Client.UpdateValetFeature(data); err == nil { + t.Fatal("Should have errored, junk team id") + } + + data["team_id"] = "12345678901234567890123456" + if _, err := Client.UpdateValetFeature(data); err == nil { + t.Fatal("Should have errored, bad team id") + } + + data["team_id"] = team.Id + data["allow_valet"] = "false" + if _, err := Client.UpdateValetFeature(data); err != nil { + t.Fatal(err) + } + + rteam = Client.Must(Client.GetMyTeam("")).Data.(*model.Team) + if rteam.AllowValet != false { + t.Fatal("Should have errored - allow valet property not updated") + } + + Client.LoginByEmail(team2.Domain, user3.Email, "pwd") + + data["team_id"] = team.Id + data["allow_valet"] = "true" + if _, err := Client.UpdateValetFeature(data); err == nil { + t.Fatal("Should have errored, not part of team") + } +} diff --git a/api/user.go b/api/user.go index 79d4bb32c4..f8382cf2f8 100644 --- a/api/user.go +++ b/api/user.go @@ -145,10 +145,6 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { } func CreateValet(c *Context, team *model.Team) *model.User { - if !utils.Cfg.TeamSettings.AllowValet { - return &model.User{} - } - valet := &model.User{} valet.TeamId = team.Id valet.Email = utils.Cfg.EmailSettings.FeedbackEmail diff --git a/config/config.json b/config/config.json index c0c236735c..e38f1701a1 100644 --- a/config/config.json +++ b/config/config.json @@ -73,7 +73,7 @@ "TeamSettings": { "MaxUsersPerTeam": 150, "AllowPublicLink": true, - "AllowValet": false, + "AllowValetDefault": false, "TermsLink": "/static/help/configure_links.html", "PrivacyLink": "/static/help/configure_links.html", "AboutLink": "/static/help/configure_links.html", diff --git a/model/client.go b/model/client.go index 0448828bb4..ab01e7d62f 100644 --- a/model/client.go +++ b/model/client.go @@ -186,6 +186,15 @@ func (c *Client) UpdateTeamName(data map[string]string) (*Result, *AppError) { } } +func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) { + if r, err := c.DoPost("/teams/update_valet_feature", MapToJson(data)); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil + } +} + func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) { if r, err := c.DoPost("/users/create", user.ToJson()); err != nil { return nil, err @@ -647,6 +656,15 @@ func (c *Client) GetStatuses() (*Result, *AppError) { } } +func (c *Client) GetMyTeam(etag string) (*Result, *AppError) { + if r, err := c.DoGet("/teams/me", "", etag); err != nil { + return nil, err + } else { + return &Result{r.Header.Get(HEADER_REQUEST_ID), + r.Header.Get(HEADER_ETAG_SERVER), TeamFromJson(r.Body)}, nil + } +} + func (c *Client) MockSession(sessionToken string) { c.AuthToken = sessionToken } diff --git a/model/team.go b/model/team.go index a510cde783..5c66f3b1fb 100644 --- a/model/team.go +++ b/model/team.go @@ -24,6 +24,7 @@ type Team struct { Type string `json:"type"` CompanyName string `json:"company_name"` AllowedDomains string `json:"allowed_domains"` + AllowValet bool `json:"allow_valet"` } type Invites struct { diff --git a/store/sql_team_store.go b/store/sql_team_store.go index 6e7fc1c1eb..ffb9f80938 100644 --- a/store/sql_team_store.go +++ b/store/sql_team_store.go @@ -5,6 +5,7 @@ package store import ( "github.com/mattermost/platform/model" + "github.com/mattermost/platform/utils" "strings" ) @@ -29,6 +30,11 @@ func NewSqlTeamStore(sqlStore *SqlStore) TeamStore { } func (s SqlTeamStore) UpgradeSchemaIfNeeded() { + defaultValue := "0" + if utils.Cfg.TeamSettings.AllowValetDefault { + defaultValue = "1" + } + s.CreateColumnIfNotExists("Teams", "AllowValet", "AllowedDomains", "tinyint(1)", defaultValue) } func (s SqlTeamStore) CreateIndexesIfNotExists() { diff --git a/utils/config.go b/utils/config.go index 6a7e4589c6..eb2ae30505 100644 --- a/utils/config.go +++ b/utils/config.go @@ -98,7 +98,7 @@ type PrivacySettings struct { type TeamSettings struct { MaxUsersPerTeam int AllowPublicLink bool - AllowValet bool + AllowValetDefault bool TermsLink string PrivacyLink string AboutLink string diff --git a/web/react/components/channel_loader.jsx b/web/react/components/channel_loader.jsx index 5252f275c2..537a41d031 100644 --- a/web/react/components/channel_loader.jsx +++ b/web/react/components/channel_loader.jsx @@ -18,6 +18,7 @@ module.exports = React.createClass({ AsyncClient.getChannelExtraInfo(true); AsyncClient.findTeams(); AsyncClient.getStatuses(); + AsyncClient.getMyTeam(); /* End of async loads */ diff --git a/web/react/components/settings_sidebar.jsx b/web/react/components/settings_sidebar.jsx index a1546890f7..ae8510cf22 100644 --- a/web/react/components/settings_sidebar.jsx +++ b/web/react/components/settings_sidebar.jsx @@ -1,6 +1,8 @@ // Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved. // See License.txt for license information. +var utils = require('../utils/utils.jsx'); + module.exports = React.createClass({ updateTab: function(tab) { this.props.updateTab(tab); @@ -11,16 +13,11 @@ module.exports = React.createClass({ return (