Merge pull request #3478 from opencloud-eu/reva-bump-main-20260907

[full-ci] chore: bump reva to latest main
This commit is contained in:
Jörn Friedrich Dreyer authored and GitHub committed 2026-09-07 12:48:12 +02:00
commit be226ebb4c
84 files changed
+143 -10302

No files matched your search

+1 -2
View File
@@ -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
+2 -4
View File
@@ -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=
@@ -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
@@ -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":
@@ -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
View File
@@ -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
View File
@@ -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(),
@@ -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
@@ -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
}
@@ -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>&nbsp;</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>
`
@@ -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
}
@@ -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>&nbsp;</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;">&nbsp;</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>
`
@@ -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
}
@@ -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>&nbsp;</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>
`
@@ -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
}
@@ -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>&nbsp;</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;">&nbsp;</span>
{{if .Account.Data.SiteAccess}}
<button type="button" onClick="handleSiteSettings();">Site settings</button>
<span style="width: 25px;">&nbsp;</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
View File
@@ -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, &registration.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
}
@@ -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
}
@@ -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>&nbsp;</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;">&nbsp;</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;">&nbsp;</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>
`
@@ -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
}
@@ -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>&nbsp;</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>
`
@@ -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
}
@@ -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>&nbsp;</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;">&nbsp;</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
View File
@@ -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
View File
@@ -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>&nbsp;</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>&nbsp;</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;">&nbsp;</span>
<button type="button" onClick="handleAction('remove', '{{.Email}}');" style="float: right;">Remove</button>
</form>
</div>
<hr>
</li>
{{end}}
</ul>
</div>
`
@@ -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
View File
@@ -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
View File
@@ -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"
)
@@ -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 = ""
}
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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>
`
@@ -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)
}
@@ -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
}
@@ -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,
},
}
}
@@ -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
}
@@ -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
View File
@@ -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)
}
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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"))
}
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,8 +0,0 @@
{
"name": "template",
"version": "0.0.1",
"private": true,
"devDependencies": {
"juice": "10.0.1"
}
}
-536
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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})
}
+1 -33
View File
@@ -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