mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-12 19:14:23 +00:00
504fe5b761
* feat: remove exif data from uploaded images (#3221) * feat: remove exif tags from images * feat: remove exif data * feat: remove exif * fix: add preferredLoginName to user grant response (#3271) * chore: log webauthn parse error (#3272) * log error * log error * feat: Help link in privacy policy * fix: convert correct detail data on organization (#3279) * fix: handle empty editor users * fix: add some missing translations (#3291) * fix: org policy translations * fix: metadata event types translation * fix: translations * fix: filter resource owner correctly on project grant members (#3281) * fix: filter resource owner correctly on project grant members * fix: filter resource owner correctly on project grant members * fix: add orgIDs to zitadel permissions request Co-authored-by: fabi <fabienne.gerschwiler@gmail.com> * fix: get IAM memberships correctly in MyZitadelPermissions (#3309) * fix: correct login names on auth and notification users (#3349) * fix: correct login names on auth and notification users * fix: migration * fix: handle resource owner in action flows (#3361) * fix merge * fix: exchange exif library (#3366) * fix: exchange exif library * ignore tiffs * requested fixes * feat: Help link in privacy policy Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com> Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
225 lines
6.7 KiB
Go
225 lines
6.7 KiB
Go
package assets
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/caos/logging"
|
|
sentryhttp "github.com/getsentry/sentry-go/http"
|
|
"github.com/gorilla/mux"
|
|
"github.com/superseriousbusiness/exifremove/pkg/exifremove"
|
|
|
|
"github.com/caos/zitadel/internal/api/authz"
|
|
http_mw "github.com/caos/zitadel/internal/api/http/middleware"
|
|
"github.com/caos/zitadel/internal/command"
|
|
"github.com/caos/zitadel/internal/domain"
|
|
"github.com/caos/zitadel/internal/id"
|
|
"github.com/caos/zitadel/internal/query"
|
|
"github.com/caos/zitadel/internal/static"
|
|
)
|
|
|
|
const (
|
|
HandlerPrefix = "/assets/v1"
|
|
)
|
|
|
|
type Handler struct {
|
|
errorHandler ErrorHandler
|
|
storage static.Storage
|
|
commands *command.Commands
|
|
authInterceptor *http_mw.AuthInterceptor
|
|
idGenerator id.Generator
|
|
query *query.Queries
|
|
}
|
|
|
|
func (h *Handler) AuthInterceptor() *http_mw.AuthInterceptor {
|
|
return h.authInterceptor
|
|
}
|
|
|
|
func (h *Handler) Commands() *command.Commands {
|
|
return h.commands
|
|
}
|
|
|
|
func (h *Handler) ErrorHandler() ErrorHandler {
|
|
return DefaultErrorHandler
|
|
}
|
|
|
|
func (h *Handler) Storage() static.Storage {
|
|
return h.storage
|
|
}
|
|
|
|
type Uploader interface {
|
|
Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error
|
|
ObjectName(data authz.CtxData) (string, error)
|
|
BucketName(data authz.CtxData) string
|
|
ContentTypeAllowed(contentType string) bool
|
|
MaxFileSize() int64
|
|
}
|
|
|
|
type Downloader interface {
|
|
ObjectName(ctx context.Context, path string) (string, error)
|
|
BucketName(ctx context.Context, id string) string
|
|
}
|
|
|
|
type ErrorHandler func(http.ResponseWriter, *http.Request, error, int)
|
|
|
|
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, code int) {
|
|
logging.Log("ASSET-g5ef1").WithError(err).WithField("uri", r.RequestURI).Error("error occurred on asset api")
|
|
http.Error(w, err.Error(), code)
|
|
}
|
|
|
|
func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries) http.Handler {
|
|
h := &Handler{
|
|
commands: commands,
|
|
errorHandler: DefaultErrorHandler,
|
|
authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig),
|
|
idGenerator: idGenerator,
|
|
storage: storage,
|
|
query: queries,
|
|
}
|
|
|
|
verifier.RegisterServer("Management-API", "assets", AssetsService_AuthMethods) //TODO: separate api?
|
|
router := mux.NewRouter()
|
|
router.Use(sentryhttp.New(sentryhttp.Options{}).Handle)
|
|
RegisterRoutes(router, h)
|
|
router.PathPrefix("/{id}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile()))
|
|
return router
|
|
}
|
|
|
|
func (h *Handler) GetFile() Downloader {
|
|
return &publicFileDownloader{}
|
|
}
|
|
|
|
type publicFileDownloader struct{}
|
|
|
|
func (l *publicFileDownloader) ObjectName(_ context.Context, path string) (string, error) {
|
|
return path, nil
|
|
}
|
|
|
|
func (l *publicFileDownloader) BucketName(_ context.Context, id string) string {
|
|
return id
|
|
}
|
|
|
|
const maxMemory = 2 << 20
|
|
const paramFile = "file"
|
|
|
|
func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
ctx := r.Context()
|
|
ctxData := authz.GetCtxData(ctx)
|
|
err := r.ParseMultipartForm(maxMemory)
|
|
file, handler, err := r.FormFile(paramFile)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer func() {
|
|
err = file.Close()
|
|
logging.Log("UPLOAD-GDg34").OnError(err).Warn("could not close file")
|
|
}()
|
|
contentType := handler.Header.Get("content-type")
|
|
size := handler.Size
|
|
if !uploader.ContentTypeAllowed(contentType) {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("invalid content-type: %s", contentType), http.StatusBadRequest)
|
|
return
|
|
}
|
|
if size > uploader.MaxFileSize() {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("file to big, max file size is %vKB", uploader.MaxFileSize()/1024), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
bucketName := uploader.BucketName(ctxData)
|
|
objectName, err := uploader.ObjectName(ctxData)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
cleanedFile, cleanedSize, err := removeExif(file, size, contentType)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("remove exif error: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
info, err := s.Commands().UploadAsset(ctx, bucketName, objectName, contentType, cleanedFile, cleanedSize)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
err = uploader.Callback(ctx, info, ctxData.OrgID, s.Commands())
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
if s.Storage() == nil {
|
|
return
|
|
}
|
|
ctx := r.Context()
|
|
id := mux.Vars(r)["id"]
|
|
bucketName := downloader.BucketName(ctx, id)
|
|
path := ""
|
|
if id != "" {
|
|
path = strings.Split(r.RequestURI, id+"/")[1]
|
|
}
|
|
objectName, err := downloader.ObjectName(ctx, path)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
if objectName == "" {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("file not found: %v", objectName), http.StatusNotFound)
|
|
return
|
|
}
|
|
reader, getInfo, err := s.Storage().GetObject(ctx, bucketName, objectName)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
data, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
info, err := getInfo()
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("content-length", strconv.FormatInt(info.Size, 10))
|
|
w.Header().Set("content-type", info.ContentType)
|
|
w.Header().Set("ETag", info.ETag)
|
|
w.Write(data)
|
|
}
|
|
}
|
|
|
|
func removeExif(file io.Reader, size int64, contentType string) (io.Reader, int64, error) {
|
|
if !isAllowedContentType(contentType) {
|
|
return file, size, nil
|
|
}
|
|
buf := new(bytes.Buffer)
|
|
_, err := buf.ReadFrom(file)
|
|
if err != nil {
|
|
return file, 0, err
|
|
}
|
|
data, err := exifremove.Remove(buf.Bytes())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return bytes.NewReader(data), int64(len(data)), nil
|
|
}
|
|
|
|
func isAllowedContentType(contentType string) bool {
|
|
return strings.HasSuffix(contentType, "png") ||
|
|
strings.HasSuffix(contentType, "jpg") ||
|
|
strings.HasSuffix(contentType, "jpeg")
|
|
}
|