initial draft

Signed-off-by: jkoberg <jkoberg@owncloud.com>
This commit is contained in:
jkoberg
2022-03-08 11:25:26 +01:00
parent c15738716f
commit f0671015f8
22 changed files with 781 additions and 1 deletions

View File

@@ -0,0 +1,14 @@
package main
import (
"os"
"github.com/owncloud/ocis/audit/pkg/command"
"github.com/owncloud/ocis/audit/pkg/config"
)
func main() {
if err := command.Execute(config.DefaultConfig()); err != nil {
os.Exit(1)
}
}

View File

@@ -0,0 +1,105 @@
// Package channels provides different communication channels to notify users.
package channels
import (
"context"
"net/smtp"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
groups "github.com/cs3org/go-cs3apis/cs3/identity/group/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
"github.com/pkg/errors"
)
// Channel defines the methods of a communication channel.
type Channel interface {
// SendMessage sends a message to users.
SendMessage(userIDs []string, msg string) error
// SendMessageToGroup sends a message to a group.
SendMessageToGroup(groupdID *groups.GroupId, msg string) error
}
// NewMailChannel instantiates a new mail communication channel.
func NewMailChannel(cfg config.Config, logger log.Logger) (Channel, error) {
gc, err := pool.GetGatewayServiceClient(cfg.Notifications.RevaGateway)
if err != nil {
logger.Error().Err(err).Msg("could not get gateway client")
return nil, err
}
return Mail{
gatewayClient: gc,
conf: cfg,
logger: logger,
}, nil
}
// Mail is the communcation channel for email.
type Mail struct {
gatewayClient gateway.GatewayAPIClient
conf config.Config
logger log.Logger
}
// SendMessage sends a message to all given users.
func (m Mail) SendMessage(userIDs []string, msg string) error {
to, err := m.getReceiverAddresses(userIDs)
if err != nil {
return err
}
body := []byte(msg)
smtpConf := m.conf.Notifications.SMTP
auth := smtp.PlainAuth("", smtpConf.Sender, smtpConf.Password, smtpConf.Host)
if err := smtp.SendMail(smtpConf.Host+":"+smtpConf.Port, auth, smtpConf.Sender, to, body); err != nil {
return errors.Wrap(err, "could not send mail")
}
return nil
}
// SendMessageToGroup sends a message to all members of the given group.
func (m Mail) SendMessageToGroup(groupID *groups.GroupId, msg string) error {
// TODO We need an authenticated context here...
res, err := m.gatewayClient.GetGroup(context.Background(), &groups.GetGroupRequest{GroupId: groupID})
if err != nil {
return err
}
if res.Status.Code != rpc.Code_CODE_OK {
return errors.New("could not get group")
}
members := make([]string, 0, len(res.Group.Members))
for _, id := range res.Group.Members {
members = append(members, id.OpaqueId)
}
return m.SendMessage(members, msg)
}
func (m Mail) getReceiverAddresses(receivers []string) ([]string, error) {
addresses := make([]string, 0, len(receivers))
for _, id := range receivers {
// Authenticate is too costly but at the moment our only option to get the user.
// We don't have an authenticated context so calling `GetUser` doesn't work.
res, err := m.gatewayClient.Authenticate(context.Background(), &gateway.AuthenticateRequest{
Type: "machine",
ClientId: "userid:" + id,
ClientSecret: m.conf.Notifications.MachineAuthSecret,
})
if err != nil {
return nil, err
}
if res.Status.Code != rpc.Code_CODE_OK {
m.logger.Error().
Interface("status", res.Status).
Str("receiver_id", id).
Msg("could not get user")
continue
}
addresses = append(addresses, res.User.Mail)
}
return addresses, nil
}

View File

@@ -0,0 +1,18 @@
package command
import (
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/urfave/cli/v2"
)
// Health is the entrypoint for the health command.
func Health(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "health",
Usage: "Check health status",
Action: func(c *cli.Context) error {
// Not implemented
return nil
},
}
}

64
audit/pkg/command/root.go Normal file
View File

@@ -0,0 +1,64 @@
package command
import (
"context"
"os"
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/clihelper"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/thejerf/suture/v4"
"github.com/urfave/cli/v2"
)
// GetCommands provides all commands for this service
func GetCommands(cfg *config.Config) cli.Commands {
return []*cli.Command{
// start this service
Server(cfg),
// interaction with this service
// infos about this service
Health(cfg),
Version(cfg),
}
}
// Execute is the entry point for the notifications command.
func Execute(cfg *config.Config) error {
app := clihelper.DefaultApp(&cli.App{
Name: "notifications",
Usage: "starts notifications service",
Commands: GetCommands(cfg),
})
cli.HelpFlag = &cli.BoolFlag{
Name: "help,h",
Usage: "Show the help",
}
return app.Run(os.Args)
}
// SutureService allows for the notifications command to be embedded and supervised by a suture supervisor tree.
type SutureService struct {
cfg *config.Config
}
// NewSutureService creates a new notifications.SutureService
func NewSutureService(cfg *ociscfg.Config) suture.Service {
cfg.Settings.Commons = cfg.Commons
return SutureService{
cfg: cfg.Notifications,
}
}
func (s SutureService) Serve(ctx context.Context) error {
s.cfg.Context = ctx
if err := Execute(s.cfg); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,50 @@
package command
import (
"fmt"
"github.com/asim/go-micro/plugins/events/nats/v4"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/events/server"
"github.com/owncloud/ocis/notifications/pkg/channels"
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/owncloud/ocis/notifications/pkg/config/parser"
"github.com/owncloud/ocis/notifications/pkg/logging"
"github.com/owncloud/ocis/notifications/pkg/service"
"github.com/urfave/cli/v2"
)
// Server is the entrypoint for the server command.
func Server(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "server",
Usage: fmt.Sprintf("start %s extension without runtime (unsupervised mode)", cfg.Service.Name),
Category: "server",
Before: func(c *cli.Context) error {
return parser.ParseConfig(cfg)
},
Action: func(c *cli.Context) error {
logger := logging.Configure(cfg.Service.Name, cfg.Log)
evs := []events.Unmarshaller{
events.ShareCreated{},
}
evtsCfg := cfg.Notifications.Events
client, err := server.NewNatsStream(nats.Address(evtsCfg.Endpoint), nats.ClusterID(evtsCfg.Cluster))
if err != nil {
return err
}
evts, err := events.Consume(client, evtsCfg.ConsumerGroup, evs...)
if err != nil {
return err
}
channel, err := channels.NewMailChannel(*cfg, logger)
if err != nil {
return err
}
svc := service.NewEventsNotifier(evts, channel, logger)
return svc.Run()
},
}
}

View File

@@ -0,0 +1,19 @@
package command
import (
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/urfave/cli/v2"
)
// Version prints the service versions of all running instances.
func Version(cfg *config.Config) *cli.Command {
return &cli.Command{
Name: "version",
Usage: "print the version of this binary and the running extension instances",
Category: "info",
Action: func(c *cli.Context) error {
// not implemented
return nil
},
}
}

View File

@@ -0,0 +1,44 @@
package config
import (
"context"
"github.com/owncloud/ocis/ocis-pkg/shared"
)
// Config combines all available configuration parts.
type Config struct {
*shared.Commons
Service Service
Log *Log `ocisConfig:"log"`
Debug Debug `ocisConfig:"debug"`
Notifications Notifications `ocisConfig:"notifications"`
Context context.Context
}
// Notifications definces the config options for the notifications service.
type Notifications struct {
SMTP SMTP `ocisConfig:"SMTP"`
Events Events `ocisConfig:"events"`
RevaGateway string `ocisConfig:"reva_gateway" env:"REVA_GATEWAY;NOTIFICATIONS_REVA_GATEWAY"`
MachineAuthSecret string `ocisConfig:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;NOTIFICATIONS_MACHINE_AUTH_API_KEY"`
}
// SMTP combines the smtp configuration options.
type SMTP struct {
Host string `ocisConfig:"smtp_host" env:"NOTIFICATIONS_SMTP_HOST"`
Port string `ocisConfig:"smtp_port" env:"NOTIFICATIONS_SMTP_PORT"`
Sender string `ocisConfig:"smtp_sender" env:"NOTIFICATIONS_SMTP_SENDER"`
Password string `ocisConfig:"smtp_password" env:"NOTIFICATIONS_SMTP_PASSWORD"`
}
// Events combines the configuration options for the event bus.
type Events struct {
Endpoint string `ocisConfig:"events_endpoint" env:"NOTIFICATIONS_EVENTS_ENDPOINT"`
Cluster string `ocisConfig:"events_cluster" env:"NOTIFICATIONS_EVENTS_CLUSTER"`
ConsumerGroup string `ocisConfig:"events_group" env:"NOTIFICATIONS_EVENTS_GROUP"`
}

View File

@@ -0,0 +1,9 @@
package config
// Debug defines the available debug configuration.
type Debug struct {
Addr string `ocisConfig:"addr" env:"NOTIFICATIONS_DEBUG_ADDR"`
Token string `ocisConfig:"token" env:"NOTIFICATIONS_DEBUG_TOKEN"`
Pprof bool `ocisConfig:"pprof" env:"NOTIFICATIONS_DEBUG_PPROF"`
Zpages bool `ocisConfig:"zpages" env:"NOTIFICATIONS_DEBUG_ZPAGES"`
}

View File

@@ -0,0 +1,27 @@
package config
// NOTE: Most of this configuration is not needed to keep it as simple as possible
// TODO: Clean up unneeded configuration
func DefaultConfig() *Config {
return &Config{
Service: Service{
Name: "notifications",
},
Notifications: Notifications{
SMTP: SMTP{
Host: "127.0.0.1",
Port: "1025",
Sender: "god@example.com",
Password: "godisdead",
},
Events: Events{
Endpoint: "127.0.0.1:9233",
Cluster: "test-cluster",
ConsumerGroup: "notifications",
},
RevaGateway: "127.0.0.1:9142",
MachineAuthSecret: "change-me-please",
},
}
}

9
audit/pkg/config/log.go Normal file
View File

@@ -0,0 +1,9 @@
package config
// Log defines the available log configuration.
type Log struct {
Level string `mapstructure:"level" env:"OCIS_LOG_LEVEL;NOTIFICATIONS_LOG_LEVEL"`
Pretty bool `mapstructure:"pretty" env:"OCIS_LOG_PRETTY;NOTIFICATIONS_LOG_PRETTY"`
Color bool `mapstructure:"color" env:"OCIS_LOG_COLOR;NOTIFICATIONS_LOG_COLOR"`
File string `mapstructure:"file" env:"OCIS_LOG_FILE;NOTIFICATIONS_LOG_FILE"`
}

View File

@@ -0,0 +1,40 @@
package parser
import (
"errors"
"github.com/owncloud/ocis/notifications/pkg/config"
ociscfg "github.com/owncloud/ocis/ocis-pkg/config"
"github.com/owncloud/ocis/ocis-pkg/config/envdecode"
)
// ParseConfig loads accounts configuration from known paths.
func ParseConfig(cfg *config.Config) error {
_, err := ociscfg.BindSourcesToStructs(cfg.Service.Name, cfg)
if err != nil {
return err
}
// provide with defaults for shared logging, since we need a valid destination address for BindEnv.
if cfg.Log == nil && cfg.Commons != nil && cfg.Commons.Log != nil {
cfg.Log = &config.Log{
Level: cfg.Commons.Log.Level,
Pretty: cfg.Commons.Log.Pretty,
Color: cfg.Commons.Log.Color,
File: cfg.Commons.Log.File,
}
} else if cfg.Log == nil {
cfg.Log = &config.Log{}
}
// load all env variables relevant to the config in the current context.
if err := envdecode.Decode(cfg); err != nil {
// no environment variable set for this config is an expected "error"
if !errors.Is(err, envdecode.ErrNoTargetFieldsAreSet) {
return err
}
}
return nil
}

View File

@@ -0,0 +1,6 @@
package config
// Service defines the available service configuration.
type Service struct {
Name string
}

View File

@@ -0,0 +1,17 @@
package logging
import (
"github.com/owncloud/ocis/notifications/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// LoggerFromConfig initializes a service-specific logger instance.
func Configure(name string, cfg *config.Log) log.Logger {
return log.NewLogger(
log.Name(name),
log.Level(cfg.Level),
log.Pretty(cfg.Pretty),
log.Color(cfg.Color),
log.File(cfg.File),
)
}

View File

@@ -0,0 +1,79 @@
package svc
import (
"encoding/json"
"fmt"
"os"
"github.com/asim/go-micro/plugins/events/nats/v4"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/events/server"
"github.com/owncloud/ocis/audit/pkg/config"
"github.com/owncloud/ocis/audit/pkg/types"
"github.com/owncloud/ocis/ocis-pkg/log"
)
func startConsumer(c config.Eventstream, log log.Logger) (<-chan interface{}, error) {
s, err := server.NewNatsStream(nats.Address(c.Address), nats.ClusterID(c.ClusterID))
if err != nil {
return nil, err
}
return events.Consume(s, "audit", events.ShareCreated{})
}
func startAuditLogger(c config.Auditlog, ch <-chan interface{}, log log.Logger) {
for {
i := <-ch
var auditEvent interface{}
switch ev := i.(type) {
case events.ShareCreated:
auditEvent = types.ShareCreated(ev)
default:
log.Error().Interface("event", ev).Msg(fmt.Sprintf("can't handle event of type '%T'", ev))
continue
}
b, err := marshal(auditEvent, c.Format)
if err != nil {
log.Error().Err(err).Msg("error marshaling the event")
continue
}
if c.LogToConsole {
log.Info().Msg(string(b))
}
if c.LogToFile {
err := writeToFile(c.FilePath, b)
if err != nil {
log.Error().Err(err).Msg("error writing audit log file")
}
}
}
}
func writeToFile(path string, ev []byte) error {
file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer file.Close()
if _, err := fmt.Fprintln(file, string(ev)); err != nil {
return err
}
return nil
}
func marshal(ev interface{}, format string) ([]byte, error) {
switch format {
default:
return nil, fmt.Errorf("unsupported format '%s'", format)
case "json":
return json.Marshal(ev)
}
}

View File

@@ -0,0 +1,21 @@
package svc
/*
// NewInstrument returns a service that instruments metrics.
func NewInstrument(next Service, metrics *metrics.Metrics) Service {
return instrument{
next: next,
metrics: metrics,
}
}
type instrument struct {
next Service
metrics *metrics.Metrics
}
// ListenForEvents implements service interface
func (i instrument) ListenForEvents() {
i.next.ListenForEvents()
}
*/

View File

@@ -0,0 +1,23 @@
package svc
import (
"github.com/owncloud/ocis/ocis-pkg/log"
)
// NewLogging returns a service that logs messages.
func NewLogging(next Service, logger log.Logger) Service {
return logging{
next: next,
logger: logger,
}
}
type logging struct {
next Service
logger log.Logger
}
// ListenForEvents implements service interface
func (l logging) ListenForEvents() {
l.next.ListenForEvents()
}

View File

@@ -0,0 +1,50 @@
package svc
import (
"net/http"
"github.com/owncloud/ocis/audit/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// Option defines a single option function.
type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
}
// newOptions initializes the available default options.
func newOptions(opts ...Option) Options {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return opt
}
// Logger provides a function to set the logger option.
func Logger(val log.Logger) Option {
return func(o *Options) {
o.Logger = val
}
}
// Config provides a function to set the config option.
func Config(val *config.Config) Option {
return func(o *Options) {
o.Config = val
}
}
// Middleware provides a function to set the middleware option.
func Middleware(val ...func(http.Handler) http.Handler) Option {
return func(o *Options) {
o.Middleware = val
}
}

View File

@@ -0,0 +1,48 @@
package svc
import (
"github.com/go-chi/chi"
"github.com/owncloud/ocis/audit/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// Service defines the extension handlers.
type Service interface {
ListenForEvents()
}
// NewService returns a service implementation for Service.
func NewService(opts ...Option) Service {
options := newOptions(opts...)
m := chi.NewMux()
m.Use(options.Middleware...)
svc := Audit{
logger: options.Logger,
config: options.Config,
mux: m,
}
go svc.ListenForEvents()
return svc
}
// Audit defines implements the business logic for Service.
type Audit struct {
logger log.Logger
config *config.Config
mux *chi.Mux
}
// ListenForEvents hooks into event queue and logs interesting events
func (g Audit) ListenForEvents() {
log := g.logger
ch, err := startConsumer(g.config.Eventstream, log)
if err != nil {
log.Fatal().Err(err).Msg("can't listen for events")
return
}
startAuditLogger(g.config.Auditlog, ch, log)
}

View File

@@ -0,0 +1,17 @@
package svc
// NewTracing returns a service that instruments traces.
func NewTracing(next Service) Service {
return tracing{
next: next,
}
}
type tracing struct {
next Service
}
// ListenForEvents implements service interface
func (t tracing) ListenForEvents() {
t.next.ListenForEvents()
}

View File

@@ -0,0 +1,79 @@
package types
import (
"fmt"
"time"
"github.com/cs3org/reva/v2/pkg/events"
)
// actions
const (
actionShareCreated = "file_shared"
)
// messages
const (
messageShareCreated = "user '%s' shared file '%s' with '%s'"
)
// BasicAuditEvent creates an AuditEvent from given values
func BasicAuditEvent(uid string, ctime string, msg string, action string) AuditEvent {
return AuditEvent{
User: uid,
Time: ctime,
App: "admin_audit",
Message: msg,
Action: action,
Level: 1,
// NOTE: those values are not in the events and can therefore not be filled at the moment
RemoteAddr: "",
URL: "",
Method: "",
UserAgent: "",
CLI: false,
}
}
// SharingAuditEvent creates an AuditEventSharing from given values
func SharingAuditEvent(fileid string, uid string, base AuditEvent) AuditEventSharing {
return AuditEventSharing{
AuditEvent: base,
FileID: fileid,
Owner: uid,
// NOTE: those values are not in the events and can therefore not be filled at the moment
ShareID: "",
Path: "",
}
}
// ShareCreated converts a ShareCreated Event to an AuditEventShareCreated
func ShareCreated(ev events.ShareCreated) AuditEventShareCreated {
with := ""
typ := ""
if ev.GranteeUserID != nil && ev.GranteeUserID.OpaqueId != "" {
with = ev.GranteeUserID.OpaqueId
typ = "user"
} else if ev.GranteeGroupID != nil && ev.GranteeGroupID.OpaqueId != "" {
with = ev.GranteeGroupID.OpaqueId
typ = "group"
}
uid := ev.Sharer.OpaqueId
t := time.Unix(int64(ev.CTime.Seconds), int64(ev.CTime.Nanos)).Format(time.RFC3339)
base := BasicAuditEvent(uid, t, fmt.Sprintf(messageShareCreated, uid, ev.ItemID.OpaqueId, with), actionShareCreated)
return AuditEventShareCreated{
AuditEventSharing: SharingAuditEvent(ev.ItemID.OpaqueId, uid, base),
ShareOwner: uid,
ShareWith: with,
ShareType: typ,
// NOTE: those values are not in the events and can therefore not be filled at the moment
ItemType: "",
ExpirationDate: "",
SharePass: false,
Permissions: "",
ShareToken: "",
}
}

40
audit/pkg/types/types.go Normal file
View File

@@ -0,0 +1,40 @@
package types
// AuditEvent is the basic audit event
type AuditEvent struct {
RemoteAddr string // the remote client IP
User string // the UID of the user performing the action. Or "IP x.x.x.x.", "cron", "CLI", "unknown"
URL string // the process request URI
Method string // the HTTP request method
UserAgent string // the HTTP request user agent
Time string // the time of the event eg: 2018-05-08T08:26:00+00:00
App string // always 'admin_audit'
Message string // sentence explaining the action
Action string // unique action identifier eg: file_delete or public_link_created
CLI bool // if the action was performed from the CLI
Level int // the log level of the entry (usually 1 for audit events)
}
// AuditEventSharing is the basic audit event for shares
type AuditEventSharing struct {
AuditEvent
FileID string // The file identifier for the item shared.
Owner string // The UID of the owner of the shared item.
Path string // The path to the shared item.
ShareID string // The sharing identifier. (not available for public_link_accessed or when recipient unshares)
}
// AuditEventShareCreated is the event logged when a share is created
type AuditEventShareCreated struct {
AuditEventSharing
ItemType string // file or folder
ExpirationDate string // The text expiration date in format 'yyyy-mm-dd'
SharePass bool // If the share is password protected.
Permissions string // The permissions string eg: "READ"
ShareType string // group user or link
ShareWith string // The UID or GID of the share recipient. (not available for public link)
ShareOwner string // The UID of the share owner.
ShareToken string // For link shares the unique token, else null
}

3
go.mod
View File

@@ -25,6 +25,7 @@ require (
github.com/cs3org/reva/v2 v2.0.0-20220304131900-b8be80d1ba81
github.com/disintegration/imaging v1.6.2
github.com/glauth/glauth/v2 v2.0.0-20211021011345-ef3151c28733
github.com/go-chi/chi v4.0.2+incompatible
github.com/go-chi/chi/v5 v5.0.7
github.com/go-chi/cors v1.2.0
github.com/go-chi/render v1.0.1
@@ -46,6 +47,7 @@ require (
github.com/mennanov/fieldmask-utils v0.5.0
github.com/mitchellh/mapstructure v1.4.3
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/nats-io/nats-server/v2 v2.7.3
github.com/nats-io/nats-streaming-server v0.24.2
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
@@ -208,7 +210,6 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 // indirect
github.com/nats-io/nats-server/v2 v2.7.3 // indirect
github.com/nats-io/nats.go v1.13.1-0.20220121202836-972a071d373d // indirect
github.com/nats-io/nkeys v0.3.0 // indirect
github.com/nats-io/nuid v1.0.1 // indirect