diff --git a/changelog/unreleased/siteacc-site-settings.md b/changelog/unreleased/siteacc-site-settings.md new file mode 100644 index 0000000000..8a3a6c2468 --- /dev/null +++ b/changelog/unreleased/siteacc-site-settings.md @@ -0,0 +1,5 @@ +Enhancement: Site accounts site-global settings + +This PR extends the site accounts service by adding site-global settings. These are used to store test user credentials that are in return used by our BBE port to perform CS3API-specific health checks. + +https://github.com/cs3org/reva/pull/2738 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 bf318f18fe..a556106c19 100644 --- a/docs/content/en/docs/config/http/services/siteacc/_index.md +++ b/docs/content/en/docs/config/http/services/siteacc/_index.md @@ -19,6 +19,15 @@ prefix = "/siteacc" {{< /highlight >}} {{% /dir %}} +## Security settings +{{% dir name="creds_passphrase" type="string" default="" %}} +The passphrase to use when encoding stored credentials. Should be exactly 32 characters long. +{{< highlight toml >}} +[http.services.siteacc.security] +creds_passphrase = "supersecretpasswordthatyouknow!" +{{< /highlight >}} +{{% /dir %}} + ## GOCDB settings {{% dir name="url" type="string" default="" %}} The external URL of the central GOCDB instance. @@ -111,12 +120,20 @@ driver = "file" {{< /highlight >}} {{% /dir %}} -### Storage settings - File driver -{{% dir name="file" type="string" default="" %}} -The file location. +### Storage settings - File drivers +{{% dir name="sites_file" type="string" default="" %}} +The sites file location. +{{< highlight toml >}} +[http.services.siteacc.storage.file] +sites_file = "/var/reva/sites.json" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="accounts_file" type="string" default="" %}} +The accounts file location. {{< highlight toml >}} [http.services.siteacc.storage.file] -file = "/var/reva/accounts.json" +accounts_file = "/var/reva/accounts.json" {{< /highlight >}} {{% /dir %}} diff --git a/examples/siteacc/siteacc.toml b/examples/siteacc/siteacc.toml index cf081e748e..68a0c52e4f 100644 --- a/examples/siteacc/siteacc.toml +++ b/examples/siteacc/siteacc.toml @@ -1,6 +1,11 @@ [http] address = "0.0.0.0:9600" +# Security settings +[http.services.siteacc.security] +creds_passphrase = "superdupersecret" + +# Connection to the GOCDB [http.services.siteacc.gocdb] url = "https://sciencemesh-test.uni-muenster.de/gocdb/" write_url = "https://sciencemesh-test.uni-muenster.de/gocdbpi/" @@ -10,7 +15,8 @@ apikey = "verysecret" [http.services.siteacc.storage] driver = "file" [http.services.siteacc.storage.file] -file = "/var/revad/accounts.json" +sites_file = "/var/revad/sites.json" +accounts_file = "/var/revad/accounts.json" # Email related settings [http.services.siteacc.email] diff --git a/pkg/siteacc/account/manage/template.go b/pkg/siteacc/account/manage/template.go index 0d73b00c9d..5dc597dca7 100644 --- a/pkg/siteacc/account/manage/template.go +++ b/pkg/siteacc/account/manage/template.go @@ -19,7 +19,7 @@ package manage const tplJavaScript = ` -function handleSettings() { +function handleAccountSettings() { setState(STATE_STATUS, "Redirecting to the account settings..."); window.location.replace("{{getServerAddress}}/account/?path=settings"); } @@ -29,9 +29,14 @@ function handleEditAccount() { window.location.replace("{{getServerAddress}}/account/?path=edit"); } -function handleRequestAccess() { +function handleSiteSettings() { + setState(STATE_STATUS, "Redirecting to the site settings..."); + window.location.replace("{{getServerAddress}}/account/?path=site"); +} + +function handleRequestAccess(scope) { setState(STATE_STATUS, "Redirecting to the contact form..."); - window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request GOCDB access")); + window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request " + scope + " access")); } function handleLogout() { @@ -58,6 +63,9 @@ const tplStyleSheet = ` html * { font-family: arial !important; } +button { + min-width: 170px; +} ` const tplBody = ` @@ -81,24 +89,44 @@ const tplBody = `
Account data:
- - -   - - - +
+ + +   + + {{if .Account.Data.SiteAccess}} + +   + {{end}} + + +
+
+ + +
-
-

Quick links:

- +
+
+
Notes:
+
    +
  • The Site access allows you to access and modify the global configuration of your site.
  • +
  • The GOCDB access allows you to log into the central database where all site metadata is stored.
  • +
+
+
+
Quick links:
+ +
` diff --git a/pkg/siteacc/account/panel.go b/pkg/siteacc/account/panel.go index 040ed8ddc8..a3c29ef55e 100644 --- a/pkg/siteacc/account/panel.go +++ b/pkg/siteacc/account/panel.go @@ -29,6 +29,7 @@ import ( "github.com/cs3org/reva/pkg/siteacc/account/manage" "github.com/cs3org/reva/pkg/siteacc/account/registration" "github.com/cs3org/reva/pkg/siteacc/account/settings" + "github.com/cs3org/reva/pkg/siteacc/account/site" "github.com/cs3org/reva/pkg/siteacc/config" "github.com/cs3org/reva/pkg/siteacc/data" "github.com/cs3org/reva/pkg/siteacc/html" @@ -50,6 +51,7 @@ const ( templateManage = "manage" templateSettings = "settings" templateEdit = "edit" + templateSite = "site" templateContact = "contact" templateRegistration = "register" ) @@ -84,6 +86,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(templateSite, &site.PanelTemplate{}); err != nil { + return errors.Wrap(err, "unable to create the site template") + } + if err := panel.htmlPanel.AddTemplate(templateContact, &contact.PanelTemplate{}); err != nil { return errors.Wrap(err, "unable to create the contact template") } @@ -97,7 +103,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, templateSettings, templateEdit, templateContact, templateRegistration} + validPaths := []string{templateLogin, templateManage, templateSettings, templateEdit, templateSite, templateContact, templateRegistration} template := templateLogin // Only allow valid template paths; redirect to the login page otherwise @@ -113,18 +119,28 @@ 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, templateSettings, templateEdit, templateContact} + protectedPaths := []string{templateManage, templateSettings, templateEdit, templateSite, templateContact} + + if user := session.LoggedInUser(); user != nil { + switch path { + case templateSite: + // If the logged in user doesn't have site access, redirect him back to the main account page + if !user.Account.Data.SiteAccess { + return panel.redirect(templateManage, w, r), nil + } - if session.LoggedInUser == nil { + case templateLogin: + case 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 + } + } else { // 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 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 } return html.ContinueExecution, nil @@ -144,6 +160,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm } type TemplateData struct { + Site *data.Site Account *data.Account Params map[string]string @@ -151,12 +168,18 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm Sites []data.SiteInformation } - return TemplateData{ - Account: session.LoggedInUser, + tplData := TemplateData{ + Site: nil, + Account: nil, Params: flatValues, Titles: []string{"Mr", "Mrs", "Ms", "Prof", "Dr"}, Sites: availSites, } + if user := session.LoggedInUser(); user != nil { + tplData.Site = panel.cloneUserSite(user.Site) + tplData.Account = user.Account + } + return tplData } return panel.htmlPanel.Execute(w, r, session, dataProvider) } @@ -179,11 +202,22 @@ func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request return html.AbortExecution } +func (panel *Panel) cloneUserSite(site *data.Site) *data.Site { + // Clone the user's site and decrypt the credentials for the panel + siteClone := site.Clone(true) + id, secret, err := site.Config.TestClientCredentials.Get(panel.conf.Security.CredentialsPassphrase) + if err == nil { + siteClone.Config.TestClientCredentials.ID = id + siteClone.Config.TestClientCredentials.Secret = secret + } + return siteClone +} + // 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 account panel") + return nil, errors.Wrap(err, "unable to initialize the account panel") } return form, nil } diff --git a/pkg/siteacc/account/settings/template.go b/pkg/siteacc/account/settings/template.go index 182dc73c72..98fec5d61b 100644 --- a/pkg/siteacc/account/settings/template.go +++ b/pkg/siteacc/account/settings/template.go @@ -70,7 +70,7 @@ const tplBody = `
 
-
+

Notification settings


diff --git a/pkg/siteacc/account/site/site.go b/pkg/siteacc/account/site/site.go new file mode 100644 index 0000000000..fc540a7fa3 --- /dev/null +++ b/pkg/siteacc/account/site/site.go @@ -0,0 +1,51 @@ +// 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 site + +import "github.com/cs3org/reva/pkg/siteacc/html" + +// PanelTemplate is the content provider for the edit form. +type PanelTemplate struct { + html.ContentProvider +} + +// GetTitle returns the title of the panel. +func (template *PanelTemplate) GetTitle() string { + return "ScienceMesh Site Configuration" +} + +// GetCaption returns the caption which is displayed on the panel. +func (template *PanelTemplate) GetCaption() string { + return "Configure your ScienceMesh Site!" +} + +// 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/site/template.go b/pkg/siteacc/account/site/template.go new file mode 100644 index 0000000000..fef2c646cc --- /dev/null +++ b/pkg/siteacc/account/site/template.go @@ -0,0 +1,117 @@ +// 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 site + +const tplJavaScript = ` +function verifyForm(formData) { + if (formData.getTrimmed("clientID") == "") { + setState(STATE_ERROR, "Please enter the name of the test user.", "form", "clientID", true); + return false; + } + + if (formData.get("secret") == "") { + setState(STATE_ERROR, "Please enter the password of the test user.", "form", "secret", true); + return false; + } + + return true; +} + +function handleAction(action) { + const formData = new FormData(document.querySelector("form")); + if (!verifyForm(formData)) { + return; + } + + setState(STATE_STATUS, "Configuring site... this should only take a moment.", "form", null, false); + + var xhr = new XMLHttpRequest(); + xhr.open("POST", "{{getServerAddress}}/" + action); + xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8'); + + xhr.onload = function() { + if (this.status == 200) { + setState(STATE_SUCCESS, "Your site was successfully configured!", "form", null, true); + } else { + var resp = JSON.parse(this.responseText); + setState(STATE_ERROR, "An error occurred while trying to configure your site:
" + resp.error + "", "form", null, true); + } + } + + var postData = { + "config": { + "testClientCredentials": { + "id": formData.getTrimmed("clientID"), + "secret": formData.get("secret") + } + } + }; + + xhr.send(JSON.stringify(postData)); +} +` + +const tplStyleSheet = ` +html * { + font-family: arial !important; +} + +input[type="checkbox"] { + width: auto; +} + +.mandatory { + color: red; + font-weight: bold; +} +` + +const tplBody = ` +
+

Configure your ScienceMesh Site below. These settings affect your entire site and not just your account.

+
+
 
+
+ +
+

Test user settings

+

In order to perform automated tests on your site, a test user has to be configured below. Please note that the user has to exist in your Reva instance! If you do not have a user for automated tests in your instance yet, create one first.

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

Go back to the main account page.

+
+` diff --git a/pkg/siteacc/admin/panel.go b/pkg/siteacc/admin/panel.go index 3e0e1d6c5d..d322674aad 100644 --- a/pkg/siteacc/admin/panel.go +++ b/pkg/siteacc/admin/panel.go @@ -109,7 +109,7 @@ func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *htm 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") + return nil, errors.Wrap(err, "unable to initialize the administration panel") } return panel, nil } diff --git a/pkg/siteacc/admin/template.go b/pkg/siteacc/admin/template.go index 7dbfcb99c6..b1ba600fa3 100644 --- a/pkg/siteacc/admin/template.go +++ b/pkg/siteacc/admin/template.go @@ -50,11 +50,11 @@ html * { ` const tplBody = ` -
+
    {{range .Accounts}}
  • -

    +

    {{.Email}}
    {{.Title}}. {{.FirstName}} {{.LastName}} (Joined: {{.DateCreated.Format "Jan 02, 2006 15:04"}}; Last modified: {{.DateModified.Format "Jan 02, 2006 15:04"}}) @@ -66,21 +66,38 @@ const tplBody = `
  • Phone: {{.PhoneNumber}}
-

-

- GOCDB access: {{if .Data.GOCDBAccess}}Granted{{else}}Not granted{{end}} -

-

+

+ +
 
+ +
+ Account data: +
    +
  • Site access: {{if .Data.SiteAccess}}Granted{{else}}Not granted{{end}}
  • +
  • GOCDB access: {{if .Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}
  • +
+
+ +
 
+ +
+ {{if .Data.SiteAccess}} + + {{else}} + + {{end}} + {{if .Data.GOCDBAccess}} {{else}} {{end}} +  
-

+

{{end}} diff --git a/pkg/siteacc/alerting/dispatcher.go b/pkg/siteacc/alerting/dispatcher.go index b09879abfd..2ae350b42c 100644 --- a/pkg/siteacc/alerting/dispatcher.go +++ b/pkg/siteacc/alerting/dispatcher.go @@ -121,7 +121,7 @@ func (dispatcher *Dispatcher) dispatchAlert(alert template.Alert, account *data. func NewDispatcher(conf *config.Configuration, log *zerolog.Logger) (*Dispatcher, error) { dispatcher := &Dispatcher{} if err := dispatcher.initialize(conf, log); err != nil { - return nil, errors.Wrapf(err, "unable to initialize the alerts dispatcher") + return nil, errors.Wrap(err, "unable to initialize the alerts dispatcher") } return dispatcher, nil } diff --git a/pkg/siteacc/config/config.go b/pkg/siteacc/config/config.go index ba746f018d..4c9484c0f8 100644 --- a/pkg/siteacc/config/config.go +++ b/pkg/siteacc/config/config.go @@ -28,11 +28,16 @@ import ( type Configuration struct { Prefix string `mapstructure:"prefix"` + Security struct { + CredentialsPassphrase string `mapstructure:"creds_passphrase"` + } `mapstructure:"security"` + Storage struct { Driver string `mapstructure:"driver"` File struct { - File string `mapstructure:"file"` + SitesFile string `mapstructure:"sites_file"` + AccountsFile string `mapstructure:"accounts_file"` } `mapstructure:"file"` } `mapstructure:"storage"` diff --git a/pkg/siteacc/config/endpoints.go b/pkg/siteacc/config/endpoints.go index 65252be146..1186ea8994 100644 --- a/pkg/siteacc/config/endpoints.go +++ b/pkg/siteacc/config/endpoints.go @@ -38,6 +38,11 @@ const ( // EndpointRemove is the endpoint path for account removal. EndpointRemove = "/remove" + // EndpointSiteGet is the endpoint path for retrieving site data. + EndpointSiteGet = "/site-get" + // EndpointSiteConfigure is the endpoint path for site configuration. + EndpointSiteConfigure = "/site-configure" + // EndpointLogin is the endpoint path for (internal) user login. EndpointLogin = "/login" // EndpointLogout is the endpoint path for (internal) user logout. @@ -50,6 +55,8 @@ const ( // EndpointVerifyUserToken is the endpoint path for user token validation. EndpointVerifyUserToken = "/verify-user-token" + // EndpointGrantSiteAccess is the endpoint path for granting or revoking Site access. + EndpointGrantSiteAccess = "/grant-site-access" // EndpointGrantGOCDBAccess is the endpoint path for granting or revoking GOCDB access. EndpointGrantGOCDBAccess = "/grant-gocdb-access" diff --git a/pkg/siteacc/credentials/credentials.go b/pkg/siteacc/credentials/credentials.go new file mode 100644 index 0000000000..cb67c0685a --- /dev/null +++ b/pkg/siteacc/credentials/credentials.go @@ -0,0 +1,69 @@ +// 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 credentials + +import ( + "github.com/cs3org/reva/pkg/siteacc/credentials/crypto" + "github.com/pkg/errors" +) + +// Credentials stores and en-/decrypts credentials +type Credentials struct { + ID string `json:"id"` + Secret string `json:"secret"` +} + +// Get decrypts and retrieves the stored credentials. +func (creds *Credentials) Get(passphrase string) (string, string, error) { + id, err := crypto.DecodeString(creds.ID, passphrase) + if err != nil { + return "", "", errors.Wrap(err, "unable to decode ID") + } + secret, err := crypto.DecodeString(creds.Secret, passphrase) + if err != nil { + return "", "", errors.Wrap(err, "unable to decode secret") + } + return id, secret, nil +} + +// Set encrypts and sets new credentials. +func (creds *Credentials) Set(id, secret string, passphrase string) error { + if s, err := crypto.EncodeString(id, passphrase); err == nil { + creds.ID = s + } else { + return errors.Wrap(err, "unable to encode ID") + } + if s, err := crypto.EncodeString(secret, passphrase); err == nil { + creds.Secret = s + } else { + return errors.Wrap(err, "unable to encode secret") + } + return nil +} + +// IsValid checks whether the credentials are valid. +func (creds *Credentials) IsValid() bool { + return len(creds.ID) > 0 && len(creds.Secret) > 0 +} + +// Clear resets the credentials. +func (creds *Credentials) Clear() { + creds.ID = "" + creds.Secret = "" +} diff --git a/pkg/siteacc/credentials/crypto/crypto.go b/pkg/siteacc/credentials/crypto/crypto.go new file mode 100644 index 0000000000..af73999942 --- /dev/null +++ b/pkg/siteacc/credentials/crypto/crypto.go @@ -0,0 +1,101 @@ +// 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 crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "io" + + "github.com/pkg/errors" +) + +const ( + passphraseLength = 32 +) + +// EncodeString encodes a string using AES and returns the base64-encoded result. +func EncodeString(s string, passphrase string) (string, error) { + if len(s) == 0 || len(passphrase) == 0 { + return "", nil + } + passphrase = normalizePassphrase(passphrase) + + gcm, err := createGCM([]byte(passphrase)) + if err != nil { + return "", err + } + + nonce := make([]byte, gcm.NonceSize()) + if _, err = io.ReadFull(rand.Reader, nonce); err != nil { + return "", errors.Wrap(err, "unable to generate nonce") + } + encryptedData := gcm.Seal(nonce, nonce, []byte(s), nil) + return base64.StdEncoding.EncodeToString(encryptedData), nil +} + +// DecodeString decodes a base64-encoded string encoded with AES. +func DecodeString(s string, passphrase string) (string, error) { + if len(s) == 0 || len(passphrase) == 0 { + return "", nil + } + data, _ := base64.StdEncoding.DecodeString(s) + passphrase = normalizePassphrase(passphrase) + + gcm, err := createGCM([]byte(passphrase)) + if err != nil { + return "", err + } + + nonceSize := gcm.NonceSize() + if len(s) < nonceSize { + return "", errors.Errorf("input string length too short") + } + nonce, data := data[:nonceSize], data[nonceSize:] + plain, err := gcm.Open(nil, nonce, data, nil) + if err != nil { + return "", errors.Wrap(err, "unable to decode string") + } + return string(plain), nil +} + +func createGCM(passphrase []byte) (cipher.AEAD, error) { + c, err := aes.NewCipher(passphrase) + if err != nil { + return nil, errors.Wrap(err, "unable to generate cipher") + } + gcm, err := cipher.NewGCM(c) + if err != nil { + return nil, errors.Wrap(err, "unable to generate GCM") + } + return gcm, nil +} + +func normalizePassphrase(passphrase string) string { + if len(passphrase) > passphraseLength { + passphrase = passphrase[:passphraseLength] + } else if len(passphrase) < passphraseLength { + for i := len(passphrase); i < passphraseLength; i++ { + passphrase += "#" + } + } + return passphrase +} diff --git a/pkg/siteacc/password/password.go b/pkg/siteacc/credentials/password.go similarity index 99% rename from pkg/siteacc/password/password.go rename to pkg/siteacc/credentials/password.go index a9a59467e4..f9ca8462e6 100644 --- a/pkg/siteacc/password/password.go +++ b/pkg/siteacc/credentials/password.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package password +package credentials import ( "strings" diff --git a/pkg/siteacc/data/account.go b/pkg/siteacc/data/account.go index 8b21814b6a..8ea5f10912 100644 --- a/pkg/siteacc/data/account.go +++ b/pkg/siteacc/data/account.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/cs3org/reva/pkg/siteacc/password" + "github.com/cs3org/reva/pkg/siteacc/credentials" "github.com/pkg/errors" "github.com/cs3org/reva/pkg/utils" @@ -38,7 +38,7 @@ type Account struct { Role string `json:"role"` PhoneNumber string `json:"phoneNumber"` - Password password.Password `json:"password"` + Password credentials.Password `json:"password"` DateCreated time.Time `json:"dateCreated"` DateModified time.Time `json:"dateModified"` @@ -50,6 +50,7 @@ type Account struct { // AccountData holds additional data for a site account. type AccountData struct { GOCDBAccess bool `json:"gocdbAccess"` + SiteAccess bool `json:"siteAccess"` } // AccountSettings holds additional settings for a site account. @@ -124,6 +125,9 @@ func (acc *Account) CheckScopeAccess(scope string) bool { case ScopeGOCDB: hasAccess = acc.Data.GOCDBAccess + + case ScopeSite: + hasAccess = acc.Data.SiteAccess } return hasAccess @@ -198,6 +202,7 @@ func NewAccount(email string, title, firstName, lastName string, site, role stri DateModified: t, Data: AccountData{ GOCDBAccess: false, + SiteAccess: false, }, Settings: AccountSettings{ ReceiveAlerts: true, diff --git a/pkg/siteacc/data/filestorage.go b/pkg/siteacc/data/filestorage.go index e7413ac5f6..66f3a4190b 100644 --- a/pkg/siteacc/data/filestorage.go +++ b/pkg/siteacc/data/filestorage.go @@ -36,7 +36,8 @@ type FileStorage struct { conf *config.Configuration log *zerolog.Logger - filePath string + sitesFilePath string + accountsFilePath string } func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog.Logger) error { @@ -50,66 +51,115 @@ func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog. } storage.log = log - if conf.Storage.File.File == "" { - return errors.Errorf("no file set in the configuration") + if conf.Storage.File.SitesFile == "" { + return errors.Errorf("no sites file set in the configuration") } - storage.filePath = conf.Storage.File.File + storage.sitesFilePath = conf.Storage.File.SitesFile - // Create the file directory if necessary - dir := filepath.Dir(storage.filePath) - _ = os.MkdirAll(dir, 0755) + if conf.Storage.File.AccountsFile == "" { + return errors.Errorf("no accounts file set in the configuration") + } + storage.accountsFilePath = conf.Storage.File.AccountsFile + + // Create the file directories if necessary + _ = os.MkdirAll(filepath.Dir(storage.sitesFilePath), 0755) + _ = os.MkdirAll(filepath.Dir(storage.accountsFilePath), 0755) return nil } -// ReadAll reads all stored accounts into the given data object. -func (storage *FileStorage) ReadAll() (*Accounts, error) { - accounts := &Accounts{} - - // Read the data from the configured file - jsonData, err := ioutil.ReadFile(storage.filePath) +func (storage *FileStorage) readData(file string, obj interface{}) error { + // Read the data from the specified file + jsonData, err := ioutil.ReadFile(file) if err != nil { - return nil, errors.Wrapf(err, "unable to read file %v", storage.filePath) + return errors.Wrapf(err, "unable to read file %v", file) } - if err := json.Unmarshal(jsonData, accounts); err != nil { - return nil, errors.Wrapf(err, "invalid file %v", storage.filePath) + if err := json.Unmarshal(jsonData, obj); err != nil { + return errors.Wrapf(err, "invalid file %v", file) } + return nil +} + +// ReadSites reads all stored sites into the given data object. +func (storage *FileStorage) ReadSites() (*Sites, error) { + sites := &Sites{} + if err := storage.readData(storage.sitesFilePath, sites); err != nil { + return nil, errors.Wrap(err, "error reading sites") + } + return sites, nil +} + +// ReadAccounts reads all stored accounts into the given data object. +func (storage *FileStorage) ReadAccounts() (*Accounts, error) { + accounts := &Accounts{} + if err := storage.readData(storage.accountsFilePath, accounts); err != nil { + return nil, errors.Wrap(err, "error reading accounts") + } return accounts, nil } -// WriteAll writes all stored accounts from the given data object. -func (storage *FileStorage) WriteAll(accounts *Accounts) error { - // Write the data to the configured file - jsonData, _ := json.MarshalIndent(accounts, "", "\t") - if err := ioutil.WriteFile(storage.filePath, jsonData, 0755); err != nil { - return errors.Wrapf(err, "unable to write file %v", storage.filePath) +func (storage *FileStorage) writeData(file string, obj interface{}) error { + // Write the data to the specified file + jsonData, _ := json.MarshalIndent(obj, "", "\t") + if err := ioutil.WriteFile(file, jsonData, 0755); err != nil { + return errors.Wrapf(err, "unable to write file %v", file) } + return nil +} + +// WriteSites writes all stored sites from the given data object. +func (storage *FileStorage) WriteSites(sites *Sites) error { + if err := storage.writeData(storage.sitesFilePath, sites); err != nil { + return errors.Wrap(err, "error writing sites") + } + return nil +} +// WriteAccounts writes all stored accounts from the given data object. +func (storage *FileStorage) WriteAccounts(accounts *Accounts) error { + if err := storage.writeData(storage.accountsFilePath, accounts); err != nil { + return errors.Wrap(err, "error writing accounts") + } return nil } +// SiteAdded is called when a site has been added. +func (storage *FileStorage) SiteAdded(site *Site) { + // Simply skip this action; all data is saved solely in WriteSites +} + +// SiteUpdated is called when a site has been updated. +func (storage *FileStorage) SiteUpdated(site *Site) { + // Simply skip this action; all data is saved solely in WriteSites +} + +// SiteRemoved is called when a site has been removed. +func (storage *FileStorage) SiteRemoved(site *Site) { + // Simply skip this action; all data is saved solely in WriteSites +} + // AccountAdded is called when an account has been added. func (storage *FileStorage) AccountAdded(account *Account) { - // Simply skip this action; all data is saved solely in WriteAll + // Simply skip this action; all data is saved solely in WriteAccounts } // AccountUpdated is called when an account has been updated. func (storage *FileStorage) AccountUpdated(account *Account) { - // Simply skip this action; all data is saved solely in WriteAll + // Simply skip this action; all data is saved solely in WriteAccounts } // AccountRemoved is called when an account has been removed. func (storage *FileStorage) AccountRemoved(account *Account) { - // Simply skip this action; all data is saved solely in WriteAll + // Simply skip this action; all data is saved solely in WriteAccounts } -// NewFileStorage creates a new filePath storage. +// NewFileStorage creates a new file storage. 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") + return nil, errors.Wrap(err, "unable to initialize the file storage") } return storage, nil } diff --git a/pkg/siteacc/data/scopes.go b/pkg/siteacc/data/scopes.go index f515fb68f1..438c9a1c53 100644 --- a/pkg/siteacc/data/scopes.go +++ b/pkg/siteacc/data/scopes.go @@ -23,4 +23,6 @@ const ( ScopeDefault = "" // ScopeGOCDB is used to access the GOCDB. ScopeGOCDB = "gocdb" + // ScopeSite is used to access the global site configuration. + ScopeSite = "site" ) diff --git a/pkg/siteacc/data/site.go b/pkg/siteacc/data/site.go new file mode 100644 index 0000000000..8e003d985d --- /dev/null +++ b/pkg/siteacc/data/site.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 data + +import ( + "github.com/cs3org/reva/pkg/siteacc/credentials" + "github.com/pkg/errors" +) + +// Site represents the global site-specific settings stored in the service. +type Site struct { + ID string `json:"id"` + + Config SiteConfiguration `json:"config"` +} + +// SiteConfiguration stores the global configuration of a site. +type SiteConfiguration struct { + TestClientCredentials credentials.Credentials `json:"testClientCredentials"` +} + +// Sites holds an array of sites. +type Sites = []*Site + +// Update copies the data of the given site to this site. +func (site *Site) Update(other *Site, credsPassphrase string) error { + if other.Config.TestClientCredentials.IsValid() { + // If credentials were provided, use those as the new ones + if err := site.UpdateTestClientCredentials(other.Config.TestClientCredentials.ID, other.Config.TestClientCredentials.Secret, credsPassphrase); err != nil { + return err + } + } + + return nil +} + +// UpdateTestClientCredentials assigns new test client credentials, encrypting the information first. +func (site *Site) UpdateTestClientCredentials(id, secret string, passphrase string) error { + if err := site.Config.TestClientCredentials.Set(id, secret, passphrase); err != nil { + return errors.Wrap(err, "unable to update the test client credentials") + } + return nil +} + +// Clone creates a copy of the site; if eraseCredentials is set to true, the (test user) credentials will be cleared in the cloned object. +func (site *Site) Clone(eraseCredentials bool) *Site { + clone := *site + + if eraseCredentials { + clone.Config.TestClientCredentials.Clear() + } + + return &clone +} + +// NewSite creates a new site. +func NewSite(id string) (*Site, error) { + site := &Site{ + ID: id, + Config: SiteConfiguration{ + TestClientCredentials: credentials.Credentials{}, + }, + } + return site, nil +} diff --git a/pkg/siteacc/data/sites.go b/pkg/siteacc/data/siteinfo.go similarity index 100% rename from pkg/siteacc/data/sites.go rename to pkg/siteacc/data/siteinfo.go diff --git a/pkg/siteacc/data/storage.go b/pkg/siteacc/data/storage.go index 221e7bcb32..7d770a3f2f 100644 --- a/pkg/siteacc/data/storage.go +++ b/pkg/siteacc/data/storage.go @@ -18,12 +18,24 @@ package data -// Storage defines the interface for accounts storages. +// Storage defines the interface for sites and accounts storages. type Storage interface { - // ReadAll reads all stored accounts into the given data object. - ReadAll() (*Accounts, error) - // WriteAll writes all stored accounts from the given data object. - WriteAll(accounts *Accounts) error + // ReadSites reads all stored sites into the given data object. + ReadSites() (*Sites, error) + // WriteSites writes all stored sites from the given data object. + WriteSites(sites *Sites) error + + // SiteAdded is called when a site has been added. + SiteAdded(site *Site) + // SiteUpdated is called when a site has been updated. + SiteUpdated(site *Site) + // SiteRemoved is called when a site has been removed. + SiteRemoved(site *Site) + + // ReadAccounts reads all stored accounts into the given data object. + ReadAccounts() (*Accounts, error) + // WriteAccounts writes all stored accounts from the given data object. + WriteAccounts(accounts *Accounts) error // AccountAdded is called when an account has been added. AccountAdded(account *Account) diff --git a/pkg/siteacc/email/email.go b/pkg/siteacc/email/email.go index 9de8d17747..f8c681d0e9 100644 --- a/pkg/siteacc/email/email.go +++ b/pkg/siteacc/email/email.go @@ -55,6 +55,11 @@ func SendAccountCreated(account *data.Account, recipients []string, params map[s return send(recipients, "ScienceMesh: Site Administrator Account created", accountCreatedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) } +// SendSiteAccessGranted sends an email about granted Site access. +func SendSiteAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error { + return send(recipients, "ScienceMesh: Site access granted", siteAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP) +} + // SendGOCDBAccessGranted sends an email about granted GOCDB access. 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) diff --git a/pkg/siteacc/email/template.go b/pkg/siteacc/email/template.go index 24efa65476..6912d1dfb2 100644 --- a/pkg/siteacc/email/template.go +++ b/pkg/siteacc/email/template.go @@ -32,6 +32,18 @@ Kind regards, The ScienceMesh Team ` +const siteAccessGrantedTemplate = ` +Dear {{.Account.FirstName}} {{.Account.LastName}}, + +You have been granted access to the global configuration of your site. + +Log in to your account to access this configuration: +{{.AccountsAddress}} + +Kind regards, +The ScienceMesh Team +` + const gocdbAccessGrantedTemplate = ` Dear {{.Account.FirstName}} {{.Account.LastName}}, diff --git a/pkg/siteacc/endpoints.go b/pkg/siteacc/endpoints.go index 9cbd72c0cf..0079843bc1 100644 --- a/pkg/siteacc/endpoints.go +++ b/pkg/siteacc/endpoints.go @@ -29,6 +29,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" "github.com/prometheus/alertmanager/template" ) @@ -38,6 +39,7 @@ const ( ) type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error) +type accessSetterCallback = func(*manager.AccountsManager, *data.Account, bool) error type endpoint struct { Path string @@ -72,6 +74,9 @@ func getEndpoints() []endpoint { {config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false}, {config.EndpointConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleConfigure), false}, {config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false}, + // Site endpoints + {config.EndpointSiteGet, callMethodEndpoint, createMethodCallbacks(handleSiteGet, nil), false}, + {config.EndpointSiteConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleSiteConfigure), false}, // Login endpoints {config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true}, {config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true}, @@ -80,6 +85,7 @@ func getEndpoints() []endpoint { // Authentication endpoints {config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(handleVerifyUserToken, nil), true}, // Access management endpoints + {config.EndpointGrantSiteAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantSiteAccess), false}, {config.EndpointGrantGOCDBAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantGOCDBAccess), false}, // Alerting endpoints {config.EndpointDispatchAlert, callMethodEndpoint, createMethodCallbacks(nil, handleDispatchAlert), false}, @@ -228,6 +234,42 @@ func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session return nil, nil } +func handleSiteGet(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + siteID := values.Get("site") + if siteID == "" { + return nil, errors.Errorf("no site specified") + } + site := siteacc.SitesManager().FindSite(siteID) + if site == nil { + return nil, errors.Errorf("no site with ID %v exists", siteID) + } + return map[string]interface{}{"site": site.Clone(false)}, nil +} + +func handleSiteConfigure(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + email, _, err := processInvoker(siteacc, values, session) + if err != nil { + return nil, err + } + account, err := siteacc.AccountsManager().FindAccount(manager.FindByEmail, email) + if err != nil { + return nil, err + } + + siteData := &data.Site{} + if err := json.Unmarshal(body, siteData); err != nil { + return nil, errors.Wrap(err, "invalid form data") + } + siteData.ID = account.Site + + // Configure the site through the sites manager + if err := siteacc.SitesManager().UpdateSite(siteData); err != nil { + return nil, errors.Wrap(err, "unable to configure site") + } + + return nil, nil +} + func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { @@ -264,7 +306,7 @@ func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, } func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { - if session.LoggedInUser == nil { + if !session.IsUserLoggedIn() { return nil, errors.Errorf("no user is currently logged in") } @@ -278,7 +320,7 @@ func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, sessio } // Send an email through the accounts manager - siteacc.AccountsManager().SendContactForm(session.LoggedInUser, strings.TrimSpace(contactData.Subject), strings.TrimSpace(contactData.Message)) + siteacc.AccountsManager().SendContactForm(session.LoggedInUser().Account, strings.TrimSpace(contactData.Subject), strings.TrimSpace(contactData.Message)) return nil, nil } @@ -316,7 +358,15 @@ func handleDispatchAlert(siteacc *SiteAccounts, values url.Values, body []byte, return nil, nil } +func handleGrantSiteAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + return handleGrantAccess((*manager.AccountsManager).GrantSiteAccess, siteacc, values, body, session) +} + func handleGrantGOCDBAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { + return handleGrantAccess((*manager.AccountsManager).GrantGOCDBAccess, siteacc, values, body, session) +} + +func handleGrantAccess(accessSetter accessSetterCallback, siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) { account, err := unmarshalRequestData(body) if err != nil { return nil, err @@ -336,8 +386,8 @@ func handleGrantGOCDBAccess(siteacc *SiteAccounts, values url.Values, body []byt } // 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") + if err := accessSetter(siteacc.AccountsManager(), account, grantAccess); err != nil { + return nil, errors.Wrap(err, "unable to change the access status of the account") } } else { return nil, errors.Errorf("no access status provided") @@ -375,11 +425,11 @@ func processInvoker(siteacc *SiteAccounts, values url.Values, session *html.Sess switch strings.ToLower(values.Get("invoker")) { case invokerUser: // If this endpoint was called by the user, set the account email from the stored session - if session.LoggedInUser == nil { + if !session.IsUserLoggedIn() { return "", false, errors.Errorf("no user is currently logged in") } - email = session.LoggedInUser.Email + email = session.LoggedInUser().Account.Email invokedByUser = true default: diff --git a/pkg/siteacc/html/panel.go b/pkg/siteacc/html/panel.go index a70c55aadd..f4b9fce13b 100644 --- a/pkg/siteacc/html/panel.go +++ b/pkg/siteacc/html/panel.go @@ -164,7 +164,7 @@ func (panel *Panel) getFullTemplateName(name string) string { 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") + return nil, errors.Wrap(err, "unable to initialize the panel") } return panel, nil } diff --git a/pkg/siteacc/html/session.go b/pkg/siteacc/html/session.go index 0f6baf744a..82fc999386 100644 --- a/pkg/siteacc/html/session.go +++ b/pkg/siteacc/html/session.go @@ -37,16 +37,22 @@ type Session struct { CreationTime time.Time Timeout time.Duration - LoggedInUser *data.Account - Data map[string]interface{} + loggedInUser *SessionUser + expirationTime time.Time halflifeTime time.Time sessionCookieName string } +// SessionUser holds information about the logged in user +type SessionUser struct { + Account *data.Account + Site *data.Site +} + func getRemoteAddress(r *http.Request) string { // Remove the port number from the remote address remoteAddress := "" @@ -56,6 +62,29 @@ func getRemoteAddress(r *http.Request) string { return remoteAddress } +// LoggedInUser retrieves the currently logged in user or nil if none is logged in. +func (sess *Session) LoggedInUser() *SessionUser { + return sess.loggedInUser +} + +// LoginUser logs in the provided user. +func (sess *Session) LoginUser(acc *data.Account, site *data.Site) { + sess.loggedInUser = &SessionUser{ + Account: acc, + Site: site, + } +} + +// LogoutUser logs out the currently logged in user. +func (sess *Session) LogoutUser() { + sess.loggedInUser = nil +} + +// IsUserLoggedIn tells whether a user is currently logged in. +func (sess *Session) IsUserLoggedIn() bool { + return sess.loggedInUser != nil +} + // Save stores the session ID in a cookie using a response writer. func (sess *Session) Save(cookiePath string, w http.ResponseWriter) { fullURL, _ := url.Parse(cookiePath) @@ -108,6 +137,7 @@ func NewSession(name string, timeout time.Duration, r *http.Request) *Session { CreationTime: time.Now(), Timeout: timeout, Data: make(map[string]interface{}, 10), + loggedInUser: nil, expirationTime: time.Now().Add(timeout), halflifeTime: time.Now().Add(timeout / 2), sessionCookieName: name, diff --git a/pkg/siteacc/html/sessionmanager.go b/pkg/siteacc/html/sessionmanager.go index 3c9c6d5d18..7879c35782 100644 --- a/pkg/siteacc/html/sessionmanager.go +++ b/pkg/siteacc/html/sessionmanager.go @@ -157,9 +157,14 @@ func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (* // Carry over the old session information, thus preserving the existing session sessionNew.MigrationID = session.ID - sessionNew.LoggedInUser = session.LoggedInUser sessionNew.Data = session.Data + if user := session.LoggedInUser(); user != nil { + sessionNew.LoginUser(user.Account, user.Site) + } else { + sessionNew.LogoutUser() + } + // Delete the old session delete(mngr.sessions, session.ID) @@ -180,7 +185,7 @@ func (mngr *SessionManager) logSessionInfo(session *Session, r *http.Request, in 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 nil, errors.Wrap(err, "unable to initialize the session manager") } return mngr, nil } diff --git a/pkg/siteacc/manager/accmanager.go b/pkg/siteacc/manager/accmanager.go index 5d64840b24..f4fc30b63b 100644 --- a/pkg/siteacc/manager/accmanager.go +++ b/pkg/siteacc/manager/accmanager.go @@ -43,16 +43,17 @@ type AccountsManager struct { conf *config.Configuration log *zerolog.Logger + storage data.Storage + accounts data.Accounts accountsListeners []AccountsListener - storage data.Storage smtp *smtpclient.SMTPCredentials mutex sync.RWMutex } -func (mngr *AccountsManager) initialize(conf *config.Configuration, log *zerolog.Logger) error { +func (mngr *AccountsManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -63,15 +64,13 @@ func (mngr *AccountsManager) initialize(conf *config.Configuration, log *zerolog } mngr.log = log - 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 { - mngr.storage = storage - mngr.readAllAccounts() - } else { - return errors.Wrap(err, "unable to create accounts storage") + if storage == nil { + return errors.Errorf("no storage provided") } + mngr.storage = storage + + mngr.accounts = make(data.Accounts, 0, 32) // Reserve some space for accounts + mngr.readAllAccounts() // Register accounts listeners if listener, err := gocdb.NewListener(mngr.conf, mngr.log); err == nil { @@ -88,16 +87,8 @@ func (mngr *AccountsManager) initialize(conf *config.Configuration, log *zerolog return nil } -func (mngr *AccountsManager) createStorage(driver string) (data.Storage, error) { - if driver == "file" { - return data.NewFileStorage(mngr.conf, mngr.log) - } - - return nil, errors.Errorf("unknown storage driver %v", driver) -} - func (mngr *AccountsManager) readAllAccounts() { - if accounts, err := mngr.storage.ReadAll(); err == nil { + if accounts, err := mngr.storage.ReadAccounts(); err == nil { mngr.accounts = *accounts } else { // Just warn when not being able to read accounts @@ -106,7 +97,7 @@ func (mngr *AccountsManager) readAllAccounts() { } func (mngr *AccountsManager) writeAllAccounts() { - if err := mngr.storage.WriteAll(&mngr.accounts); err != nil { + if err := mngr.storage.WriteAccounts(&mngr.accounts); err != nil { // Just warn when not being able to write accounts mngr.log.Warn().Err(err).Msg("error while writing accounts") } @@ -253,8 +244,8 @@ func (mngr *AccountsManager) FindAccountEx(by string, value string, cloneAccount return account, 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 { +// GrantSiteAccess sets the Site access status of the account identified by the account email; if no such account exists, an error is returned. +func (mngr *AccountsManager) GrantSiteAccess(accountData *data.Account, grantAccess bool) error { mngr.mutex.Lock() defer mngr.mutex.Unlock() @@ -263,19 +254,20 @@ func (mngr *AccountsManager) GrantGOCDBAccess(accountData *data.Account, grantAc return errors.Wrap(err, "no account with the specified email exists") } - accessOld := account.Data.GOCDBAccess - account.Data.GOCDBAccess = grantAccess + return mngr.grantAccess(account, &account.Data.SiteAccess, grantAccess, email.SendSiteAccessGranted) +} - mngr.storage.AccountUpdated(account) - mngr.writeAllAccounts() +// 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() - if account.Data.GOCDBAccess && account.Data.GOCDBAccess != accessOld { - mngr.sendEmail(account, nil, email.SendGOCDBAccessGranted) + account, err := mngr.findAccount(FindByEmail, accountData.Email) + if err != nil { + return errors.Wrap(err, "no account with the specified email exists") } - mngr.callListeners(account, AccountsListener.AccountUpdated) - - return nil + return mngr.grantAccess(account, &account.Data.GOCDBAccess, grantAccess, email.SendGOCDBAccessGranted) } // RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned. @@ -315,6 +307,22 @@ func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts { return clones } +func (mngr *AccountsManager) grantAccess(account *data.Account, accessFlag *bool, grantAccess bool, emailFunc email.SendFunction) error { + accessOld := *accessFlag + *accessFlag = grantAccess + + mngr.storage.AccountUpdated(account) + mngr.writeAllAccounts() + + if *accessFlag && *accessFlag != accessOld { + mngr.sendEmail(account, nil, emailFunc) + } + + mngr.callListeners(account, AccountsListener.AccountUpdated) + + return nil +} + func (mngr *AccountsManager) callListeners(account *data.Account, cb AccountsListenerCallback) { for _, listener := range mngr.accountsListeners { cb(listener, account) @@ -326,10 +334,10 @@ func (mngr *AccountsManager) sendEmail(account *data.Account, params map[string] } // NewAccountsManager creates a new accounts manager instance. -func NewAccountsManager(conf *config.Configuration, log *zerolog.Logger) (*AccountsManager, error) { +func NewAccountsManager(storage data.Storage, 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") + if err := mngr.initialize(storage, conf, log); err != nil { + return nil, errors.Wrap(err, "unable to initialize the accounts manager") } return mngr, nil } diff --git a/pkg/siteacc/manager/gocdb/gocdb.go b/pkg/siteacc/manager/gocdb/gocdb.go index 2aa5e4a9d4..73d26c2ecc 100644 --- a/pkg/siteacc/manager/gocdb/gocdb.go +++ b/pkg/siteacc/manager/gocdb/gocdb.go @@ -75,7 +75,7 @@ func (listener *AccountsListener) updateGOCDB(account *data.Account, forceRemova func NewListener(conf *config.Configuration, log *zerolog.Logger) (*AccountsListener, error) { listener := &AccountsListener{} if err := listener.initialize(conf, log); err != nil { - return nil, errors.Wrapf(err, "unable to initialize the GOCDB accounts listener") + return nil, errors.Wrap(err, "unable to initialize the GOCDB accounts listener") } return listener, nil } diff --git a/pkg/siteacc/manager/sitesmanager.go b/pkg/siteacc/manager/sitesmanager.go new file mode 100644 index 0000000000..0d5a4eeb8f --- /dev/null +++ b/pkg/siteacc/manager/sitesmanager.go @@ -0,0 +1,185 @@ +// 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 ( + "strings" + "sync" + + "github.com/cs3org/reva/pkg/siteacc/config" + "github.com/cs3org/reva/pkg/siteacc/data" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +// SitesManager is responsible for all sites related tasks. +type SitesManager struct { + conf *config.Configuration + log *zerolog.Logger + + storage data.Storage + + sites data.Sites + + mutex sync.RWMutex +} + +func (mngr *SitesManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) 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 storage == nil { + return errors.Errorf("no storage provided") + } + mngr.storage = storage + + mngr.sites = make(data.Sites, 0, 32) // Reserve some space for sites + mngr.readAllSites() + + return nil +} + +func (mngr *SitesManager) readAllSites() { + if sites, err := mngr.storage.ReadSites(); err == nil { + mngr.sites = *sites + } else { + // Just warn when not being able to read sites + mngr.log.Warn().Err(err).Msg("error while reading sites") + } +} + +func (mngr *SitesManager) writeAllSites() { + if err := mngr.storage.WriteSites(&mngr.sites); err != nil { + // Just warn when not being able to write sites + mngr.log.Warn().Err(err).Msg("error while writing sites") + } +} + +// GetSite retrieves the site with the given ID, creating it first if necessary. +func (mngr *SitesManager) GetSite(id string, cloneSite bool) (*data.Site, error) { + mngr.mutex.RLock() + defer mngr.mutex.RUnlock() + + site, err := mngr.getSite(id) + if err != nil { + return nil, err + } + + if cloneSite { + site = site.Clone(false) + } + + return site, nil +} + +// FindSite returns the site specified by the ID if one exists. +func (mngr *SitesManager) FindSite(id string) *data.Site { + site, _ := mngr.findSite(id) + return site +} + +// UpdateSite updates the site identified by the site ID; if no such site exists, one will be created first. +func (mngr *SitesManager) UpdateSite(siteData *data.Site) error { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + site, err := mngr.getSite(siteData.ID) + if err != nil { + return errors.Wrap(err, "site to update not found") + } + + if err := site.Update(siteData, mngr.conf.Security.CredentialsPassphrase); err == nil { + mngr.storage.SiteUpdated(site) + mngr.writeAllSites() + } else { + return errors.Wrap(err, "error while updating site") + } + + return nil +} + +// CloneSites retrieves all sites currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible. +func (mngr *SitesManager) CloneSites(eraseCredentials bool) data.Sites { + mngr.mutex.RLock() + defer mngr.mutex.RUnlock() + + clones := make(data.Sites, 0, len(mngr.sites)) + for _, site := range mngr.sites { + clones = append(clones, site.Clone(eraseCredentials)) + } + + return clones +} + +func (mngr *SitesManager) getSite(id string) (*data.Site, error) { + site, err := mngr.findSite(id) + if site == nil { + site, err = mngr.createSite(id) + } + return site, err +} + +func (mngr *SitesManager) createSite(id string) (*data.Site, error) { + site, err := data.NewSite(id) + if err != nil { + return nil, errors.Wrap(err, "error while creating site") + } + mngr.sites = append(mngr.sites, site) + mngr.storage.SiteAdded(site) + mngr.writeAllSites() + return site, nil +} + +func (mngr *SitesManager) findSite(id string) (*data.Site, error) { + if len(id) == 0 { + return nil, errors.Errorf("no search ID specified") + } + + site := mngr.findSiteByPredicate(func(site *data.Site) bool { return strings.EqualFold(site.ID, id) }) + if site != nil { + return site, nil + } + + return nil, errors.Errorf("no site found matching the specified ID") +} + +func (mngr *SitesManager) findSiteByPredicate(predicate func(*data.Site) bool) *data.Site { + for _, site := range mngr.sites { + if predicate(site) { + return site + } + } + return nil +} + +// NewSitesManager creates a new sites manager instance. +func NewSitesManager(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) (*SitesManager, error) { + mngr := &SitesManager{} + if err := mngr.initialize(storage, conf, log); err != nil { + return nil, errors.Wrap(err, "unable to initialize the sites manager") + } + return mngr, nil +} diff --git a/pkg/siteacc/manager/usersmanager.go b/pkg/siteacc/manager/usersmanager.go index 4f39b3e4bf..4bee439f9d 100644 --- a/pkg/siteacc/manager/usersmanager.go +++ b/pkg/siteacc/manager/usersmanager.go @@ -32,6 +32,7 @@ type UsersManager struct { conf *config.Configuration log *zerolog.Logger + sitesManager *SitesManager accountsManager *AccountsManager } @@ -39,7 +40,7 @@ const ( defaultPasswordLength = 12 ) -func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) error { +func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, accountsManager *AccountsManager) error { if conf == nil { return errors.Errorf("no configuration provided") } @@ -50,6 +51,11 @@ func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Lo } mngr.log = log + if sitesManager == nil { + return errors.Errorf("no sites manager provided") + } + mngr.sitesManager = sitesManager + if accountsManager == nil { return errors.Errorf("no accounts manager provided") } @@ -75,11 +81,17 @@ func (mngr *UsersManager) LoginUser(name, password string, scope string, session return "", errors.Errorf("no access to the specified scope granted") } + // Get the site the account belongs to + site, err := mngr.sitesManager.GetSite(account.Site, false) + if err != nil { + return "", errors.Wrap(err, "no site with the specified ID exists") + } + // Store the user account in the session - session.LoggedInUser = account + session.LoginUser(account, site) // Generate a token that can be used as a "ticket" - token, err := generateUserToken(session.LoggedInUser.Email, scope, mngr.conf.Webserver.SessionTimeout) + token, err := generateUserToken(session.LoggedInUser().Account.Email, scope, mngr.conf.Webserver.SessionTimeout) if err != nil { return "", errors.Wrap(err, "unable to generate user token") } @@ -90,7 +102,7 @@ func (mngr *UsersManager) LoginUser(name, password string, scope string, session // 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 + session.LogoutUser() } // VerifyUserToken is used to verify a user token against the current session. @@ -129,10 +141,10 @@ func (mngr *UsersManager) VerifyUserToken(token string, user string, scope strin } // NewUsersManager creates a new users manager instance. -func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, accountsManager *AccountsManager) (*UsersManager, error) { +func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, 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") + if err := mngr.initialize(conf, log, sitesManager, accountsManager); err != nil { + return nil, errors.Wrap(err, "unable to initialize the users manager") } return mngr, nil } diff --git a/pkg/siteacc/siteacc.go b/pkg/siteacc/siteacc.go index a819863e25..ffa7bff7e6 100644 --- a/pkg/siteacc/siteacc.go +++ b/pkg/siteacc/siteacc.go @@ -26,6 +26,7 @@ import ( "github.com/cs3org/reva/pkg/siteacc/admin" "github.com/cs3org/reva/pkg/siteacc/alerting" "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" @@ -39,6 +40,9 @@ type SiteAccounts struct { sessions *html.SessionManager + storage data.Storage + + sitesManager *manager.SitesManager accountsManager *manager.AccountsManager usersManager *manager.UsersManager @@ -66,15 +70,29 @@ func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog } siteacc.sessions = sessions + // Create the central storage + storage, err := siteacc.createStorage(conf.Storage.Driver) + if err != nil { + return errors.Wrap(err, "unable to create storage") + } + siteacc.storage = storage + + // Create the sites manager instance + smngr, err := manager.NewSitesManager(storage, conf, log) + if err != nil { + return errors.Wrap(err, "error creating the sites manager") + } + siteacc.sitesManager = smngr + // Create the accounts manager instance - amngr, err := manager.NewAccountsManager(conf, log) + amngr, err := manager.NewAccountsManager(storage, conf, log) if err != nil { 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) + umngr, err := manager.NewUsersManager(conf, log, siteacc.sitesManager, siteacc.accountsManager) if err != nil { return errors.Wrap(err, "error creating the users manager") } @@ -144,6 +162,11 @@ func (siteacc *SiteAccounts) ShowAccountPanel(w http.ResponseWriter, r *http.Req return siteacc.accountPanel.Execute(w, r, session) } +// SitesManager returns the central sites manager instance. +func (siteacc *SiteAccounts) SitesManager() *manager.SitesManager { + return siteacc.sitesManager +} + // AccountsManager returns the central accounts manager instance. func (siteacc *SiteAccounts) AccountsManager() *manager.AccountsManager { return siteacc.accountsManager @@ -173,6 +196,14 @@ func (siteacc *SiteAccounts) GetPublicEndpoints() []string { return endpoints } +func (siteacc *SiteAccounts) createStorage(driver string) (data.Storage, error) { + if driver == "file" { + return data.NewFileStorage(siteacc.conf, siteacc.log) + } + + return nil, errors.Errorf("unknown storage driver %v", driver) +} + // New returns a new Site Accounts service instance. func New(conf *config.Configuration, log *zerolog.Logger) (*SiteAccounts, error) { // Configure the accounts service diff --git a/pkg/siteacc/sitereg/sitereg.go b/pkg/siteacc/sitereg/sitereg.go deleted file mode 100644 index 685011f18b..0000000000 --- a/pkg/siteacc/sitereg/sitereg.go +++ /dev/null @@ -1,57 +0,0 @@ -// 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 sitereg - -import ( - "net/url" - - "github.com/pkg/errors" - - "github.com/cs3org/reva/pkg/mentix/key" - "github.com/cs3org/reva/pkg/mentix/utils/network" -) - -// UnregisterSite unregister a site using the given site registration endpoint. -func UnregisterSite(serviceURL string, apiKey key.APIKey, siteID key.SiteIdentifier, salt string) error { - if len(serviceURL) == 0 { - return errors.Errorf("no site registration service URL provided") - } - - if err := key.VerifyAPIKey(apiKey, salt); err != nil { - return err - } - - fullURL, err := url.Parse(serviceURL) - if err != nil { - return errors.Wrap(err, "unable to parse the site registration service URL") - } - - query := make(url.Values) - query.Set("action", "unregister") - query.Set("apiKey", apiKey) - query.Set("siteId", siteID) - fullURL.RawQuery = query.Encode() - - _, err = network.WriteEndpoint(fullURL, nil, true) - if err != nil { - return errors.Wrap(err, "unable to query the service registration endpoint") - } - - return nil -}