mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-12 21:58:58 -04:00
Merge pull request #3478 from opencloud-eu/reva-bump-main-20260907
[full-ci] chore: bump reva to latest main
This commit is contained in:
84 files changed
+143
-10302
No files matched your search
@@ -63,7 +63,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.19.1
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067
|
||||
github.com/opencloud-eu/reva/v2 v2.49.0
|
||||
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774
|
||||
github.com/opensearch-project/opensearch-go/v4 v4.7.3
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
@@ -324,7 +324,6 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/pquerna/cachecontrol v0.2.0 // indirect
|
||||
github.com/prometheus/alertmanager v0.33.1 // indirect
|
||||
github.com/prometheus/common v0.70.1 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/prometheus/statsd_exporter v0.22.8 // indirect
|
||||
|
||||
@@ -942,8 +942,8 @@ github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89 h1:W1ms+l
|
||||
github.com/opencloud-eu/icap-client v0.0.0-20250930132611-28a2afe62d89/go.mod h1:vigJkNss1N2QEceCuNw/ullDehncuJNFB6mEnzfq9UI=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067 h1:UkNMKauyJAzY6RE6mmthz9bQZLYkbvBuApm7ZDCparE=
|
||||
github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067/go.mod h1:lTM8JeGblNpoMySTW7Lui2+c5TTLI95mwxtdUIHHrhU=
|
||||
github.com/opencloud-eu/reva/v2 v2.49.0 h1:AwECMDDth3NUaihZRf9bI9HNpWutvaRijOCvQyQfTT4=
|
||||
github.com/opencloud-eu/reva/v2 v2.49.0/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg=
|
||||
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774 h1:8N42Pwzyafdxq66zYP6kJ1CH/QBRpxovQXq/0Kt6bJY=
|
||||
github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774/go.mod h1:Frg+UWnVcSy+412UB3l2LcD0KY8ZNu1samimwkNywbg=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4 h1:l2oB/RctH+t8r7QBj5p8thfEHCM/jF35aAY3WQ3hADI=
|
||||
github.com/opencloud-eu/secure v0.0.0-20260312082735-b6f5cb2244e4/go.mod h1:BmF5hyM6tXczk3MpQkFf1hpKSRqCyhqcbiQtiAF7+40=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
@@ -1001,8 +1001,6 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om
|
||||
github.com/pquerna/cachecontrol v0.2.0 h1:vBXSNuE5MYP9IJ5kjsdo8uq+w41jSPgvba2DEnkRx9k=
|
||||
github.com/pquerna/cachecontrol v0.2.0/go.mod h1:NrUG3Z7Rdu85UNR3vm7SOsl1nFIeSiQnrHV5K9mBcUI=
|
||||
github.com/pquerna/otp v1.3.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/alertmanager v0.33.1 h1:PJHGvTdb8Q0ZEpJnWff120WxB2kxIScLa707AaIudTo=
|
||||
github.com/prometheus/alertmanager v0.33.1/go.mod h1:V06Uc8EZ5X5wLOJRGhtXx+EE2LgrinFIADbKWMVm1RY=
|
||||
github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
|
||||
|
||||
-1
@@ -34,7 +34,6 @@ import (
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/prometheus"
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/reverseproxy"
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh"
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/siteacc"
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/sysinfo"
|
||||
_ "github.com/opencloud-eu/reva/v2/internal/http/services/wellknown"
|
||||
// Add your own service here
|
||||
|
||||
Generated
Vendored
+46
-1
@@ -96,6 +96,30 @@ var (
|
||||
"width",
|
||||
"height",
|
||||
}
|
||||
videoKeys = []string{
|
||||
"audioBitsPerSample",
|
||||
"audioChannels",
|
||||
"audioFormat",
|
||||
"audioSamplesPerSecond",
|
||||
"bitrate",
|
||||
"duration",
|
||||
"fourCC",
|
||||
"frameRate",
|
||||
"height",
|
||||
"width",
|
||||
}
|
||||
motionPhotoKeys = []string{
|
||||
"presentationTimestampUs",
|
||||
"version",
|
||||
"videoSize",
|
||||
}
|
||||
livePhotoKeys = []string{
|
||||
"auto",
|
||||
"contentId",
|
||||
"stillImageTimeUs",
|
||||
"vitalityScore",
|
||||
"vitalityScoringVersion",
|
||||
}
|
||||
photoKeys = []string{
|
||||
"cameraMake",
|
||||
"cameraModel",
|
||||
@@ -887,6 +911,12 @@ func metadataKeys(pf XML) ([]string, []string) {
|
||||
metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.image", imageKeys)...)
|
||||
case "http://owncloud.org/ns/photo":
|
||||
metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.photo", photoKeys)...)
|
||||
case "http://owncloud.org/ns/video":
|
||||
metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.video", videoKeys)...)
|
||||
case "http://owncloud.org/ns/motion-photo":
|
||||
metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.motionPhoto", motionPhotoKeys)...)
|
||||
case "http://owncloud.org/ns/live-photo":
|
||||
metadataKeys = append(metadataKeys, metadataKeysWithPrefix("libre.graph.livePhoto", livePhotoKeys)...)
|
||||
default:
|
||||
metadataKeys = append(metadataKeys, key)
|
||||
}
|
||||
@@ -944,7 +974,7 @@ func requiresExplicitFetching(n *xml.Name) bool {
|
||||
}
|
||||
case net.NsOwncloud:
|
||||
switch n.Local {
|
||||
case "favorite", "share-types", "checksums", "size", "tags", "audio", "location", "image", "photo":
|
||||
case "favorite", "share-types", "checksums", "size", "tags", "audio", "location", "image", "photo", "video", "motion-photo", "live-photo":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1292,6 +1322,9 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p
|
||||
appendMetadataProp(k, "oc", "location", "libre.graph.location", locationKeys)
|
||||
appendMetadataProp(k, "oc", "image", "libre.graph.image", imageKeys)
|
||||
appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys)
|
||||
appendMetadataProp(k, "oc", "video", "libre.graph.video", videoKeys)
|
||||
appendMetadataProp(k, "oc", "motion-photo", "libre.graph.motionPhoto", motionPhotoKeys)
|
||||
appendMetadataProp(k, "oc", "live-photo", "libre.graph.livePhoto", livePhotoKeys)
|
||||
}
|
||||
|
||||
if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER {
|
||||
@@ -1579,6 +1612,18 @@ func mdToPropResponse(ctx context.Context, pf *XML, md *provider.ResourceInfo, p
|
||||
if k := md.GetArbitraryMetadata().GetMetadata(); k != nil {
|
||||
appendMetadataProp(k, "oc", "photo", "libre.graph.photo", photoKeys)
|
||||
}
|
||||
case "video":
|
||||
if k := md.GetArbitraryMetadata().GetMetadata(); k != nil {
|
||||
appendMetadataProp(k, "oc", "video", "libre.graph.video", videoKeys)
|
||||
}
|
||||
case "motion-photo":
|
||||
if k := md.GetArbitraryMetadata().GetMetadata(); k != nil {
|
||||
appendMetadataProp(k, "oc", "motion-photo", "libre.graph.motionPhoto", motionPhotoKeys)
|
||||
}
|
||||
case "live-photo":
|
||||
if k := md.GetArbitraryMetadata().GetMetadata(); k != nil {
|
||||
appendMetadataProp(k, "oc", "live-photo", "libre.graph.livePhoto", livePhotoKeys)
|
||||
}
|
||||
case "name":
|
||||
appendToOK(prop.Escaped("oc:name", md.Name))
|
||||
case "shareid":
|
||||
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package siteacc
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rhttp/global"
|
||||
)
|
||||
|
||||
func init() {
|
||||
global.Register(serviceName, New)
|
||||
}
|
||||
|
||||
type svc struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
siteacc *siteacc.SiteAccounts
|
||||
}
|
||||
|
||||
const (
|
||||
serviceName = "siteacc"
|
||||
)
|
||||
|
||||
// Close is called when this service is being stopped.
|
||||
func (s *svc) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prefix returns the main endpoint of this service.
|
||||
func (s *svc) Prefix() string {
|
||||
return s.conf.Prefix
|
||||
}
|
||||
|
||||
// Unprotected returns all endpoints that can be queried without prior authorization.
|
||||
func (s *svc) Unprotected() []string {
|
||||
return s.siteacc.GetPublicEndpoints()
|
||||
}
|
||||
|
||||
// Handler serves all HTTP requests.
|
||||
func (s *svc) Handler() http.Handler {
|
||||
return s.siteacc.RequestHandler()
|
||||
}
|
||||
|
||||
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)
|
||||
conf.Cleanup()
|
||||
|
||||
if conf.Webserver.URL == "" {
|
||||
return nil, errors.Errorf("no webserver URL specified")
|
||||
}
|
||||
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
func applyDefaultConfig(conf *config.Configuration) {
|
||||
if conf.Prefix == "" {
|
||||
conf.Prefix = serviceName
|
||||
}
|
||||
|
||||
if conf.Storage.Driver == "" {
|
||||
conf.Storage.Driver = "file"
|
||||
}
|
||||
|
||||
if conf.Mentix.DataEndpoint == "" {
|
||||
conf.Mentix.DataEndpoint = "/sites"
|
||||
}
|
||||
|
||||
if conf.Mentix.SiteRegistrationEndpoint == "" {
|
||||
conf.Mentix.SiteRegistrationEndpoint = "/sitereg"
|
||||
}
|
||||
|
||||
// Enforce a minimum session timeout of 1 minute (and default to 5 minutes)
|
||||
if conf.Webserver.SessionTimeout < 60 {
|
||||
conf.Webserver.SessionTimeout = 5 * 60
|
||||
}
|
||||
}
|
||||
|
||||
// New returns a new Site Accounts service.
|
||||
func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) {
|
||||
// Prepare the configuration
|
||||
conf, err := parseConfig(m)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create the site accounts instance
|
||||
siteacc, err := siteacc.New(conf, log)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error creating the site accounts service")
|
||||
}
|
||||
|
||||
// Create the service
|
||||
s := &svc{
|
||||
conf: conf,
|
||||
log: log,
|
||||
siteacc: siteacc,
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
+1
@@ -602,6 +602,7 @@ var mimeTypes = map[string]string{
|
||||
"nc": "application/x-netcdf",
|
||||
"ncx": "application/x-dtbncx+xml",
|
||||
"nef": "image/x-nikon-nef",
|
||||
"nrw": "image/x-nikon-nrw",
|
||||
"nfo": "text/x-nfo",
|
||||
"ngdat": "application/vnd.nokia.n-gage.data",
|
||||
"nitf": "application/vnd.nitf",
|
||||
|
||||
+33
-11
@@ -289,6 +289,34 @@ func (m *Manager) initialize(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.storage.MakeDirIfNotExist(ctx, "migrations")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
// A missing "storages" directory means nobody has ever used this metadata
|
||||
// tree, so there is nothing to migrate. Record all migrations as applied
|
||||
// before creating "storages": any instance that observes "storages" is then
|
||||
// guaranteed to also observe the state file, which keeps instances starting
|
||||
// in parallel from disagreeing about whether migrations have to run.
|
||||
_, err = m.storage.Stat(ctx, "storages")
|
||||
switch err.(type) {
|
||||
case nil:
|
||||
// existing deployment, leave the migration state untouched
|
||||
case errtypes.IsNotFound:
|
||||
if err = migration.MarkAllApplied(ctx, m.storage); err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
default:
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.storage.MakeDirIfNotExist(ctx, "storages")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
@@ -307,12 +335,6 @@ func (m *Manager) initialize(ctx context.Context) error {
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
err = m.storage.MakeDirIfNotExist(ctx, "migrations")
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return err
|
||||
}
|
||||
|
||||
span.SetStatus(codes.Ok, "initialized")
|
||||
return nil
|
||||
@@ -566,7 +588,7 @@ func (m *Manager) GetShare(ctx context.Context, ref *collaboration.ShareReferenc
|
||||
if s.ResourceId.SpaceId == s.ResourceId.OpaqueId {
|
||||
if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{
|
||||
SpaceOwner: s.GetOwner(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId},
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: storagespace.FormatStorageID(s.ResourceId.StorageId, s.ResourceId.SpaceId)},
|
||||
ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())),
|
||||
GranteeUserID: s.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: s.GetGrantee().GetGroupId(),
|
||||
@@ -780,7 +802,7 @@ func (m *Manager) listSharesByIDs(ctx context.Context, user *userv1beta1.User, f
|
||||
if s.ResourceId.SpaceId == s.ResourceId.OpaqueId {
|
||||
if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{
|
||||
SpaceOwner: s.GetOwner(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId},
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: storagespace.FormatStorageID(s.ResourceId.StorageId, s.ResourceId.SpaceId)},
|
||||
ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())),
|
||||
GranteeUserID: s.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: s.GetGrantee().GetGroupId(),
|
||||
@@ -911,7 +933,7 @@ func (m *Manager) listCreatedShares(ctx context.Context, user *userv1beta1.User,
|
||||
if s.ResourceId.SpaceId == s.ResourceId.OpaqueId {
|
||||
if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{
|
||||
SpaceOwner: s.GetOwner(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId},
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: storagespace.FormatStorageID(s.ResourceId.StorageId, s.ResourceId.SpaceId)},
|
||||
ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())),
|
||||
GranteeUserID: s.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: s.GetGrantee().GetGroupId(),
|
||||
@@ -1096,7 +1118,7 @@ func (m *Manager) ListReceivedShares(ctx context.Context, filters []*collaborati
|
||||
if s.ResourceId.SpaceId == s.ResourceId.OpaqueId {
|
||||
if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{
|
||||
SpaceOwner: s.GetOwner(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId},
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: storagespace.FormatStorageID(s.ResourceId.StorageId, s.ResourceId.SpaceId)},
|
||||
ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())),
|
||||
GranteeUserID: s.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: s.GetGrantee().GetGroupId(),
|
||||
@@ -1212,7 +1234,7 @@ func (m *Manager) getReceived(ctx context.Context, ref *collaboration.ShareRefer
|
||||
if s.ResourceId.SpaceId == s.ResourceId.OpaqueId {
|
||||
if err := events.Publish(ctx, m.eventStream, events.SpaceMembershipExpired{
|
||||
SpaceOwner: s.GetOwner(),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: s.ResourceId.StorageId + "$" + s.ResourceId.SpaceId},
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: storagespace.FormatStorageID(s.ResourceId.StorageId, s.ResourceId.SpaceId)},
|
||||
ExpiredAt: time.Unix(int64(s.GetExpiration().GetSeconds()), int64(s.GetExpiration().GetNanos())),
|
||||
GranteeUserID: s.GetGrantee().GetUserId(),
|
||||
GranteeGroupID: s.GetGrantee().GetGroupId(),
|
||||
|
||||
Generated
Vendored
+34
@@ -139,6 +139,40 @@ func New(logger zerolog.Logger,
|
||||
}
|
||||
}
|
||||
|
||||
// LatestVersion returns the version of the most recent registered migration.
|
||||
func LatestVersion() int {
|
||||
version := 0
|
||||
for _, mig := range migrations {
|
||||
if mig.Version() > version {
|
||||
version = mig.Version()
|
||||
}
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// MarkAllApplied records every registered migration as applied without running
|
||||
// any of them. It is meant for fresh deployments, where there is no data to
|
||||
// migrate and running the migrations would needlessly block write operations
|
||||
// while depending on services that may not be up yet.
|
||||
//
|
||||
// The state file is created atomically and an existing state file is never
|
||||
// overwritten, so instances starting in parallel converge on the same state.
|
||||
func MarkAllApplied(ctx context.Context, storage metadata.Storage) error {
|
||||
data, err := json.Marshal(persistedState{Version: LatestVersion()})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = storage.Upload(ctx, metadata.UploadRequest{
|
||||
Path: stateFile,
|
||||
Content: data,
|
||||
IfNoneMatch: []string{"*"},
|
||||
})
|
||||
if err != nil && !isConflict(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// acquireLock tries to atomically create the lock file, blocking until the lock
|
||||
// is obtained. It returns the etag of the lock file on success. It retries
|
||||
// indefinitely until ctx is cancelled. A lock whose timestamp is older than
|
||||
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package contact
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the contact form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Contact Form"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Contact the ScienceMesh administration!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package contact
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("subject") == "") {
|
||||
setState(STATE_ERROR, "Please enter a subject.", "form", "subject", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("message") == "") {
|
||||
setState(STATE_ERROR, "Please enter a message.", "form", "message", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Sending message... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your message was successfully sent! A copy of the message has been sent to your email address.");
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to send your message:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"subject": formData.getTrimmed("subject"),
|
||||
"message": formData.getTrimmed("message")
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Contact the ScienceMesh administration using the form below.</p>
|
||||
<p style="margin-bottom: 0em;">Please include as much information as possible in your request, especially:</p>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>The site your request refers to (if not obvious from your account information)</li>
|
||||
<li>Your role within the ScienceMesh site (e.g., administrator, operational team member, etc.)</li>
|
||||
<li>Any specific reasons for your request</li>
|
||||
<li>Anything else that might help to process your request</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('contact'); return false;">
|
||||
<div style="grid-row: 1;"><label for="subject">Subject: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2; grid-column: 1 / span 2;"><input type="text" id="subject" name="subject" {{if .Params.Subject}}value="{{.Params.Subject}}" readonly{{end}}/></div>
|
||||
|
||||
<div style="grid-row: 3;"><label for="message">Message: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4; grid-column: 1 / span 2;">
|
||||
<textarea rows="10" id="message" name="message" style="box-sizing: border-box; width: 100%;" {{if .Params.Info}}placeholder="{{.Params.Info}}"{{end}}>{{if .Params.Message}}{{.Params.Message}}{{end}}</textarea>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 5; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 5; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Send</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package edit
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Edit your ScienceMesh Site Administrator Account!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-158
@@ -1,158 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package edit
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("fname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("lname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("role") == "") {
|
||||
setState(STATE_ERROR, "Please specify your role within your site.", "form", "role", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password") != "") {
|
||||
if (formData.get("password2") == "") {
|
||||
setState(STATE_ERROR, "Please confirm your new password.", "form", "password2", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password") != formData.get("password2")) {
|
||||
setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Updating account... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your account was successfully updated!", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to update your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"title": formData.getTrimmed("title"),
|
||||
"firstName": formData.getTrimmed("fname"),
|
||||
"lastName": formData.getTrimmed("lname"),
|
||||
"role": formData.getTrimmed("role"),
|
||||
"phoneNumber": formData.getTrimmed("phone"),
|
||||
"password": {
|
||||
"value": formData.get("password")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Edit your ScienceMesh Site Administrator Account information below.</p>
|
||||
<p>Please note that you cannot modify your email address using this form.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('update?invoker=user'); return false;">
|
||||
<div style="grid-row: 1;"><label for="title">Title: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;">
|
||||
<select id="title" name="title">
|
||||
{{$title := .Account.Title}}
|
||||
{{range .Titles}}
|
||||
<option value="{{.}}" {{if eq . $title}}selected{{end}}>{{.}}.</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3;"><label for="fname">First name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4;"><input type="text" id="fname" name="fname" value="{{.Account.FirstName}}"/></div>
|
||||
<div style="grid-row: 3;"><label for="lname">Last name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 4;"><input type="text" id="lname" name="lname" value="{{.Account.LastName}}"/></div>
|
||||
|
||||
<div style="grid-row: 5;"><label for="role">Role: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 6;"><input type="text" id="role" name="role" placeholder="Site administrator" value="{{.Account.Role}}"/></div>
|
||||
<div style="grid-row: 5;"><label for="phone">Phone number:</label></div>
|
||||
<div style="grid-row: 6;"><input type="text" id="phone" name="phone" placeholder="+49 030 123456" value="{{.Account.PhoneNumber}}"/></div>
|
||||
|
||||
<div style="grid-row: 7;"> </div>
|
||||
|
||||
<div style="grid-row: 8; grid-column: 1 / span 2;">If you want to change your password, fill out the fields below. Otherwise, leave them empty to keep your current one.</div>
|
||||
<div style="grid-row: 9;"><label for="password">New password:</label></div>
|
||||
<div style="grid-row: 10;"><input type="password" id="password" name="password" autocomplete="new-password"/></div>
|
||||
<div style="grid-row: 9"><label for="password2">Confirm new password:</label></div>
|
||||
<div style="grid-row: 10;"><input type="password" id="password2" name="password2" autocomplete="new-password"/></div>
|
||||
|
||||
<div style="grid-row: 11; font-style: italic; font-size: 0.8em;">
|
||||
The password must fulfil the following criteria:
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Must be at least 8 characters long</li>
|
||||
<li>Must contain at least 1 lowercase letter</li>
|
||||
<li>Must contain at least 1 uppercase letter</li>
|
||||
<li>Must contain at least 1 digit</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 12; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 12; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package login
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the login form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account Login"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Login to your ScienceMesh Site Administrator Account!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package login
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData, requirePassword = true) {
|
||||
if (formData.getTrimmed("email") == "") {
|
||||
setState(STATE_ERROR, "Please enter your email address.", "form", "email", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (requirePassword) {
|
||||
if (formData.get("password") == "") {
|
||||
setState(STATE_ERROR, "Please enter your password.", "form", "password", true);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Logging in... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your login was successful! Redirecting...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=manage");
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to login your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.getTrimmed("email"),
|
||||
"password": {
|
||||
"value": formData.get("password")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
|
||||
function handleResetPassword() {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData, false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Resetting password... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/reset-password");
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your password was successfully reset! Please check your inbox for your new password.", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to reset your password:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.get("email")
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Login to your ScienceMesh Site Administrator Account using the form below.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('login'); return false;">
|
||||
<div style="grid-row: 1;"><label for="email">Email address: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="text" id="email" name="email" placeholder="me@example.com"/></div>
|
||||
<div style="grid-row: 1;"><label for="password">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="password" id="password" name="password"/></div>
|
||||
<div style="grid-row: 3; grid-column: 2; font-style: italic; font-size: 0.8em;">
|
||||
Forgot your password? Click <a href="#" onClick="handleResetPassword();">here</a> to reset it.
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 4; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 4; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Don't' have an account yet? Register <a href="{{getServerAddress}}/account/?path=register">here</a>.</p>
|
||||
</div>
|
||||
`
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manage
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the mangement form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Welcome to your ScienceMesh Site Administrator Account!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-132
@@ -1,132 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manage
|
||||
|
||||
const tplJavaScript = `
|
||||
function handleAccountSettings() {
|
||||
setState(STATE_STATUS, "Redirecting to the account settings...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=settings");
|
||||
}
|
||||
|
||||
function handleEditAccount() {
|
||||
setState(STATE_STATUS, "Redirecting to the account editor...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=edit");
|
||||
}
|
||||
|
||||
function handleSiteSettings() {
|
||||
setState(STATE_STATUS, "Redirecting to the site settings...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=site");
|
||||
}
|
||||
|
||||
function handleRequestAccess(scope) {
|
||||
setState(STATE_STATUS, "Redirecting to the contact form...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=contact&subject=" + encodeURIComponent("Request " + scope + " access"));
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", "{{getServerAddress}}/logout");
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
setState(STATE_STATUS, "Logging out...");
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Done! Redirecting...");
|
||||
window.location.replace("{{getServerAddress}}/account/?path=login");
|
||||
} else {
|
||||
setState(STATE_ERROR, "An error occurred while logging out: " + this.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
xhr.send();
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
button {
|
||||
min-width: 170px;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p><strong>Hello {{.Account.FirstName}} {{.Account.LastName}},</strong></p>
|
||||
<p>On this page, you can manage your ScienceMesh Site Administrator Account. This includes editing your personal information, requesting access to the GOCDB and more.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<strong>Personal information:</strong>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Name: <em>{{.Account.Title}}. {{.Account.FirstName}} {{.Account.LastName}}</em></li>
|
||||
<li>Email: <em><a href="mailto:{{.Account.Email}}">{{.Account.Email}}</a></em></li>
|
||||
<li>ScienceMesh Site: <em>{{getSiteName .Account.Site false}} ({{getSiteName .Account.Site true}})</em></li>
|
||||
<li>Role: <em>{{.Account.Role}}</em></li>
|
||||
{{if .Account.PhoneNumber}}
|
||||
<li>Phone: <em>{{.Account.PhoneNumber}}</em></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Account data:</strong>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Site access: <em>{{if .Account.Data.SiteAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
<li>GOCDB access: <em>{{if .Account.Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box" style="width: 100%;">
|
||||
<div>
|
||||
<button type="button" onClick="handleAccountSettings();">Account settings</button>
|
||||
<button type="button" onClick="handleEditAccount();">Edit account</button>
|
||||
<span style="width: 25px;"> </span>
|
||||
|
||||
{{if .Account.Data.SiteAccess}}
|
||||
<button type="button" onClick="handleSiteSettings();">Site settings</button>
|
||||
<span style="width: 25px;"> </span>
|
||||
{{end}}
|
||||
|
||||
<button type="button" onClick="handleLogout();" style="float: right;">Logout</button>
|
||||
</div>
|
||||
<div style="margin-top: 0.5em;">
|
||||
<button type="button" onClick="handleRequestAccess('Site');" {{if .Account.Data.SiteAccess}}disabled{{end}}>Request Site access</button>
|
||||
<button type="button" onClick="handleRequestAccess('GOCDB');" {{if .Account.Data.GOCDBAccess}}disabled{{end}}>Request GOCDB access</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div style="font-size: 90%; margin-top: 1em;">
|
||||
<div>
|
||||
<div>Notes:</div>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>The <em>Site access</em> allows you to access and modify the global configuration of your site.</li>
|
||||
<li>The <em>GOCDB access</em> allows you to log into the central database where all site metadata is stored.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div>Quick links:</div>
|
||||
<ul style="margin-top: 0em;">
|
||||
<li><a href="https://gocdb.sciencemesh.uni-muenster.de" target="_blank">Central Database (GOCDB)</a></li>
|
||||
<li><a href="https://developer.sciencemesh.io/docs/technical-documentation/central-database/" target="_blank">Central Database documentation</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package account
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/contact"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/edit"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/login"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/manage"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/registration"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/settings"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/account/site"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// Panel represents the account panel.
|
||||
type Panel struct {
|
||||
html.PanelProvider
|
||||
|
||||
conf *config.Configuration
|
||||
|
||||
htmlPanel *html.Panel
|
||||
}
|
||||
|
||||
const (
|
||||
templateLogin = "login"
|
||||
templateManage = "manage"
|
||||
templateSettings = "settings"
|
||||
templateEdit = "edit"
|
||||
templateSite = "site"
|
||||
templateContact = "contact"
|
||||
templateRegistration = "register"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
panel.conf = conf
|
||||
|
||||
// Create the internal HTML panel
|
||||
htmlPanel, err := html.NewPanel("account-panel", panel, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create the account panel")
|
||||
}
|
||||
panel.htmlPanel = htmlPanel
|
||||
|
||||
// Add all templates
|
||||
if err := panel.htmlPanel.AddTemplate(templateLogin, &login.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the login template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateManage, &manage.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the account management template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateSettings, &settings.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the account settings template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateEdit, &edit.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the account editing template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateSite, &site.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the site template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateContact, &contact.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the contact template")
|
||||
}
|
||||
|
||||
if err := panel.htmlPanel.AddTemplate(templateRegistration, ®istration.PanelTemplate{}); err != nil {
|
||||
return errors.Wrap(err, "unable to create the registration template")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
func (panel *Panel) GetActiveTemplate(session *html.Session, path string) string {
|
||||
validPaths := []string{templateLogin, templateManage, templateSettings, templateEdit, templateSite, templateContact, templateRegistration}
|
||||
template := templateLogin
|
||||
|
||||
// Only allow valid template paths; redirect to the login page otherwise
|
||||
for _, valid := range validPaths {
|
||||
if valid == path {
|
||||
template = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
// PreExecute is called before the actual template is being executed.
|
||||
func (panel *Panel) PreExecute(session *html.Session, path string, w http.ResponseWriter, r *http.Request) (html.ExecutionResult, error) {
|
||||
protectedPaths := []string{templateManage, templateSettings, templateEdit, templateSite, templateContact}
|
||||
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
switch path {
|
||||
case templateSite:
|
||||
// If the logged in user doesn't have site access, redirect him back to the main account page
|
||||
if !user.Account.Data.SiteAccess {
|
||||
return panel.redirect(templateManage, w, r), nil
|
||||
}
|
||||
|
||||
case templateLogin:
|
||||
case templateRegistration:
|
||||
// If a user is logged in and tries to login or register again, redirect to the main account page
|
||||
return panel.redirect(templateManage, w, r), nil
|
||||
}
|
||||
} else {
|
||||
// If no user is logged in, redirect protected paths to the login page
|
||||
for _, protected := range protectedPaths {
|
||||
if protected == path {
|
||||
return panel.redirect(templateLogin, w, r), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return html.ContinueExecution, nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the form and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
dataProvider := func(*html.Session) interface{} {
|
||||
flatValues := make(map[string]string, len(r.URL.Query()))
|
||||
c := cases.Title(language.Und)
|
||||
for k, v := range r.URL.Query() {
|
||||
flatValues[c.String(k)] = v[0]
|
||||
}
|
||||
|
||||
availSites, err := data.QueryAvailableSites(panel.conf.Mentix.URL, panel.conf.Mentix.DataEndpoint)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to query available sites")
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
Site *data.Site
|
||||
Account *data.Account
|
||||
Params map[string]string
|
||||
|
||||
Titles []string
|
||||
Sites []data.SiteInformation
|
||||
}
|
||||
|
||||
tplData := TemplateData{
|
||||
Site: nil,
|
||||
Account: nil,
|
||||
Params: flatValues,
|
||||
Titles: []string{"Mr", "Mrs", "Ms", "Prof", "Dr"},
|
||||
Sites: availSites,
|
||||
}
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
tplData.Site = panel.cloneUserSite(user.Site)
|
||||
tplData.Account = user.Account
|
||||
}
|
||||
return tplData
|
||||
}
|
||||
return panel.htmlPanel.Execute(w, r, session, dataProvider)
|
||||
}
|
||||
|
||||
func (panel *Panel) redirect(path string, w http.ResponseWriter, r *http.Request) html.ExecutionResult {
|
||||
// Check if the original (full) URI path is stored in the request header; if not, use the request URI to get the path
|
||||
fullPath := r.Header.Get("X-Replaced-Path")
|
||||
if fullPath == "" {
|
||||
uri, _ := url.Parse(r.RequestURI)
|
||||
fullPath = uri.Path
|
||||
}
|
||||
|
||||
// Modify the original request URL by replacing the path parameter
|
||||
newURL, _ := url.Parse(fullPath)
|
||||
params := newURL.Query()
|
||||
params.Del("path")
|
||||
params.Add("path", path)
|
||||
newURL.RawQuery = params.Encode()
|
||||
http.Redirect(w, r, newURL.String(), http.StatusFound)
|
||||
return html.AbortExecution
|
||||
}
|
||||
|
||||
func (panel *Panel) cloneUserSite(site *data.Site) *data.Site {
|
||||
// Clone the user's site and decrypt the credentials for the panel
|
||||
siteClone := site.Clone(true)
|
||||
id, secret, err := site.Config.TestClientCredentials.Get(panel.conf.Security.CredentialsPassphrase)
|
||||
if err == nil {
|
||||
siteClone.Config.TestClientCredentials.ID = id
|
||||
siteClone.Config.TestClientCredentials.Secret = secret
|
||||
}
|
||||
return siteClone
|
||||
}
|
||||
|
||||
// NewPanel creates a new account panel.
|
||||
func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
form := &Panel{}
|
||||
if err := form.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the account panel")
|
||||
}
|
||||
return form, nil
|
||||
}
|
||||
Generated
Vendored
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registration
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the registration form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account Registration"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Welcome to the ScienceMesh Site Administrator Account Registration!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
Generated
Vendored
-186
@@ -1,186 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package registration
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("email") == "") {
|
||||
setState(STATE_ERROR, "Please specify your email address.", "form", "email", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("fname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your first name.", "form", "fname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("lname") == "") {
|
||||
setState(STATE_ERROR, "Please specify your last name.", "form", "lname", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("site") == "") {
|
||||
setState(STATE_ERROR, "Please select your ScienceMesh site.", "form", "site", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.getTrimmed("role") == "") {
|
||||
setState(STATE_ERROR, "Please specify your role within your site.", "form", "role", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password") == "") {
|
||||
setState(STATE_ERROR, "Please set a password.", "form", "password", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password2") == "") {
|
||||
setState(STATE_ERROR, "Please confirm your password.", "form", "password2", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("password") != formData.get("password2")) {
|
||||
setState(STATE_ERROR, "The entered passwords do not match.", "form", "password2", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Sending registration... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your registration was successful! Please check your inbox for a confirmation email. You will be redirected to the login page in a few seconds (if not, click <a href='{{getServerAddress}}/account/?path=login'>here</a>).");
|
||||
window.setTimeout(function() {
|
||||
window.location.replace("{{getServerAddress}}/account/?path=login");
|
||||
}, 3000);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to register your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": formData.getTrimmed("email"),
|
||||
"title": formData.getTrimmed("title"),
|
||||
"firstName": formData.getTrimmed("fname"),
|
||||
"lastName": formData.getTrimmed("lname"),
|
||||
"site": formData.getTrimmed("site"),
|
||||
"role": formData.getTrimmed("role"),
|
||||
"phoneNumber": formData.getTrimmed("phone"),
|
||||
"password": {
|
||||
"value": formData.get("password")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Fill out the form below to register for a ScienceMesh Site Administrator account. A confirmation email will be sent to you shortly after registration.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('create'); return false;">
|
||||
<div style="grid-row: 1;"><label for="email">Email address: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;"><input type="text" id="email" name="email" placeholder="me@example.com"/></div>
|
||||
<div style="grid-row: 1;"><label for="site">ScienceMesh Site: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 2;">
|
||||
<select id="site" name="site">
|
||||
{{range .Sites}}
|
||||
<option value="{{.ID}}">{{.Name}} | {{.FullName}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3;"> </div>
|
||||
|
||||
<div style="grid-row: 4;"><label for="title">Title: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 5;">
|
||||
<select id="title" name="title">
|
||||
{{range .Titles}}
|
||||
<option value="{{.}}">{{.}}.</option>
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 6;"><label for="fname">First name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 7;"><input type="text" id="fname" name="fname"/></div>
|
||||
<div style="grid-row: 6;"><label for="lname">Last name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 7;"><input type="text" id="lname" name="lname"/></div>
|
||||
|
||||
<div style="grid-row: 8;"><label for="role">Role: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 9;"><input type="text" id="role" name="role" placeholder="Site administrator"/></div>
|
||||
<div style="grid-row: 8;"><label for="phone">Phone number:</label></div>
|
||||
<div style="grid-row: 9;"><input type="text" id="phone" name="phone" placeholder="+49 030 123456"/></div>
|
||||
|
||||
<div style="grid-row: 10;"> </div>
|
||||
|
||||
<div style="grid-row: 11;"><label for="password">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 12;"><input type="password" id="password" name="password"/></div>
|
||||
<div style="grid-row: 11;"><label for="password2">Confirm password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 12;"><input type="password" id="password2" name="password2"/></div>
|
||||
|
||||
<div style="grid-row: 13; font-style: italic; font-size: 0.8em;">
|
||||
The password must fulfil the following criteria:
|
||||
<ul style="margin-top: 0em;">
|
||||
<li>Must be at least 8 characters long</li>
|
||||
<li>Must contain at least 1 lowercase letter</li>
|
||||
<li>Must contain at least 1 uppercase letter</li>
|
||||
<li>Must contain at least 1 digit</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 14; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 14; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Register</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Already have an account? Login <a href="{{getServerAddress}}/account/?path=login">here</a>.</p>
|
||||
</div>
|
||||
`
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package settings
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Account"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Configure your ScienceMesh Site Administrator Account!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-93
@@ -1,93 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package settings
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Configuring account... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your account was successfully configured!", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to configure your account:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"settings": {
|
||||
"receiveAlerts": (formData.get("rcvAlerts") === "on")
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Configure your ScienceMesh Site Administrator Account below.</p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('configure?invoker=user'); return false;">
|
||||
<div style="grid-row: 1; grid-column: 1 / span 2;">
|
||||
<h3>Notification settings</h3>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 2; grid-column: 1 / span 2;">
|
||||
<input type="checkbox" id="rcvAlerts" name="rcvAlerts" value="on" checked disabled/>
|
||||
<label for="rcvAlerts" style="font-weight: normal;">Receive email notifications about site alerts <em>(mandatory; always on)</em></label>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 3; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
-51
@@ -1,51 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package site
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
|
||||
// PanelTemplate is the content provider for the edit form.
|
||||
type PanelTemplate struct {
|
||||
html.ContentProvider
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the panel.
|
||||
func (template *PanelTemplate) GetTitle() string {
|
||||
return "ScienceMesh Site Configuration"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
func (template *PanelTemplate) GetCaption() string {
|
||||
return "Configure your ScienceMesh Site!"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (template *PanelTemplate) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (template *PanelTemplate) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (template *PanelTemplate) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
-117
@@ -1,117 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package site
|
||||
|
||||
const tplJavaScript = `
|
||||
function verifyForm(formData) {
|
||||
if (formData.getTrimmed("clientID") == "") {
|
||||
setState(STATE_ERROR, "Please enter the name of the test user.", "form", "clientID", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (formData.get("secret") == "") {
|
||||
setState(STATE_ERROR, "Please enter the password of the test user.", "form", "secret", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleAction(action) {
|
||||
const formData = new FormData(document.querySelector("form"));
|
||||
if (!verifyForm(formData)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(STATE_STATUS, "Configuring site... this should only take a moment.", "form", null, false);
|
||||
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Your site was successfully configured!", "form", null, true);
|
||||
} else {
|
||||
var resp = JSON.parse(this.responseText);
|
||||
setState(STATE_ERROR, "An error occurred while trying to configure your site:<br><em>" + resp.error + "</em>", "form", null, true);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"config": {
|
||||
"testClientCredentials": {
|
||||
"id": formData.getTrimmed("clientID"),
|
||||
"secret": formData.get("secret")
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: arial !important;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.mandatory {
|
||||
color: red;
|
||||
font-weight: bold;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div>
|
||||
<p>Configure your ScienceMesh Site below. <em>These settings affect your entire site and not just your account.</em></p>
|
||||
</div>
|
||||
<div> </div>
|
||||
<div>
|
||||
<form id="form" method="POST" class="box container-inline" style="width: 100%;" onSubmit="handleAction('site-configure?invoker=user'); return false;">
|
||||
<div style="grid-row: 1; grid-column: 1 / span 2;">
|
||||
<h3>Test user settings</h3>
|
||||
<p>In order to perform automated tests on your site, a test user has to be configured below. Please note that the user <em>has to exist in your Reva instance</em>! If you do not have a user for automated tests in your instance yet, create one first.</p>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
<div style="grid-row: 2;"><label for="clientID">User name: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 3;"><input type="text" id="clientID" name="clientID" placeholder="User name" value="{{.Site.Config.TestClientCredentials.ID}}"/></div>
|
||||
<div style="grid-row: 2;"><label for="secret">Password: <span class="mandatory">*</span></label></div>
|
||||
<div style="grid-row: 3;"><input type="password" id="secret" name="secret" placeholder="Password" value="{{.Site.Config.TestClientCredentials.Secret}}"/></div>
|
||||
|
||||
<div style="grid-row: 4;"> </div>
|
||||
|
||||
<div style="grid-row: 5; align-self: center;">
|
||||
Fields marked with <span class="mandatory">*</span> are mandatory.
|
||||
</div>
|
||||
<div style="grid-row: 5; grid-column: 2; text-align: right;">
|
||||
<button type="reset">Reset</button>
|
||||
<button type="submit" style="font-weight: bold;">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<p>Go <a href="{{getServerAddress}}/account/?path=manage">back</a> to the main account page.</p>
|
||||
</div>
|
||||
`
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Panel represents the web interface panel of the accounts service administration.
|
||||
type Panel struct {
|
||||
html.PanelProvider
|
||||
html.ContentProvider
|
||||
|
||||
htmlPanel *html.Panel
|
||||
}
|
||||
|
||||
const (
|
||||
templateMain = "main"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
// Create the internal HTML panel
|
||||
htmlPanel, err := html.NewPanel("admin-panel", panel, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create the administration panel")
|
||||
}
|
||||
panel.htmlPanel = htmlPanel
|
||||
|
||||
// Add all templates
|
||||
if err := panel.htmlPanel.AddTemplate(templateMain, panel); err != nil {
|
||||
return errors.Wrap(err, "unable to create the main template")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
func (panel *Panel) GetActiveTemplate(*html.Session, string) string {
|
||||
return templateMain
|
||||
}
|
||||
|
||||
// GetTitle returns the title of the htmlPanel.
|
||||
func (panel *Panel) GetTitle() string {
|
||||
return "ScienceMesh Site Administrator Accounts Panel"
|
||||
}
|
||||
|
||||
// GetCaption returns the caption which is displayed on the htmlPanel.
|
||||
func (panel *Panel) GetCaption() string {
|
||||
return "ScienceMesh Site Administrator Accounts ({{.Accounts | len}})"
|
||||
}
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
func (panel *Panel) GetContentJavaScript() string {
|
||||
return tplJavaScript
|
||||
}
|
||||
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
func (panel *Panel) GetContentStyleSheet() string {
|
||||
return tplStyleSheet
|
||||
}
|
||||
|
||||
// GetContentBody delivers the actual body content.
|
||||
func (panel *Panel) GetContentBody() string {
|
||||
return tplBody
|
||||
}
|
||||
|
||||
// PreExecute is called before the actual template is being executed.
|
||||
func (panel *Panel) PreExecute(*html.Session, string, http.ResponseWriter, *http.Request) (html.ExecutionResult, error) {
|
||||
return html.ContinueExecution, nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the htmlPanel and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *html.Session, accounts *data.Accounts) error {
|
||||
dataProvider := func(*html.Session) interface{} {
|
||||
type TemplateData struct {
|
||||
Accounts *data.Accounts
|
||||
}
|
||||
|
||||
return TemplateData{
|
||||
Accounts: accounts,
|
||||
}
|
||||
}
|
||||
return panel.htmlPanel.Execute(w, r, session, dataProvider)
|
||||
}
|
||||
|
||||
// NewPanel creates a new administration panel.
|
||||
func NewPanel(conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
panel := &Panel{}
|
||||
if err := panel.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the administration panel")
|
||||
}
|
||||
return panel, nil
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package admin
|
||||
|
||||
const tplJavaScript = `
|
||||
function handleAction(action, email) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("POST", "{{getServerAddress}}/" + action);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
|
||||
|
||||
setState(STATE_STATUS, "Performing request...");
|
||||
|
||||
xhr.onload = function() {
|
||||
if (this.status == 200) {
|
||||
setState(STATE_SUCCESS, "Done! Reloading...");
|
||||
location.reload();
|
||||
} else {
|
||||
setState(STATE_ERROR, "An error occurred while performing the request: " + this.responseText);
|
||||
}
|
||||
}
|
||||
|
||||
var postData = {
|
||||
"email": email,
|
||||
};
|
||||
|
||||
xhr.send(JSON.stringify(postData));
|
||||
}
|
||||
`
|
||||
|
||||
const tplStyleSheet = `
|
||||
html * {
|
||||
font-family: monospace !important;
|
||||
}
|
||||
`
|
||||
|
||||
const tplBody = `
|
||||
<div style="font-size: 14px;">
|
||||
<ul>
|
||||
{{range .Accounts}}
|
||||
<li>
|
||||
<div>
|
||||
<div>
|
||||
<strong>{{.Email}}</strong><br>
|
||||
{{.Title}}. {{.FirstName}} {{.LastName}} <em>(Joined: {{.DateCreated.Format "Jan 02, 2006 15:04"}}; Last modified: {{.DateModified.Format "Jan 02, 2006 15:04"}})</em>
|
||||
</div>
|
||||
<div>
|
||||
<ul style="padding-left: 1em;">
|
||||
<li>ScienceMesh Site: {{getSiteName .Site false}} ({{getSiteName .Site true}})</li>
|
||||
<li>Role: {{.Role}}</li>
|
||||
<li>Phone: {{.PhoneNumber}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
<div>
|
||||
<strong>Account data:</strong>
|
||||
<ul style="padding-left: 1em; padding-top: 0em;">
|
||||
<li>Site access: <em>{{if .Data.SiteAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
<li>GOCDB access: <em>{{if .Data.GOCDBAccess}}Granted{{else}}Not granted{{end}}</em></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div> </div>
|
||||
|
||||
<div>
|
||||
<form method="POST" style="width: 100%;">
|
||||
{{if .Data.SiteAccess}}
|
||||
<button type="button" onClick="handleAction('grant-site-access?status=false', '{{.Email}}');">Revoke Site access</button>
|
||||
{{else}}
|
||||
<button type="button" onClick="handleAction('grant-site-access?status=true', '{{.Email}}');">Grant Site access</button>
|
||||
{{end}}
|
||||
|
||||
{{if .Data.GOCDBAccess}}
|
||||
<button type="button" onClick="handleAction('grant-gocdb-access?status=false', '{{.Email}}');">Revoke GOCDB access</button>
|
||||
{{else}}
|
||||
<button type="button" onClick="handleAction('grant-gocdb-access?status=true', '{{.Email}}');">Grant GOCDB access</button>
|
||||
{{end}}
|
||||
|
||||
<span style="width: 25px;"> </span>
|
||||
<button type="button" onClick="handleAction('remove', '{{.Email}}');" style="float: right;">Remove</button>
|
||||
</form>
|
||||
</div>
|
||||
<hr>
|
||||
</li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
`
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package alerting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Dispatcher is used to dispatch Prometheus alerts via email.
|
||||
type Dispatcher struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
smtp *smtpclient.SMTPCredentials
|
||||
}
|
||||
|
||||
func (dispatcher *Dispatcher) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
dispatcher.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
dispatcher.log = log
|
||||
|
||||
// Create the SMTP client
|
||||
if conf.Email.SMTP != nil {
|
||||
dispatcher.smtp = smtpclient.NewSMTPCredentials(conf.Email.SMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DispatchAlerts sends the provided alert(s) via email to the appropriate recipients.
|
||||
func (dispatcher *Dispatcher) DispatchAlerts(alerts *template.Data, accounts data.Accounts) error {
|
||||
for _, alert := range alerts.Alerts {
|
||||
siteID, ok := alert.Labels["site_id"]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Dispatch the alert to all accounts configured to receive it
|
||||
for _, account := range accounts {
|
||||
if strings.EqualFold(account.Site, siteID) /* && account.Settings.ReceiveAlerts */ { // TODO: Uncomment if alert notifications aren't mandatory anymore
|
||||
if err := dispatcher.dispatchAlert(alert, account); err != nil {
|
||||
// Log errors only
|
||||
dispatcher.log.Err(err).Str("id", alert.Fingerprint).Str("recipient", account.Email).Msg("unable to dispatch alert to user")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dispatch the alert to the global receiver (if set)
|
||||
if dispatcher.conf.Email.NotificationsMail != "" {
|
||||
globalAccount := data.Account{ // On-the-fly account representing the "global alerts receiver"
|
||||
Email: dispatcher.conf.Email.NotificationsMail,
|
||||
FirstName: "ScienceMesh",
|
||||
LastName: "Global Alerts receiver",
|
||||
Site: "Global",
|
||||
Role: "Alerts receiver",
|
||||
Settings: data.AccountSettings{
|
||||
ReceiveAlerts: true,
|
||||
},
|
||||
}
|
||||
if err := dispatcher.dispatchAlert(alert, &globalAccount); err != nil {
|
||||
dispatcher.log.Err(err).Str("id", alert.Fingerprint).Str("recipient", globalAccount.Email).Msg("unable to dispatch alert to global alerts receiver")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dispatcher *Dispatcher) dispatchAlert(alert template.Alert, account *data.Account) error {
|
||||
alertValues := map[string]string{
|
||||
"Status": alert.Status,
|
||||
"StartDate": alert.StartsAt.String(),
|
||||
"EndDate": alert.EndsAt.String(),
|
||||
"Fingerprint": alert.Fingerprint,
|
||||
|
||||
"Name": alert.Labels["alertname"],
|
||||
"Service": alert.Labels["service_type"],
|
||||
"Instance": alert.Labels["instance"],
|
||||
"Job": alert.Labels["job"],
|
||||
"Severity": alert.Labels["severity"],
|
||||
"Site": alert.Labels["site"],
|
||||
"SiteID": alert.Labels["site_id"],
|
||||
|
||||
"Description": alert.Annotations["description"],
|
||||
"Summary": alert.Annotations["summary"],
|
||||
}
|
||||
|
||||
return email.SendAlertNotification(account, []string{account.Email}, alertValues, *dispatcher.conf)
|
||||
}
|
||||
|
||||
// NewDispatcher creates a new dispatcher instance.
|
||||
func NewDispatcher(conf *config.Configuration, log *zerolog.Logger) (*Dispatcher, error) {
|
||||
dispatcher := &Dispatcher{}
|
||||
if err := dispatcher.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the alerts dispatcher")
|
||||
}
|
||||
return dispatcher, nil
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
)
|
||||
|
||||
// Configuration holds the general service configuration.
|
||||
type Configuration struct {
|
||||
Prefix string `mapstructure:"prefix"`
|
||||
|
||||
Security struct {
|
||||
CredentialsPassphrase string `mapstructure:"creds_passphrase"`
|
||||
} `mapstructure:"security"`
|
||||
|
||||
Storage struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
|
||||
File struct {
|
||||
SitesFile string `mapstructure:"sites_file"`
|
||||
AccountsFile string `mapstructure:"accounts_file"`
|
||||
} `mapstructure:"file"`
|
||||
} `mapstructure:"storage"`
|
||||
|
||||
Email struct {
|
||||
SMTP *smtpclient.SMTPCredentials `mapstructure:"smtp"`
|
||||
NotificationsMail string `mapstructure:"notifications_mail"`
|
||||
} `mapstructure:"email"`
|
||||
|
||||
Mentix struct {
|
||||
URL string `mapstructure:"url"`
|
||||
DataEndpoint string `mapstructure:"data_endpoint"`
|
||||
SiteRegistrationEndpoint string `mapstructure:"sitereg_endpoint"`
|
||||
} `mapstructure:"mentix"`
|
||||
|
||||
Webserver struct {
|
||||
URL string `mapstructure:"url"`
|
||||
|
||||
SessionTimeout int `mapstructure:"session_timeout"`
|
||||
VerifyRemoteAddress bool `mapstructure:"verify_remote_address"`
|
||||
LogSessions bool `mapstructure:"log_sessions"`
|
||||
} `mapstructure:"webserver"`
|
||||
|
||||
GOCDB struct {
|
||||
URL string `mapstructure:"url"`
|
||||
WriteURL string `mapstructure:"write_url"`
|
||||
|
||||
APIKey string `mapstructure:"apikey"`
|
||||
} `mapstructure:"gocdb"`
|
||||
}
|
||||
|
||||
// Cleanup cleans up certain settings, normalizing them.
|
||||
func (cfg *Configuration) Cleanup() {
|
||||
// Ensure the webserver URL ends with a slash
|
||||
if cfg.Webserver.URL != "" && !strings.HasSuffix(cfg.Webserver.URL, "/") {
|
||||
cfg.Webserver.URL += "/"
|
||||
}
|
||||
|
||||
// Ensure the GOCDB URL ends with a slash
|
||||
if cfg.GOCDB.URL != "" && !strings.HasSuffix(cfg.GOCDB.URL, "/") {
|
||||
cfg.GOCDB.URL += "/"
|
||||
}
|
||||
|
||||
// Ensure the GOCDB Write URL ends with a slash
|
||||
if cfg.GOCDB.WriteURL != "" && !strings.HasSuffix(cfg.GOCDB.WriteURL, "/") {
|
||||
cfg.GOCDB.WriteURL += "/"
|
||||
}
|
||||
}
|
||||
-65
@@ -1,65 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package config
|
||||
|
||||
const (
|
||||
// EndpointAdministration is the endpoint path of the web interface administration panel.
|
||||
EndpointAdministration = "/admin"
|
||||
// EndpointAccount is the endpoint path of the web interface account panel.
|
||||
EndpointAccount = "/account"
|
||||
|
||||
// EndpointList is the endpoint path for listing all stored accounts.
|
||||
EndpointList = "/list"
|
||||
// EndpointFind is the endpoint path for finding accounts.
|
||||
EndpointFind = "/find"
|
||||
|
||||
// EndpointCreate is the endpoint path for account creation.
|
||||
EndpointCreate = "/create"
|
||||
// EndpointUpdate is the endpoint path for account updates.
|
||||
EndpointUpdate = "/update"
|
||||
// EndpointConfigure is the endpoint path for account configuration.
|
||||
EndpointConfigure = "/configure"
|
||||
// EndpointRemove is the endpoint path for account removal.
|
||||
EndpointRemove = "/remove"
|
||||
|
||||
// EndpointSiteGet is the endpoint path for retrieving site data.
|
||||
EndpointSiteGet = "/site-get"
|
||||
// EndpointSiteConfigure is the endpoint path for site configuration.
|
||||
EndpointSiteConfigure = "/site-configure"
|
||||
|
||||
// EndpointLogin is the endpoint path for (internal) user login.
|
||||
EndpointLogin = "/login"
|
||||
// EndpointLogout is the endpoint path for (internal) user logout.
|
||||
EndpointLogout = "/logout"
|
||||
// EndpointResetPassword is the endpoint path for resetting user passwords
|
||||
EndpointResetPassword = "/reset-password"
|
||||
// EndpointContact is the endpoint path for sending contact emails
|
||||
EndpointContact = "/contact"
|
||||
|
||||
// EndpointVerifyUserToken is the endpoint path for user token validation.
|
||||
EndpointVerifyUserToken = "/verify-user-token"
|
||||
|
||||
// EndpointGrantSiteAccess is the endpoint path for granting or revoking Site access.
|
||||
EndpointGrantSiteAccess = "/grant-site-access"
|
||||
// EndpointGrantGOCDBAccess is the endpoint path for granting or revoking GOCDB access.
|
||||
EndpointGrantGOCDBAccess = "/grant-gocdb-access"
|
||||
|
||||
// EndpointDispatchAlert is the endpoint path for dispatching alerts from Prometheus.
|
||||
EndpointDispatchAlert = "/dispatch-alert"
|
||||
)
|
||||
-69
@@ -1,69 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials/crypto"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Credentials stores and en-/decrypts credentials
|
||||
type Credentials struct {
|
||||
ID string `json:"id"`
|
||||
Secret string `json:"secret"`
|
||||
}
|
||||
|
||||
// Get decrypts and retrieves the stored credentials.
|
||||
func (creds *Credentials) Get(passphrase string) (string, string, error) {
|
||||
id, err := crypto.DecodeString(creds.ID, passphrase)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "unable to decode ID")
|
||||
}
|
||||
secret, err := crypto.DecodeString(creds.Secret, passphrase)
|
||||
if err != nil {
|
||||
return "", "", errors.Wrap(err, "unable to decode secret")
|
||||
}
|
||||
return id, secret, nil
|
||||
}
|
||||
|
||||
// Set encrypts and sets new credentials.
|
||||
func (creds *Credentials) Set(id, secret string, passphrase string) error {
|
||||
if s, err := crypto.EncodeString(id, passphrase); err == nil {
|
||||
creds.ID = s
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to encode ID")
|
||||
}
|
||||
if s, err := crypto.EncodeString(secret, passphrase); err == nil {
|
||||
creds.Secret = s
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to encode secret")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsValid checks whether the credentials are valid.
|
||||
func (creds *Credentials) IsValid() bool {
|
||||
return len(creds.ID) > 0 && len(creds.Secret) > 0
|
||||
}
|
||||
|
||||
// Clear resets the credentials.
|
||||
func (creds *Credentials) Clear() {
|
||||
creds.ID = ""
|
||||
creds.Secret = ""
|
||||
}
|
||||
-101
@@ -1,101 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"io"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
passphraseLength = 32
|
||||
)
|
||||
|
||||
// EncodeString encodes a string using AES and returns the base64-encoded result.
|
||||
func EncodeString(s string, passphrase string) (string, error) {
|
||||
if len(s) == 0 || len(passphrase) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
passphrase = normalizePassphrase(passphrase)
|
||||
|
||||
gcm, err := createGCM([]byte(passphrase))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return "", errors.Wrap(err, "unable to generate nonce")
|
||||
}
|
||||
encryptedData := gcm.Seal(nonce, nonce, []byte(s), nil)
|
||||
return base64.StdEncoding.EncodeToString(encryptedData), nil
|
||||
}
|
||||
|
||||
// DecodeString decodes a base64-encoded string encoded with AES.
|
||||
func DecodeString(s string, passphrase string) (string, error) {
|
||||
if len(s) == 0 || len(passphrase) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
data, _ := base64.StdEncoding.DecodeString(s)
|
||||
passphrase = normalizePassphrase(passphrase)
|
||||
|
||||
gcm, err := createGCM([]byte(passphrase))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(s) < nonceSize {
|
||||
return "", errors.Errorf("input string length too short")
|
||||
}
|
||||
nonce, data := data[:nonceSize], data[nonceSize:]
|
||||
plain, err := gcm.Open(nil, nonce, data, nil)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to decode string")
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
func createGCM(passphrase []byte) (cipher.AEAD, error) {
|
||||
c, err := aes.NewCipher(passphrase)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate cipher")
|
||||
}
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate GCM")
|
||||
}
|
||||
return gcm, nil
|
||||
}
|
||||
|
||||
func normalizePassphrase(passphrase string) string {
|
||||
if len(passphrase) > passphraseLength {
|
||||
passphrase = passphrase[:passphraseLength]
|
||||
} else if len(passphrase) < passphraseLength {
|
||||
for i := len(passphrase); i < passphraseLength; i++ {
|
||||
passphrase += "#"
|
||||
}
|
||||
}
|
||||
return passphrase
|
||||
}
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package credentials
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Password holds a hash password alongside its salt value.
|
||||
type Password struct {
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
const (
|
||||
passwordMinLength = 8
|
||||
)
|
||||
|
||||
// Set sets a new password by hashing the plaintext version using bcrypt.
|
||||
func (password *Password) Set(pwd string) error {
|
||||
if err := VerifyPassword(pwd); err != nil {
|
||||
return errors.Wrap(err, "invalid password")
|
||||
}
|
||||
|
||||
pwdData, err := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate password hash")
|
||||
}
|
||||
password.Value = string(pwdData)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compare checks whether the given password string equals the stored one.
|
||||
func (password *Password) Compare(pwd string) bool {
|
||||
return bcrypt.CompareHashAndPassword([]byte(password.Value), []byte(pwd)) == nil
|
||||
}
|
||||
|
||||
// IsValid checks whether the password is valid.
|
||||
func (password *Password) IsValid() bool {
|
||||
// bcrypt hashes are in the form of $[version]$[cost]$[22 character salt][31 character hash], so they have a minimum length of 58
|
||||
return len(password.Value) > 58 && strings.Count(password.Value, "$") >= 3
|
||||
}
|
||||
|
||||
// Clear resets the password.
|
||||
func (password *Password) Clear() {
|
||||
password.Value = ""
|
||||
}
|
||||
|
||||
// VerifyPassword checks whether the given password abides to the enforced password strength.
|
||||
func VerifyPassword(pwd string) error {
|
||||
if len(pwd) < passwordMinLength {
|
||||
return errors.Errorf("the password must be at least 8 characters long")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "abcdefghijklmnopqrstuvwxyz") {
|
||||
return errors.Errorf("the password must contain at least one lowercase letter")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") {
|
||||
return errors.Errorf("the password must contain at least one uppercase letter")
|
||||
}
|
||||
if !strings.ContainsAny(pwd, "0123456789") {
|
||||
return errors.Errorf("the password must contain at least one digit")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this filePath except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/utils"
|
||||
)
|
||||
|
||||
// Account represents a single site account.
|
||||
type Account struct {
|
||||
Email string `json:"email"`
|
||||
Title string `json:"title"`
|
||||
FirstName string `json:"firstName"`
|
||||
LastName string `json:"lastName"`
|
||||
Site string `json:"site"`
|
||||
Role string `json:"role"`
|
||||
PhoneNumber string `json:"phoneNumber"`
|
||||
|
||||
Password credentials.Password `json:"password"`
|
||||
|
||||
DateCreated time.Time `json:"dateCreated"`
|
||||
DateModified time.Time `json:"dateModified"`
|
||||
|
||||
Data AccountData `json:"data"`
|
||||
Settings AccountSettings `json:"settings"`
|
||||
}
|
||||
|
||||
// AccountData holds additional data for a site account.
|
||||
type AccountData struct {
|
||||
GOCDBAccess bool `json:"gocdbAccess"`
|
||||
SiteAccess bool `json:"siteAccess"`
|
||||
}
|
||||
|
||||
// AccountSettings holds additional settings for a site account.
|
||||
type AccountSettings struct {
|
||||
ReceiveAlerts bool `json:"receiveAlerts"`
|
||||
}
|
||||
|
||||
// Accounts holds an array of site accounts.
|
||||
type Accounts = []*Account
|
||||
|
||||
// Update copies the data of the given account to this account.
|
||||
func (acc *Account) Update(other *Account, setPassword bool, copyData bool) error {
|
||||
if err := other.verify(false, false); err != nil {
|
||||
return errors.Wrap(err, "unable to update account data")
|
||||
}
|
||||
|
||||
// Manually update fields
|
||||
acc.Title = other.Title
|
||||
acc.FirstName = other.FirstName
|
||||
acc.LastName = other.LastName
|
||||
acc.Role = other.Role
|
||||
acc.PhoneNumber = other.PhoneNumber
|
||||
|
||||
if setPassword && other.Password.Value != "" {
|
||||
// If a password was provided, use that as the new one
|
||||
if err := acc.UpdatePassword(other.Password.Value); err != nil {
|
||||
return errors.Wrap(err, "unable to update account data")
|
||||
}
|
||||
}
|
||||
|
||||
if copyData {
|
||||
acc.Data = other.Data
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Configure copies the settings of the given account to this account.
|
||||
func (acc *Account) Configure(other *Account) error {
|
||||
// Simply copy the stored settings
|
||||
acc.Settings = other.Settings
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePassword assigns a new password to the account, hashing it first.
|
||||
func (acc *Account) UpdatePassword(pwd string) error {
|
||||
if err := acc.Password.Set(pwd); err != nil {
|
||||
return errors.Wrap(err, "unable to update the user password")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a copy of the account; if erasePassword is set to true, the password will be cleared in the cloned object.
|
||||
func (acc *Account) Clone(erasePassword bool) *Account {
|
||||
clone := *acc
|
||||
|
||||
if erasePassword {
|
||||
clone.Password.Clear()
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
// CheckScopeAccess checks whether the user can access the specified scope.
|
||||
func (acc *Account) CheckScopeAccess(scope string) bool {
|
||||
hasAccess := false
|
||||
|
||||
switch strings.ToLower(scope) {
|
||||
case ScopeDefault:
|
||||
hasAccess = true
|
||||
|
||||
case ScopeGOCDB:
|
||||
hasAccess = acc.Data.GOCDBAccess
|
||||
|
||||
case ScopeSite:
|
||||
hasAccess = acc.Data.SiteAccess
|
||||
}
|
||||
|
||||
return hasAccess
|
||||
}
|
||||
|
||||
// Cleanup trims all string entries.
|
||||
func (acc *Account) Cleanup() {
|
||||
acc.Email = strings.TrimSpace(acc.Email)
|
||||
acc.Title = strings.TrimSpace(acc.Title)
|
||||
acc.FirstName = strings.TrimSpace(acc.FirstName)
|
||||
acc.LastName = strings.TrimSpace(acc.LastName)
|
||||
acc.Site = strings.TrimSpace(acc.Site)
|
||||
acc.Role = strings.TrimSpace(acc.Role)
|
||||
acc.PhoneNumber = strings.TrimSpace(acc.PhoneNumber)
|
||||
}
|
||||
|
||||
func (acc *Account) verify(isNewAccount, verifyPassword bool) error {
|
||||
if acc.Email == "" {
|
||||
return errors.Errorf("no email address provided")
|
||||
} else if !utils.IsEmailValid(acc.Email) {
|
||||
return errors.Errorf("invalid email address: %v", acc.Email)
|
||||
}
|
||||
|
||||
if acc.FirstName == "" {
|
||||
return errors.Errorf("no first name provided")
|
||||
} else if !utils.IsValidName(acc.FirstName) {
|
||||
return errors.Errorf("first name contains invalid characters: %v", acc.FirstName)
|
||||
}
|
||||
|
||||
if acc.LastName == "" {
|
||||
return errors.Errorf("no last name provided")
|
||||
} else if !utils.IsValidName(acc.LastName) {
|
||||
return errors.Errorf("last name contains invalid characters: %v", acc.LastName)
|
||||
}
|
||||
|
||||
if isNewAccount && acc.Site == "" {
|
||||
return errors.Errorf("no site provided")
|
||||
}
|
||||
|
||||
if acc.Role == "" {
|
||||
return errors.Errorf("no role provided")
|
||||
} else if !utils.IsValidName(acc.Role) {
|
||||
return errors.Errorf("role contains invalid characters: %v", acc.Role)
|
||||
}
|
||||
|
||||
if acc.PhoneNumber != "" && !utils.IsValidPhoneNumber(acc.PhoneNumber) {
|
||||
return errors.Errorf("invalid phone number provided")
|
||||
}
|
||||
|
||||
if verifyPassword {
|
||||
if !acc.Password.IsValid() {
|
||||
return errors.Errorf("no valid password set")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAccount creates a new site account.
|
||||
func NewAccount(email string, title, firstName, lastName string, site, role string, phoneNumber string, password string) (*Account, error) {
|
||||
t := time.Now()
|
||||
|
||||
acc := &Account{
|
||||
Email: email,
|
||||
Title: title,
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Site: site,
|
||||
Role: role,
|
||||
PhoneNumber: phoneNumber,
|
||||
DateCreated: t,
|
||||
DateModified: t,
|
||||
Data: AccountData{
|
||||
GOCDBAccess: false,
|
||||
SiteAccess: false,
|
||||
},
|
||||
Settings: AccountSettings{
|
||||
ReceiveAlerts: true,
|
||||
},
|
||||
}
|
||||
|
||||
// Set the user password, which also makes sure that the given password is strong enough
|
||||
if err := acc.UpdatePassword(password); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := acc.verify(true, true); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return acc, nil
|
||||
}
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
// 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"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// FileStorage implements a filePath-based storage.
|
||||
type FileStorage struct {
|
||||
Storage
|
||||
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sitesFilePath string
|
||||
accountsFilePath 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.SitesFile == "" {
|
||||
return errors.Errorf("no sites file set in the configuration")
|
||||
}
|
||||
storage.sitesFilePath = conf.Storage.File.SitesFile
|
||||
|
||||
if conf.Storage.File.AccountsFile == "" {
|
||||
return errors.Errorf("no accounts file set in the configuration")
|
||||
}
|
||||
storage.accountsFilePath = conf.Storage.File.AccountsFile
|
||||
|
||||
// Create the file directories if necessary
|
||||
_ = os.MkdirAll(filepath.Dir(storage.sitesFilePath), 0755)
|
||||
_ = os.MkdirAll(filepath.Dir(storage.accountsFilePath), 0755)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (storage *FileStorage) readData(file string, obj interface{}) error {
|
||||
// Read the data from the specified file
|
||||
jsonData, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "unable to read file %v", file)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(jsonData, obj); err != nil {
|
||||
return errors.Wrapf(err, "invalid file %v", file)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadSites reads all stored sites into the given data object.
|
||||
func (storage *FileStorage) ReadSites() (*Sites, error) {
|
||||
sites := &Sites{}
|
||||
if err := storage.readData(storage.sitesFilePath, sites); err != nil {
|
||||
return nil, errors.Wrap(err, "error reading sites")
|
||||
}
|
||||
return sites, nil
|
||||
}
|
||||
|
||||
// ReadAccounts reads all stored accounts into the given data object.
|
||||
func (storage *FileStorage) ReadAccounts() (*Accounts, error) {
|
||||
accounts := &Accounts{}
|
||||
if err := storage.readData(storage.accountsFilePath, accounts); err != nil {
|
||||
return nil, errors.Wrap(err, "error reading accounts")
|
||||
}
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (storage *FileStorage) writeData(file string, obj interface{}) error {
|
||||
// Write the data to the specified file
|
||||
jsonData, _ := json.MarshalIndent(obj, "", "\t")
|
||||
if err := os.WriteFile(file, jsonData, 0755); err != nil {
|
||||
return errors.Wrapf(err, "unable to write file %v", file)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteSites writes all stored sites from the given data object.
|
||||
func (storage *FileStorage) WriteSites(sites *Sites) error {
|
||||
if err := storage.writeData(storage.sitesFilePath, sites); err != nil {
|
||||
return errors.Wrap(err, "error writing sites")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteAccounts writes all stored accounts from the given data object.
|
||||
func (storage *FileStorage) WriteAccounts(accounts *Accounts) error {
|
||||
if err := storage.writeData(storage.accountsFilePath, accounts); err != nil {
|
||||
return errors.Wrap(err, "error writing accounts")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SiteAdded is called when a site has been added.
|
||||
func (storage *FileStorage) SiteAdded(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// SiteUpdated is called when a site has been updated.
|
||||
func (storage *FileStorage) SiteUpdated(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// SiteRemoved is called when a site has been removed.
|
||||
func (storage *FileStorage) SiteRemoved(site *Site) {
|
||||
// Simply skip this action; all data is saved solely in WriteSites
|
||||
}
|
||||
|
||||
// AccountAdded is called when an account has been added.
|
||||
func (storage *FileStorage) AccountAdded(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// AccountUpdated is called when an account has been updated.
|
||||
func (storage *FileStorage) AccountUpdated(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// AccountRemoved is called when an account has been removed.
|
||||
func (storage *FileStorage) AccountRemoved(account *Account) {
|
||||
// Simply skip this action; all data is saved solely in WriteAccounts
|
||||
}
|
||||
|
||||
// NewFileStorage creates a new file storage.
|
||||
func NewFileStorage(conf *config.Configuration, log *zerolog.Logger) (*FileStorage, error) {
|
||||
storage := &FileStorage{}
|
||||
if err := storage.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the file storage")
|
||||
}
|
||||
return storage, nil
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package data
|
||||
|
||||
const (
|
||||
// ScopeDefault is the default account panel scope.
|
||||
ScopeDefault = ""
|
||||
// ScopeGOCDB is used to access the GOCDB.
|
||||
ScopeGOCDB = "gocdb"
|
||||
// ScopeSite is used to access the global site configuration.
|
||||
ScopeSite = "site"
|
||||
)
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Site represents the global site-specific settings stored in the service.
|
||||
type Site struct {
|
||||
ID string `json:"id"`
|
||||
|
||||
Config SiteConfiguration `json:"config"`
|
||||
}
|
||||
|
||||
// SiteConfiguration stores the global configuration of a site.
|
||||
type SiteConfiguration struct {
|
||||
TestClientCredentials credentials.Credentials `json:"testClientCredentials"`
|
||||
}
|
||||
|
||||
// Sites holds an array of sites.
|
||||
type Sites = []*Site
|
||||
|
||||
// Update copies the data of the given site to this site.
|
||||
func (site *Site) Update(other *Site, credsPassphrase string) error {
|
||||
if other.Config.TestClientCredentials.IsValid() {
|
||||
// If credentials were provided, use those as the new ones
|
||||
if err := site.UpdateTestClientCredentials(other.Config.TestClientCredentials.ID, other.Config.TestClientCredentials.Secret, credsPassphrase); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateTestClientCredentials assigns new test client credentials, encrypting the information first.
|
||||
func (site *Site) UpdateTestClientCredentials(id, secret string, passphrase string) error {
|
||||
if err := site.Config.TestClientCredentials.Set(id, secret, passphrase); err != nil {
|
||||
return errors.Wrap(err, "unable to update the test client credentials")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clone creates a copy of the site; if eraseCredentials is set to true, the (test user) credentials will be cleared in the cloned object.
|
||||
func (site *Site) Clone(eraseCredentials bool) *Site {
|
||||
clone := *site
|
||||
|
||||
if eraseCredentials {
|
||||
clone.Config.TestClientCredentials.Clear()
|
||||
}
|
||||
|
||||
return &clone
|
||||
}
|
||||
|
||||
// NewSite creates a new site.
|
||||
func NewSite(id string) (*Site, error) {
|
||||
site := &Site{
|
||||
ID: id,
|
||||
Config: SiteConfiguration{
|
||||
TestClientCredentials: credentials.Credentials{},
|
||||
},
|
||||
}
|
||||
return site, nil
|
||||
}
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package data
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SiteInformation holds the most basic information about a site.
|
||||
type SiteInformation struct {
|
||||
ID string
|
||||
Name string
|
||||
FullName string
|
||||
}
|
||||
|
||||
// QueryAvailableSites uses Mentix to query a list of all available (registered) sites.
|
||||
func QueryAvailableSites(mentixHost, dataEndpoint string) ([]SiteInformation, error) {
|
||||
mentixURL, err := network.GenerateURL(mentixHost, dataEndpoint, network.URLParams{})
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to generate Mentix URL")
|
||||
}
|
||||
|
||||
data, err := network.ReadEndpoint(mentixURL, nil, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to read the Mentix endpoint")
|
||||
}
|
||||
|
||||
// Decode the data into a simplified, reduced data type
|
||||
type siteData struct {
|
||||
Sites []SiteInformation
|
||||
}
|
||||
sites := siteData{}
|
||||
if err := json.Unmarshal(data, &sites); err != nil {
|
||||
return nil, errors.Wrap(err, "error while decoding the JSON data")
|
||||
}
|
||||
|
||||
// Sort the sites alphabetically by their names
|
||||
sort.Slice(sites.Sites, func(i, j int) bool {
|
||||
return sites.Sites[i].Name < sites.Sites[j].Name
|
||||
})
|
||||
|
||||
return sites.Sites, nil
|
||||
}
|
||||
|
||||
// QuerySiteName uses Mentix to query the name of a site given by its ID.
|
||||
func QuerySiteName(siteID string, fullName bool, mentixHost, dataEndpoint string) (string, error) {
|
||||
sites, err := QueryAvailableSites(mentixHost, dataEndpoint)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
index := len(sites)
|
||||
for i, site := range sites {
|
||||
if site.ID == siteID {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if index != len(sites) {
|
||||
if fullName {
|
||||
return sites[index].FullName, nil
|
||||
}
|
||||
|
||||
return sites[index].Name, nil
|
||||
}
|
||||
|
||||
return "", errors.Errorf("no site with ID %v found", siteID)
|
||||
}
|
||||
-46
@@ -1,46 +0,0 @@
|
||||
// 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 sites and accounts storages.
|
||||
type Storage interface {
|
||||
// ReadSites reads all stored sites into the given data object.
|
||||
ReadSites() (*Sites, error)
|
||||
// WriteSites writes all stored sites from the given data object.
|
||||
WriteSites(sites *Sites) error
|
||||
|
||||
// SiteAdded is called when a site has been added.
|
||||
SiteAdded(site *Site)
|
||||
// SiteUpdated is called when a site has been updated.
|
||||
SiteUpdated(site *Site)
|
||||
// SiteRemoved is called when a site has been removed.
|
||||
SiteRemoved(site *Site)
|
||||
|
||||
// ReadAccounts reads all stored accounts into the given data object.
|
||||
ReadAccounts() (*Accounts, error)
|
||||
// WriteAccounts writes all stored accounts from the given data object.
|
||||
WriteAccounts(accounts *Accounts) error
|
||||
|
||||
// AccountAdded is called when an account has been added.
|
||||
AccountAdded(account *Account)
|
||||
// AccountUpdated is called when an account has been updated.
|
||||
AccountUpdated(account *Account)
|
||||
// AccountRemoved is called when an account has been removed.
|
||||
AccountRemoved(account *Account)
|
||||
}
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type emailData struct {
|
||||
Account *data.Account
|
||||
|
||||
AccountsAddress string
|
||||
GOCDBAddress string
|
||||
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// SendFunction is the definition of email send functions.
|
||||
type SendFunction = func(*data.Account, []string, map[string]string, config.Configuration) error
|
||||
|
||||
func getEmailData(account *data.Account, conf config.Configuration, params map[string]string) *emailData {
|
||||
return &emailData{
|
||||
Account: account,
|
||||
AccountsAddress: conf.Webserver.URL,
|
||||
GOCDBAddress: conf.GOCDB.URL,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
|
||||
// SendAccountCreated sends an email about account creation.
|
||||
func SendAccountCreated(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Site Administrator Account created", accountCreatedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendSiteAccessGranted sends an email about granted Site access.
|
||||
func SendSiteAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Site access granted", siteAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendGOCDBAccessGranted sends an email about granted GOCDB access.
|
||||
func SendGOCDBAccessGranted(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: GOCDB access granted", gocdbAccessGrantedTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendPasswordReset sends an email containing the user's new password.
|
||||
func SendPasswordReset(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Password reset", passwordResetTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendContactForm sends a generic contact form to the ScienceMesh admins.
|
||||
func SendContactForm(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
return send(recipients, "ScienceMesh: Contact form", contactFormTemplate, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
// SendAlertNotification sends an alert via email.
|
||||
func SendAlertNotification(account *data.Account, recipients []string, params map[string]string, conf config.Configuration) error {
|
||||
subject := params["Summary"]
|
||||
tpl := alertFiringNotificationTemplate
|
||||
if strings.EqualFold(params["Status"], "resolved") {
|
||||
tpl = alertResolvedNotificationTemplate
|
||||
subject += " [RESOLVED]"
|
||||
}
|
||||
return send(recipients, "ScienceMesh Alert: "+subject, tpl, getEmailData(account, conf, params), conf.Email.SMTP)
|
||||
}
|
||||
|
||||
func send(recipients []string, subject string, bodyTemplate string, data interface{}, smtp *smtpclient.SMTPCredentials) error {
|
||||
// Do not fail if no SMTP client or recipient is given
|
||||
if smtp == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
tpl := template.New("email")
|
||||
prepareEmailTemplate(tpl)
|
||||
|
||||
if _, err := tpl.Parse(bodyTemplate); err != nil {
|
||||
return errors.Wrap(err, "error while parsing email template")
|
||||
}
|
||||
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, data); err != nil {
|
||||
return errors.Wrap(err, "error while executing email template")
|
||||
}
|
||||
|
||||
for _, recipient := range recipients {
|
||||
if len(recipient) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Send the mail w/o blocking the main thread
|
||||
go func(recipient string) {
|
||||
_ = smtp.SendMail(recipient, subject, body.String())
|
||||
}(recipient)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareEmailTemplate(tpl *template.Template) {
|
||||
// Add some custom helper functions to the template
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"indent": func(n int, s string) string {
|
||||
lines := make([]string, 0, 10)
|
||||
for _, line := range strings.Split(s, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
line = strings.Repeat(" ", n) + line
|
||||
lines = append(lines, line)
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
},
|
||||
})
|
||||
}
|
||||
-106
@@ -1,106 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package email
|
||||
|
||||
const accountCreatedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
Your ScienceMesh Site Administrator Account has been successfully created!
|
||||
|
||||
Log in to your account by visiting the user account panel:
|
||||
{{.AccountsAddress}}
|
||||
|
||||
Using this panel, you can modify your information, request access to the GOCDB, and more.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const siteAccessGrantedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
You have been granted access to the global configuration of your site.
|
||||
|
||||
Log in to your account to access this configuration:
|
||||
{{.AccountsAddress}}
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const gocdbAccessGrantedTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
You have been granted access to the ScienceMesh GOCDB instance:
|
||||
{{.GOCDBAddress}}
|
||||
|
||||
Simply use your regular ScienceMesh Site Administrator Account credentials to log in to the GOCDB.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const passwordResetTemplate = `
|
||||
Dear {{.Account.FirstName}} {{.Account.LastName}},
|
||||
|
||||
Your password has been successfully reset!
|
||||
Your new password is: {{.Account.Password.Value}}
|
||||
|
||||
We recommend to change this password immediately after logging in.
|
||||
|
||||
Kind regards,
|
||||
The ScienceMesh Team
|
||||
`
|
||||
|
||||
const contactFormTemplate = `
|
||||
{{.Account.FirstName}} {{.Account.LastName}} ({{.Account.Email}}) has sent the following message:
|
||||
|
||||
{{.Params.Subject}}
|
||||
---------------------------------------------------------------------------------------------------
|
||||
|
||||
{{.Params.Message}}
|
||||
`
|
||||
|
||||
const alertFiringNotificationTemplate = `
|
||||
Site '{{.Params.Site}}' has generated an alert:
|
||||
|
||||
Type: {{.Params.Name}}
|
||||
Service: {{.Params.Service}}
|
||||
Instance: {{.Params.Instance}}
|
||||
Job: {{.Params.Job}}
|
||||
Severity: {{.Params.Severity}}
|
||||
|
||||
{{.Params.Description | indent 2}}
|
||||
|
||||
{{.Params.StartDate}} ({{.Params.Fingerprint}})
|
||||
`
|
||||
|
||||
const alertResolvedNotificationTemplate = `
|
||||
Site '{{.Params.Site}}' has resolved an alert:
|
||||
|
||||
Type: {{.Params.Name}}
|
||||
Service: {{.Params.Service}}
|
||||
Instance: {{.Params.Instance}}
|
||||
Job: {{.Params.Job}}
|
||||
Severity: {{.Params.Severity}}
|
||||
|
||||
{{.Params.Description | indent 2}}
|
||||
|
||||
{{.Params.StartDate}} - {{.Params.EndDate}} ({{.Params.Fingerprint}})
|
||||
`
|
||||
-440
@@ -1,440 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package siteacc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
)
|
||||
|
||||
const (
|
||||
invokerUser = "user"
|
||||
)
|
||||
|
||||
type methodCallback = func(*SiteAccounts, url.Values, []byte, *html.Session) (interface{}, error)
|
||||
type accessSetterCallback = func(*manager.AccountsManager, *data.Account, bool) error
|
||||
|
||||
type endpoint struct {
|
||||
Path string
|
||||
Handler func(*SiteAccounts, endpoint, http.ResponseWriter, *http.Request, *html.Session)
|
||||
MethodCallbacks map[string]methodCallback
|
||||
IsPublic bool
|
||||
}
|
||||
|
||||
func createMethodCallbacks(cbGet methodCallback, cbPost methodCallback) map[string]methodCallback {
|
||||
callbacks := make(map[string]methodCallback)
|
||||
|
||||
if cbGet != nil {
|
||||
callbacks[http.MethodGet] = cbGet
|
||||
}
|
||||
|
||||
if cbPost != nil {
|
||||
callbacks[http.MethodPost] = cbPost
|
||||
}
|
||||
|
||||
return callbacks
|
||||
}
|
||||
|
||||
func getEndpoints() []endpoint {
|
||||
endpoints := []endpoint{
|
||||
// Form/panel endpoints
|
||||
{config.EndpointAdministration, callAdministrationEndpoint, nil, false},
|
||||
{config.EndpointAccount, callAccountEndpoint, nil, true},
|
||||
// General account endpoints
|
||||
{config.EndpointList, callMethodEndpoint, createMethodCallbacks(handleList, nil), false},
|
||||
{config.EndpointFind, callMethodEndpoint, createMethodCallbacks(handleFind, nil), false},
|
||||
{config.EndpointCreate, callMethodEndpoint, createMethodCallbacks(nil, handleCreate), true},
|
||||
{config.EndpointUpdate, callMethodEndpoint, createMethodCallbacks(nil, handleUpdate), false},
|
||||
{config.EndpointConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleConfigure), false},
|
||||
{config.EndpointRemove, callMethodEndpoint, createMethodCallbacks(nil, handleRemove), false},
|
||||
// Site endpoints
|
||||
{config.EndpointSiteGet, callMethodEndpoint, createMethodCallbacks(handleSiteGet, nil), false},
|
||||
{config.EndpointSiteConfigure, callMethodEndpoint, createMethodCallbacks(nil, handleSiteConfigure), false},
|
||||
// Login endpoints
|
||||
{config.EndpointLogin, callMethodEndpoint, createMethodCallbacks(nil, handleLogin), true},
|
||||
{config.EndpointLogout, callMethodEndpoint, createMethodCallbacks(handleLogout, nil), true},
|
||||
{config.EndpointResetPassword, callMethodEndpoint, createMethodCallbacks(nil, handleResetPassword), true},
|
||||
{config.EndpointContact, callMethodEndpoint, createMethodCallbacks(nil, handleContact), true},
|
||||
// Authentication endpoints
|
||||
{config.EndpointVerifyUserToken, callMethodEndpoint, createMethodCallbacks(handleVerifyUserToken, nil), true},
|
||||
// Access management endpoints
|
||||
{config.EndpointGrantSiteAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantSiteAccess), false},
|
||||
{config.EndpointGrantGOCDBAccess, callMethodEndpoint, createMethodCallbacks(nil, handleGrantGOCDBAccess), false},
|
||||
// Alerting endpoints
|
||||
{config.EndpointDispatchAlert, callMethodEndpoint, createMethodCallbacks(nil, handleDispatchAlert), false},
|
||||
}
|
||||
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func callAdministrationEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
if err := siteacc.ShowAdministrationPanel(w, r, session); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "Unable to show the administration panel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func callAccountEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
if err := siteacc.ShowAccountPanel(w, r, session); err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = fmt.Fprintf(w, "Unable to show the account panel: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func callMethodEndpoint(siteacc *SiteAccounts, ep endpoint, w http.ResponseWriter, r *http.Request, session *html.Session) {
|
||||
// Every request to the accounts service results in a standardized JSON response
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// The default response is an unknown requestHandler (for the specified method)
|
||||
resp := Response{
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("unknown endpoint %v for method %v", r.URL.Path, r.Method),
|
||||
Data: nil,
|
||||
}
|
||||
|
||||
if ep.MethodCallbacks != nil {
|
||||
// Search for a matching method in the list of callbacks
|
||||
for method, cb := range ep.MethodCallbacks {
|
||||
if method == r.Method {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
|
||||
if respData, err := cb(siteacc, r.URL.Query(), body, session); err == nil {
|
||||
resp.Success = true
|
||||
resp.Error = ""
|
||||
resp.Data = respData
|
||||
} else {
|
||||
resp.Success = false
|
||||
resp.Error = fmt.Sprintf("%v", err)
|
||||
resp.Data = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Any failure during query handling results in a bad request
|
||||
if !resp.Success {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// Responses here are always JSON
|
||||
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
|
||||
|
||||
jsonData, _ := json.MarshalIndent(&resp, "", "\t")
|
||||
_, _ = w.Write(jsonData)
|
||||
}
|
||||
|
||||
func handleList(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return siteacc.AccountsManager().CloneAccounts(true), nil
|
||||
}
|
||||
|
||||
func handleFind(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := findAccount(siteacc, values.Get("by"), values.Get("value"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{"account": account.Clone(true)}, nil
|
||||
}
|
||||
|
||||
func handleCreate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create a new account through the accounts manager
|
||||
if err := siteacc.AccountsManager().CreateAccount(account); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to create account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleUpdate(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, setPassword, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Email = email
|
||||
|
||||
// Update the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().UpdateAccount(account, setPassword, false); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to update account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleConfigure(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
email, _, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account.Email = email
|
||||
|
||||
// Configure the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().ConfigureAccount(account); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to configure account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleRemove(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Remove the account through the accounts manager
|
||||
if err := siteacc.AccountsManager().RemoveAccount(account); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to remove account")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleSiteGet(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
siteID := values.Get("site")
|
||||
if siteID == "" {
|
||||
return nil, errors.Errorf("no site specified")
|
||||
}
|
||||
site := siteacc.SitesManager().FindSite(siteID)
|
||||
if site == nil {
|
||||
return nil, errors.Errorf("no site with ID %v exists", siteID)
|
||||
}
|
||||
return map[string]interface{}{"site": site.Clone(false)}, nil
|
||||
}
|
||||
|
||||
func handleSiteConfigure(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
email, _, err := processInvoker(siteacc, values, session)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := siteacc.AccountsManager().FindAccount(manager.FindByEmail, email)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
siteData := &data.Site{}
|
||||
if err := json.Unmarshal(body, siteData); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid form data")
|
||||
}
|
||||
siteData.ID = account.Site
|
||||
|
||||
// Configure the site through the sites manager
|
||||
if err := siteacc.SitesManager().UpdateSite(siteData); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to configure site")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleLogin(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Login the user through the users manager
|
||||
token, err := siteacc.UsersManager().LoginUser(account.Email, account.Password.Value, values.Get("scope"), session)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "unable to login user")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func handleLogout(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
// Logout the user through the users manager
|
||||
siteacc.UsersManager().LogoutUser(session)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleResetPassword(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Reset the password through the users manager
|
||||
if err := siteacc.AccountsManager().ResetPassword(account.Email); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to reset password")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleContact(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
if !session.IsUserLoggedIn() {
|
||||
return nil, errors.Errorf("no user is currently logged in")
|
||||
}
|
||||
|
||||
type jsonData struct {
|
||||
Subject string `json:"subject"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
contactData := &jsonData{}
|
||||
if err := json.Unmarshal(body, contactData); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid form data")
|
||||
}
|
||||
|
||||
// Send an email through the accounts manager
|
||||
siteacc.AccountsManager().SendContactForm(session.LoggedInUser().Account, strings.TrimSpace(contactData.Subject), strings.TrimSpace(contactData.Message))
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleVerifyUserToken(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
token := values.Get("token")
|
||||
if token == "" {
|
||||
return nil, errors.Errorf("no token specified")
|
||||
}
|
||||
|
||||
user := values.Get("user")
|
||||
if user == "" {
|
||||
return nil, errors.Errorf("no user specified")
|
||||
}
|
||||
|
||||
// Verify the user token using the users manager
|
||||
newToken, err := siteacc.UsersManager().VerifyUserToken(token, user, values.Get("scope"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "token verification failed")
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
func handleDispatchAlert(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
alertsData := &template.Data{}
|
||||
if err := json.Unmarshal(body, alertsData); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to unmarshal the alerts data")
|
||||
}
|
||||
|
||||
// Dispatch the alerts using the alerts dispatcher
|
||||
if err := siteacc.AlertsDispatcher().DispatchAlerts(alertsData, siteacc.AccountsManager().CloneAccounts(true)); err != nil {
|
||||
return nil, errors.Wrap(err, "error while dispatching the alerts")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func handleGrantSiteAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return handleGrantAccess((*manager.AccountsManager).GrantSiteAccess, siteacc, values, body, session)
|
||||
}
|
||||
|
||||
func handleGrantGOCDBAccess(siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
return handleGrantAccess((*manager.AccountsManager).GrantGOCDBAccess, siteacc, values, body, session)
|
||||
}
|
||||
|
||||
func handleGrantAccess(accessSetter accessSetterCallback, siteacc *SiteAccounts, values url.Values, body []byte, session *html.Session) (interface{}, error) {
|
||||
account, err := unmarshalRequestData(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if val := values.Get("status"); len(val) > 0 {
|
||||
var grantAccess bool
|
||||
switch strings.ToLower(val) {
|
||||
case "true":
|
||||
grantAccess = true
|
||||
|
||||
case "false":
|
||||
grantAccess = false
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported access status %v", val[0])
|
||||
}
|
||||
|
||||
// Grant access to the account through the accounts manager
|
||||
if err := accessSetter(siteacc.AccountsManager(), account, grantAccess); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to change the access status of the account")
|
||||
}
|
||||
} else {
|
||||
return nil, errors.Errorf("no access status provided")
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func unmarshalRequestData(body []byte) (*data.Account, error) {
|
||||
account := &data.Account{}
|
||||
if err := json.Unmarshal(body, account); err != nil {
|
||||
return nil, errors.Wrap(err, "invalid account data")
|
||||
}
|
||||
account.Cleanup()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func findAccount(siteacc *SiteAccounts, by string, value string) (*data.Account, error) {
|
||||
if len(by) == 0 && len(value) == 0 {
|
||||
return nil, errors.Errorf("missing search criteria")
|
||||
}
|
||||
|
||||
// Find the account using the accounts manager
|
||||
account, err := siteacc.AccountsManager().FindAccount(by, value)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "user not found")
|
||||
}
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func processInvoker(siteacc *SiteAccounts, values url.Values, session *html.Session) (string, bool, error) {
|
||||
var email string
|
||||
var invokedByUser bool
|
||||
|
||||
switch strings.ToLower(values.Get("invoker")) {
|
||||
case invokerUser:
|
||||
// If this endpoint was called by the user, set the account email from the stored session
|
||||
if !session.IsUserLoggedIn() {
|
||||
return "", false, errors.Errorf("no user is currently logged in")
|
||||
}
|
||||
|
||||
email = session.LoggedInUser().Account.Email
|
||||
invokedByUser = true
|
||||
|
||||
default:
|
||||
return "", false, errors.Errorf("no invoker provided")
|
||||
}
|
||||
|
||||
return email, invokedByUser, nil
|
||||
}
|
||||
-170
@@ -1,170 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package html
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// TemplateID is the type for template identifiers.
|
||||
type TemplateID = string
|
||||
|
||||
// Panel provides basic HTML panel functionality.
|
||||
type Panel struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
name string
|
||||
|
||||
provider PanelProvider
|
||||
|
||||
templates map[TemplateID]*template.Template
|
||||
}
|
||||
|
||||
const (
|
||||
pathParameterName = "path"
|
||||
)
|
||||
|
||||
func (panel *Panel) initialize(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if name == "" {
|
||||
return errors.Errorf("no name provided")
|
||||
}
|
||||
panel.name = name
|
||||
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
panel.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
panel.log = log
|
||||
|
||||
if provider == nil {
|
||||
return errors.Errorf("no panel provider provided")
|
||||
}
|
||||
panel.provider = provider
|
||||
|
||||
// Create space for the panel templates
|
||||
panel.templates = make(map[string]*template.Template, 5)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (panel *Panel) compile(provider ContentProvider) (string, error) {
|
||||
content := panelTemplate
|
||||
|
||||
// Replace placeholders by the values provided by the content provider
|
||||
content = strings.ReplaceAll(content, "$(TITLE)", provider.GetTitle())
|
||||
content = strings.ReplaceAll(content, "$(CAPTION)", provider.GetCaption())
|
||||
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_JAVASCRIPT)", provider.GetContentJavaScript())
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_STYLESHEET)", provider.GetContentStyleSheet())
|
||||
content = strings.ReplaceAll(content, "$(CONTENT_BODY)", provider.GetContentBody())
|
||||
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// AddTemplate adds and compiles a new template.
|
||||
func (panel *Panel) AddTemplate(name TemplateID, provider ContentProvider) error {
|
||||
name = panel.getFullTemplateName(name)
|
||||
|
||||
if provider == nil {
|
||||
return errors.Errorf("no content provider provided")
|
||||
}
|
||||
|
||||
content, err := panel.compile(provider)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error while compiling panel template %v", name)
|
||||
}
|
||||
|
||||
tpl := template.New(name)
|
||||
panel.prepareTemplate(tpl)
|
||||
|
||||
if _, err := tpl.Parse(content); err != nil {
|
||||
return errors.Wrapf(err, "error while parsing panel template %v", name)
|
||||
}
|
||||
panel.templates[name] = tpl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Execute generates the HTTP output of the panel and writes it to the response writer.
|
||||
func (panel *Panel) Execute(w http.ResponseWriter, r *http.Request, session *Session, dataProvider PanelDataProvider) error {
|
||||
// Get the path query parameter; the panel provider may use this to determine the template to use
|
||||
path := r.URL.Query().Get(pathParameterName)
|
||||
|
||||
actTpl := panel.provider.GetActiveTemplate(session, path)
|
||||
tplName := panel.getFullTemplateName(actTpl)
|
||||
tpl, ok := panel.templates[tplName]
|
||||
if !ok {
|
||||
return errors.Errorf("template %v not found", tplName)
|
||||
}
|
||||
|
||||
// If a data provider is specified, use it to get additional template data
|
||||
var data interface{}
|
||||
if dataProvider != nil {
|
||||
data = dataProvider(session)
|
||||
}
|
||||
|
||||
// Perform the pre-execution phase in which the panel provider can intercept the actual execution
|
||||
if state, err := panel.provider.PreExecute(session, actTpl, w, r); err == nil {
|
||||
if !state {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return errors.Wrapf(err, "pre-execution of template %v failed", tplName)
|
||||
}
|
||||
|
||||
return tpl.Execute(w, data)
|
||||
}
|
||||
|
||||
func (panel *Panel) prepareTemplate(tpl *template.Template) {
|
||||
// Add some custom helper functions to the template
|
||||
tpl.Funcs(template.FuncMap{
|
||||
"getServerAddress": func() string {
|
||||
return strings.TrimRight(panel.conf.Webserver.URL, "/")
|
||||
},
|
||||
"getSiteName": func(siteID string, fullName bool) string {
|
||||
siteName, _ := data.QuerySiteName(siteID, fullName, panel.conf.Mentix.URL, panel.conf.Mentix.DataEndpoint)
|
||||
return siteName
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (panel *Panel) getFullTemplateName(name string) string {
|
||||
return panel.name + "-" + name
|
||||
}
|
||||
|
||||
// NewPanel creates a new panel.
|
||||
func NewPanel(name string, provider PanelProvider, conf *config.Configuration, log *zerolog.Logger) (*Panel, error) {
|
||||
panel := &Panel{}
|
||||
if err := panel.initialize(name, provider, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the panel")
|
||||
}
|
||||
return panel, nil
|
||||
}
|
||||
-60
@@ -1,60 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// ContinueExecution causes the execution of a panel to continue.
|
||||
ContinueExecution = true
|
||||
// AbortExecution causes the execution of a panel to be aborted.
|
||||
AbortExecution = false
|
||||
)
|
||||
|
||||
// ExecutionResult is the type returned by the PreExecute function of PanelProvider.
|
||||
type ExecutionResult = bool
|
||||
|
||||
// PanelProvider handles general panel tasks.
|
||||
type PanelProvider interface {
|
||||
// GetActiveTemplate returns the name of the active template.
|
||||
GetActiveTemplate(*Session, string) string
|
||||
|
||||
// PreExecute is called before the actual template is being executed.
|
||||
PreExecute(*Session, string, http.ResponseWriter, *http.Request) (ExecutionResult, error)
|
||||
}
|
||||
|
||||
// PanelDataProvider is the function signature for panel data providers.
|
||||
type PanelDataProvider = func(*Session) interface{}
|
||||
|
||||
// ContentProvider defines various methods for HTML content providers.
|
||||
type ContentProvider interface {
|
||||
// GetTitle returns the title of the panel.
|
||||
GetTitle() string
|
||||
// GetCaption returns the caption which is displayed on the panel.
|
||||
GetCaption() string
|
||||
|
||||
// GetContentJavaScript delivers additional JavaScript code.
|
||||
GetContentJavaScript() string
|
||||
// GetContentStyleSheet delivers additional stylesheet code.
|
||||
GetContentStyleSheet() string
|
||||
// GetContentBody delivers the actual body content.
|
||||
GetContentBody() string
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package html
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// Session stores all data associated with an HTML session.
|
||||
type Session struct {
|
||||
ID string
|
||||
MigrationID string
|
||||
RemoteAddress string
|
||||
CreationTime time.Time
|
||||
Timeout time.Duration
|
||||
|
||||
Data map[string]interface{}
|
||||
|
||||
loggedInUser *SessionUser
|
||||
|
||||
expirationTime time.Time
|
||||
halflifeTime time.Time
|
||||
|
||||
sessionCookieName string
|
||||
}
|
||||
|
||||
// SessionUser holds information about the logged in user
|
||||
type SessionUser struct {
|
||||
Account *data.Account
|
||||
Site *data.Site
|
||||
}
|
||||
|
||||
func getRemoteAddress(r *http.Request) string {
|
||||
// Remove the port number from the remote address
|
||||
remoteAddress := ""
|
||||
if address := strings.Split(r.RemoteAddr, ":"); len(address) == 2 {
|
||||
remoteAddress = address[0]
|
||||
}
|
||||
return remoteAddress
|
||||
}
|
||||
|
||||
// LoggedInUser retrieves the currently logged in user or nil if none is logged in.
|
||||
func (sess *Session) LoggedInUser() *SessionUser {
|
||||
return sess.loggedInUser
|
||||
}
|
||||
|
||||
// LoginUser logs in the provided user.
|
||||
func (sess *Session) LoginUser(acc *data.Account, site *data.Site) {
|
||||
sess.loggedInUser = &SessionUser{
|
||||
Account: acc,
|
||||
Site: site,
|
||||
}
|
||||
}
|
||||
|
||||
// LogoutUser logs out the currently logged in user.
|
||||
func (sess *Session) LogoutUser() {
|
||||
sess.loggedInUser = nil
|
||||
}
|
||||
|
||||
// IsUserLoggedIn tells whether a user is currently logged in.
|
||||
func (sess *Session) IsUserLoggedIn() bool {
|
||||
return sess.loggedInUser != nil
|
||||
}
|
||||
|
||||
// Save stores the session ID in a cookie using a response writer.
|
||||
func (sess *Session) Save(cookiePath string, w http.ResponseWriter) {
|
||||
fullURL, _ := url.Parse(cookiePath)
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: sess.sessionCookieName,
|
||||
Secure: !strings.EqualFold(fullURL.Hostname(), "localhost"),
|
||||
Value: sess.ID,
|
||||
MaxAge: int(sess.Timeout / time.Second),
|
||||
Domain: fullURL.Hostname(),
|
||||
Path: fullURL.Path,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyRequest checks whether the provided request matches the stored session.
|
||||
func (sess *Session) VerifyRequest(r *http.Request, verifyRemoteAddress bool) error {
|
||||
cookie, err := r.Cookie(sess.sessionCookieName)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to retrieve client session ID")
|
||||
}
|
||||
if cookie.Value != sess.ID {
|
||||
return errors.Errorf("the session ID doesn't match")
|
||||
}
|
||||
|
||||
if verifyRemoteAddress && sess.RemoteAddress != "" {
|
||||
if !strings.EqualFold(getRemoteAddress(r), sess.RemoteAddress) {
|
||||
return errors.Errorf("remote address has changed (%v != %v)", r.RemoteAddr, sess.RemoteAddress)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HalftimePassed checks whether the session has passed the first half of its lifetime.
|
||||
func (sess *Session) HalftimePassed() bool {
|
||||
return time.Now().After(sess.halflifeTime)
|
||||
}
|
||||
|
||||
// HasExpired checks whether the session has reached is timeout.
|
||||
func (sess *Session) HasExpired() bool {
|
||||
return time.Now().After(sess.expirationTime)
|
||||
}
|
||||
|
||||
// NewSession creates a new session, giving it a random ID.
|
||||
func NewSession(name string, timeout time.Duration, r *http.Request) *Session {
|
||||
session := &Session{
|
||||
ID: uuid.NewString(),
|
||||
MigrationID: "",
|
||||
RemoteAddress: getRemoteAddress(r),
|
||||
CreationTime: time.Now(),
|
||||
Timeout: timeout,
|
||||
Data: make(map[string]interface{}, 10),
|
||||
loggedInUser: nil,
|
||||
expirationTime: time.Now().Add(timeout),
|
||||
halflifeTime: time.Now().Add(timeout / 2),
|
||||
sessionCookieName: name,
|
||||
}
|
||||
return session
|
||||
}
|
||||
-191
@@ -1,191 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package html
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SessionManager manages HTML sessions.
|
||||
type SessionManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sessions map[string]*Session
|
||||
|
||||
sessionName string
|
||||
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) initialize(name string, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if name == "" {
|
||||
return errors.Errorf("no session name provided")
|
||||
}
|
||||
mngr.sessionName = name
|
||||
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
mngr.sessions = make(map[string]*Session, 100)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// HandleRequest performs all session-related tasks during an HTML request. Always returns a valid session object.
|
||||
func (mngr *SessionManager) HandleRequest(w http.ResponseWriter, r *http.Request) (*Session, error) {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
var session *Session
|
||||
var sessionErr error
|
||||
|
||||
// Try to get the session ID from the request; if none has been set yet, a new one will be assigned
|
||||
cookie, err := r.Cookie(mngr.sessionName)
|
||||
if err == nil {
|
||||
session = mngr.findSession(cookie.Value)
|
||||
if session != nil {
|
||||
mngr.logSessionInfo(session, r, "existing session found")
|
||||
|
||||
// Verify the request against the session: If it is invalid, set an error; if the session has expired, create a new one; if it has already passed its halftime, migrate to a new one
|
||||
if err := session.VerifyRequest(r, mngr.conf.Webserver.VerifyRemoteAddress); err == nil {
|
||||
if session.HasExpired() {
|
||||
// The session has expired, so a new one needs to be created
|
||||
session = nil
|
||||
|
||||
mngr.logSessionInfo(session, r, "session expired")
|
||||
} else if session.HalftimePassed() {
|
||||
// The session has passed its halftime, so migrate it to a new one (makes hijacking session IDs harder)
|
||||
session, err = mngr.migrateSession(session, r)
|
||||
if err != nil {
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "unable to migrate session")
|
||||
}
|
||||
|
||||
mngr.logSessionInfo(session, r, "session migrated")
|
||||
}
|
||||
} else {
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "invalid session")
|
||||
|
||||
mngr.logSessionInfo(session, r, "session invalid (verify failed)")
|
||||
}
|
||||
}
|
||||
} else if err != http.ErrNoCookie {
|
||||
// The session cookie exists but seems to be invalid, so set an error
|
||||
session = nil
|
||||
sessionErr = errors.Wrap(err, "unable to get the session ID from the client")
|
||||
|
||||
mngr.logSessionInfo(session, r, fmt.Sprintf("session cookie error: %v", err))
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
// No session found for the client, so create a new one; this will always succeed
|
||||
session = mngr.createSession(r)
|
||||
|
||||
mngr.logSessionInfo(session, r, "assigned new session")
|
||||
}
|
||||
|
||||
// Store the session ID on the client side
|
||||
session.Save(mngr.conf.Webserver.URL, w)
|
||||
|
||||
return session, sessionErr
|
||||
}
|
||||
|
||||
// PurgeSessions removes any expired sessions.
|
||||
func (mngr *SessionManager) PurgeSessions() {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
var expiredSessions []string
|
||||
for id, session := range mngr.sessions {
|
||||
if session.HasExpired() {
|
||||
expiredSessions = append(expiredSessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range expiredSessions {
|
||||
delete(mngr.sessions, id)
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) createSession(r *http.Request) *Session {
|
||||
session := NewSession(mngr.sessionName, time.Duration(mngr.conf.Webserver.SessionTimeout)*time.Second, r)
|
||||
mngr.sessions[session.ID] = session
|
||||
return session
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) findSession(id string) *Session {
|
||||
if session, ok := mngr.sessions[id]; ok {
|
||||
return session
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) migrateSession(session *Session, r *http.Request) (*Session, error) {
|
||||
sessionNew := mngr.createSession(r)
|
||||
|
||||
// Carry over the old session information, thus preserving the existing session
|
||||
sessionNew.MigrationID = session.ID
|
||||
sessionNew.Data = session.Data
|
||||
|
||||
if user := session.LoggedInUser(); user != nil {
|
||||
sessionNew.LoginUser(user.Account, user.Site)
|
||||
} else {
|
||||
sessionNew.LogoutUser()
|
||||
}
|
||||
|
||||
// Delete the old session
|
||||
delete(mngr.sessions, session.ID)
|
||||
|
||||
return sessionNew, nil
|
||||
}
|
||||
|
||||
func (mngr *SessionManager) logSessionInfo(session *Session, r *http.Request, info string) {
|
||||
if mngr.conf.Webserver.LogSessions {
|
||||
if session != nil {
|
||||
mngr.log.Debug().Str("id", session.ID).Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info)
|
||||
} else {
|
||||
mngr.log.Debug().Str("address", r.RemoteAddr).Str("path", r.URL.Path).Msg(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewSessionManager creates a new session manager.
|
||||
func NewSessionManager(name string, conf *config.Configuration, log *zerolog.Logger) (*SessionManager, error) {
|
||||
mngr := &SessionManager{}
|
||||
if err := mngr.initialize(name, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the session manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
-170
@@ -1,170 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package html
|
||||
|
||||
const panelTemplate = `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<script>
|
||||
const STATE_NONE = 0
|
||||
const STATE_STATUS = 1
|
||||
const STATE_SUCCESS = 2
|
||||
const STATE_ERROR = 3
|
||||
|
||||
function enableForm(id, enable) {
|
||||
var form = document.getElementById(id);
|
||||
var elements = form.elements;
|
||||
for (var i = 0, len = elements.length; i < len; ++i) {
|
||||
elements[i].disabled = !enable;
|
||||
}
|
||||
}
|
||||
|
||||
function setElementVisibility(id, visible) {
|
||||
var elem = document.getElementById(id);
|
||||
if (visible) {
|
||||
elem.classList.add("visible");
|
||||
elem.classList.remove("hidden");
|
||||
} else {
|
||||
elem.classList.remove("visible");
|
||||
elem.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function setState(state, msg = "", formId = null, focusElem = null, formState = null) {
|
||||
setElementVisibility("status", state == STATE_STATUS);
|
||||
setElementVisibility("success", state == STATE_SUCCESS);
|
||||
setElementVisibility("error", state == STATE_ERROR);
|
||||
|
||||
var elem = null;
|
||||
switch (state) {
|
||||
case STATE_STATUS:
|
||||
elem = document.getElementById("status");
|
||||
break;
|
||||
|
||||
case STATE_SUCCESS:
|
||||
elem = document.getElementById("success");
|
||||
break;
|
||||
|
||||
case STATE_ERROR:
|
||||
elem = document.getElementById("error");
|
||||
break;
|
||||
}
|
||||
|
||||
if (elem !== null) {
|
||||
elem.innerHTML = msg;
|
||||
}
|
||||
|
||||
if (formId !== null && formState !== null) {
|
||||
enableForm(formId, formState);
|
||||
}
|
||||
|
||||
if (focusElem !== null) {
|
||||
var elem = document.getElementById(focusElem);
|
||||
elem.focus();
|
||||
}
|
||||
}
|
||||
|
||||
FormData.prototype.getTrimmed = function(id) {
|
||||
var val = this.get(id);
|
||||
|
||||
if (val != null) {
|
||||
return val.trim();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
$(CONTENT_JAVASCRIPT)
|
||||
</script>
|
||||
<style>
|
||||
form {
|
||||
border-color: lightgray !important;
|
||||
}
|
||||
button {
|
||||
min-width: 140px;
|
||||
}
|
||||
input {
|
||||
width: 95%;
|
||||
}
|
||||
label {
|
||||
font-weight: bold;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.box {
|
||||
width: 100%;
|
||||
border: 1px solid black;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
.container {
|
||||
width: 900px;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
.container-inline {
|
||||
display: inline-grid;
|
||||
grid-gap: 10px;
|
||||
}
|
||||
.status {
|
||||
border-color: #F7B22A;
|
||||
background: #FFEABF;
|
||||
}
|
||||
.success {
|
||||
border-color: #3CAC3A;
|
||||
background: #D3EFD2;
|
||||
}
|
||||
.error {
|
||||
border-color: #F20000;
|
||||
background: #F4D0D0;
|
||||
}
|
||||
.visible {
|
||||
display: block;
|
||||
}
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
$(CONTENT_STYLESHEET)
|
||||
</style>
|
||||
<title>$(TITLE)</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<div><h1>$(CAPTION)</h1></div>
|
||||
|
||||
$(CONTENT_BODY)
|
||||
|
||||
<div id="status" class="box status hidden">
|
||||
</div>
|
||||
<div id="success" class="box success hidden">
|
||||
</div>
|
||||
<div id="error" class="box error hidden">
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manager
|
||||
|
||||
import "github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
|
||||
// AccountsListenerCallback is the generic function type for accounts listeners.
|
||||
type AccountsListenerCallback = func(AccountsListener, *data.Account)
|
||||
|
||||
// AccountsListener is an interface that listens to accounts events.
|
||||
type AccountsListener interface {
|
||||
// AccountCreated is called whenever an account was created.
|
||||
AccountCreated(account *data.Account)
|
||||
// AccountUpdated is called whenever an account was updated.
|
||||
AccountUpdated(account *data.Account)
|
||||
// AccountRemoved is called whenever an account was removed.
|
||||
AccountRemoved(account *data.Account)
|
||||
}
|
||||
-343
@@ -1,343 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/email"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager/gocdb"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/smtpclient"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const (
|
||||
// FindByEmail holds the string value of the corresponding search criterium.
|
||||
FindByEmail = "email"
|
||||
)
|
||||
|
||||
// AccountsManager is responsible for all site account related tasks.
|
||||
type AccountsManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
storage data.Storage
|
||||
|
||||
accounts data.Accounts
|
||||
accountsListeners []AccountsListener
|
||||
|
||||
smtp *smtpclient.SMTPCredentials
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if storage == nil {
|
||||
return errors.Errorf("no storage provided")
|
||||
}
|
||||
mngr.storage = storage
|
||||
|
||||
mngr.accounts = make(data.Accounts, 0, 32) // Reserve some space for accounts
|
||||
mngr.readAllAccounts()
|
||||
|
||||
// Register accounts listeners
|
||||
if listener, err := gocdb.NewListener(mngr.conf, mngr.log); err == nil {
|
||||
mngr.accountsListeners = append(mngr.accountsListeners, listener)
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the GOCDB accounts listener")
|
||||
}
|
||||
|
||||
// Create the SMTP client
|
||||
if conf.Email.SMTP != nil {
|
||||
mngr.smtp = smtpclient.NewSMTPCredentials(conf.Email.SMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) readAllAccounts() {
|
||||
if accounts, err := mngr.storage.ReadAccounts(); 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 *AccountsManager) writeAllAccounts() {
|
||||
if err := mngr.storage.WriteAccounts(&mngr.accounts); err != nil {
|
||||
// Just warn when not being able to write accounts
|
||||
mngr.log.Warn().Err(err).Msg("error while writing accounts")
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) findAccount(by string, value string) (*data.Account, error) {
|
||||
if len(value) == 0 {
|
||||
return nil, errors.Errorf("no search value specified")
|
||||
}
|
||||
|
||||
var account *data.Account
|
||||
switch strings.ToLower(by) {
|
||||
case FindByEmail:
|
||||
account = mngr.findAccountByPredicate(func(account *data.Account) bool { return strings.EqualFold(account.Email, value) })
|
||||
|
||||
default:
|
||||
return nil, errors.Errorf("invalid search type %v", by)
|
||||
}
|
||||
|
||||
if account != nil {
|
||||
return account, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no user found matching the specified criteria")
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) findAccountByPredicate(predicate func(*data.Account) bool) *data.Account {
|
||||
for _, account := range mngr.accounts {
|
||||
if predicate(account) {
|
||||
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 *AccountsManager) CreateAccount(accountData *data.Account) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
// Accounts must be unique (identified by their email address)
|
||||
if account, _ := mngr.findAccount(FindByEmail, accountData.Email); account != nil {
|
||||
return errors.Errorf("an account with the specified email address already exists")
|
||||
}
|
||||
|
||||
if account, err := data.NewAccount(accountData.Email, accountData.Title, accountData.FirstName, accountData.LastName, accountData.Site, accountData.Role, accountData.PhoneNumber, accountData.Password.Value); err == nil {
|
||||
mngr.accounts = append(mngr.accounts, account)
|
||||
mngr.storage.AccountAdded(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.sendEmail(account, nil, email.SendAccountCreated)
|
||||
mngr.callListeners(account, AccountsListener.AccountCreated)
|
||||
} 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 *AccountsManager) UpdateAccount(accountData *data.Account, setPassword bool, copyData bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to update not found")
|
||||
}
|
||||
|
||||
if err := account.Update(accountData, setPassword, copyData); err == nil {
|
||||
account.DateModified = time.Now()
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
} else {
|
||||
return errors.Wrap(err, "error while updating account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureAccount configures the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) ConfigureAccount(accountData *data.Account) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to configure not found")
|
||||
}
|
||||
|
||||
if err := account.Configure(accountData); err == nil {
|
||||
account.DateModified = time.Now()
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
} else {
|
||||
return errors.Wrap(err, "error while configuring account")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword resets the password for the given user.
|
||||
func (mngr *AccountsManager) ResetPassword(name string) error {
|
||||
account, err := mngr.findAccount(FindByEmail, name)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "user to reset password for not found")
|
||||
}
|
||||
accountUpd := account.Clone(true)
|
||||
accountUpd.Password.Value = password.MustGenerate(defaultPasswordLength, 2, 0, false, true)
|
||||
|
||||
err = mngr.UpdateAccount(accountUpd, true, false)
|
||||
if err == nil {
|
||||
mngr.sendEmail(accountUpd, nil, email.SendPasswordReset)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// FindAccount is used to find an account by various criteria. The account is cloned to prevent data changes.
|
||||
func (mngr *AccountsManager) FindAccount(by string, value string) (*data.Account, error) {
|
||||
return mngr.FindAccountEx(by, value, true)
|
||||
}
|
||||
|
||||
// FindAccountEx is used to find an account by various criteria and optionally clone the account.
|
||||
func (mngr *AccountsManager) FindAccountEx(by string, value string, cloneAccount bool) (*data.Account, error) {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
account, err := mngr.findAccount(by, value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloneAccount {
|
||||
account = account.Clone(false)
|
||||
}
|
||||
|
||||
return account, nil
|
||||
}
|
||||
|
||||
// GrantSiteAccess sets the Site access status of the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) GrantSiteAccess(accountData *data.Account, grantAccess bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
return mngr.grantAccess(account, &account.Data.SiteAccess, grantAccess, email.SendSiteAccessGranted)
|
||||
}
|
||||
|
||||
// GrantGOCDBAccess sets the GOCDB access status of the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) GrantGOCDBAccess(accountData *data.Account, grantAccess bool) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
account, err := mngr.findAccount(FindByEmail, accountData.Email)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
return mngr.grantAccess(account, &account.Data.GOCDBAccess, grantAccess, email.SendGOCDBAccessGranted)
|
||||
}
|
||||
|
||||
// RemoveAccount removes the account identified by the account email; if no such account exists, an error is returned.
|
||||
func (mngr *AccountsManager) 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()
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountRemoved)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
return errors.Errorf("no account with the specified email exists")
|
||||
}
|
||||
|
||||
// SendContactForm sends a generic email to the ScienceMesh admins.
|
||||
func (mngr *AccountsManager) SendContactForm(account *data.Account, subject, message string) {
|
||||
mngr.sendEmail(account, map[string]string{"Subject": subject, "Message": message}, email.SendContactForm)
|
||||
}
|
||||
|
||||
// CloneAccounts retrieves all accounts currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
|
||||
func (mngr *AccountsManager) CloneAccounts(erasePasswords bool) data.Accounts {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
clones := make(data.Accounts, 0, len(mngr.accounts))
|
||||
for _, acc := range mngr.accounts {
|
||||
clones = append(clones, acc.Clone(erasePasswords))
|
||||
}
|
||||
|
||||
return clones
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) grantAccess(account *data.Account, accessFlag *bool, grantAccess bool, emailFunc email.SendFunction) error {
|
||||
accessOld := *accessFlag
|
||||
*accessFlag = grantAccess
|
||||
|
||||
mngr.storage.AccountUpdated(account)
|
||||
mngr.writeAllAccounts()
|
||||
|
||||
if *accessFlag && *accessFlag != accessOld {
|
||||
mngr.sendEmail(account, nil, emailFunc)
|
||||
}
|
||||
|
||||
mngr.callListeners(account, AccountsListener.AccountUpdated)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) callListeners(account *data.Account, cb AccountsListenerCallback) {
|
||||
for _, listener := range mngr.accountsListeners {
|
||||
cb(listener, account)
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *AccountsManager) sendEmail(account *data.Account, params map[string]string, sendFunc email.SendFunction) {
|
||||
_ = sendFunc(account, []string{account.Email, mngr.conf.Email.NotificationsMail}, params, *mngr.conf)
|
||||
}
|
||||
|
||||
// NewAccountsManager creates a new accounts manager instance.
|
||||
func NewAccountsManager(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) (*AccountsManager, error) {
|
||||
mngr := &AccountsManager{}
|
||||
if err := mngr.initialize(storage, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the accounts manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package gocdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/mentix/utils/network"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
opCreateOrUpdate = "CreateOrUpdate"
|
||||
opDelete = "Delete"
|
||||
)
|
||||
|
||||
type writeAccountUserData struct {
|
||||
Email string `json:"Email"`
|
||||
FirstName string `json:"FirstName"`
|
||||
LastName string `json:"LastName"`
|
||||
PhoneNumber string `json:"PhoneNumber"`
|
||||
}
|
||||
|
||||
type writeAccountData struct {
|
||||
APIKey string `json:"APIKey"`
|
||||
Operation string `json:"Operation"`
|
||||
|
||||
Data writeAccountUserData `json:"Data"`
|
||||
}
|
||||
|
||||
func writeAccount(account *data.Account, operation string, address string, apiKey string) error {
|
||||
// Fill in the data to send
|
||||
userData := getWriteAccountData(account)
|
||||
userData.APIKey = apiKey
|
||||
userData.Operation = operation
|
||||
|
||||
// Send the data to the GOCDB endpoint
|
||||
endpointURL, err := network.GenerateURL(address, "/ext/v1/user", network.URLParams{})
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to generate the GOCDB URL")
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(userData)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to marshal the user data")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, endpointURL.String(), bytes.NewReader(jsonData))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create HTTP request")
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to send data to endpoint")
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
msg, _ := io.ReadAll(resp.Body)
|
||||
return errors.Errorf("unable to perform request: %v", string(msg))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getWriteAccountData(account *data.Account) *writeAccountData {
|
||||
return &writeAccountData{
|
||||
Data: writeAccountUserData{
|
||||
Email: account.Email,
|
||||
FirstName: account.FirstName,
|
||||
LastName: account.LastName,
|
||||
PhoneNumber: account.PhoneNumber,
|
||||
},
|
||||
}
|
||||
}
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package gocdb
|
||||
|
||||
import (
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// AccountsListener is the GOCDB accounts listener.
|
||||
type AccountsListener struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
}
|
||||
|
||||
func (listener *AccountsListener) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
listener.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
listener.log = log
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AccountCreated is called whenever an account was created.
|
||||
func (listener *AccountsListener) AccountCreated(account *data.Account) {
|
||||
listener.updateGOCDB(account, false)
|
||||
}
|
||||
|
||||
// AccountUpdated is called whenever an account was updated.
|
||||
func (listener *AccountsListener) AccountUpdated(account *data.Account) {
|
||||
listener.updateGOCDB(account, false)
|
||||
}
|
||||
|
||||
// AccountRemoved is called whenever an account was removed.
|
||||
func (listener *AccountsListener) AccountRemoved(account *data.Account) {
|
||||
listener.updateGOCDB(account, true)
|
||||
}
|
||||
|
||||
func (listener *AccountsListener) updateGOCDB(account *data.Account, forceRemoval bool) {
|
||||
if account != nil && account.Data.GOCDBAccess && !forceRemoval {
|
||||
if err := writeAccount(account, opCreateOrUpdate, listener.conf.GOCDB.WriteURL, listener.conf.GOCDB.APIKey); err != nil {
|
||||
listener.log.Err(err).Str("userid", account.Email).Msg("unable to update GOCDB account")
|
||||
}
|
||||
} else {
|
||||
// Errors while deleting an account are ignored (account might not exist at all, for example)
|
||||
_ = writeAccount(account, opDelete, listener.conf.GOCDB.WriteURL, listener.conf.GOCDB.APIKey)
|
||||
}
|
||||
}
|
||||
|
||||
// NewListener creates a new GOCDB accounts listener.
|
||||
func NewListener(conf *config.Configuration, log *zerolog.Logger) (*AccountsListener, error) {
|
||||
listener := &AccountsListener{}
|
||||
if err := listener.initialize(conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the GOCDB accounts listener")
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
-185
@@ -1,185 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SitesManager is responsible for all sites related tasks.
|
||||
type SitesManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
storage data.Storage
|
||||
|
||||
sites data.Sites
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) initialize(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if storage == nil {
|
||||
return errors.Errorf("no storage provided")
|
||||
}
|
||||
mngr.storage = storage
|
||||
|
||||
mngr.sites = make(data.Sites, 0, 32) // Reserve some space for sites
|
||||
mngr.readAllSites()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) readAllSites() {
|
||||
if sites, err := mngr.storage.ReadSites(); err == nil {
|
||||
mngr.sites = *sites
|
||||
} else {
|
||||
// Just warn when not being able to read sites
|
||||
mngr.log.Warn().Err(err).Msg("error while reading sites")
|
||||
}
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) writeAllSites() {
|
||||
if err := mngr.storage.WriteSites(&mngr.sites); err != nil {
|
||||
// Just warn when not being able to write sites
|
||||
mngr.log.Warn().Err(err).Msg("error while writing sites")
|
||||
}
|
||||
}
|
||||
|
||||
// GetSite retrieves the site with the given ID, creating it first if necessary.
|
||||
func (mngr *SitesManager) GetSite(id string, cloneSite bool) (*data.Site, error) {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
site, err := mngr.getSite(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if cloneSite {
|
||||
site = site.Clone(false)
|
||||
}
|
||||
|
||||
return site, nil
|
||||
}
|
||||
|
||||
// FindSite returns the site specified by the ID if one exists.
|
||||
func (mngr *SitesManager) FindSite(id string) *data.Site {
|
||||
site, _ := mngr.findSite(id)
|
||||
return site
|
||||
}
|
||||
|
||||
// UpdateSite updates the site identified by the site ID; if no such site exists, one will be created first.
|
||||
func (mngr *SitesManager) UpdateSite(siteData *data.Site) error {
|
||||
mngr.mutex.Lock()
|
||||
defer mngr.mutex.Unlock()
|
||||
|
||||
site, err := mngr.getSite(siteData.ID)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "site to update not found")
|
||||
}
|
||||
|
||||
if err := site.Update(siteData, mngr.conf.Security.CredentialsPassphrase); err == nil {
|
||||
mngr.storage.SiteUpdated(site)
|
||||
mngr.writeAllSites()
|
||||
} else {
|
||||
return errors.Wrap(err, "error while updating site")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CloneSites retrieves all sites currently stored by cloning the data, thus avoiding race conflicts and making outside modifications impossible.
|
||||
func (mngr *SitesManager) CloneSites(eraseCredentials bool) data.Sites {
|
||||
mngr.mutex.RLock()
|
||||
defer mngr.mutex.RUnlock()
|
||||
|
||||
clones := make(data.Sites, 0, len(mngr.sites))
|
||||
for _, site := range mngr.sites {
|
||||
clones = append(clones, site.Clone(eraseCredentials))
|
||||
}
|
||||
|
||||
return clones
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) getSite(id string) (*data.Site, error) {
|
||||
site, err := mngr.findSite(id)
|
||||
if site == nil {
|
||||
site, err = mngr.createSite(id)
|
||||
}
|
||||
return site, err
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) createSite(id string) (*data.Site, error) {
|
||||
site, err := data.NewSite(id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error while creating site")
|
||||
}
|
||||
mngr.sites = append(mngr.sites, site)
|
||||
mngr.storage.SiteAdded(site)
|
||||
mngr.writeAllSites()
|
||||
return site, nil
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) findSite(id string) (*data.Site, error) {
|
||||
if len(id) == 0 {
|
||||
return nil, errors.Errorf("no search ID specified")
|
||||
}
|
||||
|
||||
site := mngr.findSiteByPredicate(func(site *data.Site) bool { return strings.EqualFold(site.ID, id) })
|
||||
if site != nil {
|
||||
return site, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no site found matching the specified ID")
|
||||
}
|
||||
|
||||
func (mngr *SitesManager) findSiteByPredicate(predicate func(*data.Site) bool) *data.Site {
|
||||
for _, site := range mngr.sites {
|
||||
if predicate(site) {
|
||||
return site
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSitesManager creates a new sites manager instance.
|
||||
func NewSitesManager(storage data.Storage, conf *config.Configuration, log *zerolog.Logger) (*SitesManager, error) {
|
||||
mngr := &SitesManager{}
|
||||
if err := mngr.initialize(storage, conf, log); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the sites manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
type userToken struct {
|
||||
jwt.RegisteredClaims
|
||||
|
||||
User string `json:"user"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
const (
|
||||
tokenKeyLength = 16
|
||||
tokenIssuer = "sciencemesh_siteacc"
|
||||
)
|
||||
|
||||
var (
|
||||
tokenSecret string
|
||||
)
|
||||
|
||||
func generateUserToken(user string, scope string, timeout int) (string, error) {
|
||||
// Create a JWT as the user token
|
||||
claims := userToken{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(timeout) * time.Second)),
|
||||
Issuer: tokenIssuer,
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
User: user,
|
||||
Scope: scope,
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), claims)
|
||||
signedToken, err := token.SignedString([]byte(tokenSecret))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error signing token with claims %+v", claims)
|
||||
}
|
||||
|
||||
return signedToken, nil
|
||||
}
|
||||
|
||||
func extractUserToken(token string) (*userToken, error) {
|
||||
// Parse the token and try to extract the claims
|
||||
parsedToken, err := jwt.ParseWithClaims(token, &userToken{}, func(token *jwt.Token) (interface{}, error) { return []byte(tokenSecret), nil })
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error parsing token")
|
||||
}
|
||||
|
||||
if claims, ok := parsedToken.Claims.(*userToken); ok && parsedToken.Valid {
|
||||
if claims.Issuer != tokenIssuer {
|
||||
return nil, errors.Errorf("invalid token issuer")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("invalid token")
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Generate the token secret randomly
|
||||
tokenSecret = password.MustGenerate(tokenKeyLength, tokenKeyLength/4, tokenKeyLength/4, false, true)
|
||||
}
|
||||
-150
@@ -1,150 +0,0 @@
|
||||
// Copyright 2018-2020 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package manager
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// UsersManager is responsible for managing logged in users through session objects.
|
||||
type UsersManager struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sitesManager *SitesManager
|
||||
accountsManager *AccountsManager
|
||||
}
|
||||
|
||||
const (
|
||||
defaultPasswordLength = 12
|
||||
)
|
||||
|
||||
func (mngr *UsersManager) initialize(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, accountsManager *AccountsManager) error {
|
||||
if conf == nil {
|
||||
return errors.Errorf("no configuration provided")
|
||||
}
|
||||
mngr.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return errors.Errorf("no logger provided")
|
||||
}
|
||||
mngr.log = log
|
||||
|
||||
if sitesManager == nil {
|
||||
return errors.Errorf("no sites manager provided")
|
||||
}
|
||||
mngr.sitesManager = sitesManager
|
||||
|
||||
if accountsManager == nil {
|
||||
return errors.Errorf("no accounts manager provided")
|
||||
}
|
||||
mngr.accountsManager = accountsManager
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoginUser tries to login a given username/password pair. On success, the corresponding user account is stored in the session and a user token is returned.
|
||||
func (mngr *UsersManager) LoginUser(name, password string, scope string, session *html.Session) (string, error) {
|
||||
account, err := mngr.accountsManager.FindAccountEx(FindByEmail, name, false)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "no account with the specified email exists")
|
||||
}
|
||||
|
||||
// Verify the provided password
|
||||
if !account.Password.Compare(password) {
|
||||
return "", errors.Errorf("invalid password")
|
||||
}
|
||||
|
||||
// Check if the user has access to the specified scope
|
||||
if !account.CheckScopeAccess(scope) {
|
||||
return "", errors.Errorf("no access to the specified scope granted")
|
||||
}
|
||||
|
||||
// Get the site the account belongs to
|
||||
site, err := mngr.sitesManager.GetSite(account.Site, false)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "no site with the specified ID exists")
|
||||
}
|
||||
|
||||
// Store the user account in the session
|
||||
session.LoginUser(account, site)
|
||||
|
||||
// Generate a token that can be used as a "ticket"
|
||||
token, err := generateUserToken(session.LoggedInUser().Account.Email, scope, mngr.conf.Webserver.SessionTimeout)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to generate user token")
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// LogoutUser logs the current user out.
|
||||
func (mngr *UsersManager) LogoutUser(session *html.Session) {
|
||||
// Just unset the user account stored in the session
|
||||
session.LogoutUser()
|
||||
}
|
||||
|
||||
// VerifyUserToken is used to verify a user token against the current session.
|
||||
func (mngr *UsersManager) VerifyUserToken(token string, user string, scope string) (string, error) {
|
||||
// Verify the token by trying to extract it
|
||||
utoken, err := extractUserToken(token)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to verify user token")
|
||||
}
|
||||
|
||||
// Check the provided email against the stored one
|
||||
if !strings.EqualFold(utoken.User, user) {
|
||||
return "", errors.Errorf("mismatching user")
|
||||
}
|
||||
|
||||
// Check if the user account actually exists and has proper scope access
|
||||
if strings.EqualFold(scope, utoken.Scope) {
|
||||
if acc, err := mngr.accountsManager.FindAccount(FindByEmail, utoken.User); err == nil {
|
||||
if !acc.CheckScopeAccess(scope) {
|
||||
return "", errors.Errorf("no scope access")
|
||||
}
|
||||
} else {
|
||||
return "", errors.Errorf("invalid email")
|
||||
}
|
||||
} else {
|
||||
return "", errors.Errorf("invalid scope")
|
||||
}
|
||||
|
||||
// Refresh the user token (as a form of keep-alive, since tokens expire quickly)
|
||||
newToken, err := generateUserToken(utoken.User, utoken.Scope, mngr.conf.Webserver.SessionTimeout)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "unable to refresh user token")
|
||||
}
|
||||
|
||||
return newToken, nil
|
||||
}
|
||||
|
||||
// NewUsersManager creates a new users manager instance.
|
||||
func NewUsersManager(conf *config.Configuration, log *zerolog.Logger, sitesManager *SitesManager, accountsManager *AccountsManager) (*UsersManager, error) {
|
||||
mngr := &UsersManager{}
|
||||
if err := mngr.initialize(conf, log, sitesManager, accountsManager); err != nil {
|
||||
return nil, errors.Wrap(err, "unable to initialize the users manager")
|
||||
}
|
||||
return mngr, nil
|
||||
}
|
||||
-215
@@ -1,215 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package siteacc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
accpanel "github.com/opencloud-eu/reva/v2/pkg/siteacc/account"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/admin"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/alerting"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/config"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/data"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/html"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/siteacc/manager"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// SiteAccounts represents the main Site Accounts service object.
|
||||
type SiteAccounts struct {
|
||||
conf *config.Configuration
|
||||
log *zerolog.Logger
|
||||
|
||||
sessions *html.SessionManager
|
||||
|
||||
storage data.Storage
|
||||
|
||||
sitesManager *manager.SitesManager
|
||||
accountsManager *manager.AccountsManager
|
||||
usersManager *manager.UsersManager
|
||||
|
||||
alertsDispatcher *alerting.Dispatcher
|
||||
|
||||
adminPanel *admin.Panel
|
||||
accountPanel *accpanel.Panel
|
||||
}
|
||||
|
||||
func (siteacc *SiteAccounts) initialize(conf *config.Configuration, log *zerolog.Logger) error {
|
||||
if conf == nil {
|
||||
return fmt.Errorf("no configuration provided")
|
||||
}
|
||||
siteacc.conf = conf
|
||||
|
||||
if log == nil {
|
||||
return fmt.Errorf("no logger provided")
|
||||
}
|
||||
siteacc.log = log
|
||||
|
||||
// Create the session mananger
|
||||
sessions, err := html.NewSessionManager("siteacc_session", conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error while creating the session manager")
|
||||
}
|
||||
siteacc.sessions = sessions
|
||||
|
||||
// Create the central storage
|
||||
storage, err := siteacc.createStorage(conf.Storage.Driver)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "unable to create storage")
|
||||
}
|
||||
siteacc.storage = storage
|
||||
|
||||
// Create the sites manager instance
|
||||
smngr, err := manager.NewSitesManager(storage, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the sites manager")
|
||||
}
|
||||
siteacc.sitesManager = smngr
|
||||
|
||||
// Create the accounts manager instance
|
||||
amngr, err := manager.NewAccountsManager(storage, conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the accounts manager")
|
||||
}
|
||||
siteacc.accountsManager = amngr
|
||||
|
||||
// Create the users manager instance
|
||||
umngr, err := manager.NewUsersManager(conf, log, siteacc.sitesManager, siteacc.accountsManager)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the users manager")
|
||||
}
|
||||
siteacc.usersManager = umngr
|
||||
|
||||
// Create the alerts dispatcher instance
|
||||
dispatcher, err := alerting.NewDispatcher(conf, log)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating the alerts dispatcher")
|
||||
}
|
||||
siteacc.alertsDispatcher = dispatcher
|
||||
|
||||
// Create the admin panel
|
||||
if pnl, err := admin.NewPanel(conf, log); err == nil {
|
||||
siteacc.adminPanel = pnl
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the administration panel")
|
||||
}
|
||||
|
||||
// Create the account panel
|
||||
if pnl, err := accpanel.NewPanel(conf, log); err == nil {
|
||||
siteacc.accountPanel = pnl
|
||||
} else {
|
||||
return errors.Wrap(err, "unable to create the account panel")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RequestHandler returns the HTTP request handler of the service.
|
||||
func (siteacc *SiteAccounts) RequestHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
|
||||
// Get the active session for the request (or create a new one); a valid session object will always be returned
|
||||
siteacc.sessions.PurgeSessions() // Remove expired sessions first
|
||||
session, err := siteacc.sessions.HandleRequest(w, r)
|
||||
if err != nil {
|
||||
siteacc.log.Err(err).Msg("an error occurred while handling sessions")
|
||||
}
|
||||
|
||||
epHandled := false
|
||||
for _, ep := range getEndpoints() {
|
||||
if ep.Path == r.URL.Path {
|
||||
ep.Handler(siteacc, ep, w, r, session)
|
||||
epHandled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !epHandled {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_, _ = fmt.Fprintf(w, "Unknown endpoint %v", r.URL.Path)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ShowAdministrationPanel writes the administration panel HTTP output directly to the response writer.
|
||||
func (siteacc *SiteAccounts) ShowAdministrationPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
// The admin panel only shows the stored accounts and offers actions through links, so let it use cloned data
|
||||
accounts := siteacc.accountsManager.CloneAccounts(true)
|
||||
return siteacc.adminPanel.Execute(w, r, session, &accounts)
|
||||
}
|
||||
|
||||
// ShowAccountPanel writes the account panel HTTP output directly to the response writer.
|
||||
func (siteacc *SiteAccounts) ShowAccountPanel(w http.ResponseWriter, r *http.Request, session *html.Session) error {
|
||||
return siteacc.accountPanel.Execute(w, r, session)
|
||||
}
|
||||
|
||||
// SitesManager returns the central sites manager instance.
|
||||
func (siteacc *SiteAccounts) SitesManager() *manager.SitesManager {
|
||||
return siteacc.sitesManager
|
||||
}
|
||||
|
||||
// AccountsManager returns the central accounts manager instance.
|
||||
func (siteacc *SiteAccounts) AccountsManager() *manager.AccountsManager {
|
||||
return siteacc.accountsManager
|
||||
}
|
||||
|
||||
// UsersManager returns the central users manager instance.
|
||||
func (siteacc *SiteAccounts) UsersManager() *manager.UsersManager {
|
||||
return siteacc.usersManager
|
||||
}
|
||||
|
||||
// AlertsDispatcher returns the central alerts dispatcher instance.
|
||||
func (siteacc *SiteAccounts) AlertsDispatcher() *alerting.Dispatcher {
|
||||
return siteacc.alertsDispatcher
|
||||
}
|
||||
|
||||
// GetPublicEndpoints returns a list of all public endpoints.
|
||||
func (siteacc *SiteAccounts) GetPublicEndpoints() []string {
|
||||
// TODO: Only for local testing!
|
||||
// return []string{"/"}
|
||||
|
||||
endpoints := make([]string, 0, 5)
|
||||
for _, ep := range getEndpoints() {
|
||||
if ep.IsPublic {
|
||||
endpoints = append(endpoints, ep.Path)
|
||||
}
|
||||
}
|
||||
return endpoints
|
||||
}
|
||||
|
||||
func (siteacc *SiteAccounts) createStorage(driver string) (data.Storage, error) {
|
||||
if driver == "file" {
|
||||
return data.NewFileStorage(siteacc.conf, siteacc.log)
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("unknown storage driver %v", driver)
|
||||
}
|
||||
|
||||
// New returns a new Site Accounts service instance.
|
||||
func New(conf *config.Configuration, log *zerolog.Logger) (*SiteAccounts, error) {
|
||||
// Configure the accounts service
|
||||
siteacc := new(SiteAccounts)
|
||||
if err := siteacc.initialize(conf, log); err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize site accounts: %v", err)
|
||||
}
|
||||
return siteacc, nil
|
||||
}
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
// Copyright 2018-2021 CERN
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
// In applying this license, CERN does not waive the privileges and immunities
|
||||
// granted to it by virtue of its status as an Intergovernmental Organization
|
||||
// or submit itself to any jurisdiction.
|
||||
|
||||
package smtpclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// SMTPCredentials stores the credentials required to connect to an SMTP server.
|
||||
type SMTPCredentials struct {
|
||||
SenderLogin string `mapstructure:"sender_login" docs:";The login to be used by sender."`
|
||||
SenderMail string `mapstructure:"sender_mail" docs:";The email to be used to send mails."`
|
||||
SenderPassword string `mapstructure:"sender_password" docs:";The sender's password."`
|
||||
SMTPServer string `mapstructure:"smtp_server" docs:";The hostname of the SMTP server."`
|
||||
SMTPPort int `mapstructure:"smtp_port" docs:"587;The port on which the SMTP daemon is running."`
|
||||
DisableAuth bool `mapstructure:"disable_auth" docs:"false;Whether to disable SMTP auth."`
|
||||
LocalName string `mapstructure:"local_name" docs:";The host name to be used for unauthenticated SMTP."`
|
||||
}
|
||||
|
||||
// NewSMTPCredentials creates a new SMTPCredentials object with the details of the passed object with sane defaults.
|
||||
func NewSMTPCredentials(c *SMTPCredentials) *SMTPCredentials {
|
||||
creds := c
|
||||
|
||||
if creds.SMTPPort == 0 {
|
||||
creds.SMTPPort = 587
|
||||
}
|
||||
if !creds.DisableAuth && creds.SenderPassword == "" {
|
||||
creds.SenderPassword = os.Getenv("REVA_SMTP_SENDER_PASSWORD")
|
||||
}
|
||||
if creds.LocalName == "" {
|
||||
tokens := strings.Split(creds.SenderMail, "@")
|
||||
creds.LocalName = tokens[len(tokens)-1]
|
||||
}
|
||||
if creds.SenderLogin == "" {
|
||||
creds.SenderLogin = creds.SenderMail
|
||||
}
|
||||
return creds
|
||||
}
|
||||
|
||||
// SendMail allows sending mails using a set of client credentials.
|
||||
func (creds *SMTPCredentials) SendMail(recipient, subject, body string) error {
|
||||
|
||||
headers := map[string]string{
|
||||
"From": creds.SenderMail,
|
||||
"To": recipient,
|
||||
"Subject": subject,
|
||||
"Date": time.Now().Format(time.RFC1123Z),
|
||||
"Message-ID": uuid.New().String(),
|
||||
"MIME-Version": "1.0",
|
||||
"Content-Type": "text/plain; charset=\"utf-8\"",
|
||||
"Content-Transfer-Encoding": "base64",
|
||||
}
|
||||
|
||||
message := ""
|
||||
for k, v := range headers {
|
||||
message += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
message += "\r\n" + base64.StdEncoding.EncodeToString([]byte(body))
|
||||
|
||||
if creds.DisableAuth {
|
||||
return creds.sendMailSMTP(recipient, subject, message)
|
||||
}
|
||||
return creds.sendMailAuthSMTP(recipient, subject, message)
|
||||
}
|
||||
|
||||
func (creds *SMTPCredentials) sendMailAuthSMTP(recipient, subject, message string) error {
|
||||
|
||||
auth := smtp.PlainAuth("", creds.SenderLogin, creds.SenderPassword, creds.SMTPServer)
|
||||
|
||||
err := smtp.SendMail(
|
||||
fmt.Sprintf("%s:%d", creds.SMTPServer, creds.SMTPPort),
|
||||
auth,
|
||||
creds.SenderMail,
|
||||
[]string{recipient},
|
||||
[]byte(message),
|
||||
)
|
||||
if err != nil {
|
||||
err = errors.Wrap(err, "smtpclient: error sending mail")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (creds *SMTPCredentials) sendMailSMTP(recipient, subject, message string) error {
|
||||
|
||||
c, err := smtp.Dial(fmt.Sprintf("%s:%d", creds.SMTPServer, creds.SMTPPort))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
if err = c.Hello(creds.LocalName); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.Mail(creds.SenderMail); err != nil {
|
||||
return err
|
||||
}
|
||||
if err = c.Rcpt(recipient); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wc, err := c.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wc.Close()
|
||||
|
||||
buf := bytes.NewBufferString(message)
|
||||
if _, err = buf.WriteTo(wc); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+9
-4
@@ -85,10 +85,15 @@ func (fs *Decomposedfs) SetArbitraryMetadata(ctx context.Context, ref *provider.
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range md.Metadata {
|
||||
attrName := prefixes.MetadataPrefix + k
|
||||
if err = n.SetXattrString(ctx, attrName, v); err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "Decomposedfs: could not set metadata attribute "+attrName+" to "+k))
|
||||
// one write for the whole set: a per key write publishes every intermediate
|
||||
// state to the unlocked, cache first readers
|
||||
if len(md.Metadata) > 0 {
|
||||
attribs := make(map[string][]byte, len(md.Metadata))
|
||||
for k, v := range md.Metadata {
|
||||
attribs[prefixes.MetadataPrefix+k] = []byte(v)
|
||||
}
|
||||
if err = n.SetXattrsWithContext(ctx, attribs); err != nil {
|
||||
errs = append(errs, errors.Wrap(err, "Decomposedfs: could not set metadata attributes"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-45
@@ -39,7 +39,6 @@ import (
|
||||
ocsconv "github.com/opencloud-eu/reva/v2/pkg/conversions"
|
||||
ctxpkg "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/errtypes"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/events"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
|
||||
sdk "github.com/opencloud-eu/reva/v2/pkg/sdk/common"
|
||||
@@ -848,10 +847,20 @@ func (fs *Decomposedfs) linkStorageSpaceType(ctx context.Context, spaceType, spa
|
||||
|
||||
func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node, checkPermissions bool) (*provider.StorageSpace, error) {
|
||||
user := ctxpkg.ContextMustGetUser(ctx)
|
||||
if checkPermissions && n.SpaceRoot.IsDisabled(ctx) {
|
||||
if checkPermissions {
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil || !permissions.IsManager(rp) {
|
||||
return nil, errtypes.PermissionDenied(fmt.Sprintf("user %s is not allowed to list deleted spaces %s", user.Username, n.ID))
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case !rp.Stat:
|
||||
return nil, errtypes.NotFound(fmt.Sprintf("space %s not found", n.ID))
|
||||
}
|
||||
|
||||
if n.SpaceRoot.IsDisabled(ctx) {
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
if err != nil || !permissions.IsManager(rp) {
|
||||
return nil, errtypes.PermissionDenied(fmt.Sprintf("user %s is not allowed to list deleted spaces %s", user.Username, n.ID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -897,10 +906,7 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node,
|
||||
// This way we don't have to have a cron job checking the grants in regular intervals.
|
||||
// The tradeof obviously is that this code is here.
|
||||
if isGrantExpired(g) {
|
||||
var errDeleteGrant, errIndexRemove error
|
||||
|
||||
errDeleteGrant = n.DeleteGrant(ctx, g, true)
|
||||
if errDeleteGrant != nil {
|
||||
if err := n.DeleteGrant(ctx, g, true); err != nil {
|
||||
sublog.Error().Err(err).Str("grantee", id).
|
||||
Msg("failed to delete expired space grant")
|
||||
}
|
||||
@@ -909,43 +915,19 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node,
|
||||
switch g.Grantee.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
// remove from user index
|
||||
errIndexRemove = fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), n.SpaceID)
|
||||
if errIndexRemove != nil {
|
||||
if err := fs.userSpaceIndex.Remove(g.Grantee.GetUserId().GetOpaqueId(), n.SpaceID); err != nil {
|
||||
sublog.Error().Err(err).Str("grantee", id).
|
||||
Msg("failed to delete expired user space index")
|
||||
}
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
// remove from group index
|
||||
errIndexRemove = fs.groupSpaceIndex.Remove(g.Grantee.GetGroupId().GetOpaqueId(), n.SpaceID)
|
||||
if errIndexRemove != nil {
|
||||
if err := fs.groupSpaceIndex.Remove(g.Grantee.GetGroupId().GetOpaqueId(), n.SpaceID); err != nil {
|
||||
sublog.Error().Err(err).Str("grantee", id).
|
||||
Msg("failed to delete expired group space index")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// publish SpaceMembershipExpired event
|
||||
if errDeleteGrant == nil {
|
||||
ev := events.SpaceMembershipExpired{
|
||||
SpaceOwner: n.SpaceOwnerOrManager(ctx),
|
||||
SpaceID: &provider.StorageSpaceId{OpaqueId: n.SpaceID},
|
||||
SpaceName: sname,
|
||||
ExpiredAt: time.Unix(int64(g.Expiration.Seconds), int64(g.Expiration.Nanos)),
|
||||
Timestamp: utils.TSNow(),
|
||||
}
|
||||
switch g.Grantee.Type {
|
||||
case provider.GranteeType_GRANTEE_TYPE_USER:
|
||||
ev.GranteeUserID = g.Grantee.GetUserId()
|
||||
case provider.GranteeType_GRANTEE_TYPE_GROUP:
|
||||
ev.GranteeGroupID = g.Grantee.GetGroupId()
|
||||
}
|
||||
if fs.stream != nil {
|
||||
if err := events.Publish(ctx, fs.stream, ev); err != nil {
|
||||
sublog.Error().Err(err).Msg("error publishing SpaceMembershipExpired event")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
grantExpiration[id] = g.Expiration
|
||||
@@ -953,17 +935,6 @@ func (fs *Decomposedfs) StorageSpaceFromNode(ctx context.Context, n *node.Node,
|
||||
grantMap[id] = g.Permissions
|
||||
}
|
||||
|
||||
// check permissions after expired grants have been removed
|
||||
if checkPermissions {
|
||||
rp, err := fs.p.AssemblePermissions(ctx, n)
|
||||
switch {
|
||||
case err != nil:
|
||||
return nil, err
|
||||
case !rp.Stat:
|
||||
return nil, errtypes.NotFound(fmt.Sprintf("space %s not found", n.ID))
|
||||
}
|
||||
}
|
||||
|
||||
grantMapJSON, err := json.Marshal(grantMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
-28
@@ -24,7 +24,6 @@ import (
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
@@ -47,7 +46,6 @@ import (
|
||||
var (
|
||||
matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)")
|
||||
matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])")
|
||||
matchEmail = regexp.MustCompile(`^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$`)
|
||||
|
||||
// ShareStorageProviderID is the provider id used by the sharestorageprovider
|
||||
ShareStorageProviderID = "a0ca6a90-a365-4782-871e-d44447bbc668"
|
||||
@@ -253,32 +251,6 @@ func GranteeEqual(u, v *provider.Grantee) bool {
|
||||
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)
|
||||
}
|
||||
|
||||
// IsValidWebAddress checks whether the provided address is a valid URL.
|
||||
func IsValidWebAddress(address string) bool {
|
||||
_, err := url.ParseRequestURI(address)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsValidPhoneNumber checks whether the provided phone number has a valid format.
|
||||
func IsValidPhoneNumber(number string) bool {
|
||||
re := regexp.MustCompile(`^(?:(?:\(?(?:00|\+)([1-4]\d\d|[1-9]\d?)\)?)?[\-\.\ \\\/]?)?((?:\(?\d{1,}\)?[\-\.\ \\\/]?){0,})(?:[\-\.\ \\\/]?(?:#|ext\.?|extension|x)[\-\.\ \\\/]?(\d+))?$`)
|
||||
return re.MatchString(number)
|
||||
}
|
||||
|
||||
// IsValidName cheks if the given name doesn't contain any non-alpha, space or dash characters.
|
||||
func IsValidName(name string) bool {
|
||||
re := regexp.MustCompile(`^[A-Za-z\s\-]*$`)
|
||||
return re.MatchString(name)
|
||||
}
|
||||
|
||||
// MarshalProtoV1ToJSON marshals a proto V1 message to a JSON byte array
|
||||
// TODO: update this once we start using V2 in CS3APIs
|
||||
func MarshalProtoV1ToJSON(m proto.Message) ([]byte, error) {
|
||||
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
Copyright Prometheus Team
|
||||
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.
|
||||
-201
@@ -1,201 +0,0 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
Prometheus Alertmanager
|
||||
Copyright 2013-2015 The Prometheus Authors
|
||||
|
||||
This product includes software developed at
|
||||
SoundCloud Ltd. (http://soundcloud.com/).
|
||||
|
||||
|
||||
The following components are included in this product:
|
||||
|
||||
Bootstrap
|
||||
http://getbootstrap.com
|
||||
Copyright 2011-2014 Twitter, Inc.
|
||||
Licensed under the MIT License
|
||||
-164
@@ -1,164 +0,0 @@
|
||||
// Copyright The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package alert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// Alert wraps a model.Alert with additional information relevant
|
||||
// to internal of the Alertmanager.
|
||||
// The type is never exposed to external communication and the
|
||||
// embedded alert has to be sanitized beforehand.
|
||||
type Alert struct {
|
||||
model.Alert
|
||||
|
||||
// The authoritative timestamp.
|
||||
UpdatedAt time.Time
|
||||
Timeout bool
|
||||
}
|
||||
|
||||
// Merge merges the timespan of two alerts based and overwrites annotations
|
||||
// based on the authoritative timestamp. A new alert is returned, the labels
|
||||
// are assumed to be equal.
|
||||
func (a *Alert) Merge(o *Alert) *Alert {
|
||||
// Let o always be the younger alert.
|
||||
if o.UpdatedAt.Before(a.UpdatedAt) {
|
||||
return o.Merge(a)
|
||||
}
|
||||
|
||||
res := *o
|
||||
|
||||
// Always pick the earliest starting time.
|
||||
if a.StartsAt.Before(o.StartsAt) {
|
||||
res.StartsAt = a.StartsAt
|
||||
}
|
||||
|
||||
if o.Resolved() {
|
||||
// The latest explicit resolved timestamp wins if both alerts are effectively resolved.
|
||||
if a.Resolved() && a.EndsAt.After(o.EndsAt) {
|
||||
res.EndsAt = a.EndsAt
|
||||
}
|
||||
} else {
|
||||
// A non-timeout timestamp always rules if it is the latest.
|
||||
if a.EndsAt.After(o.EndsAt) && !a.Timeout {
|
||||
res.EndsAt = a.EndsAt
|
||||
}
|
||||
}
|
||||
|
||||
return &res
|
||||
}
|
||||
|
||||
// Validate overrides the same method in model.Alert to allow UTF-8 labels.
|
||||
// This can be removed once prometheus/common has support for UTF-8.
|
||||
func (a *Alert) Validate() error {
|
||||
if a.StartsAt.IsZero() {
|
||||
return fmt.Errorf("start time missing")
|
||||
}
|
||||
if !a.EndsAt.IsZero() && a.EndsAt.Before(a.StartsAt) {
|
||||
return fmt.Errorf("start time must be before end time")
|
||||
}
|
||||
if len(a.Labels) == 0 {
|
||||
return fmt.Errorf("at least one label pair required")
|
||||
}
|
||||
if err := validateLs(a.Labels); err != nil {
|
||||
return fmt.Errorf("invalid label set: %w", err)
|
||||
}
|
||||
if err := validateLs(a.Annotations); err != nil {
|
||||
return fmt.Errorf("invalid annotations: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AlertSlice is a sortable slice of Alerts.
|
||||
type AlertSlice []*Alert
|
||||
|
||||
func (as AlertSlice) Less(i, j int) bool {
|
||||
// Look at labels.job, then labels.instance.
|
||||
for _, overrideKey := range [...]model.LabelName{"job", "instance"} {
|
||||
iVal, iOk := as[i].Labels[overrideKey]
|
||||
jVal, jOk := as[j].Labels[overrideKey]
|
||||
if !iOk && !jOk {
|
||||
continue
|
||||
}
|
||||
if !iOk {
|
||||
return false
|
||||
}
|
||||
if !jOk {
|
||||
return true
|
||||
}
|
||||
if iVal != jVal {
|
||||
return iVal < jVal
|
||||
}
|
||||
}
|
||||
return as[i].Labels.Before(as[j].Labels)
|
||||
}
|
||||
func (as AlertSlice) Swap(i, j int) { as[i], as[j] = as[j], as[i] }
|
||||
func (as AlertSlice) Len() int { return len(as) }
|
||||
|
||||
// LogValue implements slog.LogValuer. It returns a summary of alert counts per alertname,
|
||||
// e.g. "MyAlert: 3, OtherAlert: 1".
|
||||
func (as AlertSlice) LogValue() slog.Value {
|
||||
if len(as) == 0 {
|
||||
return slog.StringValue("")
|
||||
}
|
||||
|
||||
counts := make(map[string]int, len(as))
|
||||
order := make([]string, 0, len(as))
|
||||
|
||||
for _, a := range as {
|
||||
name := string(a.Labels[model.AlertNameLabel])
|
||||
if _, exists := counts[name]; !exists {
|
||||
order = append(order, name)
|
||||
}
|
||||
counts[name]++
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
// Pre-size the builder to reduce re-allocations.
|
||||
// Rough guess: (avg name length + ": " + "count" + ", ") * unique alerts
|
||||
sb.Grow(len(order) * 20)
|
||||
|
||||
for i, name := range order {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(name)
|
||||
sb.WriteString(": ")
|
||||
sb.WriteString(strconv.Itoa(counts[name]))
|
||||
}
|
||||
|
||||
return slog.StringValue(sb.String())
|
||||
}
|
||||
|
||||
// Alerts turns a sequence of internal alerts into a list of
|
||||
// exposable model.Alert structures.
|
||||
func Alerts(alerts ...*Alert) model.Alerts {
|
||||
res := make(model.Alerts, 0, len(alerts))
|
||||
for _, a := range alerts {
|
||||
v := a.Alert
|
||||
// If the end timestamp is not reached yet, do not expose it.
|
||||
if !a.Resolved() {
|
||||
v.EndsAt = time.Time{}
|
||||
}
|
||||
res = append(res, &v)
|
||||
}
|
||||
return res
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
// Copyright The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package alert
|
||||
|
||||
// AlertState is used as part of AlertStatus.
|
||||
type AlertState string
|
||||
|
||||
// Possible values for AlertState.
|
||||
const (
|
||||
AlertStateUnprocessed AlertState = "unprocessed"
|
||||
AlertStateActive AlertState = "active"
|
||||
AlertStateSuppressed AlertState = "suppressed"
|
||||
)
|
||||
|
||||
// Compare returns -1 if s has lower priority than other, 0 if equal,
|
||||
// and 1 if higher. Priority order: suppressed > active > unprocessed.
|
||||
func (s AlertState) Compare(other AlertState) int {
|
||||
p := func(st AlertState) int {
|
||||
switch st {
|
||||
case AlertStateSuppressed:
|
||||
return 2
|
||||
case AlertStateActive:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
switch a, b := p(s), p(other); {
|
||||
case a < b:
|
||||
return -1
|
||||
case a > b:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
// Copyright The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package alert
|
||||
|
||||
// AlertStatus stores the state of an alert and, as applicable, the IDs of
|
||||
// silences silencing the alert and of other alerts inhibiting the alert. Note
|
||||
// that currently, SilencedBy is supposed to be the complete set of the relevant
|
||||
// silences while InhibitedBy may contain only a subset of the inhibiting alerts
|
||||
// – in practice exactly one ID. (This somewhat confusing semantics might change
|
||||
// in the future.)
|
||||
type AlertStatus struct {
|
||||
State AlertState `json:"state"`
|
||||
SilencedBy []string `json:"silencedBy"`
|
||||
InhibitedBy []string `json:"inhibitedBy"`
|
||||
}
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
// Copyright The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package alert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/prometheus/alertmanager/matcher/compat"
|
||||
)
|
||||
|
||||
func validateLs(ls model.LabelSet) error {
|
||||
for ln, lv := range ls {
|
||||
if !compat.IsValidLabelName(ln) {
|
||||
return fmt.Errorf("invalid name %q", ln)
|
||||
}
|
||||
if !lv.IsValid() {
|
||||
return fmt.Errorf("invalid value %q", lv)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
-197
@@ -1,197 +0,0 @@
|
||||
// Copyright 2023 Prometheus Team
|
||||
// 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.
|
||||
|
||||
package featurecontrol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
FeatureAlertNamesInMetrics = "alert-names-in-metrics"
|
||||
FeatureReceiverNameInMetrics = "receiver-name-in-metrics"
|
||||
FeatureGroupKeyInMetrics = "group-key-in-metrics"
|
||||
FeatureClassicMode = "classic-mode"
|
||||
FeatureUTF8StrictMode = "utf8-strict-mode"
|
||||
FeatureAutoGOMEMLIMIT = "auto-gomemlimit"
|
||||
FeatureEventRecorder = "event-recorder"
|
||||
)
|
||||
|
||||
var AllowedFlags = []string{
|
||||
FeatureAlertNamesInMetrics,
|
||||
FeatureReceiverNameInMetrics,
|
||||
FeatureGroupKeyInMetrics,
|
||||
FeatureClassicMode,
|
||||
FeatureUTF8StrictMode,
|
||||
FeatureAutoGOMEMLIMIT,
|
||||
FeatureEventRecorder,
|
||||
}
|
||||
|
||||
type Flagger interface {
|
||||
EnableAlertNamesInMetrics() bool
|
||||
EnableReceiverNamesInMetrics() bool
|
||||
EnableGroupKeyInMetrics() bool
|
||||
ClassicMode() bool
|
||||
UTF8StrictMode() bool
|
||||
EnableAutoGOMEMLIMIT() bool
|
||||
EnableEventRecorder() bool
|
||||
}
|
||||
|
||||
type Flags struct {
|
||||
logger *slog.Logger
|
||||
enableAlertNamesInMetrics bool
|
||||
enableReceiverNamesInMetrics bool
|
||||
enableGroupKeyInMetrics bool
|
||||
classicMode bool
|
||||
utf8StrictMode bool
|
||||
enableAutoGOMEMLIMIT bool
|
||||
enableEventRecorder bool
|
||||
}
|
||||
|
||||
func (f *Flags) EnableAlertNamesInMetrics() bool {
|
||||
return f.enableAlertNamesInMetrics
|
||||
}
|
||||
|
||||
func (f *Flags) EnableReceiverNamesInMetrics() bool {
|
||||
return f.enableReceiverNamesInMetrics
|
||||
}
|
||||
|
||||
func (f *Flags) EnableGroupKeyInMetrics() bool {
|
||||
return f.enableGroupKeyInMetrics
|
||||
}
|
||||
|
||||
func (f *Flags) ClassicMode() bool {
|
||||
return f.classicMode
|
||||
}
|
||||
|
||||
func (f *Flags) UTF8StrictMode() bool {
|
||||
return f.utf8StrictMode
|
||||
}
|
||||
|
||||
func (f *Flags) EnableAutoGOMEMLIMIT() bool {
|
||||
return f.enableAutoGOMEMLIMIT
|
||||
}
|
||||
|
||||
func (f *Flags) EnableEventRecorder() bool {
|
||||
return f.enableEventRecorder
|
||||
}
|
||||
|
||||
type flagOption func(flags *Flags)
|
||||
|
||||
func enableReceiverNameInMetrics() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.enableReceiverNamesInMetrics = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableGroupKeyInMetrics() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.enableGroupKeyInMetrics = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableClassicMode() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.classicMode = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableUTF8StrictMode() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.utf8StrictMode = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableAutoGOMEMLIMIT() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.enableAutoGOMEMLIMIT = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableEventRecorder() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.enableEventRecorder = true
|
||||
}
|
||||
}
|
||||
|
||||
func enableAlertNamesInMetrics() flagOption {
|
||||
return func(configs *Flags) {
|
||||
configs.enableAlertNamesInMetrics = true
|
||||
}
|
||||
}
|
||||
|
||||
func NewFlags(logger *slog.Logger, features string) (Flagger, error) {
|
||||
fc := &Flags{logger: logger}
|
||||
opts := []flagOption{}
|
||||
|
||||
if len(features) == 0 {
|
||||
return NoopFlags{}, nil
|
||||
}
|
||||
|
||||
for feature := range strings.SplitSeq(features, ",") {
|
||||
switch feature {
|
||||
case FeatureAlertNamesInMetrics:
|
||||
opts = append(opts, enableAlertNamesInMetrics())
|
||||
logger.Warn("Alert names in metrics enabled")
|
||||
case FeatureReceiverNameInMetrics:
|
||||
opts = append(opts, enableReceiverNameInMetrics())
|
||||
logger.Warn("Experimental receiver name in metrics enabled")
|
||||
case FeatureGroupKeyInMetrics:
|
||||
opts = append(opts, enableGroupKeyInMetrics())
|
||||
logger.Warn("Experimental group key in metrics enabled")
|
||||
case FeatureClassicMode:
|
||||
opts = append(opts, enableClassicMode())
|
||||
logger.Warn("Classic mode enabled")
|
||||
case FeatureUTF8StrictMode:
|
||||
opts = append(opts, enableUTF8StrictMode())
|
||||
logger.Warn("UTF-8 strict mode enabled")
|
||||
case FeatureAutoGOMEMLIMIT:
|
||||
opts = append(opts, enableAutoGOMEMLIMIT())
|
||||
logger.Warn("Automatically set GOMEMLIMIT to match the Linux container or system memory limit.")
|
||||
case FeatureEventRecorder:
|
||||
opts = append(opts, enableEventRecorder())
|
||||
logger.Warn("Experimental event recorder enabled")
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown option '%s' for --enable-feature", feature)
|
||||
}
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(fc)
|
||||
}
|
||||
|
||||
if fc.classicMode && fc.utf8StrictMode {
|
||||
return nil, errors.New("cannot have both classic and UTF-8 modes enabled")
|
||||
}
|
||||
|
||||
return fc, nil
|
||||
}
|
||||
|
||||
type NoopFlags struct{}
|
||||
|
||||
func (n NoopFlags) EnableAlertNamesInMetrics() bool { return false }
|
||||
|
||||
func (n NoopFlags) EnableReceiverNamesInMetrics() bool { return false }
|
||||
|
||||
func (n NoopFlags) EnableGroupKeyInMetrics() bool { return false }
|
||||
|
||||
func (n NoopFlags) ClassicMode() bool { return false }
|
||||
|
||||
func (n NoopFlags) UTF8StrictMode() bool { return false }
|
||||
|
||||
func (n NoopFlags) EnableAutoGOMEMLIMIT() bool { return false }
|
||||
|
||||
func (n NoopFlags) EnableEventRecorder() bool { return false }
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
// Copyright 2023 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package compat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/common/promslog"
|
||||
|
||||
"github.com/prometheus/alertmanager/featurecontrol"
|
||||
"github.com/prometheus/alertmanager/matcher/parse"
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
)
|
||||
|
||||
var (
|
||||
isValidLabelName = isValidClassicLabelName(promslog.NewNopLogger())
|
||||
parseMatcher = ClassicMatcherParser(promslog.NewNopLogger())
|
||||
parseMatchers = ClassicMatchersParser(promslog.NewNopLogger())
|
||||
)
|
||||
|
||||
// IsValidLabelName returns true if the string is a valid label name.
|
||||
func IsValidLabelName(name model.LabelName) bool {
|
||||
return isValidLabelName(name)
|
||||
}
|
||||
|
||||
type ParseMatcher func(input, origin string) (*labels.Matcher, error)
|
||||
|
||||
type ParseMatchers func(input, origin string) (labels.Matchers, error)
|
||||
|
||||
// Matcher parses the matcher in the input string. It returns an error
|
||||
// if the input is invalid or contains two or more matchers.
|
||||
func Matcher(input, origin string) (*labels.Matcher, error) {
|
||||
return parseMatcher(input, origin)
|
||||
}
|
||||
|
||||
// Matchers parses one or more matchers in the input string. It returns
|
||||
// an error if the input is invalid.
|
||||
func Matchers(input, origin string) (labels.Matchers, error) {
|
||||
return parseMatchers(input, origin)
|
||||
}
|
||||
|
||||
// InitFromFlags initializes the compat package from the flagger.
|
||||
func InitFromFlags(l *slog.Logger, f featurecontrol.Flagger) {
|
||||
if f.ClassicMode() {
|
||||
isValidLabelName = isValidClassicLabelName(l)
|
||||
parseMatcher = ClassicMatcherParser(l)
|
||||
parseMatchers = ClassicMatchersParser(l)
|
||||
} else if f.UTF8StrictMode() {
|
||||
isValidLabelName = isValidUTF8LabelName(l)
|
||||
parseMatcher = UTF8MatcherParser(l)
|
||||
parseMatchers = UTF8MatchersParser(l)
|
||||
} else {
|
||||
isValidLabelName = isValidUTF8LabelName(l)
|
||||
parseMatcher = FallbackMatcherParser(l)
|
||||
parseMatchers = FallbackMatchersParser(l)
|
||||
}
|
||||
}
|
||||
|
||||
// ClassicMatcherParser uses the pkg/labels parser to parse the matcher in
|
||||
// the input string.
|
||||
func ClassicMatcherParser(l *slog.Logger) ParseMatcher {
|
||||
return func(input, origin string) (matcher *labels.Matcher, err error) {
|
||||
l.Debug("Parsing with classic matchers parser", "input", input, "origin", origin)
|
||||
return labels.ParseMatcher(input)
|
||||
}
|
||||
}
|
||||
|
||||
// ClassicMatchersParser uses the pkg/labels parser to parse zero or more
|
||||
// matchers in the input string. It returns an error if the input is invalid.
|
||||
func ClassicMatchersParser(l *slog.Logger) ParseMatchers {
|
||||
return func(input, origin string) (matchers labels.Matchers, err error) {
|
||||
l.Debug("Parsing with classic matchers parser", "input", input, "origin", origin)
|
||||
return labels.ParseMatchers(input)
|
||||
}
|
||||
}
|
||||
|
||||
// UTF8MatcherParser uses the new matcher/parse parser to parse the matcher
|
||||
// in the input string. If this fails it does not revert to the pkg/labels parser.
|
||||
func UTF8MatcherParser(l *slog.Logger) ParseMatcher {
|
||||
return func(input, origin string) (matcher *labels.Matcher, err error) {
|
||||
l.Debug("Parsing with UTF-8 matchers parser", "input", input, "origin", origin)
|
||||
if strings.HasPrefix(input, "{") || strings.HasSuffix(input, "}") {
|
||||
return nil, fmt.Errorf("unexpected open or close brace: %s", input)
|
||||
}
|
||||
return parse.Matcher(input)
|
||||
}
|
||||
}
|
||||
|
||||
// UTF8MatchersParser uses the new matcher/parse parser to parse zero or more
|
||||
// matchers in the input string. If this fails it does not revert to the
|
||||
// pkg/labels parser.
|
||||
func UTF8MatchersParser(l *slog.Logger) ParseMatchers {
|
||||
return func(input, origin string) (matchers labels.Matchers, err error) {
|
||||
l.Debug("Parsing with UTF-8 matchers parser", "input", input, "origin", origin)
|
||||
return parse.Matchers(input)
|
||||
}
|
||||
}
|
||||
|
||||
// FallbackMatcherParser uses the new matcher/parse parser to parse zero or more
|
||||
// matchers in the string. If this fails it reverts to the pkg/labels parser and
|
||||
// emits a warning log line.
|
||||
func FallbackMatcherParser(l *slog.Logger) ParseMatcher {
|
||||
return func(input, origin string) (matcher *labels.Matcher, err error) {
|
||||
l.Debug("Parsing with UTF-8 matchers parser, with fallback to classic matchers parser", "input", input, "origin", origin)
|
||||
if strings.HasPrefix(input, "{") || strings.HasSuffix(input, "}") {
|
||||
return nil, fmt.Errorf("unexpected open or close brace: %s", input)
|
||||
}
|
||||
// Parse the input in both parsers to look for disagreement and incompatible
|
||||
// inputs.
|
||||
nMatcher, nErr := parse.Matcher(input)
|
||||
cMatcher, cErr := labels.ParseMatcher(input)
|
||||
if nErr != nil {
|
||||
// If the input is invalid in both parsers, return the error.
|
||||
if cErr != nil {
|
||||
return nil, cErr
|
||||
}
|
||||
// The input is valid in the pkg/labels parser, but not the matcher/parse
|
||||
// parser. This means the input is not forwards compatible.
|
||||
suggestion := cMatcher.String()
|
||||
l.Warn("Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion)
|
||||
return cMatcher, nil
|
||||
}
|
||||
// If the input is valid in both parsers, but produces different results,
|
||||
// then there is disagreement.
|
||||
if cErr == nil && !reflect.DeepEqual(nMatcher, cMatcher) {
|
||||
l.Warn("Matchers input has disagreement", "input", input, "origin", origin)
|
||||
return cMatcher, nil
|
||||
}
|
||||
return nMatcher, nil
|
||||
}
|
||||
}
|
||||
|
||||
// FallbackMatchersParser uses the new matcher/parse parser to parse the
|
||||
// matcher in the input string. If this fails it falls back to the pkg/labels
|
||||
// parser and emits a warning log line.
|
||||
func FallbackMatchersParser(l *slog.Logger) ParseMatchers {
|
||||
return func(input, origin string) (matchers labels.Matchers, err error) {
|
||||
l.Debug("Parsing with UTF-8 matchers parser, with fallback to classic matchers parser", "input", input, "origin", origin)
|
||||
// Parse the input in both parsers to look for disagreement and incompatible
|
||||
// inputs.
|
||||
nMatchers, nErr := parse.Matchers(input)
|
||||
cMatchers, cErr := labels.ParseMatchers(input)
|
||||
if nErr != nil {
|
||||
// If the input is invalid in both parsers, return the error.
|
||||
if cErr != nil {
|
||||
return nil, cErr
|
||||
}
|
||||
// The input is valid in the pkg/labels parser, but not the matcher/parse
|
||||
// parser. This means the input is not forwards compatible.
|
||||
var sb strings.Builder
|
||||
for i, n := range cMatchers {
|
||||
sb.WriteString(n.String())
|
||||
if i < len(cMatchers)-1 {
|
||||
sb.WriteRune(',')
|
||||
}
|
||||
}
|
||||
suggestion := sb.String()
|
||||
// The input is valid in the pkg/labels parser, but not the
|
||||
// new matcher/parse parser.
|
||||
l.Warn("Alertmanager is moving to a new parser for labels and matchers, and this input is incompatible. Alertmanager has instead parsed the input using the classic matchers parser as a fallback. To make this input compatible with the UTF-8 matchers parser please make sure all regular expressions and values are double-quoted and backslashes are escaped. If you are still seeing this message please open an issue.", "input", input, "origin", origin, "err", nErr, "suggestion", suggestion)
|
||||
return cMatchers, nil
|
||||
}
|
||||
// If the input is valid in both parsers, but produces different results,
|
||||
// then there is disagreement. We need to compare to labels.Matchers(cMatchers)
|
||||
// as cMatchers is a []*labels.Matcher not labels.Matchers.
|
||||
if cErr == nil && !reflect.DeepEqual(nMatchers, labels.Matchers(cMatchers)) {
|
||||
l.Warn("Matchers input has disagreement", "input", input, "origin", origin)
|
||||
return cMatchers, nil
|
||||
}
|
||||
return nMatchers, nil
|
||||
}
|
||||
}
|
||||
|
||||
// isValidClassicLabelName returns true if the string is a valid classic label name.
|
||||
func isValidClassicLabelName(_ *slog.Logger) func(model.LabelName) bool {
|
||||
return func(name model.LabelName) bool {
|
||||
return model.LegacyValidation.IsValidLabelName(string(name))
|
||||
}
|
||||
}
|
||||
|
||||
// isValidUTF8LabelName returns true if the string is a valid UTF-8 label name.
|
||||
func isValidUTF8LabelName(_ *slog.Logger) func(model.LabelName) bool {
|
||||
return func(name model.LabelName) bool {
|
||||
if len(name) == 0 {
|
||||
return false
|
||||
}
|
||||
return utf8.ValidString(string(name))
|
||||
}
|
||||
}
|
||||
-309
@@ -1,309 +0,0 @@
|
||||
// Copyright 2023 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const (
|
||||
eof rune = -1
|
||||
)
|
||||
|
||||
func isReserved(r rune) bool {
|
||||
return unicode.IsSpace(r) || strings.ContainsRune("{}!=~,\\\"'`", r)
|
||||
}
|
||||
|
||||
// expectedError is returned when the next rune does not match what is expected.
|
||||
type expectedError struct {
|
||||
position
|
||||
input string
|
||||
expected string
|
||||
}
|
||||
|
||||
func (e expectedError) Error() string {
|
||||
if e.offsetEnd >= len(e.input) {
|
||||
return fmt.Sprintf("%d:%d: unexpected end of input, expected one of '%s'",
|
||||
e.columnStart,
|
||||
e.columnEnd,
|
||||
e.expected,
|
||||
)
|
||||
}
|
||||
return fmt.Sprintf("%d:%d: %s: expected one of '%s'",
|
||||
e.columnStart,
|
||||
e.columnEnd,
|
||||
e.input[e.offsetStart:e.offsetEnd],
|
||||
e.expected,
|
||||
)
|
||||
}
|
||||
|
||||
// invalidInputError is returned when the next rune in the input does not match
|
||||
// the grammar of Prometheus-like matchers.
|
||||
type invalidInputError struct {
|
||||
position
|
||||
input string
|
||||
}
|
||||
|
||||
func (e invalidInputError) Error() string {
|
||||
return fmt.Sprintf("%d:%d: %s: invalid input",
|
||||
e.columnStart,
|
||||
e.columnEnd,
|
||||
e.input[e.offsetStart:e.offsetEnd],
|
||||
)
|
||||
}
|
||||
|
||||
// unterminatedError is returned when text in quotes does not have a closing quote.
|
||||
type unterminatedError struct {
|
||||
position
|
||||
input string
|
||||
quote rune
|
||||
}
|
||||
|
||||
func (e unterminatedError) Error() string {
|
||||
return fmt.Sprintf("%d:%d: %s: missing end %c",
|
||||
e.columnStart,
|
||||
e.columnEnd,
|
||||
e.input[e.offsetStart:e.offsetEnd],
|
||||
e.quote,
|
||||
)
|
||||
}
|
||||
|
||||
// lexer scans a sequence of tokens that match the grammar of Prometheus-like
|
||||
// matchers. A token is emitted for each call to scan() which returns the
|
||||
// next token in the input or an error if the input does not conform to the
|
||||
// grammar. A token can be one of a number of kinds and corresponds to a
|
||||
// subslice of the input. Once the input has been consumed successive calls to
|
||||
// scan() return a tokenEOF token.
|
||||
type lexer struct {
|
||||
input string
|
||||
err error
|
||||
start int // The offset of the current token.
|
||||
pos int // The position of the cursor in the input.
|
||||
width int // The width of the last rune.
|
||||
column int // The column offset of the current token.
|
||||
cols int // The number of columns (runes) decoded from the input.
|
||||
}
|
||||
|
||||
// Scans the next token in the input or an error if the input does not
|
||||
// conform to the grammar. Once the input has been consumed successive
|
||||
// calls scan() return a tokenEOF token.
|
||||
func (l *lexer) scan() (token, error) {
|
||||
t := token{}
|
||||
// Do not attempt to emit more tokens if the input is invalid.
|
||||
if l.err != nil {
|
||||
return t, l.err
|
||||
}
|
||||
// Iterate over each rune in the input and either emit a token or an error.
|
||||
for r := l.next(); r != eof; r = l.next() {
|
||||
switch {
|
||||
case r == '{':
|
||||
t = l.emit(tokenOpenBrace)
|
||||
return t, l.err
|
||||
case r == '}':
|
||||
t = l.emit(tokenCloseBrace)
|
||||
return t, l.err
|
||||
case r == ',':
|
||||
t = l.emit(tokenComma)
|
||||
return t, l.err
|
||||
case r == '=' || r == '!':
|
||||
l.rewind()
|
||||
t, l.err = l.scanOperator()
|
||||
return t, l.err
|
||||
case r == '"':
|
||||
l.rewind()
|
||||
t, l.err = l.scanQuoted()
|
||||
return t, l.err
|
||||
case !isReserved(r):
|
||||
l.rewind()
|
||||
t, l.err = l.scanUnquoted()
|
||||
return t, l.err
|
||||
case unicode.IsSpace(r):
|
||||
l.skip()
|
||||
default:
|
||||
l.err = invalidInputError{
|
||||
position: l.position(),
|
||||
input: l.input,
|
||||
}
|
||||
return t, l.err
|
||||
}
|
||||
}
|
||||
return t, l.err
|
||||
}
|
||||
|
||||
func (l *lexer) scanOperator() (token, error) {
|
||||
// If the first rune is an '!' then it must be followed with either an
|
||||
// '=' or '~' to not match a string or regex.
|
||||
if l.accept("!") {
|
||||
if l.accept("=") {
|
||||
return l.emit(tokenNotEquals), nil
|
||||
}
|
||||
if l.accept("~") {
|
||||
return l.emit(tokenNotMatches), nil
|
||||
}
|
||||
return token{}, expectedError{
|
||||
position: l.position(),
|
||||
input: l.input,
|
||||
expected: "=~",
|
||||
}
|
||||
}
|
||||
// If the first rune is an '=' then it can be followed with an optional
|
||||
// '~' to match a regex.
|
||||
if l.accept("=") {
|
||||
if l.accept("~") {
|
||||
return l.emit(tokenMatches), nil
|
||||
}
|
||||
return l.emit(tokenEquals), nil
|
||||
}
|
||||
return token{}, expectedError{
|
||||
position: l.position(),
|
||||
input: l.input,
|
||||
expected: "!=",
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lexer) scanQuoted() (token, error) {
|
||||
if err := l.expect("\""); err != nil {
|
||||
return token{}, err
|
||||
}
|
||||
var isEscaped bool
|
||||
for r := l.next(); r != eof; r = l.next() {
|
||||
if isEscaped {
|
||||
isEscaped = false
|
||||
} else if r == '\\' {
|
||||
isEscaped = true
|
||||
} else if r == '"' {
|
||||
l.rewind()
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := l.expect("\""); err != nil {
|
||||
return token{}, unterminatedError{
|
||||
position: l.position(),
|
||||
input: l.input,
|
||||
quote: '"',
|
||||
}
|
||||
}
|
||||
return l.emit(tokenQuoted), nil
|
||||
}
|
||||
|
||||
func (l *lexer) scanUnquoted() (token, error) {
|
||||
for r := l.next(); r != eof; r = l.next() {
|
||||
if isReserved(r) {
|
||||
l.rewind()
|
||||
break
|
||||
}
|
||||
}
|
||||
return l.emit(tokenUnquoted), nil
|
||||
}
|
||||
|
||||
// peek the next token in the input or an error if the input does not
|
||||
// conform to the grammar. Once the input has been consumed successive
|
||||
// calls peek() return a tokenEOF token.
|
||||
func (l *lexer) peek() (token, error) {
|
||||
start := l.start
|
||||
pos := l.pos
|
||||
width := l.width
|
||||
column := l.column
|
||||
cols := l.cols
|
||||
// Do not reset l.err because we can return it on the next call to scan().
|
||||
defer func() {
|
||||
l.start = start
|
||||
l.pos = pos
|
||||
l.width = width
|
||||
l.column = column
|
||||
l.cols = cols
|
||||
}()
|
||||
return l.scan()
|
||||
}
|
||||
|
||||
// position returns the position of the last emitted token.
|
||||
func (l *lexer) position() position {
|
||||
return position{
|
||||
offsetStart: l.start,
|
||||
offsetEnd: l.pos,
|
||||
columnStart: l.column,
|
||||
columnEnd: l.cols,
|
||||
}
|
||||
}
|
||||
|
||||
// accept consumes the next if its one of the valid runes.
|
||||
// It returns true if the next rune was accepted, otherwise false.
|
||||
func (l *lexer) accept(valid string) bool {
|
||||
if strings.ContainsRune(valid, l.next()) {
|
||||
return true
|
||||
}
|
||||
l.rewind()
|
||||
return false
|
||||
}
|
||||
|
||||
// expect consumes the next rune if its one of the valid runes.
|
||||
// It returns nil if the next rune is valid, otherwise an expectedError
|
||||
// error.
|
||||
func (l *lexer) expect(valid string) error {
|
||||
if strings.ContainsRune(valid, l.next()) {
|
||||
return nil
|
||||
}
|
||||
l.rewind()
|
||||
return expectedError{
|
||||
position: l.position(),
|
||||
input: l.input,
|
||||
expected: valid,
|
||||
}
|
||||
}
|
||||
|
||||
// emits returns the scanned input as a token.
|
||||
func (l *lexer) emit(kind tokenKind) token {
|
||||
t := token{
|
||||
kind: kind,
|
||||
value: l.input[l.start:l.pos],
|
||||
position: l.position(),
|
||||
}
|
||||
l.start = l.pos
|
||||
l.column = l.cols
|
||||
return t
|
||||
}
|
||||
|
||||
// next returns the next rune in the input or eof.
|
||||
func (l *lexer) next() rune {
|
||||
if l.pos >= len(l.input) {
|
||||
l.width = 0
|
||||
return eof
|
||||
}
|
||||
r, width := utf8.DecodeRuneInString(l.input[l.pos:])
|
||||
l.width = width
|
||||
l.pos += width
|
||||
l.cols++
|
||||
return r
|
||||
}
|
||||
|
||||
// rewind the last rune in the input. It should not be called more than once
|
||||
// between consecutive calls of next.
|
||||
func (l *lexer) rewind() {
|
||||
l.pos -= l.width
|
||||
// When the next rune in the input is eof the width is zero. This check
|
||||
// prevents cols from being decremented when the next rune being accepted
|
||||
// is instead eof.
|
||||
if l.width > 0 {
|
||||
l.cols--
|
||||
}
|
||||
}
|
||||
|
||||
// skip the scanned input between start and pos.
|
||||
func (l *lexer) skip() {
|
||||
l.start = l.pos
|
||||
l.column = l.cols
|
||||
}
|
||||
-308
@@ -1,308 +0,0 @@
|
||||
// Copyright 2023 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/prometheus/alertmanager/pkg/labels"
|
||||
)
|
||||
|
||||
var (
|
||||
errEOF = errors.New("end of input")
|
||||
errExpectedEOF = errors.New("expected end of input")
|
||||
errNoOpenBrace = errors.New("expected opening brace")
|
||||
errNoCloseBrace = errors.New("expected close brace")
|
||||
errNoLabelName = errors.New("expected label name")
|
||||
errNoLabelValue = errors.New("expected label value")
|
||||
errNoOperator = errors.New("expected an operator such as '=', '!=', '=~' or '!~'")
|
||||
errExpectedComma = errors.New("expected a comma")
|
||||
errExpectedCommaOrCloseBrace = errors.New("expected a comma or close brace")
|
||||
errExpectedMatcherOrCloseBrace = errors.New("expected a matcher or close brace after comma")
|
||||
)
|
||||
|
||||
// Matchers parses one or more matchers in the input string. It returns an error
|
||||
// if the input is invalid.
|
||||
func Matchers(input string) (matchers labels.Matchers, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "parser panic: %s, %s", r, debug.Stack())
|
||||
err = errors.New("parser panic: this should never happen, check stderr for the stack trace")
|
||||
}
|
||||
}()
|
||||
p := parser{lexer: lexer{input: input}}
|
||||
return p.parse()
|
||||
}
|
||||
|
||||
// Matcher parses the matcher in the input string. It returns an error
|
||||
// if the input is invalid or contains two or more matchers.
|
||||
func Matcher(input string) (*labels.Matcher, error) {
|
||||
m, err := Matchers(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch len(m) {
|
||||
case 1:
|
||||
return m[0], nil
|
||||
case 0:
|
||||
return nil, fmt.Errorf("no matchers")
|
||||
default:
|
||||
return nil, fmt.Errorf("expected 1 matcher, found %d", len(m))
|
||||
}
|
||||
}
|
||||
|
||||
// parseFunc is state in the finite state automata.
|
||||
type parseFunc func(l *lexer) (parseFunc, error)
|
||||
|
||||
// parser reads the sequence of tokens from the lexer and returns either a
|
||||
// series of matchers or an error. It works as a finite state automata, where
|
||||
// each state in the automata is a parseFunc. The finite state automata can move
|
||||
// from one state to another by returning the next parseFunc. It terminates when
|
||||
// a parseFunc returns nil as the next parseFunc, if the lexer attempts to scan
|
||||
// input that does not match the expected grammar, or if the tokens returned from
|
||||
// the lexer cannot be parsed into a complete series of matchers.
|
||||
type parser struct {
|
||||
matchers labels.Matchers
|
||||
// Tracks if the input starts with an open brace and if we should expect to
|
||||
// parse a close brace at the end of the input.
|
||||
hasOpenBrace bool
|
||||
lexer lexer
|
||||
}
|
||||
|
||||
func (p *parser) parse() (labels.Matchers, error) {
|
||||
var (
|
||||
err error
|
||||
fn = p.parseOpenBrace
|
||||
l = &p.lexer
|
||||
)
|
||||
for {
|
||||
if fn, err = fn(l); err != nil {
|
||||
return nil, err
|
||||
} else if fn == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return p.matchers, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseOpenBrace(l *lexer) (parseFunc, error) {
|
||||
var (
|
||||
hasCloseBrace bool
|
||||
err error
|
||||
)
|
||||
// Can start with an optional open brace.
|
||||
p.hasOpenBrace, err = p.accept(l, tokenOpenBrace)
|
||||
if err != nil {
|
||||
if errors.Is(err, errEOF) {
|
||||
return p.parseEOF, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
// If the next token is a close brace there are no matchers in the input.
|
||||
hasCloseBrace, err = p.acceptPeek(l, tokenCloseBrace)
|
||||
if err != nil {
|
||||
// If there is no more input after the open brace then parse the close brace
|
||||
// so the error message contains ErrNoCloseBrace.
|
||||
if errors.Is(err, errEOF) {
|
||||
return p.parseCloseBrace, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if hasCloseBrace {
|
||||
return p.parseCloseBrace, nil
|
||||
}
|
||||
return p.parseMatcher, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseCloseBrace(l *lexer) (parseFunc, error) {
|
||||
if p.hasOpenBrace {
|
||||
// If there was an open brace there must be a matching close brace.
|
||||
if _, err := p.expect(l, tokenCloseBrace); err != nil {
|
||||
return nil, fmt.Errorf("0:%d: %w: %w", l.position().columnEnd, err, errNoCloseBrace)
|
||||
}
|
||||
} else {
|
||||
// If there was no open brace there must not be a close brace either.
|
||||
if _, err := p.expect(l, tokenCloseBrace); err == nil {
|
||||
return nil, fmt.Errorf("0:%d: }: %w", l.position().columnEnd, errNoOpenBrace)
|
||||
}
|
||||
}
|
||||
return p.parseEOF, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseMatcher(l *lexer) (parseFunc, error) {
|
||||
var (
|
||||
err error
|
||||
t token
|
||||
matchName, matchValue string
|
||||
matchTy labels.MatchType
|
||||
)
|
||||
// The first token should be the label name.
|
||||
if t, err = p.expect(l, tokenQuoted, tokenUnquoted); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", err, errNoLabelName)
|
||||
}
|
||||
matchName, err = t.unquote()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: %s: invalid input", t.columnStart, t.columnEnd, t.value)
|
||||
}
|
||||
// The next token should be the operator.
|
||||
if t, err = p.expect(l, tokenEquals, tokenNotEquals, tokenMatches, tokenNotMatches); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", err, errNoOperator)
|
||||
}
|
||||
switch t.kind {
|
||||
case tokenEquals:
|
||||
matchTy = labels.MatchEqual
|
||||
case tokenNotEquals:
|
||||
matchTy = labels.MatchNotEqual
|
||||
case tokenMatches:
|
||||
matchTy = labels.MatchRegexp
|
||||
case tokenNotMatches:
|
||||
matchTy = labels.MatchNotRegexp
|
||||
default:
|
||||
panic(fmt.Sprintf("bad operator %s", t))
|
||||
}
|
||||
// The next token should be the match value. Like the match name, this too
|
||||
// can be either double-quoted UTF-8 or unquoted UTF-8 without reserved characters.
|
||||
if t, err = p.expect(l, tokenUnquoted, tokenQuoted); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", err, errNoLabelValue)
|
||||
}
|
||||
matchValue, err = t.unquote()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%d:%d: %s: invalid input", t.columnStart, t.columnEnd, t.value)
|
||||
}
|
||||
m, err := labels.NewMatcher(matchTy, matchName, matchValue)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create matcher: %w", err)
|
||||
}
|
||||
p.matchers = append(p.matchers, m)
|
||||
return p.parseEndOfMatcher, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseEndOfMatcher(l *lexer) (parseFunc, error) {
|
||||
t, err := p.expectPeek(l, tokenComma, tokenCloseBrace)
|
||||
if err != nil {
|
||||
if errors.Is(err, errEOF) {
|
||||
// If this is the end of input we still need to check if the optional
|
||||
// open brace has a matching close brace.
|
||||
return p.parseCloseBrace, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %w", err, errExpectedCommaOrCloseBrace)
|
||||
}
|
||||
switch t.kind {
|
||||
case tokenComma:
|
||||
return p.parseComma, nil
|
||||
case tokenCloseBrace:
|
||||
return p.parseCloseBrace, nil
|
||||
default:
|
||||
panic(fmt.Sprintf("bad token %s", t))
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseComma(l *lexer) (parseFunc, error) {
|
||||
if _, err := p.expect(l, tokenComma); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", err, errExpectedComma)
|
||||
}
|
||||
// The token after the comma can be another matcher, a close brace or end of input.
|
||||
t, err := p.expectPeek(l, tokenCloseBrace, tokenUnquoted, tokenQuoted)
|
||||
if err != nil {
|
||||
if errors.Is(err, errEOF) {
|
||||
// If this is the end of input we still need to check if the optional
|
||||
// open brace has a matching close brace.
|
||||
return p.parseCloseBrace, nil
|
||||
}
|
||||
return nil, fmt.Errorf("%w: %w", err, errExpectedMatcherOrCloseBrace)
|
||||
}
|
||||
if t.kind == tokenCloseBrace {
|
||||
return p.parseCloseBrace, nil
|
||||
}
|
||||
return p.parseMatcher, nil
|
||||
}
|
||||
|
||||
func (p *parser) parseEOF(l *lexer) (parseFunc, error) {
|
||||
t, err := l.scan()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", err, errExpectedEOF)
|
||||
}
|
||||
if !t.isEOF() {
|
||||
return nil, fmt.Errorf("%d:%d: %s: %w", t.columnStart, t.columnEnd, t.value, errExpectedEOF)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// nolint:godot
|
||||
// accept returns true if the next token is one of the specified kinds,
|
||||
// otherwise false. If the token is accepted it is consumed. tokenEOF is
|
||||
// not an accepted kind and instead accept returns ErrEOF if there is no
|
||||
// more input.
|
||||
func (p *parser) accept(l *lexer, kinds ...tokenKind) (ok bool, err error) {
|
||||
ok, err = p.acceptPeek(l, kinds...)
|
||||
if ok {
|
||||
if _, err = l.scan(); err != nil {
|
||||
panic("failed to scan peeked token")
|
||||
}
|
||||
}
|
||||
return ok, err
|
||||
}
|
||||
|
||||
// nolint:godot
|
||||
// acceptPeek returns true if the next token is one of the specified kinds,
|
||||
// otherwise false. However, unlike accept, acceptPeek does not consume accepted
|
||||
// tokens. tokenEOF is not an accepted kind and instead accept returns ErrEOF
|
||||
// if there is no more input.
|
||||
func (p *parser) acceptPeek(l *lexer, kinds ...tokenKind) (bool, error) {
|
||||
t, err := l.peek()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if t.isEOF() {
|
||||
return false, errEOF
|
||||
}
|
||||
return t.isOneOf(kinds...), nil
|
||||
}
|
||||
|
||||
// nolint:godot
|
||||
// expect returns the next token if it is one of the specified kinds, otherwise
|
||||
// it returns an error. If the token is expected it is consumed. tokenEOF is not
|
||||
// an accepted kind and instead expect returns ErrEOF if there is no more input.
|
||||
func (p *parser) expect(l *lexer, kind ...tokenKind) (token, error) {
|
||||
t, err := p.expectPeek(l, kind...)
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
if _, err = l.scan(); err != nil {
|
||||
panic("failed to scan peeked token")
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// nolint:godot
|
||||
// expect returns the next token if it is one of the specified kinds, otherwise
|
||||
// it returns an error. However, unlike expect, expectPeek does not consume tokens.
|
||||
// tokenEOF is not an accepted kind and instead expect returns ErrEOF if there is no
|
||||
// more input.
|
||||
func (p *parser) expectPeek(l *lexer, kind ...tokenKind) (token, error) {
|
||||
t, err := l.peek()
|
||||
if err != nil {
|
||||
return t, err
|
||||
}
|
||||
if t.isEOF() {
|
||||
return t, errEOF
|
||||
}
|
||||
if !t.isOneOf(kind...) {
|
||||
return t, fmt.Errorf("%d:%d: unexpected %s", t.columnStart, t.columnEnd, t.value)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
// Copyright 2023 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package parse
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
tokenEOF tokenKind = iota
|
||||
tokenOpenBrace
|
||||
tokenCloseBrace
|
||||
tokenComma
|
||||
tokenEquals
|
||||
tokenNotEquals
|
||||
tokenMatches
|
||||
tokenNotMatches
|
||||
tokenQuoted
|
||||
tokenUnquoted
|
||||
)
|
||||
|
||||
func (k tokenKind) String() string {
|
||||
switch k {
|
||||
case tokenOpenBrace:
|
||||
return "OpenBrace"
|
||||
case tokenCloseBrace:
|
||||
return "CloseBrace"
|
||||
case tokenComma:
|
||||
return "Comma"
|
||||
case tokenEquals:
|
||||
return "Equals"
|
||||
case tokenNotEquals:
|
||||
return "NotEquals"
|
||||
case tokenMatches:
|
||||
return "Matches"
|
||||
case tokenNotMatches:
|
||||
return "NotMatches"
|
||||
case tokenQuoted:
|
||||
return "Quoted"
|
||||
case tokenUnquoted:
|
||||
return "Unquoted"
|
||||
default:
|
||||
return "EOF"
|
||||
}
|
||||
}
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
value string
|
||||
position
|
||||
}
|
||||
|
||||
// isEOF returns true if the token is an end of file token.
|
||||
func (t token) isEOF() bool {
|
||||
return t.kind == tokenEOF
|
||||
}
|
||||
|
||||
// isOneOf returns true if the token is one of the specified kinds.
|
||||
func (t token) isOneOf(kinds ...tokenKind) bool {
|
||||
return slices.Contains(kinds, t.kind)
|
||||
}
|
||||
|
||||
// unquote the value in token. If unquoted returns it unmodified.
|
||||
func (t token) unquote() (string, error) {
|
||||
if t.kind == tokenQuoted {
|
||||
unquoted, err := strconv.Unquote(t.value)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !utf8.ValidString(unquoted) {
|
||||
return "", errors.New("quoted string contains invalid UTF-8 code points")
|
||||
}
|
||||
return unquoted, nil
|
||||
}
|
||||
return t.value, nil
|
||||
}
|
||||
|
||||
func (t token) String() string {
|
||||
return fmt.Sprintf("(%s) '%s'", t.kind, t.value)
|
||||
}
|
||||
|
||||
type position struct {
|
||||
offsetStart int // The start position in the input.
|
||||
offsetEnd int // The end position in the input.
|
||||
columnStart int // The column number.
|
||||
columnEnd int // The end of the column.
|
||||
}
|
||||
-232
@@ -1,232 +0,0 @@
|
||||
// Copyright 2017 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
// MatchType is an enum for label matching types.
|
||||
type MatchType int
|
||||
|
||||
// Possible MatchTypes.
|
||||
const (
|
||||
MatchEqual MatchType = iota
|
||||
MatchNotEqual
|
||||
MatchRegexp
|
||||
MatchNotRegexp
|
||||
)
|
||||
|
||||
func (m MatchType) String() string {
|
||||
typeToStr := map[MatchType]string{
|
||||
MatchEqual: "=",
|
||||
MatchNotEqual: "!=",
|
||||
MatchRegexp: "=~",
|
||||
MatchNotRegexp: "!~",
|
||||
}
|
||||
if str, ok := typeToStr[m]; ok {
|
||||
return str
|
||||
}
|
||||
panic("unknown match type")
|
||||
}
|
||||
|
||||
// Matcher models the matching of a label.
|
||||
type Matcher struct {
|
||||
Type MatchType
|
||||
Name string
|
||||
Value string
|
||||
|
||||
re *regexp.Regexp
|
||||
}
|
||||
|
||||
// NewMatcher returns a matcher object.
|
||||
func NewMatcher(t MatchType, n, v string) (*Matcher, error) {
|
||||
m := &Matcher{
|
||||
Type: t,
|
||||
Name: n,
|
||||
Value: v,
|
||||
}
|
||||
if t == MatchRegexp || t == MatchNotRegexp {
|
||||
re, err := regexp.Compile("^(?:" + v + ")$")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.re = re
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Matcher) String() string {
|
||||
if strings.ContainsFunc(m.Name, isReserved) {
|
||||
return fmt.Sprintf(`%s%s%s`, strconv.Quote(m.Name), m.Type, strconv.Quote(m.Value))
|
||||
}
|
||||
return fmt.Sprintf(`%s%s"%s"`, m.Name, m.Type, openMetricsEscape(m.Value))
|
||||
}
|
||||
|
||||
// Matches returns whether the matcher matches the given string value.
|
||||
func (m *Matcher) Matches(s string) bool {
|
||||
switch m.Type {
|
||||
case MatchEqual:
|
||||
return s == m.Value
|
||||
case MatchNotEqual:
|
||||
return s != m.Value
|
||||
case MatchRegexp:
|
||||
return m.re.MatchString(s)
|
||||
case MatchNotRegexp:
|
||||
return !m.re.MatchString(s)
|
||||
}
|
||||
panic("labels.Matcher.Matches: invalid match type")
|
||||
}
|
||||
|
||||
type apiV1Matcher struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
IsRegex bool `json:"isRegex"`
|
||||
IsEqual bool `json:"isEqual"`
|
||||
}
|
||||
|
||||
// MarshalJSON retains backwards compatibility with types.Matcher for the v1 API.
|
||||
func (m Matcher) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(apiV1Matcher{
|
||||
Name: m.Name,
|
||||
Value: m.Value,
|
||||
IsRegex: m.Type == MatchRegexp || m.Type == MatchNotRegexp,
|
||||
IsEqual: m.Type == MatchRegexp || m.Type == MatchEqual,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Matcher) UnmarshalJSON(data []byte) error {
|
||||
v1m := apiV1Matcher{
|
||||
IsEqual: true,
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, &v1m); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var t MatchType
|
||||
switch {
|
||||
case v1m.IsEqual && !v1m.IsRegex:
|
||||
t = MatchEqual
|
||||
case !v1m.IsEqual && !v1m.IsRegex:
|
||||
t = MatchNotEqual
|
||||
case v1m.IsEqual && v1m.IsRegex:
|
||||
t = MatchRegexp
|
||||
case !v1m.IsEqual && v1m.IsRegex:
|
||||
t = MatchNotRegexp
|
||||
}
|
||||
|
||||
matcher, err := NewMatcher(t, v1m.Name, v1m.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*m = *matcher
|
||||
return nil
|
||||
}
|
||||
|
||||
// openMetricsEscape is similar to the usual string escaping, but more
|
||||
// restricted. It merely replaces a new-line character with '\n', a double-quote
|
||||
// character with '\"', and a backslash with '\\', which is the escaping used by
|
||||
// OpenMetrics.
|
||||
func openMetricsEscape(s string) string {
|
||||
r := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
"\n", `\n`,
|
||||
`"`, `\"`,
|
||||
)
|
||||
return r.Replace(s)
|
||||
}
|
||||
|
||||
// Matchers is a slice of Matchers that is sortable, implements Stringer, and
|
||||
// provides a Matches method to match a LabelSet against all Matchers in the
|
||||
// slice. Note that some users of Matchers might require it to be sorted.
|
||||
type Matchers []*Matcher
|
||||
|
||||
func (ms Matchers) Len() int { return len(ms) }
|
||||
func (ms Matchers) Swap(i, j int) { ms[i], ms[j] = ms[j], ms[i] }
|
||||
|
||||
func (ms Matchers) Less(i, j int) bool {
|
||||
if ms[i].Name > ms[j].Name {
|
||||
return false
|
||||
}
|
||||
if ms[i].Name < ms[j].Name {
|
||||
return true
|
||||
}
|
||||
if ms[i].Value > ms[j].Value {
|
||||
return false
|
||||
}
|
||||
if ms[i].Value < ms[j].Value {
|
||||
return true
|
||||
}
|
||||
return ms[i].Type < ms[j].Type
|
||||
}
|
||||
|
||||
// Matches checks whether all matchers are fulfilled against the given label set.
|
||||
func (ms Matchers) Matches(lset model.LabelSet) bool {
|
||||
for _, m := range ms {
|
||||
if !m.Matches(string(lset[model.LabelName(m.Name)])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (ms Matchers) String() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
buf.WriteByte('{')
|
||||
for i, m := range ms {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(m.String())
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// MatcherSet is a slice of Matchers pointers that implements OR logic across
|
||||
// multiple matcher sets. At least one matcher set must match for the MatcherSet
|
||||
// to match.
|
||||
type MatcherSet []*Matchers
|
||||
|
||||
// Matches checks whether at least one matcher set is fulfilled against the given
|
||||
// label set (OR logic across matcher sets, AND logic within each set).
|
||||
func (ms MatcherSet) Matches(lset model.LabelSet) bool {
|
||||
for _, matchers := range ms {
|
||||
if (*matchers).Matches(lset) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// This is copied from matcher/parse/lexer.go. It will be removed when
|
||||
// the transition window from classic matchers to UTF-8 matchers is complete,
|
||||
// as then we can use double quotes when printing the label name for all
|
||||
// matchers. Until then, the classic parser does not understand double quotes
|
||||
// around the label name, so we use this function as a heuristic to tell if
|
||||
// the matcher was parsed with the UTF-8 parser or the classic parser.
|
||||
func isReserved(r rune) bool {
|
||||
return unicode.IsSpace(r) || strings.ContainsRune("{}!=~,\\\"'`", r)
|
||||
}
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
// Copyright 2018 Prometheus Team
|
||||
// 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.
|
||||
|
||||
package labels
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var (
|
||||
// '=~' has to come before '=' because otherwise only the '='
|
||||
// will be consumed, and the '~' will be part of the 3rd token.
|
||||
re = regexp.MustCompile(`^\s*([a-zA-Z_:][a-zA-Z0-9_:]*)\s*(=~|=|!=|!~)\s*((?s).*?)\s*$`)
|
||||
typeMap = map[string]MatchType{
|
||||
"=": MatchEqual,
|
||||
"!=": MatchNotEqual,
|
||||
"=~": MatchRegexp,
|
||||
"!~": MatchNotRegexp,
|
||||
}
|
||||
)
|
||||
|
||||
// ParseMatchers parses a comma-separated list of Matchers. A leading '{' and/or
|
||||
// a trailing '}' is optional and will be trimmed before further
|
||||
// parsing. Individual Matchers are separated by commas outside of quoted parts
|
||||
// of the input string. Those commas may be surrounded by whitespace. Parts of the
|
||||
// string inside unescaped double quotes ('"…"') are considered quoted (and
|
||||
// commas don't act as separators there). If double quotes are escaped with a
|
||||
// single backslash ('\"'), they are ignored for the purpose of identifying
|
||||
// quoted parts of the input string. If the input string, after trimming the
|
||||
// optional trailing '}', ends with a comma, followed by optional whitespace,
|
||||
// this comma and whitespace will be trimmed.
|
||||
//
|
||||
// Examples for valid input strings:
|
||||
//
|
||||
// {foo = "bar", dings != "bums", }
|
||||
// foo=bar,dings!=bums
|
||||
// foo=bar, dings!=bums
|
||||
// {quote="She said: \"Hi, ladies! That's gender-neutral…\""}
|
||||
// statuscode=~"5.."
|
||||
//
|
||||
// See ParseMatcher for details on how an individual Matcher is parsed.
|
||||
func ParseMatchers(s string) ([]*Matcher, error) {
|
||||
matchers := []*Matcher{}
|
||||
s = strings.TrimPrefix(s, "{")
|
||||
s = strings.TrimSuffix(s, "}")
|
||||
|
||||
var (
|
||||
insideQuotes bool
|
||||
escaped bool
|
||||
token strings.Builder
|
||||
tokens []string
|
||||
)
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case ',':
|
||||
if !insideQuotes {
|
||||
tokens = append(tokens, token.String())
|
||||
token.Reset()
|
||||
continue
|
||||
}
|
||||
case '"':
|
||||
if !escaped {
|
||||
insideQuotes = !insideQuotes
|
||||
} else {
|
||||
escaped = false
|
||||
}
|
||||
case '\\':
|
||||
escaped = !escaped
|
||||
default:
|
||||
escaped = false
|
||||
}
|
||||
token.WriteRune(r)
|
||||
}
|
||||
if s := strings.TrimSpace(token.String()); s != "" {
|
||||
tokens = append(tokens, s)
|
||||
}
|
||||
for _, token := range tokens {
|
||||
m, err := ParseMatcher(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
matchers = append(matchers, m)
|
||||
}
|
||||
|
||||
return matchers, nil
|
||||
}
|
||||
|
||||
// ParseMatcher parses a matcher with a syntax inspired by PromQL and
|
||||
// OpenMetrics. This syntax is convenient to describe filters and selectors in
|
||||
// UIs and config files. To support the interactive nature of the use cases, the
|
||||
// parser is in various aspects fairly tolerant.
|
||||
//
|
||||
// The syntax of a matcher consists of three tokens: (1) A valid Prometheus
|
||||
// label name. (2) One of '=', '!=', '=~', or '!~', with the same meaning as
|
||||
// known from PromQL selectors. (3) A UTF-8 string, which may be enclosed in
|
||||
// double quotes. Before or after each token, there may be any amount of
|
||||
// whitespace, which will be discarded. The 3rd token may be the empty
|
||||
// string. Within the 3rd token, OpenMetrics escaping rules apply: '\"' for a
|
||||
// double-quote, '\n' for a line feed, '\\' for a literal backslash. Unescaped
|
||||
// '"' must not occur inside the 3rd token (only as the 1st or last
|
||||
// character). However, literal line feed characters are tolerated, as are
|
||||
// single '\' characters not followed by '\', 'n', or '"'. They act as a literal
|
||||
// backslash in that case.
|
||||
func ParseMatcher(s string) (_ *Matcher, err error) {
|
||||
ms := re.FindStringSubmatch(s)
|
||||
if len(ms) == 0 {
|
||||
return nil, fmt.Errorf("bad matcher format: %s", s)
|
||||
}
|
||||
|
||||
var (
|
||||
rawValue = ms[3]
|
||||
value strings.Builder
|
||||
escaped bool
|
||||
expectTrailingQuote bool
|
||||
)
|
||||
|
||||
if after, ok := strings.CutPrefix(rawValue, "\""); ok {
|
||||
rawValue = after
|
||||
expectTrailingQuote = true
|
||||
}
|
||||
|
||||
if !utf8.ValidString(rawValue) {
|
||||
return nil, fmt.Errorf("matcher value not valid UTF-8: %s", ms[3])
|
||||
}
|
||||
|
||||
// Unescape the rawValue.
|
||||
for i, r := range rawValue {
|
||||
if escaped {
|
||||
escaped = false
|
||||
switch r {
|
||||
case 'n':
|
||||
value.WriteByte('\n')
|
||||
case '"', '\\':
|
||||
value.WriteRune(r)
|
||||
default:
|
||||
// This was a spurious escape, so treat the '\' as literal.
|
||||
value.WriteByte('\\')
|
||||
value.WriteRune(r)
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch r {
|
||||
case '\\':
|
||||
if i < len(rawValue)-1 {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
// '\' encountered as last byte. Treat it as literal.
|
||||
value.WriteByte('\\')
|
||||
case '"':
|
||||
if !expectTrailingQuote || i < len(rawValue)-1 {
|
||||
return nil, fmt.Errorf("matcher value contains unescaped double quote: %s", ms[3])
|
||||
}
|
||||
expectTrailingQuote = false
|
||||
default:
|
||||
value.WriteRune(r)
|
||||
}
|
||||
}
|
||||
|
||||
if expectTrailingQuote {
|
||||
return nil, fmt.Errorf("matcher value contains unescaped double quote: %s", ms[3])
|
||||
}
|
||||
|
||||
return NewMatcher(typeMap[ms[2]], ms[1], value.String())
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
/node_modules/
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
node_modules: package-lock.json
|
||||
npm ci
|
||||
|
||||
email.tmpl: email.html inline-css.js node_modules
|
||||
@echo ">> inline css for html email template"
|
||||
node ./inline-css
|
||||
-236
@@ -1,236 +0,0 @@
|
||||
{{ define "__alertmanager" }}Alertmanager{{ end }}
|
||||
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }}
|
||||
|
||||
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
|
||||
{{ define "__description" }}{{ end }}
|
||||
|
||||
{{ define "__text_alert_list" }}{{ range . }}Labels:
|
||||
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}Annotations:
|
||||
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}Source: {{ .GeneratorURL }}
|
||||
{{ end }}{{ end }}
|
||||
|
||||
{{ define "__text_alert_list_markdown" }}{{ range . }}
|
||||
Labels:
|
||||
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}
|
||||
Annotations:
|
||||
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}
|
||||
Source: {{ .GeneratorURL }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }}
|
||||
{{ define "slack.default.callbackid" }}{{ end }}
|
||||
{{ define "slack.default.pretext" }}{{ end }}
|
||||
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "slack.default.iconemoji" }}{{ end }}
|
||||
{{ define "slack.default.iconurl" }}{{ end }}
|
||||
{{ define "slack.default.text" }}{{ end }}
|
||||
{{ define "slack.default.footer" }}{{ end }}
|
||||
{{ define "slack.default.color" }}{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}{{ end }}
|
||||
|
||||
|
||||
{{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }}
|
||||
|
||||
|
||||
{{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
|
||||
|
||||
{{ define "wechat.default.message" }}{{ template "__subject" . }}
|
||||
{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
AlertmanagerUrl:
|
||||
{{ template "__alertmanagerURL" . }}
|
||||
{{- end }}
|
||||
{{ define "wechat.default.to_user" }}{{ end }}
|
||||
{{ define "wechat.default.to_party" }}{{ end }}
|
||||
{{ define "wechat.default.to_tag" }}{{ end }}
|
||||
{{ define "wechat.default.agent_id" }}{{ end }}
|
||||
|
||||
|
||||
|
||||
{{ define "victorops.default.state_message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ define "victorops.default.entity_display_name" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "victorops.default.monitoring_tool" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
|
||||
{{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
|
||||
{{ define "sns.default.subject" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "sns.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "telegram.default.message" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "discord.default.content" }}{{ end }}
|
||||
{{ define "discord.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "discord.default.message" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "webex.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "msteams.default.summary" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "msteams.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "msteams.default.text" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
# Alerts Firing:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
# Alerts Resolved:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "msteamsv2.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "msteamsv2.default.text" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
# Alerts Firing:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
# Alerts Resolved:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "jira.default.summary" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "jira.default.description" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
# Alerts Firing:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
# Alerts Resolved:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{- define "jira.default.priority" -}}
|
||||
{{- $priority := "" }}
|
||||
{{- range .Alerts.Firing -}}
|
||||
{{- $severity := index .Labels "severity" -}}
|
||||
{{- if (eq $severity "critical") -}}
|
||||
{{- $priority = "High" -}}
|
||||
{{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
|
||||
{{- $priority = "Medium" -}}
|
||||
{{- else if (and (eq $severity "info") (eq $priority "")) -}}
|
||||
{{- $priority = "Low" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- if eq $priority "" -}}
|
||||
{{- range .Alerts.Resolved -}}
|
||||
{{- $severity := index .Labels "severity" -}}
|
||||
{{- if (eq $severity "critical") -}}
|
||||
{{- $priority = "High" -}}
|
||||
{{- else if (and (eq $severity "warning") (ne $priority "High")) -}}
|
||||
{{- $priority = "Medium" -}}
|
||||
{{- else if (and (eq $severity "info") (eq $priority "")) -}}
|
||||
{{- $priority = "Low" -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- end -}}
|
||||
{{- $priority -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ define "rocketchat.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "rocketchat.default.alias" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "rocketchat.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "rocketchat.default.emoji" }}{{ end }}
|
||||
{{ define "rocketchat.default.iconurl" }}{{ end }}
|
||||
{{ define "rocketchat.default.text" }}{{ end }}
|
||||
|
||||
{{ define "mattermost.default.color" }}{{ if eq .Status "firing" }}danger{{ else }}good{{ end }}{{ end }}
|
||||
{{ define "mattermost.default.username" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "mattermost.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "mattermost.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "mattermost.default.fallback" }}{{ template "mattermost.default.title" . }} | {{ template "mattermost.default.titlelink" . }}{{ end }}
|
||||
{{ define "mattermost.default.text" }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
# Alerts Firing:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
# Alerts Resolved:
|
||||
{{ template "__text_alert_list_markdown" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
-412
@@ -1,412 +0,0 @@
|
||||
<!--
|
||||
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
|
||||
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Mailgun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>{{ template "__subject" . }}</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
GLOBAL
|
||||
A very basic CSS reset
|
||||
------------------------------------- */
|
||||
* {
|
||||
margin: 0;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
box-sizing: border-box;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: none;
|
||||
width: 100% !important;
|
||||
height: 100%;
|
||||
line-height: 1.6em;
|
||||
/* 1.6em * 14px = 22.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
/* Let's make sure all tables have defaults */
|
||||
table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
body {
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
.body-wrap {
|
||||
background-color: #f6f6f6;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: block !important;
|
||||
max-width: 600px !important;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
clear: both !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background-color: #fff;
|
||||
border: 1px solid #e9e9e9;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.content-block {
|
||||
padding: 0 0 20px;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100%;
|
||||
clear: both;
|
||||
color: #999;
|
||||
padding: 20px;
|
||||
}
|
||||
.footer p, .footer a, .footer td {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1, h2, h3 {
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
color: #000;
|
||||
margin: 40px 0 0;
|
||||
line-height: 1.2em;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 500;
|
||||
/* 1.2em * 32px = 38.4px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 38px;*/
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
/* 1.2em * 24px = 28.8px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 29px;*/
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
/* 1.2em * 18px = 21.6px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 22px;*/
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p, ul, ol {
|
||||
margin-bottom: 10px;
|
||||
font-weight: normal;
|
||||
}
|
||||
p li, ul li, ol li {
|
||||
margin-left: 5px;
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
LINKS & BUTTONS
|
||||
------------------------------------- */
|
||||
a {
|
||||
color: #348eda;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
text-decoration: none;
|
||||
color: #FFF;
|
||||
background-color: #348eda;
|
||||
border: solid #348eda;
|
||||
border-width: 10px 20px;
|
||||
line-height: 2em;
|
||||
/* 2em * 14px = 28px, use px to get airier line-height also in Thunderbird, and Yahoo!, Outlook.com, AOL webmail clients */
|
||||
/*line-height: 28px;*/
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
border-radius: 5px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.aligncenter {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alignright {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.alignleft {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
ALERTS
|
||||
Change the class depending on warning email, good email or bad email
|
||||
------------------------------------- */
|
||||
.alert {
|
||||
font-size: 16px;
|
||||
color: #fff;
|
||||
font-weight: 500;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 3px 3px 0 0;
|
||||
}
|
||||
.alert a {
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
.alert.alert-warning {
|
||||
background-color: #E6522C;
|
||||
}
|
||||
.alert.alert-bad {
|
||||
background-color: #D0021B;
|
||||
}
|
||||
.alert.alert-good {
|
||||
background-color: #68B90F;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
INVOICE
|
||||
Styles for the billing table
|
||||
------------------------------------- */
|
||||
.invoice {
|
||||
margin: 40px auto;
|
||||
text-align: left;
|
||||
width: 80%;
|
||||
}
|
||||
.invoice td {
|
||||
padding: 5px 0;
|
||||
}
|
||||
.invoice .invoice-items {
|
||||
width: 100%;
|
||||
}
|
||||
.invoice .invoice-items td {
|
||||
border-top: #eee 1px solid;
|
||||
}
|
||||
.invoice .invoice-items .total td {
|
||||
border-top: 2px solid #333;
|
||||
border-bottom: 2px solid #333;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="https://schema.org/EmailMessage">
|
||||
|
||||
<table class="body-wrap">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container" width="600">
|
||||
<div class="content">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<td class="alert alert-warning">
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ else }}
|
||||
<td class="alert alert-good">
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ end }}
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-wrap">
|
||||
<table width="100%" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<a href='{{ template "__alertmanagerURL" . }}' class="btn-primary">View in {{ template "__alertmanager" . }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>[{{ .Alerts.Firing | len }}] Firing</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Firing }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>Labels</strong><br />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}">Source</a><br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<br />
|
||||
<hr />
|
||||
<br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>[{{ .Alerts.Resolved | len }}] Resolved</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Resolved }}
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<strong>Labels</strong><br />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong>Annotations</strong><br />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}">Source</a><br />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer">
|
||||
<table width="100%">
|
||||
<tr>
|
||||
<td class="aligncenter content-block"><a href='{{ .ExternalURL }}'>Sent by {{ template "__alertmanager" . }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
-179
@@ -1,179 +0,0 @@
|
||||
|
||||
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "email.default.html" }}
|
||||
<!--
|
||||
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
|
||||
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Mailgun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>{{ template "__subject" . }}</title>
|
||||
<style>
|
||||
@media only screen and (max-width: 640px) {
|
||||
body {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-weight: 800 !important;
|
||||
margin: 20px 0 5px !important;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px !important;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 18px !important;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
||||
.invoice {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body itemscope itemtype="https://schema.org/EmailMessage" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; background-color: #f6f6f6; width: 100%;">
|
||||
|
||||
<table class="body-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #f6f6f6; width: 100%;" width="100%" bgcolor="#f6f6f6">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
|
||||
<td class="container" width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block; max-width: 600px; margin: 0 auto; clear: both;" valign="top">
|
||||
<div class="content" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; margin: 0 auto; display: block; padding: 20px;">
|
||||
<table class="main" width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; background-color: #fff; border: 1px solid #e9e9e9; border-radius: 3px;" bgcolor="#fff">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<td class="alert alert-warning" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C;" valign="top" align="center" bgcolor="#E6522C">
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ else }}
|
||||
<td class="alert alert-good" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; font-size: 16px; color: #fff; font-weight: 500; padding: 20px; text-align: center; border-radius: 3px 3px 0 0; background-color: #68B90F;" valign="top" align="center" bgcolor="#68B90F">
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
{{ end }}
|
||||
</tr>
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-wrap" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 30px;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{ template "__alertmanagerURL" . }}" class="btn-primary" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; text-decoration: none; color: #FFF; background-color: #348eda; border: solid #348eda; border-width: 10px 20px; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize;">View in {{ template "__alertmanager" . }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Firing | len }}] Firing</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Firing }}
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<hr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Resolved }}
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; padding: 0 0 20px;" valign="top">
|
||||
<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Labels</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">Annotations</strong><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">{{ end }}
|
||||
<a href="{{ .GeneratorURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline;">Source</a><br style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="footer" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; padding: 20px;">
|
||||
<table width="100%" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<tr style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px;">
|
||||
<td class="aligncenter content-block" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; vertical-align: top; padding: 0 0 20px; text-align: center; color: #999; font-size: 12px;" valign="top" align="center"><a href="{{ .ExternalURL }}" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; text-decoration: underline; color: #999; font-size: 12px;">Sent by {{ template "__alertmanager" . }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top;" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{{ end }}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Copyright 2021 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
const juice = require('juice')
|
||||
const fs = require('fs')
|
||||
|
||||
const inputFile = 'email.html'
|
||||
const outputFile = 'email.tmpl'
|
||||
|
||||
var inputData = ''
|
||||
|
||||
try {
|
||||
inputData = fs.readFileSync(inputFile, 'utf8')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
var templateData = juice(inputData)
|
||||
|
||||
const outputData = `
|
||||
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "email.default.html" }}
|
||||
${templateData}
|
||||
{{ end }}
|
||||
`
|
||||
|
||||
fs.writeFileSync(outputFile, outputData)
|
||||
-511
@@ -1,511 +0,0 @@
|
||||
{
|
||||
"name": "template",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "template",
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"juice": "10.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio": {
|
||||
"version": "1.0.0-rc.12",
|
||||
"resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz",
|
||||
"integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio-select": "^2.1.0",
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"htmlparser2": "^8.0.1",
|
||||
"parse5": "^7.0.0",
|
||||
"parse5-htmlparser2-tree-adapter": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/cheerio?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/cheerio/node_modules/cheerio-select": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz",
|
||||
"integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/css-select": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/css-what": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/htmlparser2": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3",
|
||||
"domutils": "^3.0.1",
|
||||
"entities": "^4.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/parse5-htmlparser2-tree-adapter": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz",
|
||||
"integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domhandler": "^5.0.3",
|
||||
"parse5": "^7.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/cheerio/node_modules/parse5/node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz",
|
||||
"integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/juice": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/juice/-/juice-10.0.1.tgz",
|
||||
"integrity": "sha512-ZhJT1soxJCkOiO55/mz8yeBKTAJhRzX9WBO+16ZTqNTONnnVlUPyVBIzQ7lDRjaBdTbid+bAnyIon/GM3yp4cA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cheerio": "1.0.0-rc.12",
|
||||
"commander": "^6.1.0",
|
||||
"mensch": "^0.3.4",
|
||||
"slick": "^1.12.2",
|
||||
"web-resource-inliner": "^6.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"juice": "bin/juice"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/mensch": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/mensch/-/mensch-0.3.4.tgz",
|
||||
"integrity": "sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/slick": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/slick/-/slick-1.12.2.tgz",
|
||||
"integrity": "sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==",
|
||||
"dev": true,
|
||||
"license": "MIT (http://mootools.net/license.txt)",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-6.0.1.tgz",
|
||||
"integrity": "sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-colors": "^4.1.1",
|
||||
"escape-goat": "^3.0.0",
|
||||
"htmlparser2": "^5.0.0",
|
||||
"mime": "^2.4.6",
|
||||
"node-fetch": "^2.6.0",
|
||||
"valid-data-url": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/ansi-colors": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
|
||||
"integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/dom-serializer": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||
"integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^4.2.0",
|
||||
"entities": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/dom-serializer/node_modules/domhandler": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
|
||||
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
],
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/domhandler": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz",
|
||||
"integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/domutils": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz",
|
||||
"integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^1.0.1",
|
||||
"domelementtype": "^2.2.0",
|
||||
"domhandler": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/domutils/node_modules/domhandler": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz",
|
||||
"integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/entities": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/escape-goat": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz",
|
||||
"integrity": "sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/htmlparser2": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-5.0.1.tgz",
|
||||
"integrity": "sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.0.1",
|
||||
"domhandler": "^3.3.0",
|
||||
"domutils": "^2.4.2",
|
||||
"entities": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/htmlparser2?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/node-fetch": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-url": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "4.x || >=6.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"encoding": "^0.1.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"encoding": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/tr46": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/valid-data-url": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-3.0.1.tgz",
|
||||
"integrity": "sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/webidl-conversions": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/web-resource-inliner/node_modules/whatwg-url": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "~0.0.3",
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "template",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"juice": "10.0.1"
|
||||
}
|
||||
}
|
||||
-536
@@ -1,536 +0,0 @@
|
||||
// Copyright 2015 Prometheus Team
|
||||
// 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.
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
tmplhtml "html/template"
|
||||
"io"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
tmpltext "text/template"
|
||||
"time"
|
||||
|
||||
commonTemplates "github.com/prometheus/common/helpers/templates"
|
||||
"github.com/prometheus/common/model"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
)
|
||||
|
||||
//go:embed default.tmpl email.tmpl
|
||||
var asset embed.FS
|
||||
|
||||
// Template bundles a text and a html template instance.
|
||||
type Template struct {
|
||||
text *tmpltext.Template
|
||||
html *tmplhtml.Template
|
||||
|
||||
ExternalURL *url.URL
|
||||
}
|
||||
|
||||
// Option is generic modifier of the text and html templates used by a Template.
|
||||
type Option func(text *tmpltext.Template, html *tmplhtml.Template)
|
||||
|
||||
// New returns a new Template with the DefaultFuncs added. The DefaultFuncs
|
||||
// have precedence over any added custom functions. Options allow customization
|
||||
// of the text and html templates in given order.
|
||||
func New(options ...Option) (*Template, error) {
|
||||
t := &Template{
|
||||
text: tmpltext.New("").Option("missingkey=zero"),
|
||||
html: tmplhtml.New("").Option("missingkey=zero"),
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(t.text, t.html)
|
||||
}
|
||||
|
||||
t.text.Funcs(tmpltext.FuncMap(DefaultFuncs))
|
||||
t.html.Funcs(tmplhtml.FuncMap(DefaultFuncs))
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// FromGlobs calls ParseGlob on all path globs provided and returns the
|
||||
// resulting Template.
|
||||
func FromGlobs(paths []string, options ...Option) (*Template, error) {
|
||||
t, err := New(options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defaultTemplates := []string{"default.tmpl", "email.tmpl"}
|
||||
|
||||
for _, file := range defaultTemplates {
|
||||
f, err := asset.Open(file)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := t.Parse(f); err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
f.Close()
|
||||
}
|
||||
|
||||
for _, tp := range paths {
|
||||
if err := t.FromGlob(tp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// Parse parses the given text into the template.
|
||||
func (t *Template) Parse(r io.Reader) error {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.text, err = t.text.Parse(string(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.html, err = t.html.Parse(string(b)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FromGlob calls ParseGlob on given path glob provided and parses into the
|
||||
// template.
|
||||
func (t *Template) FromGlob(path string) error {
|
||||
// ParseGlob in the template packages errors if not at least one file is
|
||||
// matched. We want to allow empty matches that may be populated later on.
|
||||
p, err := filepath.Glob(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(p) > 0 {
|
||||
if t.text, err = t.text.ParseGlob(path); err != nil {
|
||||
return err
|
||||
}
|
||||
if t.html, err = t.html.ParseGlob(path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExecuteTextString needs a meaningful doc comment (TODO(fabxc)).
|
||||
func (t *Template) ExecuteTextString(text string, data any) (string, error) {
|
||||
if text == "" {
|
||||
return "", nil
|
||||
}
|
||||
tmpl, err := t.text.Clone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, data)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
// ExecuteHTMLString needs a meaningful doc comment (TODO(fabxc)).
|
||||
func (t *Template) ExecuteHTMLString(html string, data any) (string, error) {
|
||||
if html == "" {
|
||||
return "", nil
|
||||
}
|
||||
tmpl, err := t.html.Clone()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tmpl, err = tmpl.New("").Option("missingkey=zero").Parse(html)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
err = tmpl.Execute(&buf, data)
|
||||
return buf.String(), err
|
||||
}
|
||||
|
||||
type FuncMap map[string]any
|
||||
|
||||
var DefaultFuncs = FuncMap{
|
||||
"toUpper": strings.ToUpper,
|
||||
"toLower": strings.ToLower,
|
||||
"title": func(text string) string {
|
||||
// Casers should not be shared between goroutines, instead
|
||||
// create a new caser each time this function is called.
|
||||
return cases.Title(language.AmericanEnglish).String(text)
|
||||
},
|
||||
"trimSpace": strings.TrimSpace,
|
||||
// join is equal to strings.Join but inverts the argument order
|
||||
// for easier pipelining in templates.
|
||||
"join": func(sep string, s []string) string {
|
||||
return strings.Join(s, sep)
|
||||
},
|
||||
"match": regexp.MatchString,
|
||||
"safeHtml": func(text string) tmplhtml.HTML {
|
||||
return tmplhtml.HTML(text)
|
||||
},
|
||||
"safeUrl": func(text string) tmplhtml.URL {
|
||||
return tmplhtml.URL(text)
|
||||
},
|
||||
"urlUnescape": url.QueryUnescape,
|
||||
"reReplaceAll": func(pattern, repl, text string) string {
|
||||
re := regexp.MustCompile(pattern)
|
||||
return re.ReplaceAllString(text, repl)
|
||||
},
|
||||
"stringSlice": func(s ...string) []string {
|
||||
return s
|
||||
},
|
||||
// date returns the text representation of the time in the specified format.
|
||||
"date": func(fmt string, t time.Time) string {
|
||||
return t.Format(fmt)
|
||||
},
|
||||
// tz returns the time in the timezone.
|
||||
"tz": func(name string, t time.Time) (time.Time, error) {
|
||||
loc, err := time.LoadLocation(name)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return t.In(loc), nil
|
||||
},
|
||||
"now": time.Now,
|
||||
"since": time.Since,
|
||||
"humanizeDuration": commonTemplates.HumanizeDuration,
|
||||
"toJson": func(v any) (string, error) {
|
||||
bytes, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
},
|
||||
"list": func(args ...any) ([]any, error) {
|
||||
if args == nil {
|
||||
return []any{}, nil
|
||||
}
|
||||
return args, nil
|
||||
},
|
||||
"append": func(slice []any, args ...any) []any {
|
||||
return append(slice, args...)
|
||||
},
|
||||
"dict": func(values ...any) (map[string]any, error) {
|
||||
if len(values)%2 != 0 {
|
||||
return nil, fmt.Errorf("dict requires an even number of arguments")
|
||||
}
|
||||
|
||||
res := make(map[string]any, len(values)/2)
|
||||
for i := 0; i < len(values); i += 2 {
|
||||
key, ok := values[i].(string)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("dict keys must be strings")
|
||||
}
|
||||
res[key] = values[i+1]
|
||||
}
|
||||
|
||||
return res, nil
|
||||
},
|
||||
}
|
||||
|
||||
// Pair is a key/value string pair.
|
||||
type Pair struct {
|
||||
Name, Value string
|
||||
}
|
||||
|
||||
// Pairs is a list of key/value string pairs.
|
||||
type Pairs []Pair
|
||||
|
||||
// Names returns a list of names of the pairs.
|
||||
func (ps Pairs) Names() []string {
|
||||
ns := make([]string, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
ns = append(ns, p.Name)
|
||||
}
|
||||
return ns
|
||||
}
|
||||
|
||||
// Values returns a list of values of the pairs.
|
||||
func (ps Pairs) Values() []string {
|
||||
vs := make([]string, 0, len(ps))
|
||||
for _, p := range ps {
|
||||
vs = append(vs, p.Value)
|
||||
}
|
||||
return vs
|
||||
}
|
||||
|
||||
func (ps Pairs) String() string {
|
||||
b := strings.Builder{}
|
||||
for i, p := range ps {
|
||||
b.WriteString(p.Name)
|
||||
b.WriteRune('=')
|
||||
b.WriteString(p.Value)
|
||||
if i < len(ps)-1 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// KV is a set of key/value string pairs.
|
||||
type KV map[string]string
|
||||
|
||||
// SortedPairs returns a sorted list of key/value pairs.
|
||||
func (kv KV) SortedPairs() Pairs {
|
||||
var (
|
||||
pairs = make([]Pair, 0, len(kv))
|
||||
keys = make([]string, 0, len(kv))
|
||||
sortStart = 0
|
||||
)
|
||||
for k := range kv {
|
||||
if k == string(model.AlertNameLabel) {
|
||||
keys = append([]string{k}, keys...)
|
||||
sortStart = 1
|
||||
} else {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys[sortStart:])
|
||||
|
||||
for _, k := range keys {
|
||||
pairs = append(pairs, Pair{k, kv[k]})
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
// Remove returns a copy of the key/value set without the given keys.
|
||||
func (kv KV) Remove(keys []string) KV {
|
||||
keySet := make(map[string]struct{}, len(keys))
|
||||
for _, k := range keys {
|
||||
keySet[k] = struct{}{}
|
||||
}
|
||||
|
||||
res := KV{}
|
||||
for k, v := range kv {
|
||||
if _, ok := keySet[k]; !ok {
|
||||
res[k] = v
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Names returns the names of the label names in the LabelSet.
|
||||
func (kv KV) Names() []string {
|
||||
return kv.SortedPairs().Names()
|
||||
}
|
||||
|
||||
// Values returns a list of the values in the LabelSet.
|
||||
func (kv KV) Values() []string {
|
||||
return kv.SortedPairs().Values()
|
||||
}
|
||||
|
||||
func (kv KV) String() string {
|
||||
return kv.SortedPairs().String()
|
||||
}
|
||||
|
||||
// Data is the data passed to notification templates and webhook pushes.
|
||||
//
|
||||
// End-users should not be exposed to Go's type system, as this will confuse them and prevent
|
||||
// simple things like simple equality checks to fail. Map everything to float64/string.
|
||||
type Data struct {
|
||||
Receiver string `json:"receiver"`
|
||||
Status string `json:"status"`
|
||||
Alerts Alerts `json:"alerts"`
|
||||
|
||||
NotificationReason string `json:"notification_reason"`
|
||||
|
||||
GroupLabels KV `json:"groupLabels"`
|
||||
CommonLabels KV `json:"commonLabels"`
|
||||
CommonAnnotations KV `json:"commonAnnotations"`
|
||||
|
||||
ExternalURL string `json:"externalURL"`
|
||||
}
|
||||
|
||||
// Alert holds one alert for notification templates.
|
||||
type Alert struct {
|
||||
Status string `json:"status"`
|
||||
Labels KV `json:"labels"`
|
||||
Annotations KV `json:"annotations"`
|
||||
StartsAt time.Time `json:"startsAt"`
|
||||
EndsAt time.Time `json:"endsAt"`
|
||||
GeneratorURL string `json:"generatorURL"`
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
}
|
||||
|
||||
// Alerts is a list of Alert objects.
|
||||
type Alerts []Alert
|
||||
|
||||
// Firing returns the subset of alerts that are firing.
|
||||
func (as Alerts) Firing() []Alert {
|
||||
res := []Alert{}
|
||||
for _, a := range as {
|
||||
if a.Status == string(model.AlertFiring) {
|
||||
res = append(res, a)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Resolved returns the subset of alerts that are resolved.
|
||||
func (as Alerts) Resolved() []Alert {
|
||||
res := []Alert{}
|
||||
for _, a := range as {
|
||||
if a.Status == string(model.AlertResolved) {
|
||||
res = append(res, a)
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// Data assembles data for template expansion.
|
||||
func (t *Template) Data(recv string, groupLabels model.LabelSet, notificationReason string, alerts ...*types.Alert) *Data {
|
||||
typedAlerts := types.Alerts(alerts...)
|
||||
|
||||
data := &Data{
|
||||
Receiver: regexp.QuoteMeta(recv),
|
||||
Status: string(typedAlerts.Status()),
|
||||
Alerts: make(Alerts, 0, len(alerts)),
|
||||
NotificationReason: notificationReason,
|
||||
GroupLabels: KV{},
|
||||
CommonLabels: KV{},
|
||||
CommonAnnotations: KV{},
|
||||
ExternalURL: t.ExternalURL.String(),
|
||||
}
|
||||
|
||||
// The call to types.Alert is necessary to correctly resolve the internal
|
||||
// representation to the user representation.
|
||||
for _, a := range typedAlerts {
|
||||
alert := Alert{
|
||||
Status: string(a.Status()),
|
||||
Labels: make(KV, len(a.Labels)),
|
||||
Annotations: make(KV, len(a.Annotations)),
|
||||
StartsAt: a.StartsAt,
|
||||
EndsAt: a.EndsAt,
|
||||
GeneratorURL: a.GeneratorURL,
|
||||
Fingerprint: a.Fingerprint().String(),
|
||||
}
|
||||
for k, v := range a.Labels {
|
||||
alert.Labels[string(k)] = string(v)
|
||||
}
|
||||
for k, v := range a.Annotations {
|
||||
alert.Annotations[string(k)] = string(v)
|
||||
}
|
||||
data.Alerts = append(data.Alerts, alert)
|
||||
}
|
||||
|
||||
for k, v := range groupLabels {
|
||||
data.GroupLabels[string(k)] = string(v)
|
||||
}
|
||||
|
||||
if len(alerts) >= 1 {
|
||||
var (
|
||||
commonLabels = alerts[0].Labels.Clone()
|
||||
commonAnnotations = alerts[0].Annotations.Clone()
|
||||
)
|
||||
for _, a := range alerts[1:] {
|
||||
if len(commonLabels) == 0 && len(commonAnnotations) == 0 {
|
||||
break
|
||||
}
|
||||
for ln, lv := range commonLabels {
|
||||
if a.Labels[ln] != lv {
|
||||
delete(commonLabels, ln)
|
||||
}
|
||||
}
|
||||
for an, av := range commonAnnotations {
|
||||
if a.Annotations[an] != av {
|
||||
delete(commonAnnotations, an)
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, v := range commonLabels {
|
||||
data.CommonLabels[string(k)] = string(v)
|
||||
}
|
||||
for k, v := range commonAnnotations {
|
||||
data.CommonAnnotations[string(k)] = string(v)
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
type TemplateFunc func(string) (string, error)
|
||||
|
||||
// DeepCopyWithTemplate returns a deep copy of a map/slice/array/string/int/bool or combination thereof, executing the
|
||||
// provided template (with the provided data) on all string keys or values. All maps are connverted to
|
||||
// map[string]any, with all non-string keys discarded.
|
||||
func DeepCopyWithTemplate(value any, tmplTextFunc TemplateFunc) (any, error) {
|
||||
if value == nil {
|
||||
return value, nil
|
||||
}
|
||||
|
||||
valueMeta := reflect.ValueOf(value)
|
||||
switch valueMeta.Kind() {
|
||||
|
||||
case reflect.String:
|
||||
parsed, ok := tmplTextFunc(value.(string))
|
||||
if ok == nil {
|
||||
var inlineType any
|
||||
err := yaml.Unmarshal([]byte(parsed), &inlineType)
|
||||
if err != nil || (inlineType != nil && reflect.TypeOf(inlineType).Kind() == reflect.String) {
|
||||
// ignore error, thus the string is not an interface
|
||||
return parsed, ok
|
||||
}
|
||||
return DeepCopyWithTemplate(inlineType, tmplTextFunc)
|
||||
}
|
||||
return parsed, ok
|
||||
|
||||
case reflect.Array, reflect.Slice:
|
||||
arrayLen := valueMeta.Len()
|
||||
converted := make([]any, arrayLen)
|
||||
for i := range arrayLen {
|
||||
var err error
|
||||
converted[i], err = DeepCopyWithTemplate(valueMeta.Index(i).Interface(), tmplTextFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return converted, nil
|
||||
|
||||
case reflect.Map:
|
||||
keys := valueMeta.MapKeys()
|
||||
converted := make(map[string]any, len(keys))
|
||||
|
||||
for _, keyMeta := range keys {
|
||||
var err error
|
||||
strKey, isString := keyMeta.Interface().(string)
|
||||
if !isString {
|
||||
continue
|
||||
}
|
||||
strKey, err = tmplTextFunc(strKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
converted[strKey], err = DeepCopyWithTemplate(valueMeta.MapIndex(keyMeta).Interface(), tmplTextFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return converted, nil
|
||||
default:
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
// Copyright 2015 Prometheus Team
|
||||
// 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.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/prometheus/alertmanager/alert"
|
||||
)
|
||||
|
||||
// Deprecated: Use alert.Alert directly.
|
||||
type Alert = alert.Alert
|
||||
|
||||
// Deprecated: Use alert.AlertSlice directly.
|
||||
type AlertSlice = alert.AlertSlice
|
||||
|
||||
// Deprecated: Use alert.Alerts directly.
|
||||
var Alerts = alert.Alerts
|
||||
-123
@@ -1,123 +0,0 @@
|
||||
// Copyright 2024 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
package templates
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
var errNaNOrInf = errors.New("value is NaN or Inf")
|
||||
|
||||
func ConvertToFloat(i any) (float64, error) {
|
||||
switch v := i.(type) {
|
||||
case float64:
|
||||
return v, nil
|
||||
case string:
|
||||
return strconv.ParseFloat(v, 64)
|
||||
case int:
|
||||
return float64(v), nil
|
||||
case uint:
|
||||
return float64(v), nil
|
||||
case int64:
|
||||
return float64(v), nil
|
||||
case uint64:
|
||||
return float64(v), nil
|
||||
case time.Duration:
|
||||
return v.Seconds(), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("can't convert %T to float", v)
|
||||
}
|
||||
}
|
||||
|
||||
func FloatToTime(v float64) (*time.Time, error) {
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return nil, errNaNOrInf
|
||||
}
|
||||
timestamp := v * 1e9
|
||||
if timestamp > math.MaxInt64 || timestamp < math.MinInt64 {
|
||||
return nil, fmt.Errorf("%v cannot be represented as a nanoseconds timestamp since it overflows int64", v)
|
||||
}
|
||||
t := model.TimeFromUnixNano(int64(timestamp)).Time().UTC()
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func HumanizeDuration(i any) (string, error) {
|
||||
v, err := ConvertToFloat(i)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if math.IsNaN(v) || math.IsInf(v, 0) {
|
||||
return fmt.Sprintf("%.4g", v), nil
|
||||
}
|
||||
if v == 0 {
|
||||
return fmt.Sprintf("%.4gs", v), nil
|
||||
}
|
||||
if math.Abs(v) >= 1 {
|
||||
sign := ""
|
||||
if v < 0 {
|
||||
sign = "-"
|
||||
v = -v
|
||||
}
|
||||
duration := int64(v)
|
||||
seconds := duration % 60
|
||||
minutes := (duration / 60) % 60
|
||||
hours := (duration / 60 / 60) % 24
|
||||
days := duration / 60 / 60 / 24
|
||||
// For days to minutes, we display seconds as an integer.
|
||||
if days != 0 {
|
||||
return fmt.Sprintf("%s%dd %dh %dm %ds", sign, days, hours, minutes, seconds), nil
|
||||
}
|
||||
if hours != 0 {
|
||||
return fmt.Sprintf("%s%dh %dm %ds", sign, hours, minutes, seconds), nil
|
||||
}
|
||||
if minutes != 0 {
|
||||
return fmt.Sprintf("%s%dm %ds", sign, minutes, seconds), nil
|
||||
}
|
||||
// For seconds, we display 4 significant digits.
|
||||
return fmt.Sprintf("%s%.4gs", sign, v), nil
|
||||
}
|
||||
prefix := ""
|
||||
for _, p := range []string{"m", "u", "n", "p", "f", "a", "z", "y"} {
|
||||
if math.Abs(v) >= 1 {
|
||||
break
|
||||
}
|
||||
prefix = p
|
||||
v *= 1000
|
||||
}
|
||||
return fmt.Sprintf("%.4g%ss", v, prefix), nil
|
||||
}
|
||||
|
||||
func HumanizeTimestamp(i any) (string, error) {
|
||||
v, err := ConvertToFloat(i)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tm, err := FloatToTime(v)
|
||||
switch {
|
||||
case errors.Is(err, errNaNOrInf):
|
||||
return fmt.Sprintf("%.4g", v), nil
|
||||
case err != nil:
|
||||
return "", err
|
||||
}
|
||||
|
||||
return fmt.Sprint(tm), nil
|
||||
}
|
||||
-289
@@ -1,289 +0,0 @@
|
||||
// Copyright 2024 The Prometheus Authors
|
||||
// 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.
|
||||
|
||||
// Package promslog defines standardised ways to initialize the Go standard
|
||||
// library's log/slog logger.
|
||||
// It should typically only ever be imported by main packages.
|
||||
|
||||
package promslog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogStyle represents the common logging formats in the Prometheus ecosystem.
|
||||
type LogStyle string
|
||||
|
||||
const (
|
||||
SlogStyle LogStyle = "slog"
|
||||
GoKitStyle LogStyle = "go-kit"
|
||||
|
||||
reservedKeyPrefix = "logged_"
|
||||
)
|
||||
|
||||
var (
|
||||
// LevelFlagOptions represents allowed logging levels.
|
||||
LevelFlagOptions = []string{"debug", "info", "warn", "error"}
|
||||
// FormatFlagOptions represents allowed formats.
|
||||
FormatFlagOptions = []string{"logfmt", "json"}
|
||||
|
||||
defaultWriter = os.Stderr
|
||||
)
|
||||
|
||||
// Level controls a logging level, with an info default.
|
||||
// It wraps slog.LevelVar with string-based level control.
|
||||
// Level is safe to be used concurrently.
|
||||
type Level struct {
|
||||
lvl *slog.LevelVar
|
||||
}
|
||||
|
||||
// NewLevel returns a new Level.
|
||||
func NewLevel() *Level {
|
||||
return &Level{
|
||||
lvl: &slog.LevelVar{},
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Level) UnmarshalYAML(unmarshal func(any) error) error {
|
||||
var s string
|
||||
type plain string
|
||||
if err := unmarshal((*plain)(&s)); err != nil {
|
||||
return err
|
||||
}
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
if err := l.Set(s); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Level returns the value of the logging level as an slog.Level.
|
||||
func (l *Level) Level() slog.Level {
|
||||
return l.lvl.Level()
|
||||
}
|
||||
|
||||
// String returns the current level.
|
||||
func (l *Level) String() string {
|
||||
switch l.lvl.Level() {
|
||||
case slog.LevelDebug:
|
||||
return "debug"
|
||||
case slog.LevelInfo:
|
||||
return "info"
|
||||
case slog.LevelWarn:
|
||||
return "warn"
|
||||
case slog.LevelError:
|
||||
return "error"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// Set updates the logging level with the validation.
|
||||
func (l *Level) Set(s string) error {
|
||||
switch strings.ToLower(s) {
|
||||
case "debug":
|
||||
l.lvl.Set(slog.LevelDebug)
|
||||
case "info":
|
||||
l.lvl.Set(slog.LevelInfo)
|
||||
case "warn":
|
||||
l.lvl.Set(slog.LevelWarn)
|
||||
case "error":
|
||||
l.lvl.Set(slog.LevelError)
|
||||
default:
|
||||
return fmt.Errorf("unrecognized log level %s", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Format controls a logging output format.
|
||||
// Not concurrency-safe.
|
||||
type Format struct {
|
||||
s string
|
||||
}
|
||||
|
||||
// NewFormat creates a new Format.
|
||||
func NewFormat() *Format { return &Format{} }
|
||||
|
||||
func (f *Format) String() string {
|
||||
return f.s
|
||||
}
|
||||
|
||||
// Set updates the value of the allowed format.
|
||||
func (f *Format) Set(s string) error {
|
||||
switch s {
|
||||
case "logfmt", "json":
|
||||
f.s = s
|
||||
default:
|
||||
return fmt.Errorf("unrecognized log format %s", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Config is a struct containing configurable settings for the logger.
|
||||
type Config struct {
|
||||
Level *Level
|
||||
Format *Format
|
||||
Style LogStyle
|
||||
Writer io.Writer
|
||||
}
|
||||
|
||||
func newGoKitStyleReplaceAttrFunc(lvl *Level) func(groups []string, a slog.Attr) slog.Attr {
|
||||
return func(_ []string, a slog.Attr) slog.Attr {
|
||||
key := a.Key
|
||||
switch key {
|
||||
case slog.TimeKey, "ts":
|
||||
if t, ok := a.Value.Any().(time.Time); ok {
|
||||
a.Key = "ts"
|
||||
|
||||
// This timestamp format differs from RFC3339Nano by using .000 instead
|
||||
// of .999999999 which changes the timestamp from 9 variable to 3 fixed
|
||||
// decimals (.130 instead of .130987456).
|
||||
a.Value = slog.StringValue(t.UTC().Format("2006-01-02T15:04:05.000Z07:00"))
|
||||
} else {
|
||||
// If we can't cast the any from the value to a
|
||||
// time.Time, it means the caller logged
|
||||
// another attribute with a key of `ts`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to `logged_ts`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
case slog.SourceKey, "caller":
|
||||
if src, ok := a.Value.Any().(*slog.Source); ok {
|
||||
a.Key = "caller"
|
||||
switch lvl.String() {
|
||||
case "debug":
|
||||
a.Value = slog.StringValue(filepath.Base(src.File) + "(" + filepath.Base(src.Function) + "):" + strconv.Itoa(src.Line))
|
||||
default:
|
||||
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
|
||||
}
|
||||
} else {
|
||||
// If we can't cast the any from the value to
|
||||
// an *slog.Source, it means the caller logged
|
||||
// another attribute with a key of `caller`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to
|
||||
// `logged_caller`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
case slog.LevelKey:
|
||||
if lvl, ok := a.Value.Any().(slog.Level); ok {
|
||||
a.Value = slog.StringValue(strings.ToLower(lvl.String()))
|
||||
} else {
|
||||
// If we can't cast the any from the value to
|
||||
// an slog.Level, it means the caller logged
|
||||
// another attribute with a key of `level`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to `logged_level`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// Ensure time.Duration values are _always_ formatted as a Go
|
||||
// duration string (ie, "1d2h3m").
|
||||
if v, ok := a.Value.Any().(time.Duration); ok {
|
||||
a.Value = slog.StringValue(v.String())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
func defaultReplaceAttr(_ []string, a slog.Attr) slog.Attr {
|
||||
key := a.Key
|
||||
switch key {
|
||||
case slog.TimeKey:
|
||||
// Note that we do not change the timezone to UTC anymore.
|
||||
if _, ok := a.Value.Any().(time.Time); !ok {
|
||||
// If we can't cast the any from the value to a
|
||||
// time.Time, it means the caller logged
|
||||
// another attribute with a key of `time`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to `logged_time`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
case slog.SourceKey:
|
||||
if src, ok := a.Value.Any().(*slog.Source); ok {
|
||||
a.Value = slog.StringValue(filepath.Base(src.File) + ":" + strconv.Itoa(src.Line))
|
||||
} else {
|
||||
// If we can't cast the any from the value to
|
||||
// an *slog.Source, it means the caller logged
|
||||
// another attribute with a key of `source`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to
|
||||
// `logged_source`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
case slog.LevelKey:
|
||||
if _, ok := a.Value.Any().(slog.Level); !ok {
|
||||
// If we can't cast the any from the value to
|
||||
// an slog.Level, it means the caller logged
|
||||
// another attribute with a key of `level`.
|
||||
// Prevent duplicate keys (necessary for proper
|
||||
// JSON) by renaming the key to
|
||||
// `logged_level`.
|
||||
a.Key = reservedKeyPrefix + key
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
// Ensure time.Duration values are _always_ formatted as a Go duration
|
||||
// string (ie, "1d2h3m").
|
||||
if v, ok := a.Value.Any().(time.Duration); ok {
|
||||
a.Value = slog.StringValue(v.String())
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// New returns a new slog.Logger. Each logged line will be annotated
|
||||
// with a timestamp. The output always goes to stderr.
|
||||
func New(config *Config) *slog.Logger {
|
||||
if config.Level == nil {
|
||||
config.Level = NewLevel()
|
||||
}
|
||||
|
||||
if config.Writer == nil {
|
||||
config.Writer = defaultWriter
|
||||
}
|
||||
|
||||
logHandlerOpts := &slog.HandlerOptions{
|
||||
Level: config.Level.lvl,
|
||||
AddSource: true,
|
||||
ReplaceAttr: defaultReplaceAttr,
|
||||
}
|
||||
|
||||
if config.Style == GoKitStyle {
|
||||
logHandlerOpts.ReplaceAttr = newGoKitStyleReplaceAttrFunc(config.Level)
|
||||
}
|
||||
|
||||
if config.Format != nil && config.Format.s == "json" {
|
||||
return slog.New(slog.NewJSONHandler(config.Writer, logHandlerOpts))
|
||||
}
|
||||
return slog.New(slog.NewTextHandler(config.Writer, logHandlerOpts))
|
||||
}
|
||||
|
||||
// NewNopLogger is a convenience function to return an slog.Logger that writes
|
||||
// to io.Discard.
|
||||
func NewNopLogger() *slog.Logger {
|
||||
return New(&Config{Writer: io.Discard})
|
||||
}
|
||||
Vendored
+1
-33
@@ -1367,7 +1367,7 @@ github.com/opencloud-eu/icap-client
|
||||
# github.com/opencloud-eu/libre-graph-api-go v1.0.8-0.20260902170011-45af3945a067
|
||||
## explicit; go 1.23
|
||||
github.com/opencloud-eu/libre-graph-api-go
|
||||
# github.com/opencloud-eu/reva/v2 v2.49.0
|
||||
# github.com/opencloud-eu/reva/v2 v2.49.1-0.20260903122659-26f34ec05774
|
||||
## explicit; go 1.25.8
|
||||
github.com/opencloud-eu/reva/v2/cmd/revad/internal/grace
|
||||
github.com/opencloud-eu/reva/v2/cmd/revad/runtime
|
||||
@@ -1457,7 +1457,6 @@ github.com/opencloud-eu/reva/v2/internal/http/services/prometheus
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/reqres
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/reverseproxy
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/sciencemesh
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/siteacc
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/sysinfo
|
||||
github.com/opencloud-eu/reva/v2/internal/http/services/wellknown
|
||||
github.com/opencloud-eu/reva/v2/pkg/app
|
||||
@@ -1613,26 +1612,6 @@ github.com/opencloud-eu/reva/v2/pkg/share/manager/memory
|
||||
github.com/opencloud-eu/reva/v2/pkg/share/manager/registry
|
||||
github.com/opencloud-eu/reva/v2/pkg/sharedconf
|
||||
github.com/opencloud-eu/reva/v2/pkg/signedurl
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/contact
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/edit
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/login
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/manage
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/registration
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/settings
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/account/site
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/admin
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/alerting
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/config
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/credentials/crypto
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/data
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/email
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/html
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/manager
|
||||
github.com/opencloud-eu/reva/v2/pkg/siteacc/manager/gocdb
|
||||
github.com/opencloud-eu/reva/v2/pkg/smtpclient
|
||||
github.com/opencloud-eu/reva/v2/pkg/storage
|
||||
github.com/opencloud-eu/reva/v2/pkg/storage/cache
|
||||
github.com/opencloud-eu/reva/v2/pkg/storage/fs/cephfs
|
||||
@@ -1823,15 +1802,6 @@ github.com/power-devops/perfstat
|
||||
## explicit; go 1.16
|
||||
github.com/pquerna/cachecontrol
|
||||
github.com/pquerna/cachecontrol/cacheobject
|
||||
# github.com/prometheus/alertmanager v0.33.1
|
||||
## explicit; go 1.25.0
|
||||
github.com/prometheus/alertmanager/alert
|
||||
github.com/prometheus/alertmanager/featurecontrol
|
||||
github.com/prometheus/alertmanager/matcher/compat
|
||||
github.com/prometheus/alertmanager/matcher/parse
|
||||
github.com/prometheus/alertmanager/pkg/labels
|
||||
github.com/prometheus/alertmanager/template
|
||||
github.com/prometheus/alertmanager/types
|
||||
# github.com/prometheus/client_golang v1.24.1
|
||||
## explicit; go 1.25.0
|
||||
github.com/prometheus/client_golang/internal/github.com/golang/gddo/httputil
|
||||
@@ -1847,9 +1817,7 @@ github.com/prometheus/client_model/go
|
||||
# github.com/prometheus/common v0.70.1
|
||||
## explicit; go 1.25.0
|
||||
github.com/prometheus/common/expfmt
|
||||
github.com/prometheus/common/helpers/templates
|
||||
github.com/prometheus/common/model
|
||||
github.com/prometheus/common/promslog
|
||||
# github.com/prometheus/procfs v0.21.1
|
||||
## explicit; go 1.25.0
|
||||
github.com/prometheus/procfs
|
||||
|
||||
Reference in new issue
Block a user