Merge pull request #1887 from mattermost/editions

Editions
Этот коммит содержится в:
Joram Wilander
2016-01-14 08:24:47 -05:00
родитель 53b0cd8f2a c26edcf678
Коммит 7d383319df
24 изменённых файлов: 900 добавлений и 43 удалений

2
.gitignore поставляемый
Просмотреть файл

@@ -9,6 +9,8 @@ web/static/js/bundle*.js
web/static/js/bundle*.js.map web/static/js/bundle*.js.map
web/static/js/libs*.js web/static/js/libs*.js
config/active.dat
# Build Targets # Build Targets
.prepare .prepare
.prepare-go .prepare-go

Просмотреть файл

@@ -266,6 +266,9 @@ run: start-docker .prepare-go .prepare-jsx
jq -s '.[0] * .[1]' ./config/config.json $(ENTERPRISE_DIR)/config/enterprise-config-additions.json > config.json.tmp; \ jq -s '.[0] * .[1]' ./config/config.json $(ENTERPRISE_DIR)/config/enterprise-config-additions.json > config.json.tmp; \
mv config.json.tmp ./config/config.json; \ mv config.json.tmp ./config/config.json; \
sed -e '/\/\/ENTERPRISE_IMPORTS/ {' -e 'r $(ENTERPRISE_DIR)/imports' -e 'd' -e '}' -i'.bak' mattermost.go; \ sed -e '/\/\/ENTERPRISE_IMPORTS/ {' -e 'r $(ENTERPRISE_DIR)/imports' -e 'd' -e '}' -i'.bak' mattermost.go; \
sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|true|g' ./model/version.go; \
else \
sed -i'.bak' 's|_BUILD_ENTERPRISE_READY_|false|g' ./model/version.go; \
fi fi
@echo Starting go web server @echo Starting go web server
@@ -299,6 +302,7 @@ stop:
@if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \ @if [ "$(BUILD_ENTERPRISE)" = "true" ] && [ -d "$(ENTERPRISE_DIR)" ]; then \
mv ./config/config.json.bak ./config/config.json 2> /dev/null || true; \ mv ./config/config.json.bak ./config/config.json 2> /dev/null || true; \
mv ./mattermost.go.bak ./mattermost.go 2> /dev/null || true; \ mv ./mattermost.go.bak ./mattermost.go 2> /dev/null || true; \
mv ./model/version.go.bak ./model/version.go 2> /dev/null || true; \
fi fi
setup-mac: setup-mac:

Просмотреть файл

@@ -46,6 +46,7 @@ func InitApi() {
InitOAuth(r) InitOAuth(r)
InitWebhook(r) InitWebhook(r)
InitPreference(r) InitPreference(r)
InitLicense(r)
templatesDir := utils.FindDir("api/templates") templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir) l4g.Debug("Parsing server templates at %v", templatesDir)

Просмотреть файл

@@ -35,6 +35,7 @@ type Page struct {
TemplateName string TemplateName string
Props map[string]string Props map[string]string
ClientCfg map[string]string ClientCfg map[string]string
ClientLicense map[string]string
User *model.User User *model.User
Team *model.Team Team *model.Team
Channel *model.Channel Channel *model.Channel

Просмотреть файл

@@ -541,12 +541,8 @@ func writeFile(f []byte, path string) *model.AppError {
return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error()) return model.NewAppError("writeFile", "Encountered an error writing to S3", err.Error())
} }
} else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL { } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil { if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error()) return err
}
if err := ioutil.WriteFile(utils.Cfg.FileSettings.Directory+path, f, 0644); err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
} }
} else { } else {
return model.NewAppError("writeFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "") return model.NewAppError("writeFile", "File storage not configured properly. Please configure for either S3 or local server file storage.", "")
@@ -555,6 +551,18 @@ func writeFile(f []byte, path string) *model.AppError {
return nil return nil
} }
func writeFileLocally(f []byte, path string) *model.AppError {
if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
return model.NewAppError("writeFile", "Encountered an error creating the directory for the new file", err.Error())
}
if err := ioutil.WriteFile(path, f, 0644); err != nil {
return model.NewAppError("writeFile", "Encountered an error writing to local server storage", err.Error())
}
return nil
}
func readFile(path string) ([]byte, *model.AppError) { func readFile(path string) ([]byte, *model.AppError) {
if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 { if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {

100
api/license.go Обычный файл
Просмотреть файл

@@ -0,0 +1,100 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"bytes"
l4g "code.google.com/p/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"io"
"net/http"
"strings"
)
func InitLicense(r *mux.Router) {
l4g.Debug("Initializing license api routes")
sr := r.PathPrefix("/license").Subrouter()
sr.Handle("/add", ApiAdminSystemRequired(addLicense)).Methods("POST")
sr.Handle("/remove", ApiAdminSystemRequired(removeLicense)).Methods("POST")
}
func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
err := r.ParseMultipartForm(model.MAX_FILE_SIZE)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
m := r.MultipartForm
fileArray, ok := m.File["license"]
if !ok {
c.Err = model.NewAppError("addLicense", "No file under 'license' in request", "")
c.Err.StatusCode = http.StatusBadRequest
return
}
if len(fileArray) <= 0 {
c.Err = model.NewAppError("addLicense", "Empty array under 'license' in request", "")
c.Err.StatusCode = http.StatusBadRequest
return
}
fileData := fileArray[0]
file, err := fileData.Open()
defer file.Close()
if err != nil {
c.Err = model.NewAppError("addLicense", "Could not open license file", err.Error())
return
}
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
data := buf.Bytes()
var license *model.License
if success, licenseStr := utils.ValidateLicense(data); success {
license = model.LicenseFromJson(strings.NewReader(licenseStr))
if ok := utils.SetLicense(license); !ok {
c.LogAudit("failed - expired or non-started license")
c.Err = model.NewAppError("addLicense", "License is either expired or has not yet started.", "")
return
}
if err := writeFileLocally(data, utils.LicenseLocation()); err != nil {
c.LogAudit("failed - could not save license file")
c.Err = model.NewAppError("addLicense", "License did not save properly.", "path="+utils.LicenseLocation())
utils.RemoveLicense()
return
}
} else {
c.LogAudit("failed - invalid license")
c.Err = model.NewAppError("addLicense", "Invalid license file.", "")
return
}
c.LogAudit("success")
w.Write([]byte(license.ToJson()))
}
func removeLicense(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
if ok := utils.RemoveLicense(); !ok {
c.LogAudit("failed - could not remove license file")
c.Err = model.NewAppError("removeLicense", "License did not remove properly.", "")
return
}
rdata := map[string]string{}
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}

Просмотреть файл

@@ -122,6 +122,11 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
user.EmailVerified = true user.EmailVerified = true
} }
if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) {
c.Err = model.NewAppError("createUser", "The email you provided does not belong to an accepted domain. Please contact your administrator or sign up with a different email.", "")
return
}
ruser, err := CreateUser(team, user) ruser, err := CreateUser(team, user)
if err != nil { if err != nil {
c.Err = err c.Err = err
@@ -136,19 +141,29 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func CheckUserDomain(user *model.User, domains string) bool {
if len(domains) == 0 {
return true
}
domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
matched := false
for _, d := range domainArray {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
return matched
}
func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool { func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
shouldVerifyHash := true shouldVerifyHash := true
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil { if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil {
domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1)))) matched := CheckUserDomain(user, team.AllowedDomains)
matched := false
for _, d := range domains {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
if matched { if matched {
shouldVerifyHash = false shouldVerifyHash = false

Просмотреть файл

@@ -107,5 +107,28 @@
"AuthEndpoint": "", "AuthEndpoint": "",
"TokenEndpoint": "", "TokenEndpoint": "",
"UserApiEndpoint": "" "UserApiEndpoint": ""
},
"GoogleSettings": {
"Enable": false,
"Secret": "",
"Id": "",
"Scope": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
},
"LdapSettings": {
"Enable": false,
"LdapServer": null,
"LdapPort": 389,
"BaseDN": null,
"BindUsername": null,
"BindPassword": null,
"FirstNameAttribute": null,
"LastNameAttribute": null,
"EmailAttribute": null,
"UsernameAttribute": null,
"IdAttribute": null,
"QueryTimeout": 60
} }
} }

Просмотреть файл

@@ -31,7 +31,10 @@ import (
_ "github.com/go-ldap/ldap" _ "github.com/go-ldap/ldap"
) )
//ENTERPRISE_IMPORTS import (
_ "github.com/mattermost/enterprise/oauth/google"
_ "github.com/mattermost/enterprise/ldap"
)
var flagCmdCreateTeam bool var flagCmdCreateTeam bool
var flagCmdCreateUser bool var flagCmdCreateUser bool
@@ -67,6 +70,8 @@ func main() {
api.InitApi() api.InitApi()
web.InitWeb() web.InitWeb()
utils.LoadLicense()
if flagRunCmds { if flagRunCmds {
runCmds() runCmds()
} else { } else {

85
model/license.go Обычный файл
Просмотреть файл

@@ -0,0 +1,85 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type License struct {
Id string `json:"id"`
IssuedAt int64 `json:"issued_at"`
StartsAt int64 `json:"starts_at"`
ExpiresAt int64 `json:"expires_at"`
Customer *Customer `json:"customer"`
Features *Features `json:"features"`
}
type Customer struct {
Id string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Company string `json:"company"`
PhoneNumber string `json:"phone_number"`
}
type Features struct {
Users *int `json:"users"`
LDAP *bool `json:"ldap"`
GoogleSSO *bool `json:"google_sso"`
}
func (f *Features) SetDefaults() {
if f.Users == nil {
f.Users = new(int)
*f.Users = 0
}
if f.LDAP == nil {
f.LDAP = new(bool)
*f.LDAP = true
}
if f.GoogleSSO == nil {
f.GoogleSSO = new(bool)
*f.GoogleSSO = true
}
}
func (l *License) IsExpired() bool {
now := GetMillis()
if l.ExpiresAt < now {
return true
}
return false
}
func (l *License) IsStarted() bool {
now := GetMillis()
if l.StartsAt < now {
return true
}
return false
}
func (l *License) ToJson() string {
b, err := json.Marshal(l)
if err != nil {
return ""
} else {
return string(b)
}
}
func LicenseFromJson(data io.Reader) *License {
decoder := json.NewDecoder(data)
var o License
err := decoder.Decode(&o)
if err == nil {
return &o
} else {
return nil
}
}

34
model/license_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,34 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"testing"
)
func TestLicenseExpired(t *testing.T) {
l1 := License{}
l1.ExpiresAt = GetMillis() - 1000
if !l1.IsExpired() {
t.Fatal("license should be expired")
}
l1.ExpiresAt = GetMillis() + 10000
if l1.IsExpired() {
t.Fatal("license should not be expired")
}
}
func TestLicenseStarted(t *testing.T) {
l1 := License{}
l1.StartsAt = GetMillis() - 1000
if !l1.IsStarted() {
t.Fatal("license should be started")
}
l1.StartsAt = GetMillis() + 10000
if l1.IsStarted() {
t.Fatal("license should not be started")
}
}

157
utils/license.go Обычный файл
Просмотреть файл

@@ -0,0 +1,157 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"bytes"
"crypto"
"crypto/rsa"
"crypto/sha512"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"io"
"os"
"path/filepath"
"strconv"
"strings"
l4g "code.google.com/p/log4go"
"github.com/mattermost/platform/model"
)
const (
LICENSE_FILENAME = "active.dat"
)
var IsLicensed bool = false
var License *model.License = &model.License{}
var ClientLicense map[string]string = make(map[string]string)
// test public key
var publicKey []byte = []byte(`-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3/k3Al9q1Xe+xngQ/yGn
0suaJopea3Cpf6NjIHdO8sYTwLlxqt0Mdb+qBR9LbCjZfcNmqc5mZONvsyCEoN/5
VoLdlv1m9ao2BSAWphUxE2CPdUWdLOsDbQWliSc5//UhiYeR+67Xxon0Hg0LKXF6
PumRIWQenRHJWqlUQZ147e7/1v9ySVRZksKpvlmMDzgq+kCH/uyM1uVP3z7YXhlN
K7vSSQYbt4cghvWQxDZFwpLlsChoY+mmzClgq+Yv6FLhj4/lk94twdOZau/AeZFJ
NxpC+5KFhU+xSeeklNqwCgnlOyZ7qSTxmdJHb+60SwuYnnGIYzLJhY4LYDr4J+KR
1wIDAQAB
-----END PUBLIC KEY-----`)
func LoadLicense() {
file, err := os.Open(LicenseLocation())
if err != nil {
l4g.Warn("Unable to open/find license file")
return
}
defer file.Close()
buf := bytes.NewBuffer(nil)
io.Copy(buf, file)
if success, licenseStr := ValidateLicense(buf.Bytes()); success {
license := model.LicenseFromJson(strings.NewReader(licenseStr))
SetLicense(license)
}
l4g.Warn("No valid enterprise license found")
}
func SetLicense(license *model.License) bool {
license.Features.SetDefaults()
if !license.IsExpired() && license.IsStarted() {
License = license
IsLicensed = true
ClientLicense = getClientLicense(license)
return true
}
return false
}
func LicenseLocation() string {
return filepath.Dir(CfgFileName) + "/" + LICENSE_FILENAME
}
func RemoveLicense() bool {
License = &model.License{}
IsLicensed = false
ClientLicense = getClientLicense(License)
if err := os.Remove(LicenseLocation()); err != nil {
l4g.Error("Unable to remove license file, err=%v", err.Error())
return false
}
return true
}
func ValidateLicense(signed []byte) (bool, string) {
decoded := make([]byte, base64.StdEncoding.DecodedLen(len(signed)))
_, err := base64.StdEncoding.Decode(decoded, signed)
if err != nil {
l4g.Error("Encountered error decoding license, err=%v", err.Error())
return false, ""
}
if len(decoded) <= 256 {
l4g.Error("Signed license not long enough")
return false, ""
}
// remove null terminator
if decoded[len(decoded)-1] == byte(0) {
decoded = decoded[:len(decoded)-1]
}
plaintext := decoded[:len(decoded)-256]
signature := decoded[len(decoded)-256:]
block, _ := pem.Decode(publicKey)
public, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
l4g.Error("Encountered error signing license, err=%v", err.Error())
return false, ""
}
rsaPublic := public.(*rsa.PublicKey)
h := sha512.New()
h.Write(plaintext)
d := h.Sum(nil)
err = rsa.VerifyPKCS1v15(rsaPublic, crypto.SHA512, d, signature)
if err != nil {
l4g.Error("Invalid signature, err=%v", err.Error())
return false, ""
}
return true, string(plaintext)
}
func getClientLicense(l *model.License) map[string]string {
props := make(map[string]string)
props["IsLicensed"] = strconv.FormatBool(IsLicensed)
if IsLicensed {
props["Users"] = strconv.Itoa(*l.Features.Users)
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
props["IssuedAt"] = strconv.FormatInt(l.IssuedAt, 10)
props["StartsAt"] = strconv.FormatInt(l.StartsAt, 10)
props["ExpiresAt"] = strconv.FormatInt(l.ExpiresAt, 10)
props["Name"] = l.Customer.Name
props["Email"] = l.Customer.Email
props["Company"] = l.Customer.Company
props["PhoneNumber"] = l.Customer.PhoneNumber
}
return props
}

50
utils/license_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,50 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package utils
import (
"github.com/mattermost/platform/model"
"testing"
)
func TestSetLicense(t *testing.T) {
l1 := &model.License{}
l1.Features = &model.Features{}
l1.Customer = &model.Customer{}
l1.StartsAt = model.GetMillis() - 1000
l1.ExpiresAt = model.GetMillis() + 100000
if ok := SetLicense(l1); !ok {
t.Fatal("license should have worked")
}
l2 := &model.License{}
l2.Features = &model.Features{}
l2.Customer = &model.Customer{}
l2.StartsAt = model.GetMillis() - 1000
l2.ExpiresAt = model.GetMillis() - 100
if ok := SetLicense(l2); ok {
t.Fatal("license should have failed")
}
l3 := &model.License{}
l3.Features = &model.Features{}
l3.Customer = &model.Customer{}
l3.StartsAt = model.GetMillis() + 10000
l3.ExpiresAt = model.GetMillis() + 100000
if ok := SetLicense(l3); ok {
t.Fatal("license should have failed")
}
}
func TestValidateLicense(t *testing.T) {
b1 := []byte("junk")
if ok, _ := ValidateLicense(b1); ok {
t.Fatal("should have failed - bad license")
}
b2 := []byte("junkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunkjunk")
if ok, _ := ValidateLicense(b2); ok {
t.Fatal("should have failed - bad license")
}
}

Просмотреть файл

@@ -15,6 +15,19 @@ export default class AboutBuildModal extends React.Component {
render() { render() {
const config = global.window.mm_config; const config = global.window.mm_config;
const license = global.window.mm_license;
let title = 'Team Edition';
let licensee;
if (config.BuildEnterpriseReady === 'true' && license.IsLicensed === 'true') {
title = 'Enterprise Edition';
licensee = (
<div className='row form-group'>
<div className='col-sm-3 info__label'>{'Licensed by:'}</div>
<div className='col-sm-9'>{license.Company}</div>
</div>
);
}
return ( return (
<Modal <Modal
@@ -22,9 +35,15 @@ export default class AboutBuildModal extends React.Component {
onHide={this.doHide} onHide={this.doHide}
> >
<Modal.Header closeButton={true}> <Modal.Header closeButton={true}>
<Modal.Title>{`Mattermost ${config.Version}`}</Modal.Title> <Modal.Title>{'About Mattermost'}</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
<h4>{`Mattermost ${title}`}</h4>
{licensee}
<div className='row form-group'>
<div className='col-sm-3 info__label'>{'Version:'}</div>
<div className='col-sm-9'>{config.Version}</div>
</div>
<div className='row form-group'> <div className='row form-group'>
<div className='col-sm-3 info__label'>{'Build Number:'}</div> <div className='col-sm-3 info__label'>{'Build Number:'}</div>
<div className='col-sm-9'>{config.BuildNumber}</div> <div className='col-sm-9'>{config.BuildNumber}</div>

Просмотреть файл

@@ -22,6 +22,7 @@ import LegalAndSupportSettingsTab from './legal_and_support_settings.jsx';
import TeamUsersTab from './team_users.jsx'; import TeamUsersTab from './team_users.jsx';
import TeamAnalyticsTab from './team_analytics.jsx'; import TeamAnalyticsTab from './team_analytics.jsx';
import LdapSettingsTab from './ldap_settings.jsx'; import LdapSettingsTab from './ldap_settings.jsx';
import LicenseSettingsTab from './license_settings.jsx';
export default class AdminController extends React.Component { export default class AdminController extends React.Component {
constructor(props) { constructor(props) {
@@ -154,6 +155,8 @@ export default class AdminController extends React.Component {
tab = <LegalAndSupportSettingsTab config={this.state.config} />; tab = <LegalAndSupportSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'ldap_settings') { } else if (this.state.selected === 'ldap_settings') {
tab = <LdapSettingsTab config={this.state.config} />; tab = <LdapSettingsTab config={this.state.config} />;
} else if (this.state.selected === 'license') {
tab = <LicenseSettingsTab />;
} else if (this.state.selected === 'team_users') { } else if (this.state.selected === 'team_users') {
if (this.state.teams) { if (this.state.teams) {
tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />; tab = <TeamUsersTab team={this.state.teams[this.state.selectedTeam]} />;

Просмотреть файл

@@ -155,6 +155,36 @@ export default class AdminSidebar extends React.Component {
} }
} }
let ldapSettings;
let licenseSettings;
if (global.window.mm_config.BuildEnterpriseReady === 'true') {
if (global.window.mm_license.IsLicensed === 'true') {
ldapSettings = (
<li>
<a
href='#'
className={this.isSelected('ldap_settings')}
onClick={this.handleClick.bind(this, 'ldap_settings', null)}
>
{'LDAP Settings'}
</a>
</li>
);
}
licenseSettings = (
<li>
<a
href='#'
className={this.isSelected('license')}
onClick={this.handleClick.bind(this, 'license', null)}
>
{'Edition and License'}
</a>
</li>
);
}
return ( return (
<div className='sidebar--left sidebar--collapsable'> <div className='sidebar--left sidebar--collapsable'>
<div> <div>
@@ -252,6 +282,7 @@ export default class AdminSidebar extends React.Component {
{'GitLab Settings'} {'GitLab Settings'}
</a> </a>
</li> </li>
{ldapSettings}
<li> <li>
<a <a
href='#' href='#'
@@ -300,6 +331,7 @@ export default class AdminSidebar extends React.Component {
</li> </li>
</ul> </ul>
<ul className='nav nav__sub-menu padded'> <ul className='nav nav__sub-menu padded'>
{licenseSettings}
<li> <li>
<a <a
href='#' href='#'

Просмотреть файл

@@ -90,14 +90,41 @@ export default class LdapSettings extends React.Component {
saveClass = 'btn btn-primary'; saveClass = 'btn btn-primary';
} }
return ( const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP === 'true';
<div className='wrapper--fixed'>
let bannerContent;
if (licenseEnabled) {
bannerContent = (
<div className='banner'> <div className='banner'>
<div className='banner__content'> <div className='banner__content'>
<h4 className='banner__heading'>{'Note:'}</h4> <h4 className='banner__heading'>{'Note:'}</h4>
<p>{'If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'}</p> <p>{'If a user attribute changes on the LDAP server it will be updated the next time the user enters their credentials to log in to Mattermost. This includes if a user is made inactive or removed from an LDAP server. Synchronization with LDAP servers is planned in a future release.'}</p>
</div> </div>
</div> </div>
);
} else {
bannerContent = (
<div className='banner warning'>
<div className='banner__content'>
<h4 className='banner__heading'>{'Note:'}</h4>
<p>
{'LDAP is an enterprise feature. Your current license does not support LDAP. Click '}
<a
href='http://mattermost.com'
target='_blank'
>
{'here'}
</a>
{' for information and pricing on enterprise licenses.'}
</p>
</div>
</div>
);
}
return (
<div className='wrapper--fixed'>
{bannerContent}
<h3>{'LDAP Settings'}</h3> <h3>{'LDAP Settings'}</h3>
<form <form
className='form-horizontal' className='form-horizontal'
@@ -119,6 +146,7 @@ export default class LdapSettings extends React.Component {
ref='Enable' ref='Enable'
defaultChecked={this.props.config.LdapSettings.Enable} defaultChecked={this.props.config.LdapSettings.Enable}
onChange={this.handleEnable} onChange={this.handleEnable}
disabled={!licenseEnabled}
/> />
{'true'} {'true'}
</label> </label>

Просмотреть файл

@@ -0,0 +1,237 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
import * as Utils from '../../utils/utils.jsx';
import * as Client from '../../utils/client.jsx';
export default class LicenseSettings extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
this.handleRemove = this.handleRemove.bind(this);
this.state = {
fileSelected: false,
serverError: null
};
}
handleChange() {
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
if (element.prop('files').length > 0) {
this.setState({fileSelected: true});
}
}
handleSubmit(e) {
e.preventDefault();
const element = $(ReactDOM.findDOMNode(this.refs.fileInput));
if (element.prop('files').length === 0) {
return;
}
const file = element.prop('files')[0];
$('#upload-button').button('loading');
const formData = new FormData();
formData.append('license', file, file.name);
Client.uploadLicenseFile(formData,
() => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
this.setState({serverError: null});
window.location.reload(true);
},
(error) => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
this.setState({serverError: error.message});
}
);
}
handleRemove(e) {
e.preventDefault();
$('#remove-button').button('loading');
Client.removeLicenseFile(
() => {
$('#remove-button').button('reset');
this.setState({serverError: null});
window.location.reload(true);
},
(error) => {
$('#remove-button').button('reset');
this.setState({serverError: error.message});
}
);
}
render() {
var serverError = '';
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var btnClass = 'btn';
if (this.state.fileSelected) {
btnClass = 'btn btn-primary';
}
let edition;
let licenseType;
let licenseKey;
if (global.window.mm_license.IsLicensed === 'true') {
edition = 'Mattermost Enterprise Edition. Designed for enterprise-scale communication.';
licenseType = (
<div>
<p>
{'This compiled release of Mattermost platform is provided under a '}
<a
href='http://mattermost.com'
target='_blank'
>
{'commercial license'}
</a>
{' from Mattermost, Inc. based on your subscription level and is subject to the '}
<a
href={global.window.mm_config.TermsOfServiceLink}
target='_blank'
>
{'Terms of Service.'}
</a>
</p>
<p>{'Your subscription details are as follows:'}</p>
{'Name: ' + global.window.mm_license.Name}
<br/>
{'Company or organization name: ' + global.window.mm_license.Company}
<br/>
{'Number of users: ' + global.window.mm_license.Users}
<br/>
{`License issued: ${Utils.displayDate(parseInt(global.window.mm_license.IssuedAt, 10))} ${Utils.displayTime(parseInt(global.window.mm_license.IssuedAt, 10), true)}`}
<br/>
{'Start date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.StartsAt, 10))}
<br/>
{'Expiry date of license: ' + Utils.displayDate(parseInt(global.window.mm_license.ExpiresAt, 10))}
<br/>
{'LDAP: ' + global.window.mm_license.LDAP}
<br/>
</div>
);
licenseKey = (
<div className='col-sm-8'>
<button
className='btn btn-danger'
onClick={this.handleRemove}
id='remove-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Removing License...'}
>
{'Remove Enterprise License and Downgrade Server'}
</button>
<br/>
<br/>
<p className='help-text'>
{'If youre migrating servers you may need to remove your license key from this server in order to install it on a new server. To start, '}
<a
href='http://mattermost.com'
target='_blank'
>
{'disable all Enterprise Edition features on this server'}
</a>
{'. This will enable the ability to remove the license key and downgrade this server from Enterprise Edition to Team Edition.'}
</p>
</div>
);
} else {
edition = 'Mattermost Team Edition. Designed for teams from 5 to 50 users.';
licenseType = (
<span>
<p>{'This compiled release of Mattermost platform is offered under an MIT license.'}</p>
<p>{'See MIT-COMPILED-LICENSE.txt in your root install directory for details. See NOTICES.txt for information about open source software used in this system.'}</p>
</span>
);
licenseKey = (
<div className='col-sm-8'>
<input
className='pull-left'
ref='fileInput'
type='file'
accept='.mattermost-license'
onChange={this.handleChange}
/>
<button
className={btnClass + ' pull-left'}
disabled={!this.state.fileSelected}
onClick={this.handleSubmit}
id='upload-button'
data-loading-text={'<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> Uploading License...'}
>
{'Upload'}
</button>
<br/>
<br/>
<br/>
{serverError}
<p className='help-text'>
{'Upload a license key for Mattermost Enterprise Edition to upgrade this server. '}
<a
href='http://mattermost.com'
target='_blank'
>
{'Visit us online'}
</a>
{' to learn more about the benefits of Enterprise Edition or to purchase a key.'}
</p>
</div>
);
}
return (
<div className='wrapper--fixed'>
<h3>{'Edition and License'}</h3>
<form
className='form-horizontal'
role='form'
>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
{'Edition: '}
</label>
<div className='col-sm-8'>
{edition}
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
{'License: '}
</label>
<div className='col-sm-8'>
{licenseType}
</div>
</div>
<div className='form-group'>
<label
className='control-label col-sm-4'
>
{'License Key: '}
</label>
{licenseKey}
</div>
</form>
</div>
);
}
}

Просмотреть файл

@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved. // Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information. // See License.txt for license information.
import * as client from '../utils/client.jsx'; import * as Client from '../utils/client.jsx';
import Constants from '../utils/constants.jsx'; import Constants from '../utils/constants.jsx';
import ChannelStore from '../stores/channel_store.jsx'; import ChannelStore from '../stores/channel_store.jsx';
import * as Utils from '../utils/utils.jsx'; import * as Utils from '../utils/utils.jsx';
@@ -26,7 +26,7 @@ export default class FileUpload extends React.Component {
for (var j = 0; j < data.client_ids.length; j++) { for (var j = 0; j < data.client_ids.length; j++) {
delete requests[data.client_ids[j]]; delete requests[data.client_ids[j]];
} }
this.setState({requests: requests}); this.setState({requests});
} }
fileUploadFail(clientId, err) { fileUploadFail(clientId, err) {
@@ -52,7 +52,7 @@ export default class FileUpload extends React.Component {
} }
// generate a unique id that can be used by other components to refer back to this upload // generate a unique id that can be used by other components to refer back to this upload
let clientId = Utils.generateId(); const clientId = Utils.generateId();
// prepare data to be uploaded // prepare data to be uploaded
var formData = new FormData(); var formData = new FormData();
@@ -60,14 +60,14 @@ export default class FileUpload extends React.Component {
formData.append('files', files[i], files[i].name); formData.append('files', files[i], files[i].name);
formData.append('client_ids', clientId); formData.append('client_ids', clientId);
var request = client.uploadFile(formData, var request = Client.uploadFile(formData,
this.fileUploadSuccess.bind(this, channelId), this.fileUploadSuccess.bind(this, channelId),
this.fileUploadFail.bind(this, clientId) this.fileUploadFail.bind(this, clientId)
); );
var requests = this.state.requests; var requests = this.state.requests;
requests[clientId] = request; requests[clientId] = request;
this.setState({requests: requests}); this.setState({requests});
this.props.onUploadStart([clientId], channelId); this.props.onUploadStart([clientId], channelId);
@@ -90,16 +90,7 @@ export default class FileUpload extends React.Component {
this.uploadFiles(element.prop('files')); this.uploadFiles(element.prop('files'));
// clear file input for all modern browsers Utils.clearFileInput(element[0]);
try {
element[0].value = '';
if (element.value) {
element[0].type = 'text';
element[0].type = 'file';
}
} catch (e) {
// Do nothing
}
} }
handleDrop(e) { handleDrop(e) {
@@ -227,14 +218,14 @@ export default class FileUpload extends React.Component {
formData.append('files', file, name); formData.append('files', file, name);
formData.append('client_ids', clientId); formData.append('client_ids', clientId);
var request = client.uploadFile(formData, var request = Client.uploadFile(formData,
self.fileUploadSuccess.bind(self, channelId), self.fileUploadSuccess.bind(self, channelId),
self.fileUploadFail.bind(self, clientId) self.fileUploadFail.bind(self, clientId)
); );
var requests = self.state.requests; var requests = self.state.requests;
requests[clientId] = request; requests[clientId] = request;
self.setState({requests: requests}); self.setState({requests});
self.props.onUploadStart([clientId], channelId); self.props.onUploadStart([clientId], channelId);
} }
@@ -263,7 +254,7 @@ export default class FileUpload extends React.Component {
request.abort(); request.abort();
delete requests[clientId]; delete requests[clientId];
this.setState({requests: requests}); this.setState({requests});
} }
} }

Просмотреть файл

@@ -1392,3 +1392,38 @@ export function regenOutgoingHookToken(data, success, error) {
} }
}); });
} }
export function uploadLicenseFile(formData, success, error) {
$.ajax({
url: '/api/v1/license/add',
type: 'POST',
data: formData,
cache: false,
contentType: false,
processData: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('uploadLicenseFile', xhr, status, err);
error(e);
}
});
track('api', 'api_license_upload');
}
export function removeLicenseFile(success, error) {
$.ajax({
url: '/api/v1/license/remove',
type: 'POST',
cache: false,
contentType: false,
processData: false,
success,
error: function onError(xhr, status, err) {
var e = handleError('removeLicenseFile', xhr, status, err);
error(e);
}
});
track('api', 'api_license_upload');
}

Просмотреть файл

@@ -201,11 +201,21 @@ export function displayDate(ticks) {
return monthNames[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear(); return monthNames[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
} }
export function displayTime(ticks) { export function displayTime(ticks, utc) {
const d = new Date(ticks); const d = new Date(ticks);
let hours = d.getHours(); let hours;
let minutes = d.getMinutes(); let minutes;
let ampm = ''; let ampm = '';
let timezone = '';
if (utc) {
hours = d.getUTCHours();
minutes = d.getUTCMinutes();
timezone = ' UTC';
} else {
hours = d.getHours();
minutes = d.getMinutes();
}
if (minutes <= 9) { if (minutes <= 9) {
minutes = '0' + minutes; minutes = '0' + minutes;
@@ -224,7 +234,7 @@ export function displayTime(ticks) {
} }
} }
return hours + ':' + minutes + ampm; return hours + ':' + minutes + ampm + timezone;
} }
export function displayDateTime(ticks) { export function displayDateTime(ticks) {
@@ -1301,3 +1311,16 @@ export function fillArray(value, length) {
export function isFileTransfer(files) { export function isFileTransfer(files) {
return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.contains('application/x-moz-file')); return files.types != null && (files.types.indexOf ? files.types.indexOf('Files') !== -1 : files.types.contains('application/x-moz-file'));
} }
export function clearFileInput(elm) {
// clear file input for all modern browsers
try {
elm.value = '';
if (elm.value) {
elm.type = 'text';
elm.type = 'file';
}
} catch (e) {
// Do nothing
}
}

Просмотреть файл

@@ -174,6 +174,9 @@
.banner__content { .banner__content {
width: 80%; width: 80%;
} }
&.warning {
background: #e60000;
}
} }
.popover { .popover {
border-radius: 3px; border-radius: 3px;

Просмотреть файл

@@ -46,6 +46,7 @@
<script> <script>
window.mm_config = {{ .ClientCfg }}; window.mm_config = {{ .ClientCfg }};
window.mm_license = {{ .ClientLicense }};
window.mm_team = {{ .Team }}; window.mm_team = {{ .Team }};
window.mm_user = {{ .User }}; window.mm_user = {{ .User }};
window.mm_channel = {{ .Channel }}; window.mm_channel = {{ .Channel }};

Просмотреть файл

@@ -32,7 +32,7 @@ func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage {
props := make(map[string]string) props := make(map[string]string)
props["Title"] = title props["Title"] = title
return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg} return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientCfg: utils.ClientCfg, ClientLicense: utils.ClientLicense}
} }
func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) {