mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-20 23:07:33 +00:00
4980cd6a0c
* define roles and permissions * support system user memberships * don't limit system users * cleanup permissions * restrict memberships to aggregates * default to SYSTEM_OWNER * update unit tests * test: system user token test (#6778) * update unit tests * refactor: make authz testable * move session constants * cleanup * comment * comment * decode member type string to enum (#6780) * decode member type string to enum * handle all membership types * decode enums where necessary * decode member type in steps config * update system api docs * add technical advisory * tweak docs a bit * comment in comment * lint * extract token from Bearer header prefix * review changes * fix tests * fix: add fix for activityhandler * add isSystemUser * remove IsSystemUser from activity info * fix: add fix for activityhandler --------- Co-authored-by: Stefan Benz <stefan@caos.ch>
225 lines
7.2 KiB
Go
225 lines
7.2 KiB
Go
package assets
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/zitadel/logging"
|
|
|
|
"github.com/zitadel/zitadel/internal/api/authz"
|
|
http_util "github.com/zitadel/zitadel/internal/api/http"
|
|
http_mw "github.com/zitadel/zitadel/internal/api/http/middleware"
|
|
"github.com/zitadel/zitadel/internal/command"
|
|
"github.com/zitadel/zitadel/internal/id"
|
|
"github.com/zitadel/zitadel/internal/query"
|
|
"github.com/zitadel/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 h.errorHandler
|
|
}
|
|
|
|
func (h *Handler) Storage() static.Storage {
|
|
return h.storage
|
|
}
|
|
|
|
func AssetAPI(externalSecure bool) func(context.Context) string {
|
|
return func(ctx context.Context) string {
|
|
return http_util.BuildOrigin(authz.GetInstance(ctx).RequestedHost(), externalSecure) + HandlerPrefix
|
|
}
|
|
}
|
|
|
|
type Uploader interface {
|
|
UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error
|
|
ObjectName(data authz.CtxData) (string, error)
|
|
ResourceOwner(instance authz.Instance, data authz.CtxData) string
|
|
ContentTypeAllowed(contentType string) bool
|
|
MaxFileSize() int64
|
|
ObjectType() static.ObjectType
|
|
}
|
|
|
|
type Downloader interface {
|
|
ObjectName(ctx context.Context, path string) (string, error)
|
|
ResourceOwner(ctx context.Context, ownerPath string) string
|
|
}
|
|
|
|
type ErrorHandler func(w http.ResponseWriter, r *http.Request, err error, defaultCode int)
|
|
|
|
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, defaultCode int) {
|
|
logging.WithFields("uri", r.RequestURI).WithError(err).Warn("error occurred on asset api")
|
|
code, ok := http_util.ZitadelErrorToHTTPStatusCode(err)
|
|
if !ok {
|
|
code = defaultCode
|
|
}
|
|
http.Error(w, err.Error(), code)
|
|
}
|
|
|
|
func NewHandler(commands *command.Commands, verifier authz.APITokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor func(handler http.Handler) http.Handler) http.Handler {
|
|
h := &Handler{
|
|
commands: commands,
|
|
errorHandler: DefaultErrorHandler,
|
|
authInterceptor: http_mw.AuthorizationInterceptor(verifier, authConfig),
|
|
idGenerator: idGenerator,
|
|
storage: storage,
|
|
query: queries,
|
|
}
|
|
|
|
verifier.RegisterServer("Assets-API", "assets", AssetsService_AuthMethods)
|
|
router := mux.NewRouter()
|
|
csp := http_mw.SecurityHeaders(&http_mw.DefaultSCP, nil)
|
|
router.Use(callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor, csp)
|
|
router.Use(callDurationInterceptor, instanceInterceptor, assetCacheInterceptor, accessInterceptor)
|
|
RegisterRoutes(router, h)
|
|
router.PathPrefix("/{owner}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile()))
|
|
return http_util.CopyHeadersToContext(http_mw.CORSInterceptor(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) ResourceOwner(_ context.Context, ownerPath string) string {
|
|
return ownerPath
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
file, handler, err := r.FormFile(paramFile)
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
return
|
|
}
|
|
defer func() {
|
|
err = file.Close()
|
|
logging.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 too big, max file size is %vKB", uploader.MaxFileSize()/1024), http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
resourceOwner := uploader.ResourceOwner(authz.GetInstance(ctx), ctxData)
|
|
objectName, err := uploader.ObjectName(ctxData)
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
uploadInfo := &command.AssetUpload{
|
|
ResourceOwner: resourceOwner,
|
|
ObjectName: objectName,
|
|
ContentType: contentType,
|
|
ObjectType: uploader.ObjectType(),
|
|
File: file,
|
|
Size: size,
|
|
}
|
|
err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands())
|
|
if err != nil {
|
|
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %w", 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()
|
|
ownerPath := mux.Vars(r)["owner"]
|
|
resourceOwner := downloader.ResourceOwner(ctx, ownerPath)
|
|
path := ""
|
|
if ownerPath != "" {
|
|
path = strings.Split(r.RequestURI, ownerPath+"/")[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", path), http.StatusNotFound)
|
|
return
|
|
}
|
|
if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
|
|
s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName string, storage static.Storage) error {
|
|
split := strings.Split(objectName, "?v=")
|
|
if len(split) == 2 {
|
|
objectName = split[0]
|
|
}
|
|
data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName)
|
|
if err != nil {
|
|
return fmt.Errorf("download failed: %w", err)
|
|
}
|
|
info, err := getInfo()
|
|
if err != nil {
|
|
return fmt.Errorf("download failed: %w", err)
|
|
}
|
|
if info.Hash == strings.Trim(r.Header.Get(http_util.IfNoneMatch), "\"") {
|
|
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))
|
|
w.Header().Set(http_util.Etag, "\""+info.Hash+"\"")
|
|
w.WriteHeader(304)
|
|
return nil
|
|
}
|
|
w.Header().Set(http_util.ContentLength, strconv.FormatInt(info.Size, 10))
|
|
w.Header().Set(http_util.ContentType, info.ContentType)
|
|
w.Header().Set(http_util.LastModified, info.LastModified.Format(time.RFC1123))
|
|
w.Header().Set(http_util.Etag, "\""+info.Hash+"\"")
|
|
_, err = w.Write(data)
|
|
logging.New().OnError(err).Error("error writing response for asset")
|
|
return nil
|
|
}
|