diff --git a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql index bfbcc2f67bfdd581f13c77273cfeaaacc7d90bb6..2a4077e51592e130bb513a5b894c985a8a6cde1d 100644 --- a/internal/db/dbmigrate/scripts/v0.10.0.pre.sql +++ b/internal/db/dbmigrate/scripts/v0.10.0.pre.sql @@ -278,3 +278,9 @@ END; DELIMITER ; + +# Table Data +INSERT IGNORE INTO Events (event) + VALUES ('tokeninfo_notifications'); +INSERT IGNORE INTO Events (event) + VALUES ('tokeninfo_notifications_other_token'); diff --git a/internal/db/notificationsrepo/calendarrepo/calendar.go b/internal/db/notificationsrepo/calendarrepo/calendar.go index 3d04a2930ffef045a5035fa21d3e65142fbd1229..b4f2cb47fea2eea0a30c454dc5b788131df7e80c 100644 --- a/internal/db/notificationsrepo/calendarrepo/calendar.go +++ b/internal/db/notificationsrepo/calendarrepo/calendar.go @@ -2,6 +2,7 @@ package calendarrepo import ( "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" "github.com/pkg/errors" log "github.com/sirupsen/logrus" @@ -68,7 +69,7 @@ func GetMTsInCalendar(rlog log.Ext1FieldLogger, tx *sqlx.Tx, calendarID string) } // 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) { +func Get(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, 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)) @@ -88,12 +89,40 @@ func GetByID(rlog log.Ext1FieldLogger, tx *sqlx.Tx, id string) (info CalendarInf } // 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) { +func List(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID) (cals []api.NotificationCalendar, err error) { + var infos []CalendarInfo err = db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { return errors.WithStack(tx.Select(&infos, `CALL Calendar_List(?)`, mtID)) }, ) + for _, i := range infos { + cals = append( + cals, api.NotificationCalendar{ + Name: i.Name, + ICSPath: i.ICSPath, + }, + ) + } + return +} + +// ListCalendarsForMT returns a list of calendars where the passed token is used in +func ListCalendarsForMT(rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any) (cals []api.NotificationCalendar, err error) { + var infos []CalendarInfo + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + return errors.WithStack(tx.Select(&infos, `CALL Calendar_ListForMT(?)`, mtID)) + }, + ) + for _, i := range infos { + cals = append( + cals, api.NotificationCalendar{ + Name: i.Name, + ICSPath: i.ICSPath, + }, + ) + } return } diff --git a/internal/db/notificationsrepo/notifications.go b/internal/db/notificationsrepo/notifications.go index 203775d53b3de4ca144a1e2418a80512006d2f3c..39ec79c0b8b667bcbcb0fdeaa84dad5ae9af2d7b 100644 --- a/internal/db/notificationsrepo/notifications.go +++ b/internal/db/notificationsrepo/notifications.go @@ -7,6 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/oidc-mytoken/server/internal/db" + "github.com/oidc-mytoken/server/internal/db/notificationsrepo/calendarrepo" "github.com/oidc-mytoken/server/internal/endpoints/notification/pkg" "github.com/oidc-mytoken/server/internal/mytoken/pkg/mtid" ) @@ -62,7 +63,7 @@ func GetNotificationsForMTAndClass( // GetNotificationsForMT checks for and returns the found notifications for a certain mytoken func GetNotificationsForMT( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, ) (notifications []notificationInfoBaseWithClass, err error) { err = db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { @@ -73,20 +74,39 @@ func GetNotificationsForMT( return } -// GetNotificationsForUser returns all found notifications for a user -func GetNotificationsForUser( - rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, -) (notifications []api.NotificationInfo, err error) { +func GetNotificationsAndCalendarsForMT( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID any, +) (notifications []api.NotificationInfo, calendars []api.NotificationCalendar, err error) { err = db.RunWithinTransaction( rlog, tx, func(tx *sqlx.Tx) error { - var withClass []notificationInfoBaseWithClass - _, err = db.ParseError(tx.Select(&withClass, `CALL Notifications_GetForUser(?)`, mtID)) + calendars, err = calendarrepo.ListCalendarsForMT(rlog, tx, mtID) if err != nil { - return errors.WithStack(err) + return err } - notificationMap := make(map[uint64]api.NotificationInfo) - var ids []uint64 - for _, n := range withClass { + ns, err := GetNotificationsForMT(rlog, tx, mtID) + if err != nil { + return err + } + notifications, err = notificationInfoBaseWithClassToNotificationInfo(rlog, tx, ns) + return err + }, + ) + return +} + +func notificationInfoBaseWithClassToNotificationInfo( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, + in []notificationInfoBaseWithClass, +) ( + out []api. + NotificationInfo, + err error, +) { + notificationMap := make(map[uint64]api.NotificationInfo) + var ids []uint64 + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + for _, n := range in { nie, ok := notificationMap[n.NotificationID] if ok { nie.Classes = append(nie.Classes, api.NewNotificationClass(n.Class)) @@ -108,12 +128,33 @@ func GetNotificationsForUser( } notificationMap[nie.NotificationID] = nie } - for _, id := range ids { - notifications = append(notifications, notificationMap[id]) - } return nil }, ) + if err != nil { + return + } + for _, id := range ids { + out = append(out, notificationMap[id]) + } + return +} + +// GetNotificationsForUser returns all found notifications for a user +func GetNotificationsForUser( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, mtID mtid.MTID, +) (notifications []api.NotificationInfo, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + var withClass []notificationInfoBaseWithClass + _, err = db.ParseError(tx.Select(&withClass, `CALL Notifications_GetForUser(?)`, mtID)) + if err != nil { + return errors.WithStack(err) + } + notifications, err = notificationInfoBaseWithClassToNotificationInfo(rlog, tx, withClass) + return err + }, + ) return } diff --git a/internal/endpoints/notification/calendar/calendar.go b/internal/endpoints/notification/calendar/calendar.go index 321916801295c86755172423fce7b0349ea472e9..805089c8333fd160c1904a2f63fcef427b08b17e 100644 --- a/internal/endpoints/notification/calendar/calendar.go +++ b/internal/endpoints/notification/calendar/calendar.go @@ -123,6 +123,7 @@ func HandleAdd(ctx *fiber.Ctx) error { ) icsPath := utils.CombineURLPath(routes.CalendarDownloadEndpoint, id) cal.SetUrl(icsPath) + calendarInfo.ICSPath = icsPath dbInfo := calendarrepo.CalendarInfo{ ID: id, Name: calendarInfo.Name, @@ -131,7 +132,7 @@ func HandleAdd(ctx *fiber.Ctx) error { } res := model.Response{ Status: http.StatusCreated, - Response: pkg.CreateCalendarResponse{CalendarInfo: dbInfo}, + Response: pkg.CreateCalendarResponse{NotificationCalendar: calendarInfo}, } if err := db.Transact( rlog, func(tx *sqlx.Tx) error { diff --git a/internal/endpoints/notification/calendar/pkg/calendarRequest.go b/internal/endpoints/notification/calendar/pkg/calendarRequest.go index 41ac1867b77009c6d408e4aa631d94957b91c6f0..142196f48e4426cefa6ef0a4458278604973e96b 100644 --- a/internal/endpoints/notification/calendar/pkg/calendarRequest.go +++ b/internal/endpoints/notification/calendar/pkg/calendarRequest.go @@ -3,7 +3,6 @@ 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" ) @@ -16,12 +15,12 @@ type AddMytokenToCalendarRequest struct { // CreateCalendarResponse is the response returned when a new calendar is created type CreateCalendarResponse struct { - calendarrepo.CalendarInfo + api.NotificationCalendar 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"` + Calendars []api.NotificationCalendar `json:"calendars"` + TokenUpdate *pkg.MytokenResponse `json:"token_update,omitempty"` } diff --git a/internal/endpoints/tokeninfo/history.go b/internal/endpoints/tokeninfo/history.go index 2cfbc65bb6a31708fe3c78fe505c93baf61a7a8f..d55fd90f244d8e80a903dcabbdefefa10ec09235 100644 --- a/internal/endpoints/tokeninfo/history.go +++ b/internal/endpoints/tokeninfo/history.go @@ -125,8 +125,8 @@ func HandleTokenInfoHistory( } return handleTokenInfoHistory(rlog, tx, req, mt, clientMetadata) } - if !mt.Capabilities.Has(api.CapabilityHistoryAnyToken) { - for _, momid := range req.MOMIDs { + for _, momid := range req.MOMIDs { + if !mt.Capabilities.Has(api.CapabilityHistoryAnyToken) { if momid == api.MOMIDValueThis || momid == api.MOMIDValueChildren { continue } @@ -150,21 +150,21 @@ func HandleTokenInfoHistory( }, } } + } - same, err := helper.CheckMytokensAreForSameUser(rlog, tx, momid, mt.ID) - if err != nil { - return *model.ErrorToInternalServerErrorResponse(err) - } - if !same { - return model.Response{ - Status: fiber.StatusForbidden, - Response: api.Error{ - Error: api.ErrorStrInvalidGrant, - ErrorDescription: fmt.Sprintf( - "The provided token cannot be used to obtain history for mom_id '%s'", momid, - ), - }, - } + same, err := helper.CheckMytokensAreForSameUser(rlog, tx, momid, mt.ID) + if err != nil { + return *model.ErrorToInternalServerErrorResponse(err) + } + if !same { + return model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: fmt.Sprintf( + "The provided token cannot be used to obtain history for mom_id '%s'", momid, + ), + }, } } } diff --git a/internal/endpoints/tokeninfo/notifications.go b/internal/endpoints/tokeninfo/notifications.go new file mode 100644 index 0000000000000000000000000000000000000000..a0af6f7eef71c9dc23e186fdadb0ffce0eefa036 --- /dev/null +++ b/internal/endpoints/tokeninfo/notifications.go @@ -0,0 +1,169 @@ +package tokeninfo + +import ( + "fmt" + + "github.com/gofiber/fiber/v2" + "github.com/jmoiron/sqlx" + "github.com/oidc-mytoken/api/v0" + log "github.com/sirupsen/logrus" + + "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/notificationsrepo" + "github.com/oidc-mytoken/server/internal/endpoints/tokeninfo/pkg" + "github.com/oidc-mytoken/server/internal/model" + eventService "github.com/oidc-mytoken/server/internal/mytoken/event" + pkg2 "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/restrictions" + "github.com/oidc-mytoken/server/internal/mytoken/rotation" + "github.com/oidc-mytoken/server/internal/utils/auth" + "github.com/oidc-mytoken/server/internal/utils/cookies" + "github.com/oidc-mytoken/server/internal/utils/errorfmt" +) + +func doTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, + usedRestriction *restrictions.Restriction, +) (res pkg.TokeninfoNotificationsResponse, err error) { + err = db.RunWithinTransaction( + rlog, tx, func(tx *sqlx.Tx) error { + if len(req.MOMIDs) > 1 { + res.MomIDMapping = make(map[string]api.NotificationsCombinedResponse) + for _, id := range req.MOMIDs { + if id == api.MOMIDValueThis { + id = mt.ID.String() + } + var data api.NotificationsCombinedResponse + data.Notifications, data.Calendars, err = notificationsrepo.GetNotificationsAndCalendarsForMT( + rlog, tx, id, + ) + if err != nil { + return err + } + res.MomIDMapping[id] = data + } + } else { + var id any + id = mt.ID + if len(req.MOMIDs) > 0 { + id = req.MOMIDs[0] + } + res.Notifications, res.Calendars, err = notificationsrepo.GetNotificationsAndCalendarsForMT( + rlog, tx, id, + ) + if err != nil { + return err + } + } + if usedRestriction == nil { + return nil + } + if err = usedRestriction.UsedOther(rlog, tx, mt.ID); err != nil { + return err + } + res.TokenUpdate, err = rotation.RotateMytokenAfterOtherForResponse( + rlog, tx, req.Mytoken.JWT, mt, *clientMetadata, req.Mytoken.OriginalTokenType, + ) + if err != nil { + return err + } + ev := api.EventTokenInfoNotifications + if len(req.MOMIDs) > 0 { + ev = api.EventTokenInfoNotificationsOtherToken + } + return eventService.LogEvent( + rlog, tx, pkg2.MTEvent{ + Event: ev, + MTID: mt.ID, + ClientMetaData: *clientMetadata, + }, + ) + }, + ) + return +} + +func handleTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) model.Response { + usedRestriction, errRes := auth.RequireUsableRestrictionOther(rlog, nil, mt, clientMetadata) + if errRes != nil { + return *errRes + } + res, err := doTokenInfoNotifications(rlog, tx, req, mt, clientMetadata, usedRestriction) + if err != nil { + rlog.Errorf("%s", errorfmt.Full(err)) + return *model.ErrorToInternalServerErrorResponse(err) + } + rsp := model.Response{ + Status: fiber.StatusOK, + Response: res, + } + if res.TokenUpdate != nil { + rsp.Cookies = []*fiber.Cookie{cookies.MytokenCookie(res.TokenUpdate.Mytoken)} + } + return rsp +} + +// HandleTokenInfoNotifications handles a tokeninfo notifications request +func HandleTokenInfoNotifications( + rlog log.Ext1FieldLogger, tx *sqlx.Tx, req *pkg.TokenInfoRequest, mt *mytoken.Mytoken, + clientMetadata *api.ClientMetaData, +) model.Response { + // If we call this function it means the token is valid. + + rlog.Debug("Handle tokeninfo notifications request") + if len(req.MOMIDs) == 0 { + if errRes := auth.RequireCapability( + rlog, tx, api.CapabilityTokeninfoNotify, mt, clientMetadata, + ); errRes != nil { + return *errRes + } + return handleTokenInfoNotifications(rlog, tx, req, mt, clientMetadata) + } + for _, momid := range req.MOMIDs { + if !mt.Capabilities.Has(api.CapabilityNotifyAnyTokenRead) { + if momid == api.MOMIDValueThis { + continue + } + isParent, err := helper.MOMIDHasParent(rlog, tx, momid, mt.ID) + if err != nil { + return *model.ErrorToInternalServerErrorResponse(err) + } + if !isParent { + return model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInsufficientCapabilities, + ErrorDescription: fmt.Sprintf( + "The provided token is neither a parent of the the token with "+ + " mom_id '%s' nor does it have the '%s' capability", momid, + api.CapabilityNotifyAnyTokenRead.Name, + ), + }, + } + } + } + + same, err := helper.CheckMytokensAreForSameUser(rlog, tx, momid, mt.ID) + if err != nil { + return *model.ErrorToInternalServerErrorResponse(err) + } + if !same { + return model.Response{ + Status: fiber.StatusForbidden, + Response: api.Error{ + Error: api.ErrorStrInvalidGrant, + ErrorDescription: fmt.Sprintf( + "The provided token cannot be used to obtain notifications for mom_id '%s'", momid, + ), + }, + } + } + } + return handleTokenInfoNotifications(rlog, tx, req, mt, clientMetadata) +} diff --git a/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go b/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go new file mode 100644 index 0000000000000000000000000000000000000000..b42ff9645d7e29919f389b774d1994ad96b4538a --- /dev/null +++ b/internal/endpoints/tokeninfo/pkg/tokeninfoNotificationsResponse.go @@ -0,0 +1,14 @@ +package pkg + +import ( + "github.com/oidc-mytoken/api/v0" + + my "github.com/oidc-mytoken/server/internal/endpoints/token/mytoken/pkg" +) + +// TokeninfoNotificationsResponse is a type for responses to tokeninfo notification requests +type TokeninfoNotificationsResponse struct { + // on update check api.TokeninfoNotificationsResponse + api.TokeninfoNotificationsResponse + TokenUpdate *my.MytokenResponse `json:"token_update,omitempty"` +} diff --git a/internal/endpoints/tokeninfo/tokeninfo.go b/internal/endpoints/tokeninfo/tokeninfo.go index 8f9fc637441cf946a69cfc973dba883c543faca0..430802dbf7cb2c92655c86ee84473786a5bb46c4 100644 --- a/internal/endpoints/tokeninfo/tokeninfo.go +++ b/internal/endpoints/tokeninfo/tokeninfo.go @@ -30,6 +30,8 @@ func HandleTokenInfo(ctx *fiber.Ctx) error { switch req.Action { case model.TokeninfoActionIntrospect: return HandleTokenInfoIntrospect(rlog, nil, mt, req.Mytoken.OriginalTokenType, clientMetadata).Send(ctx) + case model.TokeninfoActionNotifications: + return HandleTokenInfoNotifications(rlog, nil, &req, mt, clientMetadata).Send(ctx) case model.TokeninfoActionEventHistory: return HandleTokenInfoHistory(rlog, nil, &req, mt, clientMetadata).Send(ctx) case model.TokeninfoActionSubtokenTree: diff --git a/internal/model/tokeninfoAction.go b/internal/model/tokeninfoAction.go index 922a03ef058e2af683c88d4ea007cf3c8eb69a83..bce2344a1a70e00a2de215bfae80efecbfebda71 100644 --- a/internal/model/tokeninfoAction.go +++ b/internal/model/tokeninfoAction.go @@ -20,6 +20,7 @@ const ( // assert that these are in the same order as api.AllTokeninfoActions TokeninfoActionEventHistory TokeninfoActionSubtokenTree TokeninfoActionListMytokens + TokeninfoActionNotifications maxTokeninfoAction )