3
0

Add url/param signing capapbilities to support access to attachments

This commit is contained in:
Denis Arh 2019-04-25 18:24:41 +02:00
parent 5d61b3d8cd
commit 2132d1fdca
14 changed files with 160 additions and 43 deletions

View File

@ -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 @@
}
]
}
]
]

View File

@ -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",

View File

@ -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 |
---

View File

@ -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
}
)

View File

@ -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)
})
}

43
internal/auth/signer.go Normal file
View File

@ -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...)
}

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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) {

View File

@ -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)

View File

@ -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
}

View File

@ -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)
})

View File

@ -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")