From 8d4cf01f781837490f8271710b67a729e0eee7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20M=C3=BCller?= Date: Wed, 3 Mar 2021 21:04:48 +0100 Subject: [PATCH] Site Accounts service for API keys (#1506) --- changelog/unreleased/mentix-clone-fix.md | 5 + changelog/unreleased/siteaccs-svc.md | 7 + .../config/http/services/mentix/_index.md | 35 +- .../http/services/mentix/adminapi/_index.md | 46 --- .../http/services/mentix/sitereg/_index.md | 37 ++ .../http/services/mentix/webapi/_index.md | 42 +- .../config/http/services/siteacc/_index.md | 95 +++++ examples/mentix/mentix.toml | 23 +- examples/siteacc/siteacc.toml | 19 + internal/http/services/loader/loader.go | 1 + internal/http/services/mentix/mentix.go | 8 +- .../http/services/siteacc/config/config.go | 37 ++ .../http/services/siteacc/config/endpoints.go | 48 +++ .../http/services/siteacc/data/account.go | 113 ++++++ .../http/services/siteacc/data/filestorage.go | 116 ++++++ .../http/services/siteacc/data/storage.go | 34 ++ internal/http/services/siteacc/email/email.go | 74 ++++ .../http/services/siteacc/email/template.go | 51 +++ internal/http/services/siteacc/manager.go | 331 ++++++++++++++++ internal/http/services/siteacc/panel/panel.go | 80 ++++ .../http/services/siteacc/panel/template.go | 109 ++++++ internal/http/services/siteacc/siteacc.go | 365 ++++++++++++++++++ pkg/mentix/accservice/accservice.go | 100 +++++ pkg/mentix/config/config.go | 29 +- pkg/mentix/config/ids.go | 6 +- pkg/mentix/connectors/gocdb.go | 9 +- pkg/mentix/connectors/gocdb/query.go | 9 +- pkg/mentix/connectors/localfile.go | 37 +- pkg/mentix/exchangers/exchanger.go | 77 ++++ pkg/mentix/exchangers/exporters/cs3api.go | 2 +- .../exchangers/exporters/cs3api/query.go | 4 +- pkg/mentix/exchangers/exporters/exporter.go | 72 ---- .../exchangers/exporters/reqexporter.go | 40 +- .../exchangers/exporters/siteloc/query.go | 7 +- .../exchangers/exporters/siteloc/types.go | 2 +- .../exchangers/exporters/sitelocations.go | 2 +- pkg/mentix/exchangers/exporters/webapi.go | 5 +- .../exchangers/exporters/webapi/query.go | 5 +- .../exchangers/importers/adminapi/query.go | 73 ---- pkg/mentix/exchangers/importers/importer.go | 36 +- .../exchangers/importers/reqimporter.go | 64 +-- .../importers/{adminapi.go => sitereg.go} | 28 +- .../exchangers/importers/sitereg/query.go | 157 ++++++++ .../exchangers/importers/sitereg/types.go | 143 +++++++ pkg/mentix/exchangers/importers/webapi.go | 61 --- .../exchangers/importers/webapi/query.go | 69 ---- pkg/mentix/exchangers/reqexchanger.go | 55 ++- pkg/mentix/key/apikey.go | 118 ++++++ pkg/mentix/key/siteid.go | 43 +++ pkg/mentix/mentix.go | 18 +- pkg/mentix/meshdata/meshdata.go | 44 +-- pkg/mentix/meshdata/properties.go | 4 +- pkg/mentix/meshdata/service.go | 2 +- pkg/mentix/meshdata/site.go | 31 +- pkg/mentix/utils/countries/countries.go | 316 +++++++++++++++ .../utils.go => utils/network/network.go} | 25 +- pkg/utils/utils.go | 9 + 57 files changed, 2759 insertions(+), 619 deletions(-) create mode 100644 changelog/unreleased/mentix-clone-fix.md create mode 100644 changelog/unreleased/siteaccs-svc.md delete mode 100644 docs/content/en/docs/config/http/services/mentix/adminapi/_index.md create mode 100644 docs/content/en/docs/config/http/services/mentix/sitereg/_index.md create mode 100644 docs/content/en/docs/config/http/services/siteacc/_index.md create mode 100644 examples/siteacc/siteacc.toml create mode 100644 internal/http/services/siteacc/config/config.go create mode 100644 internal/http/services/siteacc/config/endpoints.go create mode 100644 internal/http/services/siteacc/data/account.go create mode 100644 internal/http/services/siteacc/data/filestorage.go create mode 100644 internal/http/services/siteacc/data/storage.go create mode 100644 internal/http/services/siteacc/email/email.go create mode 100644 internal/http/services/siteacc/email/template.go create mode 100644 internal/http/services/siteacc/manager.go create mode 100644 internal/http/services/siteacc/panel/panel.go create mode 100644 internal/http/services/siteacc/panel/template.go create mode 100644 internal/http/services/siteacc/siteacc.go create mode 100644 pkg/mentix/accservice/accservice.go delete mode 100755 pkg/mentix/exchangers/importers/adminapi/query.go rename pkg/mentix/exchangers/importers/{adminapi.go => sitereg.go} (54%) mode change 100755 => 100644 create mode 100755 pkg/mentix/exchangers/importers/sitereg/query.go create mode 100644 pkg/mentix/exchangers/importers/sitereg/types.go delete mode 100755 pkg/mentix/exchangers/importers/webapi.go delete mode 100755 pkg/mentix/exchangers/importers/webapi/query.go create mode 100644 pkg/mentix/key/apikey.go create mode 100644 pkg/mentix/key/siteid.go create mode 100644 pkg/mentix/utils/countries/countries.go rename pkg/mentix/{network/utils.go => utils/network/network.go} (82%) diff --git a/changelog/unreleased/mentix-clone-fix.md b/changelog/unreleased/mentix-clone-fix.md new file mode 100644 index 0000000000..d5ab1f3232 --- /dev/null +++ b/changelog/unreleased/mentix-clone-fix.md @@ -0,0 +1,5 @@ +Bugfix: Cloning of internal mesh data lost some values + +This update fixes a bug in Mentix that caused some (non-critical) values to be lost during data cloning that happens internally. + +https://github.com/cs3org/reva/pull/1457 diff --git a/changelog/unreleased/siteaccs-svc.md b/changelog/unreleased/siteaccs-svc.md new file mode 100644 index 0000000000..45e98ed2d7 --- /dev/null +++ b/changelog/unreleased/siteaccs-svc.md @@ -0,0 +1,7 @@ +Enhancement: Site Accounts service for API keys + +This update adds a new service to Reva that handles site accounts creation and management. Registered sites can be assigned an API key through a simple web interface which is also part of this service. This API key can then be used to identify a user and his/her associated (vendor or partner) site. + +Furthermore, Mentix was extended to make use of this new service. This way, all sites now have a stable and unique site ID that not only avoids ID collisions but also introduces a new layer of security (i.e., sites can only be modified or removed using the correct API key). + +https://github.com/cs3org/reva/pull/1506 diff --git a/docs/content/en/docs/config/http/services/mentix/_index.md b/docs/content/en/docs/config/http/services/mentix/_index.md index 8e12bc5be7..62a614e6e3 100644 --- a/docs/content/en/docs/config/http/services/mentix/_index.md +++ b/docs/content/en/docs/config/http/services/mentix/_index.md @@ -10,6 +10,7 @@ description: > Mentix (_**Me**sh E**nti**ty E**x**changer_) is a service to read and write mesh topology data to and from one or more sources (e.g., a GOCDB instance) and export it to various targets like an HTTP endpoint or Prometheus. {{% /pageinfo %}} +## General settings {{% dir name="prefix" type="string" default="mentix" %}} The relative root path of all exposed HTTP endpoints of Mentix. {{< highlight toml >}} @@ -42,11 +43,8 @@ Mentix can import mesh data from various sources and write it to one or more tar __Supported importers:__ -- **webapi** -Mentix can import mesh data via an HTTP endpoint using the `webapi` importer. Data can be sent to the configured relative endpoint (see [here](webapi)). - -- **adminapi** - Some aspects of Mentix can be administered through an HTTP endpoint using the `adminapi` importer. Queries can be sent to the configured relative endpoint (see [here](adminapi)). +- **sitereg** +Mentix can import new sites via an HTTP endpoint using the `sitereg` importer. Data can be sent to the configured relative endpoint (see [here](sitereg)). ## Exporters Mentix exposes its gathered data by using one or more _exporters_. Such exporters can, for example, write the data to a file in a specific format, or offer the data via an HTTP endpoint. @@ -65,3 +63,30 @@ Mentix exposes its data via an HTTP endpoint using the `webapi` exporter. Data c - files: - '/usr/share/prom/sciencemesh_services.json' ``` + +## Site Accounts service +Mentix uses the Reva site accounts service to query information about site accounts. The following settings must be configured properly: + +{{% dir name="url" type="string" default="" %}} +The URL of the site accounts service. +{{< highlight toml >}} +[http.services.mentix.accounts] +url = "https://example.com/accounts" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="user" type="string" default="" %}} +The user name to use for basic HTTP authentication. +{{< highlight toml >}} +[http.services.mentix.accounts] +user = "hans" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="password" type="string" default="" %}} +The user password to use for basic HTTP authentication. +{{< highlight toml >}} +[http.services.mentix.accounts] +password = "secret" +{{< /highlight >}} +{{% /dir %}} diff --git a/docs/content/en/docs/config/http/services/mentix/adminapi/_index.md b/docs/content/en/docs/config/http/services/mentix/adminapi/_index.md deleted file mode 100644 index 55f29684eb..0000000000 --- a/docs/content/en/docs/config/http/services/mentix/adminapi/_index.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -title: "adminapi" -linkTitle: "adminapi" -weight: 10 -description: > - Configuration for the AdminAPI of the Mentix service ---- - -{{% pageinfo %}} -The AdminAPI of Mentix is a special importer that can be used to administer certain aspects of Mentix. -{{% /pageinfo %}} - -The AdminAPI importer receives instructions/queries through a `POST` request. - -The importer supports one action that must be passed in the URL: -``` -https://sciencemesh.example.com/mentix/admin/?action= -``` -Currently, the following actions are supported: -- `authorize`: Authorizes or unauthorizes a site - -For all actions, the site data must be sent as JSON data. If the call succeeded, status 200 is returned. - -{{% dir name="endpoint" type="string" default="/admin" %}} -The endpoint where the mesh data can be sent to. -{{< highlight toml >}} -[http.services.mentix.importers.adminapi] -endpoint = "/data" -{{< /highlight >}} -{{% /dir %}} - -{{% dir name="is_protected" type="bool" default="false" %}} -Whether the endpoint requires authentication. -{{< highlight toml >}} -[http.services.mentix.importers.adminapi] -is_protected = true -{{< /highlight >}} -{{% /dir %}} - -{{% dir name="enabled_connectors" type="[]string" default="" %}} -A list of all enabled connectors for the importer. Must always be provided. -{{< highlight toml >}} -[http.services.mentix.importers.adminapi] -enabled_connectors = ["localfile"] -{{< /highlight >}} -{{% /dir %}} diff --git a/docs/content/en/docs/config/http/services/mentix/sitereg/_index.md b/docs/content/en/docs/config/http/services/mentix/sitereg/_index.md new file mode 100644 index 0000000000..3591732b9e --- /dev/null +++ b/docs/content/en/docs/config/http/services/mentix/sitereg/_index.md @@ -0,0 +1,37 @@ +--- +title: "sitereg" +linkTitle: "sitereg" +weight: 10 +description: > + Configuration for site registration service +--- + +{{% pageinfo %}} +The site registration service is used to register new and unregister existing sites. +{{% /pageinfo %}} + +The site registration service is used to register new and unregister existing sites. + +{{% dir name="endpoint" type="string" default="/sitereg" %}} +The endpoint of the service. +{{< highlight toml >}} +[http.services.mentix.importers.sitereg] +endpoint = "/reg" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="enabled_connectors" type="[]string" default="" %}} +A list of all enabled connectors for the importer. +{{< highlight toml >}} +[http.services.mentix.importers.sitereg] +enabled_connectors = ["localfile"] +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="ignore_sm_sites" type="bool" default="false" %}} +If set to true, registrations from ScienceMesh sites will be ignored. +{{< highlight toml >}} +[http.services.mentix.importers.sitereg] +ignore_sm_sites = true +{{< /highlight >}} +{{% /dir %}} diff --git a/docs/content/en/docs/config/http/services/mentix/webapi/_index.md b/docs/content/en/docs/config/http/services/mentix/webapi/_index.md index 03e4c028bc..f3ebd77bfc 100644 --- a/docs/content/en/docs/config/http/services/mentix/webapi/_index.md +++ b/docs/content/en/docs/config/http/services/mentix/webapi/_index.md @@ -7,49 +7,9 @@ description: > --- {{% pageinfo %}} -The WebAPI of Mentix supports both importing and exporting of mesh data via an HTTP endpoint. Both the im- and exporter are configured separately. +The WebAPI of Mentix supports of mesh data via an HTTP endpoint. {{% /pageinfo %}} -## Importer - -The WebAPI importer receives a single _plain_ Mentix site through an HTTP `POST` request; service types are currently not supported. - -The importer supports two actions that must be passed in the URL: -``` -https://sciencemesh.example.com/mentix/webapi/?action= -``` -Currently, the following actions are supported: -- `register`: Registers a new site -- `unregister`: Unregisters an existing site - -For all actions, the site data must be sent as JSON data. If the call succeeded, status 200 is returned. - -{{% dir name="endpoint" type="string" default="/sites" %}} -The endpoint where the mesh data can be sent to. -{{< highlight toml >}} -[http.services.mentix.importers.webapi] -endpoint = "/data" -{{< /highlight >}} -{{% /dir %}} - -{{% dir name="is_protected" type="bool" default="false" %}} -Whether the endpoint requires authentication. -{{< highlight toml >}} -[http.services.mentix.importers.webapi] -is_protected = true -{{< /highlight >}} -{{% /dir %}} - -{{% dir name="enabled_connectors" type="[]string" default="" %}} -A list of all enabled connectors for the importer. Must always be provided. -{{< highlight toml >}} -[http.services.mentix.importers.webapi] -enabled_connectors = ["localfile"] -{{< /highlight >}} -{{% /dir %}} - -## Exporter - The WebAPI exporter exposes the _plain_ Mentix data via an HTTP endpoint. {{% dir name="endpoint" type="string" default="/sites" %}} diff --git a/docs/content/en/docs/config/http/services/siteacc/_index.md b/docs/content/en/docs/config/http/services/siteacc/_index.md new file mode 100644 index 0000000000..425d3f0c7c --- /dev/null +++ b/docs/content/en/docs/config/http/services/siteacc/_index.md @@ -0,0 +1,95 @@ +--- +title: "siteacc" +linkTitle: "siteacc" +weight: 10 +description: > + Configuration for the Site Accounts service +--- + +{{% pageinfo %}} +The site accounts service is used to store and manage site accounts. +{{% /pageinfo %}} + +## General settings +{{% dir name="prefix" type="string" default="accounts" %}} +The relative root path of all exposed HTTP endpoints of the service. +{{< highlight toml >}} +[http.services.siteacc] +prefix = "/siteacc" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="notifications_mail" type="string" default="" %}} +An email address where all notifications are sent to. +{{< highlight toml >}} +[http.services.siteacc] +notifications_mail = "notify@example.com" +{{< /highlight >}} +{{% /dir %}} + +## SMTP settings +{{% dir name="sender_mail" type="string" default="" %}} +An email address from which all emails are sent. +{{< highlight toml >}} +[http.services.siteacc.smtp] +sender_mail = "notify@example.com" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="sender_login" type="string" default="" %}} +The login name. +{{< highlight toml >}} +[http.services.siteacc.smtp] +sender_login = "hans" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="sender_password" type="string" default="" %}} +The password for the login. +{{< highlight toml >}} +[http.services.siteacc.smtp] +password = "secret" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="smtp_server" type="string" default="" %}} +The SMTP server to use. +{{< highlight toml >}} +[http.services.siteacc.smtp] +smtp_server = "smtp.example.com" +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="smtp_port" type="int" default="25" %}} +The SMTP server port to use. +{{< highlight toml >}} +[http.services.siteacc.smtp] +smtp_port = 25 +{{< /highlight >}} +{{% /dir %}} + +{{% dir name="disable_auth" type="bool" default="false" %}} +Whether to disable authentication. +{{< highlight toml >}} +[http.services.siteacc.smtp] +disable_auth = true +{{< /highlight >}} +{{% /dir %}} + +## Storage settings +{{% dir name="driver" type="string" default="file" %}} +The storage driver to use; currently, only `file` is supported. +{{< highlight toml >}} +[http.services.siteacc.storage] +driver = "file" +{{< /highlight >}} +{{% /dir %}} + +### Storage settings - File driver +{{% dir name="file" type="string" default="" %}} +The file location. +{{< highlight toml >}} +[http.services.siteacc.storage.file] +file = "/var/reva/accounts.json" +{{< /highlight >}} +{{% /dir %}} diff --git a/examples/mentix/mentix.toml b/examples/mentix/mentix.toml index 9baf22bbfc..8cdaa11d38 100644 --- a/examples/mentix/mentix.toml +++ b/examples/mentix/mentix.toml @@ -1,9 +1,5 @@ -[shared] -jwt_secret = "Ment1x-T0pS3cr3t" - [http] address = "0.0.0.0:9600" -enabled_services = ["mentix"] [http.services.mentix] update_interval = "15m" @@ -25,16 +21,19 @@ endpoint = "/" # If this setting is omitted, all connectors will be used as data sources enabled_connectors = ["gocdb"] -# Enable the WebAPI importer -[http.services.mentix.importers.webapi] +# Enable the site registration importer +[http.services.mentix.importers.sitereg] # For importers, this is obligatory; the connectors will be used as the target for data updates enabled_connectors = ["localfile"] - -# Enable the AdminAPI importer -[http.services.mentix.importers.adminapi] -enabled_connectors = ["localfile"] -# Should never allow access w/o prior authorization -is_protected = true +# If set to true, ScienceMesh sites will be ignored when they try to register +ignore_sm_sites = false + +# Set up the accounts service used to query information about accounts associated with registered sites +[http.services.mentix.accounts] +# Depending on where the service is running, localhost may also be used here +url = "https://sciencemesh.example.com/iop/accounts" +user = "username" +password = "userpass" # Configure the Prometheus Service Discovery: [http.services.mentix.exporters.promsd] diff --git a/examples/siteacc/siteacc.toml b/examples/siteacc/siteacc.toml new file mode 100644 index 0000000000..a03c393065 --- /dev/null +++ b/examples/siteacc/siteacc.toml @@ -0,0 +1,19 @@ +[http] +address = "0.0.0.0:9600" + +[http.services.siteacc] +# 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" + +# The SMTP server used for sending emails +[http.services.siteacc.smtp] +sender_mail = "science.mesh@example.com" +smtp_server = "mail.example.com" +smtp_port = 25 +disable_auth = true diff --git a/internal/http/services/loader/loader.go b/internal/http/services/loader/loader.go index 0013049632..91f59d051e 100644 --- a/internal/http/services/loader/loader.go +++ b/internal/http/services/loader/loader.go @@ -31,6 +31,7 @@ import ( _ "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" _ "github.com/cs3org/reva/internal/http/services/owncloud/ocs" _ "github.com/cs3org/reva/internal/http/services/prometheus" + _ "github.com/cs3org/reva/internal/http/services/siteacc" _ "github.com/cs3org/reva/internal/http/services/sysinfo" _ "github.com/cs3org/reva/internal/http/services/wellknown" // Add your own service here diff --git a/internal/http/services/mentix/mentix.go b/internal/http/services/mentix/mentix.go index f07070d31d..2d82bdb88e 100644 --- a/internal/http/services/mentix/mentix.go +++ b/internal/http/services/mentix/mentix.go @@ -137,12 +137,8 @@ func applyDefaultConfig(conf *config.Configuration) { } // Importers - if conf.Importers.WebAPI.Endpoint == "" { - conf.Importers.WebAPI.Endpoint = "/sites" - } - - if conf.Importers.AdminAPI.Endpoint == "" { - conf.Importers.AdminAPI.Endpoint = "/admin" + if conf.Importers.SiteRegistration.Endpoint == "" { + conf.Importers.SiteRegistration.Endpoint = "/sitereg" } // Exporters diff --git a/internal/http/services/siteacc/config/config.go b/internal/http/services/siteacc/config/config.go new file mode 100644 index 0000000000..6a4a282183 --- /dev/null +++ b/internal/http/services/siteacc/config/config.go @@ -0,0 +1,37 @@ +// 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 config + +import "github.com/cs3org/reva/pkg/smtpclient" + +// Configuration holds the general service configuration. +type Configuration struct { + Prefix string `mapstructure:"prefix"` + + Storage struct { + Driver string `mapstructure:"driver"` + + File struct { + File string `mapstructure:"file"` + } `mapstructure:"file"` + } `mapstructure:"storage"` + + SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"` + NotificationsMail string `mapstructure:"notifications_mail"` +} diff --git a/internal/http/services/siteacc/config/endpoints.go b/internal/http/services/siteacc/config/endpoints.go new file mode 100644 index 0000000000..80541bf803 --- /dev/null +++ b/internal/http/services/siteacc/config/endpoints.go @@ -0,0 +1,48 @@ +// 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 config + +const ( + // EndpointPanel is the endpoint path of the web interface panel. + EndpointPanel = "/panel" + + // EndpointGenerateAPIKey is the endpoint path of the API key generator. + EndpointGenerateAPIKey = "/generate-api-key" + // EndpointVerifyAPIKey is the endpoint path for API key verification. + EndpointVerifyAPIKey = "/verify-api-key" + // EndpointAssignAPIKey is the endpoint path used for assigning an API key to an account. + EndpointAssignAPIKey = "/assign-api-key" + + // EndpointList is the endpoint path for listing all stored accounts. + EndpointList = "/list" + // EndpointFind is the endpoint path for finding accounts. + EndpointFind = "/find" + + // EndpointCreate is the endpoint path for account creation. + EndpointCreate = "/create" + // EndpointUpdate is the endpoint path for account updates. + EndpointUpdate = "/update" + // EndpointRemove is the endpoint path for account removal. + EndpointRemove = "/remove" + + // EndpointAuthorize is the endpoint path for account authorization + EndpointAuthorize = "/authorize" + // EndpointIsAuthorized is the endpoint path used to check the authorization status of an account. + EndpointIsAuthorized = "/is-authorized" +) diff --git a/internal/http/services/siteacc/data/account.go b/internal/http/services/siteacc/data/account.go new file mode 100644 index 0000000000..dce4ea7b1a --- /dev/null +++ b/internal/http/services/siteacc/data/account.go @@ -0,0 +1,113 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this filePath 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 ( + "strings" + "time" + + "github.com/pkg/errors" + + "github.com/cs3org/reva/pkg/mentix/key" + "github.com/cs3org/reva/pkg/utils" +) + +// Account represents a single site account. +type Account struct { + Email string `json:"email"` + FirstName string `json:"firstName"` + LastName string `json:"lastName"` + + DateCreated time.Time `json:"dateCreated"` + DateModified time.Time `json:"dateModified"` + + Data AccountData `json:"data"` +} + +// AccountData holds additional data for a site account. +type AccountData struct { + APIKey key.APIKey `json:"apiKey"` + Authorized bool `json:"authorized"` +} + +// Accounts holds an array of site accounts. +type Accounts = []*Account + +// GetSiteID returns the site ID (generated from the API key) for the given account. +func (acc *Account) GetSiteID() key.SiteIdentifier { + if id, err := key.CalculateSiteID(acc.Data.APIKey, strings.ToLower(acc.Email)); err == nil { + return id + } + + 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 { + return errors.Wrap(err, "unable to update account data") + } + + // Manually update fields + acc.FirstName = other.FirstName + acc.LastName = other.LastName + + if copyData { + acc.Data = other.Data + } + + return nil +} + +func (acc *Account) verify() 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") + } + + return nil +} + +// NewAccount creates a new site account. +func NewAccount(email string, firstName, lastName string) (*Account, error) { + t := time.Now() + + acc := &Account{ + Email: email, + FirstName: firstName, + LastName: lastName, + DateCreated: t, + DateModified: t, + Data: AccountData{ + APIKey: "", + Authorized: false, + }, + } + + if err := acc.verify(); err != nil { + return nil, err + } + + return acc, nil +} diff --git a/internal/http/services/siteacc/data/filestorage.go b/internal/http/services/siteacc/data/filestorage.go new file mode 100644 index 0000000000..22efb99b3e --- /dev/null +++ b/internal/http/services/siteacc/data/filestorage.go @@ -0,0 +1,116 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this filePath 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 ( + "encoding/json" + "io/ioutil" + "os" + "path/filepath" + + "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 + log *zerolog.Logger + + filePath string +} + +func (storage *FileStorage) initialize(conf *config.Configuration, log *zerolog.Logger) error { + if conf == nil { + return errors.Errorf("no configuration provided") + } + storage.conf = conf + + if log == nil { + return errors.Errorf("no logger provided") + } + storage.log = log + + if conf.Storage.File.File == "" { + return errors.Errorf("no file set in the configuration") + } + storage.filePath = conf.Storage.File.File + + // Create the file directory if necessary + dir := filepath.Dir(storage.filePath) + _ = os.MkdirAll(dir, 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) + if err != nil { + return nil, errors.Wrapf(err, "unable to read file %v", storage.filePath) + } + + if err := json.Unmarshal(jsonData, accounts); err != nil { + return nil, errors.Wrapf(err, "invalid file %v", storage.filePath) + } + + 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) + } + + return nil +} + +// 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 +} + +// 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 +} + +// 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 +} + +// NewFileStorage creates a new filePath 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 storage, nil +} diff --git a/internal/http/services/siteacc/data/storage.go b/internal/http/services/siteacc/data/storage.go new file mode 100644 index 0000000000..221e7bcb32 --- /dev/null +++ b/internal/http/services/siteacc/data/storage.go @@ -0,0 +1,34 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this filePath 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 + +// Storage defines the interface for 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 + + // AccountAdded is called when an account has been added. + AccountAdded(account *Account) + // AccountUpdated is called when an account has been updated. + AccountUpdated(account *Account) + // AccountRemoved is called when an account has been removed. + AccountRemoved(account *Account) +} diff --git a/internal/http/services/siteacc/email/email.go b/internal/http/services/siteacc/email/email.go new file mode 100644 index 0000000000..72e15f3c66 --- /dev/null +++ b/internal/http/services/siteacc/email/email.go @@ -0,0 +1,74 @@ +// 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 email + +import ( + "bytes" + "text/template" + + "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 { + 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 { + 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 { + return send(recipients, "ScienceMesh: Site registration authorized", accountAuthorizedTemplate, 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 { + return nil + } + + tpl := template.New("email") + if _, err := tpl.Parse(bodyTemplate); err != nil { + return errors.Wrap(err, "error while parsing email template") + } + + var body bytes.Buffer + if err := tpl.Execute(&body, data); err != nil { + return errors.Wrap(err, "error while executing email template") + } + + for _, recipient := range recipients { + if len(recipient) == 0 { + continue + } + + // Send the mail w/o blocking the main thread + go func(recipient string) { + _ = smtp.SendMail(recipient, subject, body.String()) + }(recipient) + } + + return nil +} diff --git a/internal/http/services/siteacc/email/template.go b/internal/http/services/siteacc/email/template.go new file mode 100644 index 0000000000..25df085bec --- /dev/null +++ b/internal/http/services/siteacc/email/template.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 email + +const accountCreatedTemplate = ` +Dear {{.FirstName}} {{.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. + +Kind regards, +The ScienceMesh Team +` + +const apiKeyAssignedTemplate = ` +Dear {{.FirstName}} {{.LastName}}, + +An API key has been created for your account: +{{.Data.APIKey}} + +Keep this key in a safe and secret place! + +Kind regards, +The ScienceMesh Team +` + +const accountAuthorizedTemplate = ` +Dear {{.FirstName}} {{.LastName}}, + +Congratulations - your site registration has been authorized! + +Kind regards, +The ScienceMesh Team +` diff --git a/internal/http/services/siteacc/manager.go b/internal/http/services/siteacc/manager.go new file mode 100644 index 0000000000..f625dd96a3 --- /dev/null +++ b/internal/http/services/siteacc/manager.go @@ -0,0 +1,331 @@ +// 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 ( + "bytes" + "encoding/gob" + "net/http" + "strings" + "sync" + "time" + + "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/pkg/mentix/key" + "github.com/cs3org/reva/pkg/smtpclient" +) + +const ( + // FindByEmail holds the string value of the corresponding search criterium. + FindByEmail = "email" + // FindByAPIKey holds the string value of the corresponding search criterium. + FindByAPIKey = "apikey" + // FindBySiteID holds the string value of the corresponding search criterium. + FindBySiteID = "siteid" +) + +// Manager is responsible for all site account related tasks. +type Manager struct { + conf *config.Configuration + log *zerolog.Logger + + accounts data.Accounts + storage data.Storage + + panel *panel.Panel + smtp *smtpclient.SMTPCredentials + + mutex sync.RWMutex +} + +func (mngr *Manager) initialize(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 + + 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") + } + + // Create the web interface panel + if pnl, err := panel.NewPanel(conf, log); err == nil { + mngr.panel = pnl + } else { + return errors.Wrap(err, "unable to create panel") + } + + // Create the SMTP client + if conf.SMTP != nil { + mngr.smtp = smtpclient.NewSMTPCredentials(conf.SMTP) + } + + return nil +} + +func (mngr *Manager) 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 *Manager) readAllAccounts() { + if accounts, err := mngr.storage.ReadAll(); err == nil { + mngr.accounts = *accounts + } else { + // Just warn when not being able to read accounts + mngr.log.Warn().Err(err).Msg("error while reading accounts") + } +} + +func (mngr *Manager) 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) { + if len(value) == 0 { + return nil, errors.Errorf("no search value specified") + } + + var account *data.Account + switch strings.ToLower(by) { + case FindByEmail: + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return strings.EqualFold(account.Email, value) }) + + case FindByAPIKey: + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.Data.APIKey == value }) + + case FindBySiteID: + account = mngr.findAccountByPredicate(func(account *data.Account) bool { return account.GetSiteID() == value }) + + default: + return nil, errors.Errorf("invalid search type %v", by) + } + + if account != nil { + return account, nil + } + + return nil, errors.Errorf("no user found matching the specified criteria") +} + +func (mngr *Manager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account { + for _, account := range mngr.accounts { + if predicate(account) { + return account + } + } + 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 + accounts := mngr.CloneAccounts() + return mngr.panel.Execute(w, &accounts) +} + +// 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 { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + // Accounts must be unique (identified by their email address) + if account, _ := mngr.findAccount(FindByEmail, accountData.Email); account != nil { + 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 { + mngr.accounts = append(mngr.accounts, account) + mngr.storage.AccountAdded(account) + mngr.writeAllAccounts() + + _ = email.SendAccountCreated(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + } else { + return errors.Wrap(err, "error while creating account") + } + + return nil +} + +// 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 { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + account, err := mngr.findAccount(FindByEmail, accountData.Email) + if err != nil { + return errors.Wrap(err, "user to update not found") + } + + if err := account.Copy(accountData, copyData); err == nil { + account.DateModified = time.Now() + + mngr.storage.AccountUpdated(account) + mngr.writeAllAccounts() + } else { + return errors.Wrap(err, "error while updating account") + } + + return nil +} + +// FindAccount is used to find an account by various criteria. +func (mngr *Manager) FindAccount(by string, value string) (*data.Account, error) { + mngr.mutex.RLock() + defer mngr.mutex.RUnlock() + + account, err := mngr.findAccount(by, value) + if err != nil { + return nil, err + } + + // Clone the account to avoid external data changes + clonedAccount := *account + return &clonedAccount, nil +} + +// 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 { + 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") + } + + authorizedOld := account.Data.Authorized + account.Data.Authorized = authorized + + mngr.storage.AccountUpdated(account) + mngr.writeAllAccounts() + + if account.Data.Authorized && account.Data.Authorized != authorizedOld { + _ = 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 *data.Account, flags int) 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") + } + + if len(account.Data.APIKey) > 0 { + return errors.Errorf("the account already has an API key assigned") + } + + for { + apiKey, err := key.GenerateAPIKey(strings.ToLower(account.Email), flags) // Use the (lowered) email address as the key's salt value + if err != nil { + return errors.Wrap(err, "error while generating API key") + } + + // See if the key already exists (super extremely unlikely); if so, generate a new one and try again + if acc, _ := mngr.findAccount(FindByAPIKey, apiKey); acc != nil { + continue + } + + account.Data.APIKey = apiKey + break + } + + mngr.storage.AccountUpdated(account) + mngr.writeAllAccounts() + + _ = email.SendAPIKeyAssigned(account, []string{account.Email, mngr.conf.NotificationsMail}, mngr.smtp) + + return nil +} + +// 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 { + mngr.mutex.Lock() + defer mngr.mutex.Unlock() + + for i, account := range mngr.accounts { + if strings.EqualFold(account.Email, accountData.Email) { + mngr.accounts = append(mngr.accounts[:i], mngr.accounts[i+1:]...) + mngr.storage.AccountRemoved(account) + mngr.writeAllAccounts() + return nil + } + } + + return errors.Errorf("no account with the specified email exists") +} + +// 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 { + 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) + } + } + + return clone +} + +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") + } + return mngr, nil +} diff --git a/internal/http/services/siteacc/panel/panel.go b/internal/http/services/siteacc/panel/panel.go new file mode 100644 index 0000000000..ce8d6cd916 --- /dev/null +++ b/internal/http/services/siteacc/panel/panel.go @@ -0,0 +1,80 @@ +// 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 panel + +import ( + "html/template" + "net/http" + + "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 + log *zerolog.Logger + + tpl *template.Template +} + +func (panel *Panel) initialize(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 + + // 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") + } + + return nil +} + +// 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 { + type TemplateData struct { + Accounts *data.Accounts + } + + tplData := TemplateData{ + Accounts: accounts, + } + + return panel.tpl.Execute(w, tplData) +} + +// NewPanel creates a new web interface panel. +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") + } + return panel, nil +} diff --git a/internal/http/services/siteacc/panel/template.go b/internal/http/services/siteacc/panel/template.go new file mode 100644 index 0000000000..7493d15287 --- /dev/null +++ b/internal/http/services/siteacc/panel/template.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 panel + +const panelTemplate = ` + + + + + + Accounts panel + + + +

Accounts ({{.Accounts | len}})

+

+

    + {{range .Accounts}} +
  • +

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

    +

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

    + Authorized: + {{if .Data.Authorized}} + Yes + {{else}} + No + {{end}} +

    +

    +

    + + + + {{if .Data.Authorized}} + + {{else}} + + {{end}} + +   + +
    +

    +
    +
  • + {{end}} +
+

+ + + +` diff --git a/internal/http/services/siteacc/siteacc.go b/internal/http/services/siteacc/siteacc.go new file mode 100644 index 0000000000..1d49e73570 --- /dev/null +++ b/internal/http/services/siteacc/siteacc.go @@ -0,0 +1,365 @@ +// 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 ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + + "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" +) + +func init() { + global.Register(serviceName, New) +} + +type svc struct { + conf *config.Configuration + log *zerolog.Logger + + manager *Manager +} + +const ( + serviceName = "siteacc" +) + +// Close is called when this service is being stopped. +func (s *svc) Close() error { + return nil +} + +// Prefix returns the main endpoint of this service. +func (s *svc) Prefix() string { + return s.conf.Prefix +} + +// Unprotected returns all endpoints that can be queried without prior authorization. +func (s *svc) Unprotected() []string { + // This service currently only has one public endpoint used for account registration + return []string{config.EndpointCreate} +} + +// 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) + + 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) 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}, + } + + // 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(strings.ToLower(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, strings.ToLower(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) 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 remove 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 +} + +func parseConfig(m map[string]interface{}) (*config.Configuration, error) { + conf := &config.Configuration{} + if err := mapstructure.Decode(m, &conf); err != nil { + return nil, errors.Wrap(err, "error decoding configuration") + } + applyDefaultConfig(conf) + return conf, nil +} + +func applyDefaultConfig(conf *config.Configuration) { + if conf.Prefix == "" { + conf.Prefix = serviceName + } + + if conf.Storage.Driver == "" { + conf.Storage.Driver = "file" + } +} + +// New returns a new Site Accounts service. +func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { + // Prepare the configuration + conf, err := parseConfig(m) + if err != nil { + return nil, err + } + + // Create the accounts manager instance + mngr, err := newManager(conf, log) + if err != nil { + return nil, errors.Wrap(err, "error creating the site accounts service") + } + + // Create the service + s := &svc{ + conf: conf, + log: log, + manager: mngr, + } + return s, nil +} diff --git a/pkg/mentix/accservice/accservice.go b/pkg/mentix/accservice/accservice.go new file mode 100644 index 0000000000..8d53bbba50 --- /dev/null +++ b/pkg/mentix/accservice/accservice.go @@ -0,0 +1,100 @@ +// 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 accservice + +import ( + "encoding/json" + "fmt" + "net/url" + "path" + "strings" + + "github.com/pkg/errors" + + "github.com/cs3org/reva/pkg/mentix/config" + "github.com/cs3org/reva/pkg/mentix/utils/network" +) + +// RequestResponse holds the response of an accounts service query. +type RequestResponse struct { + Success bool + Error string + Data interface{} +} + +type accountsServiceSettings struct { + URL *url.URL + User string + Password string +} + +var settings accountsServiceSettings + +// Query performs an account service query. +func Query(endpoint string, params network.URLParams) (*RequestResponse, error) { + fullURL, err := network.GenerateURL(fmt.Sprintf("%v://%v", settings.URL.Scheme, settings.URL.Host), path.Join(settings.URL.Path, endpoint), params) + if err != nil { + return nil, errors.Wrap(err, "error while building the service accounts query URL") + } + + data, err := network.ReadEndpoint(fullURL, &network.BasicAuth{User: settings.User, Password: settings.Password}, false) + if err != nil { + return nil, errors.Wrap(err, "unable to query the service accounts endpoint") + } + + resp := &RequestResponse{} + if err := json.Unmarshal(data, resp); err != nil { + return nil, errors.Wrap(err, "unable to unmarshal response data") + } + return resp, nil +} + +// GetResponseValue gets a value from an account service query using a dotted path notation. +func GetResponseValue(resp *RequestResponse, path string) interface{} { + if data, ok := resp.Data.(map[string]interface{}); ok { + tokens := strings.Split(path, ".") + for i, name := range tokens { + if i == len(tokens)-1 { + if value, ok := data[name]; ok { + return value + } + } + + if data, ok = data[name].(map[string]interface{}); !ok { + break + } + } + } + + return nil +} + +// InitAccountsService initializes the global accounts service. +func InitAccountsService(conf *config.Configuration) error { + URL, err := url.Parse(conf.AccountsService.URL) + if err != nil { + return errors.Wrap(err, "unable to parse the accounts service URL") + } + + settings.URL = URL + settings.User = conf.AccountsService.User + settings.Password = conf.AccountsService.Password + + return nil +} diff --git a/pkg/mentix/config/config.go b/pkg/mentix/config/config.go index b84ded160f..75d44780b2 100644 --- a/pkg/mentix/config/config.go +++ b/pkg/mentix/config/config.go @@ -36,36 +36,31 @@ type Configuration struct { UpdateInterval string `mapstructure:"update_interval"` Importers struct { - WebAPI struct { - Endpoint string `mapstructure:"endpoint"` - IsProtected bool `mapstructure:"is_protected"` - EnabledConnectors []string `mapstructure:"enabled_connectors"` - } `mapstructure:"webapi"` - - AdminAPI struct { - Endpoint string `mapstructure:"endpoint"` - IsProtected bool `mapstructure:"is_protected"` - EnabledConnectors []string `mapstructure:"enabled_connectors"` - } `mapstructure:"adminapi"` + SiteRegistration struct { + Endpoint string `mapstructure:"endpoint"` + EnabledConnectors []string `mapstructure:"enabled_connectors"` + IsProtected bool `mapstructure:"is_protected"` + IgnoreScienceMeshSites bool `mapstructure:"ignore_sm_sites"` + } `mapstructure:"sitereg"` } `mapstructure:"importers"` Exporters struct { WebAPI struct { Endpoint string `mapstructure:"endpoint"` - IsProtected bool `mapstructure:"is_protected"` EnabledConnectors []string `mapstructure:"enabled_connectors"` + IsProtected bool `mapstructure:"is_protected"` } `mapstructure:"webapi"` CS3API struct { Endpoint string `mapstructure:"endpoint"` - IsProtected bool `mapstructure:"is_protected"` EnabledConnectors []string `mapstructure:"enabled_connectors"` + IsProtected bool `mapstructure:"is_protected"` } `mapstructure:"cs3api"` SiteLocations struct { Endpoint string `mapstructure:"endpoint"` - IsProtected bool `mapstructure:"is_protected"` EnabledConnectors []string `mapstructure:"enabled_connectors"` + IsProtected bool `mapstructure:"is_protected"` } `mapstructure:"siteloc"` PrometheusSD struct { @@ -75,6 +70,12 @@ type Configuration struct { } `mapstructure:"promsd"` } `mapstructure:"exporters"` + AccountsService struct { + URL string `mapstructure:"url"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + } `mapstructure:"accounts"` + // Internal settings EnabledConnectors []string `mapstructure:"-"` EnabledImporters []string `mapstructure:"-"` diff --git a/pkg/mentix/config/ids.go b/pkg/mentix/config/ids.go index 9fbb020693..74ccedbe3c 100644 --- a/pkg/mentix/config/ids.go +++ b/pkg/mentix/config/ids.go @@ -26,10 +26,8 @@ const ( ) const ( - // ImporterIDWebAPI is the identifier for the WebAPI importer. - ImporterIDWebAPI = "webapi" - // ImporterIDAdminAPI is the identifier for the AdminAPI importer. - ImporterIDAdminAPI = "adminapi" + // ImporterIDSiteRegistration is the identifier for the external site registration importer. + ImporterIDSiteRegistration = "sitereg" ) const ( diff --git a/pkg/mentix/connectors/gocdb.go b/pkg/mentix/connectors/gocdb.go index 75742f27d3..ca24e85e71 100755 --- a/pkg/mentix/connectors/gocdb.go +++ b/pkg/mentix/connectors/gocdb.go @@ -30,7 +30,7 @@ import ( "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/connectors/gocdb" "github.com/cs3org/reva/pkg/mentix/meshdata" - "github.com/cs3org/reva/pkg/mentix/network" + "github.com/cs3org/reva/pkg/mentix/utils/network" ) // GOCDBConnector is used to read mesh data from a GOCDB instance. @@ -128,9 +128,9 @@ func (connector *GOCDBConnector) querySites(meshData *meshdata.MeshData) error { for _, site := range sites.Sites { properties := connector.extensionsToMap(&site.Extensions) - // Sites coming from the GOCDB are always authorized by default - if value := meshdata.GetPropertyValue(properties, meshdata.PropertyAuthorized, ""); len(value) == 0 { - meshdata.SetPropertyValue(&properties, meshdata.PropertyAuthorized, "true") + siteID := meshdata.GetPropertyValue(properties, meshdata.PropertySiteID, "") + if len(siteID) == 0 { + return fmt.Errorf("site ID missing for site '%v'", site.ShortName) } // See if an organization has been defined using properties; otherwise, use the official name @@ -138,6 +138,7 @@ func (connector *GOCDBConnector) querySites(meshData *meshdata.MeshData) error { meshsite := &meshdata.Site{ Type: meshdata.SiteTypeScienceMesh, // All sites stored in the GOCDB are part of the mesh + ID: siteID, Name: site.ShortName, FullName: site.OfficialName, Organization: organization, diff --git a/pkg/mentix/connectors/gocdb/query.go b/pkg/mentix/connectors/gocdb/query.go index 8a188489b4..a586e38cb7 100755 --- a/pkg/mentix/connectors/gocdb/query.go +++ b/pkg/mentix/connectors/gocdb/query.go @@ -21,7 +21,7 @@ package gocdb import ( "fmt" - "github.com/cs3org/reva/pkg/mentix/network" + "github.com/cs3org/reva/pkg/mentix/utils/network" ) // QueryGOCDB retrieves data from one of GOCDB's endpoints. @@ -43,7 +43,12 @@ func QueryGOCDB(address string, method string, isPrivate bool, scope string, par } // Query the data from GOCDB - data, err := network.ReadEndpoint(address, path, params) + endpointURL, err := network.GenerateURL(address, path, params) + if err != nil { + return nil, fmt.Errorf("unable to generate the GOCDB URL: %v", err) + } + + data, err := network.ReadEndpoint(endpointURL, nil, true) if err != nil { return nil, fmt.Errorf("unable to read GOCDB endpoint: %v", err) } diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go index 6c154adc2c..6b83aab568 100755 --- a/pkg/mentix/connectors/localfile.go +++ b/pkg/mentix/connectors/localfile.go @@ -75,7 +75,7 @@ func (connector *LocalFileConnector) RetrieveMeshData() (*meshdata.MeshData, err return nil, fmt.Errorf("invalid file '%v': %v", connector.filePath, err) } - // Update the site types, as these are not part of the JSON data + // Enforce site types connector.setSiteTypes(meshData) meshData.InferMissingData() @@ -98,12 +98,6 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa case meshdata.StatusObsolete: err = connector.unmergeData(meshData, updatedData) - - case meshdata.StatusAuthorize: - err = connector.authorizeData(meshData, updatedData, true) - - case meshdata.StatusUnauthorize: - err = connector.authorizeData(meshData, updatedData, false) } if err != nil { @@ -120,21 +114,8 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa } func (connector *LocalFileConnector) mergeData(meshData *meshdata.MeshData, updatedData *meshdata.MeshData) error { - // Store the previous authorization status for already existing sites - siteAuthorizationStatus := make(map[string]string) - for _, site := range meshData.Sites { - siteAuthorizationStatus[site.ID] = meshdata.GetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false") - } - // Add/update data by merging meshData.Merge(updatedData) - - // Restore the authorization status for all sites - for siteID, status := range siteAuthorizationStatus { - if site := meshData.FindSite(siteID); site != nil { - meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, status) - } - } return nil } @@ -144,22 +125,6 @@ func (connector *LocalFileConnector) unmergeData(meshData *meshdata.MeshData, up return nil } -func (connector *LocalFileConnector) authorizeData(meshData *meshdata.MeshData, updatedData *meshdata.MeshData, authorize bool) error { - for _, placeholderSite := range updatedData.Sites { - if site := meshData.FindSite(placeholderSite.ID); site != nil { - if authorize { - meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "true") - } else { - meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "false") - } - } else { - return fmt.Errorf("no site with id '%v' found", placeholderSite.Name) - } - } - - return nil -} - func (connector *LocalFileConnector) setSiteTypes(meshData *meshdata.MeshData) { for _, site := range meshData.Sites { site.Type = meshdata.SiteTypeCommunity // Sites coming from a local file are always community sites diff --git a/pkg/mentix/exchangers/exchanger.go b/pkg/mentix/exchangers/exchanger.go index e3a3f2f534..908b2ba27d 100644 --- a/pkg/mentix/exchangers/exchanger.go +++ b/pkg/mentix/exchangers/exchanger.go @@ -27,6 +27,7 @@ import ( "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/entity" + "github.com/cs3org/reva/pkg/mentix/meshdata" ) // Exchanger is the base interface for importers and exporters. @@ -37,6 +38,12 @@ type Exchanger interface { Start() error // Stop stops any running background activities of the exchanger. Stop() + + // MeshData returns the mesh data. + MeshData() *meshdata.MeshData + + // Update is called whenever the mesh data set has changed to reflect these changes. + Update(meshdata.Map) error } // BaseExchanger implements basic exchanger functionality common to all exchangers. @@ -48,6 +55,9 @@ type BaseExchanger struct { enabledConnectors []string + meshData *meshdata.MeshData + allowUnauthorizedSites bool + locker sync.RWMutex } @@ -85,6 +95,55 @@ func (exchanger *BaseExchanger) IsConnectorEnabled(id string) bool { return false } +// Update is called whenever the mesh data set has changed to reflect these changes. +func (exchanger *BaseExchanger) Update(meshDataSet meshdata.Map) error { + // Update the stored mesh data set + if err := exchanger.storeMeshDataSet(meshDataSet); err != nil { + return fmt.Errorf("unable to store the mesh data: %v", err) + } + + return nil +} + +func (exchanger *BaseExchanger) storeMeshDataSet(meshDataSet meshdata.Map) error { + // Store the new mesh data set by cloning it and then merging the cloned data into one object + meshDataSetCloned := make(meshdata.Map) + for connectorID, meshData := range meshDataSet { + if !exchanger.IsConnectorEnabled(connectorID) { + continue + } + + meshDataCloned := meshData.Clone() + if meshDataCloned == nil { + return fmt.Errorf("unable to clone the mesh data") + } + + meshDataSetCloned[connectorID] = meshDataCloned + } + exchanger.setMeshData(meshdata.MergeMeshDataMap(meshDataSetCloned)) + + return nil +} + +func (exchanger *BaseExchanger) cloneMeshData(clean bool) *meshdata.MeshData { + exchanger.locker.RLock() + meshDataClone := exchanger.meshData.Clone() + exchanger.locker.RUnlock() + + if clean && !exchanger.allowUnauthorizedSites { + cleanedSites := make([]*meshdata.Site, 0, len(meshDataClone.Sites)) + for _, site := range meshDataClone.Sites { + // Only keep authorized sites + if site.IsAuthorized() { + cleanedSites = append(cleanedSites, site) + } + } + meshDataClone.Sites = cleanedSites + } + + return meshDataClone +} + // Config returns the configuration object. func (exchanger *BaseExchanger) Config() *config.Configuration { return exchanger.conf @@ -105,6 +164,24 @@ func (exchanger *BaseExchanger) SetEnabledConnectors(connectors []string) { exchanger.enabledConnectors = connectors } +// MeshData returns the stored mesh data. The returned data is cloned to prevent accidental data changes. +// Unauthorized sites are also removed if this exchanger doesn't allow them. +func (exchanger *BaseExchanger) MeshData() *meshdata.MeshData { + return exchanger.cloneMeshData(true) +} + +func (exchanger *BaseExchanger) setMeshData(meshData *meshdata.MeshData) { + exchanger.locker.Lock() + defer exchanger.locker.Unlock() + + exchanger.meshData = meshData +} + +// SetAllowUnauthorizedSites sets whether this exchanger allows the exchange of unauthorized sites. +func (exchanger *BaseExchanger) SetAllowUnauthorizedSites(allow bool) { + exchanger.allowUnauthorizedSites = allow +} + // Locker returns the locking object. func (exchanger *BaseExchanger) Locker() *sync.RWMutex { return &exchanger.locker diff --git a/pkg/mentix/exchangers/exporters/cs3api.go b/pkg/mentix/exchangers/exporters/cs3api.go index 3102c0ce88..983c661070 100755 --- a/pkg/mentix/exchangers/exporters/cs3api.go +++ b/pkg/mentix/exchangers/exporters/cs3api.go @@ -40,7 +40,7 @@ func (exporter *CS3APIExporter) Activate(conf *config.Configuration, log *zerolo exporter.SetEndpoint(conf.Exporters.CS3API.Endpoint, conf.Exporters.CS3API.IsProtected) exporter.SetEnabledConnectors(conf.Exporters.CS3API.EnabledConnectors) - exporter.defaultActionHandler = cs3api.HandleDefaultQuery + exporter.RegisterActionHandler("", cs3api.HandleDefaultQuery) return nil } diff --git a/pkg/mentix/exchangers/exporters/cs3api/query.go b/pkg/mentix/exchangers/exporters/cs3api/query.go index a107a1cce8..21d576ba95 100755 --- a/pkg/mentix/exchangers/exporters/cs3api/query.go +++ b/pkg/mentix/exchangers/exporters/cs3api/query.go @@ -25,12 +25,14 @@ import ( "net/url" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + "github.com/rs/zerolog" + "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/meshdata" ) // HandleDefaultQuery processes a basic query. -func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values) (int, []byte, error) { +func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values, _ *config.Configuration, _ *zerolog.Logger) (int, []byte, error) { // Convert the mesh data ocmData, err := convertMeshDataToOCMData(meshData) if err != nil { diff --git a/pkg/mentix/exchangers/exporters/exporter.go b/pkg/mentix/exchangers/exporters/exporter.go index 605ca083f2..2b25d7228e 100755 --- a/pkg/mentix/exchangers/exporters/exporter.go +++ b/pkg/mentix/exchangers/exporters/exporter.go @@ -19,87 +19,15 @@ package exporters import ( - "fmt" - "strings" - "github.com/cs3org/reva/pkg/mentix/exchangers" - "github.com/cs3org/reva/pkg/mentix/meshdata" ) // Exporter is the interface that all exporters must implement. type Exporter interface { exchangers.Exchanger - - // MeshData returns the mesh data. - MeshData() *meshdata.MeshData - - // Update is called whenever the mesh data set has changed to reflect these changes. - Update(meshdata.Map) error } // BaseExporter implements basic exporter functionality common to all exporters. type BaseExporter struct { exchangers.BaseExchanger - - meshData *meshdata.MeshData - - allowUnauthorizedSites bool -} - -// Update is called whenever the mesh data set has changed to reflect these changes. -func (exporter *BaseExporter) Update(meshDataSet meshdata.Map) error { - // Update the stored mesh data set - if err := exporter.storeMeshDataSet(meshDataSet); err != nil { - return fmt.Errorf("unable to store the mesh data: %v", err) - } - - return nil -} - -func (exporter *BaseExporter) storeMeshDataSet(meshDataSet meshdata.Map) error { - // Store the new mesh data set by cloning it and then merging the cloned data into one object - meshDataSetCloned := make(meshdata.Map) - for connectorID, meshData := range meshDataSet { - if !exporter.IsConnectorEnabled(connectorID) { - continue - } - - meshDataCloned := meshData.Clone() - if meshDataCloned == nil { - return fmt.Errorf("unable to clone the mesh data") - } - - if !exporter.allowUnauthorizedSites { - exporter.removeUnauthorizedSites(meshDataCloned) - } - - meshDataSetCloned[connectorID] = meshDataCloned - } - exporter.SetMeshData(meshdata.MergeMeshDataMap(meshDataSetCloned)) - - return nil -} - -// MeshData returns the stored mesh data. -func (exporter *BaseExporter) MeshData() *meshdata.MeshData { - return exporter.meshData -} - -// SetMeshData sets new mesh data. -func (exporter *BaseExporter) SetMeshData(meshData *meshdata.MeshData) { - exporter.Locker().Lock() - defer exporter.Locker().Unlock() - - exporter.meshData = meshData -} - -func (exporter *BaseExporter) removeUnauthorizedSites(meshData *meshdata.MeshData) { - cleanedSites := make([]*meshdata.Site, 0, len(meshData.Sites)) - for _, site := range meshData.Sites { - // Only keep authorized sites - if value := meshdata.GetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false"); strings.EqualFold(value, "true") { - cleanedSites = append(cleanedSites, site) - } - } - meshData.Sites = cleanedSites } diff --git a/pkg/mentix/exchangers/exporters/reqexporter.go b/pkg/mentix/exchangers/exporters/reqexporter.go index 368bb56260..59c15439ce 100644 --- a/pkg/mentix/exchangers/exporters/reqexporter.go +++ b/pkg/mentix/exchangers/exporters/reqexporter.go @@ -19,32 +19,26 @@ package exporters import ( - "fmt" + "io/ioutil" "net/http" "net/url" - "strings" - "github.com/cs3org/reva/pkg/mentix/exchangers" - "github.com/cs3org/reva/pkg/mentix/meshdata" -) + "github.com/rs/zerolog" -const ( - queryActionDefault = "" + "github.com/cs3org/reva/pkg/mentix/config" + "github.com/cs3org/reva/pkg/mentix/exchangers" ) -type queryCallback func(*meshdata.MeshData, url.Values) (int, []byte, error) - // BaseRequestExporter implements basic exporter functionality common to all request exporters. type BaseRequestExporter struct { BaseExporter exchangers.BaseRequestExchanger - - defaultActionHandler queryCallback } // HandleRequest handles the actual HTTP request. -func (exporter *BaseRequestExporter) HandleRequest(resp http.ResponseWriter, req *http.Request) { - status, respData, err := exporter.handleQuery(req.URL.Query()) +func (exporter *BaseRequestExporter) HandleRequest(resp http.ResponseWriter, req *http.Request, conf *config.Configuration, log *zerolog.Logger) { + body, _ := ioutil.ReadAll(req.Body) + status, respData, err := exporter.handleQuery(body, req.URL.Query(), conf, log) if err != nil { respData = []byte(err.Error()) } @@ -52,21 +46,7 @@ func (exporter *BaseRequestExporter) HandleRequest(resp http.ResponseWriter, req _, _ = resp.Write(respData) } -func (exporter *BaseRequestExporter) handleQuery(params url.Values) (int, []byte, error) { - // Data is read, so lock it for writing - exporter.Locker().RLock() - defer exporter.Locker().RUnlock() - - action := params.Get("action") - switch strings.ToLower(action) { - case queryActionDefault: - if exporter.defaultActionHandler != nil { - return exporter.defaultActionHandler(exporter.MeshData(), params) - } - - default: - return http.StatusNotImplemented, []byte{}, fmt.Errorf("unknown action '%v'", action) - } - - return http.StatusNotImplemented, []byte{}, fmt.Errorf("unhandled query for action '%v'", action) +func (exporter *BaseRequestExporter) handleQuery(body []byte, params url.Values, conf *config.Configuration, log *zerolog.Logger) (int, []byte, error) { + _, status, data, err := exporter.HandleAction(exporter.MeshData(), body, params, false, conf, log) + return status, data, err } diff --git a/pkg/mentix/exchangers/exporters/siteloc/query.go b/pkg/mentix/exchangers/exporters/siteloc/query.go index 491462d5e1..ba534e7597 100755 --- a/pkg/mentix/exchangers/exporters/siteloc/query.go +++ b/pkg/mentix/exchangers/exporters/siteloc/query.go @@ -24,11 +24,14 @@ import ( "net/http" "net/url" + "github.com/rs/zerolog" + + "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/meshdata" ) // HandleDefaultQuery processes a basic query. -func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values) (int, []byte, error) { +func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values, _ *config.Configuration, _ *zerolog.Logger) (int, []byte, error) { // Convert the mesh data locData, err := convertMeshDataToLocationData(meshData) if err != nil { @@ -49,7 +52,7 @@ func convertMeshDataToLocationData(meshData *meshdata.MeshData) ([]*SiteLocation locations := make([]*SiteLocation, 0, len(meshData.Sites)) for _, site := range meshData.Sites { locations = append(locations, &SiteLocation{ - Site: site.Name, + SiteID: site.ID, FullName: site.FullName, Longitude: site.Longitude, Latitude: site.Latitude, diff --git a/pkg/mentix/exchangers/exporters/siteloc/types.go b/pkg/mentix/exchangers/exporters/siteloc/types.go index 10b2979947..d6dd060ce0 100644 --- a/pkg/mentix/exchangers/exporters/siteloc/types.go +++ b/pkg/mentix/exchangers/exporters/siteloc/types.go @@ -20,7 +20,7 @@ package siteloc // SiteLocation represents the location information of a site. type SiteLocation struct { - Site string `json:"key"` + SiteID string `json:"key"` FullName string `json:"name"` Longitude float32 `json:"longitude"` Latitude float32 `json:"latitude"` diff --git a/pkg/mentix/exchangers/exporters/sitelocations.go b/pkg/mentix/exchangers/exporters/sitelocations.go index ce6034f89b..d3f8f1b352 100755 --- a/pkg/mentix/exchangers/exporters/sitelocations.go +++ b/pkg/mentix/exchangers/exporters/sitelocations.go @@ -40,7 +40,7 @@ func (exporter *SiteLocationsExporter) Activate(conf *config.Configuration, log exporter.SetEndpoint(conf.Exporters.SiteLocations.Endpoint, conf.Exporters.SiteLocations.IsProtected) exporter.SetEnabledConnectors(conf.Exporters.SiteLocations.EnabledConnectors) - exporter.defaultActionHandler = siteloc.HandleDefaultQuery + exporter.RegisterActionHandler("", siteloc.HandleDefaultQuery) return nil } diff --git a/pkg/mentix/exchangers/exporters/webapi.go b/pkg/mentix/exchangers/exporters/webapi.go index f784c3df0d..e65b00c662 100755 --- a/pkg/mentix/exchangers/exporters/webapi.go +++ b/pkg/mentix/exchangers/exporters/webapi.go @@ -39,10 +39,9 @@ func (exporter *WebAPIExporter) Activate(conf *config.Configuration, log *zerolo // Store WebAPI specifics exporter.SetEndpoint(conf.Exporters.WebAPI.Endpoint, conf.Exporters.WebAPI.IsProtected) exporter.SetEnabledConnectors(conf.Exporters.WebAPI.EnabledConnectors) + exporter.SetAllowUnauthorizedSites(true) - exporter.allowUnauthorizedSites = true - - exporter.defaultActionHandler = webapi.HandleDefaultQuery + exporter.RegisterActionHandler("", webapi.HandleDefaultQuery) return nil } diff --git a/pkg/mentix/exchangers/exporters/webapi/query.go b/pkg/mentix/exchangers/exporters/webapi/query.go index 362968fecb..aa664bdff5 100755 --- a/pkg/mentix/exchangers/exporters/webapi/query.go +++ b/pkg/mentix/exchangers/exporters/webapi/query.go @@ -24,11 +24,14 @@ import ( "net/http" "net/url" + "github.com/rs/zerolog" + + "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/meshdata" ) // HandleDefaultQuery processes a basic query. -func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values) (int, []byte, error) { +func HandleDefaultQuery(meshData *meshdata.MeshData, params url.Values, _ *config.Configuration, _ *zerolog.Logger) (int, []byte, error) { // Just return the plain, unfiltered data as JSON data, err := json.MarshalIndent(meshData, "", "\t") if err != nil { diff --git a/pkg/mentix/exchangers/importers/adminapi/query.go b/pkg/mentix/exchangers/importers/adminapi/query.go deleted file mode 100755 index cb7a65a198..0000000000 --- a/pkg/mentix/exchangers/importers/adminapi/query.go +++ /dev/null @@ -1,73 +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 adminapi - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "strings" - - "github.com/cs3org/reva/pkg/mentix/meshdata" - "github.com/cs3org/reva/pkg/mentix/network" -) - -func decodeAdminQueryData(data []byte) (*meshdata.MeshData, error) { - jsonData := make(map[string]interface{}) - if err := json.Unmarshal(data, &jsonData); err != nil { - return nil, err - } - - if value, ok := jsonData["id"]; ok { - if id, ok := value.(string); ok { - site := &meshdata.Site{} - site.ID = id // We only need to store the ID of the site - - meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}} - return meshData, nil - } - - return nil, fmt.Errorf("site id invalid") - } - - return nil, fmt.Errorf("site id missing") -} - -func handleAdminQuery(data []byte, params url.Values, status int, msg string) (meshdata.Vector, int, []byte, error) { - meshData, err := decodeAdminQueryData(data) - if err != nil { - return nil, http.StatusBadRequest, network.CreateResponse("INVALID_DATA", network.ResponseParams{"error": err.Error()}), nil - } - meshData.Status = status - return meshdata.Vector{meshData}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": meshData.Sites[0].Name}), nil -} - -// HandleAuthorizeSiteQuery sets the authorization status of a site. -func HandleAuthorizeSiteQuery(data []byte, params url.Values) (meshdata.Vector, int, []byte, error) { - status := params.Get("status") - - if strings.EqualFold(status, "true") { - return handleAdminQuery(data, params, meshdata.StatusAuthorize, "SITE_AUTHORIZED") - } else if strings.EqualFold(status, "false") { - return handleAdminQuery(data, params, meshdata.StatusUnauthorize, "SITE_UNAUTHORIZED") - } - - return nil, http.StatusBadRequest, network.CreateResponse("INVALID_QUERY", network.ResponseParams{}), nil -} diff --git a/pkg/mentix/exchangers/importers/importer.go b/pkg/mentix/exchangers/importers/importer.go index e6625bc868..d955f3608a 100755 --- a/pkg/mentix/exchangers/importers/importer.go +++ b/pkg/mentix/exchangers/importers/importer.go @@ -21,6 +21,7 @@ package importers import ( "fmt" "strings" + "sync" "github.com/cs3org/reva/pkg/mentix/connectors" "github.com/cs3org/reva/pkg/mentix/exchangers" @@ -31,9 +32,6 @@ import ( type Importer interface { exchangers.Exchanger - // MeshData returns the vector of imported mesh data. - MeshData() meshdata.Vector - // Process is called periodically to perform the actual import; if data has been imported, true is returned. Process(*connectors.Collection) (bool, error) } @@ -42,31 +40,33 @@ type Importer interface { type BaseImporter struct { exchangers.BaseExchanger - meshData meshdata.Vector + meshDataUpdates meshdata.Vector + + updatesLocker sync.RWMutex } // Process is called periodically to perform the actual import; if data has been imported, true is returned. func (importer *BaseImporter) Process(connectors *connectors.Collection) (bool, error) { - if importer.meshData == nil { // No data present for updating, so nothing to process + if importer.meshDataUpdates == nil { // No data present for updating, so nothing to process return false, nil } var processErrs []string // Data is read, so lock it for writing during the loop - importer.Locker().RLock() + importer.updatesLocker.RLock() for _, connector := range connectors.Connectors { if !importer.IsConnectorEnabled(connector.GetID()) { continue } - if err := importer.processMeshData(connector); err != nil { + if err := importer.processMeshDataUpdates(connector); err != nil { processErrs = append(processErrs, fmt.Sprintf("unable to process imported mesh data for connector '%v': %v", connector.GetName(), err)) } } - importer.Locker().RUnlock() + importer.updatesLocker.RUnlock() - importer.SetMeshData(nil) + importer.setMeshDataUpdates(nil) var err error if len(processErrs) != 0 { @@ -75,8 +75,8 @@ func (importer *BaseImporter) Process(connectors *connectors.Collection) (bool, return true, err } -func (importer *BaseImporter) processMeshData(connector connectors.Connector) error { - for _, meshData := range importer.meshData { +func (importer *BaseImporter) processMeshDataUpdates(connector connectors.Connector) error { + for _, meshData := range importer.meshDataUpdates { if err := connector.UpdateMeshData(meshData); err != nil { return fmt.Errorf("error while updating mesh data: %v", err) } @@ -85,15 +85,9 @@ func (importer *BaseImporter) processMeshData(connector connectors.Connector) er return nil } -// MeshData returns the vector of imported mesh data. -func (importer *BaseImporter) MeshData() meshdata.Vector { - return importer.meshData -} - -// SetMeshData sets the new mesh data vector. -func (importer *BaseImporter) SetMeshData(meshData meshdata.Vector) { - importer.Locker().Lock() - defer importer.Locker().Unlock() +func (importer *BaseImporter) setMeshDataUpdates(meshDataUpdates meshdata.Vector) { + importer.updatesLocker.Lock() + defer importer.updatesLocker.Unlock() - importer.meshData = meshData + importer.meshDataUpdates = meshDataUpdates } diff --git a/pkg/mentix/exchangers/importers/reqimporter.go b/pkg/mentix/exchangers/importers/reqimporter.go index 673e4ff161..933db84fa5 100644 --- a/pkg/mentix/exchangers/importers/reqimporter.go +++ b/pkg/mentix/exchangers/importers/reqimporter.go @@ -19,41 +19,30 @@ package importers import ( - "fmt" "io/ioutil" "net/http" "net/url" - "strings" + "github.com/rs/zerolog" + + "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/exchangers" "github.com/cs3org/reva/pkg/mentix/meshdata" ) -const ( - queryActionRegisterSite = "register" - queryActionUnregisterSite = "unregister" - queryActionAuthorizeSite = "authorize" -) - -type queryCallback func([]byte, url.Values) (meshdata.Vector, int, []byte, error) - // BaseRequestImporter implements basic importer functionality common to all request importers. type BaseRequestImporter struct { BaseImporter exchangers.BaseRequestExchanger - - registerSiteActionHandler queryCallback - unregisterSiteActionHandler queryCallback - authorizeSiteActionHandler queryCallback } // HandleRequest handles the actual HTTP request. -func (importer *BaseRequestImporter) HandleRequest(resp http.ResponseWriter, req *http.Request) { +func (importer *BaseRequestImporter) HandleRequest(resp http.ResponseWriter, req *http.Request, conf *config.Configuration, log *zerolog.Logger) { body, _ := ioutil.ReadAll(req.Body) - meshData, status, respData, err := importer.handleQuery(body, req.URL.Path, req.URL.Query()) + meshDataSet, status, respData, err := importer.handleQuery(body, req.URL.Query(), conf, log) if err == nil { - if len(meshData) > 0 { - importer.mergeImportedMeshData(meshData) + if len(meshDataSet) > 0 { + importer.mergeImportedMeshDataSet(meshDataSet) } } else { respData = []byte(err.Error()) @@ -62,40 +51,23 @@ func (importer *BaseRequestImporter) HandleRequest(resp http.ResponseWriter, req _, _ = resp.Write(respData) } -func (importer *BaseRequestImporter) mergeImportedMeshData(meshData meshdata.Vector) { +func (importer *BaseRequestImporter) mergeImportedMeshDataSet(meshDataSet meshdata.Vector) { // Merge the newly imported data with any existing data stored in the importer - if importer.meshData != nil { + if importer.meshDataUpdates != nil { // Need to manually lock the data for writing - importer.Locker().Lock() - defer importer.Locker().Unlock() + importer.updatesLocker.Lock() + defer importer.updatesLocker.Unlock() - importer.meshData = append(importer.meshData, meshData...) + importer.meshDataUpdates = append(importer.meshDataUpdates, meshDataSet...) } else { - importer.SetMeshData(meshData) // SetMeshData will do the locking itself + importer.setMeshDataUpdates(meshDataSet) // SetMeshData will do the locking itself } } -func (importer *BaseRequestImporter) handleQuery(data []byte, path string, params url.Values) (meshdata.Vector, int, []byte, error) { - action := params.Get("action") - switch strings.ToLower(action) { - case queryActionRegisterSite: - if importer.registerSiteActionHandler != nil { - return importer.registerSiteActionHandler(data, params) - } - - case queryActionUnregisterSite: - if importer.unregisterSiteActionHandler != nil { - return importer.unregisterSiteActionHandler(data, params) - } - - case queryActionAuthorizeSite: - if importer.authorizeSiteActionHandler != nil { - return importer.authorizeSiteActionHandler(data, params) - } - - default: - return nil, http.StatusNotImplemented, []byte{}, fmt.Errorf("unknown action '%v'", action) - } +func (importer *BaseRequestImporter) handleQuery(data []byte, params url.Values, conf *config.Configuration, log *zerolog.Logger) (meshdata.Vector, int, []byte, error) { + // Data is read, so lock it for writing + importer.Locker().RLock() + defer importer.Locker().RUnlock() - return nil, http.StatusNotFound, []byte{}, fmt.Errorf("unhandled query for action '%v'", action) + return importer.HandleAction(importer.MeshData(), data, params, true, conf, log) } diff --git a/pkg/mentix/exchangers/importers/adminapi.go b/pkg/mentix/exchangers/importers/sitereg.go old mode 100755 new mode 100644 similarity index 54% rename from pkg/mentix/exchangers/importers/adminapi.go rename to pkg/mentix/exchangers/importers/sitereg.go index 53ee50748b..34c642527c --- a/pkg/mentix/exchangers/importers/adminapi.go +++ b/pkg/mentix/exchangers/importers/sitereg.go @@ -22,39 +22,41 @@ import ( "github.com/rs/zerolog" "github.com/cs3org/reva/pkg/mentix/config" - "github.com/cs3org/reva/pkg/mentix/exchangers/importers/adminapi" + "github.com/cs3org/reva/pkg/mentix/exchangers/importers/sitereg" ) -// AdminAPIImporter implements the administrative API importer. -type AdminAPIImporter struct { +// SiteRegistrationImporter implements the external site registration importer. +type SiteRegistrationImporter struct { BaseRequestImporter } // Activate activates the importer. -func (importer *AdminAPIImporter) Activate(conf *config.Configuration, log *zerolog.Logger) error { +func (importer *SiteRegistrationImporter) Activate(conf *config.Configuration, log *zerolog.Logger) error { if err := importer.BaseRequestImporter.Activate(conf, log); err != nil { return err } - // Store AdminAPI specifics - importer.SetEndpoint(conf.Importers.AdminAPI.Endpoint, conf.Importers.AdminAPI.IsProtected) - importer.SetEnabledConnectors(conf.Importers.AdminAPI.EnabledConnectors) + // Store SiteRegistration specifics + importer.SetEndpoint(conf.Importers.SiteRegistration.Endpoint, conf.Importers.SiteRegistration.IsProtected) + importer.SetEnabledConnectors(conf.Importers.SiteRegistration.EnabledConnectors) + importer.SetAllowUnauthorizedSites(true) - importer.authorizeSiteActionHandler = adminapi.HandleAuthorizeSiteQuery + importer.RegisterExtendedActionHandler("register", sitereg.HandleRegisterSiteQuery) + importer.RegisterExtendedActionHandler("unregister", sitereg.HandleUnregisterSiteQuery) return nil } // GetID returns the ID of the importer. -func (importer *AdminAPIImporter) GetID() string { - return config.ImporterIDAdminAPI +func (importer *SiteRegistrationImporter) GetID() string { + return config.ImporterIDSiteRegistration } // GetName returns the display name of the importer. -func (importer *AdminAPIImporter) GetName() string { - return "AdminAPI" +func (importer *SiteRegistrationImporter) GetName() string { + return "SiteRegistration" } func init() { - registerImporter(&AdminAPIImporter{}) + registerImporter(&SiteRegistrationImporter{}) } diff --git a/pkg/mentix/exchangers/importers/sitereg/query.go b/pkg/mentix/exchangers/importers/sitereg/query.go new file mode 100755 index 0000000000..bf43a53ceb --- /dev/null +++ b/pkg/mentix/exchangers/importers/sitereg/query.go @@ -0,0 +1,157 @@ +// 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 sitereg + +import ( + "encoding/json" + "net/http" + "net/url" + "strings" + + "github.com/pkg/errors" + "github.com/rs/zerolog" + + "github.com/cs3org/reva/pkg/mentix/accservice" + "github.com/cs3org/reva/pkg/mentix/config" + "github.com/cs3org/reva/pkg/mentix/key" + "github.com/cs3org/reva/pkg/mentix/meshdata" + "github.com/cs3org/reva/pkg/mentix/utils/network" +) + +func decodeQueryData(data []byte) (*siteRegistrationData, error) { + siteData := &siteRegistrationData{} + if err := json.Unmarshal(data, siteData); err != nil { + return nil, err + } + + if err := siteData.Verify(); err != nil { + return nil, errors.Wrap(err, "verifying the imported site data failed") + } + + return siteData, nil +} + +func extractQueryInformation(params url.Values) (key.SiteIdentifier, int, string, error) { + apiKey := params.Get("apiKey") + if len(apiKey) == 0 { + return "", 0, "", errors.Errorf("no API key specified") + } + + // Try to get an account that is associated with the given API key; if none exists, return an error + resp, err := accservice.Query("find", network.URLParams{"by": "apikey", "value": apiKey}) + if err != nil { + return "", 0, "", errors.Wrap(err, "error while querying the accounts service") + } + if !resp.Success { + return "", 0, "", errors.Errorf("unable to fetch account associated with the provided API key: %v", resp.Error) + } + + // Extract email from account data; this is needed to calculate the site ID from the API key + email := "" + if value := accservice.GetResponseValue(resp, "account.email"); value != nil { + email, _ = value.(string) + } + if len(email) == 0 { + return "", 0, "", errors.Errorf("could not get the email address of the user account") + } + + _, flags, _, err := key.SplitAPIKey(apiKey) + if err != nil { + return "", 0, "", errors.Errorf("sticky API key specified") + } + + siteID, err := key.CalculateSiteID(apiKey, strings.ToLower(email)) + if err != nil { + return "", 0, "", errors.Wrap(err, "unable to get site ID") + } + + return siteID, flags, email, nil +} + +func createErrorResponse(msg string, err error) (meshdata.Vector, int, []byte, error) { + return nil, http.StatusBadRequest, network.CreateResponse(msg, network.ResponseParams{"error": err.Error()}), nil +} + +// HandleRegisterSiteQuery registers a site. +func HandleRegisterSiteQuery(meshData *meshdata.MeshData, data []byte, params url.Values, conf *config.Configuration, _ *zerolog.Logger) (meshdata.Vector, int, []byte, error) { + siteID, flags, email, err := extractQueryInformation(params) + if err != nil { + return createErrorResponse("INVALID_API_KEY", err) + } + + msg := "SITE_REGISTERED" + if meshData.FindSite(siteID) != nil { + msg = "SITE_UPDATED" + } + + // Decode the site registration data and convert it to a meshdata object + siteData, err := decodeQueryData(data) + if err != nil { + return createErrorResponse("INVALID_SITE_DATA", err) + } + + siteType := meshdata.SiteTypeCommunity + if flags&key.FlagScienceMesh == key.FlagScienceMesh { + siteType = meshdata.SiteTypeScienceMesh + } + + // If the corresponding setting is set, ignore registrations of ScienceMesh sites + if siteType == meshdata.SiteTypeScienceMesh && conf.Importers.SiteRegistration.IgnoreScienceMeshSites { + return meshdata.Vector{}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": siteID}), nil + } + + site, err := siteData.ToMeshDataSite(siteID, siteType, email) + if err != nil { + return createErrorResponse("INVALID_SITE_DATA", err) + } + + meshDataUpdate := &meshdata.MeshData{Sites: []*meshdata.Site{site}} + if err := meshDataUpdate.Verify(); err != nil { + return createErrorResponse("INVALID_MESH_DATA", err) + } + meshDataUpdate.Status = meshdata.StatusDefault + meshDataUpdate.InferMissingData() + + return meshdata.Vector{meshDataUpdate}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": siteID}), nil +} + +// HandleUnregisterSiteQuery unregisters a site. +func HandleUnregisterSiteQuery(meshData *meshdata.MeshData, _ []byte, params url.Values, _ *config.Configuration, _ *zerolog.Logger) (meshdata.Vector, int, []byte, error) { + siteID, _, _, err := extractQueryInformation(params) + if err != nil { + return createErrorResponse("INVALID_API_KEY", err) + } + + // The site ID must be provided in the call as well to enhance security further + if params.Get("siteId") != siteID { + return createErrorResponse("INVALID_SITE_ID", errors.Errorf("site ID mismatch")) + } + + // Check if the site to be removed actually exists + if meshData.FindSite(siteID) == nil { + return createErrorResponse("INVALID_SITE_ID", errors.Errorf("site not found")) + } + + // To remove a site, a meshdata object that contains a site with the given ID needs to be created + site := &meshdata.Site{ID: siteID} + meshDataUpdate := &meshdata.MeshData{Sites: []*meshdata.Site{site}} + meshDataUpdate.Status = meshdata.StatusObsolete + + return meshdata.Vector{meshDataUpdate}, http.StatusOK, network.CreateResponse("SITE_UNREGISTERED", network.ResponseParams{"id": siteID}), nil +} diff --git a/pkg/mentix/exchangers/importers/sitereg/types.go b/pkg/mentix/exchangers/importers/sitereg/types.go new file mode 100644 index 0000000000..b10bcc39dc --- /dev/null +++ b/pkg/mentix/exchangers/importers/sitereg/types.go @@ -0,0 +1,143 @@ +// 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/meshdata" + "github.com/cs3org/reva/pkg/mentix/utils/countries" + "github.com/cs3org/reva/pkg/mentix/utils/network" +) + +type siteRegistrationData struct { + Name string `json:"name"` + URL string `json:"url"` + CountryCode string `json:"countryCode"` + + Reva struct { + Host string `json:"host"` + URL string `json:"url"` + MetricsPath string `json:"metricsPath"` + } `json:"reva"` +} + +/* Example JSON: +{ + "name": "Testsite", + "url": "https://test-site.de/owncloud", + "countryCode": "DE", + "reva": { + "url": "https://test-site.de/owncloud/reva", + "metricsPath": "/iop/metrics" + } +} +*/ + +// Verify checks whether the entered data is valid and complete. +func (siteData *siteRegistrationData) Verify() error { + if len(siteData.Name) == 0 { + return errors.Errorf("no site name provided") + } + if len(siteData.URL) > 0 { + if _, err := url.Parse(siteData.URL); err != nil { + return errors.Wrap(err, "invalid site URL provided") + } + } else { + return errors.Errorf("no site URL provided") + } + + if len(siteData.Reva.Host) == 0 && len(siteData.Reva.URL) == 0 { + return errors.Errorf("no Reva host or URL provided") + } + if len(siteData.Reva.URL) > 0 { + if _, err := url.Parse(siteData.Reva.URL); err != nil { + return errors.Wrap(err, "invalid Reva URL provided") + } + } + if len(siteData.Reva.MetricsPath) == 0 { + return errors.Errorf("no Reva metrics path provided") + } + + return nil +} + +// ToMeshDataSite converts the stored data into a meshdata site object, filling out as much data as possible. +func (siteData *siteRegistrationData) ToMeshDataSite(siteID key.SiteIdentifier, siteType meshdata.SiteType, email string) (*meshdata.Site, error) { + siteURL, err := url.Parse(siteData.URL) + if err != nil { + return nil, errors.Wrap(err, "invalid site URL") + } + + // Create the Reva service entry + revaHost := siteData.Reva.Host + revaURL := siteData.Reva.URL + + if len(revaHost) == 0 { // Infer host from URL + URL, _ := url.Parse(revaURL) + revaHost = network.ExtractDomainFromURL(URL, true) + } else if len(revaURL) == 0 { // Infer URL from host + URL, _ := network.GenerateURL(revaHost, "", network.URLParams{}) + revaURL = URL.String() + } + + properties := make(map[string]string, 1) + meshdata.SetPropertyValue(&properties, meshdata.PropertyMetricsPath, siteData.Reva.MetricsPath) + + revaService := &meshdata.Service{ + ServiceEndpoint: &meshdata.ServiceEndpoint{ + Type: &meshdata.ServiceType{ + Name: "REVAD", + Description: "Reva Daemon", + }, + Name: revaHost + " - REVAD", + URL: revaURL, + IsMonitored: true, + Properties: properties, + }, + Host: revaHost, + AdditionalEndpoints: nil, + } + + // Create the site data + site := &meshdata.Site{ + Type: siteType, + ID: siteID, + Name: siteData.Name, + FullName: siteData.Name, + Organization: "", + Domain: network.ExtractDomainFromURL(siteURL, true), + Homepage: siteData.URL, + Email: email, + Description: siteData.Name + " @ " + siteData.URL, + Country: countries.LookupCountry(siteData.CountryCode), + CountryCode: siteData.CountryCode, + Location: "", + Latitude: 0, + Longitude: 0, + Services: []*meshdata.Service{revaService}, + Properties: nil, + } + + site.InferMissingData() + return site, nil +} diff --git a/pkg/mentix/exchangers/importers/webapi.go b/pkg/mentix/exchangers/importers/webapi.go deleted file mode 100755 index 508ff62643..0000000000 --- a/pkg/mentix/exchangers/importers/webapi.go +++ /dev/null @@ -1,61 +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. - -package importers - -import ( - "github.com/rs/zerolog" - - "github.com/cs3org/reva/pkg/mentix/config" - "github.com/cs3org/reva/pkg/mentix/exchangers/importers/webapi" -) - -// WebAPIImporter implements the generic Web API importer. -type WebAPIImporter struct { - BaseRequestImporter -} - -// Activate activates the importer. -func (importer *WebAPIImporter) Activate(conf *config.Configuration, log *zerolog.Logger) error { - if err := importer.BaseRequestImporter.Activate(conf, log); err != nil { - return err - } - - // Store WebAPI specifics - importer.SetEndpoint(conf.Importers.WebAPI.Endpoint, conf.Importers.WebAPI.IsProtected) - importer.SetEnabledConnectors(conf.Importers.WebAPI.EnabledConnectors) - - importer.registerSiteActionHandler = webapi.HandleRegisterSiteQuery - importer.unregisterSiteActionHandler = webapi.HandleUnregisterSiteQuery - - return nil -} - -// GetID returns the ID of the importer. -func (importer *WebAPIImporter) GetID() string { - return config.ImporterIDWebAPI -} - -// GetName returns the display name of the importer. -func (importer *WebAPIImporter) GetName() string { - return "WebAPI" -} - -func init() { - registerImporter(&WebAPIImporter{}) -} diff --git a/pkg/mentix/exchangers/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go deleted file mode 100755 index 868d0c8cc3..0000000000 --- a/pkg/mentix/exchangers/importers/webapi/query.go +++ /dev/null @@ -1,69 +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. - -package webapi - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - - "github.com/cs3org/reva/pkg/mentix/meshdata" - "github.com/cs3org/reva/pkg/mentix/network" -) - -func decodeQueryData(data []byte) (*meshdata.MeshData, error) { - site := &meshdata.Site{} - if err := json.Unmarshal(data, site); err != nil { - return nil, err - } - - // Imported sites will be assigned an ID automatically - site.ID = "" - - // Set sites imported through the WebAPI to 'unauthorized' by default - meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "false") - - meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}} - if err := meshData.Verify(); err != nil { - return nil, fmt.Errorf("verifying the imported mesh data failed: %v", err) - } - - meshData.InferMissingData() - return meshData, nil -} - -func handleQuery(data []byte, params url.Values, status int, msg string) (meshdata.Vector, int, []byte, error) { - meshData, err := decodeQueryData(data) - if err != nil { - return nil, http.StatusBadRequest, network.CreateResponse("INVALID_DATA", network.ResponseParams{"error": err.Error()}), nil - } - meshData.Status = status - return meshdata.Vector{meshData}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": meshData.Sites[0].ID}), nil -} - -// HandleRegisterSiteQuery registers a site. -func HandleRegisterSiteQuery(data []byte, params url.Values) (meshdata.Vector, int, []byte, error) { - return handleQuery(data, params, meshdata.StatusDefault, "SITE_REGISTERED") -} - -// HandleUnregisterSiteQuery unregisters a site. -func HandleUnregisterSiteQuery(data []byte, params url.Values) (meshdata.Vector, int, []byte, error) { - return handleQuery(data, params, meshdata.StatusObsolete, "SITE_UNREGISTERED") -} diff --git a/pkg/mentix/exchangers/reqexchanger.go b/pkg/mentix/exchangers/reqexchanger.go index c4c9e705af..7539b4cfa9 100644 --- a/pkg/mentix/exchangers/reqexchanger.go +++ b/pkg/mentix/exchangers/reqexchanger.go @@ -19,8 +19,15 @@ package exchangers import ( + "fmt" "net/http" + "net/url" "strings" + + "github.com/rs/zerolog" + + "github.com/cs3org/reva/pkg/mentix/config" + "github.com/cs3org/reva/pkg/mentix/meshdata" ) // RequestExchanger is the interface implemented by exchangers that offer an HTTP endpoint. @@ -32,15 +39,21 @@ type RequestExchanger interface { // WantsRequest returns whether the exchanger wants to handle the incoming request. WantsRequest(r *http.Request) bool // HandleRequest handles the actual HTTP request. - HandleRequest(resp http.ResponseWriter, req *http.Request) + HandleRequest(resp http.ResponseWriter, req *http.Request, conf *config.Configuration, log *zerolog.Logger) } +type queryCallback func(*meshdata.MeshData, url.Values, *config.Configuration, *zerolog.Logger) (int, []byte, error) +type extendedQueryCallback func(*meshdata.MeshData, []byte, url.Values, *config.Configuration, *zerolog.Logger) (meshdata.Vector, int, []byte, error) + // BaseRequestExchanger implements basic exporter functionality common to all request exporters. type BaseRequestExchanger struct { RequestExchanger endpoint string isProtectedEndpoint bool + + actionHandlers map[string]queryCallback + extendedActionHandlers map[string]extendedQueryCallback } // Endpoint returns the (relative) endpoint of the exchanger. @@ -70,6 +83,44 @@ func (exchanger *BaseRequestExchanger) WantsRequest(r *http.Request) bool { } // HandleRequest handles the actual HTTP request. -func (exchanger *BaseRequestExchanger) HandleRequest(resp http.ResponseWriter, req *http.Request) error { +func (exchanger *BaseRequestExchanger) HandleRequest(resp http.ResponseWriter, req *http.Request, conf *config.Configuration, log *zerolog.Logger) error { return nil } + +// RegisterActionHandler registers a new handler for the specified action. +func (exchanger *BaseRequestExchanger) RegisterActionHandler(action string, callback queryCallback) { + if exchanger.actionHandlers == nil { + exchanger.actionHandlers = make(map[string]queryCallback) + } + exchanger.actionHandlers[action] = callback +} + +// RegisterExtendedActionHandler registers a new handler for the specified extended action. +func (exchanger *BaseRequestExchanger) RegisterExtendedActionHandler(action string, callback extendedQueryCallback) { + if exchanger.extendedActionHandlers == nil { + exchanger.extendedActionHandlers = make(map[string]extendedQueryCallback) + } + exchanger.extendedActionHandlers[action] = callback +} + +// HandleAction executes the registered handler for the specified action, if any. +func (exchanger *BaseRequestExchanger) HandleAction(meshData *meshdata.MeshData, body []byte, params url.Values, isExtended bool, conf *config.Configuration, log *zerolog.Logger) (meshdata.Vector, int, []byte, error) { + reqAction := params.Get("action") + + if isExtended { + for action, handler := range exchanger.extendedActionHandlers { + if strings.EqualFold(action, reqAction) { + return handler(meshData, body, params, conf, log) + } + } + } else { + for action, handler := range exchanger.actionHandlers { + if strings.EqualFold(action, reqAction) { + status, data, err := handler(meshData, params, conf, log) + return nil, status, data, err + } + } + } + + return nil, http.StatusNotFound, []byte{}, fmt.Errorf("unhandled query for action '%v'", reqAction) +} diff --git a/pkg/mentix/key/apikey.go b/pkg/mentix/key/apikey.go new file mode 100644 index 0000000000..4157e08ff0 --- /dev/null +++ b/pkg/mentix/key/apikey.go @@ -0,0 +1,118 @@ +// 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 key + +import ( + "crypto/md5" + "crypto/rand" + "fmt" + hashpkg "hash" + "strconv" + + "github.com/pkg/errors" +) + +// APIKey is the type used to store API keys. +type APIKey = string + +const ( + // FlagDefault marks API keys for default (community) accounts. + FlagDefault = 0x0000 + // FlagScienceMesh marks API keys for ScienceMesh (partner) accounts. + FlagScienceMesh = 0x0001 +) + +const ( + randomStringLength = 30 + apiKeyLength = randomStringLength + 2 + 32 + + charset = "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" +) + +// GenerateAPIKey generates a new (random) API key which also contains flags and a (salted) hash. +// An API key has the following format: +// +func GenerateAPIKey(salt string, flags int) (APIKey, error) { + if len(salt) == 0 { + return "", errors.Errorf("no salt specified") + } + + randomString, err := generateRandomString(randomStringLength) + if err != nil { + return "", errors.Wrap(err, "unable to generate API key") + } + + // To verify an API key, a hash is used which contains, beside the random string and flags, the email address + hash := calculateHash(randomString, flags, salt) + return fmt.Sprintf("%s%02x%032x", randomString, flags, hash.Sum(nil)), nil +} + +// VerifyAPIKey checks if the API key is valid given the specified salt value. +func VerifyAPIKey(apiKey APIKey, salt string) error { + randomString, flags, hash, err := SplitAPIKey(apiKey) + if err != nil { + return errors.Wrap(err, "error while extracting API key information") + } + + hashCalc := calculateHash(randomString, flags, salt) + if fmt.Sprintf("%032x", hashCalc.Sum(nil)) != hash { + return errors.Errorf("the API key is invalid") + } + + return nil +} + +// SplitAPIKey splits an API key into its pieces: RandomString, Flags and Hash. +func SplitAPIKey(apiKey APIKey) (string, int, string, error) { + if len(apiKey) != apiKeyLength { + return "", 0, "", errors.Errorf("invalid API key length") + } + + randomString := apiKey[:randomStringLength] + flags, err := strconv.Atoi(apiKey[randomStringLength : randomStringLength+2]) + if err != nil { + return "", 0, "", errors.Errorf("invalid API key format") + } + hash := apiKey[randomStringLength+2:] + + return randomString, flags, hash, nil +} + +func calculateHash(randomString string, flags int, salt string) hashpkg.Hash { + hash := md5.New() + _, _ = hash.Write([]byte(randomString)) + _, _ = hash.Write([]byte(salt)) + _, _ = hash.Write([]byte(fmt.Sprintf("%04x", flags))) + return hash +} + +func generateRandomString(n int) (string, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return "", err + } + + str := "" + for _, v := range b { + str += string(charset[int(v)%len(charset)]) + } + + return str, nil +} diff --git a/pkg/mentix/key/siteid.go b/pkg/mentix/key/siteid.go new file mode 100644 index 0000000000..fda47f4426 --- /dev/null +++ b/pkg/mentix/key/siteid.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 key + +import ( + "fmt" + "hash/crc64" + + "github.com/pkg/errors" +) + +// SiteIdentifier is the type used to store site identifiers. +type SiteIdentifier = string + +// CalculateSiteID calculates a (stable) site ID from the given API key. +// The site ID is actually the CRC64 hash of the provided API key plus a salt value, thus it is stable for any given key & salt pair. +func CalculateSiteID(apiKey APIKey, salt string) (SiteIdentifier, error) { + if len(apiKey) != apiKeyLength { + return "", errors.Errorf("invalid API key length") + } + + hash := crc64.New(crc64.MakeTable(crc64.ECMA)) + _, _ = hash.Write([]byte(apiKey)) + _, _ = hash.Write([]byte(salt)) + value := hash.Sum(nil) + return fmt.Sprintf("%4x-%4x-%4x-%4x", value[:2], value[2:4], value[4:6], value[6:]), nil +} diff --git a/pkg/mentix/mentix.go b/pkg/mentix/mentix.go index 684cc10106..cb499ae870 100644 --- a/pkg/mentix/mentix.go +++ b/pkg/mentix/mentix.go @@ -27,6 +27,7 @@ import ( "github.com/rs/zerolog" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/mentix/accservice" "github.com/cs3org/reva/pkg/mentix/config" "github.com/cs3org/reva/pkg/mentix/connectors" "github.com/cs3org/reva/pkg/mentix/entity" @@ -267,9 +268,13 @@ func (mntx *Mentix) applyMeshDataSet(meshDataSet meshdata.Map) error { mntx.meshDataSet = meshDataSet - for _, exporter := range mntx.exporters.Exporters { - if err := exporter.Update(mntx.meshDataSet); err != nil { - return fmt.Errorf("unable to update mesh data on exporter '%v': %v", exporter.GetName(), err) + exchangers := make([]exchangers.Exchanger, 0, len(mntx.exporters.Exporters)+len(mntx.importers.Importers)) + exchangers = append(exchangers, mntx.exporters.Exchangers()...) + exchangers = append(exchangers, mntx.importers.Exchangers()...) + + for _, exchanger := range exchangers { + if err := exchanger.Update(mntx.meshDataSet); err != nil { + return fmt.Errorf("unable to update mesh data on exchanger '%v': %v", exchanger.GetName(), err) } } } @@ -310,13 +315,18 @@ func (mntx *Mentix) handleRequest(exchangers []exchangers.RequestExchanger, w ht // Ask each RequestExchanger if it wants to handle the request for _, exchanger := range exchangers { if exchanger.WantsRequest(r) { - exchanger.HandleRequest(w, r) + exchanger.HandleRequest(w, r, mntx.conf, log) } } } // New creates a new Mentix service instance. func New(conf *config.Configuration, log *zerolog.Logger) (*Mentix, error) { + // Configure the accounts service upfront + if err := accservice.InitAccountsService(conf); err != nil { + return nil, fmt.Errorf("unable to initialize the accounts service: %v", err) + } + mntx := new(Mentix) if err := mntx.initialize(conf, log); err != nil { return nil, fmt.Errorf("unable to initialize Mentix: %v", err) diff --git a/pkg/mentix/meshdata/meshdata.go b/pkg/mentix/meshdata/meshdata.go index 05289b72d1..7e6ec4e580 100644 --- a/pkg/mentix/meshdata/meshdata.go +++ b/pkg/mentix/meshdata/meshdata.go @@ -33,10 +33,6 @@ const ( // StatusObsolete flags the mesh data for removal. StatusObsolete - // StatusAuthorize flags the mesh data for authorization. - StatusAuthorize - // StatusUnauthorize flags the mesh data for unauthorization. - StatusUnauthorize ) // MeshData holds the entire mesh data managed by Mentix. @@ -65,16 +61,14 @@ func (meshData *MeshData) AddSite(site *Site) { } // RemoveSite removes the provided site. -func (meshData *MeshData) RemoveSite(id string) { - if site := meshData.FindSite(id); site != nil { - for idx, siteExisting := range meshData.Sites { - if siteExisting == site { - lastIdx := len(meshData.Sites) - 1 - meshData.Sites[idx] = meshData.Sites[lastIdx] - meshData.Sites[lastIdx] = nil - meshData.Sites = meshData.Sites[:lastIdx] - break - } +func (meshData *MeshData) RemoveSite(site *Site) { + for idx, siteExisting := range meshData.Sites { + if strings.EqualFold(siteExisting.ID, site.ID) { // Remove the site by its ID + lastIdx := len(meshData.Sites) - 1 + meshData.Sites[idx] = meshData.Sites[lastIdx] + meshData.Sites[lastIdx] = nil + meshData.Sites = meshData.Sites[:lastIdx] + break } } } @@ -99,16 +93,14 @@ func (meshData *MeshData) AddServiceType(serviceType *ServiceType) { } // RemoveServiceType removes the provided service type. -func (meshData *MeshData) RemoveServiceType(name string) { - if serviceType := meshData.FindServiceType(name); serviceType != nil { - for idx, svcTypeExisting := range meshData.ServiceTypes { - if svcTypeExisting == serviceType { - lastIdx := len(meshData.ServiceTypes) - 1 - meshData.ServiceTypes[idx] = meshData.ServiceTypes[lastIdx] - meshData.ServiceTypes[lastIdx] = nil - meshData.ServiceTypes = meshData.ServiceTypes[:lastIdx] - break - } +func (meshData *MeshData) RemoveServiceType(serviceType *ServiceType) { + for idx, svcTypeExisting := range meshData.ServiceTypes { + if strings.EqualFold(svcTypeExisting.Name, serviceType.Name) { // Remove the service type by its name + lastIdx := len(meshData.ServiceTypes) - 1 + meshData.ServiceTypes[idx] = meshData.ServiceTypes[lastIdx] + meshData.ServiceTypes[lastIdx] = nil + meshData.ServiceTypes = meshData.ServiceTypes[:lastIdx] + break } } } @@ -137,11 +129,11 @@ func (meshData *MeshData) Merge(inData *MeshData) { // Unmerge removes data from another MeshData instance from this one. func (meshData *MeshData) Unmerge(inData *MeshData) { for _, site := range inData.Sites { - meshData.RemoveSite(site.ID) + meshData.RemoveSite(site) } for _, serviceType := range inData.ServiceTypes { - meshData.RemoveServiceType(serviceType.Name) + meshData.RemoveServiceType(serviceType) } } diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go index 0374ec2e2f..0ca488c997 100644 --- a/pkg/mentix/meshdata/properties.go +++ b/pkg/mentix/meshdata/properties.go @@ -21,8 +21,8 @@ package meshdata import "strings" const ( - // PropertyAuthorized identifies the authorization status property. - PropertyAuthorized = "authorized" + // PropertySiteID identifies the site ID property. + PropertySiteID = "site_id" // PropertyOrganization identifies the organization property. PropertyOrganization = "organization" // PropertyMetricsPath identifies the metrics path property. diff --git a/pkg/mentix/meshdata/service.go b/pkg/mentix/meshdata/service.go index 30af06f408..c56428500b 100644 --- a/pkg/mentix/meshdata/service.go +++ b/pkg/mentix/meshdata/service.go @@ -22,7 +22,7 @@ import ( "fmt" "net/url" - "github.com/cs3org/reva/pkg/mentix/network" + "github.com/cs3org/reva/pkg/mentix/utils/network" ) // Service represents a service managed by Mentix. diff --git a/pkg/mentix/meshdata/site.go b/pkg/mentix/meshdata/site.go index ce41174ad5..e48ab6c363 100644 --- a/pkg/mentix/meshdata/site.go +++ b/pkg/mentix/meshdata/site.go @@ -23,12 +23,13 @@ import ( "net/url" "strings" - "github.com/cs3org/reva/pkg/mentix/network" + "github.com/cs3org/reva/pkg/mentix/accservice" + "github.com/cs3org/reva/pkg/mentix/utils/network" ) const ( // SiteTypeScienceMesh flags a site as being part of the mesh. - SiteTypeScienceMesh = iota + SiteTypeScienceMesh SiteType = iota // SiteTypeCommunity flags a site as being a community site. SiteTypeCommunity ) @@ -123,27 +124,29 @@ func (site *Site) InferMissingData() { } } - // Automatically assign an ID to this site if it is missing - if len(site.ID) == 0 { - site.generateID() - } - // Infer missing for services for _, service := range site.Services { service.InferMissingData() } } -// Name, Domain -func (site *Site) generateID() { - host := site.Domain - if site.Homepage != "" { - if hostURL, err := url.Parse(site.Homepage); err == nil { - host = network.ExtractDomainFromURL(hostURL, true) +// IsAuthorized checks whether the site is authorized. ScienceMesh are always authorized, while for community sites, +// the accounts service is queried. +func (site *Site) IsAuthorized() bool { + // ScienceMesh sites are always authorized + if site.Type == SiteTypeScienceMesh { + return true + } + + // Use the accounts service to find out whether the site is authorized + resp, err := accservice.Query("is-authorized", network.URLParams{"by": "siteid", "value": site.ID}) + if err == nil && resp.Success { + if authorized, ok := resp.Data.(bool); ok { + return authorized } } - site.ID = fmt.Sprintf("%s::[%s]", host, site.Name) + return false } // GetSiteTypeName returns the readable name of the given site type. diff --git a/pkg/mentix/utils/countries/countries.go b/pkg/mentix/utils/countries/countries.go new file mode 100644 index 0000000000..0fd7664ce6 --- /dev/null +++ b/pkg/mentix/utils/countries/countries.go @@ -0,0 +1,316 @@ +// 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 countries + +import ( + "strconv" + "strings" +) + +const countriesData = ` +Afghanistan AF AFG 004 +Albania AL ALB 008 +Algeria DZ DZA 012 +American Samoa AS ASM 016 +Andorra AD AND 020 +Angola AO AGO 024 +Anguilla AI AIA 660 +Antarctica AQ ATA 010 +Antigua and Barbuda AG ATG 028 +Argentina AR ARG 032 +Armenia AM ARM 051 +Aruba AW ABW 533 +Australia AU AUS 036 +Austria AT AUT 040 +Azerbaijan AZ AZE 031 +Bahamas (the) BS BHS 044 +Bahrain BH BHR 048 +Bangladesh BD BGD 050 +Barbados BB BRB 052 +Belarus BY BLR 112 +Belgium BE BEL 056 +Belize BZ BLZ 084 +Benin BJ BEN 204 +Bermuda BM BMU 060 +Bhutan BT BTN 064 +Bolivia (Plurinational State of) BO BOL 068 +Bonaire, Sint Eustatius and Saba BQ BES 535 +Bosnia and Herzegovina BA BIH 070 +Botswana BW BWA 072 +Bouvet Island BV BVT 074 +Brazil BR BRA 076 +British Indian Ocean Territory (the) IO IOT 086 +Brunei Darussalam BN BRN 096 +Bulgaria BG BGR 100 +Burkina Faso BF BFA 854 +Burundi BI BDI 108 +Cabo Verde CV CPV 132 +Cambodia KH KHM 116 +Cameroon CM CMR 120 +Canada CA CAN 124 +Cayman Islands (the) KY CYM 136 +Central African Republic (the) CF CAF 140 +Chad TD TCD 148 +Chile CL CHL 152 +China CN CHN 156 +Christmas Island CX CXR 162 +Cocos (Keeling) Islands (the) CC CCK 166 +Colombia CO COL 170 +Comoros (the) KM COM 174 +Congo (the Democratic Republic of the) CD COD 180 +Congo (the) CG COG 178 +Cook Islands (the) CK COK 184 +Costa Rica CR CRI 188 +Croatia HR HRV 191 +Cuba CU CUB 192 +Curaçao CW CUW 531 +Cyprus CY CYP 196 +Czechia CZ CZE 203 +Côte d'Ivoire CI CIV 384 +Denmark DK DNK 208 +Djibouti DJ DJI 262 +Dominica DM DMA 212 +Dominican Republic (the) DO DOM 214 +Ecuador EC ECU 218 +Egypt EG EGY 818 +El Salvador SV SLV 222 +Equatorial Guinea GQ GNQ 226 +Eritrea ER ERI 232 +Estonia EE EST 233 +Eswatini SZ SWZ 748 +Ethiopia ET ETH 231 +Falkland Islands (the) [Malvinas] FK FLK 238 +Faroe Islands (the) FO FRO 234 +Fiji FJ FJI 242 +Finland FI FIN 246 +France FR FRA 250 +French Guiana GF GUF 254 +French Polynesia PF PYF 258 +French Southern Territories (the) TF ATF 260 +Gabon GA GAB 266 +Gambia (the) GM GMB 270 +Georgia GE GEO 268 +Germany DE DEU 276 +Ghana GH GHA 288 +Gibraltar GI GIB 292 +Greece GR GRC 300 +Greenland GL GRL 304 +Grenada GD GRD 308 +Guadeloupe GP GLP 312 +Guam GU GUM 316 +Guatemala GT GTM 320 +Guernsey GG GGY 831 +Guinea GN GIN 324 +Guinea-Bissau GW GNB 624 +Guyana GY GUY 328 +Haiti HT HTI 332 +Heard Island and McDonald Islands HM HMD 334 +Holy See (the) VA VAT 336 +Honduras HN HND 340 +Hong Kong HK HKG 344 +Hungary HU HUN 348 +Iceland IS ISL 352 +India IN IND 356 +Indonesia ID IDN 360 +Iran (Islamic Republic of) IR IRN 364 +Iraq IQ IRQ 368 +Ireland IE IRL 372 +Isle of Man IM IMN 833 +Israel IL ISR 376 +Italy IT ITA 380 +Jamaica JM JAM 388 +Japan JP JPN 392 +Jersey JE JEY 832 +Jordan JO JOR 400 +Kazakhstan KZ KAZ 398 +Kenya KE KEN 404 +Kiribati KI KIR 296 +Korea (the Democratic People's Republic of) KP PRK 408 +Korea (the Republic of) KR KOR 410 +Kuwait KW KWT 414 +Kyrgyzstan KG KGZ 417 +Lao People's Democratic Republic (the) LA LAO 418 +Latvia LV LVA 428 +Lebanon LB LBN 422 +Lesotho LS LSO 426 +Liberia LR LBR 430 +Libya LY LBY 434 +Liechtenstein LI LIE 438 +Lithuania LT LTU 440 +Luxembourg LU LUX 442 +Macao MO MAC 446 +Madagascar MG MDG 450 +Malawi MW MWI 454 +Malaysia MY MYS 458 +Maldives MV MDV 462 +Mali ML MLI 466 +Malta MT MLT 470 +Marshall Islands (the) MH MHL 584 +Martinique MQ MTQ 474 +Mauritania MR MRT 478 +Mauritius MU MUS 480 +Mayotte YT MYT 175 +Mexico MX MEX 484 +Micronesia (Federated States of) FM FSM 583 +Moldova (the Republic of) MD MDA 498 +Monaco MC MCO 492 +Mongolia MN MNG 496 +Montenegro ME MNE 499 +Montserrat MS MSR 500 +Morocco MA MAR 504 +Mozambique MZ MOZ 508 +Myanmar MM MMR 104 +Namibia NA NAM 516 +Nauru NR NRU 520 +Nepal NP NPL 524 +Netherlands (the) NL NLD 528 +New Caledonia NC NCL 540 +New Zealand NZ NZL 554 +Nicaragua NI NIC 558 +Niger (the) NE NER 562 +Nigeria NG NGA 566 +Niue NU NIU 570 +Norfolk Island NF NFK 574 +Northern Mariana Islands (the) MP MNP 580 +Norway NO NOR 578 +Oman OM OMN 512 +Pakistan PK PAK 586 +Palau PW PLW 585 +Palestine, State of PS PSE 275 +Panama PA PAN 591 +Papua New Guinea PG PNG 598 +Paraguay PY PRY 600 +Peru PE PER 604 +Philippines (the) PH PHL 608 +Pitcairn PN PCN 612 +Poland PL POL 616 +Portugal PT PRT 620 +Puerto Rico PR PRI 630 +Qatar QA QAT 634 +Republic of North Macedonia MK MKD 807 +Romania RO ROU 642 +Russian Federation (the) RU RUS 643 +Rwanda RW RWA 646 +Réunion RE REU 638 +Saint Barthélemy BL BLM 652 +Saint Helena, Ascension and Tristan da Cunha SH SHN 654 +Saint Kitts and Nevis KN KNA 659 +Saint Lucia LC LCA 662 +Saint Martin (French part) MF MAF 663 +Saint Pierre and Miquelon PM SPM 666 +Saint Vincent and the Grenadines VC VCT 670 +Samoa WS WSM 882 +San Marino SM SMR 674 +Sao Tome and Principe ST STP 678 +Saudi Arabia SA SAU 682 +Senegal SN SEN 686 +Serbia RS SRB 688 +Seychelles SC SYC 690 +Sierra Leone SL SLE 694 +Singapore SG SGP 702 +Sint Maarten (Dutch part) SX SXM 534 +Slovakia SK SVK 703 +Slovenia SI SVN 705 +Solomon Islands SB SLB 090 +Somalia SO SOM 706 +South Africa ZA ZAF 710 +South Georgia and the South Sandwich Islands GS SGS 239 +South Sudan SS SSD 728 +Spain ES ESP 724 +Sri Lanka LK LKA 144 +Sudan (the) SD SDN 729 +Suriname SR SUR 740 +Svalbard and Jan Mayen SJ SJM 744 +Sweden SE SWE 752 +Switzerland CH CHE 756 +Syrian Arab Republic SY SYR 760 +Taiwan (Province of China) TW TWN 158 +Tajikistan TJ TJK 762 +Tanzania, United Republic of TZ TZA 834 +Thailand TH THA 764 +Timor-Leste TL TLS 626 +Togo TG TGO 768 +Tokelau TK TKL 772 +Tonga TO TON 776 +Trinidad and Tobago TT TTO 780 +Tunisia TN TUN 788 +Turkey TR TUR 792 +Turkmenistan TM TKM 795 +Turks and Caicos Islands (the) TC TCA 796 +Tuvalu TV TUV 798 +Uganda UG UGA 800 +Ukraine UA UKR 804 +United Arab Emirates (the) AE ARE 784 +United Kingdom of Great Britain and Northern Ireland (the) GB GBR 826 +United States Minor Outlying Islands (the) UM UMI 581 +United States of America (the) US USA 840 +Uruguay UY URY 858 +Uzbekistan UZ UZB 860 +Vanuatu VU VUT 548 +Venezuela (Bolivarian Republic of) VE VEN 862 +Viet Nam VN VNM 704 +Virgin Islands (British) VG VGB 092 +Virgin Islands (U.S.) VI VIR 850 +Wallis and Futuna WF WLF 876 +Western Sahara EH ESH 732 +Yemen YE YEM 887 +Zambia ZM ZMB 894 +Zimbabwe ZW ZWE 716 +Åland Islands AX ALA 248 +` + +type countryCode struct { + Alpha2 string + Alpha3 string + Numerical int +} + +var countryCodeTable map[string]countryCode + +// LookupCountry searches for the country specified by the given code (Alpha2/3 or numerical). +// If no country with the code exists, an empty string is returned. +func LookupCountry(code string) string { + numerical, err := strconv.Atoi(code) + if err != nil { + numerical = -1 + } + + for name, cc := range countryCodeTable { + if strings.EqualFold(code, cc.Alpha2) || strings.EqualFold(code, cc.Alpha3) || cc.Numerical == numerical { + return name + } + } + return "" +} + +func init() { + countryCodeTable = make(map[string]countryCode, 250) + for _, countryData := range strings.Split(countriesData, "\n") { + tokens := strings.Split(countryData, "\t") + if len(tokens) == 4 { + numerical, _ := strconv.Atoi(tokens[3]) + + countryCodeTable[tokens[0]] = countryCode{ + Alpha2: tokens[1], + Alpha3: tokens[2], + Numerical: numerical, + } + } + } +} diff --git a/pkg/mentix/network/utils.go b/pkg/mentix/utils/network/network.go similarity index 82% rename from pkg/mentix/network/utils.go rename to pkg/mentix/utils/network/network.go index c2c9ac7b2c..da6fa65bec 100644 --- a/pkg/mentix/network/utils.go +++ b/pkg/mentix/utils/network/network.go @@ -35,6 +35,12 @@ type URLParams map[string]string // ResponseParams holds parameters of an HTTP response. type ResponseParams map[string]interface{} +// BasicAuth holds user credentials for basic HTTP authentication. +type BasicAuth struct { + User string + Password string +} + // GenerateURL creates a URL object from a host, path and optional parameters. func GenerateURL(host string, path string, params URLParams) (*url.URL, error) { fullURL, err := url.Parse(host) @@ -42,6 +48,10 @@ func GenerateURL(host string, path string, params URLParams) (*url.URL, error) { return nil, fmt.Errorf("unable to generate URL: base=%v, path=%v, params=%v", host, path, params) } + if len(fullURL.Scheme) == 0 { + fullURL.Scheme = "https" + } + fullURL.Path = p.Join(fullURL.Path, path) query := make(url.Values) @@ -54,18 +64,23 @@ func GenerateURL(host string, path string, params URLParams) (*url.URL, error) { } // ReadEndpoint reads data from an HTTP endpoint. -func ReadEndpoint(host string, path string, params URLParams) ([]byte, error) { - endpointURL, err := GenerateURL(host, path, params) +func ReadEndpoint(endpointURL *url.URL, auth *BasicAuth, checkStatus bool) ([]byte, error) { + // Prepare the request + req, err := http.NewRequest("GET", endpointURL.String(), nil) if err != nil { - return nil, fmt.Errorf("unable to generate endpoint URL: %v", err) + return nil, fmt.Errorf("unable to create HTTP request: %v", err) + } + + if auth != nil { + req.SetBasicAuth(auth.User, auth.Password) } // Fetch the data and read the body - resp, err := http.Get(endpointURL.String()) + resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("unable to get data from endpoint: %v", err) } - if resp.StatusCode >= 400 { + if checkStatus && resp.StatusCode >= 400 { return nil, fmt.Errorf("invalid response received: %v", resp.Status) } diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index a321559039..e3bd4ab24c 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -37,6 +37,7 @@ import ( var ( matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + matchEmail = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") ) // Skip evaluates whether a source endpoint contains any of the prefixes. @@ -157,3 +158,11 @@ func GranteeEqual(u, v *provider.Grantee) bool { vu, vg := ExtractGranteeID(v) return u.Type == v.Type && (UserEqual(uu, vu) || GroupEqual(ug, vg)) } + +// IsEmailValid checks whether the provided email has a valid format. +func IsEmailValid(e string) bool { + if len(e) < 3 && len(e) > 254 { + return false + } + return matchEmail.MatchString(e) +}