From eae57e5cde4063ba72ce2008785d245206b5910c Mon Sep 17 00:00:00 2001 From: zachmann Date: Wed, 14 Dec 2022 11:27:44 +0100 Subject: [PATCH 001/195] WIP notifications --- config/example-config.yaml | 7 +- internal/config/config.go | 54 ++++++++++++- internal/db/notificationsrepo/sync.go | 81 +++++++++++++++++++ internal/server/server.go | 6 ++ .../server/web/sites/mail_notify.mustache | 28 +++++++ 5 files changed, 172 insertions(+), 4 deletions(-) create mode 100644 internal/db/notificationsrepo/sync.go create mode 100644 internal/server/web/sites/mail_notify.mustache diff --git a/config/example-config.yaml b/config/example-config.yaml index d8e3d5a3..4b16ba26 100644 --- a/config/example-config.yaml +++ b/config/example-config.yaml @@ -14,6 +14,8 @@ service_operator: # Configuration for the mytoken server server: + # If you run a single mytoken server - i.e. no fault tolerance - set this option to 'true'; this improves performance + ##single_server: true # If TLS is not enabled, mytoken will listen on this port, default: 8000 port: 8000 tls: @@ -25,9 +27,12 @@ server: cert: # The TLS certificate key file key: + # If behind a load balancer or reverse proxy and TLS is set on that and not on the mytoken server set this option + # to 'true' + ##secure: true # If behind a load balancer or reverse proxy, set this option. # Mytoken will read the client's ip address from this header. - # proxy_header: "X-FORWARDED-FOR" + ##proxy_header: "X-FORWARDED-FOR" # Configure the request limits (these are per IP) request_limits: # Unless false request limits are enabled diff --git a/internal/config/config.go b/internal/config/config.go index bd07ee3f..5984c4cc 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -141,13 +141,20 @@ type featuresConf struct { DisabledRestrictionKeys model2.RestrictionClaims `yaml:"unsupported_restrictions"` SSH sshConf `yaml:"ssh"` ServerProfiles serverProfilesConf `yaml:"server_profiles"` + Notifications notificationConf `yaml:"notifications"` } func (c *featuresConf) validate() error { if err := c.OIDCFlows.validate(); err != nil { return err } - return c.ServerProfiles.validate() + if err := c.ServerProfiles.validate(); err != nil { + return err + } + if err := c.Notifications.validate(); err != nil { + return err + } + return nil } type oidcFlowsConf struct { @@ -212,6 +219,46 @@ func (g profileGroupsCredentials) validate() error { return nil } +type notificationConf struct { + AnyEnabled bool `yaml:"-"` + SchedulerNeeded bool `yaml:"-"` + Mail mailNotificationConf `yaml:"email"` + Websocket websocketNotificationConf `yaml:"ws"` + ICS icsNotificationConf `yaml:"ics"` + ICAL icalNotificationConf `yaml:"ical"` +} + +func (c *notificationConf) validate() error { + c.AnyEnabled = true || c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled || c.ICAL.Enabled + c.SchedulerNeeded = true || c.Mail.Enabled || c.Websocket.Enabled + return nil + //TODO +} + +type mailNotificationConf struct { + Enabled bool `yaml:"enabled"` + MailServer mailServerConf `yaml:"mail_server"` +} + +type mailServerConf struct { + Host string `yaml:"host"` + Username string `yaml:"user"` + Password string `yaml:"password"` + FromAddress string `yaml:"from_address"` +} + +type websocketNotificationConf struct { + onlyEnable +} + +type icsNotificationConf struct { + onlyEnable +} + +type icalNotificationConf struct { + onlyEnable +} + type tokeninfoConfig struct { Enabled bool `yaml:"-"` Introspect onlyEnable `yaml:"introspect"` @@ -304,8 +351,9 @@ type serverConf struct { TLS tlsConf `yaml:"tls"` Secure bool `yaml:"-"` // Secure indicates if the connection to the mytoken server is secure. This is // independent of TLS, e.g. a Proxy can be used. - ProxyHeader string `yaml:"proxy_header"` - Limiter limiterConf `yaml:"request_limits"` + ProxyHeader string `yaml:"proxy_header"` + Limiter limiterConf `yaml:"request_limits"` + SingleServer bool `yaml:"single_server"` } type limiterConf struct { diff --git a/internal/db/notificationsrepo/sync.go b/internal/db/notificationsrepo/sync.go new file mode 100644 index 00000000..1cc98f2f --- /dev/null +++ b/internal/db/notificationsrepo/sync.go @@ -0,0 +1,81 @@ +package notificationsrepo + +import ( + "fmt" + "math/rand" + "time" + + "github.com/jmoiron/sqlx" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" +) + +func init() { + rand.Seed(time.Now().UnixMilli()) +} + +// NotificationScheduler syncs with other mytoken server instances so that only one server does send notifications. +// Notifications are checked every minute if there are notification due and if one of the servers takes care of +// sending them. +func NotificationScheduler() { + notMaxCounter := 1 + for { + if !config.Get().Server.SingleServer && !determineMaster(¬MaxCounter) { + continue + } + // do stuff as master + fmt.Println("I now would send notifications if there are any") + time.Sleep(time.Duration(rand.Float32()*120) * time.Second) + } +} + +func determineMaster(notMaxCounter *int) bool { + t, _ := time.Parse(time.RFC822, time.Now().Format(time.RFC822)) + t1 := t.Add(time.Minute) + t2 := t1.Add(30 * time.Second) + time.Sleep(time.Until(t1)) + + i := rand.Int() * *notMaxCounter + if err := db.Transact( + log.StandardLogger(), func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL NotificationSync_Write(?)`, i) + return err + }, + ); err != nil { + log.WithError(err).Error("notification_scheduler: determine_master: error writing to db") + return false + } + time.Sleep(time.Until(t2)) + + max := 0 + var syncNumbers []int + if err := db.Transact( + log.StandardLogger(), func(tx *sqlx.Tx) error { + return tx.Select(&syncNumbers, `CALL NotificationSync_Read()`) + }, + ); err != nil { + log.WithError(err).Error("notification_scheduler: determine_master: error reading from db") + return false + } + for _, j := range syncNumbers { + if j > max { + max = j + } + } + if max != i { + *notMaxCounter++ + return false + } + *notMaxCounter = 1 + if err := db.Transact( + log.StandardLogger(), func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL NotificationSync_Reset()`) + return err + }, + ); err != nil { + log.WithError(err).Error("notification_scheduler: determine_master: error resetting sync table") + } + return true +} diff --git a/internal/server/server.go b/internal/server/server.go index 0b957c61..b20441ca 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -15,6 +15,7 @@ import ( "github.com/oidc-mytoken/api/v0" "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo" "github.com/oidc-mytoken/server/internal/endpoints" "github.com/oidc-mytoken/server/internal/endpoints/configuration" "github.com/oidc-mytoken/server/internal/endpoints/consent" @@ -123,6 +124,11 @@ func addWebRoutes(s fiber.Router) { } func start(s *fiber.App) { + if config.Get().Features.Notifications.SchedulerNeeded { + go notificationsrepo.NotificationScheduler() + go notificationsrepo.NotificationScheduler() + go notificationsrepo.NotificationScheduler() + } if config.Get().Features.SSH.Enabled { go ssh.Serve() } diff --git a/internal/server/web/sites/mail_notify.mustache b/internal/server/web/sites/mail_notify.mustache new file mode 100644 index 00000000..d126e457 --- /dev/null +++ b/internal/server/web/sites/mail_notify.mustache @@ -0,0 +1,28 @@ +{{#FromUsage}} + A mytoken that will expire soon was just used, and you have subscribed to be notified before it expires. +{{/FromUsage}} +{{^FromUsage}} + One of your mytokens will expire soon, and you have subscribed to be notified before it expires. +{{/FromUsage}} + +Your mytoken with the name '{{Name}}' will expire in {{ExpiresIn}}. +Here is some data about this mytoken: + +{{#Name}} + Name: {{Name}} +{{/Name}} +Created: {{Created}} +Expires: {{ExpiresAt}} + +If you want to create a new mytoken with the same properties you have the following options: +{{#FromUsage}} + - Create a new mytoken with the same properties at {{RecreateURL}} +{{/FromUsage}} +{{^FromUsage}} + - Paste the mytoken at {{TokeninfoURL}} and click on the 'Create a mytoken with the same properties' button. + - Create a new mytoken at {{CreateMTURL}} and select the properties you want +{{/FromUsage}} +Additionaly, you can unsubscribe from further notifications at {{UnsubscribeURL}}. + +Kind regards, +the mytoken notification service. \ No newline at end of file From c48ab14e730f0ab9b55af901b00fc300848b36fd Mon Sep 17 00:00:00 2001 From: zachmann Date: Thu, 7 Sep 2023 15:22:22 +0200 Subject: [PATCH 002/195] add guest mode --- CHANGELOG.md | 1 + internal/config/config.go | 15 +++ .../configuration/configurationEndpoint.go | 10 +- .../endpoints/federation/entityStatement.go | 4 +- internal/endpoints/guestmode/guestmode.go | 48 ++++++++++ internal/endpoints/settings/settings.go | 4 +- internal/server/api.go | 16 ++-- internal/server/middlerwares.go | 8 +- internal/server/paths/paths.go | 91 +++++++++++++++++++ internal/server/routes/routes.go | 86 +----------------- internal/server/server.go | 14 +-- 11 files changed, 186 insertions(+), 111 deletions(-) create mode 100644 internal/endpoints/guestmode/guestmode.go create mode 100644 internal/server/paths/paths.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d907a0b..445f3f8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ ### Features - Added experimental support for OpenID Connect federations +- Added "Guest mode" to try mytoken out without using a real OP ### API diff --git a/internal/config/config.go b/internal/config/config.go index 4b85b59b..b6763c83 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -8,6 +8,7 @@ import ( "github.com/lestrrat-go/jwx/jwa" "github.com/oidc-mytoken/utils/context" + utils2 "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/utils/utils/fileutil" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -16,6 +17,7 @@ import ( "gopkg.in/yaml.v3" "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/server/paths" "github.com/oidc-mytoken/server/internal/utils" "github.com/oidc-mytoken/server/internal/utils/errorfmt" @@ -161,6 +163,7 @@ type featuresConf struct { SSH sshConf `yaml:"ssh"` ServerProfiles serverProfilesConf `yaml:"server_profiles"` Federation federationConf `yaml:"federation"` + GuestMode onlyEnable `yaml:"guest_mode"` } func (c *featuresConf) validate() error { @@ -542,6 +545,18 @@ func validate() error { } conf.Providers[i] = p } + if conf.Features.GuestMode.Enabled { + iss := utils2.CombineURLPath(conf.IssuerURL, paths.GetCurrentAPIPaths().GuestModeOP) + p := ProviderConf{ + Issuer: iss, + Name: "Guest Mode", + Endpoints: &oauth2x.Endpoints{ + Authorization: utils2.CombineURLPath(iss, "auth"), + Token: utils2.CombineURLPath(iss, "token"), + }, + } + conf.Providers = append(conf.Providers, p) + } if conf.IssuerURL == "" { return errors.New("invalid config: issuer_url not set") } diff --git a/internal/endpoints/configuration/configurationEndpoint.go b/internal/endpoints/configuration/configurationEndpoint.go index 510645c7..5ed79d6c 100644 --- a/internal/endpoints/configuration/configurationEndpoint.go +++ b/internal/endpoints/configuration/configurationEndpoint.go @@ -11,7 +11,7 @@ import ( "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/model/version" "github.com/oidc-mytoken/server/internal/oidc/oidcfed" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" ) func SupportedProviders() []api.SupportedProviderConfig { @@ -67,8 +67,8 @@ func Init() { } func basicConfiguration() *pkg.MytokenConfiguration { - apiPaths := routes.GetCurrentAPIPaths() - otherPaths := routes.GetGeneralPaths() + apiPaths := paths.GetCurrentAPIPaths() + otherPaths := paths.GetGeneralPaths() return &pkg.MytokenConfiguration{ MytokenConfiguration: api.MytokenConfiguration{ Issuer: config.Get().IssuerURL, @@ -101,7 +101,7 @@ func addTokenRevocation(mytokenConfig *pkg.MytokenConfiguration) { if config.Get().Features.TokenRevocation.Enabled { mytokenConfig.RevocationEndpoint = utils.CombineURLPath( config.Get().IssuerURL, - routes.GetCurrentAPIPaths().RevocationEndpoint, + paths.GetCurrentAPIPaths().RevocationEndpoint, ) } } @@ -114,7 +114,7 @@ func addTransferCodes(mytokenConfig *pkg.MytokenConfiguration) { if config.Get().Features.TransferCodes.Enabled { mytokenConfig.TokenTransferEndpoint = utils.CombineURLPath( config.Get().IssuerURL, - routes.GetCurrentAPIPaths().TokenTransferEndpoint, + paths.GetCurrentAPIPaths().TokenTransferEndpoint, ) model.GrantTypeTransferCode.AddToSliceIfNotFound(&mytokenConfig.MytokenEndpointGrantTypesSupported) model.ResponseTypeTransferCode.AddToSliceIfNotFound(&mytokenConfig.ResponseTypesSupported) diff --git a/internal/endpoints/federation/entityStatement.go b/internal/endpoints/federation/entityStatement.go index d089c0b9..cfb62669 100644 --- a/internal/endpoints/federation/entityStatement.go +++ b/internal/endpoints/federation/entityStatement.go @@ -12,14 +12,14 @@ import ( "github.com/oidc-mytoken/server/internal/jws" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/model/version" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" ) func InitEntityConfiguration() { if config.Get().Features.Federation.Entity != nil { return } - otherPaths := routes.GetGeneralPaths() + otherPaths := paths.GetGeneralPaths() privacyURI := utils.CombineURLPath(config.Get().IssuerURL, otherPaths.Privacy) var err error config.Get().Features.Federation.Entity, err = oidcfed.NewFederationLeaf( diff --git a/internal/endpoints/guestmode/guestmode.go b/internal/endpoints/guestmode/guestmode.go new file mode 100644 index 00000000..20b69c71 --- /dev/null +++ b/internal/endpoints/guestmode/guestmode.go @@ -0,0 +1,48 @@ +package guestmode + +import ( + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/utils/utils" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/server/paths" + "github.com/oidc-mytoken/server/internal/server/routes" +) + +func Init(s fiber.Router) { + if !config.Get().Features.GuestMode.Enabled { + return + } + baseURL := paths.GetCurrentAPIPaths().GuestModeOP + conf = map[string]any{ + "token_endpoint": utils.CombineURLPath(config.Get().IssuerURL, baseURL, "token"), + "authorization_endpoint": utils.CombineURLPath(config.Get().IssuerURL, baseURL, "auth"), + } + router := s.Group(baseURL) + router.Get(paths.WellknownOpenIDConfiguration, handleConfig) + router.Get("auth", handleAuth) + router.Post("token", handleToken) +} + +var conf map[string]any + +func handleConfig(ctx *fiber.Ctx) error { + return ctx.JSON(conf) +} + +func handleAuth(ctx *fiber.Ctx) error { + state := ctx.Query("state") + return ctx.Redirect(routes.RedirectURI + "?state=" + state + "&code=code") +} + +func handleToken(ctx *fiber.Ctx) error { + return ctx.JSON( + map[string]any{ + "access_token": utils.RandASCIIString(64), + "refresh_token": utils.RandASCIIString(64), + "id_token": `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJndWVzdCJ9. +OI5skE5VAlQjI4rqAFUjqwGyEnmmQNXBTOvO7pukZoo`, + "expires_in": 600, + }, + ) +} diff --git a/internal/endpoints/settings/settings.go b/internal/endpoints/settings/settings.go index 1597d0b6..c2852c2e 100644 --- a/internal/endpoints/settings/settings.go +++ b/internal/endpoints/settings/settings.go @@ -17,7 +17,7 @@ import ( mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/rotation" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" "github.com/oidc-mytoken/server/internal/utils/auth" "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/ctxutils" @@ -27,7 +27,7 @@ import ( // InitSettings initializes the settings metadata func InitSettings() { - apiPaths := routes.GetCurrentAPIPaths() + apiPaths := paths.GetCurrentAPIPaths() settingsMetadata.GrantTypeEndpoint = utils.CombineURLPath( config.Get().IssuerURL, apiPaths.UserSettingEndpoint, "grants", ) diff --git a/internal/server/api.go b/internal/server/api.go index a1cfce13..66bad08f 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -5,6 +5,7 @@ import ( "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/endpoints/guestmode" "github.com/oidc-mytoken/server/internal/endpoints/profiles" "github.com/oidc-mytoken/server/internal/endpoints/revocation" "github.com/oidc-mytoken/server/internal/endpoints/settings" @@ -14,17 +15,18 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken" "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo" "github.com/oidc-mytoken/server/internal/model/version" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" ) func addAPIRoutes(s fiber.Router) { for v := config.Get().API.MinVersion; v <= version.MAJOR; v++ { addAPIvXRoutes(s, v) } + guestmode.Init(s) } func addAPIvXRoutes(s fiber.Router, version int) { - apiPaths := routes.GetAPIPaths(version) + apiPaths := paths.GetAPIPaths(version) s.Post(apiPaths.MytokenEndpoint, mytoken.HandleMytokenEndpoint) s.Post(apiPaths.AccessTokenEndpoint, access.HandleAccessTokenEndpoint) if config.Get().Features.TokenRevocation.Enabled { @@ -50,7 +52,7 @@ func addAPIvXRoutes(s fiber.Router, version int) { addProfileEndpointRoutes(s, apiPaths) } -func addProfileEndpointRoutes(r fiber.Router, apiPaths routes.APIPaths) { +func addProfileEndpointRoutes(r fiber.Router, apiPaths paths.APIPaths) { if !config.Get().Features.ServerProfiles.Enabled { return } @@ -73,12 +75,12 @@ func addProfileEndpointRoutes(r fiber.Router, apiPaths routes.APIPaths) { addProfileDeleteRoute(r, apiPaths, "rotation", profiles.HandleDeleteRotation) } -func addProfileGetRoute(r fiber.Router, apiPaths routes.APIPaths, profileTypePath string, handler fiber.Handler) { +func addProfileGetRoute(r fiber.Router, apiPaths paths.APIPaths, profileTypePath string, handler fiber.Handler) { r.Get(utils.CombineURLPath(apiPaths.ProfilesEndpoint, profileTypePath), handler) r.Get(utils.CombineURLPath(apiPaths.ProfilesEndpoint, ":group", profileTypePath), handler) } -func addProfileDeleteRoute(r fiber.Router, apiPaths routes.APIPaths, profileTypePath string, handler fiber.Handler) { +func addProfileDeleteRoute(r fiber.Router, apiPaths paths.APIPaths, profileTypePath string, handler fiber.Handler) { r.Delete( utils.CombineURLPath(apiPaths.ProfilesEndpoint, profileTypePath, ":id?"), returnGroupBasicMiddleware(), userIsGroupMiddleware, handler, @@ -89,7 +91,7 @@ func addProfileDeleteRoute(r fiber.Router, apiPaths routes.APIPaths, profileType ) } -func addProfileAddRoute(r fiber.Router, apiPaths routes.APIPaths, profileTypePath string, handler fiber.Handler) { +func addProfileAddRoute(r fiber.Router, apiPaths paths.APIPaths, profileTypePath string, handler fiber.Handler) { r.Post( utils.CombineURLPath(apiPaths.ProfilesEndpoint, profileTypePath), returnGroupBasicMiddleware(), userIsGroupMiddleware, handler, @@ -100,7 +102,7 @@ func addProfileAddRoute(r fiber.Router, apiPaths routes.APIPaths, profileTypePat ) } -func addProfileUpdateRoute(r fiber.Router, apiPaths routes.APIPaths, profileTypePath string, handler fiber.Handler) { +func addProfileUpdateRoute(r fiber.Router, apiPaths paths.APIPaths, profileTypePath string, handler fiber.Handler) { r.Put( utils.CombineURLPath(apiPaths.ProfilesEndpoint, profileTypePath, ":id?"), returnGroupBasicMiddleware(), userIsGroupMiddleware, handler, diff --git a/internal/server/middlerwares.go b/internal/server/middlerwares.go index deab1d35..9f32df43 100644 --- a/internal/server/middlerwares.go +++ b/internal/server/middlerwares.go @@ -22,7 +22,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/server/apipath" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" "github.com/oidc-mytoken/server/internal/utils/fileio" "github.com/oidc-mytoken/server/internal/utils/iputils" loggerUtils "github.com/oidc-mytoken/server/internal/utils/logger" @@ -138,9 +138,9 @@ func addRequestIDMiddleware(s fiber.Router) { func addCorsMiddleware(s fiber.Router) { allowedPaths := []string{ - routes.WellknownMytokenConfiguration, - routes.WellknownOpenIDConfiguration, - routes.GetGeneralPaths().JWKSEndpoint, + paths.WellknownMytokenConfiguration, + paths.WellknownOpenIDConfiguration, + paths.GetGeneralPaths().JWKSEndpoint, } allowedPrefixes := []string{ apipath.Prefix, diff --git a/internal/server/paths/paths.go b/internal/server/paths/paths.go new file mode 100644 index 00000000..c5b69057 --- /dev/null +++ b/internal/server/paths/paths.go @@ -0,0 +1,91 @@ +package paths + +import ( + "github.com/oidc-mytoken/utils/utils" + + "github.com/oidc-mytoken/server/internal/model/version" + "github.com/oidc-mytoken/server/internal/server/apipath" +) + +var routes *paths + +// WellknownMytokenConfiguration is the mytoken configuration path suffix +const WellknownMytokenConfiguration = "/.well-known/mytoken-configuration" + +// WellknownOpenIDConfiguration is the openid configuration path suffix +const WellknownOpenIDConfiguration = "/.well-known/openid-configuration" + +// WellknownOpenIDFederation is the openid federation path suffix +const WellknownOpenIDFederation = "/.well-known/openid-federation" + +func defaultAPIPaths(api string) APIPaths { + return APIPaths{ + MytokenEndpoint: utils.CombineURLPath(api, "/token/my"), + AccessTokenEndpoint: utils.CombineURLPath(api, "/token/access"), + TokenInfoEndpoint: utils.CombineURLPath(api, "/tokeninfo"), + RevocationEndpoint: utils.CombineURLPath(api, "/token/revoke"), + TokenTransferEndpoint: utils.CombineURLPath(api, "/token/transfer"), + UserSettingEndpoint: utils.CombineURLPath(api, "/settings"), + ProfilesEndpoint: utils.CombineURLPath(api, "/pt"), + GuestModeOP: utils.CombineURLPath(api, "/guests"), + } +} + +// init initializes the server route paths +func init() { + routes = &paths{ + api: map[int]APIPaths{ + 0: defaultAPIPaths(apipath.V0), + }, + other: GeneralPaths{ + ConfigurationEndpoint: WellknownMytokenConfiguration, + FederationEndpoint: WellknownOpenIDFederation, + OIDCRedirectEndpoint: "/redirect", + JWKSEndpoint: "/jwks", + ConsentEndpoint: "/c", + Privacy: "/privacy", + }, + } +} + +type paths struct { + api map[int]APIPaths + other GeneralPaths +} + +// GeneralPaths holds all non-api route paths +type GeneralPaths struct { + ConfigurationEndpoint string + FederationEndpoint string + OIDCRedirectEndpoint string + JWKSEndpoint string + ConsentEndpoint string + Privacy string +} + +// APIPaths holds all api route paths +type APIPaths struct { + MytokenEndpoint string + AccessTokenEndpoint string + TokenInfoEndpoint string + RevocationEndpoint string + TokenTransferEndpoint string + UserSettingEndpoint string + ProfilesEndpoint string + GuestModeOP string +} + +// GetCurrentAPIPaths returns the api paths for the most recent major version +func GetCurrentAPIPaths() APIPaths { + return GetAPIPaths(version.MAJOR) +} + +// GetAPIPaths returns the api paths for the passed major version +func GetAPIPaths(apiVersion int) APIPaths { + return routes.api[apiVersion] +} + +// GetGeneralPaths returns the non-API paths +func GetGeneralPaths() GeneralPaths { + return routes.other +} diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index e3da91fe..c44810fe 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -4,97 +4,15 @@ import ( "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/server/internal/config" - "github.com/oidc-mytoken/server/internal/model/version" - "github.com/oidc-mytoken/server/internal/server/apipath" + "github.com/oidc-mytoken/server/internal/server/paths" ) -var routes *paths - -// WellknownMytokenConfiguration is the mytoken configuration path suffix -const WellknownMytokenConfiguration = "/.well-known/mytoken-configuration" - -// WellknownOpenIDConfiguration is the openid configuration path suffix -const WellknownOpenIDConfiguration = "/.well-known/openid-configuration" - -// WellknownOpenIDFederation is the openid federation path suffix -const WellknownOpenIDFederation = "/.well-known/openid-federation" - -func defaultAPIPaths(api string) APIPaths { - return APIPaths{ - MytokenEndpoint: utils.CombineURLPath(api, "/token/my"), - AccessTokenEndpoint: utils.CombineURLPath(api, "/token/access"), - TokenInfoEndpoint: utils.CombineURLPath(api, "/tokeninfo"), - RevocationEndpoint: utils.CombineURLPath(api, "/token/revoke"), - TokenTransferEndpoint: utils.CombineURLPath(api, "/token/transfer"), - UserSettingEndpoint: utils.CombineURLPath(api, "/settings"), - ProfilesEndpoint: utils.CombineURLPath(api, "/pt"), - } -} - -// init initializes the server route paths -func init() { - routes = &paths{ - api: map[int]APIPaths{ - 0: defaultAPIPaths(apipath.V0), - }, - other: GeneralPaths{ - ConfigurationEndpoint: WellknownMytokenConfiguration, - FederationEndpoint: WellknownOpenIDFederation, - OIDCRedirectEndpoint: "/redirect", - JWKSEndpoint: "/jwks", - ConsentEndpoint: "/c", - Privacy: "/privacy", - }, - } -} - -type paths struct { - api map[int]APIPaths - other GeneralPaths -} - -// GeneralPaths holds all non-api route paths -type GeneralPaths struct { - ConfigurationEndpoint string - FederationEndpoint string - OIDCRedirectEndpoint string - JWKSEndpoint string - ConsentEndpoint string - Privacy string -} - -// APIPaths holds all api route paths -type APIPaths struct { - MytokenEndpoint string - AccessTokenEndpoint string - TokenInfoEndpoint string - RevocationEndpoint string - TokenTransferEndpoint string - UserSettingEndpoint string - ProfilesEndpoint string -} - -// GetCurrentAPIPaths returns the api paths for the most recent major version -func GetCurrentAPIPaths() APIPaths { - return GetAPIPaths(version.MAJOR) -} - -// GetAPIPaths returns the api paths for the passed major version -func GetAPIPaths(apiVersion int) APIPaths { - return routes.api[apiVersion] -} - -// GetGeneralPaths returns the non-API paths -func GetGeneralPaths() GeneralPaths { - return routes.other -} - var RedirectURI string var ConsentEndpoint string // Init initializes the authcode component func Init() { - generalPaths := GetGeneralPaths() + generalPaths := paths.GetGeneralPaths() RedirectURI = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.OIDCRedirectEndpoint) ConsentEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConsentEndpoint) } diff --git a/internal/server/server.go b/internal/server/server.go index 0be5b690..c9f379ed 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,7 +22,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/redirect" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/server/apipath" - "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/server/paths" "github.com/oidc-mytoken/server/internal/server/ssh" "github.com/oidc-mytoken/server/internal/utils/fileio" ) @@ -104,19 +104,19 @@ func Init() { func addRoutes(s fiber.Router) { addWebRoutes(s) - s.Get(routes.GetGeneralPaths().ConfigurationEndpoint, configuration.HandleConfiguration) - s.Get(routes.WellknownOpenIDConfiguration, configuration.HandleConfiguration) + s.Get(paths.GetGeneralPaths().ConfigurationEndpoint, configuration.HandleConfiguration) + s.Get(paths.WellknownOpenIDConfiguration, configuration.HandleConfiguration) if config.Get().Features.Federation.Enabled { - s.Get(routes.GetGeneralPaths().FederationEndpoint, federation.HandleEntityConfiguration) + s.Get(paths.GetGeneralPaths().FederationEndpoint, federation.HandleEntityConfiguration) } - s.Get(routes.GetGeneralPaths().JWKSEndpoint, endpoints.HandleJWKS) - s.Get(routes.GetGeneralPaths().OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) + s.Get(paths.GetGeneralPaths().JWKSEndpoint, endpoints.HandleJWKS) + s.Get(paths.GetGeneralPaths().OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) s.Get("/c/:consent_code", consent.HandleConsent) s.Post("/c/:consent_code", consent.HandleConsentPost) s.Post("/c", consent.HandleCreateConsent) s.Get("/native", handleNativeCallback) s.Get("/native/abort", handleNativeConsentAbortCallback) - s.Get(routes.GetGeneralPaths().Privacy, handlePrivacy) + s.Get(paths.GetGeneralPaths().Privacy, handlePrivacy) s.Get("/settings", handleSettings) addAPIRoutes(s) } From bc5f2cb3a770be5691784325ef51124fe6f66096 Mon Sep 17 00:00:00 2001 From: zachmann Date: Thu, 7 Sep 2023 10:56:20 +0200 Subject: [PATCH 003/195] WIP notifications --- go.mod | 2 + internal/config/config.go | 11 +- internal/db/dbmigrate/scripts/v0.10.0.pre.sql | 267 ++++++++++++++++++ internal/db/dbrepo/mytokenrepo/mytoken.go | 11 +- .../mytokenrepo/mytokenrepohelper/helpers.go | 22 ++ internal/db/notificationsrepo/sync.go | 1 + internal/model/version/VERSION | 2 +- internal/mytoken/pkg/mytoken.go | 54 +++- internal/server/server.go | 1 + .../server/web/sites/mail_notify.mustache | 18 +- 10 files changed, 365 insertions(+), 24 deletions(-) create mode 100644 internal/db/dbmigrate/scripts/v0.10.0.pre.sql diff --git a/go.mod b/go.mod index 2874e30f..79f0255c 100644 --- a/go.mod +++ b/go.mod @@ -74,3 +74,5 @@ require ( ) replace github.com/urfave/cli/v2 => github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704 + +replace github.com/oidc-mytoken/api => ../api \ No newline at end of file diff --git a/internal/config/config.go b/internal/config/config.go index 222426c3..4486454f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -161,7 +161,7 @@ type featuresConf struct { SSH sshConf `yaml:"ssh"` ServerProfiles serverProfilesConf `yaml:"server_profiles"` Federation federationConf `yaml:"federation"` - Notifications notificationConf `yaml:"notifications"` + Notifications notificationConf `yaml:"notifications"` } func (c *featuresConf) validate() error { @@ -248,12 +248,11 @@ type notificationConf struct { Mail mailNotificationConf `yaml:"email"` Websocket websocketNotificationConf `yaml:"ws"` ICS icsNotificationConf `yaml:"ics"` - ICAL icalNotificationConf `yaml:"ical"` } func (c *notificationConf) validate() error { - c.AnyEnabled = true || c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled || c.ICAL.Enabled - c.SchedulerNeeded = true || c.Mail.Enabled || c.Websocket.Enabled + c.AnyEnabled = true || c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled //TODO + c.SchedulerNeeded = true || c.Mail.Enabled || c.Websocket.Enabled //TODO return nil //TODO } @@ -278,10 +277,6 @@ type icsNotificationConf struct { onlyEnable } -type icalNotificationConf struct { - onlyEnable -} - type tokeninfoConfig struct { Enabled bool `yaml:"-"` Introspect onlyEnable `yaml:"introspect"` diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql new file mode 100644 index 00000000..5526ed6d --- /dev/null +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -0,0 +1,267 @@ +# Tables +ALTER TABLE Users + ADD email TEXT NULL; +ALTER TABLE Users + ADD email_verified BOOL DEFAULT 0 NOT NULL; + +ALTER TABLE MTokens + ADD capabilities JSON NULL; +ALTER TABLE MTokens + ADD rotation JSON NULL; +ALTER TABLE MTokens + ADD restrictions JSON NULL; + +CREATE OR REPLACE TABLE Calendars +( + id VARCHAR(128) NOT NULL + PRIMARY KEY, + name VARCHAR(128) NULL, + uid BIGINT UNSIGNED NOT NULL, + ics_path VARCHAR(128) NOT NULL, + CONSTRAINT Calendars_UN + UNIQUE (ics_path), + CONSTRAINT Calendars_UN_1 + UNIQUE (name, uid), + CONSTRAINT Calendars_FK + FOREIGN KEY (uid) REFERENCES Users (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE NotificationSchedulerSync +( + sync_i BIGINT NOT NULL + PRIMARY KEY +); + +CREATE OR REPLACE TABLE Notifications +( + id VARCHAR(128) NOT NULL + PRIMARY KEY, type VARCHAR(32) NOT NULL, created DATETIME DEFAULT CURRENT_TIMESTAMP() NOT NULL +); + +CREATE OR REPLACE TABLE CalendarMapping +( + calendar_id VARCHAR(128) NOT NULL, MT_id VARCHAR(128) NOT NULL, CONSTRAINT CalendarMapping_FK + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT CalendarMapping_FK_1 + FOREIGN KEY (calendar_id) REFERENCES Calendars (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + + +CREATE OR REPLACE TABLE NotificationEventSubscriptions +( + notification_id VARCHAR(128) NOT NULL, event INT UNSIGNED NOT NULL, CONSTRAINT NotificationEventSubscriptions_FK + FOREIGN KEY (event) REFERENCES Events (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT NotificationEventSubscriptions_FK_1 + FOREIGN KEY (notification_id) REFERENCES Notifications (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE NotificationSubscriptions +( + notification_id VARCHAR(128) NOT NULL, + MT_id VARCHAR(128) NOT NULL, + added DATETIME DEFAULT CURRENT_TIMESTAMP() NOT NULL, + subscription_id VARCHAR(128) NOT NULL + PRIMARY KEY, + CONSTRAINT NotificationSubscriptions_UN + UNIQUE (notification_id, MT_id), + CONSTRAINT NotificationSubscriptions_FK + FOREIGN KEY (notification_id) REFERENCES Notifications (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT NotificationSubscriptions_FK_1 + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE NotificationSchedule +( + due_time DATETIME NOT NULL, + subscription_id VARCHAR(128) NOT NULL, + type VARCHAR(128) NOT NULL, + CONSTRAINT NotificationSchedule_FK + FOREIGN KEY (subscription_id) REFERENCES NotificationSubscriptions (subscription_id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE INDEX Notifications_FK ON Notifications (type); + +CREATE OR REPLACE TABLE Actions +( + id INT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, action VARCHAR(128) NOT NULL, CONSTRAINT Actions_UN + UNIQUE (action) +); + +CREATE OR REPLACE TABLE ActionCodes +( + id BIGINT UNSIGNED AUTO_INCREMENT + PRIMARY KEY, + action INT UNSIGNED NOT NULL, + code VARCHAR(128) NOT NULL, + expires_at DATETIME NULL, + CONSTRAINT ActionCodes_UN + UNIQUE (code), + CONSTRAINT ActionCodes_FK + FOREIGN KEY (action) REFERENCES Actions (id) +); + +CREATE OR REPLACE TABLE ActionReferencesMytokens +( + action_id BIGINT UNSIGNED NOT NULL, MT_id VARCHAR(128) NOT NULL, CONSTRAINT ActionReferencesMytokens_FK + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT ActionReferencesMytokens_FK_1 + FOREIGN KEY (MT_id) REFERENCES MTokens (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE ActionReferencesNotification +( + action_id BIGINT UNSIGNED NOT NULL, + notification_id VARCHAR(128) NOT NULL, + CONSTRAINT ActionReferencesNotification_FK + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE, + CONSTRAINT ActionReferencesNotification_FK_1 + FOREIGN KEY (notification_id) REFERENCES Notifications (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +CREATE OR REPLACE TABLE ActionReferencesUser +( + action_id BIGINT UNSIGNED NOT NULL, uid BIGINT UNSIGNED NOT NULL, CONSTRAINT ActionReferencesUser_FK + FOREIGN KEY (uid) REFERENCES Users (id) + ON UPDATE CASCADE ON DELETE CASCADE, CONSTRAINT ActionReferencesUser_FK_1 + FOREIGN KEY (action_id) REFERENCES ActionCodes (id) + ON UPDATE CASCADE ON DELETE CASCADE +); + +# Views + +CREATE OR REPLACE VIEW MailVerificationCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `aru`.`uid` AS `uid` + FROM ((SELECT `ac`.`id` AS `id`, `ac`.`action` AS `action`, `ac`.`code` AS `code`, `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'verify_email')) `fa` JOIN `ActionReferencesUser` `aru` + ON (`aru`.`action_id` = `fa`.`id`)); + +CREATE OR REPLACE VIEW MytokenRecreateAndUnsubscribeCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `arn`.`notification_id` AS `notification_id`, + `arm`.`MT_id` AS `MT_id` + FROM (((SELECT `ac`.`id` AS `id`, + `ac`.`action` AS `action`, + `ac`.`code` AS `code`, + `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'token_recreate_unsubscribe')) `fa` JOIN `ActionReferencesNotification` `arn` + ON (`arn`.`action_id` = `fa`.`id`)) JOIN `ActionReferencesMytokens` `arm` + ON (`arm`.`action_id` = `fa`.`id`)); + +CREATE OR REPLACE VIEW NotificationUnsubscribeCodes AS +SELECT `fa`.`id` AS `id`, + `fa`.`action` AS `action`, + `fa`.`code` AS `code`, + `fa`.`expires_at` AS `expires_at`, + `arn`.`notification_id` AS `notification_id` + FROM ((SELECT `ac`.`id` AS `id`, `ac`.`action` AS `action`, `ac`.`code` AS `code`, `ac`.`expires_at` AS `expires_at` + FROM `ActionCodes` `ac` + WHERE `ac`.`action` = (SELECT `a`.`id` + FROM `Actions` `a` + WHERE `a`.`action` = 'unsubscribe_notification')) `fa` JOIN `ActionReferencesNotification` `arn` + ON (`arn`.`action_id` = `fa`.`id`)); + + +# Values +INSERT IGNORE INTO Events (event) + VALUES ('expired'); +INSERT IGNORE INTO Events (event) + VALUES ('revoked'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_subscribed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_unsubscribed'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_subscribed_other'); +INSERT IGNORE INTO Events (event) + VALUES ('notification_unsubscribed_other'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_created'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('calendar_deleted'); + +INSERT IGNORE INTO Actions (action) + VALUES ('verify_email'); +INSERT IGNORE INTO Actions (action) + VALUES ('unsubscribe_notification'); +INSERT IGNORE INTO Actions (action) + VALUES ('token_recreate_unsubscribe'); + + +# Procedures +DELIMITER ;; + +CREATE OR REPLACE PROCEDURE MTokens_Insert(IN SUB TEXT, IN ISS TEXT, IN MTID VARCHAR(128), + IN SEQNO_ BIGINT UNSIGNED, IN PARENT VARCHAR(128), + IN RTID BIGINT UNSIGNED, IN NAME_ TEXT, IN IP TEXT, + IN EXPIRES_AT_ DATETIME, IN CAPABILITIES_ TEXT, + IN ROTATION_ TEXT, IN RESTR TEXT) +BEGIN + SET TIME_ZONE = "+0:00"; + CALL Users_GetID(SUB, ISS, @UID); + INSERT INTO MTokens (id, seqno, parent_id, rt_id, name, ip_created, user_id, expires_at, capabilities, rotation, + restrictions) + VALUES (MTID, SEQNO_, PARENT, RTID, NAME_, IP, @UID, EXPIRES_AT_, CAPABILITIES_, ROTATION_, RESTR); +END;; + +CREATE OR REPLACE PROCEDURE MTokens_SetMetadata(IN MTID VARCHAR(128), IN CAPABILITIES_ TEXT, + IN ROTATION_ TEXT, IN RESTR TEXT) +BEGIN + UPDATE MTokens m SET m.capabilities=CAPABILITIES_ WHERE m.id = MTID AND m.capabilities IS NULL; + UPDATE MTokens m SET m.rotation=ROTATION_ WHERE m.id = MTID AND m.rotation IS NULL; + UPDATE MTokens m SET m.restrictions=RESTR WHERE m.id = MTID AND m.restrictions IS NULL; +END;; + +CREATE OR REPLACE PROCEDURE Users_SetMail(IN UID BIGINT UNSIGNED, IN MAIL TEXT, IN VERIFIED BIT) +BEGIN + UPDATE Users u SET u.email=MAIL, u.email_verified=VERIFIED WHERE u.id = UID; +END;; + +CREATE OR REPLACE PROCEDURE Users_SetMailBySub(IN SUB TEXT, IN ISS TEXT, IN MAIL TEXT, IN VERIFIED BIT) +BEGIN + CALL Users_GetID(SUB, ISS, @UID); + CALL Users_SetMail(@UID, MAIL, VERIFIED); +END;; + +CREATE OR REPLACE PROCEDURE Cleanup_ActionCodes() +BEGIN + SET TIME_ZONE = "+0:00"; + DELETE FROM ActionCodes WHERE expires_at < CURRENT_TIMESTAMP(); +END;; + +CREATE OR REPLACE PROCEDURE Cleanup() +BEGIN + CALL Cleanup_MTokens(); + CALL Cleanup_AuthInfo(); + CALL Cleanup_ProxyTokens(); + CALL Cleanup_ActionCodes(); +END;; + + +DELIMITER ; diff --git a/internal/db/dbrepo/mytokenrepo/mytoken.go b/internal/db/dbrepo/mytokenrepo/mytoken.go index 0a289145..3a7681cd 100644 --- a/internal/db/dbrepo/mytokenrepo/mytoken.go +++ b/internal/db/dbrepo/mytokenrepo/mytoken.go @@ -11,6 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/db" + helper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" @@ -103,6 +104,12 @@ func (mte *MytokenEntry) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx, comment st Sub: mte.Token.OIDCSubject, ExpiresAt: db.NewNullTime(mte.expiresAt.Time()), } + meta, err := mte.Token.DBMetadata() + if err != nil { + return err + } + steStore.MytokenDBMetadata = meta + return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { if mte.rtID == nil { @@ -148,6 +155,7 @@ type mytokenEntryStore struct { Iss string Sub string ExpiresAt sql.NullTime + helper.MytokenDBMetadata } // Store stores the mytokenEntryStore in the database; if this is the first token for this user, the user is also added @@ -157,7 +165,8 @@ func (e *mytokenEntryStore) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec( `CALL MTokens_Insert(?,?,?,?,?,?,?,?,?)`, - e.Sub, e.Iss, e.ID, e.SeqNo, e.ParentID, e.RefreshTokenID, e.Name, e.IP, e.ExpiresAt, + e.Sub, e.Iss, e.ID, e.SeqNo, e.ParentID, e.RefreshTokenID, e.Name, e.IP, e.ExpiresAt, e.Capabilities, + e.Rotation, e.Restrictions, ) return errors.WithStack(err) }, diff --git a/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go b/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go index 0db6a6b3..46cfc5e3 100644 --- a/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go +++ b/internal/db/dbrepo/mytokenrepo/mytokenrepohelper/helpers.go @@ -112,6 +112,28 @@ func UpdateSeqNo(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, seqno uint ) } +type MytokenDBMetadata struct { + Capabilities db.NullString + Rotation db.NullString + Restrictions db.NullString +} + +// SetMetadata adds a mytoken's metadata (capabilities, rotation, +// restrictions) to the database. This is needed for legacy mytokens where the metadata was not yet stored on +// creation. token version <0.7 +func SetMetadata( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, meta MytokenDBMetadata, +) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec( + `CALL MTokens_SetMetadata(?,?,?,?)`, id, meta.Capabilities, meta.Rotation, meta.Restrictions, + ) + return errors.WithStack(err) + }, + ) +} + // revokeMT revokes the passed mytoken but no children func revokeMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id interface{}) error { return db.RunWithinTransaction( diff --git a/internal/db/notificationsrepo/sync.go b/internal/db/notificationsrepo/sync.go index 1cc98f2f..74c2efe2 100644 --- a/internal/db/notificationsrepo/sync.go +++ b/internal/db/notificationsrepo/sync.go @@ -27,6 +27,7 @@ func NotificationScheduler() { } // do stuff as master fmt.Println("I now would send notifications if there are any") + //TODO remove time.Sleep(time.Duration(rand.Float32()*120) * time.Second) } } diff --git a/internal/model/version/VERSION b/internal/model/version/VERSION index 899f24fc..2774f858 100644 --- a/internal/model/version/VERSION +++ b/internal/model/version/VERSION @@ -1 +1 @@ -0.9.0 \ No newline at end of file +0.10.0 \ No newline at end of file diff --git a/internal/mytoken/pkg/mytoken.go b/internal/mytoken/pkg/mytoken.go index dcc11195..cb3e13ac 100644 --- a/internal/mytoken/pkg/mytoken.go +++ b/internal/mytoken/pkg/mytoken.go @@ -1,6 +1,7 @@ package mytoken import ( + "encoding/json" "fmt" "github.com/golang-jwt/jwt" @@ -13,6 +14,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" response "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/jws" @@ -310,7 +312,57 @@ func parseJWT(token string, skipCalimsValidation bool) (*Mytoken, error) { if mt, ok := tok.Claims.(*Mytoken); ok && tok.Valid { mt.jwt = token - return mt, nil + return mt, specialTokenHandling(mt) } return nil, errors.New("token not valid") } + +func (mt *Mytoken) DBMetadata() (meta mytokenrepohelper.MytokenDBMetadata, err error) { + creator := func(i any) (db.NullString, error) { + data := "" + if i != nil { + dataBytes, err := json.Marshal(i) + if err != nil { + return db.NullString{}, errors.WithStack(err) + } + data = string(dataBytes) + } + return db.NewNullString(data), nil + } + + meta.Capabilities, err = creator(mt.Capabilities) + if err != nil { + return + } + meta.Rotation, err = creator(mt.Rotation) + if err != nil { + return + } + meta.Restrictions, err = creator(mt.Restrictions) + if err != nil { + return + } + return +} + +func specialTokenHandling(mt *Mytoken) error { + if mt.Version.Before( + api.TokenVersion{ + Major: 0, + Minor: 7, + }, + ) { + _ = db.Transact( + log.StandardLogger(), func(tx *sqlx.Tx) error { + meta, err := mt.DBMetadata() + if err != nil { + return err + } + return mytokenrepohelper.SetMetadata( + log.StandardLogger(), tx, mt.ID, meta, + ) + }, + ) + } + return nil +} diff --git a/internal/server/server.go b/internal/server/server.go index 2e178c09..698bb4f4 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -130,6 +130,7 @@ func addWebRoutes(s fiber.Router) { func start(s *fiber.App) { if config.Get().Features.Notifications.SchedulerNeeded { go notificationsrepo.NotificationScheduler() + //TODO remove go notificationsrepo.NotificationScheduler() go notificationsrepo.NotificationScheduler() } diff --git a/internal/server/web/sites/mail_notify.mustache b/internal/server/web/sites/mail_notify.mustache index d126e457..8426e781 100644 --- a/internal/server/web/sites/mail_notify.mustache +++ b/internal/server/web/sites/mail_notify.mustache @@ -1,10 +1,3 @@ -{{#FromUsage}} - A mytoken that will expire soon was just used, and you have subscribed to be notified before it expires. -{{/FromUsage}} -{{^FromUsage}} - One of your mytokens will expire soon, and you have subscribed to be notified before it expires. -{{/FromUsage}} - Your mytoken with the name '{{Name}}' will expire in {{ExpiresIn}}. Here is some data about this mytoken: @@ -15,14 +8,13 @@ Created: {{Created}} Expires: {{ExpiresAt}} If you want to create a new mytoken with the same properties you have the following options: -{{#FromUsage}} - - Create a new mytoken with the same properties at {{RecreateURL}} -{{/FromUsage}} -{{^FromUsage}} +{{#RecreateURL}} + - Create a new mytoken with the same properties and unsubscribe from further notifications by clicking on {{.}} +{{/RecreateURL}} - Paste the mytoken at {{TokeninfoURL}} and click on the 'Create a mytoken with the same properties' button. - Create a new mytoken at {{CreateMTURL}} and select the properties you want -{{/FromUsage}} -Additionaly, you can unsubscribe from further notifications at {{UnsubscribeURL}}. + +To unsubscribe from further notifications open: {{UnsubscribeURL}} Kind regards, the mytoken notification service. \ No newline at end of file From 4f6ee033839ca837664582bd90dbdfcb8222aea0 Mon Sep 17 00:00:00 2001 From: zachmann Date: Fri, 8 Sep 2023 09:52:12 +0200 Subject: [PATCH 004/195] WIP notifications --- internal/db/dbrepo/mytokenrepo/mytoken.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/dbrepo/mytokenrepo/mytoken.go b/internal/db/dbrepo/mytokenrepo/mytoken.go index 3a7681cd..30c93859 100644 --- a/internal/db/dbrepo/mytokenrepo/mytoken.go +++ b/internal/db/dbrepo/mytokenrepo/mytoken.go @@ -164,7 +164,7 @@ func (e *mytokenEntryStore) Store(rlog log.Ext1FieldLogger, tx *sqlx.Tx) error { return db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { _, err := tx.Exec( - `CALL MTokens_Insert(?,?,?,?,?,?,?,?,?)`, + `CALL MTokens_Insert(?,?,?,?,?,?,?,?,?,?,?,?)`, e.Sub, e.Iss, e.ID, e.SeqNo, e.ParentID, e.RefreshTokenID, e.Name, e.IP, e.ExpiresAt, e.Capabilities, e.Rotation, e.Restrictions, ) From 9f20d301d0f92587be7c6b80f9eb877243b97aea Mon Sep 17 00:00:00 2001 From: zachmann Date: Fri, 8 Sep 2023 10:17:51 +0200 Subject: [PATCH 005/195] [guest mode] create new subject on each request --- internal/endpoints/guestmode/guestmode.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/endpoints/guestmode/guestmode.go b/internal/endpoints/guestmode/guestmode.go index 20b69c71..e8b56474 100644 --- a/internal/endpoints/guestmode/guestmode.go +++ b/internal/endpoints/guestmode/guestmode.go @@ -1,6 +1,8 @@ package guestmode import ( + "encoding/base64" + "github.com/gofiber/fiber/v2" "github.com/oidc-mytoken/utils/utils" @@ -36,13 +38,14 @@ func handleAuth(ctx *fiber.Ctx) error { } func handleToken(ctx *fiber.Ctx) error { + sub := "guest-" + utils.RandASCIIString(16) + idToken := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + base64.URLEncoding.EncodeToString([]byte(sub)) + "." return ctx.JSON( map[string]any{ "access_token": utils.RandASCIIString(64), "refresh_token": utils.RandASCIIString(64), - "id_token": `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJndWVzdCJ9. -OI5skE5VAlQjI4rqAFUjqwGyEnmmQNXBTOvO7pukZoo`, - "expires_in": 600, + "id_token": idToken, + "expires_in": 600, }, ) } From 79c77ca315c910e69e62788401be0daced114a4d Mon Sep 17 00:00:00 2001 From: zachmann Date: Tue, 19 Sep 2023 13:15:17 +0200 Subject: [PATCH 006/195] WIP notifications; calendars --- go.mod | 3 +- go.sum | 8 + internal/config/config.go | 22 +- internal/db/dbmigrate/scripts/v0.10.0.pre.sql | 9 +- internal/db/dbrepo/mytokenrepo/tree/tree.go | 10 + .../calendarrepo/calendar.go | 78 +++++ .../notification/calendar/calendar.go | 298 ++++++++++++++++++ .../calendar/pkg/calendarRequest.go | 12 + internal/endpoints/tokeninfo/history.go | 4 +- internal/mytoken/event/pkg/event.go | 17 + internal/server/api.go | 13 + internal/server/paths/paths.go | 6 + internal/server/routes/routes.go | 2 + internal/server/server.go | 14 +- internal/utils/auth/auther.go | 56 +++- 15 files changed, 525 insertions(+), 27 deletions(-) create mode 100644 internal/db/notificationsrepo/calendarrepo/calendar.go create mode 100644 internal/endpoints/notification/calendar/calendar.go create mode 100644 internal/endpoints/notification/calendar/pkg/calendarRequest.go diff --git a/go.mod b/go.mod index 271c6477..9eaeeec9 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/adam-hanna/arrayOperations v1.0.1 // indirect github.com/andybalholm/brotli v1.0.5 // indirect github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect + github.com/arran4/golang-ical v0.1.0 // indirect github.com/cbroglie/mustache v1.4.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect @@ -75,4 +76,4 @@ require ( replace github.com/urfave/cli/v2 => github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704 -replace github.com/oidc-mytoken/api => ../api \ No newline at end of file +replace github.com/oidc-mytoken/api => ../api diff --git a/go.sum b/go.sum index 1558637b..ac352288 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHG github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/arran4/golang-ical v0.1.0 h1:Oz0Rd5fpeNoHNFF9B9H5uYZyt1ubuZSZ3LVdHD5KvZI= +github.com/arran4/golang-ical v0.1.0/go.mod h1:BSTTrYHuM12oAL8jDdcmPdw02SBThKYWNFHQlvEG6b0= github.com/bsm/ginkgo/v2 v2.9.5 h1:rtVBYPs3+TC5iLUVOis1B9tjLTup7Cj5IfzosKtvTJ0= github.com/bsm/gomega v1.26.0 h1:LhQm+AFcgV2M0WyKroMASzAzCAJVpAxQXv4SaI9a69Y= github.com/cbroglie/mustache v1.4.0 h1:Azg0dVhxTml5me+7PsZ7WPrQq1Gkf3WApcHMjMprYoU= @@ -95,6 +97,7 @@ github.com/coreos/go-oidc/v3 v3.6.0/go.mod h1:ZpHUsHBucTUj6WOkrP4E20UPynbLZzhTQ1 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -251,6 +254,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf2E2un5A= github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y= github.com/lestrrat-go/blackmagic v1.0.1 h1:lS5Zts+5HIC/8og6cGHb0uCcNCa3OUt1ygh3Qz2Fe80= @@ -276,6 +280,7 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oidc-mytoken/api v0.9.1/go.mod h1:DBIlUbaIgGlf607VZx8zFC97VR3WNN0kaMVO1AqyTdE= github.com/oidc-mytoken/api v0.11.1/go.mod h1:bd7obYvztiIQW1PoRVBTOg8/clWlauNGwcZEu5mRbwg= github.com/oidc-mytoken/api v0.11.2-0.20230810083726-bf164306e5b2 h1:GY8oBE+ZCEWUIfR2DdRqSjHdqTWWv38CsBqRO4EW+QA= @@ -819,11 +824,14 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/config/config.go b/internal/config/config.go index 635ba129..30daed96 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -246,16 +246,16 @@ func (g profileGroupsCredentials) validate() error { } type notificationConf struct { - AnyEnabled bool `yaml:"-"` - SchedulerNeeded bool `yaml:"-"` - Mail mailNotificationConf `yaml:"email"` - Websocket websocketNotificationConf `yaml:"ws"` - ICS icsNotificationConf `yaml:"ics"` + AnyEnabled bool `yaml:"-"` + SchedulerNeeded bool `yaml:"-"` + Mail mailNotificationConf `yaml:"email"` + Websocket onlyEnable `yaml:"ws"` + ICS onlyEnable `yaml:"ics"` } func (c *notificationConf) validate() error { - c.AnyEnabled = true || c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled //TODO - c.SchedulerNeeded = true || c.Mail.Enabled || c.Websocket.Enabled //TODO + c.AnyEnabled = c.Mail.Enabled || c.Websocket.Enabled || c.ICS.Enabled + c.SchedulerNeeded = c.Mail.Enabled || c.Websocket.Enabled return nil //TODO } @@ -272,14 +272,6 @@ type mailServerConf struct { FromAddress string `yaml:"from_address"` } -type websocketNotificationConf struct { - onlyEnable -} - -type icsNotificationConf struct { - onlyEnable -} - type tokeninfoConfig struct { Enabled bool `yaml:"-"` Introspect onlyEnable `yaml:"introspect"` diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql index 5526ed6d..2b0c8e33 100644 --- a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -17,7 +17,7 @@ CREATE OR REPLACE TABLE Calendars PRIMARY KEY, name VARCHAR(128) NULL, uid BIGINT UNSIGNED NOT NULL, - ics_path VARCHAR(128) NOT NULL, + ics_path VARCHAR(128) NOT NULL, ics LONGTEXT NOT NULL, CONSTRAINT Calendars_UN UNIQUE (ics_path), CONSTRAINT Calendars_UN_1 @@ -263,5 +263,12 @@ BEGIN CALL Cleanup_ActionCodes(); END;; +CREATE OR REPLACE PROCEDURE Mtokens_GetInfo(IN MTID VARCHAR(128)) +BEGIN + SELECT id, parent_id, id AS mom_id, name, created, expires_at, ip_created AS ip + FROM MTokens + WHERE id = MTID; +END; + DELIMITER ; diff --git a/internal/db/dbrepo/mytokenrepo/tree/tree.go b/internal/db/dbrepo/mytokenrepo/tree/tree.go index de3ce279..c97a6c78 100644 --- a/internal/db/dbrepo/mytokenrepo/tree/tree.go +++ b/internal/db/dbrepo/mytokenrepo/tree/tree.go @@ -35,6 +35,16 @@ func (ste *MytokenEntry) Root() bool { return !ste.ParentID.HashValid() } +func SingleTokenEntry(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) (m MytokenEntry, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&m, `CALL MTokens_GetInfo(?)`, tokenID)) + }, + ) + return + +} + // AllTokens returns information about all mytokens for the user linked to the passed mytoken func AllTokens(rlog log.Ext1FieldLogger, tx *sqlx.Tx, tokenID mtid.MTID) ([]*MytokenEntryTree, error) { var tokens []*MytokenEntry diff --git a/internal/db/notificationsrepo/calendarrepo/calendar.go b/internal/db/notificationsrepo/calendarrepo/calendar.go new file mode 100644 index 00000000..0ba1039f --- /dev/null +++ b/internal/db/notificationsrepo/calendarrepo/calendar.go @@ -0,0 +1,78 @@ +package calendarrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// CalendarInfo is a type holding the information stored in the database related to a calendar +type CalendarInfo struct { + ID string `db:"id" json:"id"` + Name string `db:"name" json:"name"` + ICSPath string `db:"ics_path" json:"ics_path"` + ICS string `db:"ics" json:"-"` +} + +// Insert inserts a calendar for the given user (given by the mytoken) into the database +func Insert(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, info CalendarInfo) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Insert(?,?,?,?,?)`, mtID, info.ID, info.Name, info.ICSPath, info.ICS) + return errors.WithStack(err) + }, + ) +} + +// Delete deletes a calendar for the given user (given by the mytoken) from the database +func Delete(rlog log.Ext1FieldLogger, tx *sqlx.Tx, myid mtid.MTID, name string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Delete(?,?)`, myid, name) + return errors.WithStack(err) + }, + ) +} + +// Update updates a calendar entry in the database +func Update(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, info CalendarInfo) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_Update(?,?,?,?)`, mtID, info.ID, info.Name, info.ICS) + return errors.WithStack(err) + }, + ) +} + +// Get returns a calendar entry for a user and name +func Get(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, name string) (info CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&info, `CALL Calendar_Get(?,?)`, mtID, name)) + }, + ) + return +} + +// GetByID returns a calendar entry for a certain calendar id +func GetByID(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id string) (info CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&info, `CALL Calendar_GetByID(?)`, id)) + }, + ) + return +} + +// List returns a list of all calendar entries for a user +func List(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (infos []CalendarInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Select(&infos, `CALL Calendar_List(?)`, mtID)) + }, + ) + return +} diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go new file mode 100644 index 00000000..141eae88 --- /dev/null +++ b/internal/endpoints/notification/calendar/calendar.go @@ -0,0 +1,298 @@ +package calendar + +import ( + "fmt" + "net/http" + "strings" + "time" + + ics "github.com/arran4/golang-ical" + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/utils/utils" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/tree" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar/pkg" + "github.com/oidc-mytoken/server/internal/model" + eventService "github.com/oidc-mytoken/server/internal/mytoken/event" + eventpkg "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/errorfmt" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +//TODO events from eventservice +//TODO rotation + +//TODO not found errors + +func HandleGetICS(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get ics calendar request") + cid := ctx.Params("id") + info, err := calendarrepo.GetByID(rlog, nil, cid) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + ctx.Set(fiber.HeaderContentType, "text/calendar") + ctx.Set(fiber.HeaderContentDisposition, fmt.Sprintf(`attachment; filename="%s"`, info.Name)) + return ctx.SendString(info.ICS) +} + +func HandleAdd(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle add calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + var calendarInfo api.NotificationCalendar + if err := errors.WithStack(ctx.BodyParser(&calendarInfo)); err != nil { + return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + } + + id := utils.RandASCIIString(32) + cal := ics.NewCalendar() + cal.SetMethod(ics.MethodPublish) + cal.SetName(calendarInfo.Name) + cal.SetDescription( + fmt.Sprintf( + "This calendar contains events and reminders for expiring mytokens issued from '%s'", + config.Get().IssuerURL, + ), + ) + icsPath := utils.CombineURLPath(routes.CalendarDownloadEndpoint, id) + cal.SetUrl(icsPath) + dbInfo := calendarrepo.CalendarInfo{ + ID: id, + Name: calendarInfo.Name, + ICSPath: icsPath, + ICS: cal.Serialize(), + } + if err := calendarrepo.Insert(rlog, nil, mt.ID, dbInfo); err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return model.Response{ + Status: http.StatusCreated, + Response: dbInfo, + }.Send(ctx) +} + +func HandleDelete(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + name := ctx.Params("name") + rlog.WithField("calendar", name).Debug("Handle delete calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + if err := calendarrepo.Delete(rlog, nil, mt.ID, name); err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return model.Response{ + Status: http.StatusNoContent, + }.Send(ctx) +} + +func HandleGet(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get calendar request") + calendarName := ctx.Params("name") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + info, err := calendarrepo.Get(rlog, nil, mt.ID, calendarName) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return ctx.Redirect(info.ICSPath) +} + +func HandleList(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle list calendar request") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + infos, err := calendarrepo.List(rlog, nil, mt.ID) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return model.Response{ + Status: http.StatusOK, + Response: infos, + }.Send(ctx) +} + +func HandleAddMytoken(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle add mytoken to calendar request") + + clientMetadata := ctxutils.ClientMetaData(ctx) + calendarName := ctx.Params("name") + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + + var req pkg.AddMytokenToCalendarRequest + fmt.Println(string(ctx.Body())) + if err := errors.WithStack(ctx.BodyParser(&req)); err != nil { + fmt.Println(errorfmt.Full(err)) + return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + } + + id := mt.ID + momMode := req.MomID.Hash() != id.Hash() + if momMode { + id = req.MomID.MTID + if errRes = auth.RequireMytokenIsParentOrCapability( + rlog, nil, api.CapabilityTokeninfoNotify, + api.CapabilityNotifyAnyToken, mt, id, + ); errRes != nil { + return errRes.Send(ctx) + } + if errRes = auth.RequireMytokensForSameUser(rlog, nil, id, mt.ID); errRes != nil { + return errRes.Send(ctx) + } + } + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata.IP) + if errRes != nil { + return errRes.Send(ctx) + } + + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + info, err := calendarrepo.Get(rlog, tx, id, calendarName) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + cal, err := ics.ParseCalendar(strings.NewReader(info.ICS)) + if err != nil { + err = errors.WithStack(err) + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + event, err := eventForMytoken(rlog, tx, id, req.Comment) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + cal.AddVEvent(event) + info.ICS = cal.Serialize() + if err = calendarrepo.Update(rlog, tx, id, info); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + + res = &model.Response{ + Status: http.StatusOK, + Response: info, + } + + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + mytokenEvent := eventpkg.FromNumber( + eventpkg.NotificationSubscribed, fmt.Sprintf("calendar '%s'", info.Name), + ) + if momMode { + mytokenEvent.Type = eventpkg.NotificationSubscribedOther + } + if err = eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: mytokenEvent, + MTID: mt.ID, + }, *clientMetadata, + ); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + return nil + }, + ) + return res.Send(ctx) +} + +func eventForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, comment string) (*ics.VEvent, error) { + var event *ics.VEvent + if err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + mt, err := tree.SingleTokenEntry(rlog, tx, id) + if err != nil { + return err + } + if mt.ExpiresAt == 0 { + return nil + } + event = ics.NewEvent(id.Hash()) + now := time.Now() + event.SetCreatedTime(now) + event.SetDtStampTime(now) + event.SetModifiedAt(now) + event.SetStartAt(mt.ExpiresAt.Time()) + event.SetEndAt(mt.ExpiresAt.Time()) + title := "Mytoken expires" + if mt.Name.Valid { + title = fmt.Sprintf("Mytoken '%s' expires", mt.Name.String) + } + event.SetSummary(title) + if comment != "" { + event.SetDescription(comment) + } + event.SetURL("https://mytok.eu/actions?action=recreate&code=foobar") //TODO + createAlarms(event, mt, 30, 14, 7, 3, 1, 0) + return nil + }, + ); err != nil { + return nil, err + } + return event, nil +} + +func createAlarms(event *ics.VEvent, info tree.MytokenEntry, triggerDaysBeforeExpiration ...int) { + for _, d := range triggerDaysBeforeExpiration { + if a := createAlarm(d, info); a != nil { + event.Components = append(event.Components, a) + } + } + return +} +func createAlarm(daysBeforeExpiration int, info tree.MytokenEntry) *ics.VAlarm { + now := time.Now() + expiresAt := info.ExpiresAt.Time() + createdAt := info.CreatedAt.Time() + triggerTime := expiresAt.Add(time.Duration(-24*daysBeforeExpiration) * time.Hour) + if triggerTime.Before(now) { + return nil + } + if triggerTime.Before(createdAt.Add(expiresAt.Sub(createdAt) / 2)) { + return nil + } + alarm := &ics.VAlarm{ + ComponentBase: ics.ComponentBase{}, + } + alarm.SetAction(ics.ActionDisplay) + alarm.SetTrigger(fmt.Sprintf("-PT%dD", daysBeforeExpiration)) + return alarm +} diff --git a/internal/endpoints/notification/calendar/pkg/calendarRequest.go b/internal/endpoints/notification/calendar/pkg/calendarRequest.go new file mode 100644 index 00000000..fcd4e722 --- /dev/null +++ b/internal/endpoints/notification/calendar/pkg/calendarRequest.go @@ -0,0 +1,12 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +type AddMytokenToCalendarRequest struct { + api.AddMytokenToCalendarRequest + MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` +} diff --git a/internal/endpoints/tokeninfo/history.go b/internal/endpoints/tokeninfo/history.go index 87b524f7..a105ad58 100644 --- a/internal/endpoints/tokeninfo/history.go +++ b/internal/endpoints/tokeninfo/history.go @@ -93,9 +93,7 @@ func doTokenInfoHistory( func handleTokenInfoHistory( rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, ) model.Response { - usedRestriction, errRes := auth.RequireUsableRestrictionOther( - rlog, nil, mt, clientMetadata.IP, nil, nil, - ) + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata.IP) if errRes != nil { return *errRes } diff --git a/internal/mytoken/event/pkg/event.go b/internal/mytoken/event/pkg/event.go index c466506b..7e8f6510 100644 --- a/internal/mytoken/event/pkg/event.go +++ b/internal/mytoken/event/pkg/event.go @@ -86,6 +86,16 @@ var AllEvents = [...]string{ "ssh_key_added", "revoked_other_token", "tokeninfo_history_other_token", + "expired", + "revoked", + "notification_subscribed", + "notification_listed", + "notification_unsubscribed", + "notification_subscribed_other", + "notification_unsubscribed_other", + "calendar_created", + "calendar_listed", + "calendar_deleted", } // Events for Mytokens @@ -109,5 +119,12 @@ const ( SSHKeyAdded RevokedOtherToken TokenInfoHistoryOtherToken + Expired + Revoked + NotificationSubscribed + NotificationListed + NotificationUnsubscribed + NotificationSubscribedOther + NotificationUnsubscribedOther maxEvent ) diff --git a/internal/server/api.go b/internal/server/api.go index 66bad08f..c9a6787c 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -1,11 +1,14 @@ package server import ( + "fmt" + "github.com/gofiber/fiber/v2" "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/endpoints/guestmode" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" "github.com/oidc-mytoken/server/internal/endpoints/profiles" "github.com/oidc-mytoken/server/internal/endpoints/revocation" "github.com/oidc-mytoken/server/internal/endpoints/settings" @@ -50,6 +53,16 @@ func addAPIvXRoutes(s fiber.Router, version int) { s.Delete(sshGrantPath, ssh.HandleDeleteSSHKey) } addProfileEndpointRoutes(s, apiPaths) + if config.Get().Features.Notifications.AnyEnabled { + if config.Get().Features.Notifications.ICS.Enabled { + fmt.Println(apiPaths.CalendarEndpoint) + s.Get(apiPaths.CalendarEndpoint, calendar.HandleList) + s.Post(apiPaths.CalendarEndpoint, calendar.HandleAdd) + s.Get(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleGet) + s.Post(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleAddMytoken) + s.Delete(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleDelete) + } + } } func addProfileEndpointRoutes(r fiber.Router, apiPaths paths.APIPaths) { diff --git a/internal/server/paths/paths.go b/internal/server/paths/paths.go index c5b69057..ed4921c4 100644 --- a/internal/server/paths/paths.go +++ b/internal/server/paths/paths.go @@ -28,6 +28,8 @@ func defaultAPIPaths(api string) APIPaths { UserSettingEndpoint: utils.CombineURLPath(api, "/settings"), ProfilesEndpoint: utils.CombineURLPath(api, "/pt"), GuestModeOP: utils.CombineURLPath(api, "/guests"), + NotificationEndpoint: utils.CombineURLPath(api, "/notifications"), + CalendarEndpoint: utils.CombineURLPath(api, "/notifications/calendars"), } } @@ -44,6 +46,7 @@ func init() { JWKSEndpoint: "/jwks", ConsentEndpoint: "/c", Privacy: "/privacy", + CalendarEndpoint: "/calendars", }, } } @@ -61,6 +64,7 @@ type GeneralPaths struct { JWKSEndpoint string ConsentEndpoint string Privacy string + CalendarEndpoint string } // APIPaths holds all api route paths @@ -73,6 +77,8 @@ type APIPaths struct { UserSettingEndpoint string ProfilesEndpoint string GuestModeOP string + NotificationEndpoint string + CalendarEndpoint string } // GetCurrentAPIPaths returns the api paths for the most recent major version diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index c44810fe..546dfaf2 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -9,10 +9,12 @@ import ( var RedirectURI string var ConsentEndpoint string +var CalendarDownloadEndpoint string // Init initializes the authcode component func Init() { generalPaths := paths.GetGeneralPaths() RedirectURI = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.OIDCRedirectEndpoint) ConsentEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConsentEndpoint) + CalendarDownloadEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.CalendarEndpoint) } diff --git a/internal/server/server.go b/internal/server/server.go index d36c15c6..12b44249 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -10,6 +10,7 @@ import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/template/mustache/v2" + "github.com/oidc-mytoken/utils/utils" log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/api/v0" @@ -20,6 +21,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/configuration" "github.com/oidc-mytoken/server/internal/endpoints/consent" "github.com/oidc-mytoken/server/internal/endpoints/federation" + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" "github.com/oidc-mytoken/server/internal/endpoints/redirect" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/server/apipath" @@ -105,20 +107,22 @@ func Init() { func addRoutes(s fiber.Router) { addWebRoutes(s) - s.Get(paths.GetGeneralPaths().ConfigurationEndpoint, configuration.HandleConfiguration) + generalPaths := paths.GetGeneralPaths() + s.Get(generalPaths.ConfigurationEndpoint, configuration.HandleConfiguration) s.Get(paths.WellknownOpenIDConfiguration, configuration.HandleConfiguration) if config.Get().Features.Federation.Enabled { - s.Get(paths.GetGeneralPaths().FederationEndpoint, federation.HandleEntityConfiguration) + s.Get(generalPaths.FederationEndpoint, federation.HandleEntityConfiguration) } - s.Get(paths.GetGeneralPaths().JWKSEndpoint, endpoints.HandleJWKS) - s.Get(paths.GetGeneralPaths().OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) + s.Get(generalPaths.JWKSEndpoint, endpoints.HandleJWKS) + s.Get(generalPaths.OIDCRedirectEndpoint, redirect.HandleOIDCRedirect) s.Get("/c/:consent_code", consent.HandleConsent) s.Post("/c/:consent_code", consent.HandleConsentPost) s.Post("/c", consent.HandleCreateConsent) s.Get("/native", handleNativeCallback) s.Get("/native/abort", handleNativeConsentAbortCallback) - s.Get(paths.GetGeneralPaths().Privacy, handlePrivacy) + s.Get(generalPaths.Privacy, handlePrivacy) s.Get("/settings", handleSettings) + s.Get(utils.CombineURLPath(generalPaths.CalendarEndpoint, ":id"), calendar.HandleGetICS) addAPIRoutes(s) } diff --git a/internal/utils/auth/auther.go b/internal/utils/auth/auther.go index 55826a65..b950bbaf 100644 --- a/internal/utils/auth/auther.go +++ b/internal/utils/auth/auther.go @@ -1,6 +1,8 @@ package auth import ( + "fmt" + "github.com/gofiber/fiber/v2" "github.com/jmoiron/sqlx" "github.com/oidc-mytoken/api/v0" @@ -9,6 +11,7 @@ import ( dbhelper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" "github.com/oidc-mytoken/server/internal/model" mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" @@ -173,9 +176,9 @@ func RequireUsableRestrictionAT( // RequireUsableRestrictionOther checks that the mytoken.Mytoken's restrictions allow the non-AT usage func RequireUsableRestrictionOther( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, mt *mytoken.Mytoken, ip string, scopes, auds []string, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mt *mytoken.Mytoken, ip string, ) (*restrictions.Restriction, *model.Response) { - return requireUseableRestriction(rlog, tx, mt, ip, scopes, auds, false) + return requireUseableRestriction(rlog, tx, mt, ip, nil, nil, false) } // CheckCapabilityAndRestriction checks the mytoken.Mytoken's capability and restrictions @@ -188,3 +191,52 @@ func CheckCapabilityAndRestriction( } return RequireUsableRestriction(rlog, tx, mt, ip, scopes, auds, capability) } + +// RequireMytokensForSameUser checks that the two passed mtid.MTID are mytokens for the same user and returns an error +// model.Response if not +func RequireMytokensForSameUser(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id1, id2 mtid.MTID) *model.Response { + same, err := dbhelper.CheckMytokensAreForSameUser(rlog, tx, id1, id2) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if !same { + return &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: "The provided token cannot be used to manage this mom_id", + }, + } + } + rlog.Trace("Checked mytokens are for same user") + return nil +} + +func RequireMytokenIsParentOrCapability( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, capabilityIfParent, + capabilityIfNotParent api.Capability, + mt *mytoken.Mytoken, momID mtid.MTID, +) *model.Response { + isParent, err := dbhelper.MOMIDHasParent(rlog, tx, momID.Hash(), mt.ID) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if isParent && mt.Capabilities.Has(capabilityIfParent) { + rlog.Trace("Checked mytoken is parent or has capability") + return nil + } + if mt.Capabilities.Has(capabilityIfNotParent) { + rlog.Trace("Checked mytoken is parent or has capability") + return nil + } + return &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInsufficientCapabilities, + ErrorDescription: fmt.Sprintf( + "The provided token is neither a parent of the subject token"+ + " nor does it have the '%s' capability", capabilityIfNotParent.Name, + ), + }, + } +} From 8119a64916b5acb7dcbbf59f1646ef619e42ddb8 Mon Sep 17 00:00:00 2001 From: zachmann Date: Tue, 31 Oct 2023 13:22:26 +0100 Subject: [PATCH 007/195] WIP notifications; calendars; action codes; party emails --- cmd/mytoken-server/main.go | 2 + go.mod | 3 + go.sum | 6 + internal/config/config.go | 10 + internal/db/cluster/cluster.go | 4 +- internal/db/dbmigrate/scripts/v0.10.0.pre.sql | 2 +- internal/db/dbrepo/actionrepo/actions.go | 88 +++++ internal/db/dbrepo/userrepo/users.go | 24 ++ .../calendarrepo/calendar.go | 10 + internal/endpoints/actions/actions.go | 100 ++++++ internal/endpoints/actions/pkg/actions.go | 41 +++ .../notification/calendar/calendar.go | 331 ++++++++++++++++-- .../calendar/pkg/calendarRequest.go | 15 + .../endpoints/notification/notifications.go | 13 + .../revocation/revocationEndpoint.go | 143 +++++--- internal/endpoints/settings/settings.go | 16 +- .../token/access/accessTokenEndpoint.go | 2 +- .../token/mytoken/pkg/myTokenResponse.go | 12 + internal/endpoints/tokeninfo/list.go | 4 +- internal/endpoints/tokeninfo/subtokens.go | 4 +- internal/mailing/mailing.go | 126 +++++++ internal/mytoken/mytokenHandler.go | 4 +- internal/server/api.go | 2 + internal/server/paths/paths.go | 2 + internal/server/routes/routes.go | 24 +- internal/server/server.go | 2 + internal/server/ssh/at.go | 2 +- internal/utils/auth/auther.go | 14 +- 28 files changed, 903 insertions(+), 103 deletions(-) create mode 100644 internal/db/dbrepo/actionrepo/actions.go create mode 100644 internal/db/dbrepo/userrepo/users.go create mode 100644 internal/endpoints/actions/actions.go create mode 100644 internal/endpoints/actions/pkg/actions.go create mode 100644 internal/endpoints/notification/notifications.go create mode 100644 internal/mailing/mailing.go diff --git a/cmd/mytoken-server/main.go b/cmd/mytoken-server/main.go index 698fe307..f76a4110 100644 --- a/cmd/mytoken-server/main.go +++ b/cmd/mytoken-server/main.go @@ -15,6 +15,7 @@ import ( configurationEndpoint "github.com/oidc-mytoken/server/internal/endpoints/configuration" "github.com/oidc-mytoken/server/internal/endpoints/settings" "github.com/oidc-mytoken/server/internal/jws" + "github.com/oidc-mytoken/server/internal/mailing" "github.com/oidc-mytoken/server/internal/model/version" "github.com/oidc-mytoken/server/internal/oidc/oidcfed" provider2 "github.com/oidc-mytoken/server/internal/oidc/provider" @@ -42,6 +43,7 @@ func main() { geoip.Init() settings.InitSettings() cookies.Init() + mailing.Init() server.Start() } diff --git a/go.mod b/go.mod index 9eaeeec9..9a227a04 100644 --- a/go.mod +++ b/go.mod @@ -53,6 +53,7 @@ require ( github.com/gofiber/utils v1.1.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.3.1 // indirect + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect github.com/klauspost/compress v1.16.7 // indirect github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect github.com/lestrrat-go/blackmagic v1.0.1 // indirect @@ -72,6 +73,8 @@ require ( golang.org/x/sys v0.12.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect + gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect ) replace github.com/urfave/cli/v2 => github.com/zachmann/cli/v2 v2.3.1-0.20211220102037-d619fd40a704 diff --git a/go.sum b/go.sum index ac352288..8a0fd487 100644 --- a/go.sum +++ b/go.sum @@ -243,6 +243,8 @@ github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -821,12 +823,16 @@ google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/config/config.go b/internal/config/config.go index 30daed96..c1194bcd 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -109,6 +109,15 @@ var defaultConfig = Config{ Enabled: true, Groups: make(map[string]string), }, + Notifications: notificationConf{ + Mail: mailNotificationConf{ + Enabled: false, + MailServer: mailServerConf{ + Port: 587, + }, + }, + ICS: onlyEnable{true}, + }, Federation: federationConf{ Enabled: false, EntityConfigurationLifetime: 7 * 24 * 60 * 60, @@ -267,6 +276,7 @@ type mailNotificationConf struct { type mailServerConf struct { Host string `yaml:"host"` + Port int `yaml:"port"` Username string `yaml:"user"` Password string `yaml:"password"` FromAddress string `yaml:"from_address"` diff --git a/internal/db/cluster/cluster.go b/internal/db/cluster/cluster.go index 5e8cb042..68e66095 100644 --- a/internal/db/cluster/cluster.go +++ b/internal/db/cluster/cluster.go @@ -204,12 +204,12 @@ func (n *node) trans(rlog log.Ext1FieldLogger, fn func(*sqlx.Tx) error) error { } func (c *Cluster) next(rlog log.Ext1FieldLogger) *node { - rlog.Trace("Selecting a node") + // rlog.Trace("Selecting a node") select { case n := <-c.active: if n.active { c.active <- n - rlog.WithField("host", n.host).Trace("Selected active node") + // rlog.WithField("host", n.host).Trace("Selected active node") return n } rlog.WithField("host", n.host).Trace("Found inactive node") diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql index 2b0c8e33..fd6f7779 100644 --- a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -211,7 +211,7 @@ INSERT IGNORE INTO Actions (action) INSERT IGNORE INTO Actions (action) VALUES ('unsubscribe_notification'); INSERT IGNORE INTO Actions (action) - VALUES ('token_recreate_unsubscribe'); + VALUES ('recreate_token'); # Procedures diff --git a/internal/db/dbrepo/actionrepo/actions.go b/internal/db/dbrepo/actionrepo/actions.go new file mode 100644 index 00000000..28a24cde --- /dev/null +++ b/internal/db/dbrepo/actionrepo/actions.go @@ -0,0 +1,88 @@ +package actionrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// VerifyMail verifies a mail address +func VerifyMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) (verified bool, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + result, err := tx.Exec(`CALL ActionCodes_VerifyMail(?)`, code) + if err != nil { + return errors.WithStack(err) + } + rows, err := result.RowsAffected() + if err != nil { + return errors.WithStack(err) + } + verified = rows == 1 + return DeleteCode(rlog, tx, code) + }, + ) + return +} + +// DeleteCode deletes a code +func DeleteCode(rlog log.Ext1FieldLogger, tx *sqlx.Tx, code string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL ActionCodes_Delete(?)`, code) + return errors.WithStack(err) + }, + ) +} + +// AddVerifyEmailCode adds a code for email verification to the database +func AddVerifyEmailCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code string, + expiresIn int, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddVerifyMail(?,?,?)`, mtID, code, expiresIn) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// AddRecreateTokenCode adds a code for token recreation to the database +func AddRecreateTokenCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code string, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddRecreateToken(?,?)`, mtID, code) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} + +// AddRemoveFromCalendarCode adds a code for removing a token from a calendar to the database +func AddRemoveFromCalendarCode( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, code, calendarName string, +) (err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err = tx.Exec(`CALL ActionCodes_AddRemoveFromCalendar(?,?,?)`, mtID, calendarName, code) + if err != nil { + return errors.WithStack(err) + } + return err + }, + ) + return +} diff --git a/internal/db/dbrepo/userrepo/users.go b/internal/db/dbrepo/userrepo/users.go new file mode 100644 index 00000000..80dc96d2 --- /dev/null +++ b/internal/db/dbrepo/userrepo/users.go @@ -0,0 +1,24 @@ +package userrepo + +import ( + "github.com/jmoiron/sqlx" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" +) + +// GetMail returns the mail address and verification status for a user linked to a mytoken +func GetMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, bool, error) { + var data = struct { + Mail string `db:"email"` + MailVerified bool `db:"email_verified"` + }{} + err := db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Get(&data, `CALL Users_GetMail(?)`, mtID)) + }, + ) + return data.Mail, data.MailVerified, err +} diff --git a/internal/db/notificationsrepo/calendarrepo/calendar.go b/internal/db/notificationsrepo/calendarrepo/calendar.go index 0ba1039f..8b197b57 100644 --- a/internal/db/notificationsrepo/calendarrepo/calendar.go +++ b/internal/db/notificationsrepo/calendarrepo/calendar.go @@ -76,3 +76,13 @@ func List(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (infos []Calend ) return } + +// AddMytokenToCalendar associates a mytoken with a calendar in the database; you still have to update the ics +func AddMytokenToCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, calendarID string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Calendar_AddMytoken(?, ?)`, mtID, calendarID) + return errors.WithStack(err) + }, + ) +} diff --git a/internal/endpoints/actions/actions.go b/internal/endpoints/actions/actions.go new file mode 100644 index 00000000..f2f3a8cb --- /dev/null +++ b/internal/endpoints/actions/actions.go @@ -0,0 +1,100 @@ +package actions + +import ( + "net/http" + + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/db/dbrepo/actionrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions/pkg" + "github.com/oidc-mytoken/server/internal/model" + "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/server/routes" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +// HandleActions is the main entry function to handle the different actions of the action endpoint +func HandleActions(ctx *fiber.Ctx) error { + actionInfo := pkg.CtxGetActionInfo(ctx) + rlog := logger.GetRequestLogger(ctx) + switch actionInfo.Action { + case pkg.ActionRecreate: + return handleRecreate(rlog, actionInfo.Code).Send(ctx) + case pkg.ActionUnsubscribe: + return handleUnsubscribe(rlog, actionInfo.Code).Send(ctx) + case pkg.ActionVerifyEmail: + return handleVerifyEmail(rlog, actionInfo.Code).Send(ctx) + case pkg.ActionRemoveFromCalendar: + return handleRemoveFromCalendar(rlog, actionInfo.Code).Send(ctx) + } + return model.Response{ + Status: http.StatusBadRequest, + Response: model.BadRequestError("unknown action"), + }.Send(ctx) +} + +func handleRecreate(rlog log.Ext1FieldLogger, code string) model.Response { + return model.ResponseNYI +} +func handleVerifyEmail(rlog log.Ext1FieldLogger, code string) *model.Response { + verified, err := actionrepo.VerifyMail(rlog, nil, code) + if err != nil { + return model.ErrorToInternalServerErrorResponse(err) + } + if !verified { + return &model.Response{ + Status: http.StatusBadRequest, + Response: model.BadRequestError("code not valid or expired"), + } + } + return &model.Response{ + Status: http.StatusOK, + } +} +func handleUnsubscribe(rlog log.Ext1FieldLogger, code string) model.Response { + return model.ResponseNYI +} +func handleRemoveFromCalendar(rlog log.Ext1FieldLogger, code string) model.Response { + return model.ResponseNYI +} + +// CreateVerifyEmail creates an action url for verifying a mail address +func CreateVerifyEmail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, error) { + code := pkg.ActionInfo{ + Action: pkg.ActionVerifyEmail, + Code: pkg.NewCode(), + } + if err := actionrepo.AddVerifyEmailCode(rlog, tx, mtID, code.Code, pkg.CodeLifetimes[code.Action]); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} + +// CreateRecreateToken creates an action url for recreating a mytoken +func CreateRecreateToken(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, error) { + code := pkg.ActionInfo{ + Action: pkg.ActionRecreate, + Code: pkg.NewCode(), + } + if err := actionrepo.AddRecreateTokenCode(rlog, tx, mtID, code.Code); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} + +// CreateRemoveFromCalendar creates an action url for removing a token from a calendar +func CreateRemoveFromCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, calendarName string) ( + string, + error, +) { + code := pkg.ActionInfo{ + Action: pkg.ActionRemoveFromCalendar, + Code: pkg.NewCode(), + } + if err := actionrepo.AddRemoveFromCalendarCode(rlog, tx, mtID, code.Code, calendarName); err != nil { + return "", err + } + return routes.ActionsURL(code), nil +} diff --git a/internal/endpoints/actions/pkg/actions.go b/internal/endpoints/actions/pkg/actions.go new file mode 100644 index 00000000..9b432d7a --- /dev/null +++ b/internal/endpoints/actions/pkg/actions.go @@ -0,0 +1,41 @@ +package pkg + +import ( + "github.com/gofiber/fiber/v2" + "github.com/oidc-mytoken/utils/utils" +) + +// Actions +const ( + ActionRecreate = "recreate_token" + ActionVerifyEmail = "verify_email" + ActionUnsubscribe = "unsubscribe_notification" + ActionRemoveFromCalendar = "remove_from_calendar" +) + +// CodeLifetimes holds the default lifetime of the different action codes +var CodeLifetimes = map[string]int{ + ActionUnsubscribe: 0, + ActionVerifyEmail: 3600, + ActionRecreate: 0, + ActionRemoveFromCalendar: 0, +} + +// ActionInfo is type for associating an Action with a Code +type ActionInfo struct { + Action string + Code string +} + +// CtxGetActionInfo obtains the ActionInfo from a fiber.Ctx +func CtxGetActionInfo(ctx *fiber.Ctx) ActionInfo { + return ActionInfo{ + Action: ctx.Query("action"), + Code: ctx.Query("code"), + } +} + +// NewCode creates a new code +func NewCode() string { + return utils.RandASCIIString(32) +} diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go index 141eae88..a3ef8479 100644 --- a/internal/endpoints/notification/calendar/calendar.go +++ b/internal/endpoints/notification/calendar/calendar.go @@ -17,25 +17,31 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/tree" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions" "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar/pkg" + pkg2 "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mailing" "github.com/oidc-mytoken/server/internal/model" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" eventpkg "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" "github.com/oidc-mytoken/server/internal/server/routes" "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/ctxutils" "github.com/oidc-mytoken/server/internal/utils/errorfmt" "github.com/oidc-mytoken/server/internal/utils/logger" ) //TODO events from eventservice -//TODO rotation //TODO not found errors +// HandleGetICS returns a calendar ics by its id func HandleGetICS(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle get ics calendar request") @@ -49,6 +55,7 @@ func HandleGetICS(ctx *fiber.Ctx) error { return ctx.SendString(info.ICS) } +// HandleAdd handles a request to create a new calendar func HandleAdd(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle add calendar request") @@ -57,6 +64,13 @@ func HandleAdd(ctx *fiber.Ctx) error { if errRes != nil { return errRes.Send(ctx) } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx).IP, api.CapabilityNotifyAnyToken, + ) + if errRes != nil { + return errRes.Send(ctx) + } var calendarInfo api.NotificationCalendar if err := errors.WithStack(ctx.BodyParser(&calendarInfo)); err != nil { return model.ErrorToBadRequestErrorResponse(err).Send(ctx) @@ -80,15 +94,36 @@ func HandleAdd(ctx *fiber.Ctx) error { ICSPath: icsPath, ICS: cal.Serialize(), } - if err := calendarrepo.Insert(rlog, nil, mt.ID, dbInfo); err != nil { + res := model.Response{ + Status: http.StatusCreated, + Response: pkg.CreateCalendarResponse{CalendarInfo: dbInfo}, + } + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if err := calendarrepo.Insert(rlog, nil, mt.ID, dbInfo); err != nil { + return err + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, umt.JWT, mt, *ctxutils.ClientMetaData(ctx), umt.OriginalTokenType, + ) + if err != nil { + return err + } + if tokenUpdate != nil { + res.Cookies = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} + resData := res.Response.(pkg.CreateCalendarResponse) + resData.TokenUpdate = tokenUpdate + res.Response = resData + } + return usedRestriction.UsedOther(rlog, tx, mt.ID) + }, + ); err != nil { return model.ErrorToInternalServerErrorResponse(err).Send(ctx) } - return model.Response{ - Status: http.StatusCreated, - Response: dbInfo, - }.Send(ctx) + return res.Send(ctx) } +// HandleDelete deletes a calendar func HandleDelete(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) name := ctx.Params("name") @@ -98,14 +133,49 @@ func HandleDelete(ctx *fiber.Ctx) error { if errRes != nil { return errRes.Send(ctx) } - if err := calendarrepo.Delete(rlog, nil, mt.ID, name); err != nil { + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx).IP, api.CapabilityNotifyAnyToken, + ) + if errRes != nil { + return errRes.Send(ctx) + } + + var res *model.Response + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if err := calendarrepo.Delete(rlog, tx, mt.ID, name); err != nil { + return err + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, umt.JWT, mt, *ctxutils.ClientMetaData(ctx), umt.OriginalTokenType, + ) + if err != nil { + return err + } + if tokenUpdate != nil { + res = &model.Response{ + Status: fiber.StatusOK, + Response: pkg2.OnlyTokenUpdateRes{ + TokenUpdate: tokenUpdate, + }, + Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, + } + } + return usedRestriction.UsedOther(rlog, tx, mt.ID) + }, + ); err != nil { return model.ErrorToInternalServerErrorResponse(err).Send(ctx) } + if res != nil { + return res.Send(ctx) + } return model.Response{ Status: http.StatusNoContent, }.Send(ctx) } +// HandleGet looks up the id for a calendar name for the given user (by mytoken) and redirects to the ics endpoint func HandleGet(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle get calendar request") @@ -122,6 +192,7 @@ func HandleGet(ctx *fiber.Ctx) error { return ctx.Redirect(info.ICSPath) } +// HandleList lists all calendars for a user func HandleList(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle list calendar request") @@ -130,16 +201,174 @@ func HandleList(ctx *fiber.Ctx) error { if errRes != nil { return errRes.Send(ctx) } - infos, err := calendarrepo.List(rlog, nil, mt.ID) - if err != nil { - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, + ctxutils.ClientMetaData(ctx).IP, api.CapabilityNotifyAnyTokenRead, + ) + if errRes != nil { + return errRes.Send(ctx) } - return model.Response{ - Status: http.StatusOK, - Response: infos, - }.Send(ctx) + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + infos, err := calendarrepo.List(rlog, tx, mt.ID) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + resData := pkg.CalendarListResponse{Calendars: infos} + res = &model.Response{ + Status: fiber.StatusOK, + Response: resData, + } + + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, umt.JWT, mt, *ctxutils.ClientMetaData(ctx), umt.OriginalTokenType, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if tokenUpdate != nil { + res.Cookies = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} + resData.TokenUpdate = tokenUpdate + res.Response = resData + } + return usedRestriction.UsedOther(rlog, tx, mt.ID) + }, + ) + return res.Send(ctx) +} + +// HandleCalendarEntryViaMail creates a calendar entry for a mytoken and sends it via mail +func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle calendar entry via mail request") + + clientMetadata := ctxutils.ClientMetaData(ctx) + var umt universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &umt, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + + var req pkg.AddMytokenToCalendarRequest + fmt.Println(string(ctx.Body())) + if err := errors.WithStack(ctx.BodyParser(&req)); err != nil { + fmt.Println(errorfmt.Full(err)) + return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + } + + id := mt.ID + momMode := req.MomID.Hash() != id.Hash() + if momMode { + id = req.MomID.MTID + if errRes = auth.RequireMytokenIsParentOrCapability( + rlog, nil, api.CapabilityTokeninfoNotify, + api.CapabilityNotifyAnyToken, mt, id, + ); errRes != nil { + return errRes.Send(ctx) + } + if errRes = auth.RequireMytokensForSameUser(rlog, nil, id, mt.ID); errRes != nil { + return errRes.Send(ctx) + } + } + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata.IP) + if errRes != nil { + return errRes.Send(ctx) + } + + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + mail, mailVerified, err := userrepo.GetMail(rlog, tx, id) + found, err := db.ParseError(err) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if !found { + res = &model.Response{ + Status: http.StatusPreconditionRequired, + Response: api.ErrorMailRequired, + } + return errors.New("dummy") + } + if !mailVerified { + res = &model.Response{ + Status: http.StatusPreconditionRequired, + Response: api.ErrorMailNotVerified, + } + return errors.New("dummy") + } + mtInfo, err := tree.SingleTokenEntry(rlog, tx, id) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + calText, err := mailCalendarForMytoken(rlog, tx, id, mtInfo.Name.String, req.Comment, mail) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + + filename := mtInfo.Name.String + if filename == "" { + filename = id.Hash() + } + err = mailing.ICSMailSender.Send( + mail, + fmt.Sprintf("Mytoken Expiration Calendar Reminder for '%s'", filename), + "You can add the event to your calendar to be notified before the mytoken expires.", + mailing.Attachment{ + Reader: strings.NewReader(calText), + Filename: filename + ".ics", + ContentType: "text/calendar", + }, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + mytokenEvent := eventpkg.FromNumber(eventpkg.NotificationSubscribed, "email calendar entry") + if momMode { + mytokenEvent.Type = eventpkg.NotificationSubscribedOther + } + if err = eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: mytokenEvent, + MTID: mt.ID, + }, *clientMetadata, + ); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + res = &model.Response{ + Status: http.StatusNoContent, + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, umt.JWT, mt, *ctxutils.ClientMetaData(ctx), umt.OriginalTokenType, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if tokenUpdate != nil { + res.Cookies = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} + res.Response = pkg2.OnlyTokenUpdateRes{TokenUpdate: tokenUpdate} + } + return nil + }, + ) + return res.Send(ctx) } +// HandleAddMytoken handles a request to add a mytoken to a calendar func HandleAddMytoken(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) rlog.Debug("Handle add mytoken to calendar request") @@ -186,13 +415,17 @@ func HandleAddMytoken(ctx *fiber.Ctx) error { res = model.ErrorToInternalServerErrorResponse(err) return err } + if err = calendarrepo.AddMytokenToCalendar(rlog, tx, id, info.ID); err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } cal, err := ics.ParseCalendar(strings.NewReader(info.ICS)) if err != nil { err = errors.WithStack(err) res = model.ErrorToInternalServerErrorResponse(err) return err } - event, err := eventForMytoken(rlog, tx, id, req.Comment) + event, err := eventForMytoken(rlog, tx, id, req.Comment, true, calendarName) if err != nil { res = model.ErrorToInternalServerErrorResponse(err) return err @@ -204,11 +437,6 @@ func HandleAddMytoken(ctx *fiber.Ctx) error { return err } - res = &model.Response{ - Status: http.StatusOK, - Response: info, - } - if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { res = model.ErrorToInternalServerErrorResponse(err) return err @@ -228,13 +456,33 @@ func HandleAddMytoken(ctx *fiber.Ctx) error { res = model.ErrorToInternalServerErrorResponse(err) return err } + + res = &model.Response{ + Status: http.StatusOK, + Response: info, + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, umt.JWT, mt, *ctxutils.ClientMetaData(ctx), umt.OriginalTokenType, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if tokenUpdate != nil { + res.Cookies = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} + res.Response = pkg2.OnlyTokenUpdateRes{TokenUpdate: tokenUpdate} + } + return nil }, ) return res.Send(ctx) } -func eventForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, comment string) (*ics.VEvent, error) { +func eventForMytoken( + rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, comment string, + unsubscribeOption bool, calendarName string, +) (*ics.VEvent, error) { var event *ics.VEvent if err := db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { @@ -257,10 +505,30 @@ func eventForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, com title = fmt.Sprintf("Mytoken '%s' expires", mt.Name.String) } event.SetSummary(title) - if comment != "" { - event.SetDescription(comment) + description := comment + if description != "" { + description += "\n\n" + } + recreateURL, err := actions.CreateRecreateToken(rlog, tx, id) + if err != nil { + return err } - event.SetURL("https://mytok.eu/actions?action=recreate&code=foobar") //TODO + description += fmt.Sprintf( + "To re-create a mytoken with similiar properties follow this link:\n"+ + "%s\n", recreateURL, + ) + if unsubscribeOption { + unsubscribeURL, err := actions.CreateRemoveFromCalendar(rlog, tx, id, calendarName) + if err != nil { + return err + } + description += fmt.Sprintf( + "To remove this mytoken from calendar '%s' follow this link:\n"+ + "%s\n", calendarName, unsubscribeURL, + ) + } + event.SetURL(recreateURL) + event.SetDescription(description) createAlarms(event, mt, 30, 14, 7, 3, 1, 0) return nil }, @@ -269,6 +537,21 @@ func eventForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, com } return event, nil } +func mailCalendarForMytoken(rlog logrus.Ext1FieldLogger, tx *sqlx.Tx, id mtid.MTID, name, comment, to string) ( + string, + error, +) { + event, err := eventForMytoken(rlog, tx, id, comment, false, "") + if err != nil { + return "", err + } + event.AddAttendee(to) + cal := ics.NewCalendar() + cal.SetMethod(ics.MethodRequest) + cal.SetName(name) + cal.AddVEvent(event) + return cal.Serialize(), nil +} func createAlarms(event *ics.VEvent, info tree.MytokenEntry, triggerDaysBeforeExpiration ...int) { for _, d := range triggerDaysBeforeExpiration { diff --git a/internal/endpoints/notification/calendar/pkg/calendarRequest.go b/internal/endpoints/notification/calendar/pkg/calendarRequest.go index fcd4e722..41ac1867 100644 --- a/internal/endpoints/notification/calendar/pkg/calendarRequest.go +++ b/internal/endpoints/notification/calendar/pkg/calendarRequest.go @@ -3,10 +3,25 @@ package pkg import ( "github.com/oidc-mytoken/api/v0" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" + "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) +// AddMytokenToCalendarRequest is type holding the request to add a mytoken to a calendar type AddMytokenToCalendarRequest struct { api.AddMytokenToCalendarRequest MomID mtid.MOMID `json:"mom_id" xml:"mom_id" form:"mom_id"` } + +// CreateCalendarResponse is the response returned when a new calendar is created +type CreateCalendarResponse struct { + calendarrepo.CalendarInfo + TokenUpdate *pkg.MytokenResponse `json:"token_update,omitempty"` +} + +// CalendarListResponse is the response returned to list all calendars of a user +type CalendarListResponse struct { + Calendars []calendarrepo.CalendarInfo `json:"calendars"` + TokenUpdate *pkg.MytokenResponse `json:"token_update,omitempty"` +} diff --git a/internal/endpoints/notification/notifications.go b/internal/endpoints/notification/notifications.go new file mode 100644 index 00000000..dd82fd20 --- /dev/null +++ b/internal/endpoints/notification/notifications.go @@ -0,0 +1,13 @@ +package notification + +import ( + "github.com/gofiber/fiber/v2" + + "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" +) + +// HandlePost is the main entry function for handling notification creation requests +func HandlePost(ctx *fiber.Ctx) error { + //TODO switch + return calendar.HandleCalendarEntryViaMail(ctx) +} diff --git a/internal/endpoints/revocation/revocationEndpoint.go b/internal/endpoints/revocation/revocationEndpoint.go index 9bd21e5e..46603ef9 100644 --- a/internal/endpoints/revocation/revocationEndpoint.go +++ b/internal/endpoints/revocation/revocationEndpoint.go @@ -16,12 +16,15 @@ import ( "github.com/oidc-mytoken/server/internal/db" helper "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/mytokenrepohelper" "github.com/oidc-mytoken/server/internal/db/dbrepo/mytokenrepo/transfercoderepo" + "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" "github.com/oidc-mytoken/server/internal/model" "github.com/oidc-mytoken/server/internal/mytoken" eventService "github.com/oidc-mytoken/server/internal/mytoken/event" event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" mytokenPkg "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/ctxutils" "github.com/oidc-mytoken/server/internal/utils/errorfmt" "github.com/oidc-mytoken/server/internal/utils/logger" @@ -52,9 +55,46 @@ func HandleRevoke(ctx *fiber.Ctx) error { } } if req.MOMID != "" { - errRes := revokeByID(rlog, req, ctxutils.ClientMetaData(ctx)) - if errRes != nil { - return errRes.Send(ctx) + var res *model.Response + _ = db.Transact( + rlog, func(tx *sqlx.Tx) error { + metadata := ctxutils.ClientMetaData(ctx) + token, err := universalmytoken.Parse(rlog, req.Token) + if err != nil { + res = model.ErrorToBadRequestErrorResponse(err) + return err + } + authToken, err := mytokenPkg.ParseJWT(token.JWT) + if err != nil { + res = model.ErrorToBadRequestErrorResponse(err) + return err + } + errRes := revokeByID(rlog, tx, req, authToken, metadata) + if errRes != nil { + res = errRes + return errors.New("dummy") + } + tokenUpdate, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, token.JWT, authToken, *metadata, token.OriginalTokenType, + ) + if err != nil { + res = model.ErrorToInternalServerErrorResponse(err) + return err + } + if tokenUpdate != nil { + res = &model.Response{ + Status: fiber.StatusOK, + Response: pkg.OnlyTokenUpdateRes{ + TokenUpdate: tokenUpdate, + }, + Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, + } + } + return nil + }, + ) + if res != nil { + return res.Send(ctx) } return ctx.SendStatus(fiber.StatusNoContent) } @@ -81,61 +121,64 @@ func HandleRevoke(ctx *fiber.Ctx) error { return ctx.SendStatus(fiber.StatusNoContent) } -func revokeByID(rlog log.Ext1FieldLogger, req api.RevocationRequest, clientMetadata *api.ClientMetaData) *model. - Response { - token, err := universalmytoken.Parse(rlog, req.Token) - if err != nil { - return model.ErrorToBadRequestErrorResponse(err) - } - authToken, err := mytokenPkg.ParseJWT(token.JWT) - if err != nil { - return model.ErrorToBadRequestErrorResponse(err) - } - isParent, err := helper.MOMIDHasParent(rlog, nil, req.MOMID, authToken.ID) - if err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - if !isParent && !authToken.Capabilities.Has(api.CapabilityRevokeAnyToken) { - return &model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInsufficientCapabilities, - ErrorDescription: fmt.Sprintf( - "The provided token is neither a parent of the token to be revoked"+ - " nor does it have the '%s' capability", api.CapabilityRevokeAnyToken.Name, - ), - }, - } - } - same, err := helper.CheckMytokensAreForSameUser(rlog, nil, req.MOMID, authToken.ID) - if err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - if !same { - return &model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInvalidGrant, - ErrorDescription: "The provided token cannot be used to revoke this mom_id", - }, - } - } - if err = db.Transact( - rlog, func(tx *sqlx.Tx) error { +func revokeByID( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req api.RevocationRequest, + authToken *mytokenPkg.Mytoken, + clientMetadata *api.ClientMetaData, +) (errRes *model.Response) { + dummy := errors.New("dummy") + _ = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + isParent, err := helper.MOMIDHasParent(rlog, nil, req.MOMID, authToken.ID) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if !isParent && !authToken.Capabilities.Has(api.CapabilityRevokeAnyToken) { + errRes = &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInsufficientCapabilities, + ErrorDescription: fmt.Sprintf( + "The provided token is neither a parent of the token to be revoked"+ + " nor does it have the '%s' capability", api.CapabilityRevokeAnyToken.Name, + ), + }, + } + return dummy + } + same, err := helper.CheckMytokensAreForSameUser(rlog, nil, req.MOMID, authToken.ID) + if err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + if !same { + errRes = &model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: "The provided token cannot be used to revoke this mom_id", + }, + } + return dummy + } if err = helper.RevokeMT(rlog, tx, req.MOMID, req.Recursive); err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) return err } - return eventService.LogEvent( + if err = eventService.LogEvent( rlog, tx, eventService.MTEvent{ Event: event.FromNumber(event.RevokedOtherToken, fmt.Sprintf("mom_id: %s", req.MOMID)), MTID: authToken.ID, }, *clientMetadata, - ) + ); err != nil { + errRes = model.ErrorToInternalServerErrorResponse(err) + return err + } + return nil }, - ); err != nil { - return model.ErrorToInternalServerErrorResponse(err) - } - return nil + ) + return } func revokeAnyToken( diff --git a/internal/endpoints/settings/settings.go b/internal/endpoints/settings/settings.go index c2852c2e..f069a0fd 100644 --- a/internal/endpoints/settings/settings.go +++ b/internal/endpoints/settings/settings.go @@ -61,8 +61,8 @@ func HandleSettingsHelper( if errRes != nil { return errRes.Send(ctx) } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, ctx.IP(), nil, nil, requiredCapability, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, ctx.IP(), requiredCapability, ) if errRes != nil { return errRes.Send(ctx) @@ -110,7 +110,7 @@ func HandleSettingsHelper( var cake []*fiber.Cookie if tokenUpdate != nil { if rsp == nil { - rsp = &onlyTokenUpdateRes{} + rsp = &my.OnlyTokenUpdateRes{} } rsp.SetTokenUpdate(tokenUpdate) cake = []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)} @@ -122,13 +122,3 @@ func HandleSettingsHelper( Cookies: cake, }.Send(ctx) } - -type onlyTokenUpdateRes struct { - api.OnlyTokenUpdateResponse - TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` -} - -// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface -func (res *onlyTokenUpdateRes) SetTokenUpdate(tokenUpdate *my.MytokenResponse) { - res.TokenUpdate = tokenUpdate -} diff --git a/internal/endpoints/token/access/accessTokenEndpoint.go b/internal/endpoints/token/access/accessTokenEndpoint.go index 8290c392..3459ae1e 100644 --- a/internal/endpoints/token/access/accessTokenEndpoint.go +++ b/internal/endpoints/token/access/accessTokenEndpoint.go @@ -49,7 +49,7 @@ func HandleAccessTokenEndpoint(ctx *fiber.Ctx) error { if errRes != nil { return errRes.Send(ctx) } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( + usedRestriction, errRes := auth.RequireCapabilityAndRestriction( rlog, nil, mt, ctx.IP(), utils.SplitIgnoreEmpty(req.Scope, " "), utils.SplitIgnoreEmpty(req.Audience, " "), diff --git a/internal/endpoints/token/mytoken/pkg/myTokenResponse.go b/internal/endpoints/token/mytoken/pkg/myTokenResponse.go index ce40117d..52d16022 100644 --- a/internal/endpoints/token/mytoken/pkg/myTokenResponse.go +++ b/internal/endpoints/token/mytoken/pkg/myTokenResponse.go @@ -14,3 +14,15 @@ type MytokenResponse struct { Restrictions restrictions.Restrictions `json:"restrictions,omitempty"` TokenUpdate *MytokenResponse `json:"token_update,omitempty"` } + +// OnlyTokenUpdateRes is a response that contains only a TokenUpdate and is used when a rotating mytoken was used but +// no response is returned otherwise +type OnlyTokenUpdateRes struct { + api.OnlyTokenUpdateResponse + TokenUpdate *MytokenResponse `json:"token_update,omitempty"` +} + +// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface +func (res *OnlyTokenUpdateRes) SetTokenUpdate(tokenUpdate *MytokenResponse) { + res.TokenUpdate = tokenUpdate +} diff --git a/internal/endpoints/tokeninfo/list.go b/internal/endpoints/tokeninfo/list.go index eef80d24..70769da0 100644 --- a/internal/endpoints/tokeninfo/list.go +++ b/internal/endpoints/tokeninfo/list.go @@ -61,8 +61,8 @@ func HandleTokenInfoList( rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, ) model.Response { // If we call this function it means the token is valid. - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, clientMetadata.IP, nil, nil, api.CapabilityListMT, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, clientMetadata.IP, api.CapabilityListMT, ) if errRes != nil { return *errRes diff --git a/internal/endpoints/tokeninfo/subtokens.go b/internal/endpoints/tokeninfo/subtokens.go index 0348fa6d..98e7a5cf 100644 --- a/internal/endpoints/tokeninfo/subtokens.go +++ b/internal/endpoints/tokeninfo/subtokens.go @@ -60,8 +60,8 @@ func HandleTokenInfoSubtokens( rlog log.Ext1FieldLogger, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, clientMetadata *api.ClientMetaData, ) model.Response { // If we call this function it means the token is valid. - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, clientMetadata.IP, nil, nil, api.CapabilityTokeninfoSubtokens, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, clientMetadata.IP, api.CapabilityTokeninfoSubtokens, ) if errRes != nil { return *errRes diff --git a/internal/mailing/mailing.go b/internal/mailing/mailing.go new file mode 100644 index 00000000..f3b4b74a --- /dev/null +++ b/internal/mailing/mailing.go @@ -0,0 +1,126 @@ +package mailing + +import ( + "fmt" + "io" + "net/smtp" + "time" + + "github.com/jordan-wright/email" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" +) + +var mailPool *email.Pool +var fromAddress string + +// Init initializes the mailing +func Init() { + if !config.Get().Features.Notifications.Mail.Enabled { + HTMLMailSender = noopSender{} + PlainTextMailSender = noopSender{} + return + } + mailServerConfig := + config.Get().Features.Notifications.Mail.MailServer + fromAddress = mailServerConfig.FromAddress + var err error + mailPool, err = email.NewPool( + fmt.Sprintf("%s:%d", mailServerConfig.Host, mailServerConfig.Port), + 4, + smtp.PlainAuth("", mailServerConfig.Username, mailServerConfig.Password, mailServerConfig.Host), + ) + if err != nil { + log.WithError(err).Fatal("could not connect to email server") + } +} + +// SendEMail send the passed email.Email +func SendEMail(mail *email.Email) error { + return errors.WithStack(mailPool.Send(mail, 10*time.Second)) +} + +// Attachment is a type holding information about an email attachment +type Attachment struct { + Reader io.Reader + Filename string + ContentType string +} + +// MailSender is an interface for types that can send mails +type MailSender interface { + Send(to, subject, text string, attachments ...Attachment) error +} + +type plainTextMailSender struct{} +type htmlMailSender struct{} +type icsMailSender struct{} +type noopSender struct{} + +// PlainTextMailSender is a MailSender that sends plain text mails +var PlainTextMailSender MailSender = plainTextMailSender{} + +// HTMLMailSender is a MailSender that sends html mails +var HTMLMailSender MailSender = htmlMailSender{} + +// ICSMailSender is a MailSender that sends calendar invitations +var ICSMailSender MailSender = icsMailSender{} + +// Send implements the MailSender interface +func (s noopSender) Send(_, _, _ string, _ ...Attachment) error { + return nil +} + +// Send implements the MailSender interface +func (s plainTextMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + Text: []byte(text), + } + for _, a := range attachments { + _, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + } + return SendEMail(mail) +} + +// Send implements the MailSender interface +func (s htmlMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + HTML: []byte(text), + } + for _, a := range attachments { + _, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + } + return SendEMail(mail) +} + +// Send implements the MailSender interface +func (s icsMailSender) Send(to, subject, text string, attachments ...Attachment) error { + mail := &email.Email{ + From: fromAddress, + To: []string{to}, + Subject: subject, + Text: []byte(text), + } + for _, a := range attachments { + aa, err := mail.Attach(a.Reader, a.Filename, a.ContentType) + if err != nil { + return errors.WithStack(err) + } + aa.Header.Set("Content-Disposition", "inline") + } + return SendEMail(mail) +} diff --git a/internal/mytoken/mytokenHandler.go b/internal/mytoken/mytokenHandler.go index fbd81070..6e1ef43d 100644 --- a/internal/mytoken/mytokenHandler.go +++ b/internal/mytoken/mytokenHandler.go @@ -124,8 +124,8 @@ func HandleMytokenFromMytokenReqChecks( if errRes != nil { return nil, nil, errRes } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( - rlog, nil, mt, ip, nil, nil, api.CapabilityCreateMT, + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, ip, api.CapabilityCreateMT, ) if errRes != nil { return nil, nil, errRes diff --git a/internal/server/api.go b/internal/server/api.go index c9a6787c..b8982229 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -8,6 +8,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/endpoints/guestmode" + "github.com/oidc-mytoken/server/internal/endpoints/notification" "github.com/oidc-mytoken/server/internal/endpoints/notification/calendar" "github.com/oidc-mytoken/server/internal/endpoints/profiles" "github.com/oidc-mytoken/server/internal/endpoints/revocation" @@ -54,6 +55,7 @@ func addAPIvXRoutes(s fiber.Router, version int) { } addProfileEndpointRoutes(s, apiPaths) if config.Get().Features.Notifications.AnyEnabled { + s.Post(apiPaths.NotificationEndpoint, notification.HandlePost) if config.Get().Features.Notifications.ICS.Enabled { fmt.Println(apiPaths.CalendarEndpoint) s.Get(apiPaths.CalendarEndpoint, calendar.HandleList) diff --git a/internal/server/paths/paths.go b/internal/server/paths/paths.go index ed4921c4..1c4aef46 100644 --- a/internal/server/paths/paths.go +++ b/internal/server/paths/paths.go @@ -47,6 +47,7 @@ func init() { ConsentEndpoint: "/c", Privacy: "/privacy", CalendarEndpoint: "/calendars", + ActionsEndpoint: "/actions", }, } } @@ -65,6 +66,7 @@ type GeneralPaths struct { ConsentEndpoint string Privacy string CalendarEndpoint string + ActionsEndpoint string } // APIPaths holds all api route paths diff --git a/internal/server/routes/routes.go b/internal/server/routes/routes.go index 546dfaf2..72b0bae6 100644 --- a/internal/server/routes/routes.go +++ b/internal/server/routes/routes.go @@ -1,15 +1,23 @@ package routes import ( + "fmt" + "net/url" + "github.com/oidc-mytoken/utils/utils" "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/endpoints/actions/pkg" "github.com/oidc-mytoken/server/internal/server/paths" ) -var RedirectURI string -var ConsentEndpoint string -var CalendarDownloadEndpoint string +// EndpointURIs +var ( + RedirectURI string + ConsentEndpoint string + CalendarDownloadEndpoint string + ActionsEndpoint string +) // Init initializes the authcode component func Init() { @@ -17,4 +25,14 @@ func Init() { RedirectURI = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.OIDCRedirectEndpoint) ConsentEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ConsentEndpoint) CalendarDownloadEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.CalendarEndpoint) + ActionsEndpoint = utils.CombineURLPath(config.Get().IssuerURL, generalPaths.ActionsEndpoint) +} + +// ActionsURL builds an action url from a pkg.ActionInfo +func ActionsURL(actionCode pkg.ActionInfo) string { + params := url.Values{} + params.Set("action", url.QueryEscape(actionCode.Action)) + params.Set("code", url.QueryEscape(actionCode.Code)) + p := params.Encode() + return fmt.Sprintf("%s?%s", ActionsEndpoint, p) } diff --git a/internal/server/server.go b/internal/server/server.go index 12b44249..1cc9b4ac 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -18,6 +18,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db/notificationsrepo" "github.com/oidc-mytoken/server/internal/endpoints" + "github.com/oidc-mytoken/server/internal/endpoints/actions" "github.com/oidc-mytoken/server/internal/endpoints/configuration" "github.com/oidc-mytoken/server/internal/endpoints/consent" "github.com/oidc-mytoken/server/internal/endpoints/federation" @@ -123,6 +124,7 @@ func addRoutes(s fiber.Router) { s.Get(generalPaths.Privacy, handlePrivacy) s.Get("/settings", handleSettings) s.Get(utils.CombineURLPath(generalPaths.CalendarEndpoint, ":id"), calendar.HandleGetICS) + s.Get(generalPaths.ActionsEndpoint, actions.HandleActions) addAPIRoutes(s) } diff --git a/internal/server/ssh/at.go b/internal/server/ssh/at.go index ccbd6636..d4c27a64 100644 --- a/internal/server/ssh/at.go +++ b/internal/server/ssh/at.go @@ -40,7 +40,7 @@ func handleSSHAT(reqData []byte, s ssh.Session) error { if errRes != nil { return writeErrRes(s, errRes) } - usedRestriction, errRes := auth.CheckCapabilityAndRestriction( + usedRestriction, errRes := auth.RequireCapabilityAndRestriction( rlog, nil, mt, clientMetaData.IP, utils.SplitIgnoreEmpty(req.Scope, " "), utils.SplitIgnoreEmpty(req.Audience, " "), diff --git a/internal/utils/auth/auther.go b/internal/utils/auth/auther.go index b950bbaf..f875d9cf 100644 --- a/internal/utils/auth/auther.go +++ b/internal/utils/auth/auther.go @@ -181,8 +181,8 @@ func RequireUsableRestrictionOther( return requireUseableRestriction(rlog, tx, mt, ip, nil, nil, false) } -// CheckCapabilityAndRestriction checks the mytoken.Mytoken's capability and restrictions -func CheckCapabilityAndRestriction( +// RequireCapabilityAndRestriction checks the mytoken.Mytoken's capability and restrictions +func RequireCapabilityAndRestriction( rlog log.Ext1FieldLogger, tx *sqlx.Tx, mt *mytoken.Mytoken, ip string, scopes, auds []string, capability api.Capability, ) (*restrictions.Restriction, *model.Response) { @@ -192,6 +192,16 @@ func CheckCapabilityAndRestriction( return RequireUsableRestriction(rlog, tx, mt, ip, scopes, auds, capability) } +// RequireCapabilityAndRestrictionOther checks the mytoken.Mytoken's capability and restrictions +func RequireCapabilityAndRestrictionOther( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mt *mytoken.Mytoken, ip string, capability api.Capability, +) (*restrictions.Restriction, *model.Response) { + if errRes := RequireCapability(rlog, capability, mt); errRes != nil { + return nil, errRes + } + return RequireUsableRestrictionOther(rlog, tx, mt, ip) +} + // RequireMytokensForSameUser checks that the two passed mtid.MTID are mytokens for the same user and returns an error // model.Response if not func RequireMytokensForSameUser(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id1, id2 mtid.MTID) *model.Response { From da76b76bc7eeaf437da78e2d45a731a4abdb1822 Mon Sep 17 00:00:00 2001 From: zachmann Date: Mon, 13 Nov 2023 10:55:15 +0100 Subject: [PATCH 008/195] calendars not found errors + log events --- internal/db/db.go | 13 +++-- .../notification/calendar/calendar.go | 57 +++++++++++++++---- internal/model/response.go | 11 ++++ internal/mytoken/event/pkg/event.go | 3 + 4 files changed, 66 insertions(+), 18 deletions(-) diff --git a/internal/db/db.go b/internal/db/db.go index 7dc34d2d..8cc412bc 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -131,12 +131,13 @@ func RunWithinTransaction(rlog log.Ext1FieldLogger, tx *sqlx.Tx, fn func(*sqlx.T } // ParseError parses the passed error for a sql.ErrNoRows -func ParseError(err error) (bool, error) { - if err == nil { - return true, nil +func ParseError(e error) (found bool, err error) { + if e == nil { + found = true + return } - if errors.Is(err, sql.ErrNoRows) { - err = nil + if !errors.Is(err, sql.ErrNoRows) { + err = e } - return false, err + return } diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go index a3ef8479..e0d03777 100644 --- a/internal/endpoints/notification/calendar/calendar.go +++ b/internal/endpoints/notification/calendar/calendar.go @@ -37,10 +37,6 @@ import ( "github.com/oidc-mytoken/server/internal/utils/logger" ) -//TODO events from eventservice - -//TODO not found errors - // HandleGetICS returns a calendar ics by its id func HandleGetICS(ctx *fiber.Ctx) error { rlog := logger.GetRequestLogger(ctx) @@ -48,7 +44,11 @@ func HandleGetICS(ctx *fiber.Ctx) error { cid := ctx.Params("id") info, err := calendarrepo.GetByID(rlog, nil, cid) if err != nil { - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + _, e := db.ParseError(err) + if e != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return model.NotFoundErrorResponse("calendar not found").Send(ctx) } ctx.Set(fiber.HeaderContentType, "text/calendar") ctx.Set(fiber.HeaderContentDisposition, fmt.Sprintf(`attachment; filename="%s"`, info.Name)) @@ -115,7 +115,16 @@ func HandleAdd(ctx *fiber.Ctx) error { resData.TokenUpdate = tokenUpdate res.Response = resData } - return usedRestriction.UsedOther(rlog, tx, mt.ID) + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + return eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: eventpkg.FromNumber(eventpkg.CalendarCreated, calendarInfo.Name), + MTID: mt.ID, + }, + *ctxutils.ClientMetaData(ctx), + ) }, ); err != nil { return model.ErrorToInternalServerErrorResponse(err).Send(ctx) @@ -162,7 +171,16 @@ func HandleDelete(ctx *fiber.Ctx) error { Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, } } - return usedRestriction.UsedOther(rlog, tx, mt.ID) + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + return eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: eventpkg.FromNumber(eventpkg.CalendarDeleted, name), + MTID: mt.ID, + }, + *ctxutils.ClientMetaData(ctx), + ) }, ); err != nil { return model.ErrorToInternalServerErrorResponse(err).Send(ctx) @@ -187,7 +205,11 @@ func HandleGet(ctx *fiber.Ctx) error { } info, err := calendarrepo.Get(rlog, nil, mt.ID, calendarName) if err != nil { - return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + _, e := db.ParseError(err) + if e != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + return model.NotFoundErrorResponse("calendar not found").Send(ctx) } return ctx.Redirect(info.ICSPath) } @@ -234,7 +256,16 @@ func HandleList(ctx *fiber.Ctx) error { resData.TokenUpdate = tokenUpdate res.Response = resData } - return usedRestriction.UsedOther(rlog, tx, mt.ID) + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + return eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: eventpkg.FromNumber(eventpkg.CalendarListed, ""), + MTID: mt.ID, + }, + *ctxutils.ClientMetaData(ctx), + ) }, ) return res.Send(ctx) @@ -253,9 +284,7 @@ func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { } var req pkg.AddMytokenToCalendarRequest - fmt.Println(string(ctx.Body())) if err := errors.WithStack(ctx.BodyParser(&req)); err != nil { - fmt.Println(errorfmt.Full(err)) return model.ErrorToBadRequestErrorResponse(err).Send(ctx) } @@ -412,7 +441,11 @@ func HandleAddMytoken(ctx *fiber.Ctx) error { rlog, func(tx *sqlx.Tx) error { info, err := calendarrepo.Get(rlog, tx, id, calendarName) if err != nil { - res = model.ErrorToInternalServerErrorResponse(err) + _, e := db.ParseError(err) + if e != nil { + res = model.ErrorToInternalServerErrorResponse(err) + } + res = model.NotFoundErrorResponse("calendar not found") return err } if err = calendarrepo.AddMytokenToCalendar(rlog, tx, id, info.ID); err != nil { diff --git a/internal/model/response.go b/internal/model/response.go index 06898711..a98ac49a 100644 --- a/internal/model/response.go +++ b/internal/model/response.go @@ -45,6 +45,17 @@ func ErrorToBadRequestErrorResponse(err error) *Response { } } +// NotFoundErrorResponse returns a error response for a not found error +func NotFoundErrorResponse(msg string) *Response { + return &Response{ + Status: fiber.StatusNotFound, + Response: api.Error{ + Error: "not_found", + ErrorDescription: msg, + }, + } +} + // ResponseNYI is the server response when something is not yet implemented var ResponseNYI = Response{ Status: fiber.StatusNotImplemented, diff --git a/internal/mytoken/event/pkg/event.go b/internal/mytoken/event/pkg/event.go index 7e8f6510..8591215b 100644 --- a/internal/mytoken/event/pkg/event.go +++ b/internal/mytoken/event/pkg/event.go @@ -126,5 +126,8 @@ const ( NotificationUnsubscribed NotificationSubscribedOther NotificationUnsubscribedOther + CalendarCreated + CalendarListed + CalendarDeleted maxEvent ) From 574d84d36a7c70b9b7126e225226e9c59bce47f6 Mon Sep 17 00:00:00 2001 From: zachmann Date: Wed, 15 Nov 2023 14:45:44 +0100 Subject: [PATCH 009/195] [webinterface] fix ssh settings mt web things --- internal/server/web/partials/scripts.mustache | 5 ++++- internal/server/web/static/js/restr-gui.js | 2 +- internal/server/web/static/js/ssh.js | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/server/web/partials/scripts.mustache b/internal/server/web/partials/scripts.mustache index 2e1ef15f..36d3c9f8 100644 --- a/internal/server/web/partials/scripts.mustache +++ b/internal/server/web/partials/scripts.mustache @@ -11,9 +11,11 @@ {{/logged-in}} {{#logged-in}} - {{/logged-in}} {{#home}} + {{#logged-in}} + + {{/logged-in}} @@ -38,6 +40,7 @@ {{/settings}} {{#settings-ssh}} + diff --git a/internal/server/web/static/js/restr-gui.js b/internal/server/web/static/js/restr-gui.js index 8847b992..6b6b19c3 100644 --- a/internal/server/web/static/js/restr-gui.js +++ b/internal/server/web/static/js/restr-gui.js @@ -108,7 +108,7 @@ function getSupportedScopesFromStorage(iss = "") { if (iss === "") { if (typeof (issuer) !== 'undefined') { iss = issuer; - } else if (typeof ($mtOIDCIss !== 'undefined')) { + } else if (typeof $mtOIDCIss !== 'undefined') { iss = $mtOIDCIss.val(); } else { iss = storageGet("oidc_issuer"); diff --git a/internal/server/web/static/js/ssh.js b/internal/server/web/static/js/ssh.js index 3c3861ef..91d9751c 100644 --- a/internal/server/web/static/js/ssh.js +++ b/internal/server/web/static/js/ssh.js @@ -41,8 +41,8 @@ disableGrantCallbacks['ssh'] = function disableSSHCallback() { function initSSH(...next) { initRestr(); initCapabilities(); - checkCapability("tokeninfo", mtPrefix); - checkCapability("AT", mtPrefix); + checkCapability("tokeninfo"); + checkCapability("AT"); clearSSHKeyTable(); $.ajax({ type: "GET", From b95d925ace91fbc5b0841264027689c2b9adc748 Mon Sep 17 00:00:00 2001 From: zachmann Date: Wed, 15 Nov 2023 14:46:58 +0100 Subject: [PATCH 010/195] add email settings api, add notifications email settings to webinterface --- internal/config/config.go | 5 +- internal/db/dbmigrate/scripts/v0.10.0.pre.sql | 6 + internal/db/dbrepo/userrepo/users.go | 49 +++++- internal/endpoints/consent/pkg/capability.go | 5 + .../notification/calendar/calendar.go | 8 +- internal/endpoints/settings/email/email.go | 159 ++++++++++++++++++ internal/mailing/mailing.go | 35 +++- .../mailing/mailtemplates/mailtemplates.go | 67 ++++++++ .../templates/verify_mail.html.mustache | 8 + .../templates/verify_mail.txt.mustache | 8 + internal/mytoken/event/pkg/event.go | 6 + internal/server/api.go | 5 + internal/server/web/partials/scripts.mustache | 1 + .../settings-notifications-email.mustache | 54 ++++++ .../partials/settings-notifications.mustache | 42 +++++ internal/server/web/sites/settings.mustache | 12 +- .../web/static/js/settings-notifications.js | 103 ++++++++++++ 17 files changed, 557 insertions(+), 16 deletions(-) create mode 100644 internal/endpoints/settings/email/email.go create mode 100644 internal/mailing/mailtemplates/mailtemplates.go create mode 100644 internal/mailing/mailtemplates/templates/verify_mail.html.mustache create mode 100644 internal/mailing/mailtemplates/templates/verify_mail.txt.mustache create mode 100644 internal/server/web/partials/settings-notifications-email.mustache create mode 100644 internal/server/web/partials/settings-notifications.mustache create mode 100644 internal/server/web/static/js/settings-notifications.js diff --git a/internal/config/config.go b/internal/config/config.go index c1194bcd..a59cdfdb 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -270,8 +270,9 @@ func (c *notificationConf) validate() error { } type mailNotificationConf struct { - Enabled bool `yaml:"enabled"` - MailServer mailServerConf `yaml:"mail_server"` + Enabled bool `yaml:"enabled"` + MailServer mailServerConf `yaml:"mail_server"` + OverwriteDir string `yaml:"overwrite_dir"` } type mailServerConf struct { diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql index fd6f7779..bfbcc2f6 100644 --- a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -205,6 +205,12 @@ INSERT IGNORE INTO Events (event) VALUES ('calendar_listed'); INSERT IGNORE INTO Events (event) VALUES ('calendar_deleted'); +INSERT IGNORE INTO Events (event) + VALUES ('email_settings_listed'); +INSERT IGNORE INTO Events (event) + VALUES ('email_changed'); +INSERT IGNORE INTO Events (event) + VALUES ('email_mimetype_changed'); INSERT IGNORE INTO Actions (action) VALUES ('verify_email'); diff --git a/internal/db/dbrepo/userrepo/users.go b/internal/db/dbrepo/userrepo/users.go index 80dc96d2..e4c85df6 100644 --- a/internal/db/dbrepo/userrepo/users.go +++ b/internal/db/dbrepo/userrepo/users.go @@ -6,19 +6,54 @@ import ( log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/mailing" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) +type mailInfo struct { + Mail string `db:"email"` + MailVerified bool `db:"email_verified"` + PreferHTMLMail bool `db:"prefer_html_mail"` +} + // GetMail returns the mail address and verification status for a user linked to a mytoken -func GetMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (string, bool, error) { - var data = struct { - Mail string `db:"email"` - MailVerified bool `db:"email_verified"` - }{} - err := db.RunWithinTransaction( +func GetMail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (data mailInfo, err error) { + err = db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { return errors.WithStack(tx.Get(&data, `CALL Users_GetMail(?)`, mtID)) }, ) - return data.Mail, data.MailVerified, err + return +} + +// GetTemplateMailSender returns a mailing.TemplateMailSender depending on the users preferred mime type +func GetTemplateMailSender(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (mailing.TemplateMailSender, error) { + info, err := GetMail(rlog, tx, mtID) + if err != nil { + return nil, err + } + if info.PreferHTMLMail { + return mailing.HTMLMailSender, nil + } + return mailing.PlainTextMailSender, nil +} + +// ChangeEmail changes the user's email address +func ChangeEmail(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, newMail string) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Users_ChangeMail(?,?)`, mtID, newMail) + return errors.WithStack(err) + }, + ) +} + +// ChangePreferredMailType changes the user's preferred email mimetype +func ChangePreferredMailType(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, preferHTML bool) error { + return db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + _, err := tx.Exec(`CALL Users_ChangePreferredMailType(?,?)`, mtID, preferHTML) + return errors.WithStack(err) + }, + ) } diff --git a/internal/endpoints/consent/pkg/capability.go b/internal/endpoints/consent/pkg/capability.go index a9c652f2..f54b18bc 100644 --- a/internal/endpoints/consent/pkg/capability.go +++ b/internal/endpoints/consent/pkg/capability.go @@ -128,8 +128,12 @@ var normalCapabilities = []string{ api.CapabilityTokeninfoIntrospect.Name, api.CapabilityTokeninfoHistory.Name, api.CapabilityTokeninfoSubtokens.Name, + api.CapabilityTokeninfoNotify.Name, api.CapabilityGrantsRead.Name, api.CapabilitySSHGrantRead.Name, + api.CapabilityEmail.Name, + api.CapabilityEmailRead.Name, + api.CapabilityNotifyAnyTokenRead.Name, } var warningCapabilities = []string{ api.CapabilityListMT.Name, @@ -138,6 +142,7 @@ var warningCapabilities = []string{ api.CapabilityRevokeAnyToken.Name, api.CapabilityHistoryAnyToken.Name, api.CapabilityManageMTs.Name, + api.CapabilityNotifyAnyToken.Name, } var dangerCapabilities = []string{ api.CapabilitySettings.Name, diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go index e0d03777..927faee2 100644 --- a/internal/endpoints/notification/calendar/calendar.go +++ b/internal/endpoints/notification/calendar/calendar.go @@ -310,7 +310,7 @@ func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { var res *model.Response _ = db.Transact( rlog, func(tx *sqlx.Tx) error { - mail, mailVerified, err := userrepo.GetMail(rlog, tx, id) + mailInfo, err := userrepo.GetMail(rlog, tx, id) found, err := db.ParseError(err) if err != nil { res = model.ErrorToInternalServerErrorResponse(err) @@ -323,7 +323,7 @@ func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { } return errors.New("dummy") } - if !mailVerified { + if !mailInfo.MailVerified { res = &model.Response{ Status: http.StatusPreconditionRequired, Response: api.ErrorMailNotVerified, @@ -335,7 +335,7 @@ func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { res = model.ErrorToInternalServerErrorResponse(err) return err } - calText, err := mailCalendarForMytoken(rlog, tx, id, mtInfo.Name.String, req.Comment, mail) + calText, err := mailCalendarForMytoken(rlog, tx, id, mtInfo.Name.String, req.Comment, mailInfo.Mail) if err != nil { res = model.ErrorToInternalServerErrorResponse(err) return err @@ -346,7 +346,7 @@ func HandleCalendarEntryViaMail(ctx *fiber.Ctx) error { filename = id.Hash() } err = mailing.ICSMailSender.Send( - mail, + mailInfo.Mail, fmt.Sprintf("Mytoken Expiration Calendar Reminder for '%s'", filename), "You can add the event to your calendar to be notified before the mytoken expires.", mailing.Attachment{ diff --git a/internal/endpoints/settings/email/email.go b/internal/endpoints/settings/email/email.go new file mode 100644 index 00000000..0dae6ecb --- /dev/null +++ b/internal/endpoints/settings/email/email.go @@ -0,0 +1,159 @@ +package email + +import ( + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/dbrepo/userrepo" + "github.com/oidc-mytoken/server/internal/endpoints/actions" + "github.com/oidc-mytoken/server/internal/endpoints/settings" + my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mailing/mailtemplates" + "github.com/oidc-mytoken/server/internal/model" + eventService "github.com/oidc-mytoken/server/internal/mytoken/event" + event "github.com/oidc-mytoken/server/internal/mytoken/event/pkg" + mytoken "github.com/oidc-mytoken/server/internal/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" + "github.com/oidc-mytoken/server/internal/mytoken/universalmytoken" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/cookies" + "github.com/oidc-mytoken/server/internal/utils/ctxutils" + "github.com/oidc-mytoken/server/internal/utils/logger" +) + +// MailSettingsInfoResponse is a type for the response for listing mail settings +type MailSettingsInfoResponse struct { + api.MailSettingsInfoResponse + TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` +} + +// SetTokenUpdate implements the pkg.TokenUpdatableResponse interface +func (res *MailSettingsInfoResponse) SetTokenUpdate(tokenUpdate *my.MytokenResponse) { + res.TokenUpdate = tokenUpdate +} + +// HandleGet handles GET requests to the email settings endpoint +func HandleGet(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle get email info request") + var reqMytoken universalmytoken.UniversalMytoken + + return settings.HandleSettingsHelper( + ctx, &reqMytoken, api.CapabilityEmailRead, event.FromNumber(event.EmailSettingsListed, ""), fiber.StatusOK, + func(tx *sqlx.Tx, mt *mytoken.Mytoken) (my.TokenUpdatableResponse, *model.Response) { + info, err := userrepo.GetMail(rlog, tx, mt.ID) + if err != nil { + return nil, model.ErrorToInternalServerErrorResponse(err) + } + return &MailSettingsInfoResponse{ + MailSettingsInfoResponse: api.MailSettingsInfoResponse{ + EmailAddress: info.Mail, + EmailVerified: info.MailVerified, + PreferHTMLMail: info.PreferHTMLMail, + }, + }, nil + }, false, + ) +} + +func HandlePut(ctx *fiber.Ctx) error { + rlog := logger.GetRequestLogger(ctx) + rlog.Debug("Handle update email settings request") + var req api.UpdateMailSettingsRequest + if err := ctx.BodyParser(&req); err != nil { + return model.ErrorToBadRequestErrorResponse(err).Send(ctx) + } + if req.PreferHTMLMail == nil && req.EmailAddress == "" { + return model.Response{ + Status: fiber.StatusBadRequest, + Response: model.BadRequestError("no request parameter given"), + }.Send(ctx) + } + var reqMytoken universalmytoken.UniversalMytoken + mt, errRes := auth.RequireValidMytoken(rlog, nil, &reqMytoken, ctx) + if errRes != nil { + return errRes.Send(ctx) + } + usedRestriction, errRes := auth.RequireCapabilityAndRestrictionOther( + rlog, nil, mt, ctx.IP(), api.CapabilityEmail, + ) + if errRes != nil { + return errRes.Send(ctx) + } + var tokenUpdate *my.MytokenResponse + clientMetaData := ctxutils.ClientMetaData(ctx) + if err := db.Transact( + rlog, func(tx *sqlx.Tx) error { + if req.PreferHTMLMail != nil { + if err := userrepo.ChangePreferredMailType(rlog, tx, mt.ID, *req.PreferHTMLMail); err != nil { + return err + } + eventComment := "to plain text" + if *req.PreferHTMLMail { + eventComment = "to html" + } + if err := eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: event.FromNumber(event.EmailMimetypeChanged, eventComment), + MTID: mt.ID, + }, *clientMetaData, + ); err != nil { + return err + } + } + if req.EmailAddress != "" { + if err := userrepo.ChangeEmail(rlog, tx, mt.ID, req.EmailAddress); err != nil { + return err + } + verificationURL, err := actions.CreateVerifyEmail(rlog, tx, mt.ID) + if err != nil { + return err + } + sender, err := userrepo.GetTemplateMailSender(rlog, tx, mt.ID) + if err != nil { + return err + } + if err = sender.SendTemplate( + req.EmailAddress, mailtemplates.SubjectVerifyMail, + mailtemplates.TemplateVerifyMail, map[string]any{ + "issuer": config.Get().IssuerURL, + "link": verificationURL, + }, + ); err != nil { + return err + } + if err = eventService.LogEvent( + rlog, tx, eventService.MTEvent{ + Event: event.FromNumber(event.EmailChanged, req.EmailAddress), + MTID: mt.ID, + }, *clientMetaData, + ); err != nil { + return err + } + + } + if err := usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + + tu, err := rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, reqMytoken.JWT, mt, *clientMetaData, reqMytoken.OriginalTokenType, + ) + tokenUpdate = tu + return err + }, + ); err != nil { + return model.ErrorToInternalServerErrorResponse(err).Send(ctx) + } + if tokenUpdate != nil { + return model.Response{ + Status: fiber.StatusOK, + Response: my.OnlyTokenUpdateRes{TokenUpdate: tokenUpdate}, + Cookies: []*fiber.Cookie{cookies.MytokenCookie(tokenUpdate.Mytoken)}, + }.Send(ctx) + } + return model.Response{Status: fiber.StatusNoContent}.Send(ctx) +} diff --git a/internal/mailing/mailing.go b/internal/mailing/mailing.go index f3b4b74a..29210a32 100644 --- a/internal/mailing/mailing.go +++ b/internal/mailing/mailing.go @@ -11,6 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/mailing/mailtemplates" ) var mailPool *email.Pool @@ -35,6 +36,7 @@ func Init() { if err != nil { log.WithError(err).Fatal("could not connect to email server") } + mailtemplates.Init() } // SendEMail send the passed email.Email @@ -54,16 +56,22 @@ type MailSender interface { Send(to, subject, text string, attachments ...Attachment) error } +// TemplateMailSender is an interface for types that can send template mails +type TemplateMailSender interface { + SendTemplate(to, subject, template string, binding any) error + MailSender +} + type plainTextMailSender struct{} type htmlMailSender struct{} type icsMailSender struct{} type noopSender struct{} // PlainTextMailSender is a MailSender that sends plain text mails -var PlainTextMailSender MailSender = plainTextMailSender{} +var PlainTextMailSender TemplateMailSender = plainTextMailSender{} // HTMLMailSender is a MailSender that sends html mails -var HTMLMailSender MailSender = htmlMailSender{} +var HTMLMailSender TemplateMailSender = htmlMailSender{} // ICSMailSender is a MailSender that sends calendar invitations var ICSMailSender MailSender = icsMailSender{} @@ -73,6 +81,11 @@ func (s noopSender) Send(_, _, _ string, _ ...Attachment) error { return nil } +// SendTemplate implements the TemplateMailSender interface +func (s noopSender) SendTemplate(_, _, _ string, _ any) error { + return nil +} + // Send implements the MailSender interface func (s plainTextMailSender) Send(to, subject, text string, attachments ...Attachment) error { mail := &email.Email{ @@ -90,6 +103,15 @@ func (s plainTextMailSender) Send(to, subject, text string, attachments ...Attac return SendEMail(mail) } +// SendTemplate implements the TemplateMailSender interface +func (s plainTextMailSender) SendTemplate(to, subject, template string, binding any) error { + text, err := mailtemplates.Text(template, binding) + if err != nil { + return err + } + return s.Send(to, subject, text) +} + // Send implements the MailSender interface func (s htmlMailSender) Send(to, subject, text string, attachments ...Attachment) error { mail := &email.Email{ @@ -107,6 +129,15 @@ func (s htmlMailSender) Send(to, subject, text string, attachments ...Attachment return SendEMail(mail) } +// SendTemplate implements the TemplateMailSender interface +func (s htmlMailSender) SendTemplate(to, subject, template string, binding any) error { + text, err := mailtemplates.HTML(template, binding) + if err != nil { + return err + } + return s.Send(to, subject, text) +} + // Send implements the MailSender interface func (s icsMailSender) Send(to, subject, text string, attachments ...Attachment) error { mail := &email.Email{ diff --git a/internal/mailing/mailtemplates/mailtemplates.go b/internal/mailing/mailtemplates/mailtemplates.go new file mode 100644 index 00000000..bf9b2090 --- /dev/null +++ b/internal/mailing/mailtemplates/mailtemplates.go @@ -0,0 +1,67 @@ +package mailtemplates + +import ( + "bytes" + "embed" + "io/fs" + "net/http" + + "github.com/gofiber/template/mustache/v2" + "github.com/pkg/errors" + log "github.com/sirupsen/logrus" + + "github.com/oidc-mytoken/server/internal/config" + "github.com/oidc-mytoken/server/internal/utils/fileio" +) + +// Subjects +const ( + SubjectVerifyMail = "mytoken notifications - Verify email" +) + +// TemplateNames +const ( + TemplateVerifyMail = "verify_mail" +) + +//go:embed templates +var _templates embed.FS +var templates fs.FS + +var engine *mustache.Engine + +func init() { + var err error + templates, err = fs.Sub(_templates, "templates") + if err != nil { + log.WithError(err).Fatal() + } +} + +func Init() { + + overWriteDir := config.Get().Features.Notifications.Mail.OverwriteDir + engine = mustache.NewFileSystem( + fileio.NewLocalAndOtherSearcherFilesystem(overWriteDir, http.FS(templates)), + ".mustache", + ) + if err := engine.Load(); err != nil { + log.WithError(err).Fatal() + } +} + +func render(name, suffix string, bindData any) (string, error) { + var buf bytes.Buffer + if err := engine.Render(&buf, name+suffix, bindData); err != nil { + return "", errors.WithStack(err) + } + return buf.String(), nil +} + +func HTML(name string, bindData any) (string, error) { + return render(name, ".html", bindData) +} + +func Text(name string, bindData any) (string, error) { + return render(name, ".txt", bindData) +} diff --git a/internal/mailing/mailtemplates/templates/verify_mail.html.mustache b/internal/mailing/mailtemplates/templates/verify_mail.html.mustache new file mode 100644 index 00000000..da57c3fa --- /dev/null +++ b/internal/mailing/mailtemplates/templates/verify_mail.html.mustache @@ -0,0 +1,8 @@ +Your email address was entered to be used for mytoken notifications at {{ issuer }}. +You must verify that this email address belongs to you. Please verify by clicking on the following link or copy it to +the address bar of your browser: + +{{ link }} + +Sincerly, +the mytoken mail sender bot. diff --git a/internal/mailing/mailtemplates/templates/verify_mail.txt.mustache b/internal/mailing/mailtemplates/templates/verify_mail.txt.mustache new file mode 100644 index 00000000..543f2394 --- /dev/null +++ b/internal/mailing/mailtemplates/templates/verify_mail.txt.mustache @@ -0,0 +1,8 @@ +Your email address was entered to be used for mytoken notifications at {{ issuer }}. +You must verify that this email address belongs to you. Please verify by clicking on the following link or copy it to +the address bar of your browser: + +{{ link }} + +Sincerly, +the mytoken mail sender bot. \ No newline at end of file diff --git a/internal/mytoken/event/pkg/event.go b/internal/mytoken/event/pkg/event.go index 8591215b..1d652beb 100644 --- a/internal/mytoken/event/pkg/event.go +++ b/internal/mytoken/event/pkg/event.go @@ -96,6 +96,9 @@ var AllEvents = [...]string{ "calendar_created", "calendar_listed", "calendar_deleted", + "email_settings_listed", + "email_changed", + "email_mimetype_changed", } // Events for Mytokens @@ -129,5 +132,8 @@ const ( CalendarCreated CalendarListed CalendarDeleted + EmailSettingsListed + EmailChanged + EmailMimetypeChanged maxEvent ) diff --git a/internal/server/api.go b/internal/server/api.go index b8982229..3f2cda3d 100644 --- a/internal/server/api.go +++ b/internal/server/api.go @@ -13,6 +13,7 @@ import ( "github.com/oidc-mytoken/server/internal/endpoints/profiles" "github.com/oidc-mytoken/server/internal/endpoints/revocation" "github.com/oidc-mytoken/server/internal/endpoints/settings" + "github.com/oidc-mytoken/server/internal/endpoints/settings/email" "github.com/oidc-mytoken/server/internal/endpoints/settings/grants" "github.com/oidc-mytoken/server/internal/endpoints/settings/grants/ssh" "github.com/oidc-mytoken/server/internal/endpoints/token/access" @@ -64,6 +65,10 @@ func addAPIvXRoutes(s fiber.Router, version int) { s.Post(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleAddMytoken) s.Delete(utils.CombineURLPath(apiPaths.CalendarEndpoint, ":name"), calendar.HandleDelete) } + if config.Get().Features.Notifications.Mail.Enabled { + s.Get(utils.CombineURLPath(apiPaths.UserSettingEndpoint, "email"), email.HandleGet) + s.Put(utils.CombineURLPath(apiPaths.UserSettingEndpoint, "email"), email.HandlePut) + } } } diff --git a/internal/server/web/partials/scripts.mustache b/internal/server/web/partials/scripts.mustache index 36d3c9f8..c5968611 100644 --- a/internal/server/web/partials/scripts.mustache +++ b/internal/server/web/partials/scripts.mustache @@ -38,6 +38,7 @@ + {{/settings}} {{#settings-ssh}} diff --git a/internal/server/web/partials/settings-notifications-email.mustache b/internal/server/web/partials/settings-notifications-email.mustache new file mode 100644 index 00000000..68e88a23 --- /dev/null +++ b/internal/server/web/partials/settings-notifications-email.mustache @@ -0,0 +1,54 @@ +
+
+ +
+
+
+ + + + + + + +
+ +
+ + +
+
+
+
+ +
+
+
Preferred Email Mimetype
+
+
+ + +
+ +
+ + +
+
+
+
+
\ No newline at end of file diff --git a/internal/server/web/partials/settings-notifications.mustache b/internal/server/web/partials/settings-notifications.mustache new file mode 100644 index 00000000..98defaf2 --- /dev/null +++ b/internal/server/web/partials/settings-notifications.mustache @@ -0,0 +1,42 @@ +
+ + + + + + + + + + + + + + + + + + + + + + + + +

Email

Manage settings about your notification email address. + +
+
{{> settings-notifications-email }}
+

Calendars

Manage your notification calendars. + +
+
{{> settings-notifications-calendar }}
+
+
diff --git a/internal/server/web/sites/settings.mustache b/internal/server/web/sites/settings.mustache index 5b9e7f95..e9658fd1 100644 --- a/internal/server/web/sites/settings.mustache +++ b/internal/server/web/sites/settings.mustache @@ -7,13 +7,23 @@ Grant Types - + + +
{{> grants}}
+ +
+ {{> settings-notifications}} +
+ From 935ed486aaf7157b1bd0b147a266c7db1fe7afa3 Mon Sep 17 00:00:00 2001 From: zachmann Date: Fri, 22 Dec 2023 11:09:07 +0100 Subject: [PATCH 028/195] [web] add possibility to add email notifications (for MTs) from the webinterface --- .../db/notificationsrepo/notifications.go | 2 +- internal/endpoints/consent/consent.go | 5 +- .../pkg => webentities}/capability.go | 2 +- .../webentities/notificationclass.go | 64 +++++++++++++++++++ .../pkg => webentities}/restriction.go | 2 +- .../{consent/pkg => webentities}/utils.go | 2 +- internal/server/handlers.go | 16 +++-- .../partials/capability-single-part.mustache | 12 ++-- .../notifications-subscribe-modal.mustache | 37 +++++++++-- internal/server/web/static/js/tokeninfo.js | 24 +++++++ internal/utils/templating/keys.go | 6 +- 11 files changed, 149 insertions(+), 23 deletions(-) rename internal/endpoints/{consent/pkg => webentities}/capability.go (99%) create mode 100644 internal/endpoints/webentities/notificationclass.go rename internal/endpoints/{consent/pkg => webentities}/restriction.go (95%) rename internal/endpoints/{consent/pkg => webentities}/utils.go (95%) diff --git a/internal/db/notificationsrepo/notifications.go b/internal/db/notificationsrepo/notifications.go index fb8a736c..3044f044 100644 --- a/internal/db/notificationsrepo/notifications.go +++ b/internal/db/notificationsrepo/notifications.go @@ -110,7 +110,7 @@ func newMTNotification( var nid uint64 if err := errors.WithStack( tx.Get( - &nid, `CALL Notifications_CreateForMT(?,?,?,?)`, mtID, req.IncludeChildren, req.NotificationType, + &nid, `CALL Notifications_CreateForMT(?,?,?,?,?)`, mtID, req.IncludeChildren, req.NotificationType, managementCode, ws, ), ); err != nil { diff --git a/internal/endpoints/consent/consent.go b/internal/endpoints/consent/consent.go index 91d9b358..20048343 100644 --- a/internal/endpoints/consent/consent.go +++ b/internal/endpoints/consent/consent.go @@ -16,6 +16,7 @@ import ( "github.com/oidc-mytoken/server/internal/db" pkg2 "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" + "github.com/oidc-mytoken/server/internal/endpoints/webentities" "github.com/oidc-mytoken/server/internal/model/profiled" "github.com/oidc-mytoken/server/internal/mytoken/restrictions" "github.com/oidc-mytoken/server/internal/oidc/oidcfed" @@ -45,8 +46,8 @@ func handleConsent(ctx *fiber.Ctx, info *pkg2.OIDCFlowRequest, includeConsentCal templating.MustacheKeyEmptyNavbar: true, templating.MustacheKeyRestrictionsGUI: true, templating.MustacheKeyCollapse: templating.Collapsable{All: true}, - templating.MustacheKeyRestrictions: pkg.WebRestrictions{Restrictions: info.Restrictions.Restrictions}, - templating.MustacheKeyCapabilities: pkg.AllWebCapabilities(), + templating.MustacheKeyRestrictions: webentities.WebRestrictions{Restrictions: info.Restrictions.Restrictions}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), templating.MustacheKeyCheckedCapabilities: c.Strings(), templating.MustacheKeyIss: info.Issuer, diff --git a/internal/endpoints/consent/pkg/capability.go b/internal/endpoints/webentities/capability.go similarity index 99% rename from internal/endpoints/consent/pkg/capability.go rename to internal/endpoints/webentities/capability.go index f54b18bc..149356c7 100644 --- a/internal/endpoints/consent/pkg/capability.go +++ b/internal/endpoints/webentities/capability.go @@ -1,4 +1,4 @@ -package pkg +package webentities import ( "strings" diff --git a/internal/endpoints/webentities/notificationclass.go b/internal/endpoints/webentities/notificationclass.go new file mode 100644 index 00000000..ffbd5daf --- /dev/null +++ b/internal/endpoints/webentities/notificationclass.go @@ -0,0 +1,64 @@ +package webentities + +import ( + "strings" + + "github.com/oidc-mytoken/api/v0" +) + +// WebNotificationClass is type for representing api.NoticationClass in the web compatible with WebCapability +type WebNotificationClass struct { + ReadWriteCapability *api.NotificationClass + Children []*WebNotificationClass +} + +// WebNotificationClasses creates a slice of WebNotificationClass from []api.NotificationClass +func WebNotificationClasses(ncs []*api.NotificationClass) (wnc []*WebNotificationClass) { + for _, nc := range ncs { + wnc = append( + wnc, webNotificationClassFromNotificationClass(nc), + ) + } + return +} + +// AllWebNotificationClass returns all WebNotificationClass as a tree +func AllWebNotificationClass() []*WebNotificationClass { + return allWebNotificationClass +} + +var allWebNotificationClass []*WebNotificationClass + +func init() { + if allWebNotificationClass == nil { + allWebNotificationClass = []*WebNotificationClass{} + } + for _, nc := range api.AllNotificationClasses { + if strings.Contains(nc.Name, ":") { + continue + } + allWebNotificationClass = append( + allWebNotificationClass, notificationClassToWebNotificationClass(nc), + ) + } +} + +func notificationClassToWebNotificationClass(nc *api.NotificationClass) *WebNotificationClass { + var childs []*WebNotificationClass + for _, c := range nc.GetChildren() { + childs = append(childs, notificationClassToWebNotificationClass(c)) + } + return &WebNotificationClass{ + ReadWriteCapability: nc, + Children: childs, + } +} + +func webNotificationClassFromNotificationClass(nc *api.NotificationClass) *WebNotificationClass { + for _, wnc := range allWebNotificationClass { + if wnc.ReadWriteCapability.Name == nc.Name { + return wnc + } + } + return nil +} diff --git a/internal/endpoints/consent/pkg/restriction.go b/internal/endpoints/webentities/restriction.go similarity index 95% rename from internal/endpoints/consent/pkg/restriction.go rename to internal/endpoints/webentities/restriction.go index 9be4f546..50082317 100644 --- a/internal/endpoints/consent/pkg/restriction.go +++ b/internal/endpoints/webentities/restriction.go @@ -1,4 +1,4 @@ -package pkg +package webentities import ( "encoding/json" diff --git a/internal/endpoints/consent/pkg/utils.go b/internal/endpoints/webentities/utils.go similarity index 95% rename from internal/endpoints/consent/pkg/utils.go rename to internal/endpoints/webentities/utils.go index 00453450..8126d42e 100644 --- a/internal/endpoints/consent/pkg/utils.go +++ b/internal/endpoints/webentities/utils.go @@ -1,4 +1,4 @@ -package pkg +package webentities // Bootstrap text color classes const ( diff --git a/internal/server/handlers.go b/internal/server/handlers.go index 7af407b9..6e539c0e 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -16,7 +16,7 @@ import ( "github.com/oidc-mytoken/server/internal/config" "github.com/oidc-mytoken/server/internal/db/profilerepo" configurationEndpoint "github.com/oidc-mytoken/server/internal/endpoints/configuration" - consent "github.com/oidc-mytoken/server/internal/endpoints/consent/pkg" + "github.com/oidc-mytoken/server/internal/endpoints/webentities" "github.com/oidc-mytoken/server/internal/utils/cache" "github.com/oidc-mytoken/server/internal/utils/cookies" "github.com/oidc-mytoken/server/internal/utils/templating" @@ -42,7 +42,7 @@ func homeBindingData() map[string]interface{} { templating.MustacheKeyLoggedIn: true, templating.MustacheKeyRestrictionsGUI: true, templating.MustacheKeyHome: true, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), templating.MustacheSubTokeninfo: map[string]interface{}{ templating.MustacheKeyCollapse: templating.Collapsable{ CollapseRestr: true, @@ -55,6 +55,10 @@ func homeBindingData() map[string]interface{} { templating.MustacheKeyCreateWithProfiles: true, templating.MustacheKeyProfiles: profilesBindingData(), }, + templating.MustacheSubNotifications: map[string]interface{}{ + templating.MustacheKeyPrefix: "notifications-", + templating.MustacheKeyNotificationClasses: webentities.AllWebNotificationClass(), + }, "providers": providers, } } @@ -167,8 +171,8 @@ func handleSettings(ctx *fiber.Ctx) error { partialName: "sites/settings-ssh", bindingData: map[string]interface{}{ templating.MustacheKeyRestrictionsGUI: true, - templating.MustacheKeyRestrictions: consent.WebRestrictions{}, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyRestrictions: webentities.WebRestrictions{}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), }, }, } @@ -185,8 +189,8 @@ func handleSettings(ctx *fiber.Ctx) error { templating.MustacheKeySettings: true, templating.MustacheKeySettingsSSH: true, templating.MustacheKeyRestrictionsGUI: true, - templating.MustacheKeyRestrictions: consent.WebRestrictions{}, - templating.MustacheKeyCapabilities: consent.AllWebCapabilities(), + templating.MustacheKeyRestrictions: webentities.WebRestrictions{}, + templating.MustacheKeyCapabilities: webentities.AllWebCapabilities(), } return ctx.Render("sites/settings", binding, templating.LayoutMain) } diff --git a/internal/server/web/partials/capability-single-part.mustache b/internal/server/web/partials/capability-single-part.mustache index 363bef77..9550fc76 100644 --- a/internal/server/web/partials/capability-single-part.mustache +++ b/internal/server/web/partials/capability-single-part.mustache @@ -21,12 +21,14 @@ {{/ReadOnlyCapability}} - - {{#ReadOnlyCapability}} - - {{/ReadOnlyCapability}} + {{#ReadOnlyCapability}} + + {{/ReadOnlyCapability}} + {{/CapabilityLevel}} {{Description}} {{#ReadOnlyCapability}} diff --git a/internal/server/web/partials/notifications-subscribe-modal.mustache b/internal/server/web/partials/notifications-subscribe-modal.mustache index 74aba15c..eb9332c6 100644 --- a/internal/server/web/partials/notifications-subscribe-modal.mustache +++ b/internal/server/web/partials/notifications-subscribe-modal.mustache @@ -1,7 +1,8 @@ +{{#notifications}} {{>revocation-modal}} +{{>history-modal}} +{{> error-message }} {{^logged-in}} {{/restr-gui}} +{{#calendar-view}} + + + + + + +{{/calendar-view}} \ No newline at end of file diff --git a/internal/server/web/partials/settings-notifications-calendar.mustache b/internal/server/web/partials/settings-notifications-calendar.mustache index 4264823b..c71fe4db 100644 --- a/internal/server/web/partials/settings-notifications-calendar.mustache +++ b/internal/server/web/partials/settings-notifications-calendar.mustache @@ -2,15 +2,17 @@ Name + ICS Link + - No calendar created. + No calendar created. - + diff --git a/internal/server/web/partials/style.mustache b/internal/server/web/partials/style.mustache index beaddaf3..62462d0c 100644 --- a/internal/server/web/partials/style.mustache +++ b/internal/server/web/partials/style.mustache @@ -19,3 +19,8 @@ crossorigin="anonymous" referrerpolicy="no-referrer"> {{/restr-gui}} +{{#calendar-view}} + + + +{{/calendar-view}} diff --git a/internal/server/web/sites/calendar.mustache b/internal/server/web/sites/calendar.mustache new file mode 100644 index 00000000..d8574e38 --- /dev/null +++ b/internal/server/web/sites/calendar.mustache @@ -0,0 +1 @@ +
diff --git a/internal/server/web/static/css/calendar.css b/internal/server/web/static/css/calendar.css new file mode 100644 index 00000000..d37e5859 --- /dev/null +++ b/internal/server/web/static/css/calendar.css @@ -0,0 +1,17 @@ + +.fc-list-day { + color: var(--primary); +} + +.fc-event-main-frame { + display: block !important; +} + +.fc-daygrid-event { + white-space: normal; + align-items: normal; +} + +:root { + --fc-list-event-hover-bg-color: var(--secondary); +} \ No newline at end of file diff --git a/internal/server/web/static/js/calendar.js b/internal/server/web/static/js/calendar.js new file mode 100644 index 00000000..0470788c --- /dev/null +++ b/internal/server/web/static/js/calendar.js @@ -0,0 +1,71 @@ +import tippy from '../lib/tippy/dist/tippy.esm.js' + + +$(document).ready(function () { + let url = window.location.href.replace(new RegExp("/view" + "$"), ""); + let calendar = new FullCalendar.Calendar(document.getElementById('calendar'), { + themeSystem: 'bootstrap', + headerToolbar: { + left: 'prev,next today', + center: 'title', + right: 'dayGridMonth,listWeek,listYear' + }, + + // customize the button names, + // otherwise they'd all just say "list" + views: { + listWeek: {buttonText: 'list week'}, + list: {buttonText: 'list year'} + }, + + eventDidMount: function (info) { + let title = info.event.extendedProps.description; + title = title.replaceAll('\n', '
'); + title = title.replace(/(https?\:\/\/)\S+/g, function (matched) { + return `${matched}`; + }); + info.event.setProp('url', ''); + tippy(info.el, { + placement: 'top', + content: title, + arrow: true, + // trigger: 'click focus', + trigger: 'mouseenter', + allowHTML: true, + interactive: true, + theme: 'translucent', + maxWidth: 600, + }); + }, + eventClick: function (info) { + info.jsEvent.preventDefault(); + }, + eventDisplay: 'block', + eventTimeFormat: { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + meridiem: false, + hour12: false, + }, + displayEventEnd: false, + + initialView: 'dayGridMonth', + navLinks: true, // can click day/week names to navigate views + editable: false, + dayMaxEvents: true, // allow "more" link when too many events + nowIndicator: true, + defaultTimedEventDuration: '00:00:01', + events: { + url: url, + format: 'ics' + }, + eventSourceSuccess: function (_, response) { + let h = response.headers.get("content-disposition"); + h = h.split('"')[1] + $('#navbar-center-content').html(`

Calendar: ${h}

`); + } + }); + + calendar.render(); +}); diff --git a/internal/server/web/static/js/lib/fullcalendar-6.1.10/LICENSE.md b/internal/server/web/static/js/lib/fullcalendar-6.1.10/LICENSE.md new file mode 100644 index 00000000..18ac6673 --- /dev/null +++ b/internal/server/web/static/js/lib/fullcalendar-6.1.10/LICENSE.md @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2021 Adam Shaw + +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. diff --git a/internal/server/web/static/js/lib/fullcalendar-6.1.10/README.md b/internal/server/web/static/js/lib/fullcalendar-6.1.10/README.md new file mode 100644 index 00000000..211925ff --- /dev/null +++ b/internal/server/web/static/js/lib/fullcalendar-6.1.10/README.md @@ -0,0 +1,74 @@ +# FullCalendar + +Full-sized drag & drop calendar in JavaScript + +- [Project Website](https://fullcalendar.io/) +- [Documentation](https://fullcalendar.io/docs) +- [Changelog](CHANGELOG.md) +- [Support](https://fullcalendar.io/support) +- [License](LICENSE.md) +- [Roadmap](https://fullcalendar.io/roadmap) + +Connectors: + +- [React](https://github.com/fullcalendar/fullcalendar-react) +- [Angular](https://github.com/fullcalendar/fullcalendar-angular) +- [Vue 3](https://github.com/fullcalendar/fullcalendar-vue) | + [2](https://github.com/fullcalendar/fullcalendar-vue2) + +## Bundle + +The [FullCalendar Standard Bundle](bundle) is easier to install than individual plugins, though filesize will be larger. +It works well with a CDN. + +## Installation + +Install the FullCalendar core package and any plugins you plan to use: + +```sh +npm install @fullcalendar/core @fullcalendar/interaction @fullcalendar/daygrid +``` + +## Usage + +Instantiate a Calendar with plugins and options: + +```js +import { Calendar } from '@fullcalendar/core' +import interactionPlugin from '@fullcalendar/interaction' +import dayGridPlugin from '@fullcalendar/daygrid' + +const calendarEl = document.getElementById('calendar') +const calendar = new Calendar(calendarEl, { + plugins: [ + interactionPlugin, + dayGridPlugin + ], + initialView: 'timeGridWeek', + editable: true, + events: [ + { title: 'Meeting', start: new Date() } + ] +}) + +calendar.render() +``` + +## Development + +You must install this repo with [PNPM](https://pnpm.io/): + +``` +pnpm install +``` + +Available scripts (via `pnpm run + + + +``` + +### 3. Direct Download? + +Managing dependencies by "directly downloading" them and placing them into your +source code is not recommended for a variety of reasons, including missing out +on feat/fix updates easily. Please use a versioning management system like a CDN +or npm/Yarn. + +## Usage + +The most straightforward way to get started is to import Popper from the `unpkg` +CDN, which includes all of its features. You can call the `Popper.createPopper` +constructor to create new popper instances. + +Here is a complete example: + +```html + +Popper example + + + + + + + + +``` + +Visit the [tutorial](https://popper.js.org/docs/v2/tutorial/) for an example of +how to build your own tooltip from scratch using Popper. + +### Module bundlers + +You can import the `createPopper` constructor from the fully-featured file: + +```js +import { createPopper } from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +// Pass the button, the tooltip, and some options, and Popper will do the +// magic positioning for you: +createPopper(button, tooltip, { + placement: 'right', +}); +``` + +All the modifiers listed in the docs menu will be enabled and "just work", so +you don't need to think about setting Popper up. The size of Popper including +all of its features is about 5 kB minzipped, but it may grow a bit in the +future. + +#### Popper Lite (tree-shaking) + +If bundle size is important, you'll want to take advantage of tree-shaking. The +library is built in a modular way to allow to import only the parts you really +need. + +```js +import { createPopperLite as createPopper } from '@popperjs/core'; +``` + +The Lite version includes the most necessary modifiers that will compute the +offsets of the popper, compute and add the positioning styles, and add event +listeners. This is close in bundle size to pure CSS tooltip libraries, and +behaves somewhat similarly. + +However, this does not include the features that makes Popper truly useful. + +The two most useful modifiers not included in Lite are `preventOverflow` and +`flip`: + +```js +import { + createPopperLite as createPopper, + preventOverflow, + flip, +} from '@popperjs/core'; + +const button = document.querySelector('#button'); +const tooltip = document.querySelector('#tooltip'); + +createPopper(button, tooltip, { + modifiers: [preventOverflow, flip], +}); +``` + +As you make more poppers, you may be finding yourself needing other modifiers +provided by the library. + +See [tree-shaking](https://popper.js.org/docs/v2/performance/#tree-shaking) for more +information. + +## Distribution targets + +Popper is distributed in 3 different versions, in 3 different file formats. + +The 3 file formats are: + +- `esm` (works with `import` syntax — **recommended**) +- `umd` (works with ` + +``` + +The core CSS comes bundled with the default unpkg import. + +## Usage + +For detailed usage information, +[visit the docs](https://atomiks.github.io/tippyjs/v6/getting-started/). + +## Component Wrappers + +React: [@tippyjs/react](https://github.com/atomiks/tippyjs-react) + +## License + +MIT diff --git a/internal/server/web/static/lib/tippy/dist/backdrop.css b/internal/server/web/static/lib/tippy/dist/backdrop.css new file mode 100644 index 00000000..36c26c57 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/backdrop.css @@ -0,0 +1 @@ +.tippy-box[data-placement^=top]>.tippy-backdrop{transform-origin:0 25%;border-radius:40% 40% 0 0}.tippy-box[data-placement^=top]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-55%)}.tippy-box[data-placement^=top]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-50%,-45%)}.tippy-box[data-placement^=bottom]>.tippy-backdrop{transform-origin:0 -50%;border-radius:0 0 30% 30%}.tippy-box[data-placement^=bottom]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-45%)}.tippy-box[data-placement^=bottom]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-50%)}.tippy-box[data-placement^=left]>.tippy-backdrop{transform-origin:50% 0;border-radius:50% 0 0 50%}.tippy-box[data-placement^=left]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-50%)}.tippy-box[data-placement^=left]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-75%,-50%)}.tippy-box[data-placement^=right]>.tippy-backdrop{transform-origin:-50% 0;border-radius:0 50% 50% 0}.tippy-box[data-placement^=right]>.tippy-backdrop[data-state=visible]{transform:scale(1) translate(-50%,-50%)}.tippy-box[data-placement^=right]>.tippy-backdrop[data-state=hidden]{transform:scale(.2) translate(-25%,-50%)}.tippy-box[data-animatefill]{background-color:transparent!important}.tippy-backdrop{position:absolute;background-color:#333;border-radius:50%;width:calc(110% + 32px);left:50%;top:50%;z-index:-1;transition:all cubic-bezier(.46,.1,.52,.98);-webkit-backface-visibility:hidden;backface-visibility:hidden}.tippy-backdrop[data-state=hidden]{opacity:0}.tippy-backdrop:after{content:"";float:left;padding-top:100%}.tippy-backdrop+.tippy-content{transition-property:opacity;will-change:opacity}.tippy-backdrop+.tippy-content[data-state=hidden]{opacity:0} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/border.css b/internal/server/web/static/lib/tippy/dist/border.css new file mode 100644 index 00000000..c2b8f805 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/border.css @@ -0,0 +1 @@ +.tippy-box{border:1px transparent}.tippy-box[data-placement^=top]>.tippy-arrow:after{border-top-color:inherit;border-width:8px 8px 0;bottom:-8px;left:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:after{border-bottom-color:inherit;border-width:0 8px 8px;top:-8px;left:0}.tippy-box[data-placement^=left]>.tippy-arrow:after{border-left-color:inherit;border-width:8px 0 8px 8px;right:-8px;top:0}.tippy-box[data-placement^=right]>.tippy-arrow:after{border-width:8px 8px 8px 0;left:-8px;top:0;border-right-color:inherit}.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg:first-child:not(:last-child){top:17px}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg:first-child:not(:last-child){bottom:17px}.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg:first-child:not(:last-child){left:12px}.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg:first-child:not(:last-child){right:12px}.tippy-arrow{border-color:inherit}.tippy-arrow:after{content:"";z-index:-1;position:absolute;border-color:transparent;border-style:solid} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/svg-arrow.css b/internal/server/web/static/lib/tippy/dist/svg-arrow.css new file mode 100644 index 00000000..c2a61ad7 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/svg-arrow.css @@ -0,0 +1 @@ +.tippy-box[data-placement^=top]>.tippy-svg-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-svg-arrow:after,.tippy-box[data-placement^=top]>.tippy-svg-arrow>svg{top:16px;transform:rotate(180deg)}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-svg-arrow>svg{bottom:16px}.tippy-box[data-placement^=left]>.tippy-svg-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-svg-arrow:after,.tippy-box[data-placement^=left]>.tippy-svg-arrow>svg{transform:rotate(90deg);top:calc(50% - 3px);left:11px}.tippy-box[data-placement^=right]>.tippy-svg-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-svg-arrow:after,.tippy-box[data-placement^=right]>.tippy-svg-arrow>svg{transform:rotate(-90deg);top:calc(50% - 3px);right:11px}.tippy-svg-arrow{width:16px;height:16px;fill:#333;text-align:initial}.tippy-svg-arrow,.tippy-svg-arrow>svg{position:absolute} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/tippy.css b/internal/server/web/static/lib/tippy/dist/tippy.css new file mode 100644 index 00000000..e6ae635c --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/tippy.css @@ -0,0 +1 @@ +.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1} \ No newline at end of file diff --git a/internal/server/web/static/lib/tippy/dist/tippy.esm.js b/internal/server/web/static/lib/tippy/dist/tippy.esm.js new file mode 100644 index 00000000..0314c895 --- /dev/null +++ b/internal/server/web/static/lib/tippy/dist/tippy.esm.js @@ -0,0 +1,2544 @@ +/**! + * tippy.js v6.3.7 + * (c) 2017-2021 atomiks + * MIT License + */ +import {createPopper, applyStyles} from '../../../js/lib/popperjs/popper.js'; + +var ROUND_ARROW = ''; +var BOX_CLASS = "tippy-box"; +var CONTENT_CLASS = "tippy-content"; +var BACKDROP_CLASS = "tippy-backdrop"; +var ARROW_CLASS = "tippy-arrow"; +var SVG_ARROW_CLASS = "tippy-svg-arrow"; +var TOUCH_OPTIONS = { + passive: true, + capture: true +}; +var TIPPY_DEFAULT_APPEND_TO = function TIPPY_DEFAULT_APPEND_TO() { + return document.body; +}; + +function hasOwnProperty(obj, key) { + return {}.hasOwnProperty.call(obj, key); +} + +function getValueAtIndexOrReturn(value, index, defaultValue) { + if (Array.isArray(value)) { + var v = value[index]; + return v == null ? Array.isArray(defaultValue) ? defaultValue[index] : defaultValue : v; + } + + return value; +} + +function isType(value, type) { + var str = {}.toString.call(value); + return str.indexOf('[object') === 0 && str.indexOf(type + "]") > -1; +} + +function invokeWithArgsOrReturn(value, args) { + return typeof value === 'function' ? value.apply(void 0, args) : value; +} + +function debounce(fn, ms) { + // Avoid wrapping in `setTimeout` if ms is 0 anyway + if (ms === 0) { + return fn; + } + + var timeout; + return function (arg) { + clearTimeout(timeout); + timeout = setTimeout(function () { + fn(arg); + }, ms); + }; +} + +function removeProperties(obj, keys) { + var clone = Object.assign({}, obj); + keys.forEach(function (key) { + delete clone[key]; + }); + return clone; +} + +function splitBySpaces(value) { + return value.split(/\s+/).filter(Boolean); +} + +function normalizeToArray(value) { + return [].concat(value); +} + +function pushIfUnique(arr, value) { + if (arr.indexOf(value) === -1) { + arr.push(value); + } +} + +function unique(arr) { + return arr.filter(function (item, index) { + return arr.indexOf(item) === index; + }); +} + +function getBasePlacement(placement) { + return placement.split('-')[0]; +} + +function arrayFrom(value) { + return [].slice.call(value); +} + +function removeUndefinedProps(obj) { + return Object.keys(obj).reduce(function (acc, key) { + if (obj[key] !== undefined) { + acc[key] = obj[key]; + } + + return acc; + }, {}); +} + +function div() { + return document.createElement('div'); +} + +function isElement(value) { + return ['Element', 'Fragment'].some(function (type) { + return isType(value, type); + }); +} + +function isNodeList(value) { + return isType(value, 'NodeList'); +} + +function isMouseEvent(value) { + return isType(value, 'MouseEvent'); +} + +function isReferenceElement(value) { + return !!(value && value._tippy && value._tippy.reference === value); +} + +function getArrayOfElements(value) { + if (isElement(value)) { + return [value]; + } + + if (isNodeList(value)) { + return arrayFrom(value); + } + + if (Array.isArray(value)) { + return value; + } + + return arrayFrom(document.querySelectorAll(value)); +} + +function setTransitionDuration(els, value) { + els.forEach(function (el) { + if (el) { + el.style.transitionDuration = value + "ms"; + } + }); +} + +function setVisibilityState(els, state) { + els.forEach(function (el) { + if (el) { + el.setAttribute('data-state', state); + } + }); +} + +function getOwnerDocument(elementOrElements) { + var _element$ownerDocumen; + + var _normalizeToArray = normalizeToArray(elementOrElements), + element = _normalizeToArray[0]; // Elements created via a