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}}
+
+
+
+
+
+
+ {{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)
+}