1
0
Fork 0
mirror of https://github.com/documize/community.git synced 2025-07-18 20:59:43 +02:00

major code repair from old to new API -- WIP

This commit is contained in:
Harvey Kandola 2017-07-24 16:24:21 +01:00
parent 25b576f861
commit 792c3e2ce8
46 changed files with 3403 additions and 171 deletions

View file

@ -8,7 +8,7 @@ The mission is to bring software dev inspired features (refactoring, testing, li
## Latest version ## Latest version
v1.50.2 v1.51.0
## OS Support ## OS Support

View file

@ -24,10 +24,10 @@ import (
"github.com/documize/community/core/api/entity" "github.com/documize/community/core/api/entity"
"github.com/documize/community/core/api/request" "github.com/documize/community/core/api/request"
"github.com/documize/community/core/api/util" "github.com/documize/community/core/api/util"
"github.com/documize/community/core/env"
"github.com/documize/community/core/log" "github.com/documize/community/core/log"
"github.com/documize/community/core/secrets" "github.com/documize/community/core/secrets"
"github.com/documize/community/domain/section/provider" "github.com/documize/community/domain/section/provider"
"github.com/documize/community/server/web"
) )
// Authenticate user based up HTTP Authorization header. // Authenticate user based up HTTP Authorization header.
@ -98,7 +98,7 @@ func Authenticate(w http.ResponseWriter, r *http.Request) {
} }
// Attach user accounts and work out permissions // Attach user accounts and work out permissions
attachUserAccounts(p, org.RefID, &user) AttachUserAccounts(p, org.RefID, &user)
// active check // active check
@ -201,7 +201,7 @@ func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// Fetch user permissions for this org // Fetch user permissions for this org
if context.Authenticated { if context.Authenticated {
user, err := getSecuredUser(p, org.RefID, context.UserID) user, err := GetSecuredUser(p, org.RefID, context.UserID)
if err != nil { if err != nil {
writeServerError(w, method, err) writeServerError(w, method, err)
@ -242,8 +242,6 @@ func Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
// ValidateAuthToken finds and validates authentication token. // ValidateAuthToken finds and validates authentication token.
func ValidateAuthToken(w http.ResponseWriter, r *http.Request) { func ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
log.Info("cb gh")
// TODO should this go after token validation? // TODO should this go after token validation?
if s := r.URL.Query().Get("section"); s != "" { if s := r.URL.Query().Get("section"); s != "" {
if err := provider.Callback(s, w, r); err != nil { if err := provider.Callback(s, w, r); err != nil {
@ -325,7 +323,7 @@ func ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := getSecuredUser(p, org.RefID, context.UserID) user, err := GetSecuredUser(p, org.RefID, context.UserID)
if err != nil { if err != nil {
w.WriteHeader(http.StatusUnauthorized) w.WriteHeader(http.StatusUnauthorized)
@ -349,7 +347,7 @@ func preAuthorizeStaticAssets(r *http.Request) bool {
strings.ToLower(r.URL.Path) == "/robots.txt" || strings.ToLower(r.URL.Path) == "/robots.txt" ||
strings.ToLower(r.URL.Path) == "/version" || strings.ToLower(r.URL.Path) == "/version" ||
strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") || strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") ||
((api.Runtime.Flags.SiteMode == web.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) { ((api.Runtime.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
return true return true
} }

View file

@ -140,7 +140,7 @@ func AuthenticateKeycloak(w http.ResponseWriter, r *http.Request) {
} }
// Attach user accounts and work out permissions. // Attach user accounts and work out permissions.
attachUserAccounts(p, org.RefID, &user) AttachUserAccounts(p, org.RefID, &user)
// No accounts signals data integrity problem // No accounts signals data integrity problem
// so we reject login request. // so we reject login request.
@ -301,7 +301,7 @@ func addUser(p request.Persister, u *entity.User, addSpace bool) (err error) {
return err return err
} }
} else { } else {
attachUserAccounts(p, p.Context.OrgID, &userDupe) AttachUserAccounts(p, p.Context.OrgID, &userDupe)
for _, a := range userDupe.Accounts { for _, a := range userDupe.Accounts {
if a.OrgID == p.Context.OrgID { if a.OrgID == p.Context.OrgID {

View file

@ -136,7 +136,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
log.Info("Adding user") log.Info("Adding user")
} else { } else {
attachUserAccounts(p, p.Context.OrgID, &userDupe) AttachUserAccounts(p, p.Context.OrgID, &userDupe)
for _, a := range userDupe.Accounts { for _, a := range userDupe.Accounts {
if a.OrgID == p.Context.OrgID { if a.OrgID == p.Context.OrgID {
@ -206,7 +206,7 @@ func AddUser(w http.ResponseWriter, r *http.Request) {
} }
// Send back new user record // Send back new user record
userModel, err = getSecuredUser(p, p.Context.OrgID, userID) userModel, err = GetSecuredUser(p, p.Context.OrgID, userID)
json, err := json.Marshal(userModel) json, err := json.Marshal(userModel)
if err != nil { if err != nil {
@ -254,7 +254,7 @@ func GetOrganizationUsers(w http.ResponseWriter, r *http.Request) {
} }
for i := range users { for i := range users {
attachUserAccounts(p, p.Context.OrgID, &users[i]) AttachUserAccounts(p, p.Context.OrgID, &users[i])
} }
json, err := json.Marshal(users) json, err := json.Marshal(users)
@ -333,7 +333,7 @@ func GetUser(w http.ResponseWriter, r *http.Request) {
return return
} }
user, err := getSecuredUser(p, p.Context.OrgID, userID) user, err := GetSecuredUser(p, p.Context.OrgID, userID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
writeNotFoundError(w, method, userID) writeNotFoundError(w, method, userID)
@ -719,13 +719,14 @@ func ResetUserPassword(w http.ResponseWriter, r *http.Request) {
} }
// Get user object contain associated accounts but credentials are wiped. // Get user object contain associated accounts but credentials are wiped.
func getSecuredUser(p request.Persister, orgID, user string) (u entity.User, err error) { func GetSecuredUser(p request.Persister, orgID, user string) (u entity.User, err error) {
u, err = p.GetUser(user) u, err = p.GetUser(user)
attachUserAccounts(p, orgID, &u) AttachUserAccounts(p, orgID, &u)
return return
} }
func attachUserAccounts(p request.Persister, orgID string, user *entity.User) { func AttachUserAccounts(p request.Persister, orgID string, user *entity.User) {
user.ProtectSecrets() user.ProtectSecrets()
a, err := p.GetUserAccounts(user.RefID) a, err := p.GetUserAccounts(user.RefID)

View file

@ -19,9 +19,9 @@ import (
"github.com/documize/community/core/api" "github.com/documize/community/core/api"
"github.com/documize/community/core/api/entity" "github.com/documize/community/core/api/entity"
"github.com/documize/community/core/env"
"github.com/documize/community/core/log" "github.com/documize/community/core/log"
"github.com/documize/community/core/streamutil" "github.com/documize/community/core/streamutil"
"github.com/documize/community/server/web"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -83,7 +83,7 @@ func (p *Persister) GetOrganizationByDomain(subdomain string) (org entity.Organi
err = nil err = nil
subdomain = strings.ToLower(subdomain) subdomain = strings.ToLower(subdomain)
if api.Runtime.Flags.SiteMode == web.SiteModeNormal { // only return an organization when running normally if api.Runtime.Flags.SiteMode == env.SiteModeNormal { // only return an organization when running normally
var stmt *sqlx.Stmt var stmt *sqlx.Stmt

View file

@ -49,7 +49,7 @@ func Check(runtime *env.Runtime) bool {
if err != nil { if err != nil {
runtime.Log.Error("Can't get MySQL configuration", err) runtime.Log.Error("Can't get MySQL configuration", err)
web.SiteInfo.Issue = "Can't get MySQL configuration: " + err.Error() web.SiteInfo.Issue = "Can't get MySQL configuration: " + err.Error()
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
defer streamutil.Close(rows) defer streamutil.Close(rows)
@ -65,7 +65,7 @@ func Check(runtime *env.Runtime) bool {
if err != nil { if err != nil {
runtime.Log.Error("no MySQL configuration returned", err) runtime.Log.Error("no MySQL configuration returned", err)
web.SiteInfo.Issue = "no MySQL configuration return issue: " + err.Error() web.SiteInfo.Issue = "no MySQL configuration return issue: " + err.Error()
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
@ -92,7 +92,7 @@ func Check(runtime *env.Runtime) bool {
want := fmt.Sprintf("%d.%d.%d", verInts[0], verInts[1], verInts[2]) want := fmt.Sprintf("%d.%d.%d", verInts[0], verInts[1], verInts[2])
runtime.Log.Error("MySQL version element "+strconv.Itoa(k+1)+" of '"+version+"' not high enough, need at least version "+want, errors.New("bad MySQL version")) runtime.Log.Error("MySQL version element "+strconv.Itoa(k+1)+" of '"+version+"' not high enough, need at least version "+want, errors.New("bad MySQL version"))
web.SiteInfo.Issue = "MySQL version element " + strconv.Itoa(k+1) + " of '" + version + "' not high enough, need at least version " + want web.SiteInfo.Issue = "MySQL version element " + strconv.Itoa(k+1) + " of '" + version + "' not high enough, need at least version " + want
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
} }
@ -101,13 +101,13 @@ func Check(runtime *env.Runtime) bool {
if charset != "utf8" { if charset != "utf8" {
runtime.Log.Error("MySQL character set not utf8:", errors.New(charset)) runtime.Log.Error("MySQL character set not utf8:", errors.New(charset))
web.SiteInfo.Issue = "MySQL character set not utf8: " + charset web.SiteInfo.Issue = "MySQL character set not utf8: " + charset
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
if !strings.HasPrefix(collation, "utf8") { if !strings.HasPrefix(collation, "utf8") {
runtime.Log.Error("MySQL collation sequence not utf8...:", errors.New(collation)) runtime.Log.Error("MySQL collation sequence not utf8...:", errors.New(collation))
web.SiteInfo.Issue = "MySQL collation sequence not utf8...: " + collation web.SiteInfo.Issue = "MySQL collation sequence not utf8...: " + collation
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
} }
@ -119,12 +119,12 @@ func Check(runtime *env.Runtime) bool {
`' and TABLE_TYPE='BASE TABLE'`); err != nil { `' and TABLE_TYPE='BASE TABLE'`); err != nil {
runtime.Log.Error("Can't get MySQL number of tables", err) runtime.Log.Error("Can't get MySQL number of tables", err)
web.SiteInfo.Issue = "Can't get MySQL number of tables: " + err.Error() web.SiteInfo.Issue = "Can't get MySQL number of tables: " + err.Error()
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
if strings.TrimSpace(flds[0]) == "0" { if strings.TrimSpace(flds[0]) == "0" {
runtime.Log.Info("Entering database set-up mode because the database is empty.....") runtime.Log.Info("Entering database set-up mode because the database is empty.....")
runtime.Flags.SiteMode = web.SiteModeSetup runtime.Flags.SiteMode = env.SiteModeSetup
return false return false
} }
} }
@ -140,13 +140,13 @@ func Check(runtime *env.Runtime) bool {
if err := runtime.Db.Select(&dummy, "SELECT 1 FROM "+table+" LIMIT 1;"); err != nil { if err := runtime.Db.Select(&dummy, "SELECT 1 FROM "+table+" LIMIT 1;"); err != nil {
runtime.Log.Error("Entering bad database mode because: SELECT 1 FROM "+table+" LIMIT 1;", err) runtime.Log.Error("Entering bad database mode because: SELECT 1 FROM "+table+" LIMIT 1;", err)
web.SiteInfo.Issue = "MySQL database is not empty, but does not contain table: " + table web.SiteInfo.Issue = "MySQL database is not empty, but does not contain table: " + table
runtime.Flags.SiteMode = web.SiteModeBadDB runtime.Flags.SiteMode = env.SiteModeBadDB
return false return false
} }
} }
} }
runtime.Flags.SiteMode = web.SiteModeNormal // actually no need to do this (as already ""), this for documentation runtime.Flags.SiteMode = env.SiteModeNormal // actually no need to do this (as already ""), this for documentation
web.SiteInfo.DBname = "" // do not give this info when not in set-up mode web.SiteInfo.DBname = "" // do not give this info when not in set-up mode
dbCheckOK = true dbCheckOK = true
return true return true

View file

@ -19,6 +19,7 @@ import (
"time" "time"
"github.com/documize/community/core/api" "github.com/documize/community/core/api"
"github.com/documize/community/core/env"
"github.com/documize/community/core/log" "github.com/documize/community/core/log"
"github.com/documize/community/core/secrets" "github.com/documize/community/core/secrets"
"github.com/documize/community/core/stringutil" "github.com/documize/community/core/stringutil"
@ -65,7 +66,7 @@ func Create(w http.ResponseWriter, r *http.Request) {
target := "/setup" target := "/setup"
status := http.StatusBadRequest status := http.StatusBadRequest
if api.Runtime.Flags.SiteMode == web.SiteModeNormal { if api.Runtime.Flags.SiteMode == env.SiteModeNormal {
target = "/" target = "/"
status = http.StatusOK status = http.StatusOK
} }
@ -133,7 +134,7 @@ func Create(w http.ResponseWriter, r *http.Request) {
return return
} }
api.Runtime.Flags.SiteMode = web.SiteModeNormal api.Runtime.Flags.SiteMode = env.SiteModeNormal
} }
// The result of completing the onboarding process. // The result of completing the onboarding process.

5
core/env/product.go vendored
View file

@ -58,6 +58,11 @@ func (l *License) Status() string {
return fmt.Sprintf("License is %s and %s", lp, lv) return fmt.Sprintf("License is %s and %s", lp, lv)
} }
// IsValid returns if license is valid
func (l *License) IsValid() bool {
return l.Valid == true
}
// LicenseData holds encrypted data and is unpacked into License. // LicenseData holds encrypted data and is unpacked into License.
type LicenseData struct { type LicenseData struct {
Key string `json:"key"` Key string `json:"key"`

11
core/env/runtime.go vendored
View file

@ -22,3 +22,14 @@ type Runtime struct {
Log Logger Log Logger
Product ProdInfo Product ProdInfo
} }
const (
// SiteModeNormal serves app
SiteModeNormal = ""
// SiteModeOffline serves offline.html
SiteModeOffline = "1"
// SiteModeSetup tells Ember to serve setup route
SiteModeSetup = "2"
// SiteModeBadDB redirects to db-error.html page
SiteModeBadDB = "3"
)

View file

@ -18,6 +18,12 @@ import (
"github.com/gorilla/mux" "github.com/gorilla/mux"
) )
// Param returns the requested paramater from route request.
func Param(r *http.Request, p string) string {
params := mux.Vars(r)
return params[p]
}
// Params returns the paramaters from route request. // Params returns the paramaters from route request.
func Params(r *http.Request) map[string]string { func Params(r *http.Request) map[string]string {
return mux.Vars(r) return mux.Vars(r)

View file

@ -14,10 +14,7 @@ package response
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"github.com/documize/community/core/log"
) )
// Helper for writing consistent headers back to HTTP client // Helper for writing consistent headers back to HTTP client
@ -29,55 +26,43 @@ func writeStatus(w http.ResponseWriter, status int) {
// WriteMissingDataError notifies HTTP client of missing data in request. // WriteMissingDataError notifies HTTP client of missing data in request.
func WriteMissingDataError(w http.ResponseWriter, method, parameter string) { func WriteMissingDataError(w http.ResponseWriter, method, parameter string) {
writeStatus(w, http.StatusBadRequest) writeStatus(w, http.StatusBadRequest)
_, err := w.Write([]byte("{Error: 'Missing data'}")) w.Write([]byte("{Error: 'Missing data'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Missing data %s for method %s", parameter, method))
} }
// WriteNotFoundError notifies HTTP client of 'record not found' error. // WriteNotFoundError notifies HTTP client of 'record not found' error.
func WriteNotFoundError(w http.ResponseWriter, method string, id string) { func WriteNotFoundError(w http.ResponseWriter, method string, id string) {
writeStatus(w, http.StatusNotFound) writeStatus(w, http.StatusNotFound)
_, err := w.Write([]byte("{Error: 'Not found'}")) w.Write([]byte("{Error: 'Not found'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Not found ID %s for method %s", id, method))
} }
// WriteServerError notifies HTTP client of general application error. // WriteServerError notifies HTTP client of general application error.
func WriteServerError(w http.ResponseWriter, method string, err error) { func WriteServerError(w http.ResponseWriter, method string, err error) {
writeStatus(w, http.StatusBadRequest) writeStatus(w, http.StatusBadRequest)
_, err2 := w.Write([]byte("{Error: 'Internal server error'}")) w.Write([]byte("{Error: 'Internal server error'}"))
log.IfErr(err2)
log.Error(fmt.Sprintf("Internal server error for method %s", method), err)
} }
// WriteDuplicateError notifies HTTP client of duplicate data that has been rejected. // WriteDuplicateError notifies HTTP client of duplicate data that has been rejected.
func WriteDuplicateError(w http.ResponseWriter, method, entity string) { func WriteDuplicateError(w http.ResponseWriter, method, entity string) {
writeStatus(w, http.StatusConflict) writeStatus(w, http.StatusConflict)
_, err := w.Write([]byte("{Error: 'Duplicate record'}")) w.Write([]byte("{Error: 'Duplicate record'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Duplicate %s record detected for method %s", entity, method))
} }
// WriteUnauthorizedError notifies HTTP client of rejected unauthorized request. // WriteUnauthorizedError notifies HTTP client of rejected unauthorized request.
func WriteUnauthorizedError(w http.ResponseWriter) { func WriteUnauthorizedError(w http.ResponseWriter) {
writeStatus(w, http.StatusUnauthorized) writeStatus(w, http.StatusUnauthorized)
_, err := w.Write([]byte("{Error: 'Unauthorized'}")) w.Write([]byte("{Error: 'Unauthorized'}"))
log.IfErr(err)
} }
// WriteForbiddenError notifies HTTP client of request that is not allowed. // WriteForbiddenError notifies HTTP client of request that is not allowed.
func WriteForbiddenError(w http.ResponseWriter) { func WriteForbiddenError(w http.ResponseWriter) {
writeStatus(w, http.StatusForbidden) writeStatus(w, http.StatusForbidden)
_, err := w.Write([]byte("{Error: 'Forbidden'}")) w.Write([]byte("{Error: 'Forbidden'}"))
log.IfErr(err)
} }
// WriteBadRequestError notifies HTTP client of rejected request due to bad data within request. // WriteBadRequestError notifies HTTP client of rejected request due to bad data within request.
func WriteBadRequestError(w http.ResponseWriter, method, message string) { func WriteBadRequestError(w http.ResponseWriter, method, message string) {
writeStatus(w, http.StatusBadRequest) writeStatus(w, http.StatusBadRequest)
_, err := w.Write([]byte("{Error: 'Bad Request'}")) w.Write([]byte("{Error: 'Bad Request'}"))
log.IfErr(err)
log.Info(fmt.Sprintf("Bad Request %s for method %s", message, method))
} }
// WriteBadLicense notifies HTTP client of invalid license (402) // WriteBadLicense notifies HTTP client of invalid license (402)
@ -87,44 +72,34 @@ func WriteBadLicense(w http.ResponseWriter) {
var e struct { var e struct {
Reason string Reason string
} }
e.Reason = "invalid or expired Documize license" e.Reason = "invalid or expired Documize license"
j, _ := json.Marshal(e) j, _ := json.Marshal(e)
_, err := w.Write(j) w.Write(j)
log.IfErr(err)
} }
// WriteBytes dumps bytes to HTTP response // WriteBytes dumps bytes to HTTP response
func WriteBytes(w http.ResponseWriter, data []byte) { func WriteBytes(w http.ResponseWriter, data []byte) {
writeStatus(w, http.StatusOK) writeStatus(w, http.StatusOK)
_, err := w.Write(data) w.Write(data)
log.IfErr(err)
} }
// WriteString writes string to HTTP response // WriteString writes string to HTTP response
func WriteString(w http.ResponseWriter, data string) { func WriteString(w http.ResponseWriter, data string) {
writeStatus(w, http.StatusOK) writeStatus(w, http.StatusOK)
_, err := w.Write([]byte(data)) w.Write([]byte(data))
log.IfErr(err)
} }
// WriteEmpty writes empty JSON HTTP response // WriteEmpty writes empty JSON HTTP response
func WriteEmpty(w http.ResponseWriter) { func WriteEmpty(w http.ResponseWriter) {
writeStatus(w, http.StatusOK) writeStatus(w, http.StatusOK)
_, err := w.Write([]byte("{}")) w.Write([]byte("{}"))
log.IfErr(err)
} }
// WriteJSON serializes data as JSON to HTTP response. // WriteJSON serializes data as JSON to HTTP response.
func WriteJSON(w http.ResponseWriter, v interface{}) { func WriteJSON(w http.ResponseWriter, v interface{}) {
writeStatus(w, http.StatusOK) writeStatus(w, http.StatusOK)
j, _ := json.Marshal(v)
j, err := json.Marshal(v) w.Write(j)
if err != nil {
log.IfErr(err)
}
_, err = w.Write(j)
log.IfErr(err)
} }

42
core/timeutil/nulltime.go Normal file
View file

@ -0,0 +1,42 @@
// Source: https://github.com/lib/pq/blob/b269bd035a727d6c1081f76e7a239a1b00674c40/encode.go#L521
//
// Copyright (c) 2011-2013, 'pq' Contributors Portions Copyright (C) 2011 Blake Mizerany
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// Package timeutil provides date and time related types and helpers.
package timeutil
import (
"database/sql/driver"
"time"
)
// NullTime represents a time.Time that may be null. NullTime implements the
// sql.Scanner interface so it can be used as a scan destination, similar to
// sql.NullString.
type NullTime struct {
Time time.Time
Valid bool // Valid is true if Time is not NULL
}
// Scan implements the Scanner interface.
func (nt *NullTime) Scan(value interface{}) error {
nt.Time, nt.Valid = value.(time.Time)
return nil
}
// Value implements the driver Valuer interface.
func (nt NullTime) Value() (driver.Value, error) {
if !nt.Valid {
return nil, nil
}
return nt.Time, nil
}

36
domain/account/model.go Normal file
View file

@ -0,0 +1,36 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package account
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// Account links a User to an Organization.
type Account struct {
domain.BaseEntity
Admin bool `json:"admin"`
Editor bool `json:"editor"`
UserID string `json:"userId"`
OrgID string `json:"orgId"`
Company string `json:"company"`
Title string `json:"title"`
Message string `json:"message"`
Domain string `json:"domain"`
Active bool `json:"active"`
}

144
domain/account/store.go Normal file
View file

@ -0,0 +1,144 @@
package account
import (
"database/sql"
"fmt"
"time"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/pkg/errors"
)
// Add inserts the given record into the datbase account table.
func Add(s domain.StoreContext, account Account) (err error) {
account.Created = time.Now().UTC()
account.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.Preparex("INSERT INTO account (refid, orgid, userid, admin, editor, active, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert for account")
return
}
_, err = stmt.Exec(account.RefID, account.OrgID, account.UserID, account.Admin, account.Editor, account.Active, account.Created, account.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for account")
return
}
return
}
// GetUserAccount returns the database account record corresponding to the given userID, using the client's current organizaion.
func GetUserAccount(s domain.StoreContext, userID string) (account Account, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE b.refid=a.orgid and a.orgid=? and a.userid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare select for account by user %s", userID))
return
}
err = stmt.Get(&account, s.Context.OrgID, userID)
if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute select for account by user %s", userID))
return
}
return
}
// GetUserAccounts returns a slice of database account records, for all organizations that the userID is a member of, in organization title order.
func GetUserAccounts(s domain.StoreContext, userID string) (t []Account, err error) {
err = s.Runtime.Db.Select(&t, "SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE a.userid=? AND a.orgid=b.refid AND a.active=1 ORDER BY b.title", userID)
if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select account for user %s", userID))
}
return
}
// GetAccountsByOrg returns a slice of database account records, for all users in the client's organization.
func GetAccountsByOrg(s domain.StoreContext) (t []Account, err error) {
err = s.Runtime.Db.Select(&t, "SELECT a.*, b.company, b.title, b.message, b.domain FROM account a, organization b WHERE a.orgid=b.refid AND a.orgid=? AND a.active=1", s.Context.OrgID)
if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute select account for org %s", s.Context.OrgID))
}
return
}
// CountOrgAccounts returns the numnber of active user accounts for specified organization.
func CountOrgAccounts(s domain.StoreContext) (c int) {
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM account WHERE orgid=? AND active=1", s.Context.OrgID)
err := row.Scan(&c)
if err == sql.ErrNoRows {
return 0
}
if err != nil {
err = errors.Wrap(err, "count org accounts")
return 0
}
return
}
// UpdateAccount updates the database record for the given account to the given values.
func UpdateAccount(s domain.StoreContext, account Account) (err error) {
account.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE account SET userid=:userid, admin=:admin, editor=:editor, active=:active, revised=:revised WHERE orgid=:orgid AND refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare update for account %s", account.RefID))
return
}
_, err = stmt.Exec(&account)
if err != sql.ErrNoRows && err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute update for account %s", account.RefID))
return
}
return
}
// HasOrgAccount returns if the given orgID has valid userID.
func HasOrgAccount(s domain.StoreContext, orgID, userID string) bool {
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM account WHERE orgid=? and userid=?", orgID, userID)
var count int
err := row.Scan(&count)
if err == sql.ErrNoRows {
return false
}
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, "HasOrgAccount")
return false
}
if count == 0 {
return false
}
return true
}
// DeleteAccount deletes the database record in the account table for user ID.
func DeleteAccount(s domain.StoreContext, ID string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteConstrained(s.Context.Transaction, "account", s.Context.OrgID, ID)
}

207
domain/auth/endpoint.go Normal file
View file

@ -0,0 +1,207 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package auth
import (
"database/sql"
"errors"
"net/http"
"strings"
"github.com/documize/community/core/response"
"github.com/documize/community/core/secrets"
"github.com/documize/community/domain"
"github.com/documize/community/domain/organization"
"github.com/documize/community/domain/section/provider"
"github.com/documize/community/domain/user"
)
// Authenticate user based up HTTP Authorization header.
// An encrypted authentication token is issued with an expiry date.
func (h *Handler) Authenticate(w http.ResponseWriter, r *http.Request) {
method := "Authenticate"
s := domain.StoreContext{Runtime: h.Runtime, Context: domain.GetRequestContext(r)}
// check for http header
authHeader := r.Header.Get("Authorization")
if len(authHeader) == 0 {
response.WriteBadRequestError(w, method, "Missing Authorization header")
return
}
// decode what we received
data := strings.Replace(authHeader, "Basic ", "", 1)
decodedBytes, err := secrets.DecodeBase64([]byte(data))
if err != nil {
response.WriteBadRequestError(w, method, "Unable to decode authentication token")
return
}
decoded := string(decodedBytes)
// check that we have domain:email:password (but allow for : in password field!)
credentials := strings.SplitN(decoded, ":", 3)
if len(credentials) != 3 {
response.WriteBadRequestError(w, method, "Bad authentication token, expecting domain:email:password")
return
}
dom := strings.TrimSpace(strings.ToLower(credentials[0]))
dom = organization.CheckDomain(s, dom) // TODO optimize by removing this once js allows empty domains
email := strings.TrimSpace(strings.ToLower(credentials[1]))
password := credentials[2]
h.Runtime.Log.Info("logon attempt " + email + " @ " + dom)
u, err := user.GetByDomain(s, dom, email)
if err == sql.ErrNoRows {
response.WriteUnauthorizedError(w)
return
}
if err != nil {
response.WriteServerError(w, method, err)
return
}
if len(u.Reset) > 0 || len(u.Password) == 0 {
response.WriteUnauthorizedError(w)
return
}
// Password correct and active user
if email != strings.TrimSpace(strings.ToLower(u.Email)) || !secrets.MatchPassword(u.Password, password, u.Salt) {
response.WriteUnauthorizedError(w)
return
}
org, err := organization.GetOrganizationByDomain(s, dom)
if err != nil {
response.WriteUnauthorizedError(w)
return
}
// Attach user accounts and work out permissions
user.AttachUserAccounts(s, org.RefID, &u)
// active check
if len(u.Accounts) == 0 {
response.WriteUnauthorizedError(w)
return
}
authModel := AuthenticationModel{}
authModel.Token = GenerateJWT(h.Runtime, u.RefID, org.RefID, dom)
authModel.User = u
response.WriteJSON(w, authModel)
}
// ValidateAuthToken finds and validates authentication token.
func (h *Handler) ValidateAuthToken(w http.ResponseWriter, r *http.Request) {
// TODO should this go after token validation?
if s := r.URL.Query().Get("section"); s != "" {
if err := provider.Callback(s, w, r); err != nil {
h.Runtime.Log.Error("section validation failure", err)
w.WriteHeader(http.StatusUnauthorized)
}
return
}
s := domain.StoreContext{Runtime: h.Runtime, Context: domain.GetRequestContext(r)}
token := FindJWT(r)
rc, _, tokenErr := DecodeJWT(h.Runtime, token)
var org = organization.Organization{}
var err = errors.New("")
// We always grab the org record regardless of token status.
// Why? If bad token we might be OK to alow anonymous access
// depending upon the domain in question.
if len(rc.OrgID) == 0 {
org, err = organization.GetOrganizationByDomain(s, organization.GetRequestSubdomain(s, r))
} else {
org, err = organization.GetOrganization(s, rc.OrgID)
}
rc.Subdomain = org.Domain
// Inability to find org record spells the end of this request.
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
// If we have bad auth token and the domain does not allow anon access
if !org.AllowAnonymousAccess && tokenErr != nil {
return
}
dom := organization.GetSubdomainFromHost(s, r)
dom2 := organization.GetRequestSubdomain(s, r)
if org.Domain != dom && org.Domain != dom2 {
w.WriteHeader(http.StatusUnauthorized)
return
}
// If we have bad auth token and the domain allows anon access
// then we generate guest rc.
if org.AllowAnonymousAccess {
// So you have a bad token
if len(token) > 1 {
if tokenErr != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
} else {
// Just grant anon user guest access
rc.UserID = "0"
rc.OrgID = org.RefID
rc.Authenticated = false
rc.Guest = true
}
}
rc.AllowAnonymousAccess = org.AllowAnonymousAccess
rc.OrgName = org.Title
rc.Administrator = false
rc.Editor = false
rc.Global = false
rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(s, r)
rc.SSL = r.TLS != nil
// Fetch user permissions for this org
if !rc.Authenticated {
w.WriteHeader(http.StatusUnauthorized)
return
}
u, err := user.GetSecuredUser(s, org.RefID, rc.UserID)
if err != nil {
w.WriteHeader(http.StatusUnauthorized)
return
}
rc.Administrator = u.Admin
rc.Editor = u.Editor
rc.Global = u.Global
response.WriteJSON(w, u)
return
}

133
domain/auth/jwt.go Normal file
View file

@ -0,0 +1,133 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package auth
import (
"fmt"
"net/http"
"strings"
"time"
jwt "github.com/dgrijalva/jwt-go"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// GenerateJWT generates JSON Web Token (http://jwt.io)
func GenerateJWT(rt env.Runtime, user, org, domain string) string {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "Documize",
"sub": "webapp",
"exp": time.Now().Add(time.Hour * 168).Unix(),
"user": user,
"org": org,
"domain": domain,
})
tokenString, _ := token.SignedString([]byte(rt.Flags.Salt))
return tokenString
}
// FindJWT looks for 'Authorization' request header OR query string "?token=XXX".
func FindJWT(r *http.Request) (token string) {
header := r.Header.Get("Authorization")
if header != "" {
header = strings.Replace(header, "Bearer ", "", 1)
}
if len(header) > 1 {
token = header
} else {
query := r.URL.Query()
token = query.Get("token")
}
if token == "null" {
token = ""
}
return
}
// DecodeJWT decodes raw token.
func DecodeJWT(rt env.Runtime, tokenString string) (c domain.RequestContext, claims jwt.Claims, err error) {
// sensible defaults
c.UserID = ""
c.OrgID = ""
c.Authenticated = false
c.Guest = false
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(rt.Flags.Salt), nil
})
if err != nil {
err = fmt.Errorf("bad authorization token")
return
}
if !token.Valid {
if ve, ok := err.(*jwt.ValidationError); ok {
if ve.Errors&jwt.ValidationErrorMalformed != 0 {
err = fmt.Errorf("bad token")
return
} else if ve.Errors&(jwt.ValidationErrorExpired|jwt.ValidationErrorNotValidYet) != 0 {
err = fmt.Errorf("expired token")
return
} else {
err = fmt.Errorf("bad token")
return
}
} else {
err = fmt.Errorf("bad token")
return
}
}
c = domain.RequestContext{}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
c.UserID = claims["user"].(string)
c.OrgID = claims["org"].(string)
} else {
fmt.Println(err)
}
if len(c.UserID) == 0 || len(c.OrgID) == 0 {
err = fmt.Errorf("unable parse token data")
return
}
c.Authenticated = true
c.Guest = false
return c, token.Claims, nil
}
// DecodeKeycloakJWT takes in Keycloak token string and decodes it.
func DecodeKeycloakJWT(t, pk string) (c jwt.MapClaims, err error) {
token, err := jwt.Parse(t, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return jwt.ParseRSAPublicKeyFromPEM([]byte(pk))
})
if c, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
return c, nil
}
return nil, err
}

28
domain/auth/model.go Normal file
View file

@ -0,0 +1,28 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package auth
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain/user"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// AuthenticationModel details authentication token and user details.
type AuthenticationModel struct {
Token string `json:"token"`
User user.User `json:"user"`
}

64
domain/context.go Normal file
View file

@ -0,0 +1,64 @@
package domain
import (
"fmt"
"net/http"
"time"
"github.com/documize/community/core/env"
"github.com/jmoiron/sqlx"
)
// RequestContext provides per request scoped values required
// by HTTP handlers.
type RequestContext struct {
AllowAnonymousAccess bool
Authenticated bool
Administrator bool
Guest bool
Editor bool
Global bool
UserID string
OrgID string
OrgName string
SSL bool
AppURL string // e.g. https://{url}.documize.com
Subdomain string
ClientIP string
Expires time.Time
Fullname string
Transaction *sqlx.Tx
}
//GetAppURL returns full HTTP url for the app
func (c *RequestContext) GetAppURL(endpoint string) string {
scheme := "http://"
if c.SSL {
scheme = "https://"
}
return fmt.Sprintf("%s%s/%s", scheme, c.AppURL, endpoint)
}
type key string
// DocumizeContextKey prevents key name collisions.
const DocumizeContextKey key = "documize context key"
// GetRequestContext returns RequestContext from context.Context
func GetRequestContext(r *http.Request) RequestContext {
return r.Context().Value(DocumizeContextKey).(RequestContext)
}
// StoreContext provides data persistence methods with runtime and request context.
type StoreContext struct {
Runtime env.Runtime
Context RequestContext
}
// NewContexts returns request scoped user context and store context for persistence logic.
func NewContexts(rt env.Runtime, r *http.Request) (RequestContext, StoreContext) {
ctx := GetRequestContext(r)
return ctx, StoreContext{Runtime: rt, Context: ctx}
}

39
domain/document/store.go Normal file
View file

@ -0,0 +1,39 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package document
import (
"fmt"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/pkg/errors"
)
// MoveDocumentSpace changes the label for client's organization's documents which have space "id", to "move".
func MoveDocumentSpace(s domain.StoreContext, id, move string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE document SET labelid=? WHERE orgid=? AND labelid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare document space move %s", id))
return
}
_, err = stmt.Exec(move, s.Context.OrgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute document space move %s", id))
return
}
return
}

70
domain/eventing/model.go Normal file
View file

@ -0,0 +1,70 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package eventing records and propagates events based on user actions.
package eventing
import "time"
// AppEvent represents an event initiated by a user.
type AppEvent struct {
ID uint64 `json:"-"`
OrgID string `json:"orgId"`
UserID string `json:"userId"`
Type string `json:"eventType"`
IP string `json:"ip"`
Created time.Time `json:"created"`
}
// EventType defines valid event entry types
type EventType string
const (
EventTypeDocumentAdd EventType = "added-document"
EventTypeDocumentUpload EventType = "uploaded-document"
EventTypeDocumentView EventType = "viewed-document"
EventTypeDocumentUpdate EventType = "updated-document"
EventTypeDocumentDelete EventType = "removed-document"
EventTypeDocumentRevisions EventType = "viewed-document-revisions"
EventTypeSpaceAdd EventType = "added-space"
EventTypeSpaceUpdate EventType = "updated-space"
EventTypeSpaceDelete EventType = "removed-space"
EventTypeSpacePermission EventType = "changed-space-permissions"
EventTypeSpaceJoin EventType = "joined-space"
EventTypeSpaceInvite EventType = "invited-space"
EventTypeSectionAdd EventType = "added-document-section"
EventTypeSectionUpdate EventType = "updated-document-section"
EventTypeSectionDelete EventType = "removed-document-section"
EventTypeSectionRollback EventType = "rolled-back-document-section"
EventTypeSectionResequence EventType = "resequenced-document-section"
EventTypeSectionCopy EventType = "copied-document-section"
EventTypeAttachmentAdd EventType = "added-attachment"
EventTypeAttachmentDownload EventType = "downloaded-attachment"
EventTypeAttachmentDelete EventType = "removed-attachment"
EventTypePinAdd EventType = "added-pin"
EventTypePinDelete EventType = "removed-pin"
EventTypePinResequence EventType = "resequenced-pin"
EventTypeBlockAdd EventType = "added-reusable-block"
EventTypeBlockUpdate EventType = "updated-reusable-block"
EventTypeBlockDelete EventType = "removed-reusable-block"
EventTypeTemplateAdd EventType = "added-document-template"
EventTypeTemplateUse EventType = "used-document-template"
EventTypeUserAdd EventType = "added-user"
EventTypeUserUpdate EventType = "updated-user"
EventTypeUserDelete EventType = "removed-user"
EventTypeUserPasswordReset EventType = "reset-user-password"
EventTypeAccountAdd EventType = "added-account"
EventTypeSystemLicense EventType = "changed-system-license"
EventTypeSystemAuth EventType = "changed-system-auth"
EventTypeSystemSMTP EventType = "changed-system-smtp"
EventTypeSessionStart EventType = "started-session"
EventTypeSearch EventType = "searched"
)

55
domain/eventing/store.go Normal file
View file

@ -0,0 +1,55 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package eventing records user events.
package eventing
import (
"time"
"github.com/documize/community/domain"
"github.com/pkg/errors"
)
// Record adds event entry for specified user.
func Record(s domain.StoreContext, t EventType) {
e := AppEvent{}
e.OrgID = s.Context.OrgID
e.UserID = s.Context.UserID
e.Created = time.Now().UTC()
e.IP = s.Context.ClientIP
e.Type = string(t)
tx, err := s.Runtime.Db.Beginx()
if err != nil {
err = errors.Wrap(err, "start transaction")
return
}
stmt, err := tx.Preparex("INSERT INTO userevent (orgid, userid, eventtype, ip, created) VALUES (?, ?, ?, ?, ?)")
if err != nil {
tx.Rollback()
err = errors.Wrap(err, "prepare insert RecordEvent")
return
}
_, err = stmt.Exec(e.OrgID, e.UserID, e.Type, e.IP, e.Created)
if err != nil {
err = errors.Wrap(err, "execute insert RecordEvent")
tx.Rollback()
return
}
stmt.Close()
tx.Commit()
return
}

34
domain/model.go Normal file
View file

@ -0,0 +1,34 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package domain ...
package domain
import (
"time"
)
// BaseEntity contains the database fields used in every table.
type BaseEntity struct {
ID uint64 `json:"-"`
RefID string `json:"id"`
Created time.Time `json:"created"`
Revised time.Time `json:"revised"`
}
// BaseEntityObfuscated is a mirror of BaseEntity,
// but with the fields invisible to JSON.
type BaseEntityObfuscated struct {
ID uint64 `json:"-"`
RefID string `json:"-"`
Created time.Time `json:"-"`
Revised time.Time `json:"-"`
}

View file

@ -0,0 +1,39 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package organization
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// Organization defines a company that uses this app.
type Organization struct {
domain.BaseEntity
Company string `json:"-"`
Title string `json:"title"`
Message string `json:"message"`
URL string `json:"url"`
Domain string `json:"domain"`
Email string `json:"email"`
AllowAnonymousAccess bool `json:"allowAnonymousAccess"`
AuthProvider string `json:"authProvider"`
AuthConfig string `json:"authConfig"`
ConversionEndpoint string `json:"conversionEndpoint"`
Serial string `json:"-"`
Active bool `json:"-"`
}

View file

@ -0,0 +1,46 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package organization
import (
"net/http"
"strings"
"github.com/documize/community/domain"
)
// GetRequestSubdomain extracts subdomain from referring URL.
func GetRequestSubdomain(s domain.StoreContext, r *http.Request) string {
return urlSubdomain(s, r.Referer())
}
// GetSubdomainFromHost extracts the subdomain from the requesting URL.
func GetSubdomainFromHost(s domain.StoreContext, r *http.Request) string {
return urlSubdomain(s, r.Host)
}
// Find the subdomain (which is actually the organisation).
func urlSubdomain(s domain.StoreContext, url string) string {
url = strings.ToLower(url)
url = strings.Replace(url, "https://", "", 1)
url = strings.Replace(url, "http://", "", 1)
parts := strings.Split(url, ".")
if len(parts) >= 2 {
url = parts[0]
} else {
url = ""
}
return CheckDomain(s, url)
}

View file

@ -0,0 +1,183 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package organization
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/documize/community/core/env"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
// AddOrganization inserts the passed organization record into the organization table.
func AddOrganization(s domain.StoreContext, org Organization) error {
org.Created = time.Now().UTC()
org.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.Preparex(
"INSERT INTO organization (refid, company, title, message, url, domain, email, allowanonymousaccess, serial, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert for org")
return err
}
_, err = stmt.Exec(org.RefID, org.Company, org.Title, org.Message, strings.ToLower(org.URL), strings.ToLower(org.Domain),
strings.ToLower(org.Email), org.AllowAnonymousAccess, org.Serial, org.Created, org.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for org")
return err
}
return nil
}
// GetOrganization returns the Organization reocrod from the organization database table with the given id.
func GetOrganization(s domain.StoreContext, id string) (org Organization, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for org %s", id))
return
}
err = stmt.Get(&org, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to get org %s", id))
return
}
return
}
// GetOrganizationByDomain returns the organization matching a given URL subdomain.
func GetOrganizationByDomain(s domain.StoreContext, subdomain string) (org Organization, err error) {
err = nil
subdomain = strings.ToLower(subdomain)
if s.Runtime.Flags.SiteMode == env.SiteModeNormal { // only return an organization when running normally
var stmt *sqlx.Stmt
stmt, err = s.Runtime.Db.Preparex("SELECT id, refid, company, title, message, url, domain, service as conversionendpoint, email, serial, active, allowanonymousaccess, authprovider, coalesce(authconfig,JSON_UNQUOTE('{}')) as authconfig, created, revised FROM organization WHERE domain=? AND active=1")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for subdomain %s", subdomain))
return
}
err = stmt.Get(&org, subdomain)
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for subdomain %s", subdomain))
return
}
}
return
}
// UpdateOrganization updates the given organization record in the database to the values supplied.
func UpdateOrganization(s domain.StoreContext, org Organization) (err error) {
org.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE organization SET title=:title, message=:message, service=:conversionendpoint, email=:email, allowanonymousaccess=:allowanonymousaccess, revised=:revised WHERE refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare update for org %s", org.RefID))
return
}
_, err = stmt.Exec(&org)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for org %s", org.RefID))
return
}
return
}
// DeleteOrganization deletes the orgID organization from the organization table.
func DeleteOrganization(s domain.StoreContext, orgID string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.Delete(s.Context.Transaction, "organization", orgID)
}
// RemoveOrganization sets the orgID organization to be inactive, thus executing a "soft delete" operation.
func RemoveOrganization(s domain.StoreContext, rc domain.RequestContext, orgID string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE organization SET active=0 WHERE refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare soft delete for org %s", orgID))
return
}
_, err = stmt.Exec(orgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute soft delete for org %s", orgID))
return
}
return
}
// UpdateAuthConfig updates the given organization record in the database with the auth config details.
func UpdateAuthConfig(s domain.StoreContext, org Organization) (err error) {
org.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE organization SET allowanonymousaccess=:allowanonymousaccess, authprovider=:authprovider, authconfig=:authconfig, revised=:revised WHERE refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare UpdateAuthConfig %s", org.RefID))
return
}
_, err = stmt.Exec(&org)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute UpdateAuthConfig %s", org.RefID))
return
}
return
}
// CheckDomain makes sure there is an organisation with the correct domain
func CheckDomain(s domain.StoreContext, domain string) string {
row := s.Runtime.Db.QueryRow("SELECT COUNT(*) FROM organization WHERE domain=? AND active=1", domain)
var count int
err := row.Scan(&count)
if err != nil {
return ""
}
if count == 1 {
return domain
}
return ""
}

33
domain/pin/model.go Normal file
View file

@ -0,0 +1,33 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package pin
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// Pin defines a saved link to a document or space
type Pin struct {
domain.BaseEntity
OrgID string `json:"orgId"`
UserID string `json:"userId"`
FolderID string `json:"folderId"`
DocumentID string `json:"documentId"`
Pin string `json:"pin"`
Sequence int `json:"sequence"`
}

145
domain/pin/store.go Normal file
View file

@ -0,0 +1,145 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package pin
import (
"fmt"
"time"
"github.com/documize/community/core/api/entity"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
// Add saves pinned item.
func Add(s domain.StoreContext, pin Pin) (err error) {
row := s.Runtime.Db.QueryRow("SELECT max(sequence) FROM pin WHERE orgid=? AND userid=?", s.Context.OrgID, s.Context.UserID)
var maxSeq int
err = row.Scan(&maxSeq)
if err != nil {
maxSeq = 99
}
pin.Created = time.Now().UTC()
pin.Revised = time.Now().UTC()
pin.Sequence = maxSeq + 1
stmt, err := s.Context.Transaction.Preparex("INSERT INTO pin (refid, orgid, userid, labelid, documentid, pin, sequence, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "prepare pin insert")
return
}
_, err = stmt.Exec(pin.RefID, pin.OrgID, pin.UserID, pin.FolderID, pin.DocumentID, pin.Pin, pin.Sequence, pin.Created, pin.Revised)
if err != nil {
err = errors.Wrap(err, "execute pin insert")
return
}
return
}
// GetPin returns requested pinned item.
func GetPin(s domain.StoreContext, id string) (pin Pin, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare select for pin %s", id))
return
}
err = stmt.Get(&pin, s.Context.OrgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute select for pin %s", id))
return
}
return
}
// GetUserPins returns pinned items for specified user.
func GetUserPins(s domain.StoreContext, userID string) (pins []Pin, err error) {
err = s.Runtime.Db.Select(&pins, "SELECT id, refid, orgid, userid, labelid as folderid, documentid, pin, sequence, created, revised FROM pin WHERE orgid=? AND userid=? ORDER BY sequence", s.Context.OrgID, userID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute select pins for org %s and user %s", s.Context.OrgID, userID))
return
}
return
}
// UpdatePin updates existing pinned item.
func UpdatePin(s domain.StoreContext, pin entity.Pin) (err error) {
pin.Revised = time.Now().UTC()
var stmt *sqlx.NamedStmt
stmt, err = s.Context.Transaction.PrepareNamed("UPDATE pin SET labelid=:folderid, documentid=:documentid, pin=:pin, sequence=:sequence, revised=:revised WHERE orgid=:orgid AND refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare pin update %s", pin.RefID))
return
}
_, err = stmt.Exec(&pin)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute pin update %s", pin.RefID))
return
}
return
}
// UpdatePinSequence updates existing pinned item sequence number
func UpdatePinSequence(s domain.StoreContext, pinID string, sequence int) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE pin SET sequence=?, revised=? WHERE orgid=? AND userid=? AND refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare pin sequence update %s", pinID))
return
}
_, err = stmt.Exec(sequence, time.Now().UTC(), s.Context.OrgID, s.Context.UserID, pinID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute pin sequence update %s", pinID))
return
}
return
}
// DeletePin removes folder from the store.
func DeletePin(s domain.StoreContext, id string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteConstrained(s.Context.Transaction, "pin", s.Context.OrgID, id)
}
// DeletePinnedSpace removes any pins for specified space.
func DeletePinnedSpace(s domain.StoreContext, spaceID string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteWhere(s.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND labelid=\"%s\"", s.Context.OrgID, spaceID))
}
// DeletePinnedDocument removes any pins for specified document.
func DeletePinnedDocument(s domain.StoreContext, documentID string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteWhere(s.Context.Transaction, fmt.Sprintf("DELETE FROM pin WHERE orgid=\"%s\" AND documentid=\"%s\"", s.Context.OrgID, documentID))
}

View file

@ -12,3 +12,757 @@
// Package space handles API calls and persistence for spaces. // Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents. // Spaces in Documize contain documents.
package space package space
import (
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/documize/api/wordsmith/log"
"github.com/documize/community/core/api/mail"
"github.com/documize/community/core/request"
"github.com/documize/community/core/response"
"github.com/documize/community/core/secrets"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/core/stringutil"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/domain/account"
"github.com/documize/community/domain/document"
"github.com/documize/community/domain/eventing"
"github.com/documize/community/domain/organization"
"github.com/documize/community/domain/pin"
"github.com/documize/community/domain/user"
)
// Add creates a new space.
func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
method := "AddSpace"
ctx, s := domain.NewContexts(h.Runtime, r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
var space = Space{}
err = json.Unmarshal(body, &space)
if err != nil {
response.WriteServerError(w, method, err)
return
}
if len(space.Name) == 0 {
response.WriteMissingDataError(w, method, "name")
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
space.RefID = uniqueid.Generate()
space.OrgID = ctx.OrgID
err = addSpace(s, space)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpaceAdd)
ctx.Transaction.Commit()
space, _ = Get(s, space.RefID)
response.WriteJSON(w, space)
}
// Get returns the requested space.
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
method := "Get"
_, s := domain.NewContexts(h.Runtime, r)
id := request.Param(r, "folderID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
sp, err := Get(s, id)
if err == sql.ErrNoRows {
response.WriteNotFoundError(w, method, id)
return
}
if err != nil {
response.WriteServerError(w, method, err)
return
}
response.WriteJSON(w, sp)
}
// GetAll returns spaces the user can see.
func (h *Handler) GetAll(w http.ResponseWriter, r *http.Request) {
method := "GetAll"
_, s := domain.NewContexts(h.Runtime, r)
sp, err := GetAll(s)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(sp) == 0 {
sp = []Space{}
}
response.WriteJSON(w, sp)
}
// GetSpaceViewers returns the users that can see the shared spaces.
func (h *Handler) GetSpaceViewers(w http.ResponseWriter, r *http.Request) {
method := "GetSpaceViewers"
_, s := domain.NewContexts(h.Runtime, r)
v, err := Viewers(s)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(v) == 0 {
v = []Viewer{}
}
response.WriteJSON(w, v)
}
// Update processes request to save space object to the database
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
method := "space.Update"
ctx, s := domain.NewContexts(h.Runtime, r)
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
folderID := request.Param(r, "folderID")
if len(folderID) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
var sp Space
err = json.Unmarshal(body, &sp)
if err != nil {
response.WriteBadRequestError(w, method, "marshal")
return
}
if len(sp.Name) == 0 {
response.WriteMissingDataError(w, method, "name")
return
}
sp.RefID = folderID
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
err = Update(s, sp)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpaceUpdate)
ctx.Transaction.Commit()
response.WriteJSON(w, sp)
}
// Remove moves documents to another folder before deleting it
func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) {
method := "space.Remove"
ctx, s := domain.NewContexts(h.Runtime, r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
id := request.Param(r, "folderID")
move := request.Param(r, "moveToId")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
if len(move) == 0 {
response.WriteMissingDataError(w, method, "moveToId")
return
}
var err error
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
_, err = Delete(s, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
err = document.MoveDocumentSpace(s, id, move)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
err = MoveSpaceRoles(s, id, move)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
_, err = pin.DeletePinnedSpace(s, id)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpaceDelete)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// Delete deletes empty space.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
method := "space.Delete"
ctx, s := domain.NewContexts(h.Runtime, r)
if !h.Runtime.Product.License.IsValid() {
response.WriteBadLicense(w)
return
}
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
id := request.Param(r, "folderID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
var err error
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
_, err = Delete(s, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
_, err = DeleteSpaceRoles(s, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
_, err = pin.DeletePinnedSpace(s, id)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpaceDelete)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// SetPermissions persists specified spac3 permissions
func (h *Handler) SetPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.SetPermissions"
ctx, s := domain.NewContexts(h.Runtime, r)
if !ctx.Editor {
response.WriteForbiddenError(w)
return
}
id := request.Param(r, "folderID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
sp, err := Get(s, id)
if err != nil {
response.WriteNotFoundError(w, method, "No such space")
return
}
if sp.UserID != s.Context.UserID {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
var model = RolesModel{}
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteServerError(w, method, err)
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
// We compare new permisions to what we had before.
// Why? So we can send out folder invitation emails.
previousRoles, err := GetRoles(s, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
// Store all previous roles as map for easy querying
previousRoleUsers := make(map[string]bool)
for _, v := range previousRoles {
previousRoleUsers[v.UserID] = true
}
// Who is sharing this folder?
inviter, err := user.Get(s, s.Context.UserID)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
// Nuke all previous permissions for this folder
_, err = DeleteSpaceRoles(s, id)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
me := false
hasEveryoneRole := false
roleCount := 0
url := s.Context.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
for _, role := range model.Roles {
role.OrgID = s.Context.OrgID
role.LabelID = id
// Ensure the folder owner always has access!
if role.UserID == s.Context.UserID {
me = true
role.CanView = true
role.CanEdit = true
}
if len(role.UserID) == 0 && (role.CanView || role.CanEdit) {
hasEveryoneRole = true
}
// Only persist if there is a role!
if role.CanView || role.CanEdit {
roleID := uniqueid.Generate()
role.RefID = roleID
err = AddRole(s, role)
roleCount++
log.IfErr(err)
// We send out folder invitation emails to those users
// that have *just* been given permissions.
if _, isExisting := previousRoleUsers[role.UserID]; !isExisting {
// we skip 'everyone' (user id != empty string)
if len(role.UserID) > 0 {
var existingUser user.User
existingUser, err = user.Get(s, role.UserID)
if err == nil {
go mail.ShareFolderExistingUser(existingUser.Email, inviter.Fullname(), url, sp.Name, model.Message)
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, existingUser.Email))
} else {
response.WriteServerError(w, method, err)
}
}
}
}
}
// Do we need to ensure permissions for space owner when shared?
if !me {
role := Role{}
role.LabelID = id
role.OrgID = s.Context.OrgID
role.UserID = s.Context.UserID
role.CanEdit = true
role.CanView = true
roleID := uniqueid.Generate()
role.RefID = roleID
err = AddRole(s, role)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
}
// Mark up folder type as either public, private or restricted access.
if hasEveryoneRole {
sp.Type = ScopePublic
} else {
if roleCount > 1 {
sp.Type = ScopeRestricted
} else {
sp.Type = ScopePrivate
}
}
err = Update(s, sp)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpacePermission)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}
// GetPermissions returns user permissions for the requested folder.
func (h *Handler) GetPermissions(w http.ResponseWriter, r *http.Request) {
method := "space.GetPermissions"
_, s := domain.NewContexts(h.Runtime, r)
folderID := request.Param(r, "folderID")
if len(folderID) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
roles, err := GetRoles(s, folderID)
if err != nil && err != sql.ErrNoRows {
response.WriteServerError(w, method, err)
return
}
if len(roles) == 0 {
roles = []Role{}
}
response.WriteJSON(w, roles)
}
// AcceptInvitation records the fact that a user has completed space onboard process.
func (h *Handler) AcceptInvitation(w http.ResponseWriter, r *http.Request) {
method := "space.AcceptInvitation"
ctx, s := domain.NewContexts(h.Runtime, r)
folderID := request.Param(r, "folderID")
if len(folderID) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
org, err := organization.GetOrganizationByDomain(s, ctx.Subdomain)
if err != nil {
response.WriteServerError(w, method, err)
return
}
// AcceptShare does not authenticate the user hence the context needs to set up
ctx.OrgID = org.RefID
s.Context.OrgID = org.RefID
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
var model = AcceptShareModel{}
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, err.Error())
return
}
if len(model.Serial) == 0 || len(model.Firstname) == 0 || len(model.Lastname) == 0 || len(model.Password) == 0 {
response.WriteMissingDataError(w, method, "Serial, Firstname, Lastname, Password")
return
}
u, err := user.GetBySerial(s, model.Serial)
if err != nil && err == sql.ErrNoRows {
response.WriteDuplicateError(w, method, "user")
return
}
// AcceptShare does not authenticate the user hence the context needs to set up
ctx.UserID = u.RefID
s.Context.UserID = u.RefID
u.Firstname = model.Firstname
u.Lastname = model.Lastname
u.Initials = stringutil.MakeInitials(u.Firstname, u.Lastname)
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
err = user.UpdateUser(s, u)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
salt := secrets.GenerateSalt()
err = user.UpdateUserPassword(s, u.RefID, salt, secrets.GeneratePassword(model.Password, salt))
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
eventing.Record(s, eventing.EventTypeSpaceJoin)
ctx.Transaction.Commit()
response.WriteJSON(w, u)
}
// Invite sends users folder invitation emails.
func (h *Handler) Invite(w http.ResponseWriter, r *http.Request) {
method := "space.Invite"
ctx, s := domain.NewContexts(h.Runtime, r)
id := request.Param(r, "folderID")
if len(id) == 0 {
response.WriteMissingDataError(w, method, "folderID")
return
}
sp, err := Get(s, id)
if err != nil {
response.WriteServerError(w, method, err)
return
}
if sp.UserID != s.Context.UserID {
response.WriteForbiddenError(w)
return
}
defer streamutil.Close(r.Body)
body, err := ioutil.ReadAll(r.Body)
if err != nil {
response.WriteBadRequestError(w, method, "body")
return
}
var model = InvitationModel{}
err = json.Unmarshal(body, &model)
if err != nil {
response.WriteBadRequestError(w, method, "json")
return
}
ctx.Transaction, err = h.Runtime.Db.Beginx()
if err != nil {
response.WriteServerError(w, method, err)
return
}
inviter, err := user.Get(s, ctx.UserID)
if err != nil {
response.WriteServerError(w, method, err)
return
}
for _, email := range model.Recipients {
u, err := user.GetByEmail(s, email)
if err != nil && err != sql.ErrNoRows {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
if len(u.RefID) > 0 {
// Ensure they have access to this organization
accounts, err2 := account.GetUserAccounts(s, u.RefID)
if err2 != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
// we create if they c
hasAccess := false
for _, a := range accounts {
if a.OrgID == s.Context.OrgID {
hasAccess = true
}
}
if !hasAccess {
var a account.Account
a.UserID = u.RefID
a.OrgID = s.Context.OrgID
a.Admin = false
a.Editor = false
a.Active = true
accountID := uniqueid.Generate()
a.RefID = accountID
err = account.Add(s, a)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
}
// Ensure they have space roles
DeleteUserSpaceRoles(s, sp.RefID, u.RefID)
role := Role{}
role.LabelID = sp.RefID
role.OrgID = ctx.OrgID
role.UserID = u.RefID
role.CanEdit = false
role.CanView = true
roleID := uniqueid.Generate()
role.RefID = roleID
err = AddRole(s, role)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
url := ctx.GetAppURL(fmt.Sprintf("s/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
go mail.ShareFolderExistingUser(email, inviter.Fullname(), url, sp.Name, model.Message)
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with existing user %s", inviter.Email, sp.Name, email))
} else {
// On-board new user
if strings.Contains(email, "@") {
url := ctx.GetAppURL(fmt.Sprintf("auth/share/%s/%s", sp.RefID, stringutil.MakeSlug(sp.Name)))
err = inviteNewUserToSharedSpace(s, email, inviter, url, sp, model.Message)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
h.Runtime.Log.Info(fmt.Sprintf("%s is sharing space %s with new user %s", inviter.Email, sp.Name, email))
}
}
}
// We ensure that the folder is marked as restricted as a minimum!
if len(model.Recipients) > 0 && sp.Type == ScopePrivate {
sp.Type = ScopeRestricted
err = Update(s, sp)
if err != nil {
ctx.Transaction.Rollback()
response.WriteServerError(w, method, err)
return
}
}
eventing.Record(s, eventing.EventTypeSpaceInvite)
ctx.Transaction.Commit()
response.WriteEmpty(w)
}

View file

@ -12,3 +12,92 @@
// Package space handles API calls and persistence for spaces. // Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents. // Spaces in Documize contain documents.
package space package space
import (
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// Space defines a container for documents.
type Space struct {
domain.BaseEntity
Name string `json:"name"`
OrgID string `json:"orgId"`
UserID string `json:"userId"`
Type Scope `json:"folderType"`
}
// Scope determines folder visibility.
type Scope int
const (
// ScopePublic can be seen by anyone
ScopePublic Scope = 1
// ScopePrivate can only be seen by the person who owns it
ScopePrivate Scope = 2
// ScopeRestricted can be seen by selected users
ScopeRestricted Scope = 3
)
// IsPublic means the folder can be seen by anyone.
func (l *Space) IsPublic() bool {
return l.Type == ScopePublic
}
// IsPrivate means the folder can only be seen by the person who owns it.
func (l *Space) IsPrivate() bool {
return l.Type == ScopePrivate
}
// IsRestricted means the folder can be seen by selected users.
func (l *Space) IsRestricted() bool {
return l.Type == ScopeRestricted
}
// Role determines user permissions for a folder.
type Role struct {
domain.BaseEntityObfuscated
OrgID string `json:"-"`
LabelID string `json:"folderId"`
UserID string `json:"userId"`
CanView bool `json:"canView"`
CanEdit bool `json:"canEdit"`
}
// Viewer details who can see a particular space
type Viewer struct {
Name string `json:"name"`
LabelID string `json:"folderId"`
Type int `json:"folderType"`
UserID string `json:"userId"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Email string `json:"email"`
}
// RolesModel details which users have what permissions on a given space.
type RolesModel struct {
Message string
Roles []Role
}
// AcceptShareModel is used to setup a user who has accepted a shared space.
type AcceptShareModel struct {
Serial string `json:"serial"`
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Password string `json:"password"`
}
// InvitationModel details which users have been invited to a space.
type InvitationModel struct {
Message string
Recipients []string
}

View file

@ -12,3 +12,51 @@
// Package space handles API calls and persistence for spaces. // Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents. // Spaces in Documize contain documents.
package space package space
import (
"database/sql"
"fmt"
"github.com/documize/community/domain"
)
// CanViewSpace returns if the user has permission to view the given spaceID.
func CanViewSpace(s domain.StoreContext, spaceID string) (hasPermission bool) {
roles, err := GetRoles(s, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
s.Runtime.Log.Error(fmt.Sprintf("check space permissions %s", spaceID), err)
return false
}
for _, role := range roles {
if role.LabelID == spaceID && (role.CanView || role.CanEdit) {
return true
}
}
return false
}
// CanViewSpaceDocuments returns if the user has permission to view a document within the specified space.
func CanViewSpaceDocuments(s domain.StoreContext, spaceID string) (hasPermission bool) {
roles, err := GetRoles(s, spaceID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
s.Runtime.Log.Error(fmt.Sprintf("check space permissions %s", spaceID), err)
return false
}
for _, role := range roles {
if role.LabelID == spaceID && (role.CanView || role.CanEdit) {
return true
}
}
return false
}

103
domain/space/space.go Normal file
View file

@ -0,0 +1,103 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package space
import (
"fmt"
"github.com/documize/community/core/api/mail"
"github.com/documize/community/core/secrets"
"github.com/documize/community/core/uniqueid"
"github.com/documize/community/domain"
"github.com/documize/community/domain/account"
"github.com/documize/community/domain/user"
)
// addSpace prepares and creates space record.
func addSpace(s domain.StoreContext, sp Space) (err error) {
sp.Type = ScopePrivate
sp.UserID = s.Context.UserID
err = Add(s, sp)
if err != nil {
return
}
role := Role{}
role.LabelID = sp.RefID
role.OrgID = sp.OrgID
role.UserID = s.Context.UserID
role.CanEdit = true
role.CanView = true
role.RefID = uniqueid.Generate()
err = AddRole(s, role)
return
}
// Invite new user to a folder that someone has shared with them.
// We create the user account with default values and then take them
// through a welcome process designed to capture profile data.
// We add them to the organization and grant them view-only folder access.
func inviteNewUserToSharedSpace(s domain.StoreContext, email string, invitedBy user.User,
baseURL string, sp Space, invitationMessage string) (err error) {
var u = user.User{}
u.Email = email
u.Firstname = email
u.Lastname = ""
u.Salt = secrets.GenerateSalt()
requestedPassword := secrets.GenerateRandomPassword()
u.Password = secrets.GeneratePassword(requestedPassword, u.Salt)
userID := uniqueid.Generate()
u.RefID = userID
err = user.Add(s, u)
if err != nil {
return
}
// Let's give this user access to the organization
var a account.Account
a.UserID = userID
a.OrgID = s.Context.OrgID
a.Admin = false
a.Editor = false
a.Active = true
accountID := uniqueid.Generate()
a.RefID = accountID
err = account.Add(s, a)
if err != nil {
return
}
role := Role{}
role.LabelID = sp.RefID
role.OrgID = s.Context.OrgID
role.UserID = userID
role.CanEdit = false
role.CanView = true
roleID := uniqueid.Generate()
role.RefID = roleID
err = AddRole(s, role)
if err != nil {
return
}
url := fmt.Sprintf("%s/%s", baseURL, u.Salt)
go mail.ShareFolderNewUser(u.Email, invitedBy.Fullname(), url, sp.Name, invitationMessage)
return
}

286
domain/space/store.go Normal file
View file

@ -0,0 +1,286 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents.
package space
import (
"database/sql"
"fmt"
"time"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/documize/community/domain/store/mysql"
"github.com/pkg/errors"
)
// Add adds new folder into the store.
func Add(s domain.StoreContext, sp Space) (err error) {
sp.UserID = s.Context.UserID
sp.Created = time.Now().UTC()
sp.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.Preparex("INSERT INTO label (refid, label, orgid, userid, type, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert for label")
return
}
_, err = stmt.Exec(sp.RefID, sp.Name, sp.OrgID, sp.UserID, sp.Type, sp.Created, sp.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for label")
return
}
return
}
// Get returns a space from the store.
func Get(s domain.StoreContext, id string) (sp Space, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label WHERE orgid=? and refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for label %s", id))
return
}
err = stmt.Get(&sp, s.Context.OrgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for label %s", id))
return
}
return
}
// PublicSpaces returns folders that anyone can see.
func PublicSpaces(s domain.StoreContext, orgID string) (sp []Space, err error) {
sql := "SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1"
err = s.Runtime.Db.Select(&sp, sql, orgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute GetPublicFolders for org %s", orgID))
return
}
return
}
// GetAll returns folders that the user can see.
// Also handles which folders can be seen by anonymous users.
func GetAll(s domain.StoreContext) (sp []Space, err error) {
sql := `
(SELECT id,refid,label as name,orgid,userid,type,created,revised from label WHERE orgid=? AND type=2 AND userid=?)
UNION ALL
(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=1 AND refid in
(SELECT labelid from labelrole WHERE orgid=? AND userid='' AND (canedit=1 OR canview=1)))
UNION ALL
(SELECT id,refid,label as name,orgid,userid,type,created,revised FROM label a where orgid=? AND type=3 AND refid in
(SELECT labelid from labelrole WHERE orgid=? AND userid=? AND (canedit=1 OR canview=1)))
ORDER BY name`
err = s.Runtime.Db.Select(&sp, sql,
s.Context.OrgID,
s.Context.UserID,
s.Context.OrgID,
s.Context.OrgID,
s.Context.OrgID,
s.Context.OrgID,
s.Context.UserID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute select labels for org %s", s.Context.OrgID))
return
}
return
}
// Update saves space changes.
func Update(s domain.StoreContext, sp Space) (err error) {
sp.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.PrepareNamed("UPDATE label SET label=:name, type=:type, userid=:userid, revised=:revised WHERE orgid=:orgid AND refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare update for label %s", sp.RefID))
return
}
_, err = stmt.Exec(&sp)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute update for label %s", sp.RefID))
return
}
return
}
// ChangeOwner transfer space ownership.
func ChangeOwner(s domain.StoreContext, currentOwner, newOwner string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE label SET userid=? WHERE userid=? AND orgid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare change space owner for %s", currentOwner))
return
}
_, err = stmt.Exec(newOwner, currentOwner, s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute change space owner for %s", currentOwner))
return
}
return
}
// Viewers returns the list of people who can see shared folders.
func Viewers(s domain.StoreContext) (v []Viewer, err error) {
sql := `
SELECT a.userid,
COALESCE(u.firstname, '') as firstname,
COALESCE(u.lastname, '') as lastname,
COALESCE(u.email, '') as email,
a.labelid,
b.label as name,
b.type
FROM labelrole a
LEFT JOIN label b ON b.refid=a.labelid
LEFT JOIN user u ON u.refid=a.userid
WHERE a.orgid=? AND b.type != 2
GROUP BY a.labelid,a.userid
ORDER BY u.firstname,u.lastname`
err = s.Runtime.Db.Select(&v, sql, s.Context.OrgID)
return
}
// Delete removes space from the store.
func Delete(s domain.StoreContext, id string) (rows int64, err error) {
b := mysql.BaseQuery{}
return b.DeleteConstrained(s.Context.Transaction, "label", s.Context.OrgID, id)
}
// AddRole inserts the given record into the labelrole database table.
func AddRole(s domain.StoreContext, r Role) (err error) {
r.Created = time.Now().UTC()
r.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.Preparex("INSERT INTO labelrole (refid, labelid, orgid, userid, canview, canedit, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "unable to prepare insert for space role")
return
}
_, err = stmt.Exec(r.RefID, r.LabelID, r.OrgID, r.UserID, r.CanView, r.CanEdit, r.Created, r.Revised)
if err != nil {
err = errors.Wrap(err, "unable to execute insert for space role")
return
}
return
}
// GetRoles returns a slice of labelrole records, for the given labelID in the client's organization, grouped by user.
func GetRoles(s domain.StoreContext, labelID string) (r []Role, err error) {
query := `SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? AND labelid=?` // was + "GROUP BY userid"
err = s.Runtime.Db.Select(&r, query, s.Context.OrgID, labelID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for space roles %s", labelID))
return
}
return
}
// GetUserRoles returns a slice of role records, for both the client's user and organization, and
// those space roles that exist for all users in the client's organization.
func GetUserRoles(s domain.StoreContext) (r []Role, err error) {
err = s.Runtime.Db.Select(&r, `
SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? and userid=?
UNION ALL
SELECT id, refid, labelid, orgid, userid, canview, canedit, created, revised FROM labelrole WHERE orgid=? AND userid=''`,
s.Context.OrgID, s.Context.UserID, s.Context.OrgID)
if err == sql.ErrNoRows {
err = nil
}
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user space roles %s", s.Context.UserID))
return
}
return
}
// DeleteRole deletes the labelRoleID record from the labelrole table.
func DeleteRole(s domain.StoreContext, roleID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND refid='%s'", s.Context.OrgID, roleID)
return b.DeleteWhere(s.Context.Transaction, sql)
}
// DeleteSpaceRoles deletes records from the labelrole table which have the given space ID.
func DeleteSpaceRoles(s domain.StoreContext, spaceID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND labelid='%s'", s.Context.OrgID, spaceID)
return b.DeleteWhere(s.Context.Transaction, sql)
}
// DeleteUserSpaceRoles removes all roles for the specified user, for the specified space.
func DeleteUserSpaceRoles(s domain.StoreContext, spaceID, userID string) (rows int64, err error) {
b := mysql.BaseQuery{}
sql := fmt.Sprintf("DELETE FROM labelrole WHERE orgid='%s' AND labelid='%s' AND userid='%s'",
s.Context.OrgID, spaceID, userID)
return b.DeleteWhere(s.Context.Transaction, sql)
}
// MoveSpaceRoles changes the space ID for space role records from previousLabel to newLabel.
func MoveSpaceRoles(s domain.StoreContext, previousLabel, newLabel string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE labelrole SET labelid=? WHERE labelid=? AND orgid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare move space roles for label %s", previousLabel))
return
}
_, err = stmt.Exec(newLabel, previousLabel, s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute move space roles for label %s", previousLabel))
}
return
}

View file

@ -1,14 +0,0 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents.
package space

View file

@ -1,14 +0,0 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
// Package space handles API calls and persistence for spaces.
// Spaces in Documize contain documents.
package space

100
domain/store/mysql/mysql.go Normal file
View file

@ -0,0 +1,100 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package mysql
import (
"fmt"
"github.com/documize/community/core/streamutil"
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
)
// BaseQuery provides common MySQL methods.
type BaseQuery struct {
}
// Delete record.
func (m *BaseQuery) Delete(tx *sqlx.Tx, table string, id string) (rows int64, err error) {
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare delete of row in table %s", table))
return
}
result, err := stmt.Exec(id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return
}
rows, err = result.RowsAffected()
return
}
// DeleteConstrained record constrained to Organization using refid.
func (m *BaseQuery) DeleteConstrained(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE orgid=? AND refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare constrained delete of row in table %s", table))
return
}
result, err := stmt.Exec(orgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return
}
rows, err = result.RowsAffected()
return
}
// DeleteConstrainedWithID record constrained to Organization using non refid.
func (m *BaseQuery) DeleteConstrainedWithID(tx *sqlx.Tx, table string, orgID, id string) (rows int64, err error) {
stmt, err := tx.Preparex("DELETE FROM " + table + " WHERE orgid=? AND id=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare ConstrainedWithID delete of row in table %s", table))
return
}
result, err := stmt.Exec(orgID, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to delete row in table %s", table))
return
}
rows, err = result.RowsAffected()
return
}
// DeleteWhere free form query.
func (m *BaseQuery) DeleteWhere(tx *sqlx.Tx, statement string) (rows int64, err error) {
result, err := tx.Exec(statement)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to delete rows: %s", statement))
return
}
rows, err = result.RowsAffected()
return
}

65
domain/user/model.go Normal file
View file

@ -0,0 +1,65 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package user
import (
"fmt"
"github.com/documize/community/core/env"
"github.com/documize/community/domain"
"github.com/documize/community/domain/account"
)
// Handler contains the runtime information such as logging and database.
type Handler struct {
Runtime env.Runtime
}
// User defines a login.
type User struct {
domain.BaseEntity
Firstname string `json:"firstname"`
Lastname string `json:"lastname"`
Email string `json:"email"`
Initials string `json:"initials"`
Active bool `json:"active"`
Editor bool `json:"editor"`
Admin bool `json:"admin"`
Global bool `json:"global"`
Password string `json:"-"`
Salt string `json:"-"`
Reset string `json:"-"`
Accounts []account.Account `json:"accounts"`
}
// ProtectSecrets blanks sensitive data.
func (user *User) ProtectSecrets() {
user.Password = ""
user.Salt = ""
user.Reset = ""
}
// Fullname returns Firstname + Lastname.
func (user *User) Fullname() string {
return fmt.Sprintf("%s %s", user.Firstname, user.Lastname)
}
// GetAccount returns matching org account using orgID
func (user *User) GetAccount(orgID string) (a account.Account, found bool) {
for _, a := range user.Accounts {
if a.OrgID == orgID {
return a, true
}
}
return a, false
}

294
domain/user/store.go Normal file
View file

@ -0,0 +1,294 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package user
import (
"database/sql"
"fmt"
"strings"
"time"
"github.com/documize/community/core/streamutil"
"github.com/documize/community/domain"
"github.com/pkg/errors"
)
// Add adds the given user record to the user table.
func Add(s domain.StoreContext, u User) (err error) {
u.Created = time.Now().UTC()
u.Revised = time.Now().UTC()
stmt, err := s.Context.Transaction.Preparex("INSERT INTO user (refid, firstname, lastname, email, initials, password, salt, reset, created, revised) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "prepare user insert")
return
}
_, err = stmt.Exec(u.RefID, u.Firstname, u.Lastname, strings.ToLower(u.Email), u.Initials, u.Password, u.Salt, "", u.Created, u.Revised)
if err != nil {
err = errors.Wrap(err, "execute user insert")
return
}
return
}
// Get returns the user record for the given id.
func Get(s domain.StoreContext, id string) (u User, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to prepare select for user %s", id))
return
}
err = stmt.Get(&u, id)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("unable to execute select for user %s", id))
return
}
return
}
// GetByDomain matches user by email and domain.
func GetByDomain(s domain.StoreContext, domain, email string) (u User, err error) {
email = strings.TrimSpace(strings.ToLower(email))
stmt, err := s.Runtime.Db.Preparex("SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.global, u.password, u.salt, u.reset, u.created, u.revised FROM user u, account a, organization o WHERE TRIM(LOWER(u.email))=? AND u.refid=a.userid AND a.orgid=o.refid AND TRIM(LOWER(o.domain))=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("Unable to prepare GetUserByDomain %s %s", domain, email))
return
}
err = stmt.Get(&u, email, domain)
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("Unable to execute GetUserByDomain %s %s", domain, email))
return
}
return
}
// GetByEmail returns a single row match on email.
func GetByEmail(s domain.StoreContext, email string) (u User, err error) {
email = strings.TrimSpace(strings.ToLower(email))
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE TRIM(LOWER(email))=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare select user by email %s", email))
return
}
err = stmt.Get(&u, email)
if err != nil && err != sql.ErrNoRows {
err = errors.Wrap(err, fmt.Sprintf("execute select user by email %s", email))
return
}
return
}
// GetByToken returns a user record given a reset token value.
func GetByToken(s domain.StoreContext, token string) (u User, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE reset=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare user select by token %s", token))
return
}
err = stmt.Get(&u, token)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user select by token %s", token))
return
}
return
}
// GetBySerial is used to retrieve a user via their temporary password salt value!
// This occurs when we you share a folder with a new user and they have to complete
// the onboarding process.
func GetBySerial(s domain.StoreContext, serial string) (u User, err error) {
stmt, err := s.Runtime.Db.Preparex("SELECT id, refid, firstname, lastname, email, initials, global, password, salt, reset, created, revised FROM user WHERE salt=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare user select by serial %s", serial))
return
}
err = stmt.Get(&u, serial)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user select by serial %s", serial))
return
}
return
}
// GetActiveUsersForOrganization returns a slice containing of active user records for the organization
// identified in the Persister.
func GetActiveUsersForOrganization(s domain.StoreContext) (u []User, err error) {
err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
FROM user u
WHERE u.refid IN (SELECT userid FROM account WHERE orgid = ? AND active=1) ORDER BY u.firstname,u.lastname`,
s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get active users by org %s", s.Context.OrgID))
return
}
return
}
// GetUsersForOrganization returns a slice containing all of the user records for the organizaiton
// identified in the Persister.
func GetUsersForOrganization(s domain.StoreContext) (u []User, err error) {
err = s.Runtime.Db.Select(&u,
"SELECT id, refid, firstname, lastname, email, initials, password, salt, reset, created, revised FROM user WHERE refid IN (SELECT userid FROM account where orgid = ?) ORDER BY firstname,lastname", s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf(" get users for org %s", s.Context.OrgID))
return
}
return
}
// GetSpaceUsers returns a slice containing all user records for given folder.
func GetSpaceUsers(s domain.StoreContext, folderID string) (u []User, err error) {
err = s.Runtime.Db.Select(&u,
`SELECT u.id, u.refid, u.firstname, u.lastname, u.email, u.initials, u.password, u.salt, u.reset, u.created, u.revised
FROM user u, account a
WHERE u.refid IN (SELECT userid from labelrole WHERE orgid=? AND labelid=?)
AND a.orgid=? AND u.refid = a.userid AND a.active=1
ORDER BY u.firstname, u.lastname`,
s.Context.OrgID, folderID, s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("get space users for org %s", s.Context.OrgID))
return
}
return
}
// UpdateUser updates the user table using the given replacement user record.
func UpdateUser(s domain.StoreContext, u User) (err error) {
u.Revised = time.Now().UTC()
u.Email = strings.ToLower(u.Email)
stmt, err := s.Context.Transaction.PrepareNamed(
"UPDATE user SET firstname=:firstname, lastname=:lastname, email=:email, revised=:revised, initials=:initials WHERE refid=:refid")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("prepare user update %s", u.RefID))
return
}
_, err = stmt.Exec(&u)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("execute user update %s", u.RefID))
return
}
return
}
// UpdateUserPassword updates a user record with new password and salt values.
func UpdateUserPassword(s domain.StoreContext, userID, salt, password string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE user SET salt=?, password=?, reset='' WHERE refid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "prepare user update")
return
}
_, err = stmt.Exec(salt, password, userID)
if err != nil {
err = errors.Wrap(err, "execute user update")
return
}
return
}
// DeactiveUser deletes the account record for the given userID and persister.Context.OrgID.
func DeactiveUser(s domain.StoreContext, userID string) (err error) {
stmt, err := s.Context.Transaction.Preparex("DELETE FROM account WHERE userid=? and orgid=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "prepare user deactivation")
return
}
_, err = stmt.Exec(userID, s.Context.OrgID)
if err != nil {
err = errors.Wrap(err, "execute user deactivation")
return
}
return
}
// ForgotUserPassword sets the password to '' and the reset field to token, for a user identified by email.
func ForgotUserPassword(s domain.StoreContext, email, token string) (err error) {
stmt, err := s.Context.Transaction.Preparex("UPDATE user SET reset=?, password='' WHERE LOWER(email)=?")
defer streamutil.Close(stmt)
if err != nil {
err = errors.Wrap(err, "prepare password reset")
return
}
_, err = stmt.Exec(token, strings.ToLower(email))
if err != nil {
err = errors.Wrap(err, "execute password reset")
return
}
return
}
// CountActiveUsers returns the number of active users in the system.
func CountActiveUsers(s domain.StoreContext) (c int) {
row := s.Runtime.Db.QueryRow("SELECT count(*) FROM user u WHERE u.refid IN (SELECT userid FROM account WHERE active=1)")
err := row.Scan(&c)
if err == sql.ErrNoRows {
return 0
}
if err != nil && err != sql.ErrNoRows {
s.Runtime.Log.Error("CountActiveUsers", err)
return 0
}
return
}

51
domain/user/user.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package user
import (
"github.com/documize/community/domain"
"github.com/documize/community/domain/account"
"github.com/pkg/errors"
)
// GetSecuredUser contain associated accounts but credentials are wiped.
func GetSecuredUser(s domain.StoreContext, orgID, q string) (u User, err error) {
u, err = Get(s, q)
AttachUserAccounts(s, orgID, &u)
return
}
// AttachUserAccounts attachs user accounts to user object.
func AttachUserAccounts(s domain.StoreContext, orgID string, u *User) {
u.ProtectSecrets()
a, err := account.GetUserAccounts(s, u.RefID)
if err != nil {
err = errors.Wrap(err, "fetch user accounts")
return
}
u.Accounts = a
u.Editor = false
u.Admin = false
u.Active = false
for _, account := range u.Accounts {
if account.OrgID == orgID {
u.Admin = account.Admin
u.Editor = account.Editor
u.Active = account.Active
break
}
}
}

View file

@ -19,7 +19,6 @@ import (
"github.com/documize/community/core/database" "github.com/documize/community/core/database"
"github.com/documize/community/core/env" "github.com/documize/community/core/env"
"github.com/documize/community/core/secrets" "github.com/documize/community/core/secrets"
"github.com/documize/community/server/web"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
) )
@ -66,7 +65,7 @@ func InitRuntime(r *env.Runtime) bool {
} }
// go into setup mode if required // go into setup mode if required
if r.Flags.SiteMode != web.SiteModeOffline { if r.Flags.SiteMode != env.SiteModeOffline {
if database.Check(r) { if database.Check(r) {
if err := database.Migrate(*r, true /* the config table exists */); err != nil { if err := database.Migrate(*r, true /* the config table exists */); err != nil {
r.Log.Error("unable to run database migration", err) r.Log.Error("unable to run database migration", err)

View file

@ -38,8 +38,8 @@ func main() {
// product details // product details
rt.Product = env.ProdInfo{} rt.Product = env.ProdInfo{}
rt.Product.Major = "1" rt.Product.Major = "1"
rt.Product.Minor = "50" rt.Product.Minor = "51"
rt.Product.Patch = "2" rt.Product.Patch = "0"
rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch)
rt.Product.Edition = "Community" rt.Product.Edition = "Community"
rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition) rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition)

View file

@ -1,6 +1,6 @@
{ {
"name": "documize", "name": "documize",
"version": "1.50.2", "version": "1.51.0",
"description": "The Document IDE", "description": "The Document IDE",
"private": true, "private": true,
"repository": "", "repository": "",

View file

@ -1,16 +1,16 @@
{ {
"community": "community":
{ {
"version": "1.50.2", "version": "1.51.0",
"major": 1, "major": 1,
"minor": 50, "minor": 50,
"patch": 2 "patch": 0
}, },
"enterprise": "enterprise":
{ {
"version": "1.52.2", "version": "1.53.0",
"major": 1, "major": 1,
"minor": 52, "minor": 53,
"patch": 2 "patch": 0
} }
} }

210
server/middleware.go Normal file
View file

@ -0,0 +1,210 @@
// Copyright 2016 Documize Inc. <legal@documize.com>. All rights reserved.
//
// This software (Documize Community Edition) is licensed under
// GNU AGPL v3 http://www.gnu.org/licenses/agpl-3.0.en.html
//
// You can operate outside the AGPL restrictions by purchasing
// Documize Enterprise Edition and obtaining a commercial license
// by contacting <sales@documize.com>.
//
// https://documize.com
package server
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"github.com/documize/community/core/env"
"github.com/documize/community/core/response"
"github.com/documize/community/domain"
"github.com/documize/community/domain/auth"
"github.com/documize/community/domain/organization"
"github.com/documize/community/domain/user"
)
type middleware struct {
Runtime env.Runtime
}
func (m *middleware) cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Allow-Headers", "host, content-type, accept, authorization, origin, referer, user-agent, cache-control, x-requested-with")
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status")
if r.Method == "OPTIONS" {
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
w.Header().Add("Cache-Control", "no-cache")
w.Write([]byte(""))
return
}
next(w, r)
}
func (m *middleware) metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("X-Documize-Version", m.Runtime.Product.Version)
w.Header().Add("Cache-Control", "no-cache")
// Prevent page from being displayed in an iframe
w.Header().Add("X-Frame-Options", "DENY")
next(w, r)
}
// Authorize secure API calls by inspecting authentication token.
// request.Context provides caller user information.
// Site meta sent back as HTTP custom headers.
func (m *middleware) Authorize(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
method := "Authorize"
s := domain.StoreContext{Runtime: m.Runtime, Context: domain.RequestContext{}}
// Let certain requests pass straight through
authenticated := preAuthorizeStaticAssets(m.Runtime, r)
if !authenticated {
token := auth.FindJWT(r)
rc, _, tokenErr := auth.DecodeJWT(m.Runtime, token)
var org = organization.Organization{}
var err = errors.New("")
if len(rc.OrgID) == 0 {
org, err = organization.GetOrganizationByDomain(s, organization.GetRequestSubdomain(s, r))
} else {
org, err = organization.GetOrganization(s, rc.OrgID)
}
// Inability to find org record spells the end of this request.
if err != nil {
response.WriteForbiddenError(w)
return
}
// If we have bad auth token and the domain does not allow anon access
if !org.AllowAnonymousAccess && tokenErr != nil {
response.WriteUnauthorizedError(w)
return
}
rc.Subdomain = org.Domain
dom := organization.GetSubdomainFromHost(s, r)
dom2 := organization.GetRequestSubdomain(s, r)
if org.Domain != dom && org.Domain != dom2 {
m.Runtime.Log.Info(fmt.Sprintf("domain mismatch %s vs. %s vs. %s", dom, dom2, org.Domain))
response.WriteUnauthorizedError(w)
return
}
// If we have bad auth token and the domain allows anon access
// then we generate guest context.
if org.AllowAnonymousAccess {
// So you have a bad token
if len(token) > 1 {
if tokenErr != nil {
response.WriteUnauthorizedError(w)
return
}
} else {
// Just grant anon user guest access
rc.UserID = "0"
rc.OrgID = org.RefID
rc.Authenticated = false
rc.Guest = true
}
}
rc.AllowAnonymousAccess = org.AllowAnonymousAccess
rc.OrgName = org.Title
rc.Administrator = false
rc.Editor = false
rc.Global = false
rc.AppURL = r.Host
rc.Subdomain = organization.GetSubdomainFromHost(s, r)
rc.SSL = r.TLS != nil
// get user IP from request
i := strings.LastIndex(r.RemoteAddr, ":")
if i == -1 {
rc.ClientIP = r.RemoteAddr
} else {
rc.ClientIP = r.RemoteAddr[:i]
}
fip := r.Header.Get("X-Forwarded-For")
if len(fip) > 0 {
rc.ClientIP = fip
}
// Fetch user permissions for this org
if rc.Authenticated {
u, err := user.GetSecuredUser(s, org.RefID, rc.UserID)
if err != nil {
response.WriteServerError(w, method, err)
return
}
rc.Administrator = u.Admin
rc.Editor = u.Editor
rc.Global = u.Global
rc.Fullname = u.Fullname()
// We send back with every HTTP request/response cycle the latest
// user state. This helps client-side applications to detect changes in
// user state/privileges.
var state struct {
Active bool `json:"active"`
Admin bool `json:"admin"`
Editor bool `json:"editor"`
}
state.Active = u.Active
state.Admin = u.Admin
state.Editor = u.Editor
sb, err := json.Marshal(state)
w.Header().Add("X-Documize-Status", string(sb))
}
// m.Runtime.Log.Info(fmt.Sprintf("%v", rc))
ctx := context.WithValue(r.Context(), domain.DocumizeContextKey, rc)
r = r.WithContext(ctx)
// Middleware moves on if we say 'yes' -- authenticated or allow anon access.
authenticated = rc.Authenticated || org.AllowAnonymousAccess
}
if authenticated {
next(w, r)
} else {
w.WriteHeader(http.StatusUnauthorized)
}
}
// Certain assets/URL do not require authentication.
// Just stops the log files being clogged up with failed auth errors.
func preAuthorizeStaticAssets(rt env.Runtime, r *http.Request) bool {
if strings.ToLower(r.URL.Path) == "/" ||
strings.ToLower(r.URL.Path) == "/validate" ||
strings.ToLower(r.URL.Path) == "/favicon.ico" ||
strings.ToLower(r.URL.Path) == "/robots.txt" ||
strings.ToLower(r.URL.Path) == "/version" ||
strings.HasPrefix(strings.ToLower(r.URL.Path), "/api/public/") ||
((rt.Flags.SiteMode == env.SiteModeSetup) && (strings.ToLower(r.URL.Path) == "/api/setup")) {
return true
}
return false
}

View file

@ -28,6 +28,8 @@ const (
RoutePrefixPrivate = "/api/" RoutePrefixPrivate = "/api/"
// RoutePrefixRoot used for unsecured endpoints at root (e.g. robots.txt) // RoutePrefixRoot used for unsecured endpoints at root (e.g. robots.txt)
RoutePrefixRoot = "/" RoutePrefixRoot = "/"
// RoutePrefixTesting used for isolated testing of routes with custom middleware
RoutePrefixTesting = "/testing/"
) )
type routeDef struct { type routeDef struct {

View file

@ -18,7 +18,6 @@ import (
"strings" "strings"
"github.com/codegangsta/negroni" "github.com/codegangsta/negroni"
"github.com/documize/community/core/api"
"github.com/documize/community/core/api/endpoint" "github.com/documize/community/core/api/endpoint"
"github.com/documize/community/core/api/plugins" "github.com/documize/community/core/api/plugins"
"github.com/documize/community/core/database" "github.com/documize/community/core/database"
@ -39,21 +38,24 @@ func Start(rt env.Runtime, ready chan struct{}) {
os.Exit(1) os.Exit(1)
} }
rt.Log.Info(fmt.Sprintf("Starting %s version %s", api.Runtime.Product.Title, api.Runtime.Product.Version)) rt.Log.Info(fmt.Sprintf("Starting %s version %s", rt.Product.Title, rt.Product.Version))
// decide which mode to serve up // decide which mode to serve up
switch api.Runtime.Flags.SiteMode { switch rt.Flags.SiteMode {
case web.SiteModeOffline: case env.SiteModeOffline:
rt.Log.Info("Serving OFFLINE web server") rt.Log.Info("Serving OFFLINE web server")
case web.SiteModeSetup: case env.SiteModeSetup:
routing.Add(rt, routing.RoutePrefixPrivate, "setup", []string{"POST", "OPTIONS"}, nil, database.Create) routing.Add(rt, routing.RoutePrefixPrivate, "setup", []string{"POST", "OPTIONS"}, nil, database.Create)
rt.Log.Info("Serving SETUP web server") rt.Log.Info("Serving SETUP web server")
case web.SiteModeBadDB: case env.SiteModeBadDB:
rt.Log.Info("Serving BAD DATABASE web server") rt.Log.Info("Serving BAD DATABASE web server")
default: default:
rt.Log.Info("Starting web server") rt.Log.Info("Starting web server")
} }
// define middleware
cm := middleware{Runtime: rt}
// define API endpoints // define API endpoints
routing.RegisterEndpoints(rt) routing.RegisterEndpoints(rt)
@ -62,7 +64,7 @@ func Start(rt env.Runtime, ready chan struct{}) {
// "/api/public/..." // "/api/public/..."
router.PathPrefix(routing.RoutePrefixPublic).Handler(negroni.New( router.PathPrefix(routing.RoutePrefixPublic).Handler(negroni.New(
negroni.HandlerFunc(cors), negroni.HandlerFunc(cm.cors),
negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixPublic)), negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixPublic)),
)) ))
@ -74,74 +76,46 @@ func Start(rt env.Runtime, ready chan struct{}) {
// "/..." // "/..."
router.PathPrefix(routing.RoutePrefixRoot).Handler(negroni.New( router.PathPrefix(routing.RoutePrefixRoot).Handler(negroni.New(
negroni.HandlerFunc(cors), negroni.HandlerFunc(cm.cors),
negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixRoot)), negroni.Wrap(routing.BuildRoutes(rt, routing.RoutePrefixRoot)),
)) ))
n := negroni.New() n := negroni.New()
n.Use(negroni.NewStatic(web.StaticAssetsFileSystem())) n.Use(negroni.NewStatic(web.StaticAssetsFileSystem()))
n.Use(negroni.HandlerFunc(cors)) n.Use(negroni.HandlerFunc(cm.cors))
n.Use(negroni.HandlerFunc(metrics)) n.Use(negroni.HandlerFunc(cm.metrics))
n.UseHandler(router) n.UseHandler(router)
// start server // start server
if !api.Runtime.Flags.SSLEnabled() { if !rt.Flags.SSLEnabled() {
rt.Log.Info("Starting non-SSL server on " + api.Runtime.Flags.HTTPPort) rt.Log.Info("Starting non-SSL server on " + rt.Flags.HTTPPort)
n.Run(testHost + ":" + api.Runtime.Flags.HTTPPort) n.Run(testHost + ":" + rt.Flags.HTTPPort)
} else { } else {
if api.Runtime.Flags.ForceHTTPPort2SSL != "" { if rt.Flags.ForceHTTPPort2SSL != "" {
rt.Log.Info("Starting non-SSL server on " + api.Runtime.Flags.ForceHTTPPort2SSL + " and redirecting to SSL server on " + api.Runtime.Flags.HTTPPort) rt.Log.Info("Starting non-SSL server on " + rt.Flags.ForceHTTPPort2SSL + " and redirecting to SSL server on " + rt.Flags.HTTPPort)
go func() { go func() {
err := http.ListenAndServe(":"+api.Runtime.Flags.ForceHTTPPort2SSL, http.HandlerFunc( err := http.ListenAndServe(":"+rt.Flags.ForceHTTPPort2SSL, http.HandlerFunc(
func(w http.ResponseWriter, req *http.Request) { func(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Connection", "close") w.Header().Set("Connection", "close")
var host = strings.Replace(req.Host, api.Runtime.Flags.ForceHTTPPort2SSL, api.Runtime.Flags.HTTPPort, 1) + req.RequestURI var host = strings.Replace(req.Host, rt.Flags.ForceHTTPPort2SSL, rt.Flags.HTTPPort, 1) + req.RequestURI
http.Redirect(w, req, "https://"+host, http.StatusMovedPermanently) http.Redirect(w, req, "https://"+host, http.StatusMovedPermanently)
})) }))
if err != nil { if err != nil {
rt.Log.Error("ListenAndServe on "+api.Runtime.Flags.ForceHTTPPort2SSL, err) rt.Log.Error("ListenAndServe on "+rt.Flags.ForceHTTPPort2SSL, err)
} }
}() }()
} }
rt.Log.Info("Starting SSL server on " + api.Runtime.Flags.HTTPPort + " with " + api.Runtime.Flags.SSLCertFile + " " + api.Runtime.Flags.SSLKeyFile) rt.Log.Info("Starting SSL server on " + rt.Flags.HTTPPort + " with " + rt.Flags.SSLCertFile + " " + rt.Flags.SSLKeyFile)
// TODO: https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/ // TODO: https://blog.gopheracademy.com/advent-2016/exposing-go-on-the-internet/
server := &http.Server{Addr: ":" + api.Runtime.Flags.HTTPPort, Handler: n /*, TLSConfig: myTLSConfig*/} server := &http.Server{Addr: ":" + rt.Flags.HTTPPort, Handler: n /*, TLSConfig: myTLSConfig*/}
server.SetKeepAlivesEnabled(true) server.SetKeepAlivesEnabled(true)
if err := server.ListenAndServeTLS(api.Runtime.Flags.SSLCertFile, api.Runtime.Flags.SSLKeyFile); err != nil { if err := server.ListenAndServeTLS(rt.Flags.SSLCertFile, rt.Flags.SSLKeyFile); err != nil {
rt.Log.Error("ListenAndServeTLS on "+api.Runtime.Flags.HTTPPort, err) rt.Log.Error("ListenAndServeTLS on "+rt.Flags.HTTPPort, err)
} }
} }
} }
func cors(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "PUT, GET, POST, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Allow-Headers", "host, content-type, accept, authorization, origin, referer, user-agent, cache-control, x-requested-with")
w.Header().Set("Access-Control-Expose-Headers", "x-documize-version, x-documize-status")
if r.Method == "OPTIONS" {
w.Header().Add("X-Documize-Version", api.Runtime.Product.Version)
w.Header().Add("Cache-Control", "no-cache")
w.Write([]byte(""))
return
}
next(w, r)
}
func metrics(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
w.Header().Add("X-Documize-Version", api.Runtime.Product.Version)
w.Header().Add("Cache-Control", "no-cache")
// Prevent page from being displayed in an iframe
w.Header().Add("X-Frame-Options", "DENY")
next(w, r)
}

View file

@ -17,20 +17,10 @@ import (
"net/http" "net/http"
"github.com/documize/community/core/api" "github.com/documize/community/core/api"
"github.com/documize/community/core/env"
"github.com/documize/community/core/secrets" "github.com/documize/community/core/secrets"
) )
const (
// SiteModeNormal serves app
SiteModeNormal = ""
// SiteModeOffline serves offline.html
SiteModeOffline = "1"
// SiteModeSetup tells Ember to serve setup route
SiteModeSetup = "2"
// SiteModeBadDB redirects to db-error.html page
SiteModeBadDB = "3"
)
// SiteInfo describes set-up information about the site // SiteInfo describes set-up information about the site
var SiteInfo struct { var SiteInfo struct {
DBname, DBhash, Issue string DBname, DBhash, Issue string
@ -44,11 +34,11 @@ func init() {
func EmberHandler(w http.ResponseWriter, r *http.Request) { func EmberHandler(w http.ResponseWriter, r *http.Request) {
filename := "index.html" filename := "index.html"
switch api.Runtime.Flags.SiteMode { switch api.Runtime.Flags.SiteMode {
case SiteModeOffline: case env.SiteModeOffline:
filename = "offline.html" filename = "offline.html"
case SiteModeSetup: case env.SiteModeSetup:
// NoOp // NoOp
case SiteModeBadDB: case env.SiteModeBadDB:
filename = "db-error.html" filename = "db-error.html"
default: default:
SiteInfo.DBhash = "" SiteInfo.DBhash = ""