diff --git a/core/env/product.go b/core/env/product.go index bccea09f..c6e1a5b9 100644 --- a/core/env/product.go +++ b/core/env/product.go @@ -16,9 +16,71 @@ import ( "time" ) -// ProdInfo describes a product -type ProdInfo struct { - Edition string +// Edition is either Community or Enterprise. +type Edition string + +// Package controls feature-set within edition. +type Package string + +// Plan tells us if instance if self-hosted or Documize SaaS/Cloud. +type Plan string + +// Seats represents number of users. +type Seats int + +const ( + // CommunityEdition is AGPL licensed open core of product. + CommunityEdition Edition = "Community" + + // EnterpriseEdition is proprietary closed-source product. + EnterpriseEdition Edition = "Enterprise" + + // PackageEssentials provides core capabilities. + PackageEssentials Package = "Essentials" + + // PackageAdvanced provides analytics, reporting, + // content lifecycle, content verisoning, and audit logs. + PackageAdvanced Package = "Advanced" + + // PackagePremium provides actions, feedback capture, + // approvals workflow, secure external sharing. + PackagePremium Package = "Premium" + + // PackageDataCenter provides multi-tenanting + // and a bunch of professional services. + PackageDataCenter Package = "Data Center" + + // PlanCloud represents *.documize.com hosting. + PlanCloud Plan = "Cloud" + + // PlanSelfHost represents privately hosted Documize instance. + PlanSelfHost Plan = "Self-host" + + // Seats0 is 0 users. + Seats0 Seats = 0 + + // Seats1 is 10 users. + Seats1 Seats = 10 + + // Seats2 is 25 users. + Seats2 Seats = 25 + + //Seats3 is 50 users. + Seats3 Seats = 50 + + // Seats4 is 100 users. + Seats4 Seats = 100 + + //Seats5 is 250 users. + Seats5 Seats = 250 + + // Seats6 is unlimited. + Seats6 Seats = 9999 +) + +// Product provides meta information about product and licensing. +type Product struct { + Edition Edition Title string Version string Major string @@ -28,42 +90,66 @@ type ProdInfo struct { License License } -// License holds details of product license. +// License provides details of product license. type License struct { Name string `json:"name"` Email string `json:"email"` - Edition string `json:"edition"` - Package string `json:"package"` - Plan string `json:"plan"` + Edition Edition `json:"edition"` + Package Package `json:"package"` + Plan Plan `json:"plan"` Start time.Time `json:"start"` End time.Time `json:"end"` - Seats int `json:"seats"` + Seats Seats `json:"seats"` Trial bool `json:"trial"` - Valid bool `json:"valid"` + + // UserCount is number of users within Documize instance by tenant. + // Provided at runtime. + UserCount map[string]int } // IsEmpty determines if we have a license. func (l *License) IsEmpty() bool { - return l.Seats == 0 && len(l.Name) == 0 && len(l.Email) == 0 && l.Start.Year() == 1 && l.End.Year() == 1 + return l.Seats == Seats0 && + len(l.Name) == 0 && len(l.Email) == 0 && l.Start.Year() == 1 && l.End.Year() == 1 } // Status returns formatted message stating if license is empty/populated and invalid/valid. -func (l *License) Status() string { +func (l *License) Status(orgID string) string { lp := "populated" if l.IsEmpty() { lp = "empty" } lv := "invalid" - if l.Valid { + if l.IsValid(orgID) { lv = "valid" } 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 +// IsValid returns if license is valid for specified tenant. +func (l *License) IsValid(orgID string) bool { + valid := false + + // Community edition is always valid. + if l.Edition == CommunityEdition { + valid = true + } + + // Enterprise edition is valid if subcription date is + // greater than now and we have enough users/seats. + if l.Edition == EnterpriseEdition { + if time.Now().UTC().Before(l.End) && l.UserCount[orgID] <= int(l.Seats) { + valid = true + } + } + + // Empty means we cannot be valid + if l.IsEmpty() || len(l.UserCount) == 0 { + valid = false + } + + return valid } // LicenseData holds encrypted data and is unpacked into License. @@ -71,3 +157,9 @@ type LicenseData struct { Key string `json:"key"` Signature string `json:"signature"` } + +// LicenseUserAcount states number of active users by tenant. +type LicenseUserAcount struct { + OrgID string `json:"orgId"` + Users int `json:"users"` +} diff --git a/core/env/runtime.go b/core/env/runtime.go index 6c8873e6..7eac995a 100644 --- a/core/env/runtime.go +++ b/core/env/runtime.go @@ -23,7 +23,7 @@ type Runtime struct { Db *sqlx.DB StoreProvider StoreProvider Log Logger - Product ProdInfo + Product Product } const ( @@ -39,11 +39,3 @@ const ( // SiteModeBadDB redirects to db-error.html page SiteModeBadDB = "3" ) - -const ( - // CommunityEdition is AGPL product variant - CommunityEdition = "Community" - - // EnterpriseEdition is commercial licensed product variant - EnterpriseEdition = "Enterprise" -) diff --git a/domain/auth/secrets.go b/domain/auth/secrets.go index f96f74da..fc398539 100644 --- a/domain/auth/secrets.go +++ b/domain/auth/secrets.go @@ -23,6 +23,7 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string { switch provider { case auth.AuthProviderDocumize: return config + case auth.AuthProviderKeycloak: c := auth.KeycloakConfig{} err := json.Unmarshal([]byte(config), &c) @@ -41,6 +42,7 @@ func StripAuthSecrets(r *env.Runtime, provider, config string) string { } return string(j) + case auth.AuthProviderLDAP: c := auth.LDAPConfig{} err := json.Unmarshal([]byte(config), &c) diff --git a/domain/backup/backup.go b/domain/backup/backup.go index 789c4b74..8adefcaa 100644 --- a/domain/backup/backup.go +++ b/domain/backup/backup.go @@ -206,7 +206,7 @@ func (b backerHandler) produce(id string) (files []backupItem, err error) { func (b backerHandler) manifest(id string) (string, error) { m := m.Manifest{ ID: id, - Edition: b.Runtime.Product.Edition, + Edition: b.Runtime.Product.License.Edition, Version: b.Runtime.Product.Version, Major: b.Runtime.Product.Major, Minor: b.Runtime.Product.Minor, diff --git a/domain/block/endpoint.go b/domain/block/endpoint.go index a0656e9e..a4f17856 100644 --- a/domain/block/endpoint.go +++ b/domain/block/endpoint.go @@ -39,7 +39,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { method := "block.add" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } diff --git a/domain/meta/endpoint.go b/domain/meta/endpoint.go index 7e21d7d5..3f04ab27 100644 --- a/domain/meta/endpoint.go +++ b/domain/meta/endpoint.go @@ -60,7 +60,7 @@ func (h *Handler) Meta(w http.ResponseWriter, r *http.Request) { data.Version = h.Runtime.Product.Version data.Revision = h.Runtime.Product.Revision data.Edition = h.Runtime.Product.Edition - data.Valid = h.Runtime.Product.License.Valid + data.Valid = h.Runtime.Product.License.IsValid(org.RefID) data.ConversionEndpoint = org.ConversionEndpoint data.License = h.Runtime.Product.License data.Storage = h.Runtime.StoreProvider.Type() diff --git a/domain/page/endpoint.go b/domain/page/endpoint.go index 7becd244..ccdb8825 100644 --- a/domain/page/endpoint.go +++ b/domain/page/endpoint.go @@ -52,7 +52,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { method := "page.add" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -322,7 +322,7 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { method := "page.update" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -510,7 +510,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { method := "page.delete" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -608,7 +608,7 @@ func (h *Handler) DeletePages(w http.ResponseWriter, r *http.Request) { method := "page.delete.pages" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -721,7 +721,7 @@ func (h *Handler) ChangePageSequence(w http.ResponseWriter, r *http.Request) { method := "page.sequence" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -791,7 +791,7 @@ func (h *Handler) ChangePageLevel(w http.ResponseWriter, r *http.Request) { method := "page.level" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -987,7 +987,7 @@ func (h *Handler) GetDocumentRevisions(w http.ResponseWriter, r *http.Request) { method := "page.document.revisions" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -1018,7 +1018,7 @@ func (h *Handler) GetRevisions(w http.ResponseWriter, r *http.Request) { method := "page.revisions" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -1053,7 +1053,7 @@ func (h *Handler) GetDiff(w http.ResponseWriter, r *http.Request) { method := "page.diff" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } diff --git a/domain/pin/endpoint.go b/domain/pin/endpoint.go index 348d6c31..7c62f389 100644 --- a/domain/pin/endpoint.go +++ b/domain/pin/endpoint.go @@ -45,7 +45,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { return } - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -154,7 +154,7 @@ func (h *Handler) DeleteUserPin(w http.ResponseWriter, r *http.Request) { return } - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -198,7 +198,7 @@ func (h *Handler) UpdatePinSequence(w http.ResponseWriter, r *http.Request) { return } - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } diff --git a/domain/space/endpoint.go b/domain/space/endpoint.go index f23b33a1..3e6b6bb8 100644 --- a/domain/space/endpoint.go +++ b/domain/space/endpoint.go @@ -57,7 +57,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { method := "space.add" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -582,7 +582,7 @@ func (h *Handler) Remove(w http.ResponseWriter, r *http.Request) { method := "space.remove" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } @@ -675,7 +675,7 @@ func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { method := "space.delete" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } diff --git a/domain/store/storer.go b/domain/store/storer.go index a93ec153..1e812017 100644 --- a/domain/store/storer.go +++ b/domain/store/storer.go @@ -12,6 +12,7 @@ package store import ( + "github.com/documize/community/core/env" "github.com/documize/community/domain" "github.com/documize/community/model/account" "github.com/documize/community/model/activity" @@ -122,7 +123,7 @@ type UserStorer interface { UpdateUserPassword(ctx domain.RequestContext, userID, salt, password string) (err error) DeactiveUser(ctx domain.RequestContext, userID string) (err error) ForgotUserPassword(ctx domain.RequestContext, email, token string) (err error) - CountActiveUsers() (c int) + CountActiveUsers() (c []env.LicenseUserAcount) MatchUsers(ctx domain.RequestContext, text string, maxMatches int) (u []user.User, err error) } diff --git a/domain/template/endpoint.go b/domain/template/endpoint.go index f0509cf4..80e00535 100644 --- a/domain/template/endpoint.go +++ b/domain/template/endpoint.go @@ -90,7 +90,7 @@ func (h *Handler) SaveAs(w http.ResponseWriter, r *http.Request) { method := "template.saved" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) return } diff --git a/domain/test/test.go b/domain/test/test.go index 88e7af1c..cbc2dccc 100644 --- a/domain/test/test.go +++ b/domain/test/test.go @@ -2,6 +2,7 @@ package test import ( "fmt" + "github.com/documize/community/domain/store" "github.com/documize/community/core/env" "github.com/documize/community/domain" @@ -13,20 +14,21 @@ import ( ) // SetupTest prepares test environment -func SetupTest() (rt *env.Runtime, s *domain.Store, ctx domain.RequestContext) { +func SetupTest() (rt *env.Runtime, s *store + .Store, ctx domain.RequestContext) { rt, s = startRuntime() ctx = setupContext() return rt, s, ctx } -func startRuntime() (rt *env.Runtime, s *domain.Store) { +func startRuntime() (rt *env.Runtime, s *store.Store) { rt = new(env.Runtime) - s = new(domain.Store) + s = new(store.Store) rt.Log = logging.NewLogger(false) web.Embed = embed.NewEmbedder() - rt.Product = env.ProdInfo{} + rt.Product = env.Product{} rt.Product.Major = "0" rt.Product.Minor = "0" rt.Product.Patch = "0" @@ -35,9 +37,8 @@ func startRuntime() (rt *env.Runtime, s *domain.Store) { rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition) rt.Product.License = env.License{} rt.Product.License.Seats = 1 - rt.Product.License.Valid = true rt.Product.License.Trial = false - rt.Product.License.Edition = "Community" + rt.Product.License.Edition = env.CommunityEdition // parse settings from command line and environment rt.Flags = env.ParseFlags() @@ -56,7 +57,7 @@ func setupContext() domain.RequestContext { ctx.Administrator = true ctx.Guest = false ctx.Editor = true - ctx.Global = true + ctx.GlobalAdmin = true ctx.UserID = "test" ctx.OrgID = "test" return ctx diff --git a/domain/user/endpoint.go b/domain/user/endpoint.go index 11e3e89d..09e45950 100644 --- a/domain/user/endpoint.go +++ b/domain/user/endpoint.go @@ -51,7 +51,7 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) { method := "user.Add" ctx := domain.GetRequestContext(r) - if !h.Runtime.Product.License.IsValid() { + if !h.Runtime.Product.License.IsValid(ctx.OrgID) { response.WriteBadLicense(w) } diff --git a/domain/user/store.go b/domain/user/store.go index ee596b88..fcec69fe 100644 --- a/domain/user/store.go +++ b/domain/user/store.go @@ -14,6 +14,7 @@ package user import ( "database/sql" "fmt" + "github.com/documize/community/core/env" "strconv" "strings" "time" @@ -312,17 +313,11 @@ func (s Store) ForgotUserPassword(ctx domain.RequestContext, email, token string } // CountActiveUsers returns the number of active users in the system. -func (s Store) CountActiveUsers() (c int) { - row := s.Runtime.Db.QueryRow("SELECT count(*) FROM dmz_user WHERE c_refid IN (SELECT c_userid FROM dmz_user_account WHERE c_active=true)") - - err := row.Scan(&c) - if err == sql.ErrNoRows { - return 0 - } +func (s Store) CountActiveUsers() (c []env.LicenseUserAcount) { + err := s.Runtime.Db.Select(&c, "SELECT c_orgid AS orgid, COUNT(*) AS users FROM dmz_user_account WHERE c_active=true GROUP BY c_orgid ORDER BY c_orgid") if err != nil && err != sql.ErrNoRows { s.Runtime.Log.Error("CountActiveUsers", err) - return 0 } return diff --git a/edition/community.go b/edition/community.go index d9483696..223156f3 100644 --- a/edition/community.go +++ b/edition/community.go @@ -14,6 +14,7 @@ package main import ( "fmt" + "time" "github.com/documize/community/core/env" "github.com/documize/community/domain/section" @@ -36,21 +37,20 @@ func main() { web.Embed = embed.NewEmbedder() // product details - rt.Product = env.ProdInfo{} + rt.Product = env.Product{} rt.Product.Major = "1" rt.Product.Minor = "72" rt.Product.Patch = "1" rt.Product.Revision = 181022154519 rt.Product.Version = fmt.Sprintf("%s.%s.%s", rt.Product.Major, rt.Product.Minor, rt.Product.Patch) - rt.Product.Edition = "Community" + rt.Product.Edition = env.CommunityEdition rt.Product.Title = fmt.Sprintf("%s Edition", rt.Product.Edition) - rt.Product.License = env.License{} - rt.Product.License.Seats = 1 - rt.Product.License.Valid = true - rt.Product.License.Trial = false - rt.Product.License.Edition = "Community" - // setup store + // Community edition is good to go with no user limits. + rt.Product.License = env.License{Edition: env.CommunityEdition, Seats: env.Seats6, Trial: false, + Start: time.Now().UTC(), End: time.Now().UTC().Add(time.Hour * 24 * 7 * time.Duration(52))} + + // Setup data store. s := store.Store{} // parse settings from command line and environment diff --git a/gui/app/constants/constants.js b/gui/app/constants/constants.js index 8f713b11..8d1dcdb7 100644 --- a/gui/app/constants/constants.js +++ b/gui/app/constants/constants.js @@ -141,6 +141,58 @@ let constants = EmberObject.extend({ MySQL: 'MySQL', PostgreSQL: 'PostgreSQL', }, + + // Product is where we try to balance the fine line between useful open core + // and the revenue-generating proprietary edition. + Product: { // eslint-disable-line ember/avoid-leaking-state-in-ember-objects + // CommunityEdition is AGPL licensed open core of product. + CommunityEdition: 'Community', + + // EnterpriseEdition is proprietary closed-source product. + EnterpriseEdition: 'Enterprise', + + // PackageEssentials provides core capabilities. + PackageEssentials: "Essentials", + + // PackageAdvanced provides analytics, reporting, + // content lifecycle, content verisoning, and audit logs. + PackageAdvanced: "Advanced", + + // PackagePremium provides actions, feedback capture, + // approvals workflow, secure external sharing. + PackagePremium: "Premium", + + // PackageDataCenter provides multi-tenanting + // and a bunch of professional services. + PackageDataCenter: "Data Center", + + // PlanCloud represents *.documize.com hosting. + PlanCloud: "Cloud", + + // PlanSelfHost represents privately hosted Documize instance. + PlanSelfHost: "Self-host", + + // Seats0 is 0 users. + Seats0: 0, + + // Seats1 is 10 users. + Seats1: 10, + + // Seats2 is 25 users. + Seats2: 25, + + //Seats3 is 50 users. + Seats3: 50, + + // Seats4 is 100 users. + Seats4: 100, + + //Seats5 is 250 users. + Seats5: 250, + + // Seats6 is unlimited. + Seats6: 9999 + } }); export default { constants } diff --git a/gui/app/templates/components/customize/license-key.hbs b/gui/app/templates/components/customize/license-key.hbs index cd28582b..4a088e7f 100644 --- a/gui/app/templates/components/customize/license-key.hbs +++ b/gui/app/templates/components/customize/license-key.hbs @@ -15,7 +15,7 @@ {{textarea id="product-license-xml" value=license rows="18" class=(if LicenseError 'form-control is-invalid' 'form-control')}} {{#if appMeta.valid}} - {{#if (eq appMeta.edition "Enterprise")}} + {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}}

Registered to {{appMeta.license.email}} @ {{appMeta.license.name}}

{{appMeta.license.package}} package up to {{appMeta.license.seats}} users

{{#if appMeta.license.trial}} diff --git a/gui/app/templates/components/layout/top-bar.hbs b/gui/app/templates/components/layout/top-bar.hbs index 46f12da3..ae3d6f8c 100644 --- a/gui/app/templates/components/layout/top-bar.hbs +++ b/gui/app/templates/components/layout/top-bar.hbs @@ -7,12 +7,12 @@
  • - {{#if (eq appMeta.edition 'Community')}} + {{#if (eq appMeta.edition constants.Product.CommunityEdition)}}
  • {{#link-to "folders" class=(if (eq selectItem 'spaces') 'link selected' 'link')}}SPACES{{/link-to}}
  • {{/if}} - {{#if (eq appMeta.edition 'Enterprise')}} + {{#if (eq appMeta.edition constants.Product.EnterpriseEdition)}} {{#if session.viewDashboard}}
  • {{#link-to "dashboard" class=(if (eq selectItem 'dashboard') 'link selected' 'link')}}ACTIONS{{/link-to}} @@ -36,10 +36,10 @@ menu