diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 38b146d70..2aa0a4069 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -68,6 +68,31 @@ global: # to other servers and the federation API will not be exposed. disable_federation: false + # Consent tracking configuration + # If either require_at_registration or send_server_notice_to_guest are true, consent + # messages will be sent to the users. + user_consent: + # Require consent when user registers for the first time + require_at_registration: false + # The name to be shown to the user + policy_name: "Privacy policy" + # The directory to search for templates + template_dir: "./templates/privacy" + # The version of the policy. When loading templates, ".gohtml" template is added as a suffix + # e.g: ${template_dir}/1.0.gohtml needs to exist, if this is set to "1.0" + version: "1.0" + # Send a consent message to guest users + send_server_notice_to_guest: false + # Default message to send to users + server_notice_content: + msg_type: "m.text" + body: >- + Please give your consent to the privacy policy at {{ .ConsentURL }}. + # The error message to display if the user hasn't given their consent yet + block_events_error: >- + You can't send any messages until you consent to the privacy policy at + {{ .ConsentURL }}. + # Configuration for NATS JetStream jetstream: # A list of NATS Server addresses to connect to. If none are specified, an diff --git a/setup/base/base.go b/setup/base/base.go index 819fe1ad4..8300850f9 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -21,6 +21,7 @@ import ( "io" "net" "net/http" + _ "net/http/pprof" "os" "os/signal" "syscall" @@ -56,8 +57,6 @@ import ( userapi "github.com/matrix-org/dendrite/userapi/api" userapiinthttp "github.com/matrix-org/dendrite/userapi/inthttp" "github.com/sirupsen/logrus" - - _ "net/http/pprof" ) // BaseDendrite is a base for creating new instances of dendrite. It parses @@ -74,6 +73,7 @@ type BaseDendrite struct { PublicKeyAPIMux *mux.Router PublicMediaAPIMux *mux.Router PublicWellKnownAPIMux *mux.Router + PublicConsentAPIMux *mux.Router InternalAPIMux *mux.Router SynapseAdminMux *mux.Router UseHTTPAPIs bool @@ -205,6 +205,7 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base PublicKeyAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicKeyPathPrefix).Subrouter().UseEncodedPath(), PublicMediaAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicMediaPathPrefix).Subrouter().UseEncodedPath(), PublicWellKnownAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.PublicWellKnownPrefix).Subrouter().UseEncodedPath(), + PublicConsentAPIMux: mux.NewRouter().SkipClean(true).PathPrefix("/_matrix").Subrouter().UseEncodedPath(), InternalAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.InternalPathPrefix).Subrouter().UseEncodedPath(), SynapseAdminMux: mux.NewRouter().SkipClean(true).PathPrefix("/_synapse/").Subrouter().UseEncodedPath(), apiHttpClient: &apiClient, @@ -388,6 +389,7 @@ func (b *BaseDendrite) SetupAndServeHTTP( externalRouter.PathPrefix("/_synapse/").Handler(b.SynapseAdminMux) externalRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(b.PublicMediaAPIMux) externalRouter.PathPrefix(httputil.PublicWellKnownPrefix).Handler(b.PublicWellKnownAPIMux) + externalRouter.PathPrefix("/_matrix").Handler(b.PublicConsentAPIMux) if internalAddr != NoListener && internalAddr != externalAddr { go func() { diff --git a/setup/config/config_global.go b/setup/config/config_global.go index 6f2306a6d..016504da6 100644 --- a/setup/config/config_global.go +++ b/setup/config/config_global.go @@ -1,7 +1,10 @@ package config import ( + "fmt" + "html/template" "math/rand" + "path/filepath" "time" "github.com/matrix-org/gomatrixserverlib" @@ -57,6 +60,9 @@ type Global struct { // DNS caching options for all outbound HTTP requests DNSCache DNSCacheOptions `yaml:"dns_cache"` + + // Consent tracking options + UserConsentOptions UserConsentOptions `yaml:"user_consent"` } func (c *Global) Defaults(generate bool) { @@ -72,6 +78,7 @@ func (c *Global) Defaults(generate bool) { c.Metrics.Defaults(generate) c.DNSCache.Defaults() c.Sentry.Defaults() + c.UserConsentOptions.Defaults() } func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) { @@ -82,6 +89,7 @@ func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) { c.Metrics.Verify(configErrs, isMonolith) c.Sentry.Verify(configErrs, isMonolith) c.DNSCache.Verify(configErrs, isMonolith) + c.UserConsentOptions.Verify(configErrs, isMonolith) } type OldVerifyKeys struct { @@ -195,3 +203,71 @@ func (c *DNSCacheOptions) Verify(configErrs *ConfigErrors, isMonolith bool) { checkPositive(configErrs, "cache_size", int64(c.CacheSize)) checkPositive(configErrs, "cache_lifetime", int64(c.CacheLifetime)) } + +// Consent tracking configuration +// If either require_at_registration or send_server_notice_to_guest are true, consent +// messages will be sent to the users. +type UserConsentOptions struct { + // Require consent when user registers for the first time + RequireAtRegistration bool `yaml:"require_at_registration"` + // The name to be shown to the user + PolicyName string `yaml:"policy_name"` + // The directory to search for *.gohtml templates + TemplateDir string `yaml:"template_dir"` + // The version of the policy. When loading templates, ".gohtml" template is added as a suffix + // e.g: ${template_dir}/1.0.gohtml needs to exist, if this is set to 1.0 + Version string `yaml:"version"` + // Send a consent message to guest users + SendServerNoticeToGuest bool `yaml:"send_server_notice_to_guest"` + // Default message to send to users + ServerNoticeContent struct { + MsgType string `yaml:"msg_type"` + Body string `yaml:"body"` + } `yaml:"server_notice_content"` + // The error message to display if the user hasn't given their consent yet + BlockEventsError string `yaml:"block_events_error"` + // All loaded templates + Templates *template.Template `yaml:"-"` +} + +func (c *UserConsentOptions) Defaults() { + c.RequireAtRegistration = false + c.SendServerNoticeToGuest = false + c.PolicyName = "Privacy Policy" + c.Version = "1.0" + c.TemplateDir = "./templates/privacy" +} + +func (c *UserConsentOptions) Verify(configErrors *ConfigErrors, isMonolith bool) { + if c.Enabled() { + checkNotEmpty(configErrors, "template_dir", c.TemplateDir) + checkNotEmpty(configErrors, "version", c.Version) + checkNotEmpty(configErrors, "policy_name", c.PolicyName) + if len(*configErrors) > 0 { + return + } + + p, err := filepath.Abs(c.TemplateDir) + if err != nil { + configErrors.Add("unable to get template directory") + return + } + + // Read all defined *.gohtml templates + t, err := template.ParseGlob(filepath.Join(p, "*.gohtml")) + if err != nil || t == nil { + configErrors.Add(fmt.Sprintf("unable to read consent templates: %+v", err)) + return + } + c.Templates = t + // Verify we've got a template for the defined version + versionTemplate := c.Templates.Lookup(c.Version + ".gohtml") + if versionTemplate == nil { + configErrors.Add(fmt.Sprintf("unable to load defined '%s' policy template", c.Version)) + } + } +} + +func (c *UserConsentOptions) Enabled() bool { + return c.RequireAtRegistration || c.SendServerNoticeToGuest +} diff --git a/setup/monolith.go b/setup/monolith.go index e6c955222..1ada17fca 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -55,9 +55,9 @@ type Monolith struct { } // AddAllPublicRoutes attaches all public paths to the given router -func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux *mux.Router) { +func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux, consentMux *mux.Router) { clientapi.AddPublicRoutes( - csMux, synapseMux, &m.Config.ClientAPI, m.AccountDB, + csMux, synapseMux, consentMux, &m.Config.ClientAPI, m.AccountDB, m.FedClient, m.RoomserverAPI, m.EDUInternalAPI, m.AppserviceAPI, transactions.New(), m.FederationAPI, m.UserAPI, m.KeyAPI, m.ExtPublicRoomsProvider,