From fa71253552e41de808f2b4dc7e591407cec0322a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 30 Jun 2021 12:50:50 +0200 Subject: [PATCH 01/60] Add Dockerfile.old --- Dockerfile.old | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 Dockerfile.old diff --git a/Dockerfile.old b/Dockerfile.old new file mode 100644 index 0000000000..82c4db78e3 --- /dev/null +++ b/Dockerfile.old @@ -0,0 +1,26 @@ +# Copyright 2018-2021 CERN +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# In applying this license, CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +FROM golang:1.16 + +WORKDIR /go/src/github/cs3org/reva +COPY . . +RUN make build-revad-docker && cp /go/src/github/cs3org/reva/cmd/revad/revad /go/bin/revad && mkdir -p /etc/revad/ && echo "" > /etc/revad/revad.toml +EXPOSE 9999 +EXPOSE 10000 +CMD ["/go/bin/revad", "-c", "/etc/revad/revad.toml", "-p", "/var/run/revad.pid"] From 20128434aff8a211e8c04d6937984c6f2091cd4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 30 Jun 2021 14:21:52 +0200 Subject: [PATCH 02/60] Restructure endpoint handling --- .../publicstorageprovider.go | 2 +- .../cloud/capabilities/capabilities.go | 22 +- internal/http/services/siteacc/siteacc.go | 307 +---------------- .../services => pkg}/siteacc/config/config.go | 0 .../siteacc/config/endpoints.go | 0 .../services => pkg}/siteacc/data/account.go | 0 .../siteacc/data/filestorage.go | 9 +- .../services => pkg}/siteacc/data/storage.go | 0 .../services => pkg}/siteacc/email/email.go | 8 +- .../siteacc/email/template.go | 0 pkg/siteacc/endpoints.go | 322 ++++++++++++++++++ .../http/services => pkg}/siteacc/manager.go | 78 ++--- .../services => pkg}/siteacc/panel/panel.go | 15 +- .../siteacc/panel/template.go | 0 .../siteacc/registration/form.go | 9 +- .../siteacc/registration/template.go | 0 pkg/siteacc/siteacc.go | 101 ++++++ .../siteacc/sitereg/sitereg.go | 0 18 files changed, 501 insertions(+), 372 deletions(-) rename {internal/http/services => pkg}/siteacc/config/config.go (100%) rename {internal/http/services => pkg}/siteacc/config/endpoints.go (100%) rename {internal/http/services => pkg}/siteacc/data/account.go (100%) rename {internal/http/services => pkg}/siteacc/data/filestorage.go (91%) rename {internal/http/services => pkg}/siteacc/data/storage.go (100%) rename {internal/http/services => pkg}/siteacc/email/email.go (84%) rename {internal/http/services => pkg}/siteacc/email/template.go (100%) create mode 100644 pkg/siteacc/endpoints.go rename {internal/http/services => pkg}/siteacc/manager.go (77%) rename {internal/http/services => pkg}/siteacc/panel/panel.go (83%) rename {internal/http/services => pkg}/siteacc/panel/template.go (100%) rename {internal/http/services => pkg}/siteacc/registration/form.go (87%) rename {internal/http/services => pkg}/siteacc/registration/template.go (100%) create mode 100644 pkg/siteacc/siteacc.go rename {internal/http/services => pkg}/siteacc/sitereg/sitereg.go (100%) diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index b53f5adf07..0a1f9e7e70 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -78,7 +78,7 @@ func parseConfig(m map[string]interface{}) (*config, error) { return c, nil } -// New creates a new Public Storage Provider service. +// New creates a new IsPublic Storage Provider service. func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { c, err := parseConfig(m) if err != nil { diff --git a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go index 7b3b0d21df..6243445fa7 100644 --- a/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go +++ b/internal/http/services/owncloud/ocs/handlers/cloud/capabilities/capabilities.go @@ -128,7 +128,7 @@ func (h *Handler) Init(c *config.Config) { h.c.Capabilities.FilesSharing.Public = &data.CapabilitiesFilesSharingPublic{} } - // h.c.Capabilities.FilesSharing.Public.Enabled is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Enabled is boolean h.c.Capabilities.FilesSharing.Public.Enabled = true if h.c.Capabilities.FilesSharing.Public.Password == nil { @@ -139,22 +139,22 @@ func (h *Handler) Init(c *config.Config) { h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor = &data.CapabilitiesFilesSharingPublicPasswordEnforcedFor{} } - // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.ReadOnly is boolean - // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.ReadWrite is boolean - // h.c.Capabilities.FilesSharing.Public.Password.EnforcedFor.UploadOnly is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadOnly is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.ReadWrite is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Password.EnforcedFor.UploadOnly is boolean - // h.c.Capabilities.FilesSharing.Public.Password.Enforced is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Password.Enforced is boolean if h.c.Capabilities.FilesSharing.Public.ExpireDate == nil { h.c.Capabilities.FilesSharing.Public.ExpireDate = &data.CapabilitiesFilesSharingPublicExpireDate{} } - // h.c.Capabilities.FilesSharing.Public.ExpireDate.Enabled is boolean + // h.c.Capabilities.FilesSharing.IsPublic.ExpireDate.Enabled is boolean - // h.c.Capabilities.FilesSharing.Public.SendMail is boolean - // h.c.Capabilities.FilesSharing.Public.SocialShare is boolean - // h.c.Capabilities.FilesSharing.Public.Upload is boolean - // h.c.Capabilities.FilesSharing.Public.Multiple is boolean - // h.c.Capabilities.FilesSharing.Public.SupportsUploadOnly is boolean + // h.c.Capabilities.FilesSharing.IsPublic.SendMail is boolean + // h.c.Capabilities.FilesSharing.IsPublic.SocialShare is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Upload is boolean + // h.c.Capabilities.FilesSharing.IsPublic.Multiple is boolean + // h.c.Capabilities.FilesSharing.IsPublic.SupportsUploadOnly is boolean if h.c.Capabilities.FilesSharing.User == nil { h.c.Capabilities.FilesSharing.User = &data.CapabilitiesFilesSharingUser{} diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go index e3f5765d39..a57c92cd98 100644 --- a/internal/http/services/siteacc/siteacc.go +++ b/internal/http/services/siteacc/siteacc.go @@ -19,20 +19,14 @@ package siteacc import ( - "encoding/json" - "fmt" - "io/ioutil" "net/http" - "net/url" - "strings" + "github.com/cs3org/reva/pkg/siteacc" + "github.com/cs3org/reva/pkg/siteacc/config" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/cs3org/reva/internal/http/services/siteacc/config" - "github.com/cs3org/reva/internal/http/services/siteacc/data" - "github.com/cs3org/reva/pkg/mentix/key" "github.com/cs3org/reva/pkg/rhttp/global" ) @@ -44,7 +38,7 @@ type svc struct { conf *config.Configuration log *zerolog.Logger - manager *Manager + siteacc *siteacc.SiteAccounts } const ( @@ -63,297 +57,12 @@ func (s *svc) Prefix() string { // Unprotected returns all endpoints that can be queried without prior authorization. func (s *svc) Unprotected() []string { - // The account creation endpoint is always unprotected - endpoints := []string{config.EndpointCreate} - - // If enabled, the registration registrationForm endpoint is also unprotected - if s.conf.EnableRegistrationForm { - endpoints = append(endpoints, config.EndpointRegistration) - } - - return endpoints + return s.siteacc.GetPublicEndpoints() } // Handler serves all HTTP requests. func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer r.Body.Close() - - switch r.URL.Path { - case config.EndpointPanel: - s.handlePanelEndpoint(w, r) - - case config.EndpointRegistration: - if s.conf.EnableRegistrationForm { - s.handleRegistrationEndpoint(w, r) - } - - default: - s.handleRequestEndpoints(w, r) - } - }) -} - -func (s *svc) handlePanelEndpoint(w http.ResponseWriter, r *http.Request) { - if err := s.manager.ShowPanel(w); err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface panel: %v", err))) - } -} - -func (s *svc) handleRegistrationEndpoint(w http.ResponseWriter, r *http.Request) { - if err := s.manager.ShowRegistrationForm(w); err != nil { - w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface registration registrationForm: %v", err))) - } -} - -func (s *svc) handleRequestEndpoints(w http.ResponseWriter, r *http.Request) { - // Allow definition of endpoints in a flexible and easy way - type Endpoint struct { - Path string - Method string - Handler func(url.Values, []byte) (interface{}, error) - } - - // Every request to the accounts service results in a standardized JSON response - type Response struct { - Success bool `json:"success"` - Error string `json:"error,omitempty"` - Data interface{} `json:"data,omitempty"` - } - - endpoints := []Endpoint{ - {config.EndpointGenerateAPIKey, http.MethodGet, s.handleGenerateAPIKey}, - {config.EndpointVerifyAPIKey, http.MethodGet, s.handleVerifyAPIKey}, - {config.EndpointAssignAPIKey, http.MethodPost, s.handleAssignAPIKey}, - {config.EndpointList, http.MethodGet, s.handleList}, - {config.EndpointFind, http.MethodGet, s.handleFind}, - {config.EndpointCreate, http.MethodPost, s.handleCreate}, - {config.EndpointUpdate, http.MethodPost, s.handleUpdate}, - {config.EndpointRemove, http.MethodPost, s.handleRemove}, - {config.EndpointAuthorize, http.MethodPost, s.handleAuthorize}, - {config.EndpointIsAuthorized, http.MethodGet, s.handleIsAuthorized}, - {config.EndpointUnregisterSite, http.MethodPost, s.handleUnregisterSite}, - } - - // The default response is an unknown endpoint (for the specified method) - resp := Response{ - Success: false, - Error: fmt.Sprintf("unknown endpoint %v for method %v", r.URL.Path, r.Method), - Data: nil, - } - - // Check each endpoint if it can handle the request - for _, endpoint := range endpoints { - if r.URL.Path == endpoint.Path && r.Method == endpoint.Method { - body, _ := ioutil.ReadAll(r.Body) - - if data, err := endpoint.Handler(r.URL.Query(), body); err == nil { - resp.Success = true - resp.Error = "" - resp.Data = data - } else { - resp.Success = false - resp.Error = fmt.Sprintf("%v", err) - resp.Data = nil - } - - break - } - } - - // Any failure during query handling results in a bad request - if !resp.Success { - w.WriteHeader(http.StatusBadRequest) - } - - jsonData, _ := json.MarshalIndent(&resp, "", "\t") - _, _ = w.Write(jsonData) -} - -func (s *svc) handleGenerateAPIKey(values url.Values, body []byte) (interface{}, error) { - email := values.Get("email") - flags := key.FlagDefault - - if strings.EqualFold(values.Get("isScienceMesh"), "true") { - flags |= key.FlagScienceMesh - } - - if len(email) == 0 { - return nil, errors.Errorf("no email provided") - } - - apiKey, err := key.GenerateAPIKey(key.SaltFromEmail(email), flags) - if err != nil { - return nil, errors.Wrap(err, "unable to generate API key") - } - return map[string]string{"apiKey": apiKey}, nil -} - -func (s *svc) handleVerifyAPIKey(values url.Values, body []byte) (interface{}, error) { - apiKey := values.Get("apiKey") - email := values.Get("email") - - if len(apiKey) == 0 { - return nil, errors.Errorf("no API key provided") - } - - if len(email) == 0 { - return nil, errors.Errorf("no email provided") - } - - err := key.VerifyAPIKey(apiKey, key.SaltFromEmail(email)) - if err != nil { - return nil, errors.Wrap(err, "invalid API key") - } - return nil, nil -} - -func (s *svc) handleAssignAPIKey(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - flags := key.FlagDefault - if _, ok := values["isScienceMesh"]; ok { - flags |= key.FlagScienceMesh - } - - // Assign a new API key to the account through the account manager - if err := s.manager.AssignAPIKeyToAccount(account, flags); err != nil { - return nil, errors.Wrap(err, "unable to assign API key") - } - - return nil, nil -} - -func (s *svc) handleList(values url.Values, body []byte) (interface{}, error) { - return s.manager.CloneAccounts(), nil -} - -func (s *svc) handleFind(values url.Values, body []byte) (interface{}, error) { - account, err := s.findAccount(values.Get("by"), values.Get("value")) - if err != nil { - return nil, err - } - return map[string]interface{}{"account": account}, nil -} - -func (s *svc) handleCreate(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - // Create a new account through the account manager - if err := s.manager.CreateAccount(account); err != nil { - return nil, errors.Wrap(err, "unable to create account") - } - - return nil, nil -} - -func (s *svc) handleUpdate(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - // Update the account through the account manager; only the basic data of an account can be updated through this endpoint - if err := s.manager.UpdateAccount(account, false); err != nil { - return nil, errors.Wrap(err, "unable to update account") - } - - return nil, nil -} - -func (s *svc) handleRemove(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - // Remove the account through the account manager - if err := s.manager.RemoveAccount(account); err != nil { - return nil, errors.Wrap(err, "unable to remove account") - } - - return nil, nil -} - -func (s *svc) handleIsAuthorized(values url.Values, body []byte) (interface{}, error) { - account, err := s.findAccount(values.Get("by"), values.Get("value")) - if err != nil { - return nil, err - } - return account.Data.Authorized, nil -} - -func (s *svc) handleUnregisterSite(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - // Unregister the account's site through the account manager - if err := s.manager.UnregisterAccountSite(account); err != nil { - return nil, errors.Wrap(err, "unable to unregister the site of the given account") - } - - return nil, nil -} - -func (s *svc) handleAuthorize(values url.Values, body []byte) (interface{}, error) { - account, err := s.unmarshalRequestData(body) - if err != nil { - return nil, err - } - - if val := values.Get("status"); len(val) > 0 { - var authorize bool - switch strings.ToLower(val) { - case "true": - authorize = true - - case "false": - authorize = false - - default: - return nil, errors.Errorf("unsupported authorization status %v", val[0]) - } - - // Authorize the account through the account manager - if err := s.manager.AuthorizeAccount(account, authorize); err != nil { - return nil, errors.Wrap(err, "unable to (un)authorize account") - } - } else { - return nil, errors.Errorf("no authorization status provided") - } - - return nil, nil -} - -func (s *svc) unmarshalRequestData(body []byte) (*data.Account, error) { - account := &data.Account{} - if err := json.Unmarshal(body, account); err != nil { - return nil, errors.Wrap(err, "invalid account data") - } - return account, nil -} - -func (s *svc) findAccount(by string, value string) (*data.Account, error) { - if len(by) == 0 && len(value) == 0 { - return nil, errors.Errorf("missing search criteria") - } - - // Find the account using the account manager - account, err := s.manager.FindAccount(by, value) - if err != nil { - return nil, errors.Wrap(err, "user not found") - } - return account, nil + return s.siteacc.RequestHandler() } func parseConfig(m map[string]interface{}) (*config.Configuration, error) { @@ -383,8 +92,8 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) return nil, err } - // Create the accounts manager instance - mngr, err := newManager(conf, log) + // Create the site accounts instance + siteacc, err := siteacc.New(conf, log) if err != nil { return nil, errors.Wrap(err, "error creating the site accounts service") } @@ -393,7 +102,7 @@ func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) s := &svc{ conf: conf, log: log, - manager: mngr, + siteacc: siteacc, } return s, nil } diff --git a/internal/http/services/siteacc/config/config.go b/pkg/siteacc/config/config.go similarity index 100% rename from internal/http/services/siteacc/config/config.go rename to pkg/siteacc/config/config.go diff --git a/internal/http/services/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go similarity index 100% rename from internal/http/services/siteacc/config/endpoints.go rename to pkg/siteacc/config/endpoints.go diff --git a/internal/http/services/siteacc/data/account.go b/pkg/siteacc/data/account.go similarity index 100% rename from internal/http/services/siteacc/data/account.go rename to pkg/siteacc/data/account.go diff --git a/internal/http/services/siteacc/data/filestorage.go b/pkg/siteacc/data/filestorage.go similarity index 91% rename from internal/http/services/siteacc/data/filestorage.go rename to pkg/siteacc/data/filestorage.go index 22efb99b3e..dbc788868c 100644 --- a/internal/http/services/siteacc/data/filestorage.go +++ b/pkg/siteacc/data/filestorage.go @@ -24,23 +24,22 @@ import ( "os" "path/filepath" + config2 "github.com/cs3org/reva/pkg/siteacc/config" "github.com/pkg/errors" "github.com/rs/zerolog" - - "github.com/cs3org/reva/internal/http/services/siteacc/config" ) // FileStorage implements a filePath-based storage. type FileStorage struct { Storage - conf *config.Configuration + conf *config2.Configuration log *zerolog.Logger filePath string } -func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (storage *FileStorage) initialize(conf *config2.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -107,7 +106,7 @@ func (storage *FileStorage) AccountRemoved(account *Account) { } // NewFileStorage creates a new filePath storage. -func NewFileStorage(conf *config.Configuration, log *zerolog.Logger) (*FileStorage, error) { +func NewFileStorage(conf *config2.Configuration, log *zerolog.Logger) (*FileStorage, error) { storage := &FileStorage{} if err := storage.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the filePath storage") diff --git a/internal/http/services/siteacc/data/storage.go b/pkg/siteacc/data/storage.go similarity index 100% rename from internal/http/services/siteacc/data/storage.go rename to pkg/siteacc/data/storage.go diff --git a/internal/http/services/siteacc/email/email.go b/pkg/siteacc/email/email.go similarity index 84% rename from internal/http/services/siteacc/email/email.go rename to pkg/siteacc/email/email.go index 72e15f3c66..1756cdf1b5 100644 --- a/internal/http/services/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -22,24 +22,24 @@ import ( "bytes" "text/template" + data2 "github.com/cs3org/reva/pkg/siteacc/data" "github.com/pkg/errors" - "github.com/cs3org/reva/internal/http/services/siteacc/data" "github.com/cs3org/reva/pkg/smtpclient" ) // SendAccountCreated sends an email about account creation. -func SendAccountCreated(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAccountCreated(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, account, smtp) } // SendAPIKeyAssigned sends an email about API key assignment. -func SendAPIKeyAssigned(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAPIKeyAssigned(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, account, smtp) } // SendAccountAuthorized sends an email about account authorization. -func SendAccountAuthorized(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAccountAuthorized(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, account, smtp) } diff --git a/internal/http/services/siteacc/email/template.go b/pkg/siteacc/email/template.go similarity index 100% rename from internal/http/services/siteacc/email/template.go rename to pkg/siteacc/email/template.go diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go new file mode 100644 index 0000000000..0979be02d5 --- /dev/null +++ b/pkg/siteacc/endpoints.go @@ -0,0 +1,322 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package siteacc + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "github.com/cs3org/reva/pkg/mentix/key" + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/pkg/errors" +) + +type methodCallback = func(*Manager, url.Values, []byte) (interface{}, error) + +type endpoint struct { + Path string + Handler func(*Manager, endpoint, http.ResponseWriter, *http.Request) + MethodCallbacks map[string]methodCallback + IsPublic bool +} + +func createMethodCallbacks(cbGet methodCallback, cbPost methodCallback) map[string]methodCallback { + callbacks := make(map[string]methodCallback) + + if cbGet != nil { + callbacks[http.MethodGet] = cbGet + } + + if cbPost != nil { + callbacks[http.MethodPost] = cbPost + } + + return callbacks +} + +func getEndpoints(enableRegistrationForm bool) []endpoint { + endpoints := []endpoint{ + // Form endpoints + {config.EndpointPanel, handlePanelEndpoint, nil, false}, + // Request endpoints + {config.EndpointGenerateAPIKey, handleMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, + {config.EndpointVerifyAPIKey, handleMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, + {config.EndpointAssignAPIKey, handleMethodEndpoint, createMethodCallbacks(nil, handleAssignAPIKey), false}, + {config.EndpointList, handleMethodEndpoint, createMethodCallbacks(handleList, nil), false}, + {config.EndpointFind, handleMethodEndpoint, createMethodCallbacks(handleFind, nil), false}, + {config.EndpointCreate, handleMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, + {config.EndpointUpdate, handleMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, + {config.EndpointRemove, handleMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, + {config.EndpointAuthorize, handleMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, + {config.EndpointIsAuthorized, handleMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, + {config.EndpointUnregisterSite, handleMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, + } + + if enableRegistrationForm { + endpoints = append(endpoints, endpoint{config.EndpointRegistration, handleRegistrationEndpoint, nil, true}) + } + + return endpoints +} + +func handlePanelEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { + if err := mngr.ShowPanel(w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface panel: %v", err))) + } +} + +func handleRegistrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { + if err := mngr.ShowRegistrationForm(w); err != nil { + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface registration registrationForm: %v", err))) + } +} + +func handleMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { + // Every request to the accounts service results in a standardized JSON response + type Response struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Data interface{} `json:"data,omitempty"` + } + + // The default response is an unknown requestHandler (for the specified method) + resp := Response{ + Success: false, + Error: fmt.Sprintf("unknown endpoint %v for method %v", r.URL.Path, r.Method), + Data: nil, + } + + if ep.MethodCallbacks != nil { + // Search for a matching method in the list of callbacks + for method, cb := range ep.MethodCallbacks { + if method == r.Method { + body, _ := ioutil.ReadAll(r.Body) + + if respData, err := cb(mngr, r.URL.Query(), body); err == nil { + resp.Success = true + resp.Error = "" + resp.Data = respData + } else { + resp.Success = false + resp.Error = fmt.Sprintf("%v", err) + resp.Data = nil + } + } + } + } + + // Any failure during query handling results in a bad request + if !resp.Success { + w.WriteHeader(http.StatusBadRequest) + } + + jsonData, _ := json.MarshalIndent(&resp, "", "\t") + _, _ = w.Write(jsonData) +} + +func handleGenerateAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + email := values.Get("email") + flags := key.FlagDefault + + if strings.EqualFold(values.Get("isScienceMesh"), "true") { + flags |= key.FlagScienceMesh + } + + if len(email) == 0 { + return nil, errors.Errorf("no email provided") + } + + apiKey, err := key.GenerateAPIKey(key.SaltFromEmail(email), flags) + if err != nil { + return nil, errors.Wrap(err, "unable to generate API key") + } + return map[string]string{"apiKey": apiKey}, nil +} + +func handleVerifyAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + apiKey := values.Get("apiKey") + email := values.Get("email") + + if len(apiKey) == 0 { + return nil, errors.Errorf("no API key provided") + } + + if len(email) == 0 { + return nil, errors.Errorf("no email provided") + } + + err := key.VerifyAPIKey(apiKey, key.SaltFromEmail(email)) + if err != nil { + return nil, errors.Wrap(err, "invalid API key") + } + return nil, nil +} + +func handleAssignAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + flags := key.FlagDefault + if _, ok := values["isScienceMesh"]; ok { + flags |= key.FlagScienceMesh + } + + // Assign a new API key to the account through the account manager + if err := mngr.AssignAPIKeyToAccount(account, flags); err != nil { + return nil, errors.Wrap(err, "unable to assign API key") + } + + return nil, nil +} + +func handleList(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + return mngr.CloneAccounts(), nil +} + +func handleFind(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := findAccount(mngr, values.Get("by"), values.Get("value")) + if err != nil { + return nil, err + } + return map[string]interface{}{"account": account}, nil +} + +func handleCreate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Create a new account through the account manager + if err := mngr.CreateAccount(account); err != nil { + return nil, errors.Wrap(err, "unable to create account") + } + + return nil, nil +} + +func handleUpdate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Update the account through the account manager; only the basic data of an account can be updated through this requestHandler + if err := mngr.UpdateAccount(account, false); err != nil { + return nil, errors.Wrap(err, "unable to update account") + } + + return nil, nil +} + +func handleRemove(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Remove the account through the account manager + if err := mngr.RemoveAccount(account); err != nil { + return nil, errors.Wrap(err, "unable to remove account") + } + + return nil, nil +} + +func handleIsAuthorized(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := findAccount(mngr, values.Get("by"), values.Get("value")) + if err != nil { + return nil, err + } + return account.Data.Authorized, nil +} + +func handleUnregisterSite(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Unregister the account's site through the account manager + if err := mngr.UnregisterAccountSite(account); err != nil { + return nil, errors.Wrap(err, "unable to unregister the site of the given account") + } + + return nil, nil +} + +func handleAuthorize(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + if val := values.Get("status"); len(val) > 0 { + var authorize bool + switch strings.ToLower(val) { + case "true": + authorize = true + + case "false": + authorize = false + + default: + return nil, errors.Errorf("unsupported authorization status %v", val[0]) + } + + // Authorize the account through the account manager + if err := mngr.AuthorizeAccount(account, authorize); err != nil { + return nil, errors.Wrap(err, "unable to (un)authorize account") + } + } else { + return nil, errors.Errorf("no authorization status provided") + } + + return nil, nil +} + +func unmarshalRequestData(body []byte) (*data.Account, error) { + account := &data.Account{} + if err := json.Unmarshal(body, account); err != nil { + return nil, errors.Wrap(err, "invalid account data") + } + return account, nil +} + +func findAccount(mngr *Manager, by string, value string) (*data.Account, error) { + if len(by) == 0 && len(value) == 0 { + return nil, errors.Errorf("missing search criteria") + } + + // Find the account using the account manager + account, err := mngr.FindAccount(by, value) + if err != nil { + return nil, errors.Wrap(err, "user not found") + } + return account, nil +} diff --git a/internal/http/services/siteacc/manager.go b/pkg/siteacc/manager.go similarity index 77% rename from internal/http/services/siteacc/manager.go rename to pkg/siteacc/manager.go index 23916bd117..3774d2b9f8 100644 --- a/internal/http/services/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -26,15 +26,15 @@ import ( "sync" "time" + config2 "github.com/cs3org/reva/pkg/siteacc/config" + data2 "github.com/cs3org/reva/pkg/siteacc/data" + email2 "github.com/cs3org/reva/pkg/siteacc/email" + panel2 "github.com/cs3org/reva/pkg/siteacc/panel" + registration2 "github.com/cs3org/reva/pkg/siteacc/registration" + sitereg2 "github.com/cs3org/reva/pkg/siteacc/sitereg" "github.com/pkg/errors" "github.com/rs/zerolog" - "github.com/cs3org/reva/internal/http/services/siteacc/config" - "github.com/cs3org/reva/internal/http/services/siteacc/data" - "github.com/cs3org/reva/internal/http/services/siteacc/email" - "github.com/cs3org/reva/internal/http/services/siteacc/panel" - "github.com/cs3org/reva/internal/http/services/siteacc/registration" - "github.com/cs3org/reva/internal/http/services/siteacc/sitereg" "github.com/cs3org/reva/pkg/mentix/key" "github.com/cs3org/reva/pkg/smtpclient" ) @@ -50,20 +50,20 @@ const ( // Manager is responsible for all site account related tasks. type Manager struct { - conf *config.Configuration + conf *config2.Configuration log *zerolog.Logger - accounts data.Accounts - storage data.Storage + accounts data2.Accounts + storage data2.Storage - panel *panel.Panel - registrationForm *registration.Form + panel *panel2.Panel + registrationForm *registration2.Form smtp *smtpclient.SMTPCredentials mutex sync.RWMutex } -func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (mngr *Manager) initialize(conf *config2.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -74,7 +74,7 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) } mngr.log = log - mngr.accounts = make(data.Accounts, 0, 32) // Reserve some space for accounts + mngr.accounts = make(data2.Accounts, 0, 32) // Reserve some space for accounts // Create the site accounts storage and read all stored data if storage, err := mngr.createStorage(conf.Storage.Driver); err == nil { @@ -85,14 +85,14 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) } // Create the web interface panel - if pnl, err := panel.NewPanel(conf, log); err == nil { + if pnl, err := panel2.NewPanel(conf, log); err == nil { mngr.panel = pnl } else { return errors.Wrap(err, "unable to create panel") } // Create the web interface registrationForm - if frm, err := registration.NewForm(conf, log); err == nil { + if frm, err := registration2.NewForm(conf, log); err == nil { mngr.registrationForm = frm } else { return errors.Wrap(err, "unable to create registrationForm") @@ -106,9 +106,9 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) return nil } -func (mngr *Manager) createStorage(driver string) (data.Storage, error) { +func (mngr *Manager) createStorage(driver string) (data2.Storage, error) { if driver == "file" { - return data.NewFileStorage(mngr.conf, mngr.log) + return data2.NewFileStorage(mngr.conf, mngr.log) } return nil, errors.Errorf("unknown storage driver %v", driver) @@ -130,21 +130,21 @@ func (mngr *Manager) writeAllAccounts() { } } -func (mngr *Manager) findAccount(by string, value string) (*data.Account, error) { +func (mngr *Manager) findAccount(by string, value string) (*data2.Account, error) { if len(value) == 0 { return nil, errors.Errorf("no search value specified") } - var account *data.Account + var account *data2.Account switch strings.ToLower(by) { case FindByEmail: - account = mngr.findAccountByPredicate(func(account *data.Account) bool { return strings.EqualFold(account.Email, value) }) + account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return strings.EqualFold(account.Email, value) }) case FindByAPIKey: - account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.Data.APIKey == value }) + account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return account.Data.APIKey == value }) case FindBySiteID: - account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.GetSiteID() == value }) + account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return account.GetSiteID() == value }) default: return nil, errors.Errorf("invalid search type %v", by) @@ -157,7 +157,7 @@ func (mngr *Manager) findAccount(by string, value string) (*data.Account, error) return nil, errors.Errorf("no user found matching the specified criteria") } -func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account { +func (mngr *Manager) findAccountByPredicate(predicate func(*data2.Account) bool) *data2.Account { for _, account := range mngr.accounts { if predicate(account) { return account @@ -179,7 +179,7 @@ func (mngr *Manager) ShowRegistrationForm(w http.ResponseWriter) error { } // CreateAccount creates a new account; if an account with the same email address already exists, an error is returned. -func (mngr *Manager) CreateAccount(accountData *data.Account) error { +func (mngr *Manager) CreateAccount(accountData *data2.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -188,12 +188,12 @@ func (mngr *Manager) CreateAccount(accountData *data.Account) error { return errors.Errorf("an account with the specified email address already exists") } - if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName); err == nil { + if account, err := data2.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName); err == nil { mngr.accounts = append(mngr.accounts, account) mngr.storage.AccountAdded(account) mngr.writeAllAccounts() - _ = email.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email2.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) } else { return errors.Wrap(err, "error while creating account") } @@ -202,7 +202,7 @@ func (mngr *Manager) CreateAccount(accountData *data.Account) error { } // UpdateAccount updates the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) error { +func (mngr *Manager) UpdateAccount(accountData *data2.Account, copyData bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -224,7 +224,7 @@ func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) err } // FindAccount is used to find an account by various criteria. -func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) { +func (mngr *Manager) FindAccount(by string, value string) (*data2.Account, error) { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -239,7 +239,7 @@ func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) } // AuthorizeAccount sets the authorization status of the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool) error { +func (mngr *Manager) AuthorizeAccount(accountData *data2.Account, authorized bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -255,14 +255,14 @@ func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool mngr.writeAllAccounts() if account.Data.Authorized && account.Data.Authorized != authorizedOld { - _ = email.SendAccountAuthorized(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email2.SendAccountAuthorized(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) } return nil } // AssignAPIKeyToAccount is used to assign a new API key to the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int) error { +func (mngr *Manager) AssignAPIKeyToAccount(accountData *data2.Account, flags int) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -293,13 +293,13 @@ func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int) mngr.storage.AccountUpdated(account) mngr.writeAllAccounts() - _ = email.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email2.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) return nil } // UnregisterAccountSite unregisters the site associated with the given account. -func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { +func (mngr *Manager) UnregisterAccountSite(accountData *data2.Account) error { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -314,7 +314,7 @@ func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { return errors.Wrap(err, "unable to get site ID") } - if err := sitereg.UnregisterSite(mngr.conf.SiteRegistration.URL, account.Data.APIKey, siteID, salt); err != nil { + if err := sitereg2.UnregisterSite(mngr.conf.SiteRegistration.URL, account.Data.APIKey, siteID, salt); err != nil { return errors.Wrap(err, "error while unregistering the site") } @@ -322,7 +322,7 @@ func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { } // RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) RemoveAccount(accountData *data.Account) error { +func (mngr *Manager) RemoveAccount(accountData *data2.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -339,11 +339,11 @@ func (mngr *Manager) RemoveAccount(accountData *data.Account) error { } // CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. -func (mngr *Manager) CloneAccounts() data.Accounts { +func (mngr *Manager) CloneAccounts() data2.Accounts { mngr.mutex.RLock() defer mngr.mutex.RUnlock() - clone := make(data.Accounts, 0) + clone := make(data2.Accounts, 0) // To avoid any "deep copy" packages, use gob en- and decoding instead var buf bytes.Buffer @@ -353,14 +353,14 @@ func (mngr *Manager) CloneAccounts() data.Accounts { if err := enc.Encode(mngr.accounts); err == nil { if err := dec.Decode(&clone); err != nil { // In case of an error, create an empty data set - clone = make(data.Accounts, 0) + clone = make(data2.Accounts, 0) } } return clone } -func newManager(conf *config.Configuration, log *zerolog.Logger) (*Manager, error) { +func newManager(conf *config2.Configuration, log *zerolog.Logger) (*Manager, error) { mngr := &Manager{} if err := mngr.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the accounts manager") diff --git a/internal/http/services/siteacc/panel/panel.go b/pkg/siteacc/panel/panel.go similarity index 83% rename from internal/http/services/siteacc/panel/panel.go rename to pkg/siteacc/panel/panel.go index ce8d6cd916..539e54010b 100644 --- a/internal/http/services/siteacc/panel/panel.go +++ b/pkg/siteacc/panel/panel.go @@ -22,22 +22,21 @@ import ( "html/template" "net/http" + config2 "github.com/cs3org/reva/pkg/siteacc/config" + data2 "github.com/cs3org/reva/pkg/siteacc/data" "github.com/pkg/errors" "github.com/rs/zerolog" - - "github.com/cs3org/reva/internal/http/services/siteacc/config" - "github.com/cs3org/reva/internal/http/services/siteacc/data" ) // Panel represents the web interface panel of the accounts service. type Panel struct { - conf *config.Configuration + conf *config2.Configuration log *zerolog.Logger tpl *template.Template } -func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (panel *Panel) initialize(conf *config2.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -58,9 +57,9 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { +func (panel *Panel) Execute(w http.ResponseWriter, accounts *data2.Accounts) error { type TemplateData struct { - Accounts *data.Accounts + Accounts *data2.Accounts } tplData := TemplateData{ @@ -71,7 +70,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) erro } // NewPanel creates a new web interface panel. -func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { +func NewPanel(conf *config2.Configuration, log *zerolog.Logger) (*Panel, error) { panel := &Panel{} if err := panel.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the panel") diff --git a/internal/http/services/siteacc/panel/template.go b/pkg/siteacc/panel/template.go similarity index 100% rename from internal/http/services/siteacc/panel/template.go rename to pkg/siteacc/panel/template.go diff --git a/internal/http/services/siteacc/registration/form.go b/pkg/siteacc/registration/form.go similarity index 87% rename from internal/http/services/siteacc/registration/form.go rename to pkg/siteacc/registration/form.go index e7649e891d..e6447b90f4 100644 --- a/internal/http/services/siteacc/registration/form.go +++ b/pkg/siteacc/registration/form.go @@ -22,21 +22,20 @@ import ( "html/template" "net/http" + config2 "github.com/cs3org/reva/pkg/siteacc/config" "github.com/pkg/errors" "github.com/rs/zerolog" - - "github.com/cs3org/reva/internal/http/services/siteacc/config" ) // Form represents the web interface form for user account registration. type Form struct { - conf *config.Configuration + conf *config2.Configuration log *zerolog.Logger tpl *template.Template } -func (form *Form) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (form *Form) initialize(conf *config2.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -67,7 +66,7 @@ func (form *Form) Execute(w http.ResponseWriter) error { } // NewForm creates a new web interface form. -func NewForm(conf *config.Configuration, log *zerolog.Logger) (*Form, error) { +func NewForm(conf *config2.Configuration, log *zerolog.Logger) (*Form, error) { form := &Form{} if err := form.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the form") diff --git a/internal/http/services/siteacc/registration/template.go b/pkg/siteacc/registration/template.go similarity index 100% rename from internal/http/services/siteacc/registration/template.go rename to pkg/siteacc/registration/template.go diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go new file mode 100644 index 0000000000..5041498358 --- /dev/null +++ b/pkg/siteacc/siteacc.go @@ -0,0 +1,101 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package siteacc + +import ( + "fmt" + "net/http" + + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// SiteAccounts represents the main Site Accounts service object. +type SiteAccounts struct { + conf *config.Configuration + log *zerolog.Logger + + manager *Manager +} + +func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog.Logger) error { + if conf == nil { + return fmt.Errorf("no configuration provided") + } + siteacc.conf = conf + + if log == nil { + return fmt.Errorf("no logger provided") + } + siteacc.log = log + + // Create the accounts manager instance + mngr, err := newManager(conf, log) + if err != nil { + return errors.Wrap(err, "error creating the site accounts manager") + } + siteacc.manager = mngr + + return nil +} + +// RequestHandler returns the HTTP request handler of the service. +func (siteacc *SiteAccounts) RequestHandler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + defer r.Body.Close() + + epHandled := false + for _, ep := range getEndpoints(siteacc.conf.EnableRegistrationForm) { + if ep.Path == r.URL.Path { + ep.Handler(siteacc.manager, ep, w, r) + epHandled = true + break + } + } + + if !epHandled { + w.WriteHeader(http.StatusBadRequest) + _, _ = w.Write([]byte(fmt.Sprintf("Unknown endpoint %v", r.URL.Path))) + } + }) +} + +func (siteacc *SiteAccounts) GetPublicEndpoints() []string { + // TODO: REMOVE! + return []string{"/"} + + endpoints := make([]string, 0, 5) + for _, ep := range getEndpoints(siteacc.conf.EnableRegistrationForm) { + if ep.IsPublic { + endpoints = append(endpoints, ep.Path) + } + } + return endpoints +} + +// New returns a new Site Accounts service instance. +func New(conf *config.Configuration, log *zerolog.Logger) (*SiteAccounts, error) { + // Configure the accounts service + siteacc := new(SiteAccounts) + if err := siteacc.initialize(conf, log); err != nil { + return nil, fmt.Errorf("unable to initialize SiteAccounts: %v", err) + } + return siteacc, nil +} diff --git a/internal/http/services/siteacc/sitereg/sitereg.go b/pkg/siteacc/sitereg/sitereg.go similarity index 100% rename from internal/http/services/siteacc/sitereg/sitereg.go rename to pkg/siteacc/sitereg/sitereg.go From 68639b0db21be505560fe3c711260425d0e03fcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 30 Jun 2021 16:05:14 +0200 Subject: [PATCH 03/60] Add some more fields to user accounts --- pkg/siteacc/data/account.go | 28 ++++++++-- pkg/siteacc/data/filestorage.go | 8 +-- pkg/siteacc/email/email.go | 8 +-- pkg/siteacc/manager.go | 78 ++++++++++++++-------------- pkg/siteacc/panel/panel.go | 14 ++--- pkg/siteacc/panel/template.go | 13 ++++- pkg/siteacc/registration/form.go | 8 +-- pkg/siteacc/registration/template.go | 22 ++++++-- 8 files changed, 112 insertions(+), 67 deletions(-) diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 914d5b7e12..8f24fc06ea 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -29,9 +29,19 @@ import ( // Account represents a single site account. type Account struct { - Email string `json:"email"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + Organization string `json:"organization"` + Website string `json:"website"` + PhoneNumber string `json:"phoneNumber"` + + /* + Password struct { + Hash string `json:"hash"` + Salt string `json:"salt"` + } `json:"password"` + */ DateCreated time.Time `json:"dateCreated"` DateModified time.Time `json:"dateModified"` @@ -66,6 +76,9 @@ func (acc *Account) Copy(other *Account, copyData bool) error { // Manually update fields acc.FirstName = other.FirstName acc.LastName = other.LastName + acc.Organization = other.Organization + acc.Website = other.Website + acc.PhoneNumber = other.PhoneNumber if copyData { acc.Data = other.Data @@ -85,17 +98,24 @@ func (acc *Account) verify() error { return errors.Errorf("no or incomplete name provided") } + if acc.Organization == "" { + return errors.Errorf("no organization provided") + } + return nil } // NewAccount creates a new site account. -func NewAccount(email string, firstName, lastName string) (*Account, error) { +func NewAccount(email string, firstName, lastName string, organization, website string, phoneNumber string) (*Account, error) { t := time.Now() acc := &Account{ Email: email, FirstName: firstName, LastName: lastName, + Organization: organization, + Website: website, + PhoneNumber: phoneNumber, DateCreated: t, DateModified: t, Data: AccountData{ diff --git a/pkg/siteacc/data/filestorage.go b/pkg/siteacc/data/filestorage.go index dbc788868c..e7413ac5f6 100644 --- a/pkg/siteacc/data/filestorage.go +++ b/pkg/siteacc/data/filestorage.go @@ -24,7 +24,7 @@ import ( "os" "path/filepath" - config2 "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/config" "github.com/pkg/errors" "github.com/rs/zerolog" ) @@ -33,13 +33,13 @@ import ( type FileStorage struct { Storage - conf *config2.Configuration + conf *config.Configuration log *zerolog.Logger filePath string } -func (storage *FileStorage) initialize(conf *config2.Configuration, log *zerolog.Logger) error { +func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -106,7 +106,7 @@ func (storage *FileStorage) AccountRemoved(account *Account) { } // NewFileStorage creates a new filePath storage. -func NewFileStorage(conf *config2.Configuration, log *zerolog.Logger) (*FileStorage, error) { +func NewFileStorage(conf *config.Configuration, log *zerolog.Logger) (*FileStorage, error) { storage := &FileStorage{} if err := storage.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the filePath storage") diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index 1756cdf1b5..1e90b72068 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -22,24 +22,24 @@ import ( "bytes" "text/template" - data2 "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/cs3org/reva/pkg/siteacc/data" "github.com/pkg/errors" "github.com/cs3org/reva/pkg/smtpclient" ) // SendAccountCreated sends an email about account creation. -func SendAccountCreated(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAccountCreated(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, account, smtp) } // SendAPIKeyAssigned sends an email about API key assignment. -func SendAPIKeyAssigned(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAPIKeyAssigned(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, account, smtp) } // SendAccountAuthorized sends an email about account authorization. -func SendAccountAuthorized(account *data2.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { +func SendAccountAuthorized(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, account, smtp) } diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index 3774d2b9f8..91bbf92b1f 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -26,12 +26,12 @@ import ( "sync" "time" - config2 "github.com/cs3org/reva/pkg/siteacc/config" - data2 "github.com/cs3org/reva/pkg/siteacc/data" - email2 "github.com/cs3org/reva/pkg/siteacc/email" - panel2 "github.com/cs3org/reva/pkg/siteacc/panel" - registration2 "github.com/cs3org/reva/pkg/siteacc/registration" - sitereg2 "github.com/cs3org/reva/pkg/siteacc/sitereg" + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/cs3org/reva/pkg/siteacc/email" + "github.com/cs3org/reva/pkg/siteacc/panel" + "github.com/cs3org/reva/pkg/siteacc/registration" + "github.com/cs3org/reva/pkg/siteacc/sitereg" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -50,20 +50,20 @@ const ( // Manager is responsible for all site account related tasks. type Manager struct { - conf *config2.Configuration + conf *config.Configuration log *zerolog.Logger - accounts data2.Accounts - storage data2.Storage + accounts data.Accounts + storage data.Storage - panel *panel2.Panel - registrationForm *registration2.Form + panel *panel.Panel + registrationForm *registration.Form smtp *smtpclient.SMTPCredentials mutex sync.RWMutex } -func (mngr *Manager) initialize(conf *config2.Configuration, log *zerolog.Logger) error { +func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -74,7 +74,7 @@ func (mngr *Manager) initialize(conf *config2.Configuration, log *zerolog.Logger } mngr.log = log - mngr.accounts = make(data2.Accounts, 0, 32) // Reserve some space for accounts + mngr.accounts = make(data.Accounts, 0, 32) // Reserve some space for accounts // Create the site accounts storage and read all stored data if storage, err := mngr.createStorage(conf.Storage.Driver); err == nil { @@ -85,14 +85,14 @@ func (mngr *Manager) initialize(conf *config2.Configuration, log *zerolog.Logger } // Create the web interface panel - if pnl, err := panel2.NewPanel(conf, log); err == nil { + if pnl, err := panel.NewPanel(conf, log); err == nil { mngr.panel = pnl } else { return errors.Wrap(err, "unable to create panel") } // Create the web interface registrationForm - if frm, err := registration2.NewForm(conf, log); err == nil { + if frm, err := registration.NewForm(conf, log); err == nil { mngr.registrationForm = frm } else { return errors.Wrap(err, "unable to create registrationForm") @@ -106,9 +106,9 @@ func (mngr *Manager) initialize(conf *config2.Configuration, log *zerolog.Logger return nil } -func (mngr *Manager) createStorage(driver string) (data2.Storage, error) { +func (mngr *Manager) createStorage(driver string) (data.Storage, error) { if driver == "file" { - return data2.NewFileStorage(mngr.conf, mngr.log) + return data.NewFileStorage(mngr.conf, mngr.log) } return nil, errors.Errorf("unknown storage driver %v", driver) @@ -130,21 +130,21 @@ func (mngr *Manager) writeAllAccounts() { } } -func (mngr *Manager) findAccount(by string, value string) (*data2.Account, error) { +func (mngr *Manager) findAccount(by string, value string) (*data.Account, error) { if len(value) == 0 { return nil, errors.Errorf("no search value specified") } - var account *data2.Account + var account *data.Account switch strings.ToLower(by) { case FindByEmail: - account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return strings.EqualFold(account.Email, value) }) + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return strings.EqualFold(account.Email, value) }) case FindByAPIKey: - account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return account.Data.APIKey == value }) + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.Data.APIKey == value }) case FindBySiteID: - account = mngr.findAccountByPredicate(func(account *data2.Account) bool { return account.GetSiteID() == value }) + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.GetSiteID() == value }) default: return nil, errors.Errorf("invalid search type %v", by) @@ -157,7 +157,7 @@ func (mngr *Manager) findAccount(by string, value string) (*data2.Account, error return nil, errors.Errorf("no user found matching the specified criteria") } -func (mngr *Manager) findAccountByPredicate(predicate func(*data2.Account) bool) *data2.Account { +func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account { for _, account := range mngr.accounts { if predicate(account) { return account @@ -179,7 +179,7 @@ func (mngr *Manager) ShowRegistrationForm(w http.ResponseWriter) error { } // CreateAccount creates a new account; if an account with the same email address already exists, an error is returned. -func (mngr *Manager) CreateAccount(accountData *data2.Account) error { +func (mngr *Manager) CreateAccount(accountData *data.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -188,12 +188,12 @@ func (mngr *Manager) CreateAccount(accountData *data2.Account) error { return errors.Errorf("an account with the specified email address already exists") } - if account, err := data2.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName); err == nil { + if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName, accountData.Organization, accountData.Website, accountData.PhoneNumber); err == nil { mngr.accounts = append(mngr.accounts, account) mngr.storage.AccountAdded(account) mngr.writeAllAccounts() - _ = email2.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) } else { return errors.Wrap(err, "error while creating account") } @@ -202,7 +202,7 @@ func (mngr *Manager) CreateAccount(accountData *data2.Account) error { } // UpdateAccount updates the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) UpdateAccount(accountData *data2.Account, copyData bool) error { +func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -224,7 +224,7 @@ func (mngr *Manager) UpdateAccount(accountData *data2.Account, copyData bool) er } // FindAccount is used to find an account by various criteria. -func (mngr *Manager) FindAccount(by string, value string) (*data2.Account, error) { +func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -239,7 +239,7 @@ func (mngr *Manager) FindAccount(by string, value string) (*data2.Account, error } // AuthorizeAccount sets the authorization status of the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AuthorizeAccount(accountData *data2.Account, authorized bool) error { +func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -255,14 +255,14 @@ func (mngr *Manager) AuthorizeAccount(accountData *data2.Account, authorized boo mngr.writeAllAccounts() if account.Data.Authorized && account.Data.Authorized != authorizedOld { - _ = email2.SendAccountAuthorized(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email.SendAccountAuthorized(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) } return nil } // AssignAPIKeyToAccount is used to assign a new API key to the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AssignAPIKeyToAccount(accountData *data2.Account, flags int) error { +func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -293,13 +293,13 @@ func (mngr *Manager) AssignAPIKeyToAccount(accountData *data2.Account, flags int mngr.storage.AccountUpdated(account) mngr.writeAllAccounts() - _ = email2.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = email.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) return nil } // UnregisterAccountSite unregisters the site associated with the given account. -func (mngr *Manager) UnregisterAccountSite(accountData *data2.Account) error { +func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -314,7 +314,7 @@ func (mngr *Manager) UnregisterAccountSite(accountData *data2.Account) error { return errors.Wrap(err, "unable to get site ID") } - if err := sitereg2.UnregisterSite(mngr.conf.SiteRegistration.URL, account.Data.APIKey, siteID, salt); err != nil { + if err := sitereg.UnregisterSite(mngr.conf.SiteRegistration.URL, account.Data.APIKey, siteID, salt); err != nil { return errors.Wrap(err, "error while unregistering the site") } @@ -322,7 +322,7 @@ func (mngr *Manager) UnregisterAccountSite(accountData *data2.Account) error { } // RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) RemoveAccount(accountData *data2.Account) error { +func (mngr *Manager) RemoveAccount(accountData *data.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -339,11 +339,11 @@ func (mngr *Manager) RemoveAccount(accountData *data2.Account) error { } // CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. -func (mngr *Manager) CloneAccounts() data2.Accounts { +func (mngr *Manager) CloneAccounts() data.Accounts { mngr.mutex.RLock() defer mngr.mutex.RUnlock() - clone := make(data2.Accounts, 0) + clone := make(data.Accounts, 0) // To avoid any "deep copy" packages, use gob en- and decoding instead var buf bytes.Buffer @@ -353,14 +353,14 @@ func (mngr *Manager) CloneAccounts() data2.Accounts { if err := enc.Encode(mngr.accounts); err == nil { if err := dec.Decode(&clone); err != nil { // In case of an error, create an empty data set - clone = make(data2.Accounts, 0) + clone = make(data.Accounts, 0) } } return clone } -func newManager(conf *config2.Configuration, log *zerolog.Logger) (*Manager, error) { +func newManager(conf *config.Configuration, log *zerolog.Logger) (*Manager, error) { mngr := &Manager{} if err := mngr.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the accounts manager") diff --git a/pkg/siteacc/panel/panel.go b/pkg/siteacc/panel/panel.go index 539e54010b..a7d1b3424a 100644 --- a/pkg/siteacc/panel/panel.go +++ b/pkg/siteacc/panel/panel.go @@ -22,21 +22,21 @@ import ( "html/template" "net/http" - config2 "github.com/cs3org/reva/pkg/siteacc/config" - data2 "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/data" "github.com/pkg/errors" "github.com/rs/zerolog" ) // Panel represents the web interface panel of the accounts service. type Panel struct { - conf *config2.Configuration + conf *config.Configuration log *zerolog.Logger tpl *template.Template } -func (panel *Panel) initialize(conf *config2.Configuration, log *zerolog.Logger) error { +func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -57,9 +57,9 @@ func (panel *Panel) initialize(conf *config2.Configuration, log *zerolog.Logger) } // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, accounts *data2.Accounts) error { +func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { type TemplateData struct { - Accounts *data2.Accounts + Accounts *data.Accounts } tplData := TemplateData{ @@ -70,7 +70,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, accounts *data2.Accounts) err } // NewPanel creates a new web interface panel. -func NewPanel(conf *config2.Configuration, log *zerolog.Logger) (*Panel, error) { +func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { panel := &Panel{} if err := panel.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the panel") diff --git a/pkg/siteacc/panel/template.go b/pkg/siteacc/panel/template.go index df3fa7636f..b52b0c4860 100644 --- a/pkg/siteacc/panel/template.go +++ b/pkg/siteacc/panel/template.go @@ -63,8 +63,17 @@ const panelTemplate = ` {{range .Accounts}}
  • - {{.Email}}
    - {{.FirstName}} {{.LastName}} (Joined: {{.DateCreated.Format "Jan 02, 2006 15:04"}}; Last modified: {{.DateModified.Format "Jan 02, 2006 15:04"}}) +

    + {{.Email}}
    + {{.FirstName}} {{.LastName}} (Joined: {{.DateCreated.Format "Jan 02, 2006 15:04"}}; Last modified: {{.DateModified.Format "Jan 02, 2006 15:04"}}) +
    +
    +
      +
    • Organization: {{.Organization}}
    • +
    • Website: {{.Website}}
    • +
    • Phone: {{.PhoneNumber}}
    • +
    +

    API Key: diff --git a/pkg/siteacc/registration/form.go b/pkg/siteacc/registration/form.go index e6447b90f4..e47cd4e9c2 100644 --- a/pkg/siteacc/registration/form.go +++ b/pkg/siteacc/registration/form.go @@ -22,20 +22,20 @@ import ( "html/template" "net/http" - config2 "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/config" "github.com/pkg/errors" "github.com/rs/zerolog" ) // Form represents the web interface form for user account registration. type Form struct { - conf *config2.Configuration + conf *config.Configuration log *zerolog.Logger tpl *template.Template } -func (form *Form) initialize(conf *config2.Configuration, log *zerolog.Logger) error { +func (form *Form) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -66,7 +66,7 @@ func (form *Form) Execute(w http.ResponseWriter) error { } // NewForm creates a new web interface form. -func NewForm(conf *config2.Configuration, log *zerolog.Logger) (*Form, error) { +func NewForm(conf *config.Configuration, log *zerolog.Logger) (*Form, error) { form := &Form{} if err := form.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the form") diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/registration/template.go index 8da584177d..5b52172d70 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/registration/template.go @@ -71,6 +71,11 @@ const formTemplate = ` return false; } + if (formData.get("organization") == "") { + setError("Please specify your organization/company.", "organization"); + return false; + } + return true; } @@ -105,7 +110,10 @@ const formTemplate = ` var postData = { "email": formData.get("email"), "firstName": formData.get("fname"), - "lastName": formData.get("lname") + "lastName": formData.get("lname"), + "organization": formData.get("organization"), + "website": formData.get("website"), + "phoneNumber": formData.get("phone"), }; xhr.send(JSON.stringify(postData)); @@ -193,10 +201,18 @@ const formTemplate = `

    -
    +
    +
    +
    +
    + +
    +
    + +
    Fields marked with * are mandatory.
    -
    +
    From a9f422bf2727142acfcb828e2cb8e11ce51999bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Thu, 1 Jul 2021 13:56:41 +0200 Subject: [PATCH 04/60] Add user passwords --- pkg/siteacc/data/account.go | 64 +++++++++++++++---- pkg/siteacc/endpoints.go | 36 +++++------ pkg/siteacc/manager.go | 27 +++----- pkg/siteacc/password/password.go | 95 ++++++++++++++++++++++++++++ pkg/siteacc/registration/template.go | 39 +++++++++++- 5 files changed, 208 insertions(+), 53 deletions(-) create mode 100644 pkg/siteacc/password/password.go diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 8f24fc06ea..5660a9cb6d 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -19,8 +19,11 @@ package data import ( + "bytes" + "encoding/gob" "time" + "github.com/cs3org/reva/pkg/siteacc/password" "github.com/pkg/errors" "github.com/cs3org/reva/pkg/mentix/key" @@ -36,12 +39,7 @@ type Account struct { Website string `json:"website"` PhoneNumber string `json:"phoneNumber"` - /* - Password struct { - Hash string `json:"hash"` - Salt string `json:"salt"` - } `json:"password"` - */ + Password password.Password `json:"password"` DateCreated time.Time `json:"dateCreated"` DateModified time.Time `json:"dateModified"` @@ -67,9 +65,9 @@ func (acc *Account) GetSiteID() key.SiteIdentifier { return "" } -// Copy copies the data of the given account to this account; if copyData is true, the account data is copied as well. -func (acc *Account) Copy(other *Account, copyData bool) error { - if err := other.verify(); err != nil { +// Update copies the data of the given account to this account; if copyData is true, the account data is copied as well. +func (acc *Account) Update(other *Account, copyData bool) error { + if err := other.verify(false); err != nil { return errors.Wrap(err, "unable to update account data") } @@ -87,26 +85,59 @@ func (acc *Account) Copy(other *Account, copyData bool) error { return nil } -func (acc *Account) verify() error { +// UpdatePassword assigns a new password to the account, salting and hashing it first. +func (acc *Account) UpdatePassword(newPwd string) error { + pwd, err := password.GeneratePassword(newPwd) + if err != nil { + return errors.Wrap(err, "unable to update the user password") + } + acc.Password = *pwd + return nil +} + +func (acc *Account) Clone(erasePassword bool) *Account { + clone := &Account{} + + // To avoid any "deep copy" packages, use gob en- and decoding instead + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + dec := gob.NewDecoder(&buf) + + if err := enc.Encode(acc); err == nil { + _ = dec.Decode(clone) + } + + if erasePassword { + clone.Password.Clear() + } + + return clone +} + +func (acc *Account) verify(verifyPassword bool) error { if acc.Email == "" { return errors.Errorf("no email address provided") } else if !utils.IsEmailValid(acc.Email) { return errors.Errorf("invalid email address: %v", acc.Email) } - if acc.FirstName == "" || acc.LastName == "" { return errors.Errorf("no or incomplete name provided") } - if acc.Organization == "" { return errors.Errorf("no organization provided") } + if verifyPassword { + if !acc.Password.IsValid() { + return errors.Errorf("no valid password set") + } + } + return nil } // NewAccount creates a new site account. -func NewAccount(email string, firstName, lastName string, organization, website string, phoneNumber string) (*Account, error) { +func NewAccount(email string, firstName, lastName string, organization, website string, phoneNumber string, password string) (*Account, error) { t := time.Now() acc := &Account{ @@ -124,7 +155,12 @@ func NewAccount(email string, firstName, lastName string, organization, website }, } - if err := acc.verify(); err != nil { + // Set the user password, which also makes sure that the given password is strong enough + if err := acc.UpdatePassword(password); err != nil { + return nil, err + } + + if err := acc.verify(true); err != nil { return nil, err } diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 0979be02d5..fbc01549e6 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -58,43 +58,43 @@ func createMethodCallbacks(cbGet methodCallback, cbPost methodCallback) map[stri func getEndpoints(enableRegistrationForm bool) []endpoint { endpoints := []endpoint{ // Form endpoints - {config.EndpointPanel, handlePanelEndpoint, nil, false}, + {config.EndpointPanel, callPanelEndpoint, nil, false}, // Request endpoints - {config.EndpointGenerateAPIKey, handleMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, - {config.EndpointVerifyAPIKey, handleMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, - {config.EndpointAssignAPIKey, handleMethodEndpoint, createMethodCallbacks(nil, handleAssignAPIKey), false}, - {config.EndpointList, handleMethodEndpoint, createMethodCallbacks(handleList, nil), false}, - {config.EndpointFind, handleMethodEndpoint, createMethodCallbacks(handleFind, nil), false}, - {config.EndpointCreate, handleMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, - {config.EndpointUpdate, handleMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, - {config.EndpointRemove, handleMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, - {config.EndpointAuthorize, handleMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, - {config.EndpointIsAuthorized, handleMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, - {config.EndpointUnregisterSite, handleMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, + {config.EndpointGenerateAPIKey, callMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, + {config.EndpointVerifyAPIKey, callMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, + {config.EndpointAssignAPIKey, callMethodEndpoint, createMethodCallbacks(nil, handleAssignAPIKey), false}, + {config.EndpointList, callMethodEndpoint, createMethodCallbacks(handleList, nil), false}, + {config.EndpointFind, callMethodEndpoint, createMethodCallbacks(handleFind, nil), false}, + {config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, + {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, + {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, + {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, + {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, + {config.EndpointUnregisterSite, callMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, } if enableRegistrationForm { - endpoints = append(endpoints, endpoint{config.EndpointRegistration, handleRegistrationEndpoint, nil, true}) + endpoints = append(endpoints, endpoint{config.EndpointRegistration, callRegistrationEndpoint, nil, true}) } return endpoints } -func handlePanelEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { +func callPanelEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { if err := mngr.ShowPanel(w); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface panel: %v", err))) } } -func handleRegistrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { +func callRegistrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { if err := mngr.ShowRegistrationForm(w); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface registration registrationForm: %v", err))) } } -func handleMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { +func callMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { // Every request to the accounts service results in a standardized JSON response type Response struct { Success bool `json:"success"` @@ -195,7 +195,7 @@ func handleAssignAPIKey(mngr *Manager, values url.Values, body []byte) (interfac } func handleList(mngr *Manager, values url.Values, body []byte) (interface{}, error) { - return mngr.CloneAccounts(), nil + return mngr.CloneAccounts(true), nil } func handleFind(mngr *Manager, values url.Values, body []byte) (interface{}, error) { @@ -203,7 +203,7 @@ func handleFind(mngr *Manager, values url.Values, body []byte) (interface{}, err if err != nil { return nil, err } - return map[string]interface{}{"account": account}, nil + return map[string]interface{}{"account": account.Clone(true)}, nil } func handleCreate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index 91bbf92b1f..ab23a702be 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -19,8 +19,6 @@ package siteacc import ( - "bytes" - "encoding/gob" "net/http" "strings" "sync" @@ -169,7 +167,7 @@ func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) // ShowPanel writes the panel HTTP output directly to the response writer. func (mngr *Manager) ShowPanel(w http.ResponseWriter) error { // The panel only shows the stored accounts and offers actions through links, so let it use cloned data - accounts := mngr.CloneAccounts() + accounts := mngr.CloneAccounts(true) return mngr.panel.Execute(w, &accounts) } @@ -188,7 +186,7 @@ func (mngr *Manager) CreateAccount(accountData *data.Account) error { return errors.Errorf("an account with the specified email address already exists") } - if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName, accountData.Organization, accountData.Website, accountData.PhoneNumber); err == nil { + if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName, accountData.Organization, accountData.Website, accountData.PhoneNumber, accountData.Password.Value); err == nil { mngr.accounts = append(mngr.accounts, account) mngr.storage.AccountAdded(account) mngr.writeAllAccounts() @@ -211,7 +209,7 @@ func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) err return errors.Wrap(err, "user to update not found") } - if err := account.Copy(accountData, copyData); err == nil { + if err := account.Update(accountData, copyData); err == nil { account.DateModified = time.Now() mngr.storage.AccountUpdated(account) @@ -339,25 +337,16 @@ func (mngr *Manager) RemoveAccount(accountData *data.Account) error { } // CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. -func (mngr *Manager) CloneAccounts() data.Accounts { +func (mngr *Manager) CloneAccounts(erasePasswords bool) data.Accounts { mngr.mutex.RLock() defer mngr.mutex.RUnlock() - clone := make(data.Accounts, 0) - - // To avoid any "deep copy" packages, use gob en- and decoding instead - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - dec := gob.NewDecoder(&buf) - - if err := enc.Encode(mngr.accounts); err == nil { - if err := dec.Decode(&clone); err != nil { - // In case of an error, create an empty data set - clone = make(data.Accounts, 0) - } + clones := make(data.Accounts, 0, len(mngr.accounts)) + for _, acc := range mngr.accounts { + clones = append(clones, acc.Clone(erasePasswords)) } - return clone + return clones } func newManager(conf *config.Configuration, log *zerolog.Logger) (*Manager, error) { diff --git a/pkg/siteacc/password/password.go b/pkg/siteacc/password/password.go new file mode 100644 index 0000000000..bfb3cc98a2 --- /dev/null +++ b/pkg/siteacc/password/password.go @@ -0,0 +1,95 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package password + +import ( + "crypto/sha256" + "fmt" + "strings" + + "github.com/cs3org/reva/pkg/utils" + "github.com/pkg/errors" +) + +// Password holds a hash password alongside its salt value. +type Password struct { + Value string `json:"value"` + Salt string `json:"salt"` +} + +const ( + passwordMinLength = 8 + passwordSaltLength = 16 +) + +// IsValid checks whether the password is valid. +func (password *Password) IsValid() bool { + return len(password.Value) == 64 && len(password.Salt) == passwordSaltLength +} + +// Clear resets the password. +func (password *Password) Clear() { + password.Value = "" + password.Salt = "" +} + +// Compare checks whether the given password string equals the stored one. +func (password *Password) Compare(pwd string) bool { + hashedPwd := hashPassword(pwd, password.Salt) + return hashedPwd == password.Value +} + +// GeneratePassword salts and hashes the given password. +func GeneratePassword(pwd string) (*Password, error) { + if err := VerifyPassword(pwd); err != nil { + return nil, errors.Wrap(err, "invalid password") + } + + // Create a random salt string + salt := utils.RandString(passwordSaltLength) + + return &Password{Value: hashPassword(pwd, salt), Salt: salt}, nil +} + +func hashPassword(pwd, salt string) string { + saltedPwd := pwd + salt + + // Value the salted password using SHA256 + h := sha256.New() + h.Write([]byte(saltedPwd)) + return fmt.Sprintf("%x", h.Sum(nil)) +} + +// VerifyPassword checks whether the given password abides to the enforced password strength. +func VerifyPassword(pwd string) error { + if len(pwd) < passwordMinLength { + return errors.Errorf("the password must be at least 8 characters long") + } + if !strings.ContainsAny(pwd, "abcdefghijklmnopqrstuvwxyz") { + return errors.Errorf("the password must contain at least one lowercase letter") + } + if !strings.ContainsAny(pwd, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") { + return errors.Errorf("the password must contain at least one uppercase letter") + } + if !strings.ContainsAny(pwd, "0123456789") { + return errors.Errorf("the password must contain at least one digit") + } + + return nil +} diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/registration/template.go index 5b52172d70..ae38cd8f53 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/registration/template.go @@ -76,6 +76,21 @@ const formTemplate = ` return false; } + if (formData.get("password") == "") { + setError("Please set a password.", "password"); + return false; + } + + if (formData.get("password2") == "") { + setError("Please repeat your password.", "password2"); + return false; + } + + if (formData.get("password") != formData.get("password2")) { + setError("The entered passwords do not match.", "password2"); + return false; + } + return true; } @@ -114,6 +129,9 @@ const formTemplate = ` "organization": formData.get("organization"), "website": formData.get("website"), "phoneNumber": formData.get("phone"), + "password": { + "value": formData.get("password") + } }; xhr.send(JSON.stringify(postData)); @@ -209,10 +227,27 @@ const formTemplate = `
    -
    +
     
    + +
    +
    +
    +
    + +
    + The password must fulfil the following criteria: +
      +
    • Must be at least 8 characters long
    • +
    • Must contain at least 1 lowercase letter
    • +
    • Must contain at least 1 uppercase letter
    • +
    • Must contain at least 1 digit
    • +
    +
    + +
    Fields marked with * are mandatory.
    -
    +
    From 42ed780592b8e3e89498fad79afd6fcc91bdc2ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 2 Jul 2021 12:52:39 +0200 Subject: [PATCH 05/60] Minor corrections --- pkg/siteacc/registration/template.go | 6 +++--- pkg/siteacc/siteacc.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/registration/template.go index ae38cd8f53..f0fe1369c1 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/registration/template.go @@ -222,16 +222,16 @@ const formTemplate = `
    -
    +
    -
    +
     
    -
    +
    diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index 5041498358..c4be3de04d 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -56,8 +56,8 @@ func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog return nil } -// RequestHandler returns the HTTP request handler of the service. -func (siteacc *SiteAccounts) RequestHandler() http.Handler { +// HTTPHandler returns the HTTP request handler of the service. +func (siteacc *SiteAccounts) HTTPHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() From b3fe4e1184c712c060968ed94a4f24d795d4bc10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 2 Jul 2021 13:49:17 +0200 Subject: [PATCH 06/60] Rename panel to admin --- pkg/siteacc/{panel => admin}/panel.go | 16 ++++++++-------- pkg/siteacc/{panel => admin}/template.go | 2 +- pkg/siteacc/config/config.go | 2 -- pkg/siteacc/config/endpoints.go | 4 ++-- pkg/siteacc/endpoints.go | 17 +++++++---------- pkg/siteacc/manager.go | 20 ++++++++++---------- pkg/siteacc/siteacc.go | 8 ++++---- 7 files changed, 32 insertions(+), 37 deletions(-) rename pkg/siteacc/{panel => admin}/panel.go (79%) rename pkg/siteacc/{panel => admin}/template.go (99%) diff --git a/pkg/siteacc/panel/panel.go b/pkg/siteacc/admin/panel.go similarity index 79% rename from pkg/siteacc/panel/panel.go rename to pkg/siteacc/admin/panel.go index a7d1b3424a..b89b4f7bd4 100644 --- a/pkg/siteacc/panel/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package panel +package admin import ( "html/template" @@ -28,15 +28,15 @@ import ( "github.com/rs/zerolog" ) -// Panel represents the web interface panel of the accounts service. -type Panel struct { +// AdministrationPanel represents the web interface panel of the accounts service administration. +type AdministrationPanel struct { conf *config.Configuration log *zerolog.Logger tpl *template.Template } -func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (panel *AdministrationPanel) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -57,7 +57,7 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { +func (panel *AdministrationPanel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { type TemplateData struct { Accounts *data.Accounts } @@ -70,10 +70,10 @@ func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) erro } // NewPanel creates a new web interface panel. -func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { - panel := &Panel{} +func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*AdministrationPanel, error) { + panel := &AdministrationPanel{} if err := panel.initialize(conf, log); err != nil { - return nil, errors.Wrapf(err, "unable to initialize the panel") + return nil, errors.Wrapf(err, "unable to initialize the administration panel") } return panel, nil } diff --git a/pkg/siteacc/panel/template.go b/pkg/siteacc/admin/template.go similarity index 99% rename from pkg/siteacc/panel/template.go rename to pkg/siteacc/admin/template.go index b52b0c4860..23618e4bd0 100644 --- a/pkg/siteacc/panel/template.go +++ b/pkg/siteacc/admin/template.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package panel +package admin const panelTemplate = ` diff --git a/pkg/siteacc/config/config.go b/pkg/siteacc/config/config.go index 60eeec1902..15c23bb68f 100644 --- a/pkg/siteacc/config/config.go +++ b/pkg/siteacc/config/config.go @@ -32,8 +32,6 @@ type Configuration struct { } `mapstructure:"file"` } `mapstructure:"storage"` - EnableRegistrationForm bool `mapstructure:"enable_registration_form"` - SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"` NotificationsMail string `mapstructure:"notifications_mail"` diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index 8042085e61..cc4be321e2 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -19,8 +19,8 @@ package config const ( - // EndpointPanel is the endpoint path of the web interface panel. - EndpointPanel = "/panel" + // EndpointAdministration is the endpoint path of the web interface administration panel. + EndpointAdministration = "/admin" // EndpointRegistration is the endpoint path of the web interface registration form. EndpointRegistration = "/register" diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index fbc01549e6..4641cb4c07 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -55,10 +55,11 @@ func createMethodCallbacks(cbGet methodCallback, cbPost methodCallback) map[stri return callbacks } -func getEndpoints(enableRegistrationForm bool) []endpoint { +func getEndpoints() []endpoint { endpoints := []endpoint{ - // Form endpoints - {config.EndpointPanel, callPanelEndpoint, nil, false}, + // Form/panel endpoints + {config.EndpointAdministration, callAdministrationEndpoint, nil, false}, + {config.EndpointRegistration, callRegistrationEndpoint, nil, true}, // Request endpoints {config.EndpointGenerateAPIKey, callMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, {config.EndpointVerifyAPIKey, callMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, @@ -73,17 +74,13 @@ func getEndpoints(enableRegistrationForm bool) []endpoint { {config.EndpointUnregisterSite, callMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, } - if enableRegistrationForm { - endpoints = append(endpoints, endpoint{config.EndpointRegistration, callRegistrationEndpoint, nil, true}) - } - return endpoints } -func callPanelEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowPanel(w); err != nil { +func callAdministrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { + if err := mngr.ShowAdministrationPanel(w); err != nil { w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface panel: %v", err))) + _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface administration adminPanel: %v", err))) } } diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index ab23a702be..5a04fabe60 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -24,10 +24,10 @@ import ( "sync" "time" + "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/email" - "github.com/cs3org/reva/pkg/siteacc/panel" "github.com/cs3org/reva/pkg/siteacc/registration" "github.com/cs3org/reva/pkg/siteacc/sitereg" "github.com/pkg/errors" @@ -54,7 +54,7 @@ type Manager struct { accounts data.Accounts storage data.Storage - panel *panel.Panel + adminPanel *admin.AdministrationPanel registrationForm *registration.Form smtp *smtpclient.SMTPCredentials @@ -82,11 +82,11 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create accounts storage") } - // Create the web interface panel - if pnl, err := panel.NewPanel(conf, log); err == nil { - mngr.panel = pnl + // Create the web interface adminPanel + if pnl, err := admin.NewPanel(conf, log); err == nil { + mngr.adminPanel = pnl } else { - return errors.Wrap(err, "unable to create panel") + return errors.Wrap(err, "unable to create adminPanel") } // Create the web interface registrationForm @@ -164,11 +164,11 @@ func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) return nil } -// ShowPanel writes the panel HTTP output directly to the response writer. -func (mngr *Manager) ShowPanel(w http.ResponseWriter) error { - // The panel only shows the stored accounts and offers actions through links, so let it use cloned data +// ShowAdministrationPanel writes the adminPanel HTTP output directly to the response writer. +func (mngr *Manager) ShowAdministrationPanel(w http.ResponseWriter) error { + // The adminPanel only shows the stored accounts and offers actions through links, so let it use cloned data accounts := mngr.CloneAccounts(true) - return mngr.panel.Execute(w, &accounts) + return mngr.adminPanel.Execute(w, &accounts) } // ShowRegistrationForm writes the registration registrationForm HTTP output directly to the response writer. diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index c4be3de04d..c0223f673e 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -56,13 +56,13 @@ func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog return nil } -// HTTPHandler returns the HTTP request handler of the service. -func (siteacc *SiteAccounts) HTTPHandler() http.Handler { +// RequestHandler returns the HTTP request handler of the service. +func (siteacc *SiteAccounts) RequestHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() epHandled := false - for _, ep := range getEndpoints(siteacc.conf.EnableRegistrationForm) { + for _, ep := range getEndpoints() { if ep.Path == r.URL.Path { ep.Handler(siteacc.manager, ep, w, r) epHandled = true @@ -82,7 +82,7 @@ func (siteacc *SiteAccounts) GetPublicEndpoints() []string { return []string{"/"} endpoints := make([]string, 0, 5) - for _, ep := range getEndpoints(siteacc.conf.EnableRegistrationForm) { + for _, ep := range getEndpoints() { if ep.IsPublic { endpoints = append(endpoints, ep.Path) } From cf8b597bfd518c709e060672e72ea3aea4ed8656 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 2 Jul 2021 15:33:04 +0200 Subject: [PATCH 07/60] Use bcrypt for storing passwords --- pkg/siteacc/data/account.go | 9 +++--- pkg/siteacc/password/password.go | 54 +++++++++++++------------------- 2 files changed, 26 insertions(+), 37 deletions(-) diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 5660a9cb6d..55d17ae893 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -85,16 +85,15 @@ func (acc *Account) Update(other *Account, copyData bool) error { return nil } -// UpdatePassword assigns a new password to the account, salting and hashing it first. -func (acc *Account) UpdatePassword(newPwd string) error { - pwd, err := password.GeneratePassword(newPwd) - if err != nil { +// UpdatePassword assigns a new password to the account, hashing it first. +func (acc *Account) UpdatePassword(pwd string) error { + if err := acc.Password.Set(pwd); err != nil { return errors.Wrap(err, "unable to update the user password") } - acc.Password = *pwd return nil } +// Clone creates a copy of the account; if erasePassword is set to true, the password will be cleared in the cloned object. func (acc *Account) Clone(erasePassword bool) *Account { clone := &Account{} diff --git a/pkg/siteacc/password/password.go b/pkg/siteacc/password/password.go index bfb3cc98a2..049252c346 100644 --- a/pkg/siteacc/password/password.go +++ b/pkg/siteacc/password/password.go @@ -19,61 +19,51 @@ package password import ( - "crypto/sha256" "fmt" "strings" - "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" ) // Password holds a hash password alongside its salt value. type Password struct { Value string `json:"value"` - Salt string `json:"salt"` } const ( - passwordMinLength = 8 - passwordSaltLength = 16 + passwordMinLength = 8 ) -// IsValid checks whether the password is valid. -func (password *Password) IsValid() bool { - return len(password.Value) == 64 && len(password.Salt) == passwordSaltLength -} +// Set sets a new password by hashing the plaintext version using bcrypt. +func (password *Password) Set(pwd string) error { + if err := VerifyPassword(pwd); err != nil { + return errors.Wrap(err, "invalid password") + } -// Clear resets the password. -func (password *Password) Clear() { - password.Value = "" - password.Salt = "" + pwdData, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) + if err != nil { + return errors.Wrap(err, "unable to generate password hash") + } + password.Value = string(pwdData) + fmt.Println(password.Value) + return nil } // Compare checks whether the given password string equals the stored one. func (password *Password) Compare(pwd string) bool { - hashedPwd := hashPassword(pwd, password.Salt) - return hashedPwd == password.Value + return bcrypt.CompareHashAndPassword([]byte(password.Value), []byte(pwd)) == nil } -// GeneratePassword salts and hashes the given password. -func GeneratePassword(pwd string) (*Password, error) { - if err := VerifyPassword(pwd); err != nil { - return nil, errors.Wrap(err, "invalid password") - } - - // Create a random salt string - salt := utils.RandString(passwordSaltLength) - - return &Password{Value: hashPassword(pwd, salt), Salt: salt}, nil +// IsValid checks whether the password is valid. +func (password *Password) IsValid() bool { + // bcrypt hashes are in the form of $[version]$[cost]$[22 character salt][31 character hash], so they have a minimum length of 58 + return len(password.Value) > 58 && strings.Count(password.Value, "$") >= 3 } -func hashPassword(pwd, salt string) string { - saltedPwd := pwd + salt - - // Value the salted password using SHA256 - h := sha256.New() - h.Write([]byte(saltedPwd)) - return fmt.Sprintf("%x", h.Sum(nil)) +// Clear resets the password. +func (password *Password) Clear() { + password.Value = "" } // VerifyPassword checks whether the given password abides to the enforced password strength. From 54b52582bb6fb43754fd937196502a35c760fba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 5 Jul 2021 14:41:09 +0200 Subject: [PATCH 08/60] Implement an HTML panel engine --- pkg/siteacc/admin/panel.go | 55 ++-- pkg/siteacc/admin/template.go | 73 +++--- pkg/siteacc/html/panel.go | 97 +++++++ pkg/siteacc/html/provider.go | 34 +++ pkg/siteacc/html/template.go | 157 +++++++++++ pkg/siteacc/registration/form.go | 53 ++-- pkg/siteacc/registration/template.go | 377 ++++++++++----------------- 7 files changed, 526 insertions(+), 320 deletions(-) create mode 100644 pkg/siteacc/html/panel.go create mode 100644 pkg/siteacc/html/provider.go create mode 100644 pkg/siteacc/html/template.go diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index b89b4f7bd4..cf5580aa56 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -19,44 +19,59 @@ package admin import ( - "html/template" "net/http" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" "github.com/rs/zerolog" ) // AdministrationPanel represents the web interface panel of the accounts service administration. type AdministrationPanel struct { - conf *config.Configuration - log *zerolog.Logger + html.ContentProvider - tpl *template.Template + htmlPanel *html.Panel } func (panel *AdministrationPanel) initialize(conf *config.Configuration, log *zerolog.Logger) error { - if conf == nil { - return errors.Errorf("no configuration provided") + // Create the internal HTML panel + htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log) + if err != nil { + return errors.Wrap(err, "unable to create the administration panel") } - panel.conf = conf + panel.htmlPanel = htmlPanel - if log == nil { - return errors.Errorf("no logger provided") - } - panel.log = log + return nil +} - // Create the panel template - panel.tpl = template.New("panel") - if _, err := panel.tpl.Parse(panelTemplate); err != nil { - return errors.Wrap(err, "error while parsing panel template") - } +// GetTitle returns the title of the htmlPanel. +func (panel *AdministrationPanel) GetTitle() string { + return "Administration Panel" +} - return nil +// GetCaption returns the caption which is displayed on the htmlPanel. +func (panel *AdministrationPanel) GetCaption() string { + return "Accounts ({{.Accounts | len}})" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (panel *AdministrationPanel) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (panel *AdministrationPanel) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (panel *AdministrationPanel) GetContentBody() string { + return tplBody } -// Execute generates the HTTP output of the panel and writes it to the response writer. +// Execute generates the HTTP output of the htmlPanel and writes it to the response writer. func (panel *AdministrationPanel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { type TemplateData struct { Accounts *data.Accounts @@ -66,10 +81,10 @@ func (panel *AdministrationPanel) Execute(w http.ResponseWriter, accounts *data. Accounts: accounts, } - return panel.tpl.Execute(w, tplData) + return panel.htmlPanel.Execute(w, tplData) } -// NewPanel creates a new web interface panel. +// NewPanel creates a new administration panel. func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*AdministrationPanel, error) { panel := &AdministrationPanel{} if err := panel.initialize(conf, log); err != nil { diff --git a/pkg/siteacc/admin/template.go b/pkg/siteacc/admin/template.go index 23618e4bd0..479c350deb 100644 --- a/pkg/siteacc/admin/template.go +++ b/pkg/siteacc/admin/template.go @@ -18,47 +18,41 @@ package admin -const panelTemplate = ` - - - - - - Accounts panel - - + xhr.send(JSON.stringify(postData)); +} +` -

    Accounts ({{.Accounts | len}})

    -

    +const tplStyleSheet = ` +html * { + font-family: monospace !important; +} +` + +const tplBody = ` +

      {{range .Accounts}}
    • @@ -114,8 +108,5 @@ const panelTemplate = `
    • {{end}}
    -

    - - - +
    ` diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go new file mode 100644 index 0000000000..06f74d273a --- /dev/null +++ b/pkg/siteacc/html/panel.go @@ -0,0 +1,97 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package html + +import ( + "html/template" + "net/http" + "strings" + + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// Panel provides basic HTML panel functionality. +type Panel struct { + conf *config.Configuration + log *zerolog.Logger + + provider ContentProvider + + tpl *template.Template +} + +func (panel *Panel) initialize(name string, provider ContentProvider, conf *config.Configuration, log *zerolog.Logger) error { + if conf == nil { + return errors.Errorf("no configuration provided") + } + panel.conf = conf + + if log == nil { + return errors.Errorf("no logger provided") + } + panel.log = log + + if provider == nil { + return errors.Errorf("no content provider provided") + } + panel.provider = provider + + // Create the panel template + content, err := panel.compile() + if err != nil { + return errors.Wrap(err, "error while compiling the panel template") + } + + panel.tpl = template.New(name) + if _, err := panel.tpl.Parse(content); err != nil { + return errors.Wrap(err, "error while parsing the panel template") + } + + return nil +} + +func (panel *Panel) compile() (string, error) { + content := panelTemplate + + // Replace placeholders by the values provided by the content provider + content = strings.ReplaceAll(content, "$(TITLE)", panel.provider.GetTitle()) + content = strings.ReplaceAll(content, "$(CAPTION)", panel.provider.GetCaption()) + + content = strings.ReplaceAll(content, "$(CONTENT_JAVASCRIPT)", panel.provider.GetContentJavaScript()) + content = strings.ReplaceAll(content, "$(CONTENT_STYLESHEET)", panel.provider.GetContentStyleSheet()) + content = strings.ReplaceAll(content, "$(CONTENT_BODY)", panel.provider.GetContentBody()) + + return content, nil +} + +// Execute generates the HTTP output of the panel and writes it to the response writer. +func (panel *Panel) Execute(w http.ResponseWriter, data interface{}) error { + return panel.tpl.Execute(w, data) +} + +// NewPanel creates a new panel. +func NewPanel(name string, provider ContentProvider, conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { + panel := &Panel{} + if err := panel.initialize(name, provider, conf, log); err != nil { + return nil, errors.Wrapf(err, "unable to initialize the panel") + } + return panel, nil +} diff --git a/pkg/siteacc/html/provider.go b/pkg/siteacc/html/provider.go new file mode 100644 index 0000000000..668b920568 --- /dev/null +++ b/pkg/siteacc/html/provider.go @@ -0,0 +1,34 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package html + +// ContentProvider defines various methods for HTML content providers. +type ContentProvider interface { + // GetTitle returns the title of the panel. + GetTitle() string + // GetCaption returns the caption which is displayed on the panel. + GetCaption() string + + // GetContentJavaScript delivers additional JavaScript code. + GetContentJavaScript() string + // GetContentStyleSheet delivers additional stylesheet code. + GetContentStyleSheet() string + // GetContentBody delivers the actual body content. + GetContentBody() string +} diff --git a/pkg/siteacc/html/template.go b/pkg/siteacc/html/template.go new file mode 100644 index 0000000000..c24d530054 --- /dev/null +++ b/pkg/siteacc/html/template.go @@ -0,0 +1,157 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package html + +const panelTemplate = ` + + + + + + $(TITLE) + + + +
    +

    $(CAPTION)

    + + $(CONTENT_BODY) + + + + +
    + + +` diff --git a/pkg/siteacc/registration/form.go b/pkg/siteacc/registration/form.go index e47cd4e9c2..b849f01969 100644 --- a/pkg/siteacc/registration/form.go +++ b/pkg/siteacc/registration/form.go @@ -19,40 +19,55 @@ package registration import ( - "html/template" "net/http" "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" "github.com/rs/zerolog" ) // Form represents the web interface form for user account registration. type Form struct { - conf *config.Configuration - log *zerolog.Logger + html.ContentProvider - tpl *template.Template + htmlPanel *html.Panel } func (form *Form) initialize(conf *config.Configuration, log *zerolog.Logger) error { - if conf == nil { - return errors.Errorf("no configuration provided") + // Create the internal HTML panel + htmlPanel, err := html.NewPanel("registration-form", form, conf, log) + if err != nil { + return errors.Wrap(err, "unable to create the registration form") } - form.conf = conf + form.htmlPanel = htmlPanel - if log == nil { - return errors.Errorf("no logger provided") - } - form.log = log + return nil +} - // Create the form template - form.tpl = template.New("form") - if _, err := form.tpl.Parse(formTemplate); err != nil { - return errors.Wrap(err, "error while parsing form template") - } +// GetTitle returns the title of the htmlPanel. +func (form *Form) GetTitle() string { + return "ScienceMesh Account Registration" +} - return nil +// GetCaption returns the caption which is displayed on the htmlPanel. +func (form *Form) GetCaption() string { + return "Welcome to the ScienceMesh Account Registration!" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (form *Form) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (form *Form) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (form *Form) GetContentBody() string { + return tplBody } // Execute generates the HTTP output of the form and writes it to the response writer. @@ -62,14 +77,14 @@ func (form *Form) Execute(w http.ResponseWriter) error { tplData := TemplateData{} - return form.tpl.Execute(w, tplData) + return form.htmlPanel.Execute(w, tplData) } // NewForm creates a new web interface form. func NewForm(conf *config.Configuration, log *zerolog.Logger) (*Form, error) { form := &Form{} if err := form.initialize(conf, log); err != nil { - return nil, errors.Wrapf(err, "unable to initialize the form") + return nil, errors.Wrapf(err, "unable to initialize the registration form") } return form, nil } diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/registration/template.go index f0fe1369c1..a9fd1b2862 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/registration/template.go @@ -18,250 +18,147 @@ package registration -const formTemplate = ` - - - - - - ScienceMesh Account Registration - - - -
    -

    Welcome to the ScienceMesh Account Registration!

    -
    -

    Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.

    -

    - After inspection by a ScienceMesh administrator, you will also receive an API key which can then be used in the - ownCloud or - Nextcloud web application. -

    -
    -
     
    -
    -
    -
    -
    -
    -
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
     
    + } + } + + var postData = { + "email": formData.get("email"), + "firstName": formData.get("fname"), + "lastName": formData.get("lname"), + "organization": formData.get("organization"), + "website": formData.get("website"), + "phoneNumber": formData.get("phone"), + "password": { + "value": formData.get("password") + } + }; + + xhr.send(JSON.stringify(postData)); +} +` -
    -
    -
    -
    +const tplStyleSheet = ` +html * { + font-family: arial !important; +} -
    - The password must fulfil the following criteria: -
      -
    • Must be at least 8 characters long
    • -
    • Must contain at least 1 lowercase letter
    • -
    • Must contain at least 1 uppercase letter
    • -
    • Must contain at least 1 digit
    • -
    -
    +.mandatory { + color: red; + font-weight: bold; +} +` -
    - Fields marked with * are mandatory. -
    -
    - - -
    -
    -
    - - - +const tplBody = ` +
    +

    Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.

    +

    + After inspection by a ScienceMesh administrator, you will also receive an API key which can then be used in the + ownCloud or + Nextcloud web application. +

    +
    +
     
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + +
     
    + +
    +
    +
    +
    + +
    + The password must fulfil the following criteria: +
      +
    • Must be at least 8 characters long
    • +
    • Must contain at least 1 lowercase letter
    • +
    • Must contain at least 1 uppercase letter
    • +
    • Must contain at least 1 digit
    • +
    +
    + +
    + Fields marked with * are mandatory. +
    +
    + + +
    +
    - - ` From 6abccff0c9e535e5578dd08299d15839ebdad2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 5 Jul 2021 14:47:27 +0200 Subject: [PATCH 09/60] Add user role to accounts --- pkg/siteacc/admin/template.go | 1 + pkg/siteacc/data/account.go | 8 +++++++- pkg/siteacc/manager.go | 2 +- pkg/siteacc/registration/template.go | 8 ++++++++ 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/siteacc/admin/template.go b/pkg/siteacc/admin/template.go index 479c350deb..a25dbadce3 100644 --- a/pkg/siteacc/admin/template.go +++ b/pkg/siteacc/admin/template.go @@ -65,6 +65,7 @@ const tplBody = `
    • Organization: {{.Organization}}
    • Website: {{.Website}}
    • +
    • Role: {{.Role}}
    • Phone: {{.PhoneNumber}}
    diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 55d17ae893..1d46707c36 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -37,6 +37,7 @@ type Account struct { LastName string `json:"lastName"` Organization string `json:"organization"` Website string `json:"website"` + Role string `json:"role"` PhoneNumber string `json:"phoneNumber"` Password password.Password `json:"password"` @@ -76,6 +77,7 @@ func (acc *Account) Update(other *Account, copyData bool) error { acc.LastName = other.LastName acc.Organization = other.Organization acc.Website = other.Website + acc.Role = other.Role acc.PhoneNumber = other.PhoneNumber if copyData { @@ -125,6 +127,9 @@ func (acc *Account) verify(verifyPassword bool) error { if acc.Organization == "" { return errors.Errorf("no organization provided") } + if acc.Role == "" { + return errors.Errorf("no role provided") + } if verifyPassword { if !acc.Password.IsValid() { @@ -136,7 +141,7 @@ func (acc *Account) verify(verifyPassword bool) error { } // NewAccount creates a new site account. -func NewAccount(email string, firstName, lastName string, organization, website string, phoneNumber string, password string) (*Account, error) { +func NewAccount(email string, firstName, lastName string, organization, website string, role string, phoneNumber string, password string) (*Account, error) { t := time.Now() acc := &Account{ @@ -145,6 +150,7 @@ func NewAccount(email string, firstName, lastName string, organization, website LastName: lastName, Organization: organization, Website: website, + Role: role, PhoneNumber: phoneNumber, DateCreated: t, DateModified: t, diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index 5a04fabe60..f810656353 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -186,7 +186,7 @@ func (mngr *Manager) CreateAccount(accountData *data.Account) error { return errors.Errorf("an account with the specified email address already exists") } - if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName, accountData.Organization, accountData.Website, accountData.PhoneNumber, accountData.Password.Value); err == nil { + if account, err := data.NewAccount(accountData.Email, accountData.FirstName, accountData.LastName, accountData.Organization, accountData.Website, accountData.Role, accountData.PhoneNumber, accountData.Password.Value); err == nil { mngr.accounts = append(mngr.accounts, account) mngr.storage.AccountAdded(account) mngr.writeAllAccounts() diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/registration/template.go index a9fd1b2862..ffc34fc4fa 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/registration/template.go @@ -40,6 +40,11 @@ function verifyForm(formData) { return false; } + if (formData.get("role") == "") { + setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true); + return false; + } + if (formData.get("password") == "") { setState(STATE_ERROR, "Please set a password.", "form", "password", true); return false; @@ -87,6 +92,7 @@ function handleAction(action) { "lastName": formData.get("lname"), "organization": formData.get("organization"), "website": formData.get("website"), + "role": formData.get("role"), "phoneNumber": formData.get("phone"), "password": { "value": formData.get("password") @@ -132,6 +138,8 @@ const tplBody = `
    +
    +
    From e5a9f2e52222139d707d29357994bc65d11fe38b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 5 Jul 2021 16:18:07 +0200 Subject: [PATCH 10/60] Move endpoint /register to /account --- .../form.go => account/panel.go} | 38 +++++++++---------- .../{registration => account}/template.go | 2 +- pkg/siteacc/admin/panel.go | 24 ++++++------ pkg/siteacc/config/endpoints.go | 4 +- pkg/siteacc/endpoints.go | 10 ++--- pkg/siteacc/manager.go | 29 +++++++------- 6 files changed, 54 insertions(+), 53 deletions(-) rename pkg/siteacc/{registration/form.go => account/panel.go} (64%) rename pkg/siteacc/{registration => account}/template.go (99%) diff --git a/pkg/siteacc/registration/form.go b/pkg/siteacc/account/panel.go similarity index 64% rename from pkg/siteacc/registration/form.go rename to pkg/siteacc/account/panel.go index b849f01969..a5014af6a4 100644 --- a/pkg/siteacc/registration/form.go +++ b/pkg/siteacc/account/panel.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package registration +package account import ( "net/http" @@ -27,64 +27,64 @@ import ( "github.com/rs/zerolog" ) -// Form represents the web interface form for user account registration. -type Form struct { +// Panel represents the account panel. +type Panel struct { html.ContentProvider htmlPanel *html.Panel } -func (form *Form) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { // Create the internal HTML panel - htmlPanel, err := html.NewPanel("registration-form", form, conf, log) + htmlPanel, err := html.NewPanel("account-panel", panel, conf, log) if err != nil { - return errors.Wrap(err, "unable to create the registration form") + return errors.Wrap(err, "unable to create the account panel") } - form.htmlPanel = htmlPanel + panel.htmlPanel = htmlPanel return nil } // GetTitle returns the title of the htmlPanel. -func (form *Form) GetTitle() string { - return "ScienceMesh Account Registration" +func (panel *Panel) GetTitle() string { + return "ScienceMesh Account Panel" } // GetCaption returns the caption which is displayed on the htmlPanel. -func (form *Form) GetCaption() string { +func (panel *Panel) GetCaption() string { return "Welcome to the ScienceMesh Account Registration!" } // GetContentJavaScript delivers additional JavaScript code. -func (form *Form) GetContentJavaScript() string { +func (panel *Panel) GetContentJavaScript() string { return tplJavaScript } // GetContentStyleSheet delivers additional stylesheet code. -func (form *Form) GetContentStyleSheet() string { +func (panel *Panel) GetContentStyleSheet() string { return tplStyleSheet } // GetContentBody delivers the actual body content. -func (form *Form) GetContentBody() string { +func (panel *Panel) GetContentBody() string { return tplBody } // Execute generates the HTTP output of the form and writes it to the response writer. -func (form *Form) Execute(w http.ResponseWriter) error { +func (panel *Panel) Execute(w http.ResponseWriter) error { type TemplateData struct { } tplData := TemplateData{} - return form.htmlPanel.Execute(w, tplData) + return panel.htmlPanel.Execute(w, tplData) } -// NewForm creates a new web interface form. -func NewForm(conf *config.Configuration, log *zerolog.Logger) (*Form, error) { - form := &Form{} +// NewPanel creates a new account panel. +func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { + form := &Panel{} if err := form.initialize(conf, log); err != nil { - return nil, errors.Wrapf(err, "unable to initialize the registration form") + return nil, errors.Wrapf(err, "unable to initialize the account panel") } return form, nil } diff --git a/pkg/siteacc/registration/template.go b/pkg/siteacc/account/template.go similarity index 99% rename from pkg/siteacc/registration/template.go rename to pkg/siteacc/account/template.go index ffc34fc4fa..d40e49c6d9 100644 --- a/pkg/siteacc/registration/template.go +++ b/pkg/siteacc/account/template.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package registration +package account const tplJavaScript = ` function verifyForm(formData) { diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index cf5580aa56..b28a38b49a 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -28,14 +28,14 @@ import ( "github.com/rs/zerolog" ) -// AdministrationPanel represents the web interface panel of the accounts service administration. -type AdministrationPanel struct { +// Panel represents the web interface panel of the accounts service administration. +type Panel struct { html.ContentProvider htmlPanel *html.Panel } -func (panel *AdministrationPanel) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { // Create the internal HTML panel htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log) if err != nil { @@ -47,32 +47,32 @@ func (panel *AdministrationPanel) initialize(conf *config.Configuration, log *ze } // GetTitle returns the title of the htmlPanel. -func (panel *AdministrationPanel) GetTitle() string { - return "Administration Panel" +func (panel *Panel) GetTitle() string { + return "ScienceMesh Administration Panel" } // GetCaption returns the caption which is displayed on the htmlPanel. -func (panel *AdministrationPanel) GetCaption() string { +func (panel *Panel) GetCaption() string { return "Accounts ({{.Accounts | len}})" } // GetContentJavaScript delivers additional JavaScript code. -func (panel *AdministrationPanel) GetContentJavaScript() string { +func (panel *Panel) GetContentJavaScript() string { return tplJavaScript } // GetContentStyleSheet delivers additional stylesheet code. -func (panel *AdministrationPanel) GetContentStyleSheet() string { +func (panel *Panel) GetContentStyleSheet() string { return tplStyleSheet } // GetContentBody delivers the actual body content. -func (panel *AdministrationPanel) GetContentBody() string { +func (panel *Panel) GetContentBody() string { return tplBody } // Execute generates the HTTP output of the htmlPanel and writes it to the response writer. -func (panel *AdministrationPanel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { +func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { type TemplateData struct { Accounts *data.Accounts } @@ -85,8 +85,8 @@ func (panel *AdministrationPanel) Execute(w http.ResponseWriter, accounts *data. } // NewPanel creates a new administration panel. -func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*AdministrationPanel, error) { - panel := &AdministrationPanel{} +func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { + panel := &Panel{} if err := panel.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the administration panel") } diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index cc4be321e2..44fe68bb29 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -21,8 +21,8 @@ package config const ( // EndpointAdministration is the endpoint path of the web interface administration panel. EndpointAdministration = "/admin" - // EndpointRegistration is the endpoint path of the web interface registration form. - EndpointRegistration = "/register" + // EndpointAccount is the endpoint path of the web interface account panel. + EndpointAccount = "/account" // EndpointGenerateAPIKey is the endpoint path of the API key generator. EndpointGenerateAPIKey = "/generate-api-key" diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 4641cb4c07..6cde4ccce9 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -59,7 +59,7 @@ func getEndpoints() []endpoint { endpoints := []endpoint{ // Form/panel endpoints {config.EndpointAdministration, callAdministrationEndpoint, nil, false}, - {config.EndpointRegistration, callRegistrationEndpoint, nil, true}, + {config.EndpointAccount, callAccountEndpoint, nil, true}, // Request endpoints {config.EndpointGenerateAPIKey, callMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, {config.EndpointVerifyAPIKey, callMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, @@ -80,14 +80,14 @@ func getEndpoints() []endpoint { func callAdministrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { if err := mngr.ShowAdministrationPanel(w); err != nil { w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface administration adminPanel: %v", err))) + _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the administration panel: %v", err))) } } -func callRegistrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowRegistrationForm(w); err != nil { +func callAccountEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { + if err := mngr.ShowAccountPanel(w); err != nil { w.WriteHeader(http.StatusInternalServerError) - _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the web interface registration registrationForm: %v", err))) + _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the account panel: %v", err))) } } diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index f810656353..8668bc4133 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -24,11 +24,11 @@ import ( "sync" "time" + "github.com/cs3org/reva/pkg/siteacc/account" "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/email" - "github.com/cs3org/reva/pkg/siteacc/registration" "github.com/cs3org/reva/pkg/siteacc/sitereg" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -54,9 +54,10 @@ type Manager struct { accounts data.Accounts storage data.Storage - adminPanel *admin.AdministrationPanel - registrationForm *registration.Form - smtp *smtpclient.SMTPCredentials + adminPanel *admin.Panel + accountPanel *account.Panel + + smtp *smtpclient.SMTPCredentials mutex sync.RWMutex } @@ -82,18 +83,18 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create accounts storage") } - // Create the web interface adminPanel + // Create the admin panel if pnl, err := admin.NewPanel(conf, log); err == nil { mngr.adminPanel = pnl } else { - return errors.Wrap(err, "unable to create adminPanel") + return errors.Wrap(err, "unable to create the administration panel") } - // Create the web interface registrationForm - if frm, err := registration.NewForm(conf, log); err == nil { - mngr.registrationForm = frm + // Create the account panel + if pnl, err := account.NewPanel(conf, log); err == nil { + mngr.accountPanel = pnl } else { - return errors.Wrap(err, "unable to create registrationForm") + return errors.Wrap(err, "unable to create the account panel") } // Create the SMTP client @@ -164,16 +165,16 @@ func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) return nil } -// ShowAdministrationPanel writes the adminPanel HTTP output directly to the response writer. +// ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer. func (mngr *Manager) ShowAdministrationPanel(w http.ResponseWriter) error { // The adminPanel only shows the stored accounts and offers actions through links, so let it use cloned data accounts := mngr.CloneAccounts(true) return mngr.adminPanel.Execute(w, &accounts) } -// ShowRegistrationForm writes the registration registrationForm HTTP output directly to the response writer. -func (mngr *Manager) ShowRegistrationForm(w http.ResponseWriter) error { - return mngr.registrationForm.Execute(w) +// ShowAccountPanel writes the account panel HTTP output directly to the response writer. +func (mngr *Manager) ShowAccountPanel(w http.ResponseWriter) error { + return mngr.accountPanel.Execute(w) } // CreateAccount creates a new account; if an account with the same email address already exists, an error is returned. From 951bfe543c2465358ffda110207dc77a53dc8f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 6 Jul 2021 13:50:40 +0200 Subject: [PATCH 11/60] Add session management --- .../config/http/services/siteacc/_index.md | 9 ++ examples/siteacc/siteacc.toml | 5 + internal/http/services/siteacc/siteacc.go | 4 + pkg/siteacc/account/panel.go | 4 +- pkg/siteacc/admin/panel.go | 4 +- pkg/siteacc/config/config.go | 4 + pkg/siteacc/endpoints.go | 4 +- pkg/siteacc/html/panel.go | 19 ++- pkg/siteacc/html/session.go | 81 ++++++++++ pkg/siteacc/html/sessionmanager.go | 139 ++++++++++++++++++ pkg/siteacc/manager.go | 8 +- 11 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 pkg/siteacc/html/session.go create mode 100644 pkg/siteacc/html/sessionmanager.go diff --git a/docs/content/en/docs/config/http/services/siteacc/_index.md b/docs/content/en/docs/config/http/services/siteacc/_index.md index d56f901c0f..7320466789 100644 --- a/docs/content/en/docs/config/http/services/siteacc/_index.md +++ b/docs/content/en/docs/config/http/services/siteacc/_index.md @@ -111,3 +111,12 @@ The registration service URL. url = "https://iop.example.com/sitereg" {{< /highlight >}} {{% /dir %}} + +## Webserver settings +{{% dir name="session_timeout" type="int" default="120" %}} +The session timeout in seconds. +{{< highlight toml >}} +[http.services.siteacc.webserver] +session_timeout = 600 +{{< /highlight >}} +{{% /dir %}} diff --git a/examples/siteacc/siteacc.toml b/examples/siteacc/siteacc.toml index 093fe58f86..5e4d33031c 100644 --- a/examples/siteacc/siteacc.toml +++ b/examples/siteacc/siteacc.toml @@ -19,3 +19,8 @@ sender_mail = "science.mesh@example.com" smtp_server = "mail.example.com" smtp_port = 25 disable_auth = true + +# The webserver section defines various webserver-related settings +[http.services.siteacc.webserver] +session_timeout = 60 + diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go index a57c92cd98..0c50cf3bf3 100644 --- a/internal/http/services/siteacc/siteacc.go +++ b/internal/http/services/siteacc/siteacc.go @@ -82,6 +82,10 @@ func applyDefaultConfig(conf *config.Configuration) { if conf.Storage.Driver == "" { conf.Storage.Driver = "file" } + + if conf.Webserver.SessionTimeout < 60 { + conf.Webserver.SessionTimeout = 120 + } } // New returns a new Site Accounts service. diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index a5014af6a4..21c0de86d7 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -71,13 +71,13 @@ func (panel *Panel) GetContentBody() string { } // Execute generates the HTTP output of the form and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter) error { +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request) error { type TemplateData struct { } tplData := TemplateData{} - return panel.htmlPanel.Execute(w, tplData) + return panel.htmlPanel.Execute(w, r, tplData) } // NewPanel creates a new account panel. diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index b28a38b49a..9fba25bab1 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -72,7 +72,7 @@ func (panel *Panel) GetContentBody() string { } // Execute generates the HTTP output of the htmlPanel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error { +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, accounts *data.Accounts) error { type TemplateData struct { Accounts *data.Accounts } @@ -81,7 +81,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) erro Accounts: accounts, } - return panel.htmlPanel.Execute(w, tplData) + return panel.htmlPanel.Execute(w, r, tplData) } // NewPanel creates a new administration panel. diff --git a/pkg/siteacc/config/config.go b/pkg/siteacc/config/config.go index 15c23bb68f..cf802bc4ab 100644 --- a/pkg/siteacc/config/config.go +++ b/pkg/siteacc/config/config.go @@ -38,4 +38,8 @@ type Configuration struct { SiteRegistration struct { URL string `mapstructure:"url"` } `mapstructure:"sitereg"` + + Webserver struct { + SessionTimeout int `mapstructure:"session_timeout"` + } `mapstructure:"webserver"` } diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 6cde4ccce9..8aca833860 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -78,14 +78,14 @@ func getEndpoints() []endpoint { } func callAdministrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowAdministrationPanel(w); err != nil { + if err := mngr.ShowAdministrationPanel(w, r); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the administration panel: %v", err))) } } func callAccountEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowAccountPanel(w); err != nil { + if err := mngr.ShowAccountPanel(w, r); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the account panel: %v", err))) } diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index 06f74d273a..3f69e17c0a 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -35,7 +35,8 @@ type Panel struct { provider ContentProvider - tpl *template.Template + tpl *template.Template + sessions *SessionManager } func (panel *Panel) initialize(name string, provider ContentProvider, conf *config.Configuration, log *zerolog.Logger) error { @@ -55,6 +56,7 @@ func (panel *Panel) initialize(name string, provider ContentProvider, conf *conf panel.provider = provider // Create the panel template + // TODO: Dynamic content; use session object for handling content, err := panel.compile() if err != nil { return errors.Wrap(err, "error while compiling the panel template") @@ -65,6 +67,13 @@ func (panel *Panel) initialize(name string, provider ContentProvider, conf *conf return errors.Wrap(err, "error while parsing the panel template") } + // Create the session mananger + sessions, err := NewSessionManager(name+"_session", conf, log) + if err != nil { + return errors.Wrap(err, "error while creating the session manager") + } + panel.sessions = sessions + return nil } @@ -83,7 +92,13 @@ func (panel *Panel) compile() (string, error) { } // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, data interface{}) error { +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, data interface{}) error { + // TODO: Use returned session + _, err := panel.sessions.HandleRequest(w, r) + if err != nil { + return errors.Wrap(err, "an error occurred while handling sessions") + } + return panel.tpl.Execute(w, data) } diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go new file mode 100644 index 0000000000..521c9b6f5c --- /dev/null +++ b/pkg/siteacc/html/session.go @@ -0,0 +1,81 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package html + +import ( + "net/http" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" +) + +// Session stores all data associated with an HTML session. +type Session struct { + ID string + RemoteAddress string + Expires time.Time + + Data map[string]interface{} + + sessionCookieName string +} + +// Save stores the session ID in a cookie using a response writer. +func (sess *Session) Save(w http.ResponseWriter) { + http.SetCookie(w, &http.Cookie{ + Name: sess.sessionCookieName, + Value: sess.ID, + Expires: sess.Expires, + }) +} + +// VerifyRequest checks whether the provided request matches the stored session. +func (sess *Session) VerifyRequest(r *http.Request) error { + cookie, err := r.Cookie(sess.sessionCookieName) + if err != nil { + return errors.Wrap(err, "unable to retrieve client session ID") + } + if cookie.Value != sess.ID { + return errors.Errorf("the session ID doesn't match") + } + + if r.RemoteAddr != sess.RemoteAddress { + return errors.Errorf("remote address has changed") + } + + return nil +} + +// HasExpired checks whether the session has reached is timeout. +func (sess *Session) HasExpired() bool { + return time.Now().After(sess.Expires) +} + +// NewSession creates a new session, giving it a random ID. +func NewSession(name string, timeout time.Duration, r *http.Request) (*Session, error) { + session := &Session{ + ID: uuid.NewString(), + RemoteAddress: r.RemoteAddr, + Expires: time.Now().Add(timeout), + Data: nil, + sessionCookieName: name, + } + return session, nil +} diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go new file mode 100644 index 0000000000..6674718d89 --- /dev/null +++ b/pkg/siteacc/html/sessionmanager.go @@ -0,0 +1,139 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package html + +import ( + "net/http" + "time" + + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// SessionManager manages HTML sessions. +type SessionManager struct { + conf *config.Configuration + log *zerolog.Logger + + sessions map[string]*Session + + sessionName string +} + +func (mngr *SessionManager) initialize(name string, conf *config.Configuration, log *zerolog.Logger) error { + if name == "" { + return errors.Errorf("no session name provided") + } + mngr.sessionName = name + + if conf == nil { + return errors.Errorf("no configuration provided") + } + mngr.conf = conf + + if log == nil { + return errors.Errorf("no logger provided") + } + mngr.log = log + + mngr.sessions = make(map[string]*Session, 100) + + return nil +} + +// HandleRequest performs all session-related tasks during an HTML request. +func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request) (*Session, error) { + var session *Session + + // Try to get the session ID from the request; if none has been set yet, a new one will be assigned + cookie, err := r.Cookie(mngr.sessionName) + if err == nil { + session = mngr.findSession(cookie.Value) + if session != nil { + // Verify the request against the session: If it is invalid, return an error; if the session has expired, migrate to a new one; otherwise, just continue + if err := session.VerifyRequest(r); err == nil { + if session.HasExpired() { + session, err = mngr.migrateSession(session, r) + if err != nil { + return nil, errors.Wrap(err, "unable to migrate to a new session") + } + } + } else { + return nil, errors.Wrap(err, "invalid session") + } + } + } else if err != http.ErrNoCookie { + // The session cookie exists but seems to be invalid, so return an error + return nil, errors.Wrap(err, "unable to get the session ID from the client") + } + + if session == nil { + // No session found for the client, so create a new one + session, err = mngr.createSession(r) + if err != nil { + return nil, errors.Wrap(err, "unable to assign a new session to the client") + } + } + + // Store the session ID on the client side + session.Save(w) + + return session, nil +} + +func (mngr *SessionManager) createSession(r *http.Request) (*Session, error) { + session, err := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r) + if err != nil { + return nil, errors.Wrap(err, "unable to create a new session") + } + mngr.sessions[session.ID] = session + return session, nil +} + +func (mngr *SessionManager) findSession(id string) *Session { + if session, ok := mngr.sessions[id]; ok { + return session + } + return nil +} + +func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (*Session, error) { + sessionNew, err := mngr.createSession(r) + if err != nil { + return nil, err + } + + // Carry over the old session data, thus preserving the existing session + sessionNew.Data = session.Data + + // Delete the old session + delete(mngr.sessions, session.ID) + + return sessionNew, nil +} + +// NewSessionManager creates a new session manager. +func NewSessionManager(name string, conf *config.Configuration, log *zerolog.Logger) (*SessionManager, error) { + mngr := &SessionManager{} + if err := mngr.initialize(name, conf, log); err != nil { + return nil, errors.Wrapf(err, "unable to initialize the session manager") + } + return mngr, nil +} diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index 8668bc4133..8fe8252cfb 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -166,15 +166,15 @@ func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) } // ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer. -func (mngr *Manager) ShowAdministrationPanel(w http.ResponseWriter) error { +func (mngr *Manager) ShowAdministrationPanel(w http.ResponseWriter, r *http.Request) error { // The adminPanel only shows the stored accounts and offers actions through links, so let it use cloned data accounts := mngr.CloneAccounts(true) - return mngr.adminPanel.Execute(w, &accounts) + return mngr.adminPanel.Execute(w, r, &accounts) } // ShowAccountPanel writes the account panel HTTP output directly to the response writer. -func (mngr *Manager) ShowAccountPanel(w http.ResponseWriter) error { - return mngr.accountPanel.Execute(w) +func (mngr *Manager) ShowAccountPanel(w http.ResponseWriter, r *http.Request) error { + return mngr.accountPanel.Execute(w, r) } // CreateAccount creates a new account; if an account with the same email address already exists, an error is returned. From 9836a6c1df85e5d7f64d81d421797bfc93e7ac1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 6 Jul 2021 15:34:51 +0200 Subject: [PATCH 12/60] Support multiple templates per panel --- pkg/siteacc/account/panel.go | 26 +++++++-- pkg/siteacc/admin/panel.go | 32 ++++++++--- pkg/siteacc/html/panel.go | 91 ++++++++++++++++++++++---------- pkg/siteacc/html/provider.go | 8 +++ pkg/siteacc/html/session.go | 2 +- pkg/siteacc/password/password.go | 2 - 6 files changed, 118 insertions(+), 43 deletions(-) diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 21c0de86d7..6cbd21c443 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -29,11 +29,16 @@ import ( // Panel represents the account panel. type Panel struct { + html.PanelProvider html.ContentProvider htmlPanel *html.Panel } +const ( + templateRegister = "register" +) + func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { // Create the internal HTML panel htmlPanel, err := html.NewPanel("account-panel", panel, conf, log) @@ -42,9 +47,19 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } panel.htmlPanel = htmlPanel + // Add all templates + if err := panel.htmlPanel.AddTemplate(templateRegister, panel); err != nil { + return errors.Wrap(err, "unable to create the registration template") + } + return nil } +// GetActiveTemplate returns the name of the active template. +func (panel *Panel) GetActiveTemplate(*html.Session) string { + return templateRegister +} + // GetTitle returns the title of the htmlPanel. func (panel *Panel) GetTitle() string { return "ScienceMesh Account Panel" @@ -72,12 +87,13 @@ func (panel *Panel) GetContentBody() string { // Execute generates the HTTP output of the form and writes it to the response writer. func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request) error { - type TemplateData struct { - } + dataProvider := func(*html.Session) interface{} { + type TemplateData struct { + } - tplData := TemplateData{} - - return panel.htmlPanel.Execute(w, r, tplData) + return TemplateData{} + } + return panel.htmlPanel.Execute(w, r, dataProvider) } // NewPanel creates a new account panel. diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index 9fba25bab1..a613872e2b 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -30,11 +30,16 @@ import ( // Panel represents the web interface panel of the accounts service administration. type Panel struct { + html.PanelProvider html.ContentProvider htmlPanel *html.Panel } +const ( + templateMain = "main" +) + func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { // Create the internal HTML panel htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log) @@ -43,9 +48,19 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } panel.htmlPanel = htmlPanel + // Add all templates + if err := panel.htmlPanel.AddTemplate(templateMain, panel); err != nil { + return errors.Wrap(err, "unable to create the main template") + } + return nil } +// GetActiveTemplate returns the name of the active template. +func (panel *Panel) GetActiveTemplate(*html.Session) string { + return templateMain +} + // GetTitle returns the title of the htmlPanel. func (panel *Panel) GetTitle() string { return "ScienceMesh Administration Panel" @@ -73,15 +88,16 @@ func (panel *Panel) GetContentBody() string { // Execute generates the HTTP output of the htmlPanel and writes it to the response writer. func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, accounts *data.Accounts) error { - type TemplateData struct { - Accounts *data.Accounts - } - - tplData := TemplateData{ - Accounts: accounts, + dataProvider := func(*html.Session) interface{} { + type TemplateData struct { + Accounts *data.Accounts + } + + return TemplateData{ + Accounts: accounts, + } } - - return panel.htmlPanel.Execute(w, r, tplData) + return panel.htmlPanel.Execute(w, r, dataProvider) } // NewPanel creates a new administration panel. diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index 3f69e17c0a..0d5e763010 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -28,18 +28,27 @@ import ( "github.com/rs/zerolog" ) +type TemplateID = string + // Panel provides basic HTML panel functionality. type Panel struct { conf *config.Configuration log *zerolog.Logger - provider ContentProvider + name string + + provider PanelProvider - tpl *template.Template - sessions *SessionManager + templates map[TemplateID]*template.Template + sessions *SessionManager } -func (panel *Panel) initialize(name string, provider ContentProvider, conf *config.Configuration, log *zerolog.Logger) error { +func (panel *Panel) initialize(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) error { + if name == "" { + return errors.Errorf("no name provided") + } + panel.name = name + if conf == nil { return errors.Errorf("no configuration provided") } @@ -51,21 +60,12 @@ func (panel *Panel) initialize(name string, provider ContentProvider, conf *conf panel.log = log if provider == nil { - return errors.Errorf("no content provider provided") + return errors.Errorf("no panel provider provided") } panel.provider = provider - // Create the panel template - // TODO: Dynamic content; use session object for handling - content, err := panel.compile() - if err != nil { - return errors.Wrap(err, "error while compiling the panel template") - } - - panel.tpl = template.New(name) - if _, err := panel.tpl.Parse(content); err != nil { - return errors.Wrap(err, "error while parsing the panel template") - } + // Create space for the panel templates + panel.templates = make(map[string]*template.Template, 5) // Create the session mananger sessions, err := NewSessionManager(name+"_session", conf, log) @@ -77,33 +77,70 @@ func (panel *Panel) initialize(name string, provider ContentProvider, conf *conf return nil } -func (panel *Panel) compile() (string, error) { +func (panel *Panel) compile(provider ContentProvider) (string, error) { content := panelTemplate // Replace placeholders by the values provided by the content provider - content = strings.ReplaceAll(content, "$(TITLE)", panel.provider.GetTitle()) - content = strings.ReplaceAll(content, "$(CAPTION)", panel.provider.GetCaption()) + content = strings.ReplaceAll(content, "$(TITLE)", provider.GetTitle()) + content = strings.ReplaceAll(content, "$(CAPTION)", provider.GetCaption()) - content = strings.ReplaceAll(content, "$(CONTENT_JAVASCRIPT)", panel.provider.GetContentJavaScript()) - content = strings.ReplaceAll(content, "$(CONTENT_STYLESHEET)", panel.provider.GetContentStyleSheet()) - content = strings.ReplaceAll(content, "$(CONTENT_BODY)", panel.provider.GetContentBody()) + content = strings.ReplaceAll(content, "$(CONTENT_JAVASCRIPT)", provider.GetContentJavaScript()) + content = strings.ReplaceAll(content, "$(CONTENT_STYLESHEET)", provider.GetContentStyleSheet()) + content = strings.ReplaceAll(content, "$(CONTENT_BODY)", provider.GetContentBody()) return content, nil } +// AddTemplate adds and compiles a new template. +func (panel *Panel) AddTemplate(name TemplateID, provider ContentProvider) error { + name = panel.getFullTemplateName(name) + + if provider == nil { + return errors.Errorf("no content provider provided") + } + + content, err := panel.compile(provider) + if err != nil { + return errors.Wrapf(err, "error while compiling panel template %v", name) + } + + tpl := template.New(name) + if _, err := tpl.Parse(content); err != nil { + return errors.Wrapf(err, "error while parsing panel template %v", name) + } + panel.templates[name] = tpl + + return nil +} + // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, data interface{}) error { - // TODO: Use returned session - _, err := panel.sessions.HandleRequest(w, r) +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, dataProvider PanelDataProvider) error { + session, err := panel.sessions.HandleRequest(w, r) if err != nil { return errors.Wrap(err, "an error occurred while handling sessions") } - return panel.tpl.Execute(w, data) + tplName := panel.getFullTemplateName(panel.provider.GetActiveTemplate(session)) + tpl, ok := panel.templates[tplName] + if !ok { + return errors.Errorf("template %v not found", tplName) + } + + // If a data provider is specified, use it to get additional template data + var data interface{} + if dataProvider != nil { + data = dataProvider(session) + } + + return tpl.Execute(w, data) +} + +func (panel *Panel) getFullTemplateName(name string) string { + return panel.name + "-" + name } // NewPanel creates a new panel. -func NewPanel(name string, provider ContentProvider, conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { +func NewPanel(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) (*Panel, error) { panel := &Panel{} if err := panel.initialize(name, provider, conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the panel") diff --git a/pkg/siteacc/html/provider.go b/pkg/siteacc/html/provider.go index 668b920568..e6142bc784 100644 --- a/pkg/siteacc/html/provider.go +++ b/pkg/siteacc/html/provider.go @@ -18,6 +18,14 @@ package html +// PanelProvider handles general panel tasks. +type PanelProvider interface { + // GetActiveTemplate returns the name of the active template. + GetActiveTemplate(*Session) string +} + +type PanelDataProvider = func(*Session) interface{} + // ContentProvider defines various methods for HTML content providers. type ContentProvider interface { // GetTitle returns the title of the panel. diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 521c9b6f5c..c9f13be389 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -57,7 +57,7 @@ func (sess *Session) VerifyRequest(r *http.Request) error { } if r.RemoteAddr != sess.RemoteAddress { - return errors.Errorf("remote address has changed") + return errors.Errorf("remote address has changed (%v != %v)", r.RemoteAddr, sess.RemoteAddress) } return nil diff --git a/pkg/siteacc/password/password.go b/pkg/siteacc/password/password.go index 049252c346..a9a59467e4 100644 --- a/pkg/siteacc/password/password.go +++ b/pkg/siteacc/password/password.go @@ -19,7 +19,6 @@ package password import ( - "fmt" "strings" "github.com/pkg/errors" @@ -46,7 +45,6 @@ func (password *Password) Set(pwd string) error { return errors.Wrap(err, "unable to generate password hash") } password.Value = string(pwdData) - fmt.Println(password.Value) return nil } From 4174708c78b76840b7444c865906a4bf73fa7d31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 7 Jul 2021 13:57:26 +0200 Subject: [PATCH 13/60] Add login form (WIP) --- pkg/siteacc/account/login/login.go | 50 +++++++++ pkg/siteacc/account/login/template.go | 106 ++++++++++++++++++ pkg/siteacc/account/panel.go | 49 ++++---- .../account/registration/registration.go | 50 +++++++++ .../account/{ => registration}/template.go | 5 +- pkg/siteacc/admin/panel.go | 2 +- pkg/siteacc/endpoints.go | 5 +- pkg/siteacc/html/panel.go | 9 +- pkg/siteacc/html/provider.go | 2 +- 9 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 pkg/siteacc/account/login/login.go create mode 100644 pkg/siteacc/account/login/template.go create mode 100644 pkg/siteacc/account/registration/registration.go rename pkg/siteacc/account/{ => registration}/template.go (98%) diff --git a/pkg/siteacc/account/login/login.go b/pkg/siteacc/account/login/login.go new file mode 100644 index 0000000000..237ff5dcb0 --- /dev/null +++ b/pkg/siteacc/account/login/login.go @@ -0,0 +1,50 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package login + +import "github.com/cs3org/reva/pkg/siteacc/html" + +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the htmlPanel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Account Login" +} + +// GetCaption returns the caption which is displayed on the htmlPanel. +func (template *PanelTemplate) GetCaption() string { + return "Login to your ScienceMesh Account!" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (template *PanelTemplate) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (template *PanelTemplate) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (template *PanelTemplate) GetContentBody() string { + return tplBody +} diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go new file mode 100644 index 0000000000..19cc7bb069 --- /dev/null +++ b/pkg/siteacc/account/login/template.go @@ -0,0 +1,106 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package login + +const tplJavaScript = ` +function verifyForm(formData) { + if (formData.get("email") == "") { + setState(STATE_ERROR, "Please enter your email address.", "form", "email", true); + return false; + } + + if (formData.get("password") == "") { + setState(STATE_ERROR, "Please enter your password.", "form", "password", true); + return false; + } + + return true; +} + +function handleAction(action) { + const formData = new FormData(document.querySelector("form")); + if (!verifyForm(formData)) { + return; + } + + setState(STATE_STATUS, "Logging in... this should only take a moment.", "form", null, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", action); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + xhr.onreadystatechange = function() { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status == 200) { + setState(STATE_SUCCESS, "Your login was successful! Redirecting..."); + window.location.replace("?manage"); + } else { + var resp = JSON.parse(this.responseText); + setState(STATE_ERROR, "An error occurred while trying to login your account:
    " + resp.error + "", "form", null, true); + } + } + } + + var postData = { + "email": formData.get("email"), + "password": { + "value": formData.get("password") + } + }; + + xhr.send(JSON.stringify(postData)); +} +` + +const tplStyleSheet = ` +html * { + font-family: arial !important; +} + +.mandatory { + color: red; + font-weight: bold; +} +` + +const tplBody = ` +
    +

    Login to your ScienceMesh account using the form below.

    +
    +
     
    +
    +
    +
    +
    +
    +
    + +
    + Fields marked with * are mandatory. +
    +
    + + +
    +
    +
    +
    +

    Don't' have an account yet? Register here.

    +
    +` diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 6cbd21c443..b66da7a3cf 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -21,6 +21,8 @@ package account import ( "net/http" + "github.com/cs3org/reva/pkg/siteacc/account/login" + "github.com/cs3org/reva/pkg/siteacc/account/registration" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" @@ -30,13 +32,13 @@ import ( // Panel represents the account panel. type Panel struct { html.PanelProvider - html.ContentProvider htmlPanel *html.Panel } const ( - templateRegister = "register" + templateLogin = "login" + templateRegistration = "register" ) func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error { @@ -48,7 +50,11 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) panel.htmlPanel = htmlPanel // Add all templates - if err := panel.htmlPanel.AddTemplate(templateRegister, panel); err != nil { + if err := panel.htmlPanel.AddTemplate(templateLogin, &login.PanelTemplate{}); err != nil { + return errors.Wrap(err, "unable to create the login template") + } + + if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil { return errors.Wrap(err, "unable to create the registration template") } @@ -56,33 +62,20 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } // GetActiveTemplate returns the name of the active template. -func (panel *Panel) GetActiveTemplate(*html.Session) string { - return templateRegister -} - -// GetTitle returns the title of the htmlPanel. -func (panel *Panel) GetTitle() string { - return "ScienceMesh Account Panel" -} - -// GetCaption returns the caption which is displayed on the htmlPanel. -func (panel *Panel) GetCaption() string { - return "Welcome to the ScienceMesh Account Registration!" -} - -// GetContentJavaScript delivers additional JavaScript code. -func (panel *Panel) GetContentJavaScript() string { - return tplJavaScript -} +func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string { + template := templateLogin // TODO: Check if user is logged in + + // Invalid paths are just ignored and redirected to the login/main page + switch path { + case templateLogin: + case templateRegistration: + template = path + } -// GetContentStyleSheet delivers additional stylesheet code. -func (panel *Panel) GetContentStyleSheet() string { - return tplStyleSheet -} + // TODO: Check path access + // TODO: If user is logged in and path == login, redirect to main -// GetContentBody delivers the actual body content. -func (panel *Panel) GetContentBody() string { - return tplBody + return template } // Execute generates the HTTP output of the form and writes it to the response writer. diff --git a/pkg/siteacc/account/registration/registration.go b/pkg/siteacc/account/registration/registration.go new file mode 100644 index 0000000000..f8720a7e7b --- /dev/null +++ b/pkg/siteacc/account/registration/registration.go @@ -0,0 +1,50 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package registration + +import "github.com/cs3org/reva/pkg/siteacc/html" + +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the htmlPanel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Account Registration" +} + +// GetCaption returns the caption which is displayed on the htmlPanel. +func (template *PanelTemplate) GetCaption() string { + return "Welcome to the ScienceMesh Account Registration!" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (template *PanelTemplate) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (template *PanelTemplate) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (template *PanelTemplate) GetContentBody() string { + return tplBody +} diff --git a/pkg/siteacc/account/template.go b/pkg/siteacc/account/registration/template.go similarity index 98% rename from pkg/siteacc/account/template.go rename to pkg/siteacc/account/registration/template.go index d40e49c6d9..d58e3ce184 100644 --- a/pkg/siteacc/account/template.go +++ b/pkg/siteacc/account/registration/template.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package account +package registration const tplJavaScript = ` function verifyForm(formData) { @@ -169,4 +169,7 @@ const tplBody = `
    +
    +

    Already have an account? Login here.

    +
    ` diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index a613872e2b..9e8bd50042 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -57,7 +57,7 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) } // GetActiveTemplate returns the name of the active template. -func (panel *Panel) GetActiveTemplate(*html.Session) string { +func (panel *Panel) GetActiveTemplate(*html.Session, string) string { return templateMain } diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 8aca833860..781194f7f6 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -60,17 +60,20 @@ func getEndpoints() []endpoint { // Form/panel endpoints {config.EndpointAdministration, callAdministrationEndpoint, nil, false}, {config.EndpointAccount, callAccountEndpoint, nil, true}, - // Request endpoints + // API key endpoints {config.EndpointGenerateAPIKey, callMethodEndpoint, createMethodCallbacks(handleGenerateAPIKey, nil), false}, {config.EndpointVerifyAPIKey, callMethodEndpoint, createMethodCallbacks(handleVerifyAPIKey, nil), false}, {config.EndpointAssignAPIKey, callMethodEndpoint, createMethodCallbacks(nil, handleAssignAPIKey), false}, + // General account endpoints {config.EndpointList, callMethodEndpoint, createMethodCallbacks(handleList, nil), false}, {config.EndpointFind, callMethodEndpoint, createMethodCallbacks(handleFind, nil), false}, {config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, + // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, + // Account site endpoints {config.EndpointUnregisterSite, callMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, } diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index 0d5e763010..a0782c3126 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -43,6 +43,10 @@ type Panel struct { sessions *SessionManager } +const ( + pathParameterName = "path" +) + func (panel *Panel) initialize(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) error { if name == "" { return errors.Errorf("no name provided") @@ -120,7 +124,10 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, dataProvider return errors.Wrap(err, "an error occurred while handling sessions") } - tplName := panel.getFullTemplateName(panel.provider.GetActiveTemplate(session)) + // Get the path query parameter; the panel provider may use this to determine the template to use + path := r.URL.Query().Get(pathParameterName) + + tplName := panel.getFullTemplateName(panel.provider.GetActiveTemplate(session, path)) tpl, ok := panel.templates[tplName] if !ok { return errors.Errorf("template %v not found", tplName) diff --git a/pkg/siteacc/html/provider.go b/pkg/siteacc/html/provider.go index e6142bc784..2dc5afb5d2 100644 --- a/pkg/siteacc/html/provider.go +++ b/pkg/siteacc/html/provider.go @@ -21,7 +21,7 @@ package html // PanelProvider handles general panel tasks. type PanelProvider interface { // GetActiveTemplate returns the name of the active template. - GetActiveTemplate(*Session) string + GetActiveTemplate(*Session, string) string } type PanelDataProvider = func(*Session) interface{} From 6f89beb299b3955efa6a01a928e8ed806b216df2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 7 Jul 2021 15:03:35 +0200 Subject: [PATCH 14/60] Add user authentication endpoint --- pkg/siteacc/account/login/template.go | 2 +- pkg/siteacc/config/endpoints.go | 3 +++ pkg/siteacc/endpoints.go | 16 ++++++++++++++++ pkg/siteacc/manager.go | 21 ++++++++++++++++++--- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 19cc7bb069..c2c3e9247b 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -96,7 +96,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index 44fe68bb29..d8c12bfb43 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -43,6 +43,9 @@ const ( // EndpointRemove is the endpoint path for account removal. EndpointRemove = "/remove" + // EndpointAuthenticate is the endpoint path for user authentication. + EndpointAuthenticate = "/authenticate" + // EndpointAuthorize is the endpoint path for account authorization. EndpointAuthorize = "/authorize" // EndpointIsAuthorized is the endpoint path used to check the authorization status of an account. diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 781194f7f6..62142553fa 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -70,6 +70,8 @@ func getEndpoints() []endpoint { {config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, + // Authentication endpoints + {config.EndpointAuthenticate, callMethodEndpoint, createMethodCallbacks(nil, handleAuthenticate), true}, // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, @@ -270,6 +272,20 @@ func handleUnregisterSite(mngr *Manager, values url.Values, body []byte) (interf return nil, nil } +func handleAuthenticate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Authenticate the user through the account manager + if _, err := mngr.AuthenticateUser(account.Email, account.Password.Value); err != nil { + return nil, errors.Wrap(err, "unable to authenticate user") + } + + return nil, nil +} + func handleAuthorize(mngr *Manager, values url.Values, body []byte) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager.go index 8fe8252cfb..a7d73336f0 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager.go @@ -24,7 +24,7 @@ import ( "sync" "time" - "github.com/cs3org/reva/pkg/siteacc/account" + accpanel "github.com/cs3org/reva/pkg/siteacc/account" "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" @@ -55,7 +55,7 @@ type Manager struct { storage data.Storage adminPanel *admin.Panel - accountPanel *account.Panel + accountPanel *accpanel.Panel smtp *smtpclient.SMTPCredentials @@ -91,7 +91,7 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) } // Create the account panel - if pnl, err := account.NewPanel(conf, log); err == nil { + if pnl, err := accpanel.NewPanel(conf, log); err == nil { mngr.accountPanel = pnl } else { return errors.Wrap(err, "unable to create the account panel") @@ -350,6 +350,21 @@ func (mngr *Manager) CloneAccounts(erasePasswords bool) data.Accounts { return clones } +// AuthenticateUser tries to authenticate a given username/password pair. On success, the corresponding user account is returned. +func (mngr *Manager) AuthenticateUser(name, password string) (*data.Account, error) { + account, err := mngr.findAccount(FindByEmail, name) + if err != nil { + return nil, errors.Wrap(err, "no account with the specified email exists") + } + + // Verify the provided password + if !account.Password.Compare(password) { + return nil, errors.Errorf("invalid password") + } + + return account, nil +} + func newManager(conf *config.Configuration, log *zerolog.Logger) (*Manager, error) { mngr := &Manager{} if err := mngr.initialize(conf, log); err != nil { From a94675fadc8a96109ae03314d21898cfec27f0da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Thu, 8 Jul 2021 14:51:06 +0200 Subject: [PATCH 15/60] Restructure session management and support login --- pkg/mentix/utils/network/network.go | 3 +- pkg/siteacc/account/login/template.go | 5 +- pkg/siteacc/account/panel.go | 13 ++- pkg/siteacc/admin/panel.go | 9 +- pkg/siteacc/config/endpoints.go | 6 +- pkg/siteacc/endpoints.go | 94 ++++++++++--------- pkg/siteacc/html/panel.go | 23 ++--- pkg/siteacc/html/provider.go | 7 ++ pkg/siteacc/html/session.go | 7 +- pkg/siteacc/html/sessionmanager.go | 39 ++++---- .../{manager.go => manager/accmanager.go} | 86 ++++------------- pkg/siteacc/manager/usersmanager.go | 83 ++++++++++++++++ pkg/siteacc/siteacc.go | 76 ++++++++++++++- 13 files changed, 288 insertions(+), 163 deletions(-) rename pkg/siteacc/{manager.go => manager/accmanager.go} (75%) create mode 100644 pkg/siteacc/manager/usersmanager.go diff --git a/pkg/mentix/utils/network/network.go b/pkg/mentix/utils/network/network.go index 6185ce8fde..80c3c96368 100644 --- a/pkg/mentix/utils/network/network.go +++ b/pkg/mentix/utils/network/network.go @@ -79,11 +79,12 @@ func queryEndpoint(method string, endpointURL *url.URL, auth *BasicAuth, checkSt if err != nil { return nil, fmt.Errorf("unable to get data from endpoint: %v", err) } + defer resp.Body.Close() + if checkStatus && resp.StatusCode >= 400 { return nil, fmt.Errorf("invalid response received: %v", resp.Status) } - defer resp.Body.Close() body, _ := ioutil.ReadAll(resp.Body) return body, nil } diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index c2c3e9247b..d8823cbe0a 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -48,8 +48,7 @@ function handleAction(action) { xhr.onreadystatechange = function() { if (this.readyState === XMLHttpRequest.DONE) { if (this.status == 200) { - setState(STATE_SUCCESS, "Your login was successful! Redirecting..."); - window.location.replace("?manage"); + setState(STATE_SUCCESS, "Your login was successful!"); } else { var resp = JSON.parse(this.responseText); setState(STATE_ERROR, "An error occurred while trying to login your account:
    " + resp.error + "", "form", null, true); @@ -96,7 +95,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index b66da7a3cf..494f9d9924 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -78,15 +78,24 @@ func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string return template } +// PreExecute is called before the actual template is being executed. +func (panel *Panel) PreExecute(*html.Session, string, *http.Request) error { + return nil +} + // Execute generates the HTTP output of the form and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request) error { +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error { dataProvider := func(*html.Session) interface{} { type TemplateData struct { } return TemplateData{} } - return panel.htmlPanel.Execute(w, r, dataProvider) + return panel.htmlPanel.Execute(w, r, session, dataProvider) +} + +func (panel *Panel) executeLogin(session *html.Session, r *http.Request) error { + return nil } // NewPanel creates a new account panel. diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index 9e8bd50042..1389ccb052 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -86,8 +86,13 @@ func (panel *Panel) GetContentBody() string { return tplBody } +// PreExecute is called before the actual template is being executed. +func (panel *Panel) PreExecute(*html.Session, string, *http.Request) error { + return nil +} + // Execute generates the HTTP output of the htmlPanel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, accounts *data.Accounts) error { +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session, accounts *data.Accounts) error { dataProvider := func(*html.Session) interface{} { type TemplateData struct { Accounts *data.Accounts @@ -97,7 +102,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, accounts *da Accounts: accounts, } } - return panel.htmlPanel.Execute(w, r, dataProvider) + return panel.htmlPanel.Execute(w, r, session, dataProvider) } // NewPanel creates a new administration panel. diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index d8c12bfb43..56d87fe8e2 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -43,8 +43,10 @@ const ( // EndpointRemove is the endpoint path for account removal. EndpointRemove = "/remove" - // EndpointAuthenticate is the endpoint path for user authentication. - EndpointAuthenticate = "/authenticate" + // EndpointLogin is the endpoint path for (internal) user login. + EndpointLogin = "/login" + // EndpointLogout is the endpoint path for (internal) user logout. + EndpointLogout = "/logout" // EndpointAuthorize is the endpoint path for account authorization. EndpointAuthorize = "/authorize" diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 62142553fa..74d010bde3 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -29,14 +29,15 @@ import ( "github.com/cs3org/reva/pkg/mentix/key" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" ) -type methodCallback = func(*Manager, url.Values, []byte) (interface{}, error) +type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error) type endpoint struct { Path string - Handler func(*Manager, endpoint, http.ResponseWriter, *http.Request) + Handler func(*SiteAccounts, endpoint, http.ResponseWriter, *http.Request, *html.Session) MethodCallbacks map[string]methodCallback IsPublic bool } @@ -70,8 +71,9 @@ func getEndpoints() []endpoint { {config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, - // Authentication endpoints - {config.EndpointAuthenticate, callMethodEndpoint, createMethodCallbacks(nil, handleAuthenticate), true}, + // Login endpoints + {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), false}, + {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), false}, // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, @@ -82,21 +84,21 @@ func getEndpoints() []endpoint { return endpoints } -func callAdministrationEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowAdministrationPanel(w, r); err != nil { +func callAdministrationEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) { + if err := siteacc.ShowAdministrationPanel(w, r, session); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the administration panel: %v", err))) } } -func callAccountEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { - if err := mngr.ShowAccountPanel(w, r); err != nil { +func callAccountEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) { + if err := siteacc.ShowAccountPanel(w, r, session); err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(fmt.Sprintf("Unable to show the account panel: %v", err))) } } -func callMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *http.Request) { +func callMethodEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) { // Every request to the accounts service results in a standardized JSON response type Response struct { Success bool `json:"success"` @@ -117,7 +119,7 @@ func callMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *ht if method == r.Method { body, _ := ioutil.ReadAll(r.Body) - if respData, err := cb(mngr, r.URL.Query(), body); err == nil { + if respData, err := cb(siteacc, r.URL.Query(), body, session); err == nil { resp.Success = true resp.Error = "" resp.Data = respData @@ -139,7 +141,7 @@ func callMethodEndpoint(mngr *Manager, ep endpoint, w http.ResponseWriter, r *ht _, _ = w.Write(jsonData) } -func handleGenerateAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleGenerateAPIKey(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { email := values.Get("email") flags := key.FlagDefault @@ -158,7 +160,7 @@ func handleGenerateAPIKey(mngr *Manager, values url.Values, body []byte) (interf return map[string]string{"apiKey": apiKey}, nil } -func handleVerifyAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleVerifyAPIKey(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { apiKey := values.Get("apiKey") email := values.Get("email") @@ -177,7 +179,7 @@ func handleVerifyAPIKey(mngr *Manager, values url.Values, body []byte) (interfac return nil, nil } -func handleAssignAPIKey(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleAssignAPIKey(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err @@ -188,105 +190,111 @@ func handleAssignAPIKey(mngr *Manager, values url.Values, body []byte) (interfac flags |= key.FlagScienceMesh } - // Assign a new API key to the account through the account manager - if err := mngr.AssignAPIKeyToAccount(account, flags); err != nil { + // Assign a new API key to the account through the account accountsManager + if err := siteacc.AccountsManager().AssignAPIKeyToAccount(account, flags); err != nil { return nil, errors.Wrap(err, "unable to assign API key") } return nil, nil } -func handleList(mngr *Manager, values url.Values, body []byte) (interface{}, error) { - return mngr.CloneAccounts(true), nil +func handleList(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + return siteacc.AccountsManager().CloneAccounts(true), nil } -func handleFind(mngr *Manager, values url.Values, body []byte) (interface{}, error) { - account, err := findAccount(mngr, values.Get("by"), values.Get("value")) +func handleFind(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + account, err := findAccount(siteacc, values.Get("by"), values.Get("value")) if err != nil { return nil, err } return map[string]interface{}{"account": account.Clone(true)}, nil } -func handleCreate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleCreate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err } - // Create a new account through the account manager - if err := mngr.CreateAccount(account); err != nil { + // Create a new account through the account accountsManager + if err := siteacc.AccountsManager().CreateAccount(account); err != nil { return nil, errors.Wrap(err, "unable to create account") } return nil, nil } -func handleUpdate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err } - // Update the account through the account manager; only the basic data of an account can be updated through this requestHandler - if err := mngr.UpdateAccount(account, false); err != nil { + // Update the account through the account accountsManager; only the basic data of an account can be updated through this requestHandler + if err := siteacc.AccountsManager().UpdateAccount(account, false); err != nil { return nil, errors.Wrap(err, "unable to update account") } return nil, nil } -func handleRemove(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err } - // Remove the account through the account manager - if err := mngr.RemoveAccount(account); err != nil { + // Remove the account through the account accountsManager + if err := siteacc.AccountsManager().RemoveAccount(account); err != nil { return nil, errors.Wrap(err, "unable to remove account") } return nil, nil } -func handleIsAuthorized(mngr *Manager, values url.Values, body []byte) (interface{}, error) { - account, err := findAccount(mngr, values.Get("by"), values.Get("value")) +func handleIsAuthorized(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + account, err := findAccount(siteacc, values.Get("by"), values.Get("value")) if err != nil { return nil, err } return account.Data.Authorized, nil } -func handleUnregisterSite(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleUnregisterSite(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err } - // Unregister the account's site through the account manager - if err := mngr.UnregisterAccountSite(account); err != nil { + // Unregister the account's site through the account accountsManager + if err := siteacc.AccountsManager().UnregisterAccountSite(account); err != nil { return nil, errors.Wrap(err, "unable to unregister the site of the given account") } return nil, nil } -func handleAuthenticate(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err } - // Authenticate the user through the account manager - if _, err := mngr.AuthenticateUser(account.Email, account.Password.Value); err != nil { - return nil, errors.Wrap(err, "unable to authenticate user") + // Login the user through the users manager + if err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, session); err != nil { + return nil, errors.Wrap(err, "unable to login user") } return nil, nil } -func handleAuthorize(mngr *Manager, values url.Values, body []byte) (interface{}, error) { +func handleLogout(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + // Logout the user through the users manager + siteacc.UsersManager().LogoutUser(session) + return nil, nil +} + +func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err @@ -305,8 +313,8 @@ func handleAuthorize(mngr *Manager, values url.Values, body []byte) (interface{} return nil, errors.Errorf("unsupported authorization status %v", val[0]) } - // Authorize the account through the account manager - if err := mngr.AuthorizeAccount(account, authorize); err != nil { + // Authorize the account through the account accountsManager + if err := siteacc.AccountsManager().AuthorizeAccount(account, authorize); err != nil { return nil, errors.Wrap(err, "unable to (un)authorize account") } } else { @@ -324,13 +332,13 @@ func unmarshalRequestData(body []byte) (*data.Account, error) { return account, nil } -func findAccount(mngr *Manager, by string, value string) (*data.Account, error) { +func findAccount(siteacc *SiteAccounts, by string, value string) (*data.Account, error) { if len(by) == 0 && len(value) == 0 { return nil, errors.Errorf("missing search criteria") } - // Find the account using the account manager - account, err := mngr.FindAccount(by, value) + // Find the account using the account accountsManager + account, err := siteacc.AccountsManager().FindAccount(by, value) if err != nil { return nil, errors.Wrap(err, "user not found") } diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index a0782c3126..8b120ed177 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -40,7 +40,6 @@ type Panel struct { provider PanelProvider templates map[TemplateID]*template.Template - sessions *SessionManager } const ( @@ -71,13 +70,6 @@ func (panel *Panel) initialize(name string, provider PanelProvider, conf *config // Create space for the panel templates panel.templates = make(map[string]*template.Template, 5) - // Create the session mananger - sessions, err := NewSessionManager(name+"_session", conf, log) - if err != nil { - return errors.Wrap(err, "error while creating the session manager") - } - panel.sessions = sessions - return nil } @@ -118,16 +110,12 @@ func (panel *Panel) AddTemplate(name TemplateID, provider ContentProvider) error } // Execute generates the HTTP output of the panel and writes it to the response writer. -func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, dataProvider PanelDataProvider) error { - session, err := panel.sessions.HandleRequest(w, r) - if err != nil { - return errors.Wrap(err, "an error occurred while handling sessions") - } - +func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *Session, dataProvider PanelDataProvider) error { // Get the path query parameter; the panel provider may use this to determine the template to use path := r.URL.Query().Get(pathParameterName) - tplName := panel.getFullTemplateName(panel.provider.GetActiveTemplate(session, path)) + actTpl := panel.provider.GetActiveTemplate(session, path) + tplName := panel.getFullTemplateName(actTpl) tpl, ok := panel.templates[tplName] if !ok { return errors.Errorf("template %v not found", tplName) @@ -139,6 +127,11 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, dataProvider data = dataProvider(session) } + // Perform the pre-execution phase in which the panel provider can intercept the actual execution + if err := panel.provider.PreExecute(session, actTpl, r); err != nil { + return errors.Wrapf(err, "pre-execution of template %v failed", tplName) + } + return tpl.Execute(w, data) } diff --git a/pkg/siteacc/html/provider.go b/pkg/siteacc/html/provider.go index 2dc5afb5d2..0f70ad521d 100644 --- a/pkg/siteacc/html/provider.go +++ b/pkg/siteacc/html/provider.go @@ -18,10 +18,17 @@ package html +import ( + "net/http" +) + // PanelProvider handles general panel tasks. type PanelProvider interface { // GetActiveTemplate returns the name of the active template. GetActiveTemplate(*Session, string) string + + // PreExecute is called before the actual template is being executed. + PreExecute(*Session, string, *http.Request) error } type PanelDataProvider = func(*Session) interface{} diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index c9f13be389..03e34e9f7b 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -22,6 +22,7 @@ import ( "net/http" "time" + "github.com/cs3org/reva/pkg/siteacc/data" "github.com/google/uuid" "github.com/pkg/errors" ) @@ -32,6 +33,8 @@ type Session struct { RemoteAddress string Expires time.Time + LoggedInUser *data.Account + Data map[string]interface{} sessionCookieName string @@ -69,7 +72,7 @@ func (sess *Session) HasExpired() bool { } // NewSession creates a new session, giving it a random ID. -func NewSession(name string, timeout time.Duration, r *http.Request) (*Session, error) { +func NewSession(name string, timeout time.Duration, r *http.Request) *Session { session := &Session{ ID: uuid.NewString(), RemoteAddress: r.RemoteAddr, @@ -77,5 +80,5 @@ func NewSession(name string, timeout time.Duration, r *http.Request) (*Session, Data: nil, sessionCookieName: name, } - return session, nil + return session } diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 6674718d89..52b2fed1fa 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -58,53 +58,51 @@ func (mngr *SessionManager) initialize(name string, conf *config.Configuration, return nil } -// HandleRequest performs all session-related tasks during an HTML request. +// HandleRequest performs all session-related tasks during an HTML request. Always returns a valid session object. func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request) (*Session, error) { var session *Session + var sessionErr error // Try to get the session ID from the request; if none has been set yet, a new one will be assigned cookie, err := r.Cookie(mngr.sessionName) if err == nil { session = mngr.findSession(cookie.Value) if session != nil { - // Verify the request against the session: If it is invalid, return an error; if the session has expired, migrate to a new one; otherwise, just continue + // Verify the request against the session: If it is invalid, set an error; if the session has expired, migrate to a new one; otherwise, just continue if err := session.VerifyRequest(r); err == nil { if session.HasExpired() { session, err = mngr.migrateSession(session, r) if err != nil { - return nil, errors.Wrap(err, "unable to migrate to a new session") + session = nil + sessionErr = errors.Wrap(err, "unable to migrate to a new session") } } } else { - return nil, errors.Wrap(err, "invalid session") + session = nil + sessionErr = errors.Wrap(err, "invalid session") } } } else if err != http.ErrNoCookie { - // The session cookie exists but seems to be invalid, so return an error - return nil, errors.Wrap(err, "unable to get the session ID from the client") + // The session cookie exists but seems to be invalid, so set an error + session = nil + sessionErr = errors.Wrap(err, "unable to get the session ID from the client") } if session == nil { - // No session found for the client, so create a new one - session, err = mngr.createSession(r) - if err != nil { - return nil, errors.Wrap(err, "unable to assign a new session to the client") - } + // No session found for the client, so create a new one; this will always succeed + session = mngr.createSession(r) } // Store the session ID on the client side session.Save(w) - return session, nil + return session, sessionErr } -func (mngr *SessionManager) createSession(r *http.Request) (*Session, error) { - session, err := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r) - if err != nil { - return nil, errors.Wrap(err, "unable to create a new session") - } +func (mngr *SessionManager) createSession(r *http.Request) *Session { + session := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r) mngr.sessions[session.ID] = session - return session, nil + return session } func (mngr *SessionManager) findSession(id string) *Session { @@ -115,10 +113,7 @@ func (mngr *SessionManager) findSession(id string) *Session { } func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (*Session, error) { - sessionNew, err := mngr.createSession(r) - if err != nil { - return nil, err - } + sessionNew := mngr.createSession(r) // Carry over the old session data, thus preserving the existing session sessionNew.Data = session.Data diff --git a/pkg/siteacc/manager.go b/pkg/siteacc/manager/accmanager.go similarity index 75% rename from pkg/siteacc/manager.go rename to pkg/siteacc/manager/accmanager.go index a7d73336f0..442033c50b 100644 --- a/pkg/siteacc/manager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -16,16 +16,13 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package siteacc +package manager import ( - "net/http" "strings" "sync" "time" - accpanel "github.com/cs3org/reva/pkg/siteacc/account" - "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/email" @@ -46,23 +43,20 @@ const ( FindBySiteID = "siteid" ) -// Manager is responsible for all site account related tasks. -type Manager struct { +// AccountsManager is responsible for all site account related tasks. +type AccountsManager struct { conf *config.Configuration log *zerolog.Logger accounts data.Accounts storage data.Storage - adminPanel *admin.Panel - accountPanel *accpanel.Panel - smtp *smtpclient.SMTPCredentials mutex sync.RWMutex } -func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (mngr *AccountsManager) initialize(conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -83,20 +77,6 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create accounts storage") } - // Create the admin panel - if pnl, err := admin.NewPanel(conf, log); err == nil { - mngr.adminPanel = pnl - } else { - return errors.Wrap(err, "unable to create the administration panel") - } - - // Create the account panel - if pnl, err := accpanel.NewPanel(conf, log); err == nil { - mngr.accountPanel = pnl - } else { - return errors.Wrap(err, "unable to create the account panel") - } - // Create the SMTP client if conf.SMTP != nil { mngr.smtp = smtpclient.NewSMTPCredentials(conf.SMTP) @@ -105,7 +85,7 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger) return nil } -func (mngr *Manager) createStorage(driver string) (data.Storage, error) { +func (mngr *AccountsManager) createStorage(driver string) (data.Storage, error) { if driver == "file" { return data.NewFileStorage(mngr.conf, mngr.log) } @@ -113,7 +93,7 @@ func (mngr *Manager) createStorage(driver string) (data.Storage, error) { return nil, errors.Errorf("unknown storage driver %v", driver) } -func (mngr *Manager) readAllAccounts() { +func (mngr *AccountsManager) readAllAccounts() { if accounts, err := mngr.storage.ReadAll(); err == nil { mngr.accounts = *accounts } else { @@ -122,14 +102,14 @@ func (mngr *Manager) readAllAccounts() { } } -func (mngr *Manager) writeAllAccounts() { +func (mngr *AccountsManager) writeAllAccounts() { if err := mngr.storage.WriteAll(&mngr.accounts); err != nil { // Just warn when not being able to write accounts mngr.log.Warn().Err(err).Msg("error while writing accounts") } } -func (mngr *Manager) findAccount(by string, value string) (*data.Account, error) { +func (mngr *AccountsManager) findAccount(by string, value string) (*data.Account, error) { if len(value) == 0 { return nil, errors.Errorf("no search value specified") } @@ -156,7 +136,7 @@ func (mngr *Manager) findAccount(by string, value string) (*data.Account, error) return nil, errors.Errorf("no user found matching the specified criteria") } -func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account { +func (mngr *AccountsManager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account { for _, account := range mngr.accounts { if predicate(account) { return account @@ -165,20 +145,8 @@ func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) return nil } -// ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer. -func (mngr *Manager) ShowAdministrationPanel(w http.ResponseWriter, r *http.Request) error { - // The adminPanel only shows the stored accounts and offers actions through links, so let it use cloned data - accounts := mngr.CloneAccounts(true) - return mngr.adminPanel.Execute(w, r, &accounts) -} - -// ShowAccountPanel writes the account panel HTTP output directly to the response writer. -func (mngr *Manager) ShowAccountPanel(w http.ResponseWriter, r *http.Request) error { - return mngr.accountPanel.Execute(w, r) -} - // CreateAccount creates a new account; if an account with the same email address already exists, an error is returned. -func (mngr *Manager) CreateAccount(accountData *data.Account) error { +func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -201,7 +169,7 @@ func (mngr *Manager) CreateAccount(accountData *data.Account) error { } // UpdateAccount updates the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) error { +func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, copyData bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -223,7 +191,7 @@ func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) err } // FindAccount is used to find an account by various criteria. -func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) { +func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account, error) { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -238,7 +206,7 @@ func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) } // AuthorizeAccount sets the authorization status of the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool) error { +func (mngr *AccountsManager) AuthorizeAccount(accountData *data.Account, authorized bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -261,7 +229,7 @@ func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool } // AssignAPIKeyToAccount is used to assign a new API key to the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int) error { +func (mngr *AccountsManager) AssignAPIKeyToAccount(accountData *data.Account, flags int) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -298,7 +266,7 @@ func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int) } // UnregisterAccountSite unregisters the site associated with the given account. -func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { +func (mngr *AccountsManager) UnregisterAccountSite(accountData *data.Account) error { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -321,7 +289,7 @@ func (mngr *Manager) UnregisterAccountSite(accountData *data.Account) error { } // RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned. -func (mngr *Manager) RemoveAccount(accountData *data.Account) error { +func (mngr *AccountsManager) RemoveAccount(accountData *data.Account) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -338,7 +306,7 @@ func (mngr *Manager) RemoveAccount(accountData *data.Account) error { } // CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. -func (mngr *Manager) CloneAccounts(erasePasswords bool) data.Accounts { +func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -350,23 +318,9 @@ func (mngr *Manager) CloneAccounts(erasePasswords bool) data.Accounts { return clones } -// AuthenticateUser tries to authenticate a given username/password pair. On success, the corresponding user account is returned. -func (mngr *Manager) AuthenticateUser(name, password string) (*data.Account, error) { - account, err := mngr.findAccount(FindByEmail, name) - if err != nil { - return nil, errors.Wrap(err, "no account with the specified email exists") - } - - // Verify the provided password - if !account.Password.Compare(password) { - return nil, errors.Errorf("invalid password") - } - - return account, nil -} - -func newManager(conf *config.Configuration, log *zerolog.Logger) (*Manager, error) { - mngr := &Manager{} +// NewAccountsManager creates a new accounts manager instance. +func NewAccountsManager(conf *config.Configuration, log *zerolog.Logger) (*AccountsManager, error) { + mngr := &AccountsManager{} if err := mngr.initialize(conf, log); err != nil { return nil, errors.Wrapf(err, "unable to initialize the accounts manager") } diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go new file mode 100644 index 0000000000..04fbe16d7f --- /dev/null +++ b/pkg/siteacc/manager/usersmanager.go @@ -0,0 +1,83 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package manager + +import ( + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/html" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// UsersManager is responsible for managing logged in users through session objects. +type UsersManager struct { + conf *config.Configuration + log *zerolog.Logger + + accountsManager *AccountsManager +} + +func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) error { + if conf == nil { + return errors.Errorf("no configuration provided") + } + mngr.conf = conf + + if log == nil { + return errors.Errorf("no logger provided") + } + mngr.log = log + + if accountsManager == nil { + return errors.Errorf("no accounts manager provided") + } + mngr.accountsManager = accountsManager + + return nil +} + +// LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session. +func (mngr *UsersManager) LoginUser(name, password string, session *html.Session) error { + account, err := mngr.accountsManager.FindAccount(FindByEmail, name) + if err != nil { + return errors.Wrap(err, "no account with the specified email exists") + } + + // Verify the provided password + if !account.Password.Compare(password) { + return errors.Errorf("invalid password") + } + + return nil +} + +// LogoutUser logs the current user out. +func (mngr *UsersManager) LogoutUser(session *html.Session) { + // Just unset the user account stored in the session + session.LoggedInUser = nil +} + +// NewUsersManager creates a new users manager instance. +func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) (*UsersManager, error) { + mngr := &UsersManager{} + if err := mngr.initialize(conf, log, accountsManager); err != nil { + return nil, errors.Wrapf(err, "unable to initialize the users manager") + } + return mngr, nil +} diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index c0223f673e..0ed120ef0c 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -22,7 +22,11 @@ import ( "fmt" "net/http" + accpanel "github.com/cs3org/reva/pkg/siteacc/account" + "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/html" + "github.com/cs3org/reva/pkg/siteacc/manager" "github.com/pkg/errors" "github.com/rs/zerolog" ) @@ -32,7 +36,13 @@ type SiteAccounts struct { conf *config.Configuration log *zerolog.Logger - manager *Manager + sessions *html.SessionManager + + accountsManager *manager.AccountsManager + usersManager *manager.UsersManager + + adminPanel *admin.Panel + accountPanel *accpanel.Panel } func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog.Logger) error { @@ -46,12 +56,40 @@ func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog } siteacc.log = log + // Create the session mananger + sessions, err := html.NewSessionManager("siteacc_session", conf, log) + if err != nil { + return errors.Wrap(err, "error while creating the session manager") + } + siteacc.sessions = sessions + // Create the accounts manager instance - mngr, err := newManager(conf, log) + amngr, err := manager.NewAccountsManager(conf, log) if err != nil { - return errors.Wrap(err, "error creating the site accounts manager") + return errors.Wrap(err, "error creating the accounts manager") + } + siteacc.accountsManager = amngr + + // Create the users manager instance + umngr, err := manager.NewUsersManager(conf, log, siteacc.accountsManager) + if err != nil { + return errors.Wrap(err, "error creating the users manager") + } + siteacc.usersManager = umngr + + // Create the admin panel + if pnl, err := admin.NewPanel(conf, log); err == nil { + siteacc.adminPanel = pnl + } else { + return errors.Wrap(err, "unable to create the administration panel") + } + + // Create the account panel + if pnl, err := accpanel.NewPanel(conf, log); err == nil { + siteacc.accountPanel = pnl + } else { + return errors.Wrap(err, "unable to create the account panel") } - siteacc.manager = mngr return nil } @@ -61,10 +99,16 @@ func (siteacc *SiteAccounts) RequestHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() + // Get the active session for the request (or create a new one); a valid session object will always be returned + session, err := siteacc.sessions.HandleRequest(w, r) + if err != nil { + siteacc.log.Err(err).Msg("an error occurred while handling sessions") + } + epHandled := false for _, ep := range getEndpoints() { if ep.Path == r.URL.Path { - ep.Handler(siteacc.manager, ep, w, r) + ep.Handler(siteacc, ep, w, r, session) epHandled = true break } @@ -77,6 +121,28 @@ func (siteacc *SiteAccounts) RequestHandler() http.Handler { }) } +// ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer. +func (siteacc *SiteAccounts) ShowAdministrationPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error { + // The admin panel only shows the stored accounts and offers actions through links, so let it use cloned data + accounts := siteacc.accountsManager.CloneAccounts(true) + return siteacc.adminPanel.Execute(w, r, session, &accounts) +} + +// ShowAccountPanel writes the account panel HTTP output directly to the response writer. +func (siteacc *SiteAccounts) ShowAccountPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error { + return siteacc.accountPanel.Execute(w, r, session) +} + +// AccountsManager returns the central accounts manager instance. +func (siteacc *SiteAccounts) AccountsManager() *manager.AccountsManager { + return siteacc.accountsManager +} + +// UsersManager returns the central users manager instance. +func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager { + return siteacc.usersManager +} + func (siteacc *SiteAccounts) GetPublicEndpoints() []string { // TODO: REMOVE! return []string{"/"} From 5f556a3c9b7a6c6c19ba16fde7ef9b710a80e221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Thu, 8 Jul 2021 16:00:46 +0200 Subject: [PATCH 16/60] Implement user login --- pkg/siteacc/account/login/login.go | 4 +- pkg/siteacc/account/login/template.go | 3 +- pkg/siteacc/account/manage/manage.go | 50 +++++++++++++++++ pkg/siteacc/account/manage/template.go | 43 ++++++++++++++ pkg/siteacc/account/panel.go | 56 +++++++++++++++---- .../account/registration/registration.go | 4 +- pkg/siteacc/admin/panel.go | 4 +- pkg/siteacc/html/panel.go | 6 +- pkg/siteacc/html/provider.go | 9 ++- pkg/siteacc/html/sessionmanager.go | 3 +- pkg/siteacc/manager/usersmanager.go | 2 + 11 files changed, 162 insertions(+), 22 deletions(-) create mode 100644 pkg/siteacc/account/manage/manage.go create mode 100644 pkg/siteacc/account/manage/template.go diff --git a/pkg/siteacc/account/login/login.go b/pkg/siteacc/account/login/login.go index 237ff5dcb0..7a31072124 100644 --- a/pkg/siteacc/account/login/login.go +++ b/pkg/siteacc/account/login/login.go @@ -24,12 +24,12 @@ type PanelTemplate struct { html.ContentProvider } -// GetTitle returns the title of the htmlPanel. +// GetTitle returns the title of the panel. func (template *PanelTemplate) GetTitle() string { return "ScienceMesh Account Login" } -// GetCaption returns the caption which is displayed on the htmlPanel. +// GetCaption returns the caption which is displayed on the panel. func (template *PanelTemplate) GetCaption() string { return "Login to your ScienceMesh Account!" } diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index d8823cbe0a..0ca5b6916e 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -48,7 +48,8 @@ function handleAction(action) { xhr.onreadystatechange = function() { if (this.readyState === XMLHttpRequest.DONE) { if (this.status == 200) { - setState(STATE_SUCCESS, "Your login was successful!"); + setState(STATE_SUCCESS, "Your login was successful! Redirecting..."); + window.location.replace("?path=manage"); } else { var resp = JSON.parse(this.responseText); setState(STATE_ERROR, "An error occurred while trying to login your account:
    " + resp.error + "", "form", null, true); diff --git a/pkg/siteacc/account/manage/manage.go b/pkg/siteacc/account/manage/manage.go new file mode 100644 index 0000000000..f6c3dd23e3 --- /dev/null +++ b/pkg/siteacc/account/manage/manage.go @@ -0,0 +1,50 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package manage + +import "github.com/cs3org/reva/pkg/siteacc/html" + +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the panel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Account Management" +} + +// GetCaption returns the caption which is displayed on the panel. +func (template *PanelTemplate) GetCaption() string { + return "Welcome to your ScienceMesh Account!" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (template *PanelTemplate) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (template *PanelTemplate) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (template *PanelTemplate) GetContentBody() string { + return tplBody +} diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go new file mode 100644 index 0000000000..a74a2be2f0 --- /dev/null +++ b/pkg/siteacc/account/manage/template.go @@ -0,0 +1,43 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package manage + +const tplJavaScript = ` +` + +const tplStyleSheet = ` +html * { + font-family: arial !important; +} + +.mandatory { + color: red; + font-weight: bold; +} +` + +const tplBody = ` +
    +

    Welcome {{.Account.FirstName}} {{.Account.LastName}}! Here you can manage your ScienceMesh Account.

    +
    +
     
    +
    + More to come... +
    +` diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 494f9d9924..062e404861 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -20,10 +20,13 @@ package account import ( "net/http" + "net/url" "github.com/cs3org/reva/pkg/siteacc/account/login" + "github.com/cs3org/reva/pkg/siteacc/account/manage" "github.com/cs3org/reva/pkg/siteacc/account/registration" "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" "github.com/rs/zerolog" @@ -38,6 +41,7 @@ type Panel struct { const ( templateLogin = "login" + templateManage = "manage" templateRegistration = "register" ) @@ -54,6 +58,10 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create the login template") } + if err := panel.htmlPanel.AddTemplate(templateManage, &manage.PanelTemplate{}); err != nil { + return errors.Wrap(err, "unable to create the account management template") + } + if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil { return errors.Wrap(err, "unable to create the registration template") } @@ -63,39 +71,63 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) // GetActiveTemplate returns the name of the active template. func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string { - template := templateLogin // TODO: Check if user is logged in + template := templateLogin - // Invalid paths are just ignored and redirected to the login/main page + // Invalid paths are just ignored and redirected to the login page switch path { - case templateLogin: - case templateRegistration: + case templateLogin, + templateManage, + templateRegistration: template = path } - // TODO: Check path access - // TODO: If user is logged in and path == login, redirect to main - return template } // PreExecute is called before the actual template is being executed. -func (panel *Panel) PreExecute(*html.Session, string, *http.Request) error { - return nil +func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) { + protectedPaths := []string{templateManage} + + if session.LoggedInUser == nil { + // If no user is logged in, redirect protected paths to the login page + for _, protected := range protectedPaths { + if protected == path { + return panel.redirect(templateLogin, w, r), nil + } + } + } else { + // If a user is logged in and tries to login or register again, redirect to the main account page + if path == templateLogin || path == templateRegistration { + return panel.redirect(templateManage, w, r), nil + } + } + + return html.ContinueExecution, nil } // Execute generates the HTTP output of the form and writes it to the response writer. func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error { dataProvider := func(*html.Session) interface{} { type TemplateData struct { + Account *data.Account } - return TemplateData{} + return TemplateData{ + Account: session.LoggedInUser, + } } return panel.htmlPanel.Execute(w, r, session, dataProvider) } -func (panel *Panel) executeLogin(session *html.Session, r *http.Request) error { - return nil +func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult { + // Modify the original request URI by replacing the path parameter + newURI, _ := url.Parse(r.RequestURI) + params := newURI.Query() + params.Del("path") + params.Add("path", path) + newURI.RawQuery = params.Encode() + http.Redirect(w, r, newURI.String(), http.StatusFound) + return html.AbortExecution } // NewPanel creates a new account panel. diff --git a/pkg/siteacc/account/registration/registration.go b/pkg/siteacc/account/registration/registration.go index f8720a7e7b..fc2d728192 100644 --- a/pkg/siteacc/account/registration/registration.go +++ b/pkg/siteacc/account/registration/registration.go @@ -24,12 +24,12 @@ type PanelTemplate struct { html.ContentProvider } -// GetTitle returns the title of the htmlPanel. +// GetTitle returns the title of the panel. func (template *PanelTemplate) GetTitle() string { return "ScienceMesh Account Registration" } -// GetCaption returns the caption which is displayed on the htmlPanel. +// GetCaption returns the caption which is displayed on the panel. func (template *PanelTemplate) GetCaption() string { return "Welcome to the ScienceMesh Account Registration!" } diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index 1389ccb052..7d274d8b76 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -87,8 +87,8 @@ func (panel *Panel) GetContentBody() string { } // PreExecute is called before the actual template is being executed. -func (panel *Panel) PreExecute(*html.Session, string, *http.Request) error { - return nil +func (panel *Panel) PreExecute(*html.Session, string, http.ResponseWriter, *http.Request) (html.ExecutionResult, error) { + return html.ContinueExecution, nil } // Execute generates the HTTP output of the htmlPanel and writes it to the response writer. diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index 8b120ed177..e4d8b4b360 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -128,7 +128,11 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *Ses } // Perform the pre-execution phase in which the panel provider can intercept the actual execution - if err := panel.provider.PreExecute(session, actTpl, r); err != nil { + if state, err := panel.provider.PreExecute(session, actTpl, w, r); err == nil { + if state == AbortExecution { + return nil + } + } else { return errors.Wrapf(err, "pre-execution of template %v failed", tplName) } diff --git a/pkg/siteacc/html/provider.go b/pkg/siteacc/html/provider.go index 0f70ad521d..e3f437229e 100644 --- a/pkg/siteacc/html/provider.go +++ b/pkg/siteacc/html/provider.go @@ -22,13 +22,20 @@ import ( "net/http" ) +const ( + ContinueExecution = true + AbortExecution = false +) + +type ExecutionResult = bool + // PanelProvider handles general panel tasks. type PanelProvider interface { // GetActiveTemplate returns the name of the active template. GetActiveTemplate(*Session, string) string // PreExecute is called before the actual template is being executed. - PreExecute(*Session, string, *http.Request) error + PreExecute(*Session, string, http.ResponseWriter, *http.Request) (ExecutionResult, error) } type PanelDataProvider = func(*Session) interface{} diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 52b2fed1fa..3bcbc7179f 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -115,7 +115,8 @@ func (mngr *SessionManager) findSession(id string) *Session { func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (*Session, error) { sessionNew := mngr.createSession(r) - // Carry over the old session data, thus preserving the existing session + // Carry over the old session information, thus preserving the existing session + sessionNew.LoggedInUser = session.LoggedInUser sessionNew.Data = session.Data // Delete the old session diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 04fbe16d7f..52b679fce1 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -64,6 +64,8 @@ func (mngr *UsersManager) LoginUser(name, password string, session *html.Session return errors.Errorf("invalid password") } + // Store the user account in the session + session.LoggedInUser = account return nil } From b7b87098d71c8f764d68360cb87cd75807fe9c15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 9 Jul 2021 13:27:40 +0200 Subject: [PATCH 17/60] Work on user management template --- pkg/siteacc/account/login/template.go | 5 +- pkg/siteacc/account/manage/template.go | 70 ++++++++++++++++++++++++-- pkg/siteacc/admin/template.go | 2 +- 3 files changed, 69 insertions(+), 8 deletions(-) diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 0ca5b6916e..8406738098 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -79,6 +79,7 @@ html * { } ` +// TODO: Default values raus const tplBody = `

    Login to your ScienceMesh account using the form below.

    @@ -87,9 +88,9 @@ const tplBody = `
    -
    +
    -
    +
    Fields marked with * are mandatory. diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go index a74a2be2f0..f8466cb748 100644 --- a/pkg/siteacc/account/manage/template.go +++ b/pkg/siteacc/account/manage/template.go @@ -19,6 +19,38 @@ package manage const tplJavaScript = ` +function handleEditAccount() { + setState(STATE_STATUS, "No one has implemented this function yet :("); +} + +function handleRequestAccess() { + setState(STATE_STATUS, "No one has implemented this function yet :("); +} + +function handleRequestKey() { + setState(STATE_STATUS, "No one has implemented this function yet :("); +} + +function handleLogout() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "logout"); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + setState(STATE_STATUS, "Logging out..."); + + xhr.onreadystatechange = function() { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status == 200) { + setState(STATE_SUCCESS, "Done! Redirecting..."); + window.location.replace("?path=login"); + } else { + setState(STATE_ERROR, "An error occurred while logging out: " + this.responseText); + } + } + } + + xhr.send(); +} ` const tplStyleSheet = ` @@ -26,18 +58,46 @@ html * { font-family: arial !important; } -.mandatory { - color: red; - font-weight: bold; +.apikey { + font-family: monospace !important; + background: antiquewhite; + border: 1px solid black; + padding: 2px; } ` const tplBody = `
    -

    Welcome {{.Account.FirstName}} {{.Account.LastName}}! Here you can manage your ScienceMesh Account.

    +

    Hello {{.Account.FirstName}} {{.Account.LastName}},

    +

    On this page, you can manage your ScienceMesh user account. This includes editing your personal information, requesting access to the GOCDB and more.

     
    - More to come... + Personal information: +
      +
    • Name: {{.Account.FirstName}} {{.Account.LastName}}
    • +
    • Email: {{.Account.Email}}
    • +
    • Organization/company: {{.Account.Organization}} {{if .Account.Website}}({{.Account.Website}}){{end}}
    • +
    • Role: {{.Account.Role}}
    • + {{if .Account.PhoneNumber}} +
    • Phone: {{.Account.PhoneNumber}}
    • + {{end}} +
    +
    +
    + Account data: +
      +
    • API Key: {{if .Account.Data.APIKey}}{{.Account.Data.APIKey}}{{else}}(no key assigned yet){{end}}
    • +
    +
    +
    + + +   + + + + +
    ` diff --git a/pkg/siteacc/admin/template.go b/pkg/siteacc/admin/template.go index a25dbadce3..aaccdea6d7 100644 --- a/pkg/siteacc/admin/template.go +++ b/pkg/siteacc/admin/template.go @@ -88,7 +88,7 @@ const tplBody = ` {{end}}

    -

    + From 97681563c0108ee4c1a19ed820ec615bfbdd6feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 12 Jul 2021 14:40:27 +0200 Subject: [PATCH 18/60] Add account editing --- pkg/siteacc/account/edit/edit.go | 50 ++++++++ pkg/siteacc/account/edit/template.go | 161 +++++++++++++++++++++++++ pkg/siteacc/account/manage/manage.go | 2 +- pkg/siteacc/account/manage/template.go | 3 +- pkg/siteacc/account/panel.go | 23 ++-- pkg/siteacc/data/account.go | 11 +- pkg/siteacc/endpoints.go | 31 +++-- pkg/siteacc/html/session.go | 8 +- pkg/siteacc/html/sessionmanager.go | 2 + pkg/siteacc/manager/accmanager.go | 4 +- 10 files changed, 270 insertions(+), 25 deletions(-) create mode 100644 pkg/siteacc/account/edit/edit.go create mode 100644 pkg/siteacc/account/edit/template.go diff --git a/pkg/siteacc/account/edit/edit.go b/pkg/siteacc/account/edit/edit.go new file mode 100644 index 0000000000..501005e488 --- /dev/null +++ b/pkg/siteacc/account/edit/edit.go @@ -0,0 +1,50 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package edit + +import "github.com/cs3org/reva/pkg/siteacc/html" + +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the panel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Account" +} + +// GetCaption returns the caption which is displayed on the panel. +func (template *PanelTemplate) GetCaption() string { + return "Edit your ScienceMesh Account!" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (template *PanelTemplate) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (template *PanelTemplate) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (template *PanelTemplate) GetContentBody() string { + return tplBody +} diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go new file mode 100644 index 0000000000..4ad6aea47f --- /dev/null +++ b/pkg/siteacc/account/edit/template.go @@ -0,0 +1,161 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package edit + +const tplJavaScript = ` +function verifyForm(formData) { + if (formData.get("fname") == "") { + setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true); + return false; + } + + if (formData.get("lname") == "") { + setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true); + return false; + } + + if (formData.get("organization") == "") { + setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true); + return false; + } + + if (formData.get("role") == "") { + setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true); + return false; + } + + if (formData.get("password") != "") { + if (formData.get("password2") == "") { + setState(STATE_ERROR, "Please confirm your new password.", "form", "password2", true); + return false; + } + + if (formData.get("password") != formData.get("password2")) { + setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true); + return false; + } + } + + return true; +} + +function handleAction(action) { + const formData = new FormData(document.querySelector("form")); + if (!verifyForm(formData)) { + return; + } + + setState(STATE_STATUS, "Updating account... this should only take a moment.", "form", null, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", action); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + xhr.onreadystatechange = function() { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status == 200) { + setState(STATE_SUCCESS, "Your account was successfully updated!", "form", null, true); + } else { + var resp = JSON.parse(this.responseText); + setState(STATE_ERROR, "An error occurred while trying to update your account:
    " + resp.error + "", "form", null, true); + } + } + } + + var postData = { + "firstName": formData.get("fname"), + "lastName": formData.get("lname"), + "organization": formData.get("organization"), + "website": formData.get("website"), + "role": formData.get("role"), + "phoneNumber": formData.get("phone"), + "password": { + "value": formData.get("password") + } + }; + + xhr.send(JSON.stringify(postData)); +} +` + +const tplStyleSheet = ` +html * { + font-family: arial !important; +} + +.mandatory { + color: red; + font-weight: bold; +} +` + +const tplBody = ` +
    +

    Edit your ScienceMesh account information below.

    +

    Please note that you cannot modify your email address using this form.

    +
    +
     
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +
    +
    + +
     
    + +
    If you want to change your password, fill out the fields below. Otherwise, leave them empty to keep your current one.
    +
    +
    +
    +
    + +
    + The password must fulfil the following criteria: +
      +
    • Must be at least 8 characters long
    • +
    • Must contain at least 1 lowercase letter
    • +
    • Must contain at least 1 uppercase letter
    • +
    • Must contain at least 1 digit
    • +
    +
    + +
    + Fields marked with * are mandatory. +
    +
    + + +
    + +
    +
    +

    Go back to the main account page.

    +
    +` diff --git a/pkg/siteacc/account/manage/manage.go b/pkg/siteacc/account/manage/manage.go index f6c3dd23e3..77a96d9838 100644 --- a/pkg/siteacc/account/manage/manage.go +++ b/pkg/siteacc/account/manage/manage.go @@ -26,7 +26,7 @@ type PanelTemplate struct { // GetTitle returns the title of the panel. func (template *PanelTemplate) GetTitle() string { - return "ScienceMesh Account Management" + return "ScienceMesh Account" } // GetCaption returns the caption which is displayed on the panel. diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go index f8466cb748..3a4e29a4d5 100644 --- a/pkg/siteacc/account/manage/template.go +++ b/pkg/siteacc/account/manage/template.go @@ -20,7 +20,8 @@ package manage const tplJavaScript = ` function handleEditAccount() { - setState(STATE_STATUS, "No one has implemented this function yet :("); + setState(STATE_STATUS, "Redirecting to the account editor..."); + window.location.replace("?path=edit"); } function handleRequestAccess() { diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 062e404861..465be85fa0 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -22,6 +22,7 @@ import ( "net/http" "net/url" + "github.com/cs3org/reva/pkg/siteacc/account/edit" "github.com/cs3org/reva/pkg/siteacc/account/login" "github.com/cs3org/reva/pkg/siteacc/account/manage" "github.com/cs3org/reva/pkg/siteacc/account/registration" @@ -42,6 +43,7 @@ type Panel struct { const ( templateLogin = "login" templateManage = "manage" + templateEdit = "edit" templateRegistration = "register" ) @@ -62,6 +64,10 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create the account management template") } + if err := panel.htmlPanel.AddTemplate(templateEdit, &edit.PanelTemplate{}); err != nil { + return errors.Wrap(err, "unable to create the account editing template") + } + if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil { return errors.Wrap(err, "unable to create the registration template") } @@ -71,14 +77,15 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) // GetActiveTemplate returns the name of the active template. func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string { + validPaths := []string{templateLogin, templateManage, templateEdit, templateRegistration} template := templateLogin - // Invalid paths are just ignored and redirected to the login page - switch path { - case templateLogin, - templateManage, - templateRegistration: - template = path + // Only allow valid template paths; redirect to the login page otherwise + for _, valid := range validPaths { + if valid == path { + template = path + break + } } return template @@ -86,7 +93,7 @@ func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string // PreExecute is called before the actual template is being executed. func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) { - protectedPaths := []string{templateManage} + protectedPaths := []string{templateManage, templateEdit} if session.LoggedInUser == nil { // If no user is logged in, redirect protected paths to the login page @@ -96,8 +103,8 @@ func (panel *Panel) PreExecute(session *html.Session, path string, w http.Respon } } } else { - // If a user is logged in and tries to login or register again, redirect to the main account page if path == templateLogin || path == templateRegistration { + // If a user is logged in and tries to login or register again, redirect to the main account page return panel.redirect(templateManage, w, r), nil } } diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 1d46707c36..ab175659e0 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -66,8 +66,8 @@ func (acc *Account) GetSiteID() key.SiteIdentifier { return "" } -// Update copies the data of the given account to this account; if copyData is true, the account data is copied as well. -func (acc *Account) Update(other *Account, copyData bool) error { +// Update copies the data of the given account to this account. +func (acc *Account) Update(other *Account, setPassword bool, copyData bool) error { if err := other.verify(false); err != nil { return errors.Wrap(err, "unable to update account data") } @@ -80,6 +80,13 @@ func (acc *Account) Update(other *Account, copyData bool) error { acc.Role = other.Role acc.PhoneNumber = other.PhoneNumber + if setPassword && other.Password.Value != "" { + // If a password was provided, use that as the new one + if err := acc.UpdatePassword(other.Password.Value); err != nil { + return errors.Wrap(err, "unable to update account data") + } + } + if copyData { acc.Data = other.Data } diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 74d010bde3..6b712354af 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -33,6 +33,11 @@ import ( "github.com/pkg/errors" ) +const ( + invokerDefault = "" + invokerUser = "user" +) + type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error) type endpoint struct { @@ -190,7 +195,7 @@ func handleAssignAPIKey(siteacc *SiteAccounts, values url.Values, body []byte, s flags |= key.FlagScienceMesh } - // Assign a new API key to the account through the account accountsManager + // Assign a new API key to the account through the accounts manager if err := siteacc.AccountsManager().AssignAPIKeyToAccount(account, flags); err != nil { return nil, errors.Wrap(err, "unable to assign API key") } @@ -216,7 +221,7 @@ func handleCreate(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, err } - // Create a new account through the account accountsManager + // Create a new account through the accounts manager if err := siteacc.AccountsManager().CreateAccount(account); err != nil { return nil, errors.Wrap(err, "unable to create account") } @@ -230,8 +235,18 @@ func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, err } - // Update the account through the account accountsManager; only the basic data of an account can be updated through this requestHandler - if err := siteacc.AccountsManager().UpdateAccount(account, false); err != nil { + invokedByUser := strings.EqualFold(values.Get("invoker"), invokerUser) + if invokedByUser { + // If this endpoint was called by the user, set the account email from the stored session + if session.LoggedInUser == nil { + return nil, errors.Errorf("no user is currently logged in") + } + + account.Email = session.LoggedInUser.Email + } + + // Update the account through the accounts manager; only the basic data of an account can be updated through this requestHandler + if err := siteacc.AccountsManager().UpdateAccount(account, invokedByUser, false); err != nil { return nil, errors.Wrap(err, "unable to update account") } @@ -244,7 +259,7 @@ func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, err } - // Remove the account through the account accountsManager + // Remove the account through the accounts manager if err := siteacc.AccountsManager().RemoveAccount(account); err != nil { return nil, errors.Wrap(err, "unable to remove account") } @@ -266,7 +281,7 @@ func handleUnregisterSite(siteacc *SiteAccounts, values url.Values, body []byte, return nil, err } - // Unregister the account's site through the account accountsManager + // Unregister the account's site through the accounts manager if err := siteacc.AccountsManager().UnregisterAccountSite(account); err != nil { return nil, errors.Wrap(err, "unable to unregister the site of the given account") } @@ -313,7 +328,7 @@ func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, sess return nil, errors.Errorf("unsupported authorization status %v", val[0]) } - // Authorize the account through the account accountsManager + // Authorize the account through the accounts manager if err := siteacc.AccountsManager().AuthorizeAccount(account, authorize); err != nil { return nil, errors.Wrap(err, "unable to (un)authorize account") } @@ -337,7 +352,7 @@ func findAccount(siteacc *SiteAccounts, by string, value string) (*data.Account, return nil, errors.Errorf("missing search criteria") } - // Find the account using the account accountsManager + // Find the account using the accounts manager account, err := siteacc.AccountsManager().FindAccount(by, value) if err != nil { return nil, errors.Wrap(err, "user not found") diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 03e34e9f7b..a69bc23515 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -31,6 +31,7 @@ import ( type Session struct { ID string RemoteAddress string + Timeout time.Duration Expires time.Time LoggedInUser *data.Account @@ -43,9 +44,9 @@ type Session struct { // Save stores the session ID in a cookie using a response writer. func (sess *Session) Save(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ - Name: sess.sessionCookieName, - Value: sess.ID, - Expires: sess.Expires, + Name: sess.sessionCookieName, + Value: sess.ID, + MaxAge: int(sess.Timeout / time.Second), }) } @@ -76,6 +77,7 @@ func NewSession(name string, timeout time.Duration, r *http.Request) *Session { session := &Session{ ID: uuid.NewString(), RemoteAddress: r.RemoteAddr, + Timeout: timeout, Expires: time.Now().Add(timeout), Data: nil, sessionCookieName: name, diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 3bcbc7179f..3a0d63f96c 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -69,6 +69,8 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request session = mngr.findSession(cookie.Value) if session != nil { // Verify the request against the session: If it is invalid, set an error; if the session has expired, migrate to a new one; otherwise, just continue + // TODO: Refresh session; new ID, new timeout + // TODO: Expired sessions are NOT restored but removed! if err := session.VerifyRequest(r); err == nil { if session.HasExpired() { session, err = mngr.migrateSession(session, r) diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 442033c50b..9949deb37f 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -169,7 +169,7 @@ func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error { } // UpdateAccount updates the account identified by the account email; if no such account exists, an error is returned. -func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, copyData bool) error { +func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, setPassword bool, copyData bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -178,7 +178,7 @@ func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, copyData b return errors.Wrap(err, "user to update not found") } - if err := account.Update(accountData, copyData); err == nil { + if err := account.Update(accountData, setPassword, copyData); err == nil { account.DateModified = time.Now() mngr.storage.AccountUpdated(account) From 94a4f6b81c1409445214258c9e76ed1e30022042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 12 Jul 2021 16:21:09 +0200 Subject: [PATCH 19/60] Improve session handling --- pkg/siteacc/html/session.go | 16 ++++++++++++--- pkg/siteacc/html/sessionmanager.go | 33 ++++++++++++++++++++++++++---- pkg/siteacc/siteacc.go | 1 + 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index a69bc23515..517bf66e37 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -31,13 +31,16 @@ import ( type Session struct { ID string RemoteAddress string + CreationTime time.Time Timeout time.Duration - Expires time.Time LoggedInUser *data.Account Data map[string]interface{} + expirationTime time.Time + halflifeTime time.Time + sessionCookieName string } @@ -67,9 +70,14 @@ func (sess *Session) VerifyRequest(r *http.Request) error { return nil } +// HalftimePassed checks whether the session has passed the first half of its lifetime. +func (sess *Session) HalftimePassed() bool { + return time.Now().After(sess.halflifeTime) +} + // HasExpired checks whether the session has reached is timeout. func (sess *Session) HasExpired() bool { - return time.Now().After(sess.Expires) + return time.Now().After(sess.expirationTime) } // NewSession creates a new session, giving it a random ID. @@ -77,9 +85,11 @@ func NewSession(name string, timeout time.Duration, r *http.Request) *Session { session := &Session{ ID: uuid.NewString(), RemoteAddress: r.RemoteAddr, + CreationTime: time.Now(), Timeout: timeout, - Expires: time.Now().Add(timeout), Data: nil, + expirationTime: time.Now().Add(timeout), + halflifeTime: time.Now().Add(timeout / 2), sessionCookieName: name, } return session diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 3a0d63f96c..9332b4ad84 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -20,6 +20,7 @@ package html import ( "net/http" + "sync" "time" "github.com/cs3org/reva/pkg/siteacc/config" @@ -35,6 +36,8 @@ type SessionManager struct { sessions map[string]*Session sessionName string + + mutex sync.Mutex } func (mngr *SessionManager) initialize(name string, conf *config.Configuration, log *zerolog.Logger) error { @@ -60,6 +63,9 @@ func (mngr *SessionManager) initialize(name string, conf *config.Configuration, // HandleRequest performs all session-related tasks during an HTML request. Always returns a valid session object. func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request) (*Session, error) { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + var session *Session var sessionErr error @@ -68,15 +74,17 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request if err == nil { session = mngr.findSession(cookie.Value) if session != nil { - // Verify the request against the session: If it is invalid, set an error; if the session has expired, migrate to a new one; otherwise, just continue - // TODO: Refresh session; new ID, new timeout - // TODO: Expired sessions are NOT restored but removed! + // Verify the request against the session: If it is invalid, set an error; if the session has expired, create a new one; if it has already passed its halftime, migrate to a new one if err := session.VerifyRequest(r); err == nil { if session.HasExpired() { + // The session has expired, so a new one needs to be created + session = nil + } else if session.HalftimePassed() { + // The session has passed its halftime, so migrate it to a new one (makes hijacking session IDs harder) session, err = mngr.migrateSession(session, r) if err != nil { session = nil - sessionErr = errors.Wrap(err, "unable to migrate to a new session") + sessionErr = errors.Wrap(err, "unable to migrate session") } } } else { @@ -101,6 +109,23 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request return session, sessionErr } +// PurgeSessions removes any expired sessions. +func (mngr *SessionManager) PurgeSessions() { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + var expiredSessions []string + for id, session := range mngr.sessions { + if session.HasExpired() { + expiredSessions = append(expiredSessions, id) + } + } + + for _, id := range expiredSessions { + delete(mngr.sessions, id) + } +} + func (mngr *SessionManager) createSession(r *http.Request) *Session { session := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r) mngr.sessions[session.ID] = session diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index 0ed120ef0c..6d9375ed4c 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -100,6 +100,7 @@ func (siteacc *SiteAccounts) RequestHandler() http.Handler { defer r.Body.Close() // Get the active session for the request (or create a new one); a valid session object will always be returned + siteacc.sessions.PurgeSessions() // Remove expired sessions first session, err := siteacc.sessions.HandleRequest(w, r) if err != nil { siteacc.log.Err(err).Msg("an error occurred while handling sessions") From 9a3b1822d48715b1da43e6d638e5764a252a318e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 13 Jul 2021 13:30:53 +0200 Subject: [PATCH 20/60] Enable password reset --- pkg/siteacc/account/login/template.go | 49 +++++++++++++++++++++++---- pkg/siteacc/config/endpoints.go | 2 ++ pkg/siteacc/email/email.go | 11 ++++-- pkg/siteacc/email/template.go | 12 +++++++ pkg/siteacc/endpoints.go | 49 +++++++++++++++++++++++---- pkg/siteacc/manager/accmanager.go | 33 ++++++++++++++---- pkg/siteacc/manager/usersmanager.go | 4 +++ 7 files changed, 138 insertions(+), 22 deletions(-) diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 8406738098..2f1f53c900 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -19,15 +19,17 @@ package login const tplJavaScript = ` -function verifyForm(formData) { +function verifyForm(formData, requirePassword = true) { if (formData.get("email") == "") { setState(STATE_ERROR, "Please enter your email address.", "form", "email", true); return false; } - if (formData.get("password") == "") { - setState(STATE_ERROR, "Please enter your password.", "form", "password", true); - return false; + if (requirePassword) { + if (formData.get("password") == "") { + setState(STATE_ERROR, "Please enter your password.", "form", "password", true); + return false; + } } return true; @@ -66,6 +68,36 @@ function handleAction(action) { xhr.send(JSON.stringify(postData)); } + +function handleResetPassword() { + const formData = new FormData(document.querySelector("form"), false); + if (!verifyForm(formData)) { + return; + } + + setState(STATE_STATUS, "Resetting password... this should only take a moment.", "form", null, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", "reset-password"); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + xhr.onreadystatechange = function() { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status == 200) { + setState(STATE_SUCCESS, "Your password was successfully reset! Please check your inbox for your new password.", "form", null, true); + } else { + var resp = JSON.parse(this.responseText); + setState(STATE_ERROR, "An error occurred while trying to reset your password:
    " + resp.error + "", "form", null, true); + } + } + } + + var postData = { + "email": formData.get("email") + }; + + xhr.send(JSON.stringify(postData)); +} ` const tplStyleSheet = ` @@ -91,14 +123,17 @@ const tplBody = `
    +
    + Forgot your password? Click here to reset it. +
    -
    +
    Fields marked with * are mandatory.
    -
    +
    -
    +
    diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index 56d87fe8e2..e9f3b5cec0 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -47,6 +47,8 @@ const ( EndpointLogin = "/login" // EndpointLogout is the endpoint path for (internal) user logout. EndpointLogout = "/logout" + // EndpointResetPassword is the endpoint path for resetting user passwords + EndpointResetPassword = "/reset-password" // EndpointAuthorize is the endpoint path for account authorization. EndpointAuthorize = "/authorize" diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index 1e90b72068..c01ab12aab 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -23,11 +23,13 @@ import ( "text/template" "github.com/cs3org/reva/pkg/siteacc/data" - "github.com/pkg/errors" - "github.com/cs3org/reva/pkg/smtpclient" + "github.com/pkg/errors" ) +// SendFunction is the definition of email send functions. +type SendFunction = func(*data.Account, []string, *smtpclient.SMTPCredentials) error + // SendAccountCreated sends an email about account creation. func SendAccountCreated(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, account, smtp) @@ -43,6 +45,11 @@ func SendAccountAuthorized(account *data.Account, recipients []string, smtp *smt return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, account, smtp) } +// SendPasswordReset sends an email containing the user's new password. +func SendPasswordReset(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { + return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, account, smtp) +} + func send(recipients []string, subject string, bodyTemplate string, data interface{}, smtp *smtpclient.SMTPCredentials) error { // Do not fail if no SMTP client or recipient is given if smtp == nil { diff --git a/pkg/siteacc/email/template.go b/pkg/siteacc/email/template.go index 25df085bec..673544410b 100644 --- a/pkg/siteacc/email/template.go +++ b/pkg/siteacc/email/template.go @@ -49,3 +49,15 @@ Congratulations - your site registration has been authorized! Kind regards, The ScienceMesh Team ` + +const passwordResetTemplate = ` +Dear {{.FirstName}} {{.LastName}}, + +Your password has been successfully reset! +Your new password is: {{.Password.Value}} + +We recommend to change this password immediately after logging in. + +Kind regards, +The ScienceMesh Team +` diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 6b712354af..9945627c10 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -30,6 +30,7 @@ import ( "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/html" + "github.com/cs3org/reva/pkg/siteacc/manager" "github.com/pkg/errors" ) @@ -74,11 +75,12 @@ func getEndpoints() []endpoint { {config.EndpointList, callMethodEndpoint, createMethodCallbacks(handleList, nil), false}, {config.EndpointFind, callMethodEndpoint, createMethodCallbacks(handleFind, nil), false}, {config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true}, - {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, + {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), true}, {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, // Login endpoints - {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), false}, - {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), false}, + {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true}, + {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true}, + {config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true}, // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, @@ -235,18 +237,37 @@ func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, err } - invokedByUser := strings.EqualFold(values.Get("invoker"), invokerUser) - if invokedByUser { + setPassword := false + + switch strings.ToLower(values.Get("invoker")) { + case invokerDefault: + // If the endpoint was called through the API, an API key must be provided identifying the account + apiKey := values.Get("apikey") + if apiKey == "" { + return nil, errors.Errorf("no API key provided") + } + + accountFound, err := findAccount(siteacc, manager.FindByAPIKey, apiKey) + if err != nil { + return nil, errors.Wrap(err, "no account for the specified API key found") + } + account.Email = accountFound.Email + + case invokerUser: // If this endpoint was called by the user, set the account email from the stored session if session.LoggedInUser == nil { return nil, errors.Errorf("no user is currently logged in") } account.Email = session.LoggedInUser.Email + setPassword = true + + default: + return nil, errors.Errorf("no invoker provided") } - // Update the account through the accounts manager; only the basic data of an account can be updated through this requestHandler - if err := siteacc.AccountsManager().UpdateAccount(account, invokedByUser, false); err != nil { + // Update the account through the accounts manager + if err := siteacc.AccountsManager().UpdateAccount(account, setPassword, false); err != nil { return nil, errors.Wrap(err, "unable to update account") } @@ -309,6 +330,20 @@ func handleLogout(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, nil } +func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + // Reset the password through the users manager + if err := siteacc.AccountsManager().ResetPassword(account.Email); err != nil { + return nil, errors.Wrap(err, "unable to reset password") + } + + return nil, nil +} + func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 9949deb37f..845b62a03e 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -23,15 +23,15 @@ import ( "sync" "time" + "github.com/cs3org/reva/pkg/mentix/key" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/email" "github.com/cs3org/reva/pkg/siteacc/sitereg" + "github.com/cs3org/reva/pkg/smtpclient" "github.com/pkg/errors" "github.com/rs/zerolog" - - "github.com/cs3org/reva/pkg/mentix/key" - "github.com/cs3org/reva/pkg/smtpclient" + "github.com/sethvargo/go-password/password" ) const ( @@ -160,7 +160,7 @@ func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error { mngr.storage.AccountAdded(account) mngr.writeAllAccounts() - _ = email.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + mngr.sendEmail(account, email.SendAccountCreated) } else { return errors.Wrap(err, "error while creating account") } @@ -190,6 +190,23 @@ func (mngr *AccountsManager) UpdateAccount(accountData *data.Account, setPasswor return nil } +// ResetPassword resets the password for the given user. +func (mngr *AccountsManager) ResetPassword(name string) error { + account, err := mngr.findAccount(FindByEmail, name) + if err != nil { + return errors.Wrap(err, "user to reset password for not found") + } + accountUpd := account.Clone(true) + accountUpd.Password.Value = password.MustGenerate(defaultPasswordLength, 2, 0, false, true) + + err = mngr.UpdateAccount(accountUpd, true, false) + if err == nil { + mngr.sendEmail(accountUpd, email.SendPasswordReset) + } + + return err +} + // FindAccount is used to find an account by various criteria. func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account, error) { mngr.mutex.RLock() @@ -222,7 +239,7 @@ func (mngr *AccountsManager) AuthorizeAccount(accountData *data.Account, authori mngr.writeAllAccounts() if account.Data.Authorized && account.Data.Authorized != authorizedOld { - _ = email.SendAccountAuthorized(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + mngr.sendEmail(account, email.SendAccountAuthorized) } return nil @@ -260,7 +277,7 @@ func (mngr *AccountsManager) AssignAPIKeyToAccount(accountData *data.Account, fl mngr.storage.AccountUpdated(account) mngr.writeAllAccounts() - _ = email.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + mngr.sendEmail(account, email.SendAPIKeyAssigned) return nil } @@ -318,6 +335,10 @@ func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { return clones } +func (mngr *AccountsManager) sendEmail(account *data.Account, sendFunc email.SendFunction) { + _ = sendFunc(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) +} + // NewAccountsManager creates a new accounts manager instance. func NewAccountsManager(conf *config.Configuration, log *zerolog.Logger) (*AccountsManager, error) { mngr := &AccountsManager{} diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 52b679fce1..35a957c7f3 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -33,6 +33,10 @@ type UsersManager struct { accountsManager *AccountsManager } +const ( + defaultPasswordLength = 12 +) + func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) error { if conf == nil { return errors.Errorf("no configuration provided") From ed4d952abaa2ddda0878733e52c365875db7fded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 13 Jul 2021 13:52:56 +0200 Subject: [PATCH 21/60] Fix storing of logged in account --- pkg/siteacc/data/account.go | 15 ++------------- pkg/siteacc/manager/accmanager.go | 15 +++++++++++---- pkg/siteacc/manager/usersmanager.go | 2 +- pkg/siteacc/siteacc.go | 2 +- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index ab175659e0..624aa544f3 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -19,8 +19,6 @@ package data import ( - "bytes" - "encoding/gob" "time" "github.com/cs3org/reva/pkg/siteacc/password" @@ -104,22 +102,13 @@ func (acc *Account) UpdatePassword(pwd string) error { // Clone creates a copy of the account; if erasePassword is set to true, the password will be cleared in the cloned object. func (acc *Account) Clone(erasePassword bool) *Account { - clone := &Account{} - - // To avoid any "deep copy" packages, use gob en- and decoding instead - var buf bytes.Buffer - enc := gob.NewEncoder(&buf) - dec := gob.NewDecoder(&buf) - - if err := enc.Encode(acc); err == nil { - _ = dec.Decode(clone) - } + clone := *acc if erasePassword { clone.Password.Clear() } - return clone + return &clone } func (acc *Account) verify(verifyPassword bool) error { diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 845b62a03e..7368b5a545 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -207,8 +207,13 @@ func (mngr *AccountsManager) ResetPassword(name string) error { return err } -// FindAccount is used to find an account by various criteria. +// FindAccount is used to find an account by various criteria. The account is cloned to prevent data changes. func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account, error) { + return mngr.FindAccountEx(by, value, true) +} + +// FindAccountEx is used to find an account by various criteria and optionally clone the account. +func (mngr *AccountsManager) FindAccountEx(by string, value string, cloneAccount bool) (*data.Account, error) { mngr.mutex.RLock() defer mngr.mutex.RUnlock() @@ -217,9 +222,11 @@ func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account return nil, err } - // Clone the account to avoid external data changes - clonedAccount := *account - return &clonedAccount, nil + if cloneAccount { + account = account.Clone(false) + } + + return account, nil } // AuthorizeAccount sets the authorization status of the account identified by the account email; if no such account exists, an error is returned. diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 35a957c7f3..21cc3a4bdd 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -58,7 +58,7 @@ func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Lo // LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session. func (mngr *UsersManager) LoginUser(name, password string, session *html.Session) error { - account, err := mngr.accountsManager.FindAccount(FindByEmail, name) + account, err := mngr.accountsManager.FindAccountEx(FindByEmail, name, false) if err != nil { return errors.Wrap(err, "no account with the specified email exists") } diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index 6d9375ed4c..82ac0b2ef9 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -146,7 +146,7 @@ func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager { func (siteacc *SiteAccounts) GetPublicEndpoints() []string { // TODO: REMOVE! - return []string{"/"} + //return []string{"/"} endpoints := make([]string, 0, 5) for _, ep := range getEndpoints() { From fd818125798bfd9d5ce2960989ad6ae9b1334ab8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 13 Jul 2021 15:19:18 +0200 Subject: [PATCH 22/60] Remove debug output --- pkg/mentix/connectors/gocdb/query.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/mentix/connectors/gocdb/query.go b/pkg/mentix/connectors/gocdb/query.go index 9e597d5576..9ce86958c5 100755 --- a/pkg/mentix/connectors/gocdb/query.go +++ b/pkg/mentix/connectors/gocdb/query.go @@ -52,7 +52,6 @@ func QueryGOCDB(address string, method string, isPrivate bool, scope string, api return nil, fmt.Errorf("unable to generate the GOCDB URL: %v", err) } - fmt.Println(endpointURL.String()) data, err := network.ReadEndpoint(endpointURL, nil, true) if err != nil { return nil, fmt.Errorf("unable to read GOCDB endpoint: %v", err) From 8c5d9c13593e6111e7381b9fd29c6e819df1d5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 13 Jul 2021 15:45:15 +0200 Subject: [PATCH 23/60] Fix password reset form --- Dockerfile.old | 26 -------------------------- pkg/siteacc/account/login/template.go | 4 ++-- 2 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 Dockerfile.old diff --git a/Dockerfile.old b/Dockerfile.old deleted file mode 100644 index 82c4db78e3..0000000000 --- a/Dockerfile.old +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2018-2021 CERN -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# In applying this license, CERN does not waive the privileges and immunities -# granted to it by virtue of its status as an Intergovernmental Organization -# or submit itself to any jurisdiction. - -FROM golang:1.16 - -WORKDIR /go/src/github/cs3org/reva -COPY . . -RUN make build-revad-docker && cp /go/src/github/cs3org/reva/cmd/revad/revad /go/bin/revad && mkdir -p /etc/revad/ && echo "" > /etc/revad/revad.toml -EXPOSE 9999 -EXPOSE 10000 -CMD ["/go/bin/revad", "-c", "/etc/revad/revad.toml", "-p", "/var/run/revad.pid"] diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 2f1f53c900..4525932512 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -70,8 +70,8 @@ function handleAction(action) { } function handleResetPassword() { - const formData = new FormData(document.querySelector("form"), false); - if (!verifyForm(formData)) { + const formData = new FormData(document.querySelector("form")); + if (!verifyForm(formData, false)) { return; } From 035d773808120c6f3591e96cb555ffb441756b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 14 Jul 2021 13:22:27 +0200 Subject: [PATCH 24/60] Add GOCDB access flag to accounts --- pkg/siteacc/account/manage/template.go | 1 + pkg/siteacc/admin/template.go | 25 ++++++++++---------- pkg/siteacc/config/endpoints.go | 4 ++++ pkg/siteacc/data/account.go | 10 ++++---- pkg/siteacc/email/email.go | 5 ++++ pkg/siteacc/email/template.go | 12 ++++++++++ pkg/siteacc/endpoints.go | 32 ++++++++++++++++++++++++++ pkg/siteacc/manager/accmanager.go | 23 ++++++++++++++++++ 8 files changed, 95 insertions(+), 17 deletions(-) diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go index 3a4e29a4d5..f3d842d2b4 100644 --- a/pkg/siteacc/account/manage/template.go +++ b/pkg/siteacc/account/manage/template.go @@ -89,6 +89,7 @@ const tplBody = ` Account data:
    • API Key: {{if .Account.Data.APIKey}}{{.Account.Data.APIKey}}{{else}}(no key assigned yet){{end}}
    • +
    • GOCDB access: {{if .Account.Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}
    diff --git a/pkg/siteacc/admin/template.go b/pkg/siteacc/admin/template.go index aaccdea6d7..1fdc9c9d5f 100644 --- a/pkg/siteacc/admin/template.go +++ b/pkg/siteacc/admin/template.go @@ -71,26 +71,25 @@ const tplBody = `

    - API Key: - {{if .Data.APIKey}} - {{.Data.APIKey}} - {{else}} - Not assigned - {{end}} + API Key:{{if .Data.APIKey}}{{.Data.APIKey}}{{else}}Not assigned{{end}}
    Site ID: {{.GetSiteID}}

    - Authorized: - {{if .Data.Authorized}} - Yes - {{else}} - No - {{end}} + Authorized: {{if .Data.Authorized}}Yes{{else}}No{{end}} +
    + GOCDB access: {{if .Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}

    +

    + + {{if .Data.GOCDBAccess}} + + {{else}} + + {{end}} {{if .Data.Authorized}} @@ -102,7 +101,7 @@ const tplBody = `   - +


    diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index e9f3b5cec0..82c2f4e772 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -54,6 +54,10 @@ const ( EndpointAuthorize = "/authorize" // EndpointIsAuthorized is the endpoint path used to check the authorization status of an account. EndpointIsAuthorized = "/is-authorized" + + // EndpointGrantGOCDBAccess is the endpoint path for granting or revoking GOCDB access. + EndpointGrantGOCDBAccess = "/grant-gocdb-access" + // EndpointUnregisterSite is the endpoint path for site unregistration. EndpointUnregisterSite = "/unregister-site" ) diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 624aa544f3..eec1eb1e8d 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -48,8 +48,9 @@ type Account struct { // AccountData holds additional data for a site account. type AccountData struct { - APIKey key.APIKey `json:"apiKey"` - Authorized bool `json:"authorized"` + APIKey key.APIKey `json:"apiKey"` + GOCDBAccess bool `json:"gocdbAccess"` + Authorized bool `json:"authorized"` } // Accounts holds an array of site accounts. @@ -151,8 +152,9 @@ func NewAccount(email string, firstName, lastName string, organization, website DateCreated: t, DateModified: t, Data: AccountData{ - APIKey: "", - Authorized: false, + APIKey: "", + GOCDBAccess: false, + Authorized: false, }, } diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index c01ab12aab..20b6e4bda8 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -45,6 +45,11 @@ func SendAccountAuthorized(account *data.Account, recipients []string, smtp *smt return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, account, smtp) } +// SendGOCDBAccessGranted sends an email about granted GOCDB access. +func SendGOCDBAccessGranted(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { + return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, account, smtp) +} + // SendPasswordReset sends an email containing the user's new password. func SendPasswordReset(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, account, smtp) diff --git a/pkg/siteacc/email/template.go b/pkg/siteacc/email/template.go index 673544410b..60d1931a16 100644 --- a/pkg/siteacc/email/template.go +++ b/pkg/siteacc/email/template.go @@ -50,6 +50,18 @@ Kind regards, The ScienceMesh Team ` +const gocdbAccessGrantedTemplate = ` +Dear {{.FirstName}} {{.LastName}}, + +You have been granted access to the ScienceMesh GOCDB instance: +https://gocdb.sciencemesh.uni-muenster.de + +Simply use your regular ScienceMesh account credentials to log in. + +Kind regards, +The ScienceMesh Team +` + const passwordResetTemplate = ` Dear {{.FirstName}} {{.LastName}}, diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 9945627c10..3122b4b18c 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -84,6 +84,8 @@ func getEndpoints() []endpoint { // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, + // Access management endpoints + {config.EndpointGrantGOCDBAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantGOCDBAccess), false}, // Account site endpoints {config.EndpointUnregisterSite, callMethodEndpoint, createMethodCallbacks(nil, handleUnregisterSite), false}, } @@ -374,6 +376,36 @@ func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, sess return nil, nil } +func handleGrantGOCDBAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + account, err := unmarshalRequestData(body) + if err != nil { + return nil, err + } + + if val := values.Get("status"); len(val) > 0 { + var grantAccess bool + switch strings.ToLower(val) { + case "true": + grantAccess = true + + case "false": + grantAccess = false + + default: + return nil, errors.Errorf("unsupported access status %v", val[0]) + } + + // Grant access to the account through the accounts manager + if err := siteacc.AccountsManager().GrantGOCDBAccess(account, grantAccess); err != nil { + return nil, errors.Wrap(err, "unable to change the GOCDB access status of the account") + } + } else { + return nil, errors.Errorf("no access status provided") + } + + return nil, nil +} + func unmarshalRequestData(body []byte) (*data.Account, error) { account := &data.Account{} if err := json.Unmarshal(body, account); err != nil { diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 7368b5a545..8d6a55d86f 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -252,6 +252,29 @@ func (mngr *AccountsManager) AuthorizeAccount(accountData *data.Account, authori return nil } +// GrantGOCDBAccess sets the GOCDB access status of the account identified by the account email; if no such account exists, an error is returned. +func (mngr *AccountsManager) GrantGOCDBAccess(accountData *data.Account, grantAccess bool) error { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + account, err := mngr.findAccount(FindByEmail, accountData.Email) + if err != nil { + return errors.Wrap(err, "no account with the specified email exists") + } + + accessOld := account.Data.GOCDBAccess + account.Data.GOCDBAccess = grantAccess + + mngr.storage.AccountUpdated(account) + mngr.writeAllAccounts() + + if account.Data.GOCDBAccess && account.Data.GOCDBAccess != accessOld { + mngr.sendEmail(account, email.SendGOCDBAccessGranted) + } + + return nil +} + // AssignAPIKeyToAccount is used to assign a new API key to the account identified by the account email; if no such account exists, an error is returned. func (mngr *AccountsManager) AssignAPIKeyToAccount(accountData *data.Account, flags int) error { mngr.mutex.Lock() From f6efa98b34de06385b67f4b89cadb3841a699dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 14 Jul 2021 16:07:28 +0200 Subject: [PATCH 25/60] Verify account website and phone number --- internal/http/services/siteacc/siteacc.go | 5 +++-- pkg/siteacc/data/account.go | 6 ++++++ pkg/siteacc/siteacc.go | 2 +- pkg/utils/utils.go | 13 +++++++++++++ 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go index 0c50cf3bf3..4d8462fc21 100644 --- a/internal/http/services/siteacc/siteacc.go +++ b/internal/http/services/siteacc/siteacc.go @@ -83,8 +83,9 @@ func applyDefaultConfig(conf *config.Configuration) { conf.Storage.Driver = "file" } - if conf.Webserver.SessionTimeout < 60 { - conf.Webserver.SessionTimeout = 120 + // Enforce a minimum session timeout of 5 minutes + if conf.Webserver.SessionTimeout < 5*60 { + conf.Webserver.SessionTimeout = 5 * 60 } } diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index eec1eb1e8d..d71405f2f1 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -124,9 +124,15 @@ func (acc *Account) verify(verifyPassword bool) error { if acc.Organization == "" { return errors.Errorf("no organization provided") } + if acc.Website != "" && !utils.IsValidWebAddress(acc.Website) { + return errors.Errorf("invalid website provided") + } if acc.Role == "" { return errors.Errorf("no role provided") } + if acc.PhoneNumber != "" && !utils.IsValidPhoneNumber(acc.PhoneNumber) { + return errors.Errorf("invalid phone number provided") + } if verifyPassword { if !acc.Password.IsValid() { diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index 82ac0b2ef9..6d9375ed4c 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -146,7 +146,7 @@ func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager { func (siteacc *SiteAccounts) GetPublicEndpoints() []string { // TODO: REMOVE! - //return []string{"/"} + return []string{"/"} endpoints := make([]string, 0, 5) for _, ep := range getEndpoints() { diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index 13c28007dc..9ebd02f3c6 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -22,6 +22,7 @@ import ( "math/rand" "net" "net/http" + "net/url" "os/user" "path" "path/filepath" @@ -181,6 +182,18 @@ func IsEmailValid(e string) bool { return matchEmail.MatchString(e) } +// IsValidWebAddress checks whether the provided address is a valid URL. +func IsValidWebAddress(address string) bool { + _, err := url.ParseRequestURI(address) + return err == nil +} + +// IsValidPhoneNumber checks whether the provided phone number has a valid format. +func IsValidPhoneNumber(number string) bool { + re := regexp.MustCompile(`^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$`) + return re.MatchString(number) +} + // MarshalProtoV1ToJSON marshals a proto V1 message to a JSON byte array // TODO: update this once we start using V2 in CS3APIs func MarshalProtoV1ToJSON(m proto.Message) ([]byte, error) { From 35133a50eb2d925151347d946cd7a77e7613ca88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 13:24:31 +0200 Subject: [PATCH 26/60] Session handling improvements --- .../config/http/services/siteacc/_index.md | 10 +++++++- pkg/siteacc/config/config.go | 3 ++- pkg/siteacc/html/session.go | 19 ++++++++++++--- pkg/siteacc/html/sessionmanager.go | 23 +++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/docs/content/en/docs/config/http/services/siteacc/_index.md b/docs/content/en/docs/config/http/services/siteacc/_index.md index 7320466789..365842c067 100644 --- a/docs/content/en/docs/config/http/services/siteacc/_index.md +++ b/docs/content/en/docs/config/http/services/siteacc/_index.md @@ -113,10 +113,18 @@ url = "https://iop.example.com/sitereg" {{% /dir %}} ## Webserver settings -{{% dir name="session_timeout" type="int" default="120" %}} +{{% dir name="session_timeout" type="int" default="300" %}} The session timeout in seconds. {{< highlight toml >}} [http.services.siteacc.webserver] session_timeout = 600 {{< /highlight >}} {{% /dir %}} + +{{% dir name="log_sessions" type="bool" default="false" %}} +If enabled, debug information about sessions will be printed. +{{< highlight toml >}} +[http.services.siteacc.webserver] +log_sessions = true +{{< /highlight >}} +{{% /dir %}} diff --git a/pkg/siteacc/config/config.go b/pkg/siteacc/config/config.go index cf802bc4ab..6c5c11acdb 100644 --- a/pkg/siteacc/config/config.go +++ b/pkg/siteacc/config/config.go @@ -40,6 +40,7 @@ type Configuration struct { } `mapstructure:"sitereg"` Webserver struct { - SessionTimeout int `mapstructure:"session_timeout"` + SessionTimeout int `mapstructure:"session_timeout"` + LogSessions bool `mapstructure:"log_sessions"` } `mapstructure:"webserver"` } diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 517bf66e37..5f930bf155 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -20,6 +20,7 @@ package html import ( "net/http" + "strings" "time" "github.com/cs3org/reva/pkg/siteacc/data" @@ -44,6 +45,15 @@ type Session struct { sessionCookieName string } +func getRemoteAddress(r *http.Request) string { + // Remove the port number from the remote address + remoteAddress := "" + if address := strings.Split(r.RemoteAddr, ":"); len(address) == 2 { + remoteAddress = address[0] + } + return remoteAddress +} + // Save stores the session ID in a cookie using a response writer. func (sess *Session) Save(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ @@ -63,8 +73,10 @@ func (sess *Session) VerifyRequest(r *http.Request) error { return errors.Errorf("the session ID doesn't match") } - if r.RemoteAddr != sess.RemoteAddress { - return errors.Errorf("remote address has changed (%v != %v)", r.RemoteAddr, sess.RemoteAddress) + if sess.RemoteAddress != "" { + if !strings.EqualFold(getRemoteAddress(r), sess.RemoteAddress) { + return errors.Errorf("remote address has changed (%v != %v)", r.RemoteAddr, sess.RemoteAddress) + } } return nil @@ -82,9 +94,10 @@ func (sess *Session) HasExpired() bool { // NewSession creates a new session, giving it a random ID. func NewSession(name string, timeout time.Duration, r *http.Request) *Session { + session := &Session{ ID: uuid.NewString(), - RemoteAddress: r.RemoteAddr, + RemoteAddress: getRemoteAddress(r), CreationTime: time.Now(), Timeout: timeout, Data: nil, diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 9332b4ad84..edcbfef59e 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -19,6 +19,7 @@ package html import ( + "fmt" "net/http" "sync" "time" @@ -74,11 +75,15 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request if err == nil { session = mngr.findSession(cookie.Value) if session != nil { + mngr.logSessionInfo(session, r, "existing session found") + // Verify the request against the session: If it is invalid, set an error; if the session has expired, create a new one; if it has already passed its halftime, migrate to a new one if err := session.VerifyRequest(r); err == nil { if session.HasExpired() { // The session has expired, so a new one needs to be created session = nil + + mngr.logSessionInfo(session, r, "session expired") } else if session.HalftimePassed() { // The session has passed its halftime, so migrate it to a new one (makes hijacking session IDs harder) session, err = mngr.migrateSession(session, r) @@ -86,21 +91,29 @@ func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request session = nil sessionErr = errors.Wrap(err, "unable to migrate session") } + + mngr.logSessionInfo(session, r, "session migrated") } } else { session = nil sessionErr = errors.Wrap(err, "invalid session") + + mngr.logSessionInfo(session, r, "session invalid (verify failed)") } } } else if err != http.ErrNoCookie { // The session cookie exists but seems to be invalid, so set an error session = nil sessionErr = errors.Wrap(err, "unable to get the session ID from the client") + + mngr.logSessionInfo(session, r, fmt.Sprintf("session cookie error: %v", err)) } if session == nil { // No session found for the client, so create a new one; this will always succeed session = mngr.createSession(r) + + mngr.logSessionInfo(session, r, "assigned new session") } // Store the session ID on the client side @@ -152,6 +165,16 @@ func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (* return sessionNew, nil } +func (mngr *SessionManager) logSessionInfo(session *Session, r *http.Request, info string) { + if mngr.conf.Webserver.LogSessions { + if session != nil { + mngr.log.Debug().Str("id", session.ID).Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info) + } else { + mngr.log.Debug().Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info) + } + } +} + // NewSessionManager creates a new session manager. func NewSessionManager(name string, conf *config.Configuration, log *zerolog.Logger) (*SessionManager, error) { mngr := &SessionManager{} From 27516be152845a359a24fb6ff6d7939f5b7ec13c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 13:36:34 +0200 Subject: [PATCH 27/60] Session handling improvements --- internal/http/services/siteacc/siteacc.go | 4 ++-- pkg/siteacc/html/session.go | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go index 4d8462fc21..b79fd57244 100644 --- a/internal/http/services/siteacc/siteacc.go +++ b/internal/http/services/siteacc/siteacc.go @@ -83,8 +83,8 @@ func applyDefaultConfig(conf *config.Configuration) { conf.Storage.Driver = "file" } - // Enforce a minimum session timeout of 5 minutes - if conf.Webserver.SessionTimeout < 5*60 { + // Enforce a minimum session timeout of 1 minute (and default to 5 minutes) + if conf.Webserver.SessionTimeout < 60 { conf.Webserver.SessionTimeout = 5 * 60 } } diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 5f930bf155..e24cd9b1b1 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -94,7 +94,6 @@ func (sess *Session) HasExpired() bool { // NewSession creates a new session, giving it a random ID. func NewSession(name string, timeout time.Duration, r *http.Request) *Session { - session := &Session{ ID: uuid.NewString(), RemoteAddress: getRemoteAddress(r), From f5e7a74a4ee672e71cbbd8faf52f9bf634d3b8f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 13:41:49 +0200 Subject: [PATCH 28/60] Debugging --- pkg/siteacc/account/panel.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 465be85fa0..b4af8281ad 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -19,6 +19,7 @@ package account import ( + "fmt" "net/http" "net/url" @@ -128,6 +129,9 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult { // Modify the original request URI by replacing the path parameter + fmt.Println(r.RequestURI) + fmt.Println(r.URL) + fmt.Println(r.Host) newURI, _ := url.Parse(r.RequestURI) params := newURI.Query() params.Del("path") From efb469e750d139586d047d4c78d75df6dd7f2dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 14:01:59 +0200 Subject: [PATCH 29/60] Debugging --- pkg/siteacc/account/panel.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index b4af8281ad..e5ad9ee679 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -129,9 +129,11 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult { // Modify the original request URI by replacing the path parameter - fmt.Println(r.RequestURI) + fmt.Println("------------------------------------------------------") + fmt.Println(r) fmt.Println(r.URL) - fmt.Println(r.Host) + fmt.Println(r.Header) + fmt.Println("------------------------------------------------------") newURI, _ := url.Parse(r.RequestURI) params := newURI.Query() params.Del("path") From a64215d208296befad9e7c3d8a686e9d6f5d5167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 14:27:08 +0200 Subject: [PATCH 30/60] Fix URI path handling --- pkg/siteacc/account/panel.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index e5ad9ee679..f9ee625844 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -19,7 +19,6 @@ package account import ( - "fmt" "net/http" "net/url" @@ -128,13 +127,14 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm } func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult { - // Modify the original request URI by replacing the path parameter - fmt.Println("------------------------------------------------------") - fmt.Println(r) - fmt.Println(r.URL) - fmt.Println(r.Header) - fmt.Println("------------------------------------------------------") - newURI, _ := url.Parse(r.RequestURI) + fullPath := r.Header.Get("X-Replaced-Path") + if fullPath == "" { + uri, _ := url.Parse(r.RequestURI) + fullPath = uri.Path + } + + // Modify the original request URL by replacing the path parameter + newURI, _ := url.Parse(fullPath) params := newURI.Query() params.Del("path") params.Add("path", path) From e1fdfb0a1ede1a6c1b0850bce80776a1706ac129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 14:38:53 +0200 Subject: [PATCH 31/60] Fixes --- pkg/siteacc/account/edit/template.go | 2 +- pkg/siteacc/account/panel.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go index 4ad6aea47f..a446c1b376 100644 --- a/pkg/siteacc/account/edit/template.go +++ b/pkg/siteacc/account/edit/template.go @@ -112,7 +112,7 @@ const tplBody = `
     
    -
    +
    diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index f9ee625844..8d1b6579b1 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -127,6 +127,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm } func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult { + // Check if the original (full) URI path is stored in the request header; if not, use the request URI to get the path fullPath := r.Header.Get("X-Replaced-Path") if fullPath == "" { uri, _ := url.Parse(r.RequestURI) @@ -134,12 +135,12 @@ func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request } // Modify the original request URL by replacing the path parameter - newURI, _ := url.Parse(fullPath) - params := newURI.Query() + newURL, _ := url.Parse(fullPath) + params := newURL.Query() params.Del("path") params.Add("path", path) - newURI.RawQuery = params.Encode() - http.Redirect(w, r, newURI.String(), http.StatusFound) + newURL.RawQuery = params.Encode() + http.Redirect(w, r, newURL.String(), http.StatusFound) return html.AbortExecution } From 3f3870617dfea7362cfdef0017ff7be4f033b5ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 16 Jul 2021 14:52:29 +0200 Subject: [PATCH 32/60] Edit account template improvements --- pkg/siteacc/account/edit/template.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go index a446c1b376..088e9ed9df 100644 --- a/pkg/siteacc/account/edit/template.go +++ b/pkg/siteacc/account/edit/template.go @@ -112,7 +112,7 @@ const tplBody = `
     
    - +
    @@ -132,9 +132,9 @@ const tplBody = `
    If you want to change your password, fill out the fields below. Otherwise, leave them empty to keep your current one.
    -
    +
    -
    +
    The password must fulfil the following criteria: From 687319956c263cdb9d58551597db6db0ab6f1da6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 19 Jul 2021 14:25:20 +0200 Subject: [PATCH 33/60] Add user tokens for external authentication --- pkg/siteacc/config/endpoints.go | 3 + pkg/siteacc/endpoints.go | 21 +++++- pkg/siteacc/html/session.go | 2 +- pkg/siteacc/manager/token.go | 109 ++++++++++++++++++++++++++++ pkg/siteacc/manager/usersmanager.go | 39 ++++++++-- 5 files changed, 166 insertions(+), 8 deletions(-) create mode 100644 pkg/siteacc/manager/token.go diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index 82c2f4e772..e96b1e5e3e 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -50,6 +50,9 @@ const ( // EndpointResetPassword is the endpoint path for resetting user passwords EndpointResetPassword = "/reset-password" + // EndpointVerifyUserToken is the endpoint path for user token validation. + EndpointVerifyUserToken = "/verify-user-token" + // EndpointAuthorize is the endpoint path for account authorization. EndpointAuthorize = "/authorize" // EndpointIsAuthorized is the endpoint path used to check the authorization status of an account. diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 3122b4b18c..5016746d6d 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -81,6 +81,8 @@ func getEndpoints() []endpoint { {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true}, {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true}, {config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true}, + // Authentication endpoints + {config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(nil, handleVerifyUserToken), true}, // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, @@ -319,11 +321,12 @@ func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session } // Login the user through the users manager - if err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, session); err != nil { + token, err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, session) + if err != nil { return nil, errors.Wrap(err, "unable to login user") } - return nil, nil + return token, nil } func handleLogout(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { @@ -346,6 +349,20 @@ func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, return nil, nil } +func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + token := values.Get("token") + if token == "" { + return nil, errors.Errorf("no token specified") + } + + // Verify the user token using the users manager + if err := siteacc.UsersManager().VerifyUserToken(token, session); err != nil { + return nil, errors.Wrap(err, "token verification failed") + } + + return nil, nil +} + func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index e24cd9b1b1..59246ba23e 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -99,7 +99,7 @@ func NewSession(name string, timeout time.Duration, r *http.Request) *Session { RemoteAddress: getRemoteAddress(r), CreationTime: time.Now(), Timeout: timeout, - Data: nil, + Data: make(map[string]interface{}, 10), expirationTime: time.Now().Add(timeout), halflifeTime: time.Now().Add(timeout / 2), sessionCookieName: name, diff --git a/pkg/siteacc/manager/token.go b/pkg/siteacc/manager/token.go new file mode 100644 index 0000000000..610905c99f --- /dev/null +++ b/pkg/siteacc/manager/token.go @@ -0,0 +1,109 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package manager + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + "github.com/cs3org/reva/pkg/siteacc/html" + "github.com/pkg/errors" + "github.com/sethvargo/go-password/password" +) + +type userToken struct { + SessionID string + User string +} + +const ( + tokenKeyLength = 16 + + tokenNonceName = "usertoken_nonce" +) + +var ( + tokenKey string +) + +func generateUserToken(session *html.Session) (string, error) { + // The token consists of the session ID and the logged in user's email address + token := userToken{ + SessionID: session.ID, + User: session.LoggedInUser.Email, + } + + data, err := json.Marshal(&token) + if err != nil { + return "", errors.Wrap(err, "unable to marshal the token") + } + + // Encrypt the data using AES + block, _ := aes.NewCipher([]byte(tokenKey)) + aesgcm, _ := cipher.NewGCM(block) + + // Generate a nonce and store it in the session + nonce := make([]byte, aesgcm.NonceSize()) + _, _ = io.ReadFull(rand.Reader, nonce) + session.Data[tokenNonceName] = nonce + + cipherText := fmt.Sprintf("%x", aesgcm.Seal(nil, nonce, data, nil)) + + return cipherText, nil +} + +func extractUserToken(token string, session *html.Session) (*userToken, error) { + // Get the nonce from the session + var nonce []byte + if nonceData, ok := session.Data[tokenNonceName]; ok { + nonce, ok = nonceData.([]byte) + if !ok { + return nil, errors.Errorf("invalid nonce stored in the current session") + } + } else { + return nil, errors.Errorf("no nonce found in the current session") + } + + // Decrypt the data using AES + cipherText, _ := hex.DecodeString(token) + + block, _ := aes.NewCipher([]byte(tokenKey)) + aesgcm, _ := cipher.NewGCM(block) + + plainText, err := aesgcm.Open(nil, nonce, cipherText, nil) + if err != nil { + return nil, errors.Wrap(err, "unable to decrypt the token") + } + + var utoken userToken + if err := json.Unmarshal(plainText, &utoken); err != nil { + return nil, errors.Wrap(err, "unable to unmarshal the token") + } + return &utoken, nil +} + +func init() { + // Generate the key used for AES encryption + tokenKey = password.MustGenerate(tokenKeyLength, tokenKeyLength/4, tokenKeyLength/4, false, true) +} diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 21cc3a4bdd..2904dacf34 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -56,21 +56,28 @@ func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Lo return nil } -// LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session. -func (mngr *UsersManager) LoginUser(name, password string, session *html.Session) error { +// LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session and a user token is returned. +func (mngr *UsersManager) LoginUser(name, password string, session *html.Session) (string, error) { account, err := mngr.accountsManager.FindAccountEx(FindByEmail, name, false) if err != nil { - return errors.Wrap(err, "no account with the specified email exists") + return "", errors.Wrap(err, "no account with the specified email exists") } // Verify the provided password if !account.Password.Compare(password) { - return errors.Errorf("invalid password") + return "", errors.Errorf("invalid password") } // Store the user account in the session session.LoggedInUser = account - return nil + + // Encapsulate all necessary authentication information in a token + token, err := generateUserToken(session) + if err != nil { + return "", errors.Wrap(err, "unable to generate user token") + } + + return token, nil } // LogoutUser logs the current user out. @@ -79,6 +86,28 @@ func (mngr *UsersManager) LogoutUser(session *html.Session) { session.LoggedInUser = nil } +// VerifyUserToken is used to verify a user token against the current session. +func (mngr *UsersManager) VerifyUserToken(token string, session *html.Session) error { + if session.LoggedInUser == nil { + return errors.Errorf("no user logged in") + } + + utoken, err := extractUserToken(token, session) + if err != nil { + return errors.Wrap(err, "unable to verify user authentication") + } + + // Check the token values against the session + if utoken.SessionID != session.ID { + return errors.Errorf("session ID mismatch") + } + if utoken.User != session.LoggedInUser.Email { + return errors.Errorf("session user mismatch") + } + + return nil +} + // NewUsersManager creates a new users manager instance. func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) (*UsersManager, error) { mngr := &UsersManager{} From 02538dac7c7ee98965df2d6287df09d590377af8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 20 Jul 2021 13:00:56 +0200 Subject: [PATCH 34/60] User token refreshing --- pkg/siteacc/endpoints.go | 5 +++-- pkg/siteacc/html/session.go | 2 ++ pkg/siteacc/html/sessionmanager.go | 1 + pkg/siteacc/manager/usersmanager.go | 20 +++++++++++++------- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 5016746d6d..6b52c50280 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -356,11 +356,12 @@ func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte } // Verify the user token using the users manager - if err := siteacc.UsersManager().VerifyUserToken(token, session); err != nil { + newToken, err := siteacc.UsersManager().VerifyUserToken(token, session) + if err != nil { return nil, errors.Wrap(err, "token verification failed") } - return nil, nil + return newToken, nil } func handleAuthorize(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 59246ba23e..6d915b6ee9 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -31,6 +31,7 @@ import ( // Session stores all data associated with an HTML session. type Session struct { ID string + MigrationID string RemoteAddress string CreationTime time.Time Timeout time.Duration @@ -96,6 +97,7 @@ func (sess *Session) HasExpired() bool { func NewSession(name string, timeout time.Duration, r *http.Request) *Session { session := &Session{ ID: uuid.NewString(), + MigrationID: "", RemoteAddress: getRemoteAddress(r), CreationTime: time.Now(), Timeout: timeout, diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index edcbfef59e..e3c8fe5cbb 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -156,6 +156,7 @@ func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (* sessionNew := mngr.createSession(r) // Carry over the old session information, thus preserving the existing session + sessionNew.MigrationID = session.ID sessionNew.LoggedInUser = session.LoggedInUser sessionNew.Data = session.Data diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 2904dacf34..bffe4d1b05 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -87,25 +87,31 @@ func (mngr *UsersManager) LogoutUser(session *html.Session) { } // VerifyUserToken is used to verify a user token against the current session. -func (mngr *UsersManager) VerifyUserToken(token string, session *html.Session) error { +func (mngr *UsersManager) VerifyUserToken(token string, session *html.Session) (string, error) { if session.LoggedInUser == nil { - return errors.Errorf("no user logged in") + return "", errors.Errorf("no user logged in") } utoken, err := extractUserToken(token, session) if err != nil { - return errors.Wrap(err, "unable to verify user authentication") + return "", errors.Wrap(err, "unable to verify user authentication") } // Check the token values against the session - if utoken.SessionID != session.ID { - return errors.Errorf("session ID mismatch") + if utoken.SessionID != session.ID && utoken.SessionID != session.MigrationID { + return "", errors.Errorf("session ID mismatch") } if utoken.User != session.LoggedInUser.Email { - return errors.Errorf("session user mismatch") + return "", errors.Errorf("session user mismatch") } - return nil + // Refresh the user token, as the session ID might have changed due to migration + newToken, err := generateUserToken(session) + if err != nil { + return "", errors.Wrap(err, "unable to generate user token") + } + + return newToken, nil } // NewUsersManager creates a new users manager instance. From b70dc83ee647b17c5a833ed91a7f9c006e63bc48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 20 Jul 2021 14:15:37 +0200 Subject: [PATCH 35/60] Add scope check to login --- pkg/siteacc/data/account.go | 16 ++++++++++++++++ pkg/siteacc/data/scopes.go | 26 ++++++++++++++++++++++++++ pkg/siteacc/endpoints.go | 2 +- pkg/siteacc/manager/usersmanager.go | 7 ++++++- 4 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 pkg/siteacc/data/scopes.go diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index d71405f2f1..5668d67d74 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -19,6 +19,7 @@ package data import ( + "strings" "time" "github.com/cs3org/reva/pkg/siteacc/password" @@ -112,6 +113,21 @@ func (acc *Account) Clone(erasePassword bool) *Account { return &clone } +// CheckScopeAccess checks whether the user can access the specified scope. +func (acc *Account) CheckScopeAccess(scope string) bool { + hasAccess := false + + switch strings.ToLower(scope) { + case ScopeDefault: + hasAccess = true + + case ScopeGOCDB: + hasAccess = acc.Data.GOCDBAccess + } + + return hasAccess +} + func (acc *Account) verify(verifyPassword bool) error { if acc.Email == "" { return errors.Errorf("no email address provided") diff --git a/pkg/siteacc/data/scopes.go b/pkg/siteacc/data/scopes.go new file mode 100644 index 0000000000..f515fb68f1 --- /dev/null +++ b/pkg/siteacc/data/scopes.go @@ -0,0 +1,26 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package data + +const ( + // ScopeDefault is the default account panel scope. + ScopeDefault = "" + // ScopeGOCDB is used to access the GOCDB. + ScopeGOCDB = "gocdb" +) diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 6b52c50280..faff2cd195 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -321,7 +321,7 @@ func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session } // Login the user through the users manager - token, err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, session) + token, err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, values.Get("scope"), session) if err != nil { return nil, errors.Wrap(err, "unable to login user") } diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index bffe4d1b05..4bee07da43 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -57,7 +57,7 @@ func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Lo } // LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session and a user token is returned. -func (mngr *UsersManager) LoginUser(name, password string, session *html.Session) (string, error) { +func (mngr *UsersManager) LoginUser(name, password string, scope string, session *html.Session) (string, error) { account, err := mngr.accountsManager.FindAccountEx(FindByEmail, name, false) if err != nil { return "", errors.Wrap(err, "no account with the specified email exists") @@ -68,6 +68,11 @@ func (mngr *UsersManager) LoginUser(name, password string, session *html.Session return "", errors.Errorf("invalid password") } + // Check if the user has access to the specified scope + if !account.CheckScopeAccess(scope) { + return "", errors.Errorf("no access to the specified scope granted") + } + // Store the user account in the session session.LoggedInUser = account From a3c6e752db8d0e9c4b7211b92e4526bf06b7a74d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 21 Jul 2021 15:53:28 +0200 Subject: [PATCH 36/60] Make verify-user-token a GET endpoint --- pkg/siteacc/endpoints.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index faff2cd195..6b18b34a41 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -82,7 +82,7 @@ func getEndpoints() []endpoint { {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true}, {config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true}, // Authentication endpoints - {config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(nil, handleVerifyUserToken), true}, + {config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(handleVerifyUserToken, nil), true}, // Authorization endpoints {config.EndpointAuthorize, callMethodEndpoint, createMethodCallbacks(nil, handleAuthorize), false}, {config.EndpointIsAuthorized, callMethodEndpoint, createMethodCallbacks(handleIsAuthorized, nil), false}, From 0c3ebb4408b98bbfda45ca8c23aa3c73dec87260 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Thu, 22 Jul 2021 14:23:18 +0200 Subject: [PATCH 37/60] Switch to JWT for user tokens --- pkg/siteacc/endpoints.go | 7 ++- pkg/siteacc/manager/token.go | 92 +++++++++++------------------ pkg/siteacc/manager/usersmanager.go | 43 ++++++++------ 3 files changed, 67 insertions(+), 75 deletions(-) diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 6b18b34a41..8496ea7052 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -355,8 +355,13 @@ func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte return nil, errors.Errorf("no token specified") } + user := values.Get("user") + if user == "" { + return nil, errors.Errorf("no user specified") + } + // Verify the user token using the users manager - newToken, err := siteacc.UsersManager().VerifyUserToken(token, session) + newToken, err := siteacc.UsersManager().VerifyUserToken(token, user, values.Get("scope")) if err != nil { return nil, errors.Wrap(err, "token verification failed") } diff --git a/pkg/siteacc/manager/token.go b/pkg/siteacc/manager/token.go index 610905c99f..a7216c0828 100644 --- a/pkg/siteacc/manager/token.go +++ b/pkg/siteacc/manager/token.go @@ -19,91 +19,69 @@ package manager import ( - "crypto/aes" - "crypto/cipher" - "crypto/rand" - "encoding/hex" - "encoding/json" - "fmt" - "io" + "time" - "github.com/cs3org/reva/pkg/siteacc/html" + "github.com/dgrijalva/jwt-go" "github.com/pkg/errors" "github.com/sethvargo/go-password/password" ) type userToken struct { - SessionID string - User string + jwt.StandardClaims + + User string `json:"user"` + Scope string `json:"scope"` } const ( tokenKeyLength = 16 - - tokenNonceName = "usertoken_nonce" + tokenIssuer = "sciencemesh_siteacc" ) var ( - tokenKey string + tokenSecret string ) -func generateUserToken(session *html.Session) (string, error) { - // The token consists of the session ID and the logged in user's email address - token := userToken{ - SessionID: session.ID, - User: session.LoggedInUser.Email, +func generateUserToken(user string, scope string, timeout int) (string, error) { + // Create a JWT as the user token + claims := userToken{ + StandardClaims: jwt.StandardClaims{ + ExpiresAt: time.Now().Add(time.Duration(timeout) * time.Second).Unix(), + Issuer: tokenIssuer, + IssuedAt: time.Now().Unix(), + }, + User: user, + Scope: scope, } - data, err := json.Marshal(&token) + token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims) + signedToken, err := token.SignedString([]byte(tokenSecret)) if err != nil { - return "", errors.Wrap(err, "unable to marshal the token") + return "", errors.Wrapf(err, "error signing token with claims %+v", claims) } - // Encrypt the data using AES - block, _ := aes.NewCipher([]byte(tokenKey)) - aesgcm, _ := cipher.NewGCM(block) - - // Generate a nonce and store it in the session - nonce := make([]byte, aesgcm.NonceSize()) - _, _ = io.ReadFull(rand.Reader, nonce) - session.Data[tokenNonceName] = nonce - - cipherText := fmt.Sprintf("%x", aesgcm.Seal(nil, nonce, data, nil)) - - return cipherText, nil + return signedToken, nil } -func extractUserToken(token string, session *html.Session) (*userToken, error) { - // Get the nonce from the session - var nonce []byte - if nonceData, ok := session.Data[tokenNonceName]; ok { - nonce, ok = nonceData.([]byte) - if !ok { - return nil, errors.Errorf("invalid nonce stored in the current session") - } - } else { - return nil, errors.Errorf("no nonce found in the current session") +func extractUserToken(token string) (*userToken, error) { + // Parse the token and try to extract the claims + parsedToken, err := jwt.ParseWithClaims(token, &userToken{}, func(token *jwt.Token) (interface{}, error) { return []byte(tokenSecret), nil }) + if err != nil { + return nil, errors.Wrap(err, "error parsing token") } - // Decrypt the data using AES - cipherText, _ := hex.DecodeString(token) - - block, _ := aes.NewCipher([]byte(tokenKey)) - aesgcm, _ := cipher.NewGCM(block) + if claims, ok := parsedToken.Claims.(*userToken); ok && parsedToken.Valid { + if claims.Issuer != tokenIssuer { + return nil, errors.Errorf("invalid token issuer") + } - plainText, err := aesgcm.Open(nil, nonce, cipherText, nil) - if err != nil { - return nil, errors.Wrap(err, "unable to decrypt the token") + return claims, nil } - var utoken userToken - if err := json.Unmarshal(plainText, &utoken); err != nil { - return nil, errors.Wrap(err, "unable to unmarshal the token") - } - return &utoken, nil + return nil, errors.Errorf("invalid token") } func init() { - // Generate the key used for AES encryption - tokenKey = password.MustGenerate(tokenKeyLength, tokenKeyLength/4, tokenKeyLength/4, false, true) + // Generate the token secret randomly + tokenSecret = password.MustGenerate(tokenKeyLength, tokenKeyLength/4, tokenKeyLength/4, false, true) } diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 4bee07da43..4f39b3e4bf 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -19,6 +19,8 @@ package manager import ( + "strings" + "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/html" "github.com/pkg/errors" @@ -76,8 +78,8 @@ func (mngr *UsersManager) LoginUser(name, password string, scope string, session // Store the user account in the session session.LoggedInUser = account - // Encapsulate all necessary authentication information in a token - token, err := generateUserToken(session) + // Generate a token that can be used as a "ticket" + token, err := generateUserToken(session.LoggedInUser.Email, scope, mngr.conf.Webserver.SessionTimeout) if err != nil { return "", errors.Wrap(err, "unable to generate user token") } @@ -92,28 +94,35 @@ func (mngr *UsersManager) LogoutUser(session *html.Session) { } // VerifyUserToken is used to verify a user token against the current session. -func (mngr *UsersManager) VerifyUserToken(token string, session *html.Session) (string, error) { - if session.LoggedInUser == nil { - return "", errors.Errorf("no user logged in") - } - - utoken, err := extractUserToken(token, session) +func (mngr *UsersManager) VerifyUserToken(token string, user string, scope string) (string, error) { + // Verify the token by trying to extract it + utoken, err := extractUserToken(token) if err != nil { - return "", errors.Wrap(err, "unable to verify user authentication") + return "", errors.Wrap(err, "unable to verify user token") } - // Check the token values against the session - if utoken.SessionID != session.ID && utoken.SessionID != session.MigrationID { - return "", errors.Errorf("session ID mismatch") + // Check the provided email against the stored one + if !strings.EqualFold(utoken.User, user) { + return "", errors.Errorf("mismatching user") } - if utoken.User != session.LoggedInUser.Email { - return "", errors.Errorf("session user mismatch") + + // Check if the user account actually exists and has proper scope access + if strings.EqualFold(scope, utoken.Scope) { + if acc, err := mngr.accountsManager.FindAccount(FindByEmail, utoken.User); err == nil { + if !acc.CheckScopeAccess(scope) { + return "", errors.Errorf("no scope access") + } + } else { + return "", errors.Errorf("invalid email") + } + } else { + return "", errors.Errorf("invalid scope") } - // Refresh the user token, as the session ID might have changed due to migration - newToken, err := generateUserToken(session) + // Refresh the user token (as a form of keep-alive, since tokens expire quickly) + newToken, err := generateUserToken(utoken.User, utoken.Scope, mngr.conf.Webserver.SessionTimeout) if err != nil { - return "", errors.Wrap(err, "unable to generate user token") + return "", errors.Wrap(err, "unable to refresh user token") } return newToken, nil From e942d4553f5e34d378e13fed1b71fbb629ede7f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Fri, 23 Jul 2021 13:29:11 +0200 Subject: [PATCH 38/60] Form improvements --- pkg/siteacc/account/edit/template.go | 2 +- pkg/siteacc/account/login/template.go | 2 +- pkg/siteacc/account/registration/template.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go index 088e9ed9df..df3b3fceb8 100644 --- a/pkg/siteacc/account/edit/template.go +++ b/pkg/siteacc/account/edit/template.go @@ -151,7 +151,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 4525932512..a857203e6e 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -132,7 +132,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/account/registration/template.go b/pkg/siteacc/account/registration/template.go index d58e3ce184..2ec70f137f 100644 --- a/pkg/siteacc/account/registration/template.go +++ b/pkg/siteacc/account/registration/template.go @@ -165,7 +165,7 @@ const tplBody = `
    - +
    From aaabc72657f7fb59e76e1133252bde93533d5429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Mon, 26 Jul 2021 13:53:29 +0200 Subject: [PATCH 39/60] Email updates --- .../config/http/services/siteacc/_index.md | 38 +++++++++++-------- examples/siteacc/siteacc.toml | 15 ++++---- pkg/siteacc/account/edit/template.go | 2 +- pkg/siteacc/account/login/template.go | 6 +-- pkg/siteacc/account/registration/template.go | 9 +---- pkg/siteacc/config/config.go | 9 ++++- pkg/siteacc/email/email.go | 38 +++++++++++++------ pkg/siteacc/email/template.go | 29 ++++++++------ pkg/siteacc/manager/accmanager.go | 6 +-- pkg/siteacc/siteacc.go | 4 +- 10 files changed, 92 insertions(+), 64 deletions(-) diff --git a/docs/content/en/docs/config/http/services/siteacc/_index.md b/docs/content/en/docs/config/http/services/siteacc/_index.md index 365842c067..17c25e1f80 100644 --- a/docs/content/en/docs/config/http/services/siteacc/_index.md +++ b/docs/content/en/docs/config/http/services/siteacc/_index.md @@ -19,28 +19,36 @@ prefix = "/siteacc" {{< /highlight >}} {{% /dir %}} -{{% dir name="enable_registration_form" type="string" default="false" %}} -If set to true, the service will expose a simple form for account registration. +## Email settings +{{% dir name="notifications_mail" type="string" default="" %}} +An email address where all notifications are sent to. +{{< highlight toml >}} +[http.services.siteacc.email] +notifications_mail = "notify@example.com" +{{< /highlight >}} +{{% /dir %}} +{{% dir name="accounts_address" type="string" default="" %}} +The URL for the site accounts user panel which will be used in emails. {{< highlight toml >}} -[http.services.siteacc] -enable_registration_form = true +[http.services.siteacc.email] +accounts_address = "https://www.sciencemesh.eu/accounts/" {{< /highlight >}} {{% /dir %}} -{{% dir name="notifications_mail" type="string" default="" %}} -An email address where all notifications are sent to. +{{% dir name="gocdb_address" type="string" default="" %}} +The URL for the GOCDB which will be used in emails. {{< highlight toml >}} -[http.services.siteacc] -notifications_mail = "notify@example.com" +[http.services.siteacc.email] +gocdb_address = "https://www.sciencemesh.eu/gocdb/" {{< /highlight >}} {{% /dir %}} -## SMTP settings +### SMTP settings {{% dir name="sender_mail" type="string" default="" %}} An email address from which all emails are sent. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] sender_mail = "notify@example.com" {{< /highlight >}} {{% /dir %}} @@ -48,7 +56,7 @@ sender_mail = "notify@example.com" {{% dir name="sender_login" type="string" default="" %}} The login name. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] sender_login = "hans" {{< /highlight >}} {{% /dir %}} @@ -56,7 +64,7 @@ sender_login = "hans" {{% dir name="sender_password" type="string" default="" %}} The password for the login. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] password = "secret" {{< /highlight >}} {{% /dir %}} @@ -64,7 +72,7 @@ password = "secret" {{% dir name="smtp_server" type="string" default="" %}} The SMTP server to use. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] smtp_server = "smtp.example.com" {{< /highlight >}} {{% /dir %}} @@ -72,7 +80,7 @@ smtp_server = "smtp.example.com" {{% dir name="smtp_port" type="int" default="25" %}} The SMTP server port to use. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] smtp_port = 25 {{< /highlight >}} {{% /dir %}} @@ -80,7 +88,7 @@ smtp_port = 25 {{% dir name="disable_auth" type="bool" default="false" %}} Whether to disable authentication. {{< highlight toml >}} -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] disable_auth = true {{< /highlight >}} {{% /dir %}} diff --git a/examples/siteacc/siteacc.toml b/examples/siteacc/siteacc.toml index 5e4d33031c..e03f5adcbf 100644 --- a/examples/siteacc/siteacc.toml +++ b/examples/siteacc/siteacc.toml @@ -1,20 +1,20 @@ [http] address = "0.0.0.0:9600" -[http.services.siteacc] -# If this is set to true, the service will expose a simple form for account registration -enable_registration_form = true -# All notification emails are sent to this email -notifications_mail = "science.mesh@example.com" - # Set up the storage driver [http.services.siteacc.storage] driver = "file" [http.services.siteacc.storage.file] file = "/var/revad/accounts.json" +# Email related settings +[http.services.siteacc.email] +notifications_mail = "science.mesh@example.com" +accounts_address = "https://sciencemesh-test.uni-muenster.de/api/accounts/" +gocdb_address = "https://sciencemesh-test.uni-muenster.de/gocdb/" + # The SMTP server used for sending emails -[http.services.siteacc.smtp] +[http.services.siteacc.email.smtp] sender_mail = "science.mesh@example.com" smtp_server = "mail.example.com" smtp_port = 25 @@ -23,4 +23,3 @@ disable_auth = true # The webserver section defines various webserver-related settings [http.services.siteacc.webserver] session_timeout = 60 - diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go index df3b3fceb8..088e9ed9df 100644 --- a/pkg/siteacc/account/edit/template.go +++ b/pkg/siteacc/account/edit/template.go @@ -151,7 +151,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index a857203e6e..76cc7c569e 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -120,9 +120,9 @@ const tplBody = `
    -
    +
    -
    +
    Forgot your password? Click here to reset it.
    @@ -132,7 +132,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/account/registration/template.go b/pkg/siteacc/account/registration/template.go index 2ec70f137f..f169aded47 100644 --- a/pkg/siteacc/account/registration/template.go +++ b/pkg/siteacc/account/registration/template.go @@ -116,12 +116,7 @@ html * { const tplBody = `
    -

    Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.

    -

    - After inspection by a ScienceMesh administrator, you will also receive an API key which can then be used in the - ownCloud or - Nextcloud web application. -

    +

    Fill out the form below to register for a ScienceMesh account. A confirmation email will be sent to you shortly after registration.

     
    @@ -165,7 +160,7 @@ const tplBody = `
    - +
    diff --git a/pkg/siteacc/config/config.go b/pkg/siteacc/config/config.go index 6c5c11acdb..38e8016b33 100644 --- a/pkg/siteacc/config/config.go +++ b/pkg/siteacc/config/config.go @@ -32,8 +32,13 @@ type Configuration struct { } `mapstructure:"file"` } `mapstructure:"storage"` - SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"` - NotificationsMail string `mapstructure:"notifications_mail"` + Email struct { + SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"` + NotificationsMail string `mapstructure:"notifications_mail"` + + AccountsAddress string `mapstructure:"accounts_address"` + GOCDBAddress string `mapstructure:"gocdb_address"` + } `mapstructure:"email"` SiteRegistration struct { URL string `mapstructure:"url"` diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index 20b6e4bda8..44adf00fb9 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -22,37 +22,53 @@ import ( "bytes" "text/template" + "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/smtpclient" "github.com/pkg/errors" ) +type emailData struct { + Account *data.Account + + AccountsAddress string + GOCDBAddress string +} + // SendFunction is the definition of email send functions. -type SendFunction = func(*data.Account, []string, *smtpclient.SMTPCredentials) error +type SendFunction = func(*data.Account, []string, config.Configuration) error + +func getEmailData(account *data.Account, conf config.Configuration) *emailData { + return &emailData{ + Account: account, + AccountsAddress: conf.Email.AccountsAddress, + GOCDBAddress: conf.Email.GOCDBAddress, + } +} // SendAccountCreated sends an email about account creation. -func SendAccountCreated(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { - return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, account, smtp) +func SendAccountCreated(account *data.Account, recipients []string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, getEmailData(account, conf), conf.Email.SMTP) } // SendAPIKeyAssigned sends an email about API key assignment. -func SendAPIKeyAssigned(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { - return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, account, smtp) +func SendAPIKeyAssigned(account *data.Account, recipients []string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, getEmailData(account, conf), conf.Email.SMTP) } // SendAccountAuthorized sends an email about account authorization. -func SendAccountAuthorized(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { - return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, account, smtp) +func SendAccountAuthorized(account *data.Account, recipients []string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, getEmailData(account, conf), conf.Email.SMTP) } // SendGOCDBAccessGranted sends an email about granted GOCDB access. -func SendGOCDBAccessGranted(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { - return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, account, smtp) +func SendGOCDBAccessGranted(account *data.Account, recipients []string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, getEmailData(account, conf), conf.Email.SMTP) } // SendPasswordReset sends an email containing the user's new password. -func SendPasswordReset(account *data.Account, recipients []string, smtp *smtpclient.SMTPCredentials) error { - return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, account, smtp) +func SendPasswordReset(account *data.Account, recipients []string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, getEmailData(account, conf), conf.Email.SMTP) } func send(recipients []string, subject string, bodyTemplate string, data interface{}, smtp *smtpclient.SMTPCredentials) error { diff --git a/pkg/siteacc/email/template.go b/pkg/siteacc/email/template.go index 60d1931a16..bd14f46e57 100644 --- a/pkg/siteacc/email/template.go +++ b/pkg/siteacc/email/template.go @@ -19,30 +19,35 @@ package email const accountCreatedTemplate = ` -Dear {{.FirstName}} {{.LastName}}, +Dear {{.Account.FirstName}} {{.Account.LastName}}, Your ScienceMesh account has been successfully created! -An administrator will soon create an API key for your account; you will receive a separate email containing the key. +Log in to your account by visiting the user account panel: +{{.AccountsAddress}} + +Using this panel, you can modify your information, request an API key or access to the GOCDB, and more. Kind regards, The ScienceMesh Team ` const apiKeyAssignedTemplate = ` -Dear {{.FirstName}} {{.LastName}}, +Dear {{.Account.FirstName}} {{.Account.LastName}}, + +An API key has been created for your account! -An API key has been created for your account: -{{.Data.APIKey}} +To view your new API key, log in to your user account panel: +{{.AccountsAddress}} -Keep this key in a safe and secret place! +Your key will appear on the front page once logged in. Kind regards, The ScienceMesh Team ` const accountAuthorizedTemplate = ` -Dear {{.FirstName}} {{.LastName}}, +Dear {{.Account.FirstName}} {{.Account.LastName}}, Congratulations - your site registration has been authorized! @@ -51,22 +56,22 @@ The ScienceMesh Team ` const gocdbAccessGrantedTemplate = ` -Dear {{.FirstName}} {{.LastName}}, +Dear {{.Account.FirstName}} {{.Account.LastName}}, You have been granted access to the ScienceMesh GOCDB instance: -https://gocdb.sciencemesh.uni-muenster.de +{{.GOCDBAddress}} -Simply use your regular ScienceMesh account credentials to log in. +Simply use your regular ScienceMesh account credentials to log in to the GOCDB. Kind regards, The ScienceMesh Team ` const passwordResetTemplate = ` -Dear {{.FirstName}} {{.LastName}}, +Dear {{.Account.FirstName}} {{.Account.LastName}}, Your password has been successfully reset! -Your new password is: {{.Password.Value}} +Your new password is: {{.Account.Password.Value}} We recommend to change this password immediately after logging in. diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 8d6a55d86f..a7d0d36655 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -78,8 +78,8 @@ func (mngr *AccountsManager) initialize(conf *config.Configuration, log *zerolog } // Create the SMTP client - if conf.SMTP != nil { - mngr.smtp = smtpclient.NewSMTPCredentials(conf.SMTP) + if conf.Email.SMTP != nil { + mngr.smtp = smtpclient.NewSMTPCredentials(conf.Email.SMTP) } return nil @@ -366,7 +366,7 @@ func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { } func (mngr *AccountsManager) sendEmail(account *data.Account, sendFunc email.SendFunction) { - _ = sendFunc(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + _ = sendFunc(account, []string{account.Email, mngr.conf.Email.NotificationsMail}, *mngr.conf) } // NewAccountsManager creates a new accounts manager instance. diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index 6d9375ed4c..3640833464 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -145,8 +145,8 @@ func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager { } func (siteacc *SiteAccounts) GetPublicEndpoints() []string { - // TODO: REMOVE! - return []string{"/"} + // TODO: Only for local testing! + // return []string{"/"} endpoints := make([]string, 0, 5) for _, ep := range getEndpoints() { From 2a4467fdbf09f1007994040c505390395b2206c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Tue, 27 Jul 2021 15:42:15 +0200 Subject: [PATCH 40/60] Add request form --- pkg/siteacc/account/contact/contact.go | 50 +++++++++++ pkg/siteacc/account/contact/template.go | 113 ++++++++++++++++++++++++ pkg/siteacc/account/manage/template.go | 12 +-- pkg/siteacc/account/panel.go | 18 +++- pkg/siteacc/config/endpoints.go | 2 + pkg/siteacc/email/email.go | 32 ++++--- pkg/siteacc/email/template.go | 9 ++ pkg/siteacc/endpoints.go | 20 +++++ pkg/siteacc/manager/accmanager.go | 19 ++-- 9 files changed, 249 insertions(+), 26 deletions(-) create mode 100644 pkg/siteacc/account/contact/contact.go create mode 100644 pkg/siteacc/account/contact/template.go diff --git a/pkg/siteacc/account/contact/contact.go b/pkg/siteacc/account/contact/contact.go new file mode 100644 index 0000000000..cdcf0fec69 --- /dev/null +++ b/pkg/siteacc/account/contact/contact.go @@ -0,0 +1,50 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package contact + +import "github.com/cs3org/reva/pkg/siteacc/html" + +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the panel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Contact Form" +} + +// GetCaption returns the caption which is displayed on the panel. +func (template *PanelTemplate) GetCaption() string { + return "Contact the ScienceMesh administration" +} + +// GetContentJavaScript delivers additional JavaScript code. +func (template *PanelTemplate) GetContentJavaScript() string { + return tplJavaScript +} + +// GetContentStyleSheet delivers additional stylesheet code. +func (template *PanelTemplate) GetContentStyleSheet() string { + return tplStyleSheet +} + +// GetContentBody delivers the actual body content. +func (template *PanelTemplate) GetContentBody() string { + return tplBody +} diff --git a/pkg/siteacc/account/contact/template.go b/pkg/siteacc/account/contact/template.go new file mode 100644 index 0000000000..ba44e4146b --- /dev/null +++ b/pkg/siteacc/account/contact/template.go @@ -0,0 +1,113 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package contact + +const tplJavaScript = ` +function verifyForm(formData) { + if (formData.get("subject") == "") { + setState(STATE_ERROR, "Please enter a subject.", "form", "subject", true); + return false; + } + + if (formData.get("message") == "") { + setState(STATE_ERROR, "Please enter a message.", "form", "message", true); + return false; + } + + return true; +} + +function handleAction(action) { + const formData = new FormData(document.querySelector("form")); + if (!verifyForm(formData)) { + return; + } + + setState(STATE_STATUS, "Sending message... this should only take a moment.", "form", null, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", action); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + xhr.onreadystatechange = function() { + if (this.readyState === XMLHttpRequest.DONE) { + if (this.status == 200) { + setState(STATE_SUCCESS, "Your message was successfully sent! A copy of the message has been sent to your email address."); + } else { + var resp = JSON.parse(this.responseText); + setState(STATE_ERROR, "An error occurred while trying to send your message:
    " + resp.error + "", "form", null, true); + } + } + } + + var postData = { + "subject": formData.get("subject"), + "message": formData.get("message") + }; + + xhr.send(JSON.stringify(postData)); +} +` + +const tplStyleSheet = ` +html * { + font-family: arial !important; +} + +.mandatory { + color: red; + font-weight: bold; +} +` + +const tplBody = ` +
    +

    Contact the ScienceMesh administration using the form below.

    +

    Please include as much information as possible in your request, especially:

    +
      +
    • The site your request refers to (if not obvious from your account information)
    • +
    • Your role within the ScienceMesh site (e.g., administrator, operational team member, etc.)
    • +
    • Any specific reasons for your request
    • +
    • Anything else that might help to process your request
    • +
    +
    +
     
    +
    +
    +
    +
    + +
    +
    + +
    + +
    + Fields marked with * are mandatory. +
    +
    + + +
    +
    +
    +
    +

    Go back to the main account page.

    +
    +` diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go index f3d842d2b4..b41056c7ef 100644 --- a/pkg/siteacc/account/manage/template.go +++ b/pkg/siteacc/account/manage/template.go @@ -25,11 +25,13 @@ function handleEditAccount() { } function handleRequestAccess() { - setState(STATE_STATUS, "No one has implemented this function yet :("); + setState(STATE_STATUS, "Redirecting to the contact form..."); + window.location.replace("?path=contact&subject=" + encodeURIComponent("Request GOCDB access")); } function handleRequestKey() { - setState(STATE_STATUS, "No one has implemented this function yet :("); + setState(STATE_STATUS, "Redirecting to the contact form..."); + window.location.replace("?path=contact&subject=" + encodeURIComponent("Request API key")); } function handleLogout() { @@ -70,7 +72,7 @@ html * { const tplBody = `

    Hello {{.Account.FirstName}} {{.Account.LastName}},

    -

    On this page, you can manage your ScienceMesh user account. This includes editing your personal information, requesting access to the GOCDB and more.

    +

    On this page, you can manage your ScienceMesh user account. This includes editing your personal information, requesting an API key or access to the GOCDB and more.

     
    @@ -96,8 +98,8 @@ const tplBody = `
      - - + +
    diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 8d1b6579b1..3ffd1eb716 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -21,7 +21,9 @@ package account import ( "net/http" "net/url" + "strings" + "github.com/cs3org/reva/pkg/siteacc/account/contact" "github.com/cs3org/reva/pkg/siteacc/account/edit" "github.com/cs3org/reva/pkg/siteacc/account/login" "github.com/cs3org/reva/pkg/siteacc/account/manage" @@ -44,6 +46,7 @@ const ( templateLogin = "login" templateManage = "manage" templateEdit = "edit" + templateContact = "contact" templateRegistration = "register" ) @@ -68,6 +71,10 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) return errors.Wrap(err, "unable to create the account editing template") } + if err := panel.htmlPanel.AddTemplate(templateContact, &contact.PanelTemplate{}); err != nil { + return errors.Wrap(err, "unable to create the contact template") + } + if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil { return errors.Wrap(err, "unable to create the registration template") } @@ -77,7 +84,7 @@ func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) // GetActiveTemplate returns the name of the active template. func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string { - validPaths := []string{templateLogin, templateManage, templateEdit, templateRegistration} + validPaths := []string{templateLogin, templateManage, templateEdit, templateContact, templateRegistration} template := templateLogin // Only allow valid template paths; redirect to the login page otherwise @@ -93,7 +100,7 @@ func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string // PreExecute is called before the actual template is being executed. func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) { - protectedPaths := []string{templateManage, templateEdit} + protectedPaths := []string{templateManage, templateEdit, templateContact} if session.LoggedInUser == nil { // If no user is logged in, redirect protected paths to the login page @@ -115,12 +122,19 @@ func (panel *Panel) PreExecute(session *html.Session, path string, w http.Respon // Execute generates the HTTP output of the form and writes it to the response writer. func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error { dataProvider := func(*html.Session) interface{} { + flatValues := make(map[string]string, len(r.URL.Query())) + for k, v := range r.URL.Query() { + flatValues[strings.Title(k)] = v[0] + } + type TemplateData struct { Account *data.Account + Params map[string]string } return TemplateData{ Account: session.LoggedInUser, + Params: flatValues, } } return panel.htmlPanel.Execute(w, r, session, dataProvider) diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index e96b1e5e3e..feef521aab 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -49,6 +49,8 @@ const ( EndpointLogout = "/logout" // EndpointResetPassword is the endpoint path for resetting user passwords EndpointResetPassword = "/reset-password" + // EndpointContact is the endpoint path for sending contact emails + EndpointContact = "/contact" // EndpointVerifyUserToken is the endpoint path for user token validation. EndpointVerifyUserToken = "/verify-user-token" diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index 44adf00fb9..c90df187d1 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -33,42 +33,50 @@ type emailData struct { AccountsAddress string GOCDBAddress string + + Params map[string]string } // SendFunction is the definition of email send functions. -type SendFunction = func(*data.Account, []string, config.Configuration) error +type SendFunction = func(*data.Account, []string, map[string]string, config.Configuration) error -func getEmailData(account *data.Account, conf config.Configuration) *emailData { +func getEmailData(account *data.Account, conf config.Configuration, params map[string]string) *emailData { return &emailData{ Account: account, AccountsAddress: conf.Email.AccountsAddress, GOCDBAddress: conf.Email.GOCDBAddress, + Params: params, } } // SendAccountCreated sends an email about account creation. -func SendAccountCreated(account *data.Account, recipients []string, conf config.Configuration) error { - return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, getEmailData(account, conf), conf.Email.SMTP) +func SendAccountCreated(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Site account created", accountCreatedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } // SendAPIKeyAssigned sends an email about API key assignment. -func SendAPIKeyAssigned(account *data.Account, recipients []string, conf config.Configuration) error { - return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, getEmailData(account, conf), conf.Email.SMTP) +func SendAPIKeyAssigned(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Your API key", apiKeyAssignedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } // SendAccountAuthorized sends an email about account authorization. -func SendAccountAuthorized(account *data.Account, recipients []string, conf config.Configuration) error { - return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, getEmailData(account, conf), conf.Email.SMTP) +func SendAccountAuthorized(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } // SendGOCDBAccessGranted sends an email about granted GOCDB access. -func SendGOCDBAccessGranted(account *data.Account, recipients []string, conf config.Configuration) error { - return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, getEmailData(account, conf), conf.Email.SMTP) +func SendGOCDBAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } // SendPasswordReset sends an email containing the user's new password. -func SendPasswordReset(account *data.Account, recipients []string, conf config.Configuration) error { - return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, getEmailData(account, conf), conf.Email.SMTP) +func SendPasswordReset(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, getEmailData(account, conf, params), conf.Email.SMTP) +} + +// SendContactForm sends a generic contact form to the ScienceMesh admins. +func SendContactForm(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Contact form", contactFormTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } func send(recipients []string, subject string, bodyTemplate string, data interface{}, smtp *smtpclient.SMTPCredentials) error { diff --git a/pkg/siteacc/email/template.go b/pkg/siteacc/email/template.go index bd14f46e57..3992a56052 100644 --- a/pkg/siteacc/email/template.go +++ b/pkg/siteacc/email/template.go @@ -78,3 +78,12 @@ We recommend to change this password immediately after logging in. Kind regards, The ScienceMesh Team ` + +const contactFormTemplate = ` +{{.Account.FirstName}} {{.Account.LastName}} ({{.Account.Email}}) has sent the following message: + +{{.Params.Subject}} +--------------------------------------------------------------------------------------------------- + +{{.Params.Message}} +` diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 8496ea7052..8d11ae69b4 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -81,6 +81,7 @@ func getEndpoints() []endpoint { {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true}, {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true}, {config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true}, + {config.EndpointContact, callMethodEndpoint, createMethodCallbacks(nil, handleContact), true}, // Authentication endpoints {config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(handleVerifyUserToken, nil), true}, // Authorization endpoints @@ -349,6 +350,25 @@ func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, return nil, nil } +func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + if session.LoggedInUser == nil { + return nil, errors.Errorf("no user is currently logged in") + } + + type jsonData struct { + Subject string `json:"subject"` + Message string `json:"message"` + } + contactData := &jsonData{} + if err := json.Unmarshal(body, contactData); err != nil { + return nil, errors.Wrap(err, "invalid form data") + } + + // Send an email through the accounts manager + siteacc.AccountsManager().SendContactForm(session.LoggedInUser, contactData.Subject, contactData.Message) + return nil, nil +} + func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { token := values.Get("token") if token == "" { diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index a7d0d36655..b405abafcb 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -160,7 +160,7 @@ func (mngr *AccountsManager) CreateAccount(accountData *data.Account) error { mngr.storage.AccountAdded(account) mngr.writeAllAccounts() - mngr.sendEmail(account, email.SendAccountCreated) + mngr.sendEmail(account, nil, email.SendAccountCreated) } else { return errors.Wrap(err, "error while creating account") } @@ -201,7 +201,7 @@ func (mngr *AccountsManager) ResetPassword(name string) error { err = mngr.UpdateAccount(accountUpd, true, false) if err == nil { - mngr.sendEmail(accountUpd, email.SendPasswordReset) + mngr.sendEmail(accountUpd, nil, email.SendPasswordReset) } return err @@ -246,7 +246,7 @@ func (mngr *AccountsManager) AuthorizeAccount(accountData *data.Account, authori mngr.writeAllAccounts() if account.Data.Authorized && account.Data.Authorized != authorizedOld { - mngr.sendEmail(account, email.SendAccountAuthorized) + mngr.sendEmail(account, nil, email.SendAccountAuthorized) } return nil @@ -269,7 +269,7 @@ func (mngr *AccountsManager) GrantGOCDBAccess(accountData *data.Account, grantAc mngr.writeAllAccounts() if account.Data.GOCDBAccess && account.Data.GOCDBAccess != accessOld { - mngr.sendEmail(account, email.SendGOCDBAccessGranted) + mngr.sendEmail(account, nil, email.SendGOCDBAccessGranted) } return nil @@ -307,7 +307,7 @@ func (mngr *AccountsManager) AssignAPIKeyToAccount(accountData *data.Account, fl mngr.storage.AccountUpdated(account) mngr.writeAllAccounts() - mngr.sendEmail(account, email.SendAPIKeyAssigned) + mngr.sendEmail(account, nil, email.SendAPIKeyAssigned) return nil } @@ -352,6 +352,11 @@ func (mngr *AccountsManager) RemoveAccount(accountData *data.Account) error { return errors.Errorf("no account with the specified email exists") } +// SendContactForm sends a generic email to the ScienceMesh admins. +func (mngr *AccountsManager) SendContactForm(account *data.Account, subject, message string) { + mngr.sendEmail(account, map[string]string{"Subject": subject, "Message": message}, email.SendContactForm) +} + // CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { mngr.mutex.RLock() @@ -365,8 +370,8 @@ func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { return clones } -func (mngr *AccountsManager) sendEmail(account *data.Account, sendFunc email.SendFunction) { - _ = sendFunc(account, []string{account.Email, mngr.conf.Email.NotificationsMail}, *mngr.conf) +func (mngr *AccountsManager) sendEmail(account *data.Account, params map[string]string, sendFunc email.SendFunction) { + _ = sendFunc(account, []string{account.Email, mngr.conf.Email.NotificationsMail}, params, *mngr.conf) } // NewAccountsManager creates a new accounts manager instance. From 9f2d0f6e5affb66ffeb625d51cc2cc99205704c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 28 Jul 2021 16:07:45 +0200 Subject: [PATCH 41/60] Trim form entries --- pkg/siteacc/account/contact/template.go | 8 +++---- pkg/siteacc/account/edit/template.go | 20 ++++++++-------- pkg/siteacc/account/login/template.go | 4 ++-- pkg/siteacc/account/registration/template.go | 24 ++++++++++---------- pkg/siteacc/data/account.go | 11 +++++++++ pkg/siteacc/endpoints.go | 3 ++- pkg/siteacc/html/template.go | 10 ++++++++ pkg/siteacc/siteacc.go | 2 +- 8 files changed, 52 insertions(+), 30 deletions(-) diff --git a/pkg/siteacc/account/contact/template.go b/pkg/siteacc/account/contact/template.go index ba44e4146b..5d2b974c7e 100644 --- a/pkg/siteacc/account/contact/template.go +++ b/pkg/siteacc/account/contact/template.go @@ -20,12 +20,12 @@ package contact const tplJavaScript = ` function verifyForm(formData) { - if (formData.get("subject") == "") { + if (formData.getTrimmed("subject") == "") { setState(STATE_ERROR, "Please enter a subject.", "form", "subject", true); return false; } - if (formData.get("message") == "") { + if (formData.getTrimmed("message") == "") { setState(STATE_ERROR, "Please enter a message.", "form", "message", true); return false; } @@ -57,8 +57,8 @@ function handleAction(action) { } var postData = { - "subject": formData.get("subject"), - "message": formData.get("message") + "subject": formData.getTrimmed("subject"), + "message": formData.getTrimmed("message") }; xhr.send(JSON.stringify(postData)); diff --git a/pkg/siteacc/account/edit/template.go b/pkg/siteacc/account/edit/template.go index 088e9ed9df..9d771d4b03 100644 --- a/pkg/siteacc/account/edit/template.go +++ b/pkg/siteacc/account/edit/template.go @@ -20,22 +20,22 @@ package edit const tplJavaScript = ` function verifyForm(formData) { - if (formData.get("fname") == "") { + if (formData.getTrimmed("fname") == "") { setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true); return false; } - if (formData.get("lname") == "") { + if (formData.getTrimmed("lname") == "") { setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true); return false; } - if (formData.get("organization") == "") { + if (formData.getTrimmed("organization") == "") { setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true); return false; } - if (formData.get("role") == "") { + if (formData.getTrimmed("role") == "") { setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true); return false; } @@ -79,12 +79,12 @@ function handleAction(action) { } var postData = { - "firstName": formData.get("fname"), - "lastName": formData.get("lname"), - "organization": formData.get("organization"), - "website": formData.get("website"), - "role": formData.get("role"), - "phoneNumber": formData.get("phone"), + "firstName": formData.getTrimmed("fname"), + "lastName": formData.getTrimmed("lname"), + "organization": formData.getTrimmed("organization"), + "website": formData.getTrimmed("website"), + "role": formData.getTrimmed("role"), + "phoneNumber": formData.getTrimmed("phone"), "password": { "value": formData.get("password") } diff --git a/pkg/siteacc/account/login/template.go b/pkg/siteacc/account/login/template.go index 76cc7c569e..7a3d56805b 100644 --- a/pkg/siteacc/account/login/template.go +++ b/pkg/siteacc/account/login/template.go @@ -20,7 +20,7 @@ package login const tplJavaScript = ` function verifyForm(formData, requirePassword = true) { - if (formData.get("email") == "") { + if (formData.getTrimmed("email") == "") { setState(STATE_ERROR, "Please enter your email address.", "form", "email", true); return false; } @@ -60,7 +60,7 @@ function handleAction(action) { } var postData = { - "email": formData.get("email"), + "email": formData.getTrimmed("email"), "password": { "value": formData.get("password") } diff --git a/pkg/siteacc/account/registration/template.go b/pkg/siteacc/account/registration/template.go index f169aded47..3cbd7c2c54 100644 --- a/pkg/siteacc/account/registration/template.go +++ b/pkg/siteacc/account/registration/template.go @@ -20,27 +20,27 @@ package registration const tplJavaScript = ` function verifyForm(formData) { - if (formData.get("email") == "") { + if (formData.getTrimmed("email") == "") { setState(STATE_ERROR, "Please specify your email address.", "form", "email", true); return false; } - if (formData.get("fname") == "") { + if (formData.getTrimmed("fname") == "") { setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true); return false; } - if (formData.get("lname") == "") { + if (formData.getTrimmed("lname") == "") { setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true); return false; } - if (formData.get("organization") == "") { + if (formData.getTrimmed("organization") == "") { setState(STATE_ERROR, "Please specify your organization/company.", "form", "organization", true); return false; } - if (formData.get("role") == "") { + if (formData.getTrimmed("role") == "") { setState(STATE_ERROR, "Please specify your role within the organization/company.", "form", "role", true); return false; } @@ -87,13 +87,13 @@ function handleAction(action) { } var postData = { - "email": formData.get("email"), - "firstName": formData.get("fname"), - "lastName": formData.get("lname"), - "organization": formData.get("organization"), - "website": formData.get("website"), - "role": formData.get("role"), - "phoneNumber": formData.get("phone"), + "email": formData.getTrimmed("email"), + "firstName": formData.getTrimmed("fname"), + "lastName": formData.getTrimmed("lname"), + "organization": formData.getTrimmed("organization"), + "website": formData.getTrimmed("website"), + "role": formData.getTrimmed("role"), + "phoneNumber": formData.getTrimmed("phone"), "password": { "value": formData.get("password") } diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 5668d67d74..781369218b 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -128,6 +128,17 @@ func (acc *Account) CheckScopeAccess(scope string) bool { return hasAccess } +// Cleanup trims all string entries. +func (acc *Account) Cleanup() { + acc.Email = strings.TrimSpace(acc.Email) + acc.FirstName = strings.TrimSpace(acc.FirstName) + acc.LastName = strings.TrimSpace(acc.LastName) + acc.Organization = strings.TrimSpace(acc.Organization) + acc.Website = strings.TrimSpace(acc.Website) + acc.Role = strings.TrimSpace(acc.Role) + acc.PhoneNumber = strings.TrimSpace(acc.PhoneNumber) +} + func (acc *Account) verify(verifyPassword bool) error { if acc.Email == "" { return errors.Errorf("no email address provided") diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 8d11ae69b4..5687b77586 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -365,7 +365,7 @@ func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, sessio } // Send an email through the accounts manager - siteacc.AccountsManager().SendContactForm(session.LoggedInUser, contactData.Subject, contactData.Message) + siteacc.AccountsManager().SendContactForm(session.LoggedInUser, strings.TrimSpace(contactData.Subject), strings.TrimSpace(contactData.Message)) return nil, nil } @@ -454,6 +454,7 @@ func unmarshalRequestData(body []byte) (*data.Account, error) { if err := json.Unmarshal(body, account); err != nil { return nil, errors.Wrap(err, "invalid account data") } + account.Cleanup() return account, nil } diff --git a/pkg/siteacc/html/template.go b/pkg/siteacc/html/template.go index c24d530054..b0fa54706d 100644 --- a/pkg/siteacc/html/template.go +++ b/pkg/siteacc/html/template.go @@ -81,6 +81,16 @@ const panelTemplate = ` } } + FormData.prototype.getTrimmed = function(id) { + var val = this.get(id); + + if (val != null) { + return val.trim(); + } else { + return ""; + } + } + $(CONTENT_JAVASCRIPT)