diff --git a/server-go/collections/entity.go b/server-go/collections/entity.go index 3d3ea064..5e6e53e9 100644 --- a/server-go/collections/entity.go +++ b/server-go/collections/entity.go @@ -1,8 +1,14 @@ package collections import ( + "encoding/json" + "errors" + "fmt" "github.com/riotkit-org/backup-repository/config" "github.com/riotkit-org/backup-repository/security" + "github.com/riotkit-org/backup-repository/users" + cron "github.com/robfig/cron/v3" + "time" ) type StrategySpec struct { @@ -13,6 +19,58 @@ type StrategySpec struct { type BackupWindow struct { From string `json:"from"` Duration string `json:"duration"` + + parsed cron.Schedule + parsedDuration time.Duration +} + +// UnmarshalJSON performs a validation when decoding a JSON +func (b *BackupWindow) UnmarshalJSON(in []byte) error { + v := struct { + From string `json:"from"` + Duration string `json:"duration"` + }{} + + if unmarshalErr := json.Unmarshal(in, &v); unmarshalErr != nil { + return unmarshalErr + } + + parser := cron.NewParser(cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.DowOptional) + err := errors.New("") + b.parsed, err = parser.Parse(b.From) + + if err != nil { + return errors.New(fmt.Sprintf("cannot parse Backup Window: %v. Error: %v", b.From, err)) + } + + return nil +} + +func (b *BackupWindow) IsInWindowNow(current time.Time) (bool, error) { + nextRun := b.parsed.Next(current) + var startDate time.Time + retries := 0 + + // First calculate startDate run to get "START DATE" and calculate "END DATE" + // because the library does not provide a "Previous" method unfortunately + for true { + retries = retries + 1 + startDate = current.Add(time.Minute * time.Duration(-1)) + + if startDate.Format(time.RFC822) != nextRun.Format(time.RFC822) { + break + } + + // six months + if retries > 60*24*30*6 { + return false, errors.New("cannot find a previous date in the backup window") + } + } + + endDate := startDate.Add(b.parsedDuration) + + // previous run -> previous run + duration + return current.After(startDate) && current.Before(endDate), nil } type BackupWindows []BackupWindow @@ -31,4 +89,19 @@ type Spec struct { type Collection struct { Metadata config.ObjectMetadata `json:"metadata"` + Spec Spec `json:"spec"` +} + +func (c Collection) CanUploadToMe(user *users.User) bool { + if user.Spec.Roles.HasRole(security.RoleBackupUploader) { + return true + } + + for _, permitted := range c.Spec.AccessControl { + if permitted.UserName == user.Metadata.Name && permitted.Roles.HasRole(security.RoleBackupUploader) { + return true + } + } + + return false } diff --git a/server-go/collections/service.go b/server-go/collections/service.go index 14ed45a0..a3a3c07a 100644 --- a/server-go/collections/service.go +++ b/server-go/collections/service.go @@ -1,6 +1,11 @@ package collections -import "github.com/riotkit-org/backup-repository/config" +import ( + "fmt" + "github.com/riotkit-org/backup-repository/config" + "github.com/sirupsen/logrus" + "time" +) type Service struct { repository collectionRepository @@ -10,6 +15,27 @@ func (s *Service) GetCollectionById(id string) (*Collection, error) { return s.repository.getById(id) } +func (s *Service) ValidateIsBackupWindowAllowingToUpload(collection *Collection, contextTime time.Time) bool { + // no defined Backup Windows = no limits, ITS OPTIONAL + if len(collection.Spec.Windows) == 0 { + return true + } + + for _, window := range collection.Spec.Windows { + result, err := window.IsInWindowNow(contextTime) + + if err != nil { + logrus.Error(fmt.Sprintf("Backup Window validation error (collection id=%v): %v", collection.Metadata.Name, err)) + } + + if result { + return true + } + } + + return false +} + func NewService(config config.ConfigurationProvider) Service { return Service{ repository: collectionRepository{ diff --git a/server-go/go.mod b/server-go/go.mod index 5b5abd87..10d97746 100644 --- a/server-go/go.mod +++ b/server-go/go.mod @@ -53,6 +53,7 @@ require ( github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect diff --git a/server-go/http/collection.go b/server-go/http/collection.go index 6f98dbdd..4dafcd5a 100644 --- a/server-go/http/collection.go +++ b/server-go/http/collection.go @@ -4,12 +4,12 @@ import ( "errors" "github.com/gin-gonic/gin" "github.com/riotkit-org/backup-repository/core" + "github.com/riotkit-org/backup-repository/security" + "time" ) func addUploadRoute(r *gin.RouterGroup, ctx *core.ApplicationContainer) { r.POST("/repository/collection/:collectionId/version", func(c *gin.Context) { - // todo: check if collection exists - // todo: check if backup window is OK // todo: check if rotation strategy allows uploading // todo: deactivate token if temporary token is used // todo: handle upload @@ -17,12 +17,29 @@ func addUploadRoute(r *gin.RouterGroup, ctx *core.ApplicationContainer) { // todo: check if there are gpg header and footer // todo: handle upload interruptions + ctxUser, _ := GetContextUser(ctx, c) + + // Check if Colection exists collection, err := ctx.Collections.GetCollectionById(c.Param("collectionId")) if err != nil { NotFoundResponse(c, errors.New("cannot find specified collection")) return } + // Check permissions + if !collection.CanUploadToMe(ctxUser) { + UnauthorizedResponse(c, errors.New("not authorized to upload versions to this collection")) + } + + // Backup Windows support + if !ctx.Collections.ValidateIsBackupWindowAllowingToUpload(collection, time.Now()) && + !ctxUser.Spec.Roles.HasRole(security.RoleUploadsAnytime) { + + UnauthorizedResponse(c, errors.New("backup window does not allow you to send a backup at this time. "+ + "You need a token from a user that has a special permission 'uploadsAnytime'")) + return + } + println(collection) }) } diff --git a/server-go/security/constants.go b/server-go/security/constants.go index 5bbadedb..aec9f920 100644 --- a/server-go/security/constants.go +++ b/server-go/security/constants.go @@ -5,5 +5,8 @@ const ( RoleCollectionManager = "collectionManager" RoleBackupDownloader = "backupDownloader" RoleBackupUploader = "backupUploader" - RoleSysAdmin = "systemAdmin" + + // RoleUploadsAnytime allows uploading versions regardless of Backup Windows + RoleUploadsAnytime = "uploadsAnytime" + RoleSysAdmin = "systemAdmin" )