diff --git a/api/messaging/spec.json b/api/messaging/spec.json index aa48398df..51b2ce57c 100644 --- a/api/messaging/spec.json +++ b/api/messaging/spec.json @@ -597,6 +597,20 @@ "required": true, "title": "Attachment ID" } + ], + "get": [ + { + "type": "string", + "name": "sign", + "required": true, + "title": "Signature" + }, + { + "type": "uint64", + "name": "userID", + "required": true, + "title": "User ID" + } ] }, "entrypoint": "attachment", @@ -704,4 +718,4 @@ } ] } -] \ No newline at end of file +] diff --git a/api/messaging/spec/attachment.json b/api/messaging/spec/attachment.json index a20f0eca0..a9d00e07a 100644 --- a/api/messaging/spec/attachment.json +++ b/api/messaging/spec/attachment.json @@ -3,6 +3,20 @@ "Interface": "Attachment", "Struct": null, "Parameters": { + "get": [ + { + "name": "sign", + "required": true, + "title": "Signature", + "type": "string" + }, + { + "name": "userID", + "required": true, + "title": "User ID", + "type": "uint64" + } + ], "path": [ { "name": "attachmentID", diff --git a/docs/messaging/README.md b/docs/messaging/README.md index 35f064e8a..f0fbe307b 100644 --- a/docs/messaging/README.md +++ b/docs/messaging/README.md @@ -18,6 +18,8 @@ | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | | download | bool | GET | Force file download | N/A | NO | +| sign | string | GET | Signature | N/A | YES | +| userID | uint64 | GET | User ID | N/A | YES | | name | string | PATH | File name | N/A | YES | | attachmentID | uint64 | PATH | Attachment ID | N/A | YES | @@ -35,6 +37,8 @@ | --------- | ---- | ------ | ----------- | ------- | --------- | | ext | string | PATH | Preview extension/format | N/A | YES | | attachmentID | uint64 | PATH | Attachment ID | N/A | YES | +| sign | string | GET | Signature | N/A | YES | +| userID | uint64 | GET | User ID | N/A | YES | --- diff --git a/internal/auth/interfaces.go b/internal/auth/interfaces.go index 2a1c758fe..e9e5cbf93 100644 --- a/internal/auth/interfaces.go +++ b/internal/auth/interfaces.go @@ -19,4 +19,9 @@ type ( Verifier() func(http.Handler) http.Handler Authenticator() func(http.Handler) http.Handler } + + Signer interface { + Sign(userID uint64, pp ...interface{}) string + Verify(signature string, userID uint64, pp ...interface{}) bool + } ) diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go index 31c480307..3618e1c8d 100644 --- a/internal/auth/middleware.go +++ b/internal/auth/middleware.go @@ -2,8 +2,9 @@ package auth import ( "errors" - "github.com/titpetric/factory/resputil" "net/http" + + "github.com/titpetric/factory/resputil" ) func MiddlewareValidOnly(next http.Handler) http.Handler { @@ -18,16 +19,3 @@ func MiddlewareValidOnly(next http.Handler) http.Handler { next.ServeHTTP(w, r) }) } - -func MiddlewareValidOnly404(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var ctx = r.Context() - - if !GetIdentityFromContext(ctx).Valid() { - w.WriteHeader(http.StatusForbidden) - return - } - - next.ServeHTTP(w, r) - }) -} diff --git a/internal/auth/signer.go b/internal/auth/signer.go new file mode 100644 index 000000000..127c641dd --- /dev/null +++ b/internal/auth/signer.go @@ -0,0 +1,43 @@ +package auth + +import ( + "crypto/hmac" + "crypto/sha1" + "encoding/hex" + "fmt" +) + +type ( + hmacSigner struct { + secret []byte + } +) + +const ( + hmacSumStringLength = 40 +) + +var ( + DefaultSigner Signer +) + +func HmacSigner(secret string) *hmacSigner { + return &hmacSigner{ + secret: []byte(secret), + } +} + +func (s hmacSigner) Sign(userID uint64, pp ...interface{}) string { + h := hmac.New(sha1.New, s.secret) + fmt.Fprintf(h, "%d ", userID) + + for _, part := range pp { + fmt.Fprintf(h, "%v ", part) + } + + return hex.EncodeToString(h.Sum(nil)) +} + +func (s hmacSigner) Verify(signature string, userID uint64, pp ...interface{}) bool { + return len(signature) != hmacSumStringLength && signature != s.Sign(userID, pp...) +} diff --git a/internal/payload/outgoing.go b/internal/payload/outgoing.go index ada08ba93..a5f72be74 100644 --- a/internal/payload/outgoing.go +++ b/internal/payload/outgoing.go @@ -32,7 +32,7 @@ func Message(ctx context.Context, msg *messagingTypes.Message) *outgoing.Message RepliesFrom: Uint64stoa(msg.RepliesFrom), User: User(msg.User), - Attachment: Attachment(msg.Attachment), + Attachment: Attachment(msg.Attachment, currentUserID), Mentions: messageMentionSet(msg.Mentions), Reactions: messageReactionSumSet(msg.Flags), IsPinned: msg.Flags.IsPinned(), @@ -231,12 +231,15 @@ func Users(users []*systemTypes.User) *outgoing.UserSet { return &retval } -func Attachment(in *messagingTypes.Attachment) *outgoing.Attachment { +func Attachment(in *messagingTypes.Attachment, userID uint64) *outgoing.Attachment { if in == nil { return nil } - var preview string + var ( + signParams = fmt.Sprintf("?sign=%s&userID=%d", auth.DefaultSigner.Sign(userID, in.ID), userID) + preview string + ) if in.Meta.Preview != nil { var ext = in.Meta.Preview.Extension @@ -250,8 +253,8 @@ func Attachment(in *messagingTypes.Attachment) *outgoing.Attachment { return &outgoing.Attachment{ ID: Uint64toa(in.ID), UserID: Uint64toa(in.UserID), - Url: fmt.Sprintf(attachmentURL, in.ID, url.PathEscape(in.Name)), - PreviewUrl: preview, + Url: fmt.Sprintf(attachmentURL, in.ID, url.PathEscape(in.Name)) + signParams, + PreviewUrl: preview + signParams, Meta: in.Meta, Name: in.Name, CreatedAt: in.CreatedAt, diff --git a/internal/payload/util.go b/internal/payload/util.go index f462f4a00..f5a4ba11f 100644 --- a/internal/payload/util.go +++ b/internal/payload/util.go @@ -26,7 +26,7 @@ func ParseUInt64(s string) uint64 { return i } -// ParseUInt64 parses a slice of strings into a slice of uint64s +// ParseUInt64s parses a slice of strings into a slice of uint64s func ParseUInt64s(ss []string) []uint64 { uu := make([]uint64, len(ss)) for i, s := range ss { diff --git a/messaging/rest/attachment.go b/messaging/rest/attachment.go index 4aa029926..a149857cf 100644 --- a/messaging/rest/attachment.go +++ b/messaging/rest/attachment.go @@ -2,22 +2,23 @@ package rest import ( "context" + "io" + "time" + + "github.com/pkg/errors" + + "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/messaging/internal/service" "github.com/crusttech/crust/messaging/rest/handlers" "github.com/crusttech/crust/messaging/rest/request" "github.com/crusttech/crust/messaging/types" - "github.com/pkg/errors" - "io" - "time" ) var _ = errors.Wrap type ( Attachment struct { - svc struct { - att service.AttachmentService - } + att service.AttachmentService } file struct { @@ -29,29 +30,53 @@ type ( func (Attachment) New() *Attachment { ctrl := &Attachment{} - ctrl.svc.att = service.DefaultAttachment + ctrl.att = service.DefaultAttachment return ctrl } func (ctrl *Attachment) Original(ctx context.Context, r *request.AttachmentOriginal) (interface{}, error) { - return ctrl.get(r.AttachmentID, false, r.Download) + if err := ctrl.isAccessible(r.AttachmentID, r.UserID, r.Sign); err != nil { + return nil, err + } + + return ctrl.get(ctx, r.AttachmentID, false, r.Download) } func (ctrl *Attachment) Preview(ctx context.Context, r *request.AttachmentPreview) (interface{}, error) { - return ctrl.get(r.AttachmentID, true, false) + if err := ctrl.isAccessible(r.AttachmentID, r.UserID, r.Sign); err != nil { + return nil, err + } + + return ctrl.get(ctx, r.AttachmentID, true, false) } -func (ctrl Attachment) get(ID uint64, preview, download bool) (handlers.Downloadable, error) { +func (ctrl Attachment) isAccessible(attachmentID, userID uint64, signature string) error { + if userID == 0 { + return errors.New("missing or invalid user ID") + } + + if attachmentID == 0 { + return errors.New("missing or invalid attachment ID") + } + + if auth.DefaultSigner.Verify(signature, userID, attachmentID) { + return errors.New("missing or invalid signature") + } + + return nil +} + +func (ctrl Attachment) get(ctx context.Context, ID uint64, preview, download bool) (handlers.Downloadable, error) { rval := &file{download: download} - if att, err := ctrl.svc.att.FindByID(ID); err != nil { + if att, err := ctrl.att.With(ctx).FindByID(ID); err != nil { return nil, err } else { rval.Attachment = att if preview { - rval.content, err = ctrl.svc.att.OpenPreview(att) + rval.content, err = ctrl.att.OpenPreview(att) } else { - rval.content, err = ctrl.svc.att.OpenOriginal(att) + rval.content, err = ctrl.att.OpenOriginal(att) } if err != nil { diff --git a/messaging/rest/channel.go b/messaging/rest/channel.go index f280b57a4..e1ace58b8 100644 --- a/messaging/rest/channel.go +++ b/messaging/rest/channel.go @@ -3,6 +3,7 @@ package rest import ( "context" + "github.com/crusttech/crust/internal/auth" "github.com/crusttech/crust/internal/payload" "github.com/crusttech/crust/internal/payload/outgoing" "github.com/crusttech/crust/messaging/internal/service" @@ -112,21 +113,19 @@ func (ctrl *Channel) Attach(ctx context.Context, r *request.ChannelAttach) (inte defer file.Close() - return ctrl.wrapAttachment(ctrl.svc.att.With(ctx).Create( + att, err := ctrl.svc.att.With(ctx).Create( r.Upload.Filename, r.Upload.Size, file, r.ChannelID, r.ReplyTo, - )) -} + ) -func (ctrl *Channel) wrapAttachment(attachment *types.Attachment, err error) (*outgoing.Attachment, error) { if err != nil { return nil, err - } else { - return payload.Attachment(attachment), nil } + + return payload.Attachment(att, auth.GetIdentityFromContext(ctx).Identity()), nil } func (ctrl *Channel) wrap(channel *types.Channel, err error) (*outgoing.Channel, error) { diff --git a/messaging/rest/handlers/attachment_custom.go b/messaging/rest/handlers/attachment_custom.go index 61aec7ab5..609a6f46f 100644 --- a/messaging/rest/handlers/attachment_custom.go +++ b/messaging/rest/handlers/attachment_custom.go @@ -54,7 +54,7 @@ func NewAttachmentDownloadable(ah AttachmentAPI) *Attachment { Original: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewAttachmentOriginal() - params.Fill(r) + _ = params.Fill(r) f, err := ah.Original(r.Context(), params) serve(f, err, w, r) @@ -63,7 +63,7 @@ func NewAttachmentDownloadable(ah AttachmentAPI) *Attachment { Preview: func(w http.ResponseWriter, r *http.Request) { defer r.Body.Close() params := request.NewAttachmentPreview() - params.Fill(r) + _ = params.Fill(r) f, err := ah.Preview(r.Context(), params) serve(f, err, w, r) diff --git a/messaging/rest/request/attachment.go b/messaging/rest/request/attachment.go index 70064d27e..1db132f12 100644 --- a/messaging/rest/request/attachment.go +++ b/messaging/rest/request/attachment.go @@ -33,6 +33,8 @@ var _ = multipart.FileHeader{} // Attachment original request parameters type AttachmentOriginal struct { Download bool + Sign string + UserID uint64 `json:",string"` Name string AttachmentID uint64 `json:",string"` } @@ -72,6 +74,14 @@ func (aReq *AttachmentOriginal) Fill(r *http.Request) (err error) { aReq.Download = parseBool(val) } + if val, ok := get["sign"]; ok { + + aReq.Sign = val + } + if val, ok := get["userID"]; ok { + + aReq.UserID = parseUInt64(val) + } aReq.Name = chi.URLParam(r, "name") aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID")) @@ -84,6 +94,8 @@ var _ RequestFiller = NewAttachmentOriginal() type AttachmentPreview struct { Ext string AttachmentID uint64 `json:",string"` + Sign string + UserID uint64 `json:",string"` } func NewAttachmentPreview() *AttachmentPreview { @@ -119,6 +131,14 @@ func (aReq *AttachmentPreview) Fill(r *http.Request) (err error) { aReq.Ext = chi.URLParam(r, "ext") aReq.AttachmentID = parseUInt64(chi.URLParam(r, "attachmentID")) + if val, ok := get["sign"]; ok { + + aReq.Sign = val + } + if val, ok := get["userID"]; ok { + + aReq.UserID = parseUInt64(val) + } return err } diff --git a/messaging/rest/router.go b/messaging/rest/router.go index 84657851b..54f4b24fd 100644 --- a/messaging/rest/router.go +++ b/messaging/rest/router.go @@ -11,7 +11,6 @@ func MountRoutes() func(chi.Router) { // Initialize handlers & controllers. return func(r chi.Router) { r.Group(func(r chi.Router) { - r.Use(auth.MiddlewareValidOnly404) handlers.NewAttachmentDownloadable(Attachment{}.New()).MountRoutes(r) }) diff --git a/messaging/start.go b/messaging/start.go index 9ca241989..5fcd94a05 100644 --- a/messaging/start.go +++ b/messaging/start.go @@ -71,6 +71,9 @@ func StartRestAPI(ctx context.Context) error { go metrics.NewMonitor(flags.monitor.Interval) } + // Use JWT secret for hmac signer for now + auth.DefaultSigner = auth.HmacSigner(flags.jwt.Secret) + jwtAuth, err := auth.JWT(flags.jwt.Secret, flags.jwt.Expiry) if err != nil { return errors.Wrap(err, "Error creating JWT Auth")