From 3f08891ec852113d41a0537f65913d6dbdc174aa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 30 Nov 2020 13:49:32 +0100
Subject: [PATCH 01/59] Add a site property to explicitly enable health checks
---
pkg/mentix/exchangers/exporters/promsd.go | 6 ++++++
pkg/mentix/meshdata/properties.go | 2 ++
2 files changed, 8 insertions(+)
diff --git a/pkg/mentix/exchangers/exporters/promsd.go b/pkg/mentix/exchangers/exporters/promsd.go
index 66d86d269a..67065697e2 100755
--- a/pkg/mentix/exchangers/exporters/promsd.go
+++ b/pkg/mentix/exchangers/exporters/promsd.go
@@ -24,6 +24,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
+ "strings"
"github.com/rs/zerolog"
@@ -68,6 +69,11 @@ func createBlackboxSDScrapeConfig(site *meshdata.Site, host string, endpoint *me
labels := getScrapeTargetLabels(site, endpoint)
+ // Check if health checks are enabled for the endpoint; if they aren't, skip this endpoint
+ if enableHealthChecks := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyEnableHealthChecks, "false"); !strings.EqualFold(enableHealthChecks, "true") {
+ return nil
+ }
+
return &prometheus.ScrapeConfig{
Targets: []string{target},
Labels: labels,
diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go
index 7dfb8f7a57..479a4631c6 100644
--- a/pkg/mentix/meshdata/properties.go
+++ b/pkg/mentix/meshdata/properties.go
@@ -25,6 +25,8 @@ const (
PropertyOrganization = "organization"
// PropertyMetricsPath identifies the metrics path property.
PropertyMetricsPath = "metrics_path"
+ // PropertyEnableHealthChecks identifies the enable health checks property.
+ PropertyEnableHealthChecks = "enable_health_checks"
// PropertyAPIVersion identifies the API version property.
PropertyAPIVersion = "api_version"
)
From ed873af3998f5764c78c530154a6cc0c21212bda Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 30 Nov 2020 13:57:39 +0100
Subject: [PATCH 02/59] Add changelog
---
changelog/unreleased/mentix-checks.md | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 changelog/unreleased/mentix-checks.md
diff --git a/changelog/unreleased/mentix-checks.md b/changelog/unreleased/mentix-checks.md
new file mode 100644
index 0000000000..928533b283
--- /dev/null
+++ b/changelog/unreleased/mentix-checks.md
@@ -0,0 +1,5 @@
+Enhancement: Support property to enable health checking on a service
+
+This update introduces a new service property called `ENABLE_HEALTH_CHECKS` that must be explicitly set to `true` if a service should be checked for its health status. This allows us to only enable these checks for partner sites only, skipping vendor sites.
+
+https://github.com/cs3org/reva/pull/1347
From 1ffc3773de3a2239eab81028e630db67f6c3967d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Tue, 1 Dec 2020 14:11:44 +0100
Subject: [PATCH 03/59] Add some more labels to Prometheus targets
---
pkg/mentix/exchangers/exporters/promsd.go | 16 ++++++++++++----
pkg/mentix/meshdata/properties.go | 2 ++
2 files changed, 14 insertions(+), 4 deletions(-)
diff --git a/pkg/mentix/exchangers/exporters/promsd.go b/pkg/mentix/exchangers/exporters/promsd.go
index 67065697e2..2c10c15a35 100755
--- a/pkg/mentix/exchangers/exporters/promsd.go
+++ b/pkg/mentix/exchangers/exporters/promsd.go
@@ -47,7 +47,7 @@ type PrometheusSDExporter struct {
}
func createMetricsSDScrapeConfig(site *meshdata.Site, host string, endpoint *meshdata.ServiceEndpoint) *prometheus.ScrapeConfig {
- labels := getScrapeTargetLabels(site, endpoint)
+ labels := getScrapeTargetLabels(site, host, endpoint)
// If a metrics path was specified as a property, use that one by setting the corresponding label
if metricsPath := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyMetricsPath, ""); len(metricsPath) > 0 {
@@ -67,7 +67,7 @@ func createBlackboxSDScrapeConfig(site *meshdata.Site, host string, endpoint *me
return nil
}
- labels := getScrapeTargetLabels(site, endpoint)
+ labels := getScrapeTargetLabels(site, host, endpoint)
// Check if health checks are enabled for the endpoint; if they aren't, skip this endpoint
if enableHealthChecks := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyEnableHealthChecks, "false"); !strings.EqualFold(enableHealthChecks, "true") {
@@ -80,14 +80,22 @@ func createBlackboxSDScrapeConfig(site *meshdata.Site, host string, endpoint *me
}
}
-func getScrapeTargetLabels(site *meshdata.Site, endpoint *meshdata.ServiceEndpoint) map[string]string {
- return map[string]string{
+func getScrapeTargetLabels(site *meshdata.Site, host string, endpoint *meshdata.ServiceEndpoint) map[string]string {
+ labels := map[string]string{
"__meta_mentix_site": site.Name,
"__meta_mentix_site_type": meshdata.GetSiteTypeName(site.Type),
"__meta_mentix_site_id": site.GetID(),
+ "__meta_mentix_host": host,
"__meta_mentix_country": site.CountryCode,
"__meta_mentix_service_type": endpoint.Type.Name,
}
+
+ // Get the gRPC port if the corresponding property has been set
+ if port := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyGRPCPort, ""); len(port) > 0 {
+ labels["__meta_mentix_grpc_port"] = port
+ }
+
+ return labels
}
func (exporter *PrometheusSDExporter) registerScrapeCreators(conf *config.Configuration) error {
diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go
index 479a4631c6..b4962b4b0e 100644
--- a/pkg/mentix/meshdata/properties.go
+++ b/pkg/mentix/meshdata/properties.go
@@ -25,6 +25,8 @@ const (
PropertyOrganization = "organization"
// PropertyMetricsPath identifies the metrics path property.
PropertyMetricsPath = "metrics_path"
+ // PropertyGRPCPort identifies the gRPC port property.
+ PropertyGRPCPort = "grpc_port"
// PropertyEnableHealthChecks identifies the enable health checks property.
PropertyEnableHealthChecks = "enable_health_checks"
// PropertyAPIVersion identifies the API version property.
From 83ba2ca59ea3d4ff8266678f70fcd73916576398 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Tue, 1 Dec 2020 15:46:26 +0100
Subject: [PATCH 04/59] Skip endpoints for BBE if no gRPC port is set
---
pkg/mentix/exchangers/exporters/promsd.go | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/pkg/mentix/exchangers/exporters/promsd.go b/pkg/mentix/exchangers/exporters/promsd.go
index 2c10c15a35..6a30cb763d 100755
--- a/pkg/mentix/exchangers/exporters/promsd.go
+++ b/pkg/mentix/exchangers/exporters/promsd.go
@@ -67,13 +67,18 @@ func createBlackboxSDScrapeConfig(site *meshdata.Site, host string, endpoint *me
return nil
}
- labels := getScrapeTargetLabels(site, host, endpoint)
-
// Check if health checks are enabled for the endpoint; if they aren't, skip this endpoint
if enableHealthChecks := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyEnableHealthChecks, "false"); !strings.EqualFold(enableHealthChecks, "true") {
return nil
}
+ labels := getScrapeTargetLabels(site, host, endpoint)
+
+ // For health checks, the gRPC port must be set
+ if _, ok := labels["__meta_mentix_grpc_port"]; !ok {
+ return nil
+ }
+
return &prometheus.ScrapeConfig{
Targets: []string{target},
Labels: labels,
From d67790f82a9699ac911915a9aaef3fadef430182 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 4 Jan 2021 16:05:07 +0100
Subject: [PATCH 05/59] Minor TUS adjustments
---
pkg/sdk/common/net/tus.go | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/pkg/sdk/common/net/tus.go b/pkg/sdk/common/net/tus.go
index 09d96e4892..409a43b77e 100644
--- a/pkg/sdk/common/net/tus.go
+++ b/pkg/sdk/common/net/tus.go
@@ -52,13 +52,8 @@ func (client *TUSClient) initClient(endpoint string, accessToken string, transpo
}
client.config.Store = memStore
- if accessToken != "" {
- client.config.Header.Add(AccessTokenName, accessToken)
- }
-
- if transportToken != "" {
- client.config.Header.Add(TransportTokenName, transportToken)
- }
+ client.config.Header.Add(AccessTokenName, accessToken)
+ client.config.Header.Add(TransportTokenName, transportToken)
// Create the TUS client
tusClient, err := tus.NewClient(endpoint, client.config)
From bf0b0388ea04694bd428d4cb329a6e1de5dc80ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Thu, 14 Jan 2021 13:19:55 +0100
Subject: [PATCH 06/59] Set scheme for Prometheus targets
---
changelog/unreleased/mentix-prom-scheme.md | 5 +++++
pkg/mentix/exchangers/exporters/promsd.go | 7 +++++++
2 files changed, 12 insertions(+)
create mode 100644 changelog/unreleased/mentix-prom-scheme.md
diff --git a/changelog/unreleased/mentix-prom-scheme.md b/changelog/unreleased/mentix-prom-scheme.md
new file mode 100644
index 0000000000..8c33586ca0
--- /dev/null
+++ b/changelog/unreleased/mentix-prom-scheme.md
@@ -0,0 +1,5 @@
+Enhancement: Set scheme for Prometheus targets in Mentix
+
+This enhancement lets Mentix set the scheme for Prometheus targets. This allows us to also support monitoring of sites that do not support the default HTTPS scheme.
+
+https://github.com/cs3org/reva/pull/1398
diff --git a/pkg/mentix/exchangers/exporters/promsd.go b/pkg/mentix/exchangers/exporters/promsd.go
index 6a30cb763d..e8b4746ea2 100755
--- a/pkg/mentix/exchangers/exporters/promsd.go
+++ b/pkg/mentix/exchangers/exporters/promsd.go
@@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"io/ioutil"
+ "net/url"
"os"
"path/filepath"
"strings"
@@ -49,6 +50,12 @@ type PrometheusSDExporter struct {
func createMetricsSDScrapeConfig(site *meshdata.Site, host string, endpoint *meshdata.ServiceEndpoint) *prometheus.ScrapeConfig {
labels := getScrapeTargetLabels(site, host, endpoint)
+ // Support both HTTP and HTTPS endpoints by setting the scheme label accordingly
+ if len(endpoint.URL) > 0 {
+ if url, err := url.Parse(endpoint.URL); err == nil && (url.Scheme == "http" || url.Scheme == "https") {
+ labels["__scheme__"] = url.Scheme
+ }
+ }
// If a metrics path was specified as a property, use that one by setting the corresponding label
if metricsPath := meshdata.GetPropertyValue(endpoint.Properties, meshdata.PropertyMetricsPath, ""); len(metricsPath) > 0 {
labels["__metrics_path__"] = metricsPath
From 5fe83509e0483986c46428a33e93cab277602dfc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 13:50:33 +0100
Subject: [PATCH 07/59] Add site authorization status support
---
pkg/mentix/connectors/gocdb.go | 3 +++
pkg/mentix/exchangers/exporters/exporter.go | 19 +++++++++++++++++++
.../exchangers/importers/webapi/query.go | 7 +++++++
pkg/mentix/meshdata/properties.go | 2 ++
4 files changed, 31 insertions(+)
diff --git a/pkg/mentix/connectors/gocdb.go b/pkg/mentix/connectors/gocdb.go
index 9ac1a4399c..4fef500127 100755
--- a/pkg/mentix/connectors/gocdb.go
+++ b/pkg/mentix/connectors/gocdb.go
@@ -127,6 +127,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
+ properties[strings.ToUpper(meshdata.PropertyAuthorized)] = "true"
+
// See if an organization has been defined using properties; otherwise, use the official name
organization := meshdata.GetPropertyValue(properties, meshdata.PropertyOrganization, site.OfficialName)
diff --git a/pkg/mentix/exchangers/exporters/exporter.go b/pkg/mentix/exchangers/exporters/exporter.go
index a52306d0de..6d76b36f64 100755
--- a/pkg/mentix/exchangers/exporters/exporter.go
+++ b/pkg/mentix/exchangers/exporters/exporter.go
@@ -20,6 +20,7 @@ package exporters
import (
"fmt"
+ "strings"
"github.com/cs3org/reva/pkg/mentix/exchangers"
"github.com/cs3org/reva/pkg/mentix/meshdata"
@@ -41,6 +42,8 @@ type BaseExporter struct {
exchangers.BaseExchanger
meshData *meshdata.MeshData
+
+ allowUnauthorizedSites bool
}
// Update is called whenever the mesh data set has changed to reflect these changes.
@@ -65,6 +68,11 @@ func (exporter *BaseExporter) storeMeshDataSet(meshDataSet meshdata.Map) error {
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))
@@ -84,3 +92,14 @@ func (exporter *BaseExporter) SetMeshData(meshData *meshdata.MeshData) {
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/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go
index bc99011a80..f4743b2abd 100755
--- a/pkg/mentix/exchangers/importers/webapi/query.go
+++ b/pkg/mentix/exchangers/importers/webapi/query.go
@@ -23,6 +23,7 @@ import (
"fmt"
"net/http"
"net/url"
+ "strings"
"github.com/cs3org/reva/pkg/mentix/meshdata"
"github.com/cs3org/reva/pkg/mentix/network"
@@ -38,6 +39,12 @@ func decodeQueryData(data []byte) (*meshdata.MeshData, error) {
if err := meshData.Verify(); err != nil {
return nil, fmt.Errorf("verifying the imported mesh data failed: %v", err)
}
+
+ // Set sites imported through the WebAPI to 'unauthorized' by default
+ for _, site := range meshData.Sites {
+ site.Properties[strings.ToUpper(meshdata.PropertyAuthorized)] = "false"
+ }
+
meshData.InferMissingData()
return meshData, nil
}
diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go
index b4962b4b0e..6675b2f997 100644
--- a/pkg/mentix/meshdata/properties.go
+++ b/pkg/mentix/meshdata/properties.go
@@ -21,6 +21,8 @@ package meshdata
import "strings"
const (
+ // ProperyAuthorized identifies the authorization status property.
+ PropertyAuthorized = "authorized"
// PropertyOrganization identifies the organization property.
PropertyOrganization = "organization"
// PropertyMetricsPath identifies the metrics path property.
From 7ec1186823781cbe3f0d93e20698b9009b44aa4d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 14:22:14 +0100
Subject: [PATCH 08/59] Persist authorization status
---
pkg/mentix/connectors/gocdb.go | 6 ++++--
pkg/mentix/connectors/localfile.go | 13 +++++++++++++
pkg/mentix/exchangers/importers/reqimporter.go | 4 +++-
pkg/mentix/exchangers/importers/webapi/query.go | 9 +++------
pkg/mentix/meshdata/properties.go | 7 ++++++-
5 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/pkg/mentix/connectors/gocdb.go b/pkg/mentix/connectors/gocdb.go
index 4fef500127..425af0525e 100755
--- a/pkg/mentix/connectors/gocdb.go
+++ b/pkg/mentix/connectors/gocdb.go
@@ -127,8 +127,10 @@ 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
- properties[strings.ToUpper(meshdata.PropertyAuthorized)] = "true"
+ // 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")
+ }
// See if an organization has been defined using properties; otherwise, use the official name
organization := meshdata.GetPropertyValue(properties, meshdata.PropertyOrganization, site.OfficialName)
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index 15918375e3..0a8de0b524 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -93,8 +93,21 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa
// Remove data by unmerging
meshData.Unmerge(updatedData)
} else {
+ // Store the previous authorization status for already existing sites
+ siteAuthorizationStatus := make(map[string]string)
+ for _, site := range meshData.Sites {
+ siteAuthorizationStatus[site.GetID()] = 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)
+ }
+ }
}
// Write the updated sites back to the file
diff --git a/pkg/mentix/exchangers/importers/reqimporter.go b/pkg/mentix/exchangers/importers/reqimporter.go
index 1f4d4767bf..f8c49bcc66 100644
--- a/pkg/mentix/exchangers/importers/reqimporter.go
+++ b/pkg/mentix/exchangers/importers/reqimporter.go
@@ -50,7 +50,9 @@ func (importer *BaseRequestImporter) HandleRequest(resp http.ResponseWriter, req
body, _ := ioutil.ReadAll(req.Body)
meshData, status, respData, err := importer.handleQuery(body, req.URL.Query())
if err == nil {
- importer.mergeImportedMeshData(meshData)
+ if len(meshData) > 0 {
+ importer.mergeImportedMeshData(meshData)
+ }
} else {
respData = []byte(err.Error())
}
diff --git a/pkg/mentix/exchangers/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go
index f4743b2abd..b015b141d8 100755
--- a/pkg/mentix/exchangers/importers/webapi/query.go
+++ b/pkg/mentix/exchangers/importers/webapi/query.go
@@ -23,7 +23,6 @@ import (
"fmt"
"net/http"
"net/url"
- "strings"
"github.com/cs3org/reva/pkg/mentix/meshdata"
"github.com/cs3org/reva/pkg/mentix/network"
@@ -35,16 +34,14 @@ func decodeQueryData(data []byte) (*meshdata.MeshData, error) {
return nil, err
}
+ // 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)
}
- // Set sites imported through the WebAPI to 'unauthorized' by default
- for _, site := range meshData.Sites {
- site.Properties[strings.ToUpper(meshdata.PropertyAuthorized)] = "false"
- }
-
meshData.InferMissingData()
return meshData, nil
}
diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go
index 6675b2f997..7339d1796c 100644
--- a/pkg/mentix/meshdata/properties.go
+++ b/pkg/mentix/meshdata/properties.go
@@ -21,7 +21,7 @@ package meshdata
import "strings"
const (
- // ProperyAuthorized identifies the authorization status property.
+ // PropertyAuthorized identifies the authorization status property.
PropertyAuthorized = "authorized"
// PropertyOrganization identifies the organization property.
PropertyOrganization = "organization"
@@ -45,3 +45,8 @@ func GetPropertyValue(props map[string]string, id string, defValue string) strin
return defValue
}
+
+// SetPropertyValue sets a property value.
+func SetPropertyValue(props map[string]string, id string, value string) {
+ props[strings.ToUpper(id)] = value
+}
From 46b2d5335f07f157d533e887bd14b6c8edf660ab Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 16:48:53 +0100
Subject: [PATCH 09/59] Admin API for site authorization
---
internal/http/services/mentix/mentix.go | 28 +++++--
pkg/mentix/config/config.go | 10 +++
pkg/mentix/config/ids.go | 2 +
pkg/mentix/connectors/localfile.go | 70 +++++++++++++-----
pkg/mentix/exchangers/exporters/cs3api.go | 2 +-
.../exchangers/exporters/sitelocations.go | 2 +-
pkg/mentix/exchangers/exporters/webapi.go | 2 +-
pkg/mentix/exchangers/importers/adminapi.go | 60 +++++++++++++++
.../exchangers/importers/adminapi/query.go | 73 +++++++++++++++++++
.../exchangers/importers/reqimporter.go | 11 ++-
pkg/mentix/exchangers/importers/webapi.go | 16 ++--
.../exchangers/importers/webapi/query.go | 8 +-
pkg/mentix/exchangers/reqexchanger.go | 15 +++-
pkg/mentix/meshdata/meshdata.go | 18 +++--
14 files changed, 265 insertions(+), 52 deletions(-)
create mode 100755 pkg/mentix/exchangers/importers/adminapi.go
create mode 100755 pkg/mentix/exchangers/importers/adminapi/query.go
diff --git a/internal/http/services/mentix/mentix.go b/internal/http/services/mentix/mentix.go
index 51d8323ef5..4eed621baa 100644
--- a/internal/http/services/mentix/mentix.go
+++ b/internal/http/services/mentix/mentix.go
@@ -27,6 +27,7 @@ import (
"github.com/cs3org/reva/pkg/mentix"
"github.com/cs3org/reva/pkg/mentix/config"
+ "github.com/cs3org/reva/pkg/mentix/exchangers"
"github.com/cs3org/reva/pkg/rhttp/global"
)
@@ -63,13 +64,20 @@ func (s *svc) Unprotected() []string {
importers := s.mntx.GetRequestImporters()
exporters := s.mntx.GetRequestExporters()
- endpoints := make([]string, len(importers)+len(exporters))
- for idx, importer := range importers {
- endpoints[idx] = importer.Endpoint()
- }
- for idx, exporter := range exporters {
- endpoints[idx] = exporter.Endpoint()
+ getEndpoints := func(exchangers []exchangers.RequestExchanger) []string {
+ endpoints := make([]string, 0, len(exchangers))
+ for _, exchanger := range exchangers {
+ if !exchanger.IsProtectedEndpoint() {
+ endpoints = append(endpoints, exchanger.Endpoint())
+ }
+ }
+ return endpoints
}
+
+ endpoints := make([]string, 0, len(importers)+len(exporters))
+ endpoints = append(endpoints, getEndpoints(importers)...)
+ endpoints = append(endpoints, getEndpoints(exporters)...)
+
return endpoints
}
@@ -130,7 +138,11 @@ func applyDefaultConfig(conf *config.Configuration) {
// Importers
if conf.Importers.WebAPI.Endpoint == "" {
- conf.Importers.WebAPI.Endpoint = "/"
+ conf.Importers.WebAPI.Endpoint = "/sites"
+ }
+
+ if conf.Importers.AdminAPI.Endpoint == "" {
+ conf.Importers.AdminAPI.Endpoint = "/admin"
}
// Exporters
@@ -141,7 +153,7 @@ func applyDefaultConfig(conf *config.Configuration) {
}
if conf.Exporters.WebAPI.Endpoint == "" {
- conf.Exporters.WebAPI.Endpoint = "/"
+ conf.Exporters.WebAPI.Endpoint = "/sites"
}
addDefaultConnector(&conf.Exporters.WebAPI.EnabledConnectors)
diff --git a/pkg/mentix/config/config.go b/pkg/mentix/config/config.go
index f43c4e6dfd..7fb38e7ea1 100644
--- a/pkg/mentix/config/config.go
+++ b/pkg/mentix/config/config.go
@@ -38,23 +38,33 @@ type Configuration struct {
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"`
} `mapstructure:"importers"`
Exporters struct {
WebAPI struct {
Endpoint string `mapstructure:"endpoint"`
+ IsProtected bool `mapstructure:"is_protected"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
} `mapstructure:"webapi"`
CS3API struct {
Endpoint string `mapstructure:"endpoint"`
+ IsProtected bool `mapstructure:"is_protected"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
} `mapstructure:"cs3api"`
SiteLocations struct {
Endpoint string `mapstructure:"endpoint"`
+ IsProtected bool `mapstructure:"is_protected"`
EnabledConnectors []string `mapstructure:"enabled_connectors"`
} `mapstructure:"siteloc"`
diff --git a/pkg/mentix/config/ids.go b/pkg/mentix/config/ids.go
index 979c60885f..a49c242209 100644
--- a/pkg/mentix/config/ids.go
+++ b/pkg/mentix/config/ids.go
@@ -28,6 +28,8 @@ const (
const (
// ImporterIDWebAPI is the identifier for the WebAPI importer.
ImporterIDWebAPI = "webapi"
+ // ImporterIDAdminAPI is the identifier for the AdminAPI importer.
+ ImporterIDAdminAPI = "adminapi"
)
const (
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index 0a8de0b524..c6d5b39b54 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -89,25 +89,19 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa
meshData = &meshdata.MeshData{}
}
- if (updatedData.Flags & meshdata.FlagObsolete) == meshdata.FlagObsolete {
- // Remove data by unmerging
- meshData.Unmerge(updatedData)
- } else {
- // Store the previous authorization status for already existing sites
- siteAuthorizationStatus := make(map[string]string)
- for _, site := range meshData.Sites {
- siteAuthorizationStatus[site.GetID()] = meshdata.GetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false")
- }
+ err = nil
+ switch updatedData.Status {
+ case meshdata.StatusDefault:
+ err = connector.mergeData(meshData, updatedData)
- // Add/update data by merging
- meshData.Merge(updatedData)
+ case meshdata.StatusObsolete:
+ err = connector.unmergeData(meshData, 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)
- }
- }
+ case meshdata.StatusAuthorize:
+ err = connector.authorizeData(meshData, updatedData, true)
+
+ case meshdata.StatusUnauthorize:
+ err = connector.authorizeData(meshData, updatedData, false)
}
// Write the updated sites back to the file
@@ -119,6 +113,48 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa
return nil
}
+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.GetID()] = 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
+}
+
+func (connector *LocalFileConnector) unmergeData(meshData *meshdata.MeshData, updatedData *meshdata.MeshData) error {
+ // Remove data by unmerging
+ meshData.Unmerge(updatedData)
+ return nil
+}
+
+func (connector *LocalFileConnector) authorizeData(meshData *meshdata.MeshData, updatedData *meshdata.MeshData, authorize bool) error {
+ for _, placeholderSite := range updatedData.Sites {
+ // The site ID is stored in the updated site's name
+ if site := meshData.FindSite(placeholderSite.Name); 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/exporters/cs3api.go b/pkg/mentix/exchangers/exporters/cs3api.go
index 849de25591..b224eadc65 100755
--- a/pkg/mentix/exchangers/exporters/cs3api.go
+++ b/pkg/mentix/exchangers/exporters/cs3api.go
@@ -37,7 +37,7 @@ func (exporter *CS3APIExporter) Activate(conf *config.Configuration, log *zerolo
}
// Store CS3API specifics
- exporter.SetEndpoint(conf.Exporters.CS3API.Endpoint)
+ exporter.SetEndpoint(conf.Exporters.CS3API.Endpoint, conf.Exporters.CS3API.IsProtected)
exporter.SetEnabledConnectors(conf.Exporters.CS3API.EnabledConnectors)
exporter.defaultActionHandler = cs3api.HandleDefaultQuery
diff --git a/pkg/mentix/exchangers/exporters/sitelocations.go b/pkg/mentix/exchangers/exporters/sitelocations.go
index fffd6325c1..33ad4c838c 100755
--- a/pkg/mentix/exchangers/exporters/sitelocations.go
+++ b/pkg/mentix/exchangers/exporters/sitelocations.go
@@ -37,7 +37,7 @@ func (exporter *SiteLocationsExporter) Activate(conf *config.Configuration, log
}
// Store SiteLocations specifics
- exporter.SetEndpoint(conf.Exporters.SiteLocations.Endpoint)
+ exporter.SetEndpoint(conf.Exporters.SiteLocations.Endpoint, conf.Exporters.SiteLocations.IsProtected)
exporter.SetEnabledConnectors(conf.Exporters.SiteLocations.EnabledConnectors)
exporter.defaultActionHandler = siteloc.HandleDefaultQuery
diff --git a/pkg/mentix/exchangers/exporters/webapi.go b/pkg/mentix/exchangers/exporters/webapi.go
index 346cc10907..36cdeabed2 100755
--- a/pkg/mentix/exchangers/exporters/webapi.go
+++ b/pkg/mentix/exchangers/exporters/webapi.go
@@ -37,7 +37,7 @@ func (exporter *WebAPIExporter) Activate(conf *config.Configuration, log *zerolo
}
// Store WebAPI specifics
- exporter.SetEndpoint(conf.Exporters.WebAPI.Endpoint)
+ exporter.SetEndpoint(conf.Exporters.WebAPI.Endpoint, conf.Exporters.WebAPI.IsProtected)
exporter.SetEnabledConnectors(conf.Exporters.WebAPI.EnabledConnectors)
exporter.defaultActionHandler = webapi.HandleDefaultQuery
diff --git a/pkg/mentix/exchangers/importers/adminapi.go b/pkg/mentix/exchangers/importers/adminapi.go
new file mode 100755
index 0000000000..53ee50748b
--- /dev/null
+++ b/pkg/mentix/exchangers/importers/adminapi.go
@@ -0,0 +1,60 @@
+// 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 importers
+
+import (
+ "github.com/rs/zerolog"
+
+ "github.com/cs3org/reva/pkg/mentix/config"
+ "github.com/cs3org/reva/pkg/mentix/exchangers/importers/adminapi"
+)
+
+// AdminAPIImporter implements the administrative API importer.
+type AdminAPIImporter struct {
+ BaseRequestImporter
+}
+
+// Activate activates the importer.
+func (importer *AdminAPIImporter) 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)
+
+ importer.authorizeSiteActionHandler = adminapi.HandleAuthorizeSiteQuery
+
+ return nil
+}
+
+// GetID returns the ID of the importer.
+func (importer *AdminAPIImporter) GetID() string {
+ return config.ImporterIDAdminAPI
+}
+
+// GetName returns the display name of the importer.
+func (importer *AdminAPIImporter) GetName() string {
+ return "AdminAPI"
+}
+
+func init() {
+ registerImporter(&AdminAPIImporter{})
+}
diff --git a/pkg/mentix/exchangers/importers/adminapi/query.go b/pkg/mentix/exchangers/importers/adminapi/query.go
new file mode 100755
index 0000000000..f98902c594
--- /dev/null
+++ b/pkg/mentix/exchangers/importers/adminapi/query.go
@@ -0,0 +1,73 @@
+// 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.Name = id // Store the provided site ID in the site's name
+
+ meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}}
+ return meshData, nil
+ } else {
+ return nil, fmt.Errorf("site id invalid")
+ }
+ } else {
+ 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/reqimporter.go b/pkg/mentix/exchangers/importers/reqimporter.go
index f8c49bcc66..ae4c6c7d65 100644
--- a/pkg/mentix/exchangers/importers/reqimporter.go
+++ b/pkg/mentix/exchangers/importers/reqimporter.go
@@ -32,6 +32,7 @@ import (
const (
queryActionRegisterSite = "register"
queryActionUnregisterSite = "unregister"
+ queryActionAuthorizeSite = "authorize"
)
type queryCallback func([]byte, url.Values) (meshdata.Vector, int, []byte, error)
@@ -43,12 +44,13 @@ type BaseRequestImporter struct {
registerSiteActionHandler queryCallback
unregisterSiteActionHandler queryCallback
+ authorizeSiteActionHandler queryCallback
}
// HandleRequest handles the actual HTTP request.
func (importer *BaseRequestImporter) HandleRequest(resp http.ResponseWriter, req *http.Request) {
body, _ := ioutil.ReadAll(req.Body)
- meshData, status, respData, err := importer.handleQuery(body, req.URL.Query())
+ meshData, status, respData, err := importer.handleQuery(body, req.URL.Path, req.URL.Query())
if err == nil {
if len(meshData) > 0 {
importer.mergeImportedMeshData(meshData)
@@ -73,7 +75,7 @@ func (importer *BaseRequestImporter) mergeImportedMeshData(meshData meshdata.Vec
}
}
-func (importer *BaseRequestImporter) handleQuery(data []byte, params url.Values) (meshdata.Vector, int, []byte, error) {
+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:
@@ -86,6 +88,11 @@ func (importer *BaseRequestImporter) handleQuery(data []byte, params url.Values)
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)
}
diff --git a/pkg/mentix/exchangers/importers/webapi.go b/pkg/mentix/exchangers/importers/webapi.go
index ac42fd0750..1a542d59ad 100755
--- a/pkg/mentix/exchangers/importers/webapi.go
+++ b/pkg/mentix/exchangers/importers/webapi.go
@@ -31,28 +31,28 @@ type WebAPIImporter struct {
}
// Activate activates the importer.
-func (exporter *WebAPIImporter) Activate(conf *config.Configuration, log *zerolog.Logger) error {
- if err := exporter.BaseRequestImporter.Activate(conf, log); err != nil {
+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
- exporter.SetEndpoint(conf.Importers.WebAPI.Endpoint)
- exporter.SetEnabledConnectors(conf.Importers.WebAPI.EnabledConnectors)
+ importer.SetEndpoint(conf.Importers.WebAPI.Endpoint, conf.Importers.WebAPI.IsProtected)
+ importer.SetEnabledConnectors(conf.Importers.WebAPI.EnabledConnectors)
- exporter.registerSiteActionHandler = webapi.HandleRegisterSiteQuery
- exporter.unregisterSiteActionHandler = webapi.HandleUnregisterSiteQuery
+ importer.registerSiteActionHandler = webapi.HandleRegisterSiteQuery
+ importer.unregisterSiteActionHandler = webapi.HandleUnregisterSiteQuery
return nil
}
// GetID returns the ID of the importer.
-func (exporter *WebAPIImporter) GetID() string {
+func (importer *WebAPIImporter) GetID() string {
return config.ImporterIDWebAPI
}
// GetName returns the display name of the importer.
-func (exporter *WebAPIImporter) GetName() string {
+func (importer *WebAPIImporter) GetName() string {
return "WebAPI"
}
diff --git a/pkg/mentix/exchangers/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go
index b015b141d8..9929ce7881 100755
--- a/pkg/mentix/exchangers/importers/webapi/query.go
+++ b/pkg/mentix/exchangers/importers/webapi/query.go
@@ -46,21 +46,21 @@ func decodeQueryData(data []byte) (*meshdata.MeshData, error) {
return meshData, nil
}
-func handleQuery(data []byte, params url.Values, flags int32, msg string) (meshdata.Vector, int, []byte, error) {
+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.Flags = flags
+ meshData.Status = status
return meshdata.Vector{meshData}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": meshData.Sites[0].GetID()}), nil
}
// HandleRegisterSiteQuery registers a site.
func HandleRegisterSiteQuery(data []byte, params url.Values) (meshdata.Vector, int, []byte, error) {
- return handleQuery(data, params, meshdata.FlagsNone, "SITE_REGISTERED")
+ 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.FlagObsolete, "SITE_UNREGISTERED")
+ return handleQuery(data, params, meshdata.StatusObsolete, "SITE_UNREGISTERED")
}
diff --git a/pkg/mentix/exchangers/reqexchanger.go b/pkg/mentix/exchangers/reqexchanger.go
index c8cd381655..fcbb4daf80 100644
--- a/pkg/mentix/exchangers/reqexchanger.go
+++ b/pkg/mentix/exchangers/reqexchanger.go
@@ -27,6 +27,8 @@ import (
type RequestExchanger interface {
// Endpoint returns the (relative) endpoint of the exchanger.
Endpoint() string
+ // IsProtectedEndpoint returns true if the endpoint can only be accessed with authorization.
+ IsProtectedEndpoint() bool
// WantsRequest returns whether the exchanger wants to handle the incoming request.
WantsRequest(r *http.Request) bool
// HandleRequest handles the actual HTTP request.
@@ -37,7 +39,8 @@ type RequestExchanger interface {
type BaseRequestExchanger struct {
RequestExchanger
- endpoint string
+ endpoint string
+ isProtectedEndpoint bool
}
// Endpoint returns the (relative) endpoint of the exchanger.
@@ -50,12 +53,18 @@ func (exchanger *BaseRequestExchanger) Endpoint() string {
return strings.TrimSpace(endpoint)
}
+// IsProtectedEndpoint returns true if the endpoint can only be accessed with authorization.
+func (exchanger *BaseRequestExchanger) IsProtectedEndpoint() bool {
+ return exchanger.isProtectedEndpoint
+}
+
// SetEndpoint sets the (relative) endpoint of the exchanger.
-func (exchanger *BaseRequestExchanger) SetEndpoint(endpoint string) {
+func (exchanger *BaseRequestExchanger) SetEndpoint(endpoint string, isProtected bool) {
exchanger.endpoint = endpoint
+ exchanger.isProtectedEndpoint = isProtected
}
-// WantsRequest returns whether the exporter wants to handle the incoming request.
+// WantsRequest returns whether the exchanger wants to handle the incoming request.
func (exchanger *BaseRequestExchanger) WantsRequest(r *http.Request) bool {
return r.URL.Path == exchanger.Endpoint()
}
diff --git a/pkg/mentix/meshdata/meshdata.go b/pkg/mentix/meshdata/meshdata.go
index 5162dab859..83f5344a33 100644
--- a/pkg/mentix/meshdata/meshdata.go
+++ b/pkg/mentix/meshdata/meshdata.go
@@ -25,11 +25,15 @@ import (
)
const (
- // FlagsNone resets all mesh data flags.
- FlagsNone = 0
-
- // FlagObsolete flags the mesh data for removal.
- FlagObsolete = 0x0001
+ // StatusDefault signals that this is just regular data.
+ StatusDefault = iota
+
+ // 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.
@@ -37,7 +41,7 @@ type MeshData struct {
Sites []*Site
ServiceTypes []*ServiceType
- Flags int32 `json:"-"`
+ Status int `json:"-"`
}
// Clear removes all saved data, leaving an empty mesh.
@@ -45,7 +49,7 @@ func (meshData *MeshData) Clear() {
meshData.Sites = nil
meshData.ServiceTypes = nil
- meshData.Flags = FlagsNone
+ meshData.Status = StatusDefault
}
// AddSite adds a new site; if a site with the same ID already exists, the existing one is overwritten.
From c3e082e8f92e388b02c10dc393c7b5bc87bf803c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 16:52:13 +0100
Subject: [PATCH 10/59] Update changelog
---
changelog/unreleased/mentix-prom-scheme.md | 5 -----
changelog/unreleased/mentix-site-auth.md | 7 +++++++
2 files changed, 7 insertions(+), 5 deletions(-)
delete mode 100644 changelog/unreleased/mentix-prom-scheme.md
create mode 100644 changelog/unreleased/mentix-site-auth.md
diff --git a/changelog/unreleased/mentix-prom-scheme.md b/changelog/unreleased/mentix-prom-scheme.md
deleted file mode 100644
index 8c33586ca0..0000000000
--- a/changelog/unreleased/mentix-prom-scheme.md
+++ /dev/null
@@ -1,5 +0,0 @@
-Enhancement: Set scheme for Prometheus targets in Mentix
-
-This enhancement lets Mentix set the scheme for Prometheus targets. This allows us to also support monitoring of sites that do not support the default HTTPS scheme.
-
-https://github.com/cs3org/reva/pull/1398
diff --git a/changelog/unreleased/mentix-site-auth.md b/changelog/unreleased/mentix-site-auth.md
new file mode 100644
index 0000000000..22372c181a
--- /dev/null
+++ b/changelog/unreleased/mentix-site-auth.md
@@ -0,0 +1,7 @@
+Enhancement: Support site authorization status in Mentix
+
+This enhancement adds support for a site authorization status to Mentix. This way, sites registered via a web app can now be excluded until authorized manually by an administrator.
+
+Furthermore, Mentix now sets the scheme for Prometheus targets. This allows us to also support monitoring of sites that do not support the default HTTPS scheme.
+
+https://github.com/cs3org/reva/pull/1398
From 63836d1941812ad94eb04dca5b0fd167db45966d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 16:54:33 +0100
Subject: [PATCH 11/59] Hound fix
---
pkg/mentix/exchangers/importers/adminapi/query.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/mentix/exchangers/importers/adminapi/query.go b/pkg/mentix/exchangers/importers/adminapi/query.go
index f98902c594..ede43f9f27 100755
--- a/pkg/mentix/exchangers/importers/adminapi/query.go
+++ b/pkg/mentix/exchangers/importers/adminapi/query.go
@@ -45,9 +45,9 @@ func decodeAdminQueryData(data []byte) (*meshdata.MeshData, error) {
} else {
return nil, fmt.Errorf("site id invalid")
}
- } else {
- return nil, fmt.Errorf("site id missing")
}
+
+ return nil, fmt.Errorf("site id missing")
}
func handleAdminQuery(data []byte, params url.Values, status int, msg string) (meshdata.Vector, int, []byte, error) {
From c39ee4f9562510b3edaa2bb262cd0b6d3d5bc3a1 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 16:55:23 +0100
Subject: [PATCH 12/59] Hound fix
---
pkg/mentix/exchangers/importers/adminapi/query.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/pkg/mentix/exchangers/importers/adminapi/query.go b/pkg/mentix/exchangers/importers/adminapi/query.go
index ede43f9f27..36c2265849 100755
--- a/pkg/mentix/exchangers/importers/adminapi/query.go
+++ b/pkg/mentix/exchangers/importers/adminapi/query.go
@@ -42,9 +42,9 @@ func decodeAdminQueryData(data []byte) (*meshdata.MeshData, error) {
meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}}
return meshData, nil
- } else {
- return nil, fmt.Errorf("site id invalid")
}
+
+ return nil, fmt.Errorf("site id invalid")
}
return nil, fmt.Errorf("site id missing")
From 0fe77e731f56db478dbd16a4a5296e93ee55b144 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 15 Jan 2021 17:11:37 +0100
Subject: [PATCH 13/59] Missing error check
---
pkg/mentix/connectors/localfile.go | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index c6d5b39b54..7642e1897e 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -104,6 +104,10 @@ func (connector *LocalFileConnector) UpdateMeshData(updatedData *meshdata.MeshDa
err = connector.authorizeData(meshData, updatedData, false)
}
+ if err != nil {
+ return err
+ }
+
// Write the updated sites back to the file
jsonData, _ := json.MarshalIndent(meshData.Sites, "", "\t")
if err := ioutil.WriteFile(connector.filePath, jsonData, 0755); err != nil {
From 990af828a1e099ec1df6bbc3e89c0b11e2105fb5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 18 Jan 2021 15:47:10 +0100
Subject: [PATCH 14/59] Expose site ID
---
pkg/mentix/connectors/gocdb.go | 1 +
pkg/mentix/connectors/localfile.go | 3 ++-
pkg/mentix/exchangers/exporters/promsd.go | 2 +-
pkg/mentix/exchangers/exporters/webapi.go | 2 ++
pkg/mentix/exchangers/importers/webapi/query.go | 5 ++++-
pkg/mentix/meshdata/meshdata.go | 6 +++---
pkg/mentix/meshdata/site.go | 12 +++++++++---
7 files changed, 22 insertions(+), 9 deletions(-)
diff --git a/pkg/mentix/connectors/gocdb.go b/pkg/mentix/connectors/gocdb.go
index 425af0525e..8b008ec124 100755
--- a/pkg/mentix/connectors/gocdb.go
+++ b/pkg/mentix/connectors/gocdb.go
@@ -75,6 +75,7 @@ func (connector *GOCDBConnector) RetrieveMeshData() (*meshdata.MeshData, error)
}
}
+ meshData.InferMissingData()
return meshData, nil
}
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index 7642e1897e..1cfeae7ae5 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -78,6 +78,7 @@ func (connector *LocalFileConnector) RetrieveMeshData() (*meshdata.MeshData, err
// Update the site types, as these are not part of the JSON data
connector.setSiteTypes(meshData)
+ meshData.InferMissingData()
return meshData, nil
}
@@ -121,7 +122,7 @@ func (connector *LocalFileConnector) mergeData(meshData *meshdata.MeshData, upda
// Store the previous authorization status for already existing sites
siteAuthorizationStatus := make(map[string]string)
for _, site := range meshData.Sites {
- siteAuthorizationStatus[site.GetID()] = meshdata.GetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false")
+ siteAuthorizationStatus[site.ID] = meshdata.GetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false")
}
// Add/update data by merging
diff --git a/pkg/mentix/exchangers/exporters/promsd.go b/pkg/mentix/exchangers/exporters/promsd.go
index e8b4746ea2..0cb1fba4c8 100755
--- a/pkg/mentix/exchangers/exporters/promsd.go
+++ b/pkg/mentix/exchangers/exporters/promsd.go
@@ -96,7 +96,7 @@ func getScrapeTargetLabels(site *meshdata.Site, host string, endpoint *meshdata.
labels := map[string]string{
"__meta_mentix_site": site.Name,
"__meta_mentix_site_type": meshdata.GetSiteTypeName(site.Type),
- "__meta_mentix_site_id": site.GetID(),
+ "__meta_mentix_site_id": site.ID,
"__meta_mentix_host": host,
"__meta_mentix_country": site.CountryCode,
"__meta_mentix_service_type": endpoint.Type.Name,
diff --git a/pkg/mentix/exchangers/exporters/webapi.go b/pkg/mentix/exchangers/exporters/webapi.go
index 36cdeabed2..30f2b18bd6 100755
--- a/pkg/mentix/exchangers/exporters/webapi.go
+++ b/pkg/mentix/exchangers/exporters/webapi.go
@@ -40,6 +40,8 @@ func (exporter *WebAPIExporter) Activate(conf *config.Configuration, log *zerolo
exporter.SetEndpoint(conf.Exporters.WebAPI.Endpoint, conf.Exporters.WebAPI.IsProtected)
exporter.SetEnabledConnectors(conf.Exporters.WebAPI.EnabledConnectors)
+ exporter.allowUnauthorizedSites = true
+
exporter.defaultActionHandler = webapi.HandleDefaultQuery
return nil
diff --git a/pkg/mentix/exchangers/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go
index 9929ce7881..189bb849ef 100755
--- a/pkg/mentix/exchangers/importers/webapi/query.go
+++ b/pkg/mentix/exchangers/importers/webapi/query.go
@@ -34,6 +34,9 @@ func decodeQueryData(data []byte) (*meshdata.MeshData, error) {
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")
@@ -52,7 +55,7 @@ func handleQuery(data []byte, params url.Values, status int, msg string) (meshda
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].GetID()}), nil
+ return meshdata.Vector{meshData}, http.StatusOK, network.CreateResponse(msg, network.ResponseParams{"id": meshData.Sites[0].ID}), nil
}
// HandleRegisterSiteQuery registers a site.
diff --git a/pkg/mentix/meshdata/meshdata.go b/pkg/mentix/meshdata/meshdata.go
index 83f5344a33..40832c503f 100644
--- a/pkg/mentix/meshdata/meshdata.go
+++ b/pkg/mentix/meshdata/meshdata.go
@@ -54,7 +54,7 @@ func (meshData *MeshData) Clear() {
// AddSite adds a new site; if a site with the same ID already exists, the existing one is overwritten.
func (meshData *MeshData) AddSite(site *Site) {
- if siteExisting := meshData.FindSite(site.GetID()); siteExisting != nil {
+ if siteExisting := meshData.FindSite(site.ID); siteExisting != nil {
*siteExisting = *site
} else {
meshData.Sites = append(meshData.Sites, site)
@@ -79,7 +79,7 @@ func (meshData *MeshData) RemoveSite(id string) {
// FindSite searches for a site with the given ID.
func (meshData *MeshData) FindSite(id string) *Site {
for _, site := range meshData.Sites {
- if strings.EqualFold(site.GetID(), id) {
+ if strings.EqualFold(site.ID, id) {
return site
}
}
@@ -134,7 +134,7 @@ 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.GetID())
+ meshData.RemoveSite(site.ID)
}
for _, serviceType := range inData.ServiceTypes {
diff --git a/pkg/mentix/meshdata/site.go b/pkg/mentix/meshdata/site.go
index 7fd5c5d120..da6ffce2d6 100644
--- a/pkg/mentix/meshdata/site.go
+++ b/pkg/mentix/meshdata/site.go
@@ -40,6 +40,7 @@ type SiteType int
type Site struct {
Type SiteType `json:"-"`
+ ID string
Name string
FullName string
Organization string
@@ -122,15 +123,20 @@ 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()
}
}
-// GetID generates a unique ID for the site; the following fields are used for this:
+// GenerateID generates a unique ID for the site; the following fields are used for this:
// Name, Domain
-func (site *Site) GetID() string {
+func (site *Site) GenerateID() {
host := site.Domain
if site.Homepage != "" {
if hostURL, err := url.Parse(site.Homepage); err == nil {
@@ -138,7 +144,7 @@ func (site *Site) GetID() string {
}
}
- return fmt.Sprintf("%s::[%s]", host, site.Name)
+ site.ID = fmt.Sprintf("%s::[%s]", host, site.Name)
}
// GetSiteTypeName returns the readable name of the given site type.
From 177b3bf0971dc5e9371397c99e8320e84744e287 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 18 Jan 2021 15:55:12 +0100
Subject: [PATCH 15/59] Update documentation and example
---
.../config/http/services/mentix/_index.md | 3 ++
.../http/services/mentix/adminapi/_index.md | 46 +++++++++++++++++++
.../http/services/mentix/cs3api/_index.md | 10 +++-
.../http/services/mentix/siteloc/_index.md | 10 +++-
.../http/services/mentix/webapi/_index.md | 20 +++++++-
examples/mentix/mentix.toml | 6 +++
6 files changed, 91 insertions(+), 4 deletions(-)
create mode 100644 docs/content/en/docs/config/http/services/mentix/adminapi/_index.md
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 0f696886a2..8e12bc5be7 100644
--- a/docs/content/en/docs/config/http/services/mentix/_index.md
+++ b/docs/content/en/docs/config/http/services/mentix/_index.md
@@ -45,6 +45,9 @@ __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)).
+
## 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.
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
new file mode 100644
index 0000000000..55f29684eb
--- /dev/null
+++ b/docs/content/en/docs/config/http/services/mentix/adminapi/_index.md
@@ -0,0 +1,46 @@
+---
+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/cs3api/_index.md b/docs/content/en/docs/config/http/services/mentix/cs3api/_index.md
index d2707bae1c..167de32c17 100644
--- a/docs/content/en/docs/config/http/services/mentix/cs3api/_index.md
+++ b/docs/content/en/docs/config/http/services/mentix/cs3api/_index.md
@@ -10,7 +10,7 @@ description: >
The CS3API exporter exposes Mentix data in a format that is compliant with the CS3API `ProviderInfo` structure via an HTTP endpoint.
{{% /pageinfo %}}
-{{% dir name="endpoint" type="string" default="/" %}}
+{{% dir name="endpoint" type="string" default="/cs3" %}}
The endpoint where the mesh data can be queried.
{{< highlight toml >}}
[http.services.mentix.exporters.cs3api]
@@ -18,6 +18,14 @@ endpoint = "/data"
{{< /highlight >}}
{{% /dir %}}
+{{% dir name="is_protected" type="bool" default="false" %}}
+Whether the endpoint requires authentication.
+{{< highlight toml >}}
+[http.services.mentix.exporters.cs3api]
+is_protected = true
+{{< /highlight >}}
+{{% /dir %}}
+
{{% dir name="enabled_connectors" type="[]string" default="*" %}}
A list of all enabled connectors for the exporter.
{{< highlight toml >}}
diff --git a/docs/content/en/docs/config/http/services/mentix/siteloc/_index.md b/docs/content/en/docs/config/http/services/mentix/siteloc/_index.md
index d33477dbc2..013d8d3f07 100644
--- a/docs/content/en/docs/config/http/services/mentix/siteloc/_index.md
+++ b/docs/content/en/docs/config/http/services/mentix/siteloc/_index.md
@@ -10,7 +10,7 @@ description: >
The Site Locations exporter exposes location information of all sites to be consumed by Grafana via an HTTP endpoint.
{{% /pageinfo %}}
-{{% dir name="endpoint" type="string" default="/" %}}
+{{% dir name="endpoint" type="string" default="/siteloc" %}}
The endpoint where the locations data can be queried.
{{< highlight toml >}}
[http.services.mentix.exporters.siteloc]
@@ -18,6 +18,14 @@ endpoint = "/loc"
{{< /highlight >}}
{{% /dir %}}
+{{% dir name="is_protected" type="bool" default="false" %}}
+Whether the endpoint requires authentication.
+{{< highlight toml >}}
+[http.services.mentix.exporters.siteloc]
+is_protected = true
+{{< /highlight >}}
+{{% /dir %}}
+
{{% dir name="enabled_connectors" type="[]string" default="*" %}}
A list of all enabled connectors for the exporter.
{{< highlight toml >}}
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 9cf4eaea6c..03e4c028bc 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
@@ -24,7 +24,7 @@ Currently, the following actions are supported:
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="/" %}}
+{{% dir name="endpoint" type="string" default="/sites" %}}
The endpoint where the mesh data can be sent to.
{{< highlight toml >}}
[http.services.mentix.importers.webapi]
@@ -32,6 +32,14 @@ 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 >}}
@@ -44,7 +52,7 @@ enabled_connectors = ["localfile"]
The WebAPI exporter exposes the _plain_ Mentix data via an HTTP endpoint.
-{{% dir name="endpoint" type="string" default="/" %}}
+{{% dir name="endpoint" type="string" default="/sites" %}}
The endpoint where the mesh data can be queried.
{{< highlight toml >}}
[http.services.mentix.exporters.webapi]
@@ -52,6 +60,14 @@ endpoint = "/data"
{{< /highlight >}}
{{% /dir %}}
+{{% dir name="is_protected" type="bool" default="false" %}}
+Whether the endpoint requires authentication.
+{{< highlight toml >}}
+[http.services.mentix.exporters.webapi]
+is_protected = true
+{{< /highlight >}}
+{{% /dir %}}
+
{{% dir name="enabled_connectors" type="[]string" default="*" %}}
A list of all enabled connectors for the exporter.
{{< highlight toml >}}
diff --git a/examples/mentix/mentix.toml b/examples/mentix/mentix.toml
index e82c6f8ee1..9baf22bbfc 100644
--- a/examples/mentix/mentix.toml
+++ b/examples/mentix/mentix.toml
@@ -30,6 +30,12 @@ enabled_connectors = ["gocdb"]
# 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
+
# Configure the Prometheus Service Discovery:
[http.services.mentix.exporters.promsd]
# The following files must be made available to Prometheus.
From 5839218ab4d9491d1ac11af3dc2778d9edadc995 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 18 Jan 2021 16:20:56 +0100
Subject: [PATCH 16/59] Make GenerateId private
---
pkg/mentix/meshdata/site.go | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/pkg/mentix/meshdata/site.go b/pkg/mentix/meshdata/site.go
index ae8cbeb7ee..ce41174ad5 100644
--- a/pkg/mentix/meshdata/site.go
+++ b/pkg/mentix/meshdata/site.go
@@ -125,7 +125,7 @@ func (site *Site) InferMissingData() {
// Automatically assign an ID to this site if it is missing
if len(site.ID) == 0 {
- site.GenerateID()
+ site.generateID()
}
// Infer missing for services
@@ -134,9 +134,8 @@ func (site *Site) InferMissingData() {
}
}
-// GenerateID generates a unique ID for the site; the following fields are used for this:
// Name, Domain
-func (site *Site) GenerateID() {
+func (site *Site) generateID() {
host := site.Domain
if site.Homepage != "" {
if hostURL, err := url.Parse(site.Homepage); err == nil {
From d6a74a2c1c5f323846de02d05628f5932b72a0b4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Mon, 18 Jan 2021 21:36:38 +0100
Subject: [PATCH 17/59] Fix crash when (un)registering a site
---
pkg/mentix/connectors/gocdb.go | 2 +-
pkg/mentix/connectors/localfile.go | 6 +++---
pkg/mentix/exchangers/importers/webapi/query.go | 2 +-
pkg/mentix/meshdata/properties.go | 9 +++++++--
4 files changed, 12 insertions(+), 7 deletions(-)
diff --git a/pkg/mentix/connectors/gocdb.go b/pkg/mentix/connectors/gocdb.go
index c7d7e80819..75742f27d3 100755
--- a/pkg/mentix/connectors/gocdb.go
+++ b/pkg/mentix/connectors/gocdb.go
@@ -130,7 +130,7 @@ func (connector *GOCDBConnector) querySites(meshData *meshdata.MeshData) error {
// 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")
+ meshdata.SetPropertyValue(&properties, meshdata.PropertyAuthorized, "true")
}
// See if an organization has been defined using properties; otherwise, use the official name
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index e82bf28c07..cbe3da9605 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -131,7 +131,7 @@ func (connector *LocalFileConnector) mergeData(meshData *meshdata.MeshData, upda
// 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)
+ meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, status)
}
}
return nil
@@ -148,9 +148,9 @@ func (connector *LocalFileConnector) authorizeData(meshData *meshdata.MeshData,
// The site ID is stored in the updated site's name
if site := meshData.FindSite(placeholderSite.Name); site != nil {
if authorize {
- meshdata.SetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "true")
+ meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "true")
} else {
- meshdata.SetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false")
+ meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "false")
}
} else {
return fmt.Errorf("no site with id '%v' found", placeholderSite.Name)
diff --git a/pkg/mentix/exchangers/importers/webapi/query.go b/pkg/mentix/exchangers/importers/webapi/query.go
index 6a2e89e7f7..868d0c8cc3 100755
--- a/pkg/mentix/exchangers/importers/webapi/query.go
+++ b/pkg/mentix/exchangers/importers/webapi/query.go
@@ -38,7 +38,7 @@ func decodeQueryData(data []byte) (*meshdata.MeshData, error) {
site.ID = ""
// Set sites imported through the WebAPI to 'unauthorized' by default
- meshdata.SetPropertyValue(site.Properties, meshdata.PropertyAuthorized, "false")
+ meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "false")
meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}}
if err := meshData.Verify(); err != nil {
diff --git a/pkg/mentix/meshdata/properties.go b/pkg/mentix/meshdata/properties.go
index 1739888300..0374ec2e2f 100644
--- a/pkg/mentix/meshdata/properties.go
+++ b/pkg/mentix/meshdata/properties.go
@@ -47,6 +47,11 @@ func GetPropertyValue(props map[string]string, id string, defValue string) strin
}
// SetPropertyValue sets a property value.
-func SetPropertyValue(props map[string]string, id string, value string) {
- props[strings.ToUpper(id)] = value
+func SetPropertyValue(props *map[string]string, id string, value string) {
+ // If the provided properties map is nil, create an empty one
+ if *props == nil {
+ *props = make(map[string]string)
+ }
+
+ (*props)[strings.ToUpper(id)] = value
}
From dcc9a946a7d672579351a0517708ce6854966677 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Tue, 19 Jan 2021 13:26:02 +0100
Subject: [PATCH 18/59] Use site ID field for authorization
---
pkg/mentix/connectors/localfile.go | 3 +--
pkg/mentix/exchangers/importers/adminapi/query.go | 2 +-
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index cbe3da9605..b968c2fe17 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -145,8 +145,7 @@ func (connector *LocalFileConnector) unmergeData(meshData *meshdata.MeshData, up
func (connector *LocalFileConnector) authorizeData(meshData *meshdata.MeshData, updatedData *meshdata.MeshData, authorize bool) error {
for _, placeholderSite := range updatedData.Sites {
- // The site ID is stored in the updated site's name
- if site := meshData.FindSite(placeholderSite.Name); site != nil {
+ if site := meshData.FindSite(placeholderSite.ID); site != nil {
if authorize {
meshdata.SetPropertyValue(&site.Properties, meshdata.PropertyAuthorized, "true")
} else {
diff --git a/pkg/mentix/exchangers/importers/adminapi/query.go b/pkg/mentix/exchangers/importers/adminapi/query.go
index 36c2265849..cb7a65a198 100755
--- a/pkg/mentix/exchangers/importers/adminapi/query.go
+++ b/pkg/mentix/exchangers/importers/adminapi/query.go
@@ -38,7 +38,7 @@ func decodeAdminQueryData(data []byte) (*meshdata.MeshData, error) {
if value, ok := jsonData["id"]; ok {
if id, ok := value.(string); ok {
site := &meshdata.Site{}
- site.Name = id // Store the provided site ID in the site's name
+ site.ID = id // We only need to store the ID of the site
meshData := &meshdata.MeshData{Sites: []*meshdata.Site{site}}
return meshData, nil
From ea1269d5abaee7e27b1e21aa21f0439e0c074bb2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 5 Feb 2021 13:07:54 +0100
Subject: [PATCH 19/59] Fix Mentix data cloning bug
---
changelog/unreleased/mentix-clone-fix.md | 5 +++++
pkg/mentix/connectors/localfile.go | 1 +
pkg/mentix/meshdata/meshdata.go | 23 +++++++++++------------
3 files changed, 17 insertions(+), 12 deletions(-)
create mode 100644 changelog/unreleased/mentix-clone-fix.md
diff --git a/changelog/unreleased/mentix-clone-fix.md b/changelog/unreleased/mentix-clone-fix.md
new file mode 100644
index 0000000000..635f49064d
--- /dev/null
+++ b/changelog/unreleased/mentix-clone-fix.md
@@ -0,0 +1,5 @@
+Fix: 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/pkg/mentix/connectors/localfile.go b/pkg/mentix/connectors/localfile.go
index b968c2fe17..6c154adc2c 100755
--- a/pkg/mentix/connectors/localfile.go
+++ b/pkg/mentix/connectors/localfile.go
@@ -79,6 +79,7 @@ func (connector *LocalFileConnector) RetrieveMeshData() (*meshdata.MeshData, err
connector.setSiteTypes(meshData)
meshData.InferMissingData()
+
return meshData, nil
}
diff --git a/pkg/mentix/meshdata/meshdata.go b/pkg/mentix/meshdata/meshdata.go
index 9ab0ecdf4a..05289b72d1 100644
--- a/pkg/mentix/meshdata/meshdata.go
+++ b/pkg/mentix/meshdata/meshdata.go
@@ -19,8 +19,11 @@
package meshdata
import (
+ "bytes"
+ "encoding/gob"
"encoding/json"
"fmt"
+ "reflect"
"strings"
)
@@ -196,10 +199,13 @@ func (meshData *MeshData) FromJSON(data string) error {
func (meshData *MeshData) Clone() *MeshData {
clone := &MeshData{}
- // To avoid any "deep copy" packages, use JSON en- and decoding instead
- data, err := meshData.ToJSON()
- if err == nil {
- if err := clone.FromJSON(data); err != nil {
+ // 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(meshData); err == nil {
+ if err := dec.Decode(clone); err != nil {
// In case of an error, clear the data
clone.Clear()
}
@@ -210,14 +216,7 @@ func (meshData *MeshData) Clone() *MeshData {
// Compare checks whether the stored data equals the data of another MeshData object.
func (meshData *MeshData) Compare(other *MeshData) bool {
- if other == nil {
- return false
- }
-
- // To avoid cumbersome comparisons, just compare the JSON-encoded data
- json1, _ := meshData.ToJSON()
- json2, _ := other.ToJSON()
- return json1 == json2
+ return reflect.DeepEqual(meshData, other)
}
// New returns a new (empty) MeshData object.
From 7ab6db27240b65a986359328287caf7004a46f9c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Fri, 5 Feb 2021 13:32:27 +0100
Subject: [PATCH 20/59] Fix changelog
---
changelog/unreleased/mentix-clone-fix.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/changelog/unreleased/mentix-clone-fix.md b/changelog/unreleased/mentix-clone-fix.md
index 635f49064d..d5ab1f3232 100644
--- a/changelog/unreleased/mentix-clone-fix.md
+++ b/changelog/unreleased/mentix-clone-fix.md
@@ -1,4 +1,4 @@
-Fix: Cloning of internal mesh data lost some values
+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.
From 26495e07b185e08fb6a0884cfecf5498d0e3ce6c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Tue, 16 Feb 2021 16:33:05 +0100
Subject: [PATCH 21/59] Begin work on new accounts service
---
internal/http/services/accounts/accounts.go | 177 ++++++++++++++++++
.../http/services/accounts/config/config.go | 32 ++++
.../services/accounts/config/endpoints.go | 26 +++
internal/http/services/accounts/data/data.go | 69 +++++++
.../services/accounts/data/filestorage.go | 113 +++++++++++
.../http/services/accounts/data/storage.go | 34 ++++
internal/http/services/accounts/manager.go | 126 +++++++++++++
internal/http/services/loader/loader.go | 1 +
pkg/apikey/apikey.go | 22 +++
pkg/utils/utils.go | 9 +
10 files changed, 609 insertions(+)
create mode 100644 internal/http/services/accounts/accounts.go
create mode 100644 internal/http/services/accounts/config/config.go
create mode 100644 internal/http/services/accounts/config/endpoints.go
create mode 100644 internal/http/services/accounts/data/data.go
create mode 100644 internal/http/services/accounts/data/filestorage.go
create mode 100644 internal/http/services/accounts/data/storage.go
create mode 100644 internal/http/services/accounts/manager.go
create mode 100644 pkg/apikey/apikey.go
diff --git a/internal/http/services/accounts/accounts.go b/internal/http/services/accounts/accounts.go
new file mode 100644
index 0000000000..ddb1502b6b
--- /dev/null
+++ b/internal/http/services/accounts/accounts.go
@@ -0,0 +1,177 @@
+// 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 accounts
+
+import (
+ "encoding/json"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+
+ "github.com/mitchellh/mapstructure"
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+
+ "github.com/cs3org/reva/internal/http/services/accounts/config"
+ "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 = "accounts"
+)
+
+// 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 {
+ // TODO: For testing only
+ return []string{"/"}
+ // This service currently only has one public endpoint (called "register") used for account registration
+ return []string{config.EndpointRegister}
+}
+
+// Handler serves all HTTP requests.
+func (s *svc) Handler() http.Handler {
+ // Allow definition of endpoints in a flexible and easy way
+ type Endpoint struct {
+ Path string
+ Method string
+ Handler func(url.Values, []byte) (interface{}, error)
+ }
+
+ endpoints := []Endpoint{
+ {config.EndpointList, http.MethodGet, s.handleList},
+ {config.EndpointRegister, http.MethodPost, s.handleRegister},
+ }
+
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ defer r.Body.Close()
+
+ // Every request to the accounts service results in a standardized JSON response
+ type Response struct {
+ Success bool `json:"success"`
+ Error string `json:"error,omitempty"`
+ Data interface{} `json:"data,omitempty"`
+ }
+
+ // The default response is an unknown 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) handleList(values url.Values, data []byte) (interface{}, error) {
+ return s.manager.ClonedAccounts(), nil
+}
+
+func (s *svc) handleRegister(values url.Values, data []byte) (interface{}, error) {
+ return map[string]string{"id": "okiii"}, 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 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 accounts service")
+ }
+
+ // Create the service
+ s := &svc{
+ conf: conf,
+ log: log,
+ manager: mngr,
+ }
+ return s, nil
+}
diff --git a/internal/http/services/accounts/config/config.go b/internal/http/services/accounts/config/config.go
new file mode 100644
index 0000000000..7861701968
--- /dev/null
+++ b/internal/http/services/accounts/config/config.go
@@ -0,0 +1,32 @@
+// 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
+
+// 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"`
+}
diff --git a/internal/http/services/accounts/config/endpoints.go b/internal/http/services/accounts/config/endpoints.go
new file mode 100644
index 0000000000..c6cb64bb26
--- /dev/null
+++ b/internal/http/services/accounts/config/endpoints.go
@@ -0,0 +1,26 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package config
+
+const (
+ // EndpointList is the endpoint path for listing all stored accounts.
+ EndpointList = "/list"
+ // EndpointRegister is the endpoint path for account registration.
+ EndpointRegister = "/register"
+)
diff --git a/internal/http/services/accounts/data/data.go b/internal/http/services/accounts/data/data.go
new file mode 100644
index 0000000000..1ec6a2403d
--- /dev/null
+++ b/internal/http/services/accounts/data/data.go
@@ -0,0 +1,69 @@
+// 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 (
+ "github.com/pkg/errors"
+
+ "github.com/cs3org/reva/pkg/apikey"
+ "github.com/cs3org/reva/pkg/utils"
+)
+
+// Account represents a single user account.
+type Account struct {
+ Email string `json:"email"`
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+
+ Data AccountData `json:"data"`
+}
+
+// AccountData holds additional data for a user account.
+type AccountData struct {
+ APIKey apikey.APIKey `json:"apiKey"`
+ Authorized bool `json:"authorized"`
+}
+
+// Accounts holds an array of user accounts.
+type Accounts = []*Account
+
+// NewAccount creates a new user account.
+func NewAccount(email string, firstName, lastName string) (*Account, error) {
+ if email == "" {
+ return nil, errors.Errorf("no email address provided")
+ } else if !utils.IsEmailValid(email) {
+ return nil, errors.Errorf("invalid email address: %v", email)
+ }
+
+ if firstName == "" || lastName == "" {
+ return nil, errors.Errorf("no or incomplete name provided")
+ }
+
+ acc := &Account{
+ Email: email,
+ FirstName: firstName,
+ LastName: lastName,
+ Data: AccountData{
+ APIKey: "",
+ Authorized: false,
+ },
+ }
+
+ return acc, nil
+}
diff --git a/internal/http/services/accounts/data/filestorage.go b/internal/http/services/accounts/data/filestorage.go
new file mode 100644
index 0000000000..b240af8de3
--- /dev/null
+++ b/internal/http/services/accounts/data/filestorage.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 (
+ "encoding/json"
+ "io/ioutil"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+
+ "github.com/cs3org/reva/internal/http/services/accounts/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
+
+ 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) error {
+ // Simply skip this action; all data is saved solely in WriteAll
+ return nil
+}
+
+// AccountAdded is called when an account has been updated.
+func (storage *FileStorage) AccountUpdated(account *Account) error {
+ // Simply skip this action; all data is saved solely in WriteAll
+ return nil
+}
+
+// AccountAdded is called when an account has been removed.
+func (storage *FileStorage) AccountRemoved(account *Account) error {
+ // Simply skip this action; all data is saved solely in WriteAll
+ return nil
+}
+
+// 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/accounts/data/storage.go b/internal/http/services/accounts/data/storage.go
new file mode 100644
index 0000000000..ffbd5e64c6
--- /dev/null
+++ b/internal/http/services/accounts/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) error
+ // AccountAdded is called when an account has been updated.
+ AccountUpdated(account *Account) error
+ // AccountAdded is called when an account has been removed.
+ AccountRemoved(account *Account) error
+}
diff --git a/internal/http/services/accounts/manager.go b/internal/http/services/accounts/manager.go
new file mode 100644
index 0000000000..6f83c84ce1
--- /dev/null
+++ b/internal/http/services/accounts/manager.go
@@ -0,0 +1,126 @@
+// 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 accounts
+
+import (
+ "bytes"
+ "encoding/gob"
+ "sync"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+
+ "github.com/cs3org/reva/internal/http/services/accounts/config"
+ "github.com/cs3org/reva/internal/http/services/accounts/data"
+)
+
+// Manager is responsible for all user account related tasks.
+type Manager struct {
+ conf *config.Configuration
+ log *zerolog.Logger
+
+ accounts data.Accounts
+ storage data.Storage
+
+ 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 user 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")
+ }
+
+ 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() {
+ mngr.mutex.Lock()
+ defer mngr.mutex.Unlock()
+
+ 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() {
+ mngr.mutex.RLock()
+ defer mngr.mutex.RUnlock()
+
+ 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")
+ }
+}
+
+// ClonedAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
+func (mngr *Manager) ClonedAccounts() 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/loader/loader.go b/internal/http/services/loader/loader.go
index 0013049632..4a5c12ab98 100644
--- a/internal/http/services/loader/loader.go
+++ b/internal/http/services/loader/loader.go
@@ -20,6 +20,7 @@ package loader
import (
// Load core HTTP services
+ _ "github.com/cs3org/reva/internal/http/services/accounts"
_ "github.com/cs3org/reva/internal/http/services/datagateway"
_ "github.com/cs3org/reva/internal/http/services/dataprovider"
_ "github.com/cs3org/reva/internal/http/services/helloworld"
diff --git a/pkg/apikey/apikey.go b/pkg/apikey/apikey.go
new file mode 100644
index 0000000000..2727ab15c8
--- /dev/null
+++ b/pkg/apikey/apikey.go
@@ -0,0 +1,22 @@
+// 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 apikey
+
+// APIKey is the type used to store API keys.
+type APIKey = string
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index ad2a9cbcd8..c7c495e883 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -36,6 +36,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.
@@ -146,3 +147,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)
+}
From 0516ceb75616a5c9221bcc32c1e8d8fc16e1961d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Tue, 16 Feb 2021 17:37:16 +0100
Subject: [PATCH 22/59] Implement account creation, update, removal endpoints
---
internal/http/services/accounts/accounts.go | 53 ++++++++--
.../services/accounts/config/endpoints.go | 8 +-
internal/http/services/accounts/data/data.go | 41 ++++++--
.../services/accounts/data/filestorage.go | 9 +-
.../http/services/accounts/data/storage.go | 6 +-
internal/http/services/accounts/manager.go | 98 +++++++++++++++++--
6 files changed, 182 insertions(+), 33 deletions(-)
diff --git a/internal/http/services/accounts/accounts.go b/internal/http/services/accounts/accounts.go
index ddb1502b6b..791bf0b95d 100644
--- a/internal/http/services/accounts/accounts.go
+++ b/internal/http/services/accounts/accounts.go
@@ -30,6 +30,7 @@ import (
"github.com/rs/zerolog"
"github.com/cs3org/reva/internal/http/services/accounts/config"
+ "github.com/cs3org/reva/internal/http/services/accounts/data"
"github.com/cs3org/reva/pkg/rhttp/global"
)
@@ -63,7 +64,7 @@ func (s *svc) Unprotected() []string {
// TODO: For testing only
return []string{"/"}
// This service currently only has one public endpoint (called "register") used for account registration
- return []string{config.EndpointRegister}
+ return []string{config.EndpointCreate}
}
// Handler serves all HTTP requests.
@@ -77,7 +78,9 @@ func (s *svc) Handler() http.Handler {
endpoints := []Endpoint{
{config.EndpointList, http.MethodGet, s.handleList},
- {config.EndpointRegister, http.MethodPost, s.handleRegister},
+ {config.EndpointCreate, http.MethodPost, s.handleCreate},
+ {config.EndpointUpdate, http.MethodPost, s.handleUpdate},
+ {config.EndpointRemove, http.MethodPost, s.handleRemove},
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -126,12 +129,50 @@ func (s *svc) Handler() http.Handler {
})
}
-func (s *svc) handleList(values url.Values, data []byte) (interface{}, error) {
- return s.manager.ClonedAccounts(), nil
+func (s *svc) handleList(values url.Values, body []byte) (interface{}, error) {
+ return s.manager.CloneAccounts(), nil
}
-func (s *svc) handleRegister(values url.Values, data []byte) (interface{}, error) {
- return map[string]string{"id": "okiii"}, nil
+func (s *svc) handleCreate(values url.Values, body []byte) (interface{}, error) {
+ account := &data.Account{}
+ if err := json.Unmarshal(body, account); err != nil {
+ return nil, errors.Wrap(err, "invalid account data")
+ }
+
+ // 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 := &data.Account{}
+ if err := json.Unmarshal(body, account); err != nil {
+ return nil, errors.Wrap(err, "invalid account data")
+ }
+
+ // 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 := &data.Account{}
+ if err := json.Unmarshal(body, account); err != nil {
+ return nil, errors.Wrap(err, "invalid account data")
+ }
+
+ // 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 parseConfig(m map[string]interface{}) (*config.Configuration, error) {
diff --git a/internal/http/services/accounts/config/endpoints.go b/internal/http/services/accounts/config/endpoints.go
index c6cb64bb26..6b0f2ef07b 100644
--- a/internal/http/services/accounts/config/endpoints.go
+++ b/internal/http/services/accounts/config/endpoints.go
@@ -21,6 +21,10 @@ package config
const (
// EndpointList is the endpoint path for listing all stored accounts.
EndpointList = "/list"
- // EndpointRegister is the endpoint path for account registration.
- EndpointRegister = "/register"
+ // 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"
)
diff --git a/internal/http/services/accounts/data/data.go b/internal/http/services/accounts/data/data.go
index 1ec6a2403d..22c7aaf8c3 100644
--- a/internal/http/services/accounts/data/data.go
+++ b/internal/http/services/accounts/data/data.go
@@ -43,18 +43,39 @@ type AccountData struct {
// Accounts holds an array of user accounts.
type Accounts = []*Account
-// NewAccount creates a new user account.
-func NewAccount(email string, firstName, lastName string) (*Account, error) {
- if email == "" {
- return nil, errors.Errorf("no email address provided")
- } else if !utils.IsEmailValid(email) {
- return nil, errors.Errorf("invalid email address: %v", email)
+// 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
}
- if firstName == "" || lastName == "" {
- return nil, errors.Errorf("no or incomplete name provided")
+ 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 user account.
+func NewAccount(email string, firstName, lastName string) (*Account, error) {
acc := &Account{
Email: email,
FirstName: firstName,
@@ -65,5 +86,9 @@ func NewAccount(email string, firstName, lastName string) (*Account, error) {
},
}
+ if err := acc.verify(); err != nil {
+ return nil, err
+ }
+
return acc, nil
}
diff --git a/internal/http/services/accounts/data/filestorage.go b/internal/http/services/accounts/data/filestorage.go
index b240af8de3..681e133758 100644
--- a/internal/http/services/accounts/data/filestorage.go
+++ b/internal/http/services/accounts/data/filestorage.go
@@ -86,21 +86,18 @@ func (storage *FileStorage) WriteAll(accounts *Accounts) error {
}
// AccountAdded is called when an account has been added.
-func (storage *FileStorage) AccountAdded(account *Account) error {
+func (storage *FileStorage) AccountAdded(account *Account) {
// Simply skip this action; all data is saved solely in WriteAll
- return nil
}
// AccountAdded is called when an account has been updated.
-func (storage *FileStorage) AccountUpdated(account *Account) error {
+func (storage *FileStorage) AccountUpdated(account *Account) {
// Simply skip this action; all data is saved solely in WriteAll
- return nil
}
// AccountAdded is called when an account has been removed.
-func (storage *FileStorage) AccountRemoved(account *Account) error {
+func (storage *FileStorage) AccountRemoved(account *Account) {
// Simply skip this action; all data is saved solely in WriteAll
- return nil
}
// NewFileStorage creates a new filePath storage.
diff --git a/internal/http/services/accounts/data/storage.go b/internal/http/services/accounts/data/storage.go
index ffbd5e64c6..58269c72ba 100644
--- a/internal/http/services/accounts/data/storage.go
+++ b/internal/http/services/accounts/data/storage.go
@@ -26,9 +26,9 @@ type Storage interface {
WriteAll(accounts *Accounts) error
// AccountAdded is called when an account has been added.
- AccountAdded(account *Account) error
+ AccountAdded(account *Account)
// AccountAdded is called when an account has been updated.
- AccountUpdated(account *Account) error
+ AccountUpdated(account *Account)
// AccountAdded is called when an account has been removed.
- AccountRemoved(account *Account) error
+ AccountRemoved(account *Account)
}
diff --git a/internal/http/services/accounts/manager.go b/internal/http/services/accounts/manager.go
index 6f83c84ce1..0508a8402b 100644
--- a/internal/http/services/accounts/manager.go
+++ b/internal/http/services/accounts/manager.go
@@ -21,6 +21,7 @@ package accounts
import (
"bytes"
"encoding/gob"
+ "strings"
"sync"
"github.com/pkg/errors"
@@ -28,6 +29,7 @@ import (
"github.com/cs3org/reva/internal/http/services/accounts/config"
"github.com/cs3org/reva/internal/http/services/accounts/data"
+ "github.com/cs3org/reva/pkg/apikey"
)
// Manager is responsible for all user account related tasks.
@@ -74,9 +76,6 @@ func (mngr *Manager) createStorage(driver string) (data.Storage, error) {
}
func (mngr *Manager) readAllAccounts() {
- mngr.mutex.Lock()
- defer mngr.mutex.Unlock()
-
if accounts, err := mngr.storage.ReadAll(); err == nil {
mngr.accounts = *accounts
} else {
@@ -86,17 +85,100 @@ func (mngr *Manager) readAllAccounts() {
}
func (mngr *Manager) writeAllAccounts() {
- mngr.mutex.RLock()
- defer mngr.mutex.RUnlock()
-
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")
}
}
-// ClonedAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
-func (mngr *Manager) ClonedAccounts() data.Accounts {
+func (mngr *Manager) findAccountByEmail(email string) *data.Account {
+ if email == "" {
+ return nil
+ }
+
+ // Perform a case-insensitive search of the given email address
+ for _, account := range mngr.accounts {
+ if strings.EqualFold(account.Email, email) {
+ return account
+ }
+ }
+ return nil
+}
+
+func (mngr *Manager) findAccountByAPIKey(key apikey.APIKey) *data.Account {
+ if key == "" {
+ return nil
+ }
+
+ // Perform a case-sensitive search of the given API key
+ for _, account := range mngr.accounts {
+ if account.Data.APIKey == key {
+ return account
+ }
+ }
+ return nil
+}
+
+// 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 mngr.findAccountByEmail(accountData.Email) != 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()
+ } 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 := mngr.findAccountByEmail(accountData.Email)
+ if account == nil {
+ return errors.Errorf("no account with the specified email exists")
+ }
+
+ if err := account.Copy(accountData, copyData); err == nil {
+ mngr.storage.AccountUpdated(account)
+ mngr.writeAllAccounts()
+ } else {
+ return errors.Wrap(err, "error while updating account")
+ }
+
+ 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()
From 7be2952b8150b743911626eb65ab2b90d7f9a439 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Wed, 17 Feb 2021 15:00:13 +0100
Subject: [PATCH 23/59] Add web interface panel
---
internal/http/services/accounts/accounts.go | 153 ++++++++++++------
.../services/accounts/config/endpoints.go | 7 +
internal/http/services/accounts/data/data.go | 15 +-
internal/http/services/accounts/manager.go | 38 +++++
.../http/services/accounts/panel/panel.go | 81 ++++++++++
.../http/services/accounts/panel/template.go | 106 ++++++++++++
6 files changed, 349 insertions(+), 51 deletions(-)
create mode 100644 internal/http/services/accounts/panel/panel.go
create mode 100644 internal/http/services/accounts/panel/template.go
diff --git a/internal/http/services/accounts/accounts.go b/internal/http/services/accounts/accounts.go
index 791bf0b95d..03271c273e 100644
--- a/internal/http/services/accounts/accounts.go
+++ b/internal/http/services/accounts/accounts.go
@@ -24,6 +24,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
+ "strings"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
@@ -69,6 +70,27 @@ func (s *svc) Unprotected() []string {
// 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
@@ -76,57 +98,54 @@ func (s *svc) Handler() http.Handler {
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.EndpointList, http.MethodGet, s.handleList},
{config.EndpointCreate, http.MethodPost, s.handleCreate},
{config.EndpointUpdate, http.MethodPost, s.handleUpdate},
{config.EndpointRemove, http.MethodPost, s.handleRemove},
+ {config.EndpointAuthorize, http.MethodPost, s.handleAuthorize},
}
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- defer r.Body.Close()
-
- // Every request to the accounts service results in a standardized JSON response
- type Response struct {
- Success bool `json:"success"`
- Error string `json:"error,omitempty"`
- Data interface{} `json:"data,omitempty"`
- }
-
- // The default response is an unknown 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,
- }
+ // 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
+ // 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
}
- }
- // Any failure during query handling results in a bad request
- if !resp.Success {
- w.WriteHeader(http.StatusBadRequest)
+ break
}
+ }
- jsonData, _ := json.MarshalIndent(&resp, "", "\t")
- _, _ = w.Write(jsonData)
- })
+ // 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) handleList(values url.Values, body []byte) (interface{}, error) {
@@ -134,9 +153,9 @@ func (s *svc) handleList(values url.Values, body []byte) (interface{}, error) {
}
func (s *svc) handleCreate(values url.Values, body []byte) (interface{}, error) {
- account := &data.Account{}
- if err := json.Unmarshal(body, account); err != nil {
- return nil, errors.Wrap(err, "invalid account data")
+ account, err := s.unmarshalRequestData(body)
+ if err != nil {
+ return nil, err
}
// Create a new account through the account manager
@@ -148,9 +167,9 @@ func (s *svc) handleCreate(values url.Values, body []byte) (interface{}, error)
}
func (s *svc) handleUpdate(values url.Values, body []byte) (interface{}, error) {
- account := &data.Account{}
- if err := json.Unmarshal(body, account); err != nil {
- return nil, errors.Wrap(err, "invalid account data")
+ 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
@@ -162,9 +181,9 @@ func (s *svc) handleUpdate(values url.Values, body []byte) (interface{}, error)
}
func (s *svc) handleRemove(values url.Values, body []byte) (interface{}, error) {
- account := &data.Account{}
- if err := json.Unmarshal(body, account); err != nil {
- return nil, errors.Wrap(err, "invalid account data")
+ account, err := s.unmarshalRequestData(body)
+ if err != nil {
+ return nil, err
}
// Remove the account through the account manager
@@ -175,6 +194,44 @@ func (s *svc) handleRemove(values url.Values, body []byte) (interface{}, error)
return nil, nil
}
+func (s *svc) handleAuthorize(values url.Values, body []byte) (interface{}, error) {
+ account, err := s.unmarshalRequestData(body)
+ if err != nil {
+ return nil, err
+ }
+
+ if val, ok := values["status"]; ok && len(val) > 0 {
+ var authorize bool
+ switch strings.ToLower(val[0]) {
+ 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 parseConfig(m map[string]interface{}) (*config.Configuration, error) {
conf := &config.Configuration{}
if err := mapstructure.Decode(m, &conf); err != nil {
diff --git a/internal/http/services/accounts/config/endpoints.go b/internal/http/services/accounts/config/endpoints.go
index 6b0f2ef07b..f6077ca6c7 100644
--- a/internal/http/services/accounts/config/endpoints.go
+++ b/internal/http/services/accounts/config/endpoints.go
@@ -19,12 +19,19 @@
package config
const (
+ // EndpointPanel is the endpoint of the web interface panel.
+ EndpointPanel = "/panel"
+
// EndpointList is the endpoint path for listing all stored accounts.
EndpointList = "/list"
+
// 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"
)
diff --git a/internal/http/services/accounts/data/data.go b/internal/http/services/accounts/data/data.go
index 22c7aaf8c3..9f6b5b5639 100644
--- a/internal/http/services/accounts/data/data.go
+++ b/internal/http/services/accounts/data/data.go
@@ -19,6 +19,8 @@
package data
import (
+ "time"
+
"github.com/pkg/errors"
"github.com/cs3org/reva/pkg/apikey"
@@ -31,6 +33,9 @@ type Account struct {
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
+ DateCreated time.Time `json:"dateCreated"`
+ DateModified time.Time `json:"dateModified"`
+
Data AccountData `json:"data"`
}
@@ -76,10 +81,14 @@ func (acc *Account) verify() error {
// NewAccount creates a new user account.
func NewAccount(email string, firstName, lastName string) (*Account, error) {
+ t := time.Now()
+
acc := &Account{
- Email: email,
- FirstName: firstName,
- LastName: lastName,
+ Email: email,
+ FirstName: firstName,
+ LastName: lastName,
+ DateCreated: t,
+ DateModified: t,
Data: AccountData{
APIKey: "",
Authorized: false,
diff --git a/internal/http/services/accounts/manager.go b/internal/http/services/accounts/manager.go
index 0508a8402b..0dcf4704b2 100644
--- a/internal/http/services/accounts/manager.go
+++ b/internal/http/services/accounts/manager.go
@@ -21,14 +21,17 @@ package accounts
import (
"bytes"
"encoding/gob"
+ "net/http"
"strings"
"sync"
+ "time"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/cs3org/reva/internal/http/services/accounts/config"
"github.com/cs3org/reva/internal/http/services/accounts/data"
+ "github.com/cs3org/reva/internal/http/services/accounts/panel"
"github.com/cs3org/reva/pkg/apikey"
)
@@ -40,6 +43,8 @@ type Manager struct {
accounts data.Accounts
storage data.Storage
+ panel *panel.Panel
+
mutex sync.RWMutex
}
@@ -64,6 +69,13 @@ func (mngr *Manager) initialize(conf *config.Configuration, log *zerolog.Logger)
return errors.Wrap(err, "unable to create accounts storage")
}
+ // Create the web interface panel
+ if pnl, err := panel.NewPanel(conf, log); err == nil {
+ mngr.panel = pnl
+ } else {
+ return errors.Wrap(err, "unable to create panel")
+ }
+
return nil
}
@@ -119,6 +131,12 @@ func (mngr *Manager) findAccountByAPIKey(key apikey.APIKey) *data.Account {
return nil
}
+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()
@@ -151,6 +169,8 @@ func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) err
}
if err := account.Copy(accountData, copyData); err == nil {
+ account.DateModified = time.Now()
+
mngr.storage.AccountUpdated(account)
mngr.writeAllAccounts()
} else {
@@ -160,6 +180,24 @@ func (mngr *Manager) UpdateAccount(accountData *data.Account, copyData bool) err
return 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 := mngr.findAccountByEmail(accountData.Email)
+ if account == nil {
+ return errors.Errorf("no account with the specified email exists")
+ }
+
+ account.Data.Authorized = authorized
+
+ mngr.storage.AccountUpdated(account)
+ mngr.writeAllAccounts()
+
+ 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()
diff --git a/internal/http/services/accounts/panel/panel.go b/internal/http/services/accounts/panel/panel.go
new file mode 100644
index 0000000000..908f84ab7f
--- /dev/null
+++ b/internal/http/services/accounts/panel/panel.go
@@ -0,0 +1,81 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package panel
+
+import (
+ "html/template"
+ "net/http"
+
+ "github.com/pkg/errors"
+ "github.com/rs/zerolog"
+
+ "github.com/cs3org/reva/internal/http/services/accounts/config"
+ "github.com/cs3org/reva/internal/http/services/accounts/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
+}
+
+func (panel *Panel) Execute(w http.ResponseWriter, accounts *data.Accounts) error {
+ type TemplateData struct {
+ Count int
+ Accounts *data.Accounts
+ }
+
+ data := TemplateData{
+ Count: len(*accounts),
+ Accounts: accounts,
+ }
+
+ return panel.tpl.Execute(w, data)
+}
+
+// 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/accounts/panel/template.go b/internal/http/services/accounts/panel/template.go
new file mode 100644
index 0000000000..c07af578e1
--- /dev/null
+++ b/internal/http/services/accounts/panel/template.go
@@ -0,0 +1,106 @@
+// Copyright 2018-2020 CERN
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// In applying this license, CERN does not waive the privileges and immunities
+// granted to it by virtue of its status as an Intergovernmental Organization
+// or submit itself to any jurisdiction.
+
+package panel
+
+const panelTemplate = `
+
+
+
+
+
+ Accounts panel
+
+
+
+Accounts ({{.Count}})
+
+
+ {{range .Accounts}}
+ -
+
+ {{.Email}}
+ {{.FirstName}} {{.LastName}} (Joined: {{.DateCreated.Format "Jan 01, 2000 10:00"}}; Last modified: {{.DateModified.Format "Jan 01, 2000 10:00"}})
+
+
+ API-Key:
+ {{if .Data.APIKey}}
+ {{.Data.APIKey}}
+ {{else}}
+ Not assigned
+ {{end}}
+
+ Authorized:
+ {{if .Data.Authorized}}
+ Yes
+ {{else}}
+ No
+ {{end}}
+
+
+
+
+
+
+ {{end}}
+
+
+
+
+
+`
From 92dd65a8633cb80c6cf1b0f143777c1aafd74491 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20M=C3=BCller?=
Date: Wed, 17 Feb 2021 16:50:39 +0100
Subject: [PATCH 24/59] Add API key generator and endpoints
---
internal/http/services/accounts/accounts.go | 40 ++++++++++++++-
.../services/accounts/config/endpoints.go | 7 ++-
internal/http/services/accounts/manager.go | 25 ++++++++++
.../http/services/accounts/panel/template.go | 4 +-
pkg/apikey/apikey.go | 50 +++++++++++++++++++
5 files changed, 121 insertions(+), 5 deletions(-)
diff --git a/internal/http/services/accounts/accounts.go b/internal/http/services/accounts/accounts.go
index 03271c273e..87dee03433 100644
--- a/internal/http/services/accounts/accounts.go
+++ b/internal/http/services/accounts/accounts.go
@@ -32,6 +32,7 @@ import (
"github.com/cs3org/reva/internal/http/services/accounts/config"
"github.com/cs3org/reva/internal/http/services/accounts/data"
+ "github.com/cs3org/reva/pkg/apikey"
"github.com/cs3org/reva/pkg/rhttp/global"
)
@@ -106,11 +107,13 @@ func (s *svc) handleRequestEndpoints(w http.ResponseWriter, r *http.Request) {
}
endpoints := []Endpoint{
+ {config.EndpointGenerateAPIKey, http.MethodGet, s.handleGenerateAPIKey},
{config.EndpointList, http.MethodGet, s.handleList},
{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.EndpointAssignAPIKey, http.MethodPost, s.handleAssignAPIKey},
}
// The default response is an unknown endpoint (for the specified method)
@@ -148,6 +151,39 @@ func (s *svc) handleRequestEndpoints(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write(jsonData)
}
+func (s *svc) handleGenerateAPIKey(values url.Values, body []byte) (interface{}, error) {
+ email := values.Get("email")
+ flags := apikey.FlagDefault
+
+ if strings.EqualFold(values.Get("isScienceMesh"), "true") {
+ flags |= apikey.FlagScienceMesh
+ }
+
+ if len(email) == 0 {
+ return nil, errors.Errorf("no email provided")
+ }
+
+ apiKey, err := apikey.GenerateAPIKey(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) handleAssignAPIKey(values url.Values, body []byte) (interface{}, error) {
+ account, err := s.unmarshalRequestData(body)
+ if err != nil {
+ return nil, err
+ }
+
+ // Assign a new API key to the account through the account manager
+ if err := s.manager.AssignAPIKeyToAccount(account, apikey.FlagDefault); 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
}
@@ -200,9 +236,9 @@ func (s *svc) handleAuthorize(values url.Values, body []byte) (interface{}, erro
return nil, err
}
- if val, ok := values["status"]; ok && len(val) > 0 {
+ if val := values.Get("status"); len(val) > 0 {
var authorize bool
- switch strings.ToLower(val[0]) {
+ switch strings.ToLower(val) {
case "true":
authorize = true
diff --git a/internal/http/services/accounts/config/endpoints.go b/internal/http/services/accounts/config/endpoints.go
index f6077ca6c7..dd5cedbdcc 100644
--- a/internal/http/services/accounts/config/endpoints.go
+++ b/internal/http/services/accounts/config/endpoints.go
@@ -19,9 +19,12 @@
package config
const (
- // EndpointPanel is the endpoint of the web interface panel.
+ // 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"
+
// EndpointList is the endpoint path for listing all stored accounts.
EndpointList = "/list"
@@ -34,4 +37,6 @@ const (
// EndpointAuthorize is the endpoint path for account authorization
EndpointAuthorize = "/authorize"
+ // EndpointAssignsAPIKey is the endpoint path used for assigning an API key to an account.
+ EndpointAssignAPIKey = "/assign-api-key"
)
diff --git a/internal/http/services/accounts/manager.go b/internal/http/services/accounts/manager.go
index 0dcf4704b2..26825fb906 100644
--- a/internal/http/services/accounts/manager.go
+++ b/internal/http/services/accounts/manager.go
@@ -198,6 +198,31 @@ func (mngr *Manager) AuthorizeAccount(accountData *data.Account, authorized bool
return nil
}
+func (mngr *Manager) AssignAPIKeyToAccount(accountData *data.Account, flags int16) error {
+ mngr.mutex.Lock()
+ defer mngr.mutex.Unlock()
+
+ account := mngr.findAccountByEmail(accountData.Email)
+ if account == nil {
+ return errors.Errorf("no account with the specified email exists")
+ }
+
+ if len(account.Data.APIKey) > 0 {
+ return errors.Errorf("the account already has an API key assigned")
+ }
+
+ apiKey, err := apikey.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")
+ }
+ account.Data.APIKey = apiKey
+
+ mngr.storage.AccountUpdated(account)
+ mngr.writeAllAccounts()
+
+ 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()
diff --git a/internal/http/services/accounts/panel/template.go b/internal/http/services/accounts/panel/template.go
index c07af578e1..0e17591452 100644
--- a/internal/http/services/accounts/panel/template.go
+++ b/internal/http/services/accounts/panel/template.go
@@ -67,7 +67,7 @@ const panelTemplate = `
{{.FirstName}} {{.LastName}} (Joined: {{.DateCreated.Format "Jan 01, 2000 10:00"}}; Last modified: {{.DateModified.Format "Jan 01, 2000 10:00"}})
- API-Key:
+ API Key:
{{if .Data.APIKey}}
{{.Data.APIKey}}
{{else}}
@@ -83,7 +83,7 @@ const panelTemplate = `