From 4a0d61d75a575fe9239695ba5109474707f60cde Mon Sep 17 00:00:00 2001 From: Livio Amstutz Date: Wed, 6 Apr 2022 08:13:40 +0200 Subject: [PATCH] feat: store assets in database (#3290) * feat: use database as asset storage * being only uploading assets if allowed * tests * fixes * cleanup after merge * renaming * various fixes * fix: change to repository event types and removed unused code * feat: set default features * error handling * error handling and naming * fix tests * fix tests * fix merge * rename --- cmd/admin/setup/02.go | 35 +- cmd/admin/setup/03.go | 22 + cmd/admin/setup/config.go | 5 +- cmd/admin/setup/setup.go | 16 +- cmd/admin/setup/steps.yaml | 25 +- cmd/admin/start/start.go | 15 +- .../eventsourcing/handler/styling.go | 7 +- internal/api/assets/asset.go | 111 ++-- internal/api/assets/generator/asset.yaml | 4 +- internal/api/assets/login_policy.go | 69 ++- internal/api/assets/user_avatar.go | 17 +- internal/api/http/header.go | 3 + internal/api/ui/login/resources_handler.go | 76 +-- internal/command/instance_policy_label.go | 77 +-- .../command/instance_policy_label_test.go | 441 +++++++++++---- internal/command/org_policy_label.go | 68 +-- internal/command/org_policy_label_test.go | 520 +++++++++++++----- internal/command/statics.go | 63 ++- internal/command/user_human_avatar.go | 13 +- internal/command/user_human_avatar_test.go | 116 ++-- internal/command/v2/instance.go | 53 +- internal/command/v2/instance_features.go | 95 ++++ internal/command/v2/instance_features_test.go | 152 +++++ internal/query/projection/feature.go | 4 +- internal/static/config/config.go | 106 +--- internal/static/database/crdb.go | 182 ++++++ internal/static/database/crdb_test.go | 203 +++++++ internal/static/i18n/de.yaml | 1 + internal/static/i18n/en.yaml | 1 + internal/static/i18n/it.yaml | 1 + internal/static/mock/storage_mock.go | 162 +----- internal/static/mock/storage_mock.impl.go | 29 +- internal/static/s3/config.go | 16 + internal/static/s3/minio.go | 213 +++---- internal/static/storage.go | 41 +- internal/user/model/user_view.go | 21 - 36 files changed, 2016 insertions(+), 967 deletions(-) create mode 100644 cmd/admin/setup/03.go create mode 100644 internal/command/v2/instance_features.go create mode 100644 internal/command/v2/instance_features_test.go create mode 100644 internal/static/database/crdb.go create mode 100644 internal/static/database/crdb_test.go diff --git a/cmd/admin/setup/02.go b/cmd/admin/setup/02.go index 617ab05b7c..8b20ebce94 100644 --- a/cmd/admin/setup/02.go +++ b/cmd/admin/setup/02.go @@ -2,21 +2,36 @@ package setup import ( "context" - - command "github.com/caos/zitadel/internal/command/v2" + "database/sql" + _ "embed" ) -type DefaultInstance struct { - cmd *command.Command - InstanceSetup command.InstanceSetup +const ( + createAssets = ` +CREATE TABLE system.assets ( + instance_id TEXT, + asset_type TEXT, + resource_owner TEXT, + name TEXT, + content_type TEXT, + hash TEXT AS (md5(data)) STORED, + data BYTES, + updated_at TIMESTAMPTZ, + + PRIMARY KEY (instance_id, resource_owner, name) +); +` +) + +type AssetTable struct { + dbClient *sql.DB } -func (mig *DefaultInstance) Execute(ctx context.Context) error { - _, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup) - +func (mig *AssetTable) Execute(ctx context.Context) error { + _, err := mig.dbClient.ExecContext(ctx, createAssets) return err } -func (mig *DefaultInstance) String() string { - return "02_default_instance" +func (mig *AssetTable) String() string { + return "02_assets" } diff --git a/cmd/admin/setup/03.go b/cmd/admin/setup/03.go new file mode 100644 index 0000000000..71e3ba7a95 --- /dev/null +++ b/cmd/admin/setup/03.go @@ -0,0 +1,22 @@ +package setup + +import ( + "context" + + "github.com/caos/zitadel/internal/command/v2" +) + +type DefaultInstance struct { + cmd *command.Command + InstanceSetup command.InstanceSetup +} + +func (mig *DefaultInstance) Execute(ctx context.Context) error { + _, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup) + + return err +} + +func (mig *DefaultInstance) String() string { + return "03_default_instance" +} diff --git a/cmd/admin/setup/config.go b/cmd/admin/setup/config.go index a28bd7d0cd..171028031c 100644 --- a/cmd/admin/setup/config.go +++ b/cmd/admin/setup/config.go @@ -35,8 +35,9 @@ func MustNewConfig(v *viper.Viper) *Config { } type Steps struct { - S1ProjectionTable *ProjectionTable - S2DefaultInstance *DefaultInstance + s1ProjectionTable *ProjectionTable + s2AssetsTable *AssetTable + S3DefaultInstance *DefaultInstance } func MustNewSteps(v *viper.Viper) *Steps { diff --git a/cmd/admin/setup/setup.go b/cmd/admin/setup/setup.go index 23ecd953b5..21ca6c850d 100644 --- a/cmd/admin/setup/setup.go +++ b/cmd/admin/setup/setup.go @@ -9,7 +9,7 @@ import ( "github.com/spf13/viper" http_util "github.com/caos/zitadel/internal/api/http" - command "github.com/caos/zitadel/internal/command/v2" + "github.com/caos/zitadel/internal/command/v2" "github.com/caos/zitadel/internal/database" "github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/migration" @@ -46,12 +46,14 @@ func Setup(config *Config, steps *Steps) { cmd := command.New(eventstoreClient, "localhost", config.SystemDefaults) - steps.S2DefaultInstance.cmd = cmd - steps.S1ProjectionTable = &ProjectionTable{dbClient: dbClient} - steps.S2DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure - steps.S2DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) + steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient} + steps.s2AssetsTable = &AssetTable{dbClient: dbClient} + steps.S3DefaultInstance.cmd = cmd + steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure + steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) ctx := context.Background() - migration.Migrate(ctx, eventstoreClient, steps.S1ProjectionTable) - migration.Migrate(ctx, eventstoreClient, steps.S2DefaultInstance) + migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable) + migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable) + migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance) } diff --git a/cmd/admin/setup/steps.yaml b/cmd/admin/setup/steps.yaml index afc99c4e13..9d3f5febc7 100644 --- a/cmd/admin/setup/steps.yaml +++ b/cmd/admin/setup/steps.yaml @@ -1,4 +1,4 @@ -S2DefaultInstance: +S3DefaultInstance: InstanceSetup: Org: Name: ZITADEL @@ -13,6 +13,29 @@ S2DefaultInstance: Gender: Phone: Password: Password1! + Features: + TierName: Default Tier + TierDescription: "" + State: 1 #active + StateDescription: "" + Retention: 8760h #1year + LoginPolicyFactors: true + LoginPolicyIDP: true + LoginPolicyPasswordless: true + LoginPolicyRegistration: true + LoginPolicyUsernameLogin: true + LoginPolicyPasswordReset: true + PasswordComplexityPolicy: true + LabelPolicyPrivateLabel: true + LabelPolicyWatermark: true + CustomDomain: true + PrivacyPolicy: true + MetadataUser: true + CustomTextMessage: true + CustomTextLogin: true + LockoutPolicy: true + ActionsAllowed: 2 #ActionsAllowedUnlimited + MaxActions: #not necessary because of ActionsAllowedUnlimited PasswordComplexityPolicy: MinLength: 8 HasLowercase: true diff --git a/cmd/admin/start/start.go b/cmd/admin/start/start.go index d160c2856b..eb58248252 100644 --- a/cmd/admin/start/start.go +++ b/cmd/admin/start/start.go @@ -94,12 +94,6 @@ func startZitadel(config *Config, masterKey string) error { return err } - var storage static.Storage - //TODO: enable when storage is implemented again - //if *assetsEnabled { - //storage, err = config.AssetStorage.Config.NewStorage() - //logging.Log("MAIN-Bfhe2").OnError(err).Fatal("Unable to start asset storage") - //} eventstoreClient, err := eventstore.Start(dbClient) if err != nil { return fmt.Errorf("cannot start eventstore for queries: %w", err) @@ -114,6 +108,11 @@ func startZitadel(config *Config, masterKey string) error { if err != nil { return fmt.Errorf("error starting authz repo: %w", err) } + + storage, err := config.AssetStorage.NewStorage(dbClient) + if err != nil { + return fmt.Errorf("cannot start asset storage client: %w", err) + } webAuthNConfig := webauthn.Config{ ID: config.ExternalDomain, Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), @@ -163,13 +162,13 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman return err } - apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries)) + instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader) + apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries, instanceInterceptor.Handler)) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure) if err != nil { return err } - instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader) issuer := oidc.Issuer(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, issuer, login.DefaultLoggedOutPath, commands, queries, authRepo, config.SystemDefaults.KeyConfig, keys.OIDC, keys.OIDCKey, eventstore, dbClient, keyChan, userAgentInterceptor, instanceInterceptor.Handler) diff --git a/internal/admin/repository/eventsourcing/handler/styling.go b/internal/admin/repository/eventsourcing/handler/styling.go index f641987fed..75839570a2 100644 --- a/internal/admin/repository/eventsourcing/handler/styling.go +++ b/internal/admin/repository/eventsourcing/handler/styling.go @@ -163,7 +163,7 @@ func (m *Styling) generateStylingFile(policy *iam_model.LabelPolicyView) error { if err != nil { return err } - return m.uploadFilesToBucket(policy.AggregateID, "text/css", reader, size) + return m.uploadFilesToStorage(policy.InstanceID, policy.AggregateID, "text/css", reader, size) } func (m *Styling) writeFile(policy *iam_model.LabelPolicyView) (io.Reader, int64, error) { @@ -245,9 +245,10 @@ const fontFaceTemplate = ` } ` -func (m *Styling) uploadFilesToBucket(aggregateID, contentType string, reader io.Reader, size int64) error { +func (m *Styling) uploadFilesToStorage(instanceID, aggregateID, contentType string, reader io.Reader, size int64) error { fileName := domain.CssPath + "/" + domain.CssVariablesFileName - _, err := m.static.PutObject(context.Background(), aggregateID, fileName, contentType, reader, size, true) + //TODO: handle location as soon as possible + _, err := m.static.PutObject(context.Background(), instanceID, "", aggregateID, fileName, contentType, static.ObjectTypeStyling, reader, size) return err } diff --git a/internal/api/assets/asset.go b/internal/api/assets/asset.go index 374b5f6fd0..98f4a191b1 100644 --- a/internal/api/assets/asset.go +++ b/internal/api/assets/asset.go @@ -1,24 +1,21 @@ package assets import ( - "bytes" "context" "fmt" - "io" - "io/ioutil" "net/http" "strconv" "strings" + "time" "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_util "github.com/caos/zitadel/internal/api/http" 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" @@ -54,26 +51,27 @@ func (h *Handler) Storage() static.Storage { } type Uploader interface { - Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error + UploadAsset(ctx context.Context, info string, asset *command.AssetUpload, commands *command.Commands) error ObjectName(data authz.CtxData) (string, error) - BucketName(data authz.CtxData) string + 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) - BucketName(ctx context.Context, id string) string + ResourceOwner(ctx context.Context, ownerPath 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") + logging.WithFields("uri", r.RequestURI).WithError(err).Warn("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 { +func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authConfig authz.Config, idGenerator id.Generator, storage static.Storage, queries *query.Queries, instanceInterceptor func(handler http.Handler) http.Handler) http.Handler { h := &Handler{ commands: commands, errorHandler: DefaultErrorHandler, @@ -85,9 +83,9 @@ func NewHandler(commands *command.Commands, verifier *authz.TokenVerifier, authC verifier.RegisterServer("Management-API", "assets", AssetsService_AuthMethods) //TODO: separate api? router := mux.NewRouter() - router.Use(sentryhttp.New(sentryhttp.Options{}).Handle) + router.Use(sentryhttp.New(sentryhttp.Options{}).Handle, instanceInterceptor) RegisterRoutes(router, h) - router.PathPrefix("/{id}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile())) + router.PathPrefix("/{owner}").Methods("GET").HandlerFunc(DownloadHandleFunc(h, h.GetFile())) return router } @@ -101,8 +99,8 @@ func (l *publicFileDownloader) ObjectName(_ context.Context, path string) (strin return path, nil } -func (l *publicFileDownloader) BucketName(_ context.Context, id string) string { - return id +func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string) string { + return ownerPath } const maxMemory = 2 << 20 @@ -120,7 +118,7 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit } defer func() { err = file.Close() - logging.Log("UPLOAD-GDg34").OnError(err).Warn("could not close file") + logging.OnError(err).Warn("could not close file") }() contentType := handler.Header.Get("content-type") size := handler.Size @@ -133,24 +131,21 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit return } - bucketName := uploader.BucketName(ctxData) + 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 } - cleanedFile, cleanedSize, err := removeExif(file, size, contentType) - if err != nil { - s.ErrorHandler()(w, r, fmt.Errorf("remove exif error: %v", err), http.StatusInternalServerError) - return + uploadInfo := &command.AssetUpload{ + ResourceOwner: resourceOwner, + ObjectName: objectName, + ContentType: contentType, + ObjectType: uploader.ObjectType(), + File: file, + Size: size, } - - 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()) + err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands()) if err != nil { s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError) return @@ -164,11 +159,11 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon return } ctx := r.Context() - id := mux.Vars(r)["id"] - bucketName := downloader.BucketName(ctx, id) + ownerPath := mux.Vars(r)["owner"] + resourceOwner := downloader.ResourceOwner(ctx, ownerPath) path := "" - if id != "" { - path = strings.Split(r.RequestURI, id+"/")[1] + if ownerPath != "" { + path = strings.Split(r.RequestURI, ownerPath+"/")[1] } objectName, err := downloader.ObjectName(ctx, path) if err != nil { @@ -176,49 +171,33 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon return } if objectName == "" { - s.ErrorHandler()(w, r, fmt.Errorf("file not found: %v", objectName), http.StatusNotFound) + s.ErrorHandler()(w, r, fmt.Errorf("file not found: %v", path), 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 + if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil { + s.ErrorHandler()(w, r, err, http.StatusInternalServerError) } - 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) +func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName string, storage static.Storage) error { + data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName) if err != nil { - return file, 0, err + return fmt.Errorf("download failed: %v", err) } - data, err := exifremove.Remove(buf.Bytes()) + info, err := getInfo() if err != nil { - return nil, 0, err + return fmt.Errorf("download failed: %v", 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") + if info.Hash == r.Header.Get(http_util.IfNoneMatch) { + 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 } diff --git a/internal/api/assets/generator/asset.yaml b/internal/api/assets/generator/asset.yaml index ee1d4595ba..cefb90d208 100644 --- a/internal/api/assets/generator/asset.yaml +++ b/internal/api/assets/generator/asset.yaml @@ -1,6 +1,6 @@ Services: IAM: - Prefix: "/iam" + Prefix: "/instance" Methods: DefaultLabelPolicyLogo: Path: "/policy/label/logo" @@ -116,4 +116,4 @@ Services: - Name: Get Comment: Type: download - Permission: authenticated \ No newline at end of file + Permission: authenticated diff --git a/internal/api/assets/login_policy.go b/internal/api/assets/login_policy.go index e816469fce..8c671a3e57 100644 --- a/internal/api/assets/login_policy.go +++ b/internal/api/assets/login_policy.go @@ -9,6 +9,7 @@ import ( "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/query" + "github.com/caos/zitadel/internal/static" ) func (h *Handler) UploadDefaultLabelPolicyLogo() Uploader { @@ -44,6 +45,10 @@ func (l *labelPolicyLogoUploader) ContentTypeAllowed(contentType string) bool { return false } +func (l *labelPolicyLogoUploader) ObjectType() static.ObjectType { + return static.ObjectTypeStyling +} + func (l *labelPolicyLogoUploader) MaxFileSize() int64 { return l.maxSize } @@ -60,27 +65,27 @@ func (l *labelPolicyLogoUploader) ObjectName(_ authz.CtxData) (string, error) { return prefix + "-" + suffixID, nil } -func (l *labelPolicyLogoUploader) BucketName(ctxData authz.CtxData) string { +func (l *labelPolicyLogoUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string { if l.defaultPolicy { - return domain.IAMID + return instance.InstanceID() } return ctxData.OrgID } -func (l *labelPolicyLogoUploader) Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error { +func (l *labelPolicyLogoUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error { if l.defaultPolicy { if l.darkMode { - _, err := commands.AddLogoDarkDefaultLabelPolicy(ctx, info.Key) + _, err := commands.AddLogoDarkDefaultLabelPolicy(ctx, upload) return err } - _, err := commands.AddLogoDefaultLabelPolicy(ctx, info.Key) + _, err := commands.AddLogoDefaultLabelPolicy(ctx, upload) return err } if l.darkMode { - _, err := commands.AddLogoDarkLabelPolicy(ctx, orgID, info.Key) + _, err := commands.AddLogoDarkLabelPolicy(ctx, orgID, upload) return err } - _, err := commands.AddLogoLabelPolicy(ctx, orgID, info.Key) + _, err := commands.AddLogoLabelPolicy(ctx, orgID, upload) return err } @@ -134,8 +139,8 @@ func (l *labelPolicyLogoDownloader) ObjectName(ctx context.Context, path string) return policy.Light.LogoURL, nil } -func (l *labelPolicyLogoDownloader) BucketName(ctx context.Context, id string) string { - return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) +func (l *labelPolicyLogoDownloader) ResourceOwner(ctx context.Context, _ string) string { + return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query) } func (h *Handler) UploadDefaultLabelPolicyIcon() Uploader { @@ -171,6 +176,10 @@ func (l *labelPolicyIconUploader) ContentTypeAllowed(contentType string) bool { return false } +func (l *labelPolicyIconUploader) ObjectType() static.ObjectType { + return static.ObjectTypeStyling +} + func (l *labelPolicyIconUploader) MaxFileSize() int64 { return l.maxSize } @@ -187,28 +196,28 @@ func (l *labelPolicyIconUploader) ObjectName(_ authz.CtxData) (string, error) { return prefix + "-" + suffixID, nil } -func (l *labelPolicyIconUploader) BucketName(ctxData authz.CtxData) string { +func (l *labelPolicyIconUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string { if l.defaultPolicy { - return domain.IAMID + return instance.InstanceID() } return ctxData.OrgID } -func (l *labelPolicyIconUploader) Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error { +func (l *labelPolicyIconUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error { if l.defaultPolicy { if l.darkMode { - _, err := commands.AddIconDarkDefaultLabelPolicy(ctx, info.Key) + _, err := commands.AddIconDarkDefaultLabelPolicy(ctx, upload) return err } - _, err := commands.AddIconDefaultLabelPolicy(ctx, info.Key) + _, err := commands.AddIconDefaultLabelPolicy(ctx, upload) return err } if l.darkMode { - _, err := commands.AddIconDarkLabelPolicy(ctx, orgID, info.Key) + _, err := commands.AddIconDarkLabelPolicy(ctx, orgID, upload) return err } - _, err := commands.AddIconLabelPolicy(ctx, orgID, info.Key) + _, err := commands.AddIconLabelPolicy(ctx, orgID, upload) return err } @@ -262,8 +271,8 @@ func (l *labelPolicyIconDownloader) ObjectName(ctx context.Context, path string) return policy.Light.IconURL, nil } -func (l *labelPolicyIconDownloader) BucketName(ctx context.Context, id string) string { - return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) +func (l *labelPolicyIconDownloader) ResourceOwner(ctx context.Context, _ string) string { + return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query) } func (h *Handler) UploadDefaultLabelPolicyFont() Uploader { @@ -290,6 +299,10 @@ func (l *labelPolicyFontUploader) ContentTypeAllowed(contentType string) bool { return false } +func (l *labelPolicyFontUploader) ObjectType() static.ObjectType { + return static.ObjectTypeStyling +} + func (l *labelPolicyFontUploader) MaxFileSize() int64 { return l.maxSize } @@ -303,19 +316,19 @@ func (l *labelPolicyFontUploader) ObjectName(_ authz.CtxData) (string, error) { return prefix + "-" + suffixID, nil } -func (l *labelPolicyFontUploader) BucketName(ctxData authz.CtxData) string { +func (l *labelPolicyFontUploader) ResourceOwner(instance authz.Instance, ctxData authz.CtxData) string { if l.defaultPolicy { - return domain.IAMID + return instance.InstanceID() } return ctxData.OrgID } -func (l *labelPolicyFontUploader) Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error { +func (l *labelPolicyFontUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error { if l.defaultPolicy { - _, err := commands.AddFontDefaultLabelPolicy(ctx, info.Key) + _, err := commands.AddFontDefaultLabelPolicy(ctx, upload) return err } - _, err := commands.AddFontLabelPolicy(ctx, orgID, info.Key) + _, err := commands.AddFontLabelPolicy(ctx, orgID, upload) return err } @@ -349,8 +362,8 @@ func (l *labelPolicyFontDownloader) ObjectName(ctx context.Context, path string) return policy.FontURL, nil } -func (l *labelPolicyFontDownloader) BucketName(ctx context.Context, id string) string { - return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) +func (l *labelPolicyFontDownloader) ResourceOwner(ctx context.Context, _ string) string { + return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query) } func getLabelPolicy(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) (*query.LabelPolicy, error) { @@ -366,16 +379,16 @@ func getLabelPolicy(ctx context.Context, defaultPolicy, preview bool, queries *q return queries.ActiveLabelPolicyByOrg(ctx, authz.GetCtxData(ctx).OrgID) } -func getLabelPolicyBucketName(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) string { +func getLabelPolicyResourceOwner(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) string { if defaultPolicy { - return domain.IAMID + return authz.GetInstance(ctx).InstanceID() } policy, err := getLabelPolicy(ctx, defaultPolicy, preview, queries) if err != nil { return "" } if policy.IsDefault { - return domain.IAMID + return authz.GetInstance(ctx).InstanceID() } return authz.GetCtxData(ctx).OrgID } diff --git a/internal/api/assets/user_avatar.go b/internal/api/assets/user_avatar.go index 687f0578e8..bffae8021d 100644 --- a/internal/api/assets/user_avatar.go +++ b/internal/api/assets/user_avatar.go @@ -7,6 +7,7 @@ import ( "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/command" "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/static" ) func (h *Handler) UploadMyUserAvatar() Uploader { @@ -27,6 +28,10 @@ func (l *myHumanAvatarUploader) ContentTypeAllowed(contentType string) bool { return false } +func (l *myHumanAvatarUploader) ObjectType() static.ObjectType { + return static.ObjectTypeUserAvatar +} + func (l *myHumanAvatarUploader) MaxFileSize() int64 { return l.maxSize } @@ -35,12 +40,12 @@ func (l *myHumanAvatarUploader) ObjectName(ctxData authz.CtxData) (string, error return domain.GetHumanAvatarAssetPath(ctxData.UserID), nil } -func (l *myHumanAvatarUploader) BucketName(ctxData authz.CtxData) string { - return ctxData.OrgID +func (l *myHumanAvatarUploader) ResourceOwner(_ authz.Instance, ctxData authz.CtxData) string { + return ctxData.ResourceOwner } -func (l *myHumanAvatarUploader) Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error { - _, err := commands.AddHumanAvatar(ctx, orgID, authz.GetCtxData(ctx).UserID, info.Key) +func (l *myHumanAvatarUploader) UploadAsset(ctx context.Context, orgID string, upload *command.AssetUpload, commands *command.Commands) error { + _, err := commands.AddHumanAvatar(ctx, orgID, authz.GetCtxData(ctx).UserID, upload) return err } @@ -54,6 +59,6 @@ func (l *myHumanAvatarDownloader) ObjectName(ctx context.Context, path string) ( return domain.GetHumanAvatarAssetPath(authz.GetCtxData(ctx).UserID), nil } -func (l *myHumanAvatarDownloader) BucketName(ctx context.Context, id string) string { - return authz.GetCtxData(ctx).OrgID +func (l *myHumanAvatarDownloader) ResourceOwner(ctx context.Context, _ string) string { + return authz.GetCtxData(ctx).ResourceOwner } diff --git a/internal/api/http/header.go b/internal/api/http/header.go index beb3b53a57..bdb8877f44 100644 --- a/internal/api/http/header.go +++ b/internal/api/http/header.go @@ -22,6 +22,9 @@ const ( ForwardedFor = "x-forwarded-for" XUserAgent = "x-user-agent" XGrpcWeb = "x-grpc-web" + IfNoneMatch = "If-None-Match" + LastModified = "Last-Modified" + Etag = "Etag" ContentSecurityPolicy = "content-security-policy" XXSSProtection = "x-xss-protection" diff --git a/internal/api/ui/login/resources_handler.go b/internal/api/ui/login/resources_handler.go index b2cce55be2..fcfb7d4e7b 100644 --- a/internal/api/ui/login/resources_handler.go +++ b/internal/api/ui/login/resources_handler.go @@ -1,11 +1,12 @@ package login import ( - "context" "net/http" + "github.com/caos/logging" + + "github.com/caos/zitadel/internal/api/assets" "github.com/caos/zitadel/internal/api/authz" - "github.com/caos/zitadel/internal/domain" ) type dynamicResourceData struct { @@ -25,74 +26,11 @@ func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) { return } - bucketName := authz.GetInstance(r.Context()).InstanceID() + resourceOwner := authz.GetInstance(r.Context()).InstanceID() if data.OrgID != "" && !data.DefaultPolicy { - bucketName = data.OrgID + resourceOwner = data.OrgID } - etag := r.Header.Get("If-None-Match") - asset, info, err := l.getStatic(r.Context(), bucketName, data.FileName) - if info != nil && info.ETag == etag { - w.WriteHeader(304) - return - } - if err != nil { - return - } - //TODO: enable again when assets are implemented again - _ = asset - //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(asset) -} - -func (l *Login) getStatic(ctx context.Context, bucketName, fileName string) ([]byte, *domain.AssetInfo, error) { - s := new(staticAsset) - //TODO: enable again when assets are implemented again - //key := bucketName + "-" + fileName - //err := l.staticCache.Get(key, s) - //if err == nil && s.Info != nil && (s.Info.Expiration.After(time.Now().Add(-1 * time.Minute))) { //TODO: config? - // return s.Data, s.Info, nil - //} - - //info, err := l.staticStorage.GetObjectInfo(ctx, bucketName, fileName) - //if err != nil { - // if caos_errs.IsNotFound(err) { - // return nil, nil, err - // } - // return s.Data, s.Info, err - //} - //if s.Info != nil && s.Info.ETag == info.ETag { - // if info.Expiration.After(s.Info.Expiration) { - // s.Info = info - // //l.cacheStatic(bucketName, fileName, s) - // } - // return s.Data, s.Info, nil - //} - // - //reader, _, err := l.staticStorage.GetObject(ctx, bucketName, fileName) - //if err != nil { - // return s.Data, s.Info, err - //} - //s.Data, err = ioutil.ReadAll(reader) - //if err != nil { - // return nil, nil, err - //} - //s.Info = info - //l.cacheStatic(bucketName, fileName, s) - return s.Data, s.Info, nil -} - -//TODO: enable again when assets are implemented again -// -//func (l *Login) cacheStatic(bucketName, fileName string, s *staticAsset) { -// key := bucketName + "-" + fileName -// err := l.staticCache.Set(key, &s) -// logging.Log("HANDLER-dfht2").OnError(err).Warnf("caching of asset %s: %s failed", bucketName, fileName) -//} - -type staticAsset struct { - Data []byte - Info *domain.AssetInfo + err = assets.GetAsset(w, r, resourceOwner, data.FileName, l.staticStorage) + logging.WithFields("file", data.FileName, "org", resourceOwner).OnError(err).Warn("asset in login could not be served") } diff --git a/internal/command/instance_policy_label.go b/internal/command/instance_policy_label.go index 6e3815801f..efaa44c8f6 100644 --- a/internal/command/instance_policy_label.go +++ b/internal/command/instance_policy_label.go @@ -124,10 +124,7 @@ func (c *Commands) ActivateDefaultLabelPolicy(ctx context.Context) (*domain.Obje return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-3m20c", "Errors.Assets.EmptyKey") - } +func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) { existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err @@ -136,8 +133,12 @@ func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, storageKey str if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Qw0pd", "Errors.IAM.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "INSTANCE-3m20c", "Errors.Assets.Object.PutFailed") + } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoAddedEvent(ctx, instanceAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoAddedEvent(ctx, instanceAgg, asset.Name)) if err != nil { return nil, err } @@ -158,7 +159,7 @@ func (c *Commands) RemoveLogoDefaultLabelPolicy(ctx context.Context) (*domain.Ob return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Xc8Kf", "Errors.IAM.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoKey) + err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoKey) if err != nil { return nil, err } @@ -174,10 +175,7 @@ func (c *Commands) RemoveLogoDefaultLabelPolicy(ctx context.Context) (*domain.Ob return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-yxE4f", "Errors.Assets.EmptyKey") - } +func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) { existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err @@ -186,8 +184,12 @@ func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, storageKey str if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1yMx0", "Errors.IAM.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "INSTANCE-yxE4f", "Errors.Assets.Object.PutFailed") + } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconAddedEvent(ctx, instanceAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconAddedEvent(ctx, instanceAgg, asset.Name)) if err != nil { return nil, err } @@ -207,7 +209,7 @@ func (c *Commands) RemoveIconDefaultLabelPolicy(ctx context.Context) (*domain.Ob if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-4M0qw", "Errors.IAM.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconKey) + err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconKey) if err != nil { return nil, err } @@ -223,20 +225,21 @@ func (c *Commands) RemoveIconDefaultLabelPolicy(ctx context.Context) (*domain.Ob return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-4fMs9", "Errors.Assets.EmptyKey") - } +func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) { existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-ZR9fs", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-ZR9fs", "Errors.Instance.LabelPolicy.NotFound") + } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "INSTANCE-4fMs9", "Errors.Assets.Object.PutFailed") } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoDarkAddedEvent(ctx, instanceAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoDarkAddedEvent(ctx, instanceAgg, asset.Name)) if err != nil { return nil, err } @@ -254,9 +257,9 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-3FGds", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-3FGds", "Errors.Instance.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoDarkKey) + err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoDarkKey) if err != nil { return nil, err } @@ -272,20 +275,21 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1cxM3", "Errors.Assets.EmptyKey") - } +func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) { existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-vMsf9", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-vMsf9", "Errors.Instance.LabelPolicy.NotFound") + } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "INSTANCE-1cxM3", "Errors.Assets.Object.PutFailed") } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconDarkAddedEvent(ctx, instanceAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconDarkAddedEvent(ctx, instanceAgg, asset.Name)) if err != nil { return nil, err } @@ -303,9 +307,9 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-2nc7F", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-2nc7F", "Errors.Instance.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconDarkKey) + err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconDarkKey) if err != nil { return nil, err } @@ -321,20 +325,21 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1N8fs", "Errors.Assets.EmptyKey") - } +func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) { existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) if err != nil { return nil, err } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1N8fE", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1N8fE", "Errors.Instance.LabelPolicy.NotFound") + } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(nil, "INSTANCE-1N8fs", "Errors.Assets.Object.PutFailed") } instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyFontAddedEvent(ctx, instanceAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyFontAddedEvent(ctx, instanceAgg, asset.Name)) if err != nil { return nil, err } @@ -352,9 +357,9 @@ func (c *Commands) RemoveFontDefaultLabelPolicy(ctx context.Context) (*domain.Ob } if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { - return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Tk0gw", "Errors.IAM.LabelPolicy.NotFound") + return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Tk0gw", "Errors.Instance.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.FontKey) + err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.FontKey) if err != nil { return nil, err } diff --git a/internal/command/instance_policy_label_test.go b/internal/command/instance_policy_label_test.go index 1a4a3a3704..8403084eb2 100644 --- a/internal/command/instance_policy_label_test.go +++ b/internal/command/instance_policy_label_test.go @@ -1,13 +1,14 @@ package command import ( + "bytes" "context" "testing" - "github.com/caos/zitadel/internal/api/authz" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" + "github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore" @@ -452,8 +453,8 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { storage static.Storage } type args struct { - ctx context.Context - storageKey string + ctx context.Context + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -465,20 +466,6 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { args args res res }{ - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "label policy not existing, not found error", fields: fields{ @@ -488,13 +475,61 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "text/css", + ObjectType: static.ObjectTypeStyling, + File: nil, + Size: 0, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewLabelPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "logo added, ok", fields: fields{ @@ -523,16 +558,24 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { eventFromEventPusher( instance.NewLabelPolicyLogoAddedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, - "key", + "logo", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -545,8 +588,9 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddLogoDefaultLabelPolicy(tt.args.ctx, tt.args.storageKey) + got, err := r.AddLogoDefaultLabelPolicy(tt.args.ctx, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -593,7 +637,6 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) { err: caos_errs.IsNotFound, }, }, - { name: "asset remove error, internal error", fields: fields{ @@ -708,10 +751,11 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - storageKey string + ctx context.Context + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -723,20 +767,6 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { args args res res }{ - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "label policy not existing, not found error", fields: fields{ @@ -746,13 +776,61 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewLabelPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "icon added, ok", fields: fields{ @@ -781,16 +859,24 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { eventFromEventPusher( instance.NewLabelPolicyIconAddedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, - "key", + "icon", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -803,8 +889,9 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddIconDefaultLabelPolicy(tt.args.ctx, tt.args.storageKey) + got, err := r.AddIconDefaultLabelPolicy(tt.args.ctx, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -926,11 +1013,12 @@ func TestCommandSide_RemoveIconDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { ctx context.Context instanceID string - storageKey string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -942,21 +1030,6 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { args args res res }{ - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - instanceID: "INSTANCE", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "label policy not existing, not found error", fields: fields{ @@ -968,12 +1041,61 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { args: args{ ctx: context.Background(), instanceID: "INSTANCE", - storageKey: "key", + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewLabelPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + instanceID: "INSTANCE", + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "logo dark added, ok", fields: fields{ @@ -1002,16 +1124,24 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { eventFromEventPusher( instance.NewLabelPolicyLogoDarkAddedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, - "key", + "logo", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1024,8 +1154,9 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddLogoDarkDefaultLabelPolicy(tt.args.ctx, tt.args.storageKey) + got, err := r.AddLogoDarkDefaultLabelPolicy(tt.args.ctx, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1186,10 +1317,11 @@ func TestCommandSide_RemoveLogoDarkDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - storageKey string + ctx context.Context + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1201,20 +1333,6 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { args args res res }{ - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "label policy not existing, not found error", fields: fields{ @@ -1224,13 +1342,61 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewLabelPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "icon dark added, ok", fields: fields{ @@ -1259,16 +1425,24 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { eventFromEventPusher( instance.NewLabelPolicyIconDarkAddedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, - "key", + "icon", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1281,8 +1455,9 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddIconDarkDefaultLabelPolicy(tt.args.ctx, tt.args.storageKey) + got, err := r.AddIconDarkDefaultLabelPolicy(tt.args.ctx, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1443,10 +1618,11 @@ func TestCommandSide_RemoveIconDarkDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - storageKey string + ctx context.Context + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1458,20 +1634,6 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { args args res res }{ - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, { name: "label policy not existing, not found error", fields: fields{ @@ -1481,13 +1643,61 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + instance.NewLabelPolicyAddedEvent(context.Background(), + &instance.NewAggregate("INSTANCE").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "font added, ok", fields: fields{ @@ -1516,16 +1726,24 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { eventFromEventPusher( instance.NewLabelPolicyFontAddedEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, - "key", + "font", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - storageKey: "key", + ctx: context.Background(), + upload: &AssetUpload{ + ResourceOwner: "IAM", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1538,8 +1756,9 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddFontDefaultLabelPolicy(tt.args.ctx, tt.args.storageKey) + got, err := r.AddFontDefaultLabelPolicy(tt.args.ctx, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/org_policy_label.go b/internal/command/org_policy_label.go index 62c2b14bc9..d31b8aa82f 100644 --- a/internal/command/org_policy_label.go +++ b/internal/command/org_policy_label.go @@ -6,6 +6,7 @@ import ( "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/repository/org" + "github.com/caos/zitadel/internal/static" ) func (c *Commands) AddLabelPolicy(ctx context.Context, resourceOwner string, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) { @@ -150,13 +151,10 @@ func (c *Commands) ActivateLabelPolicy(ctx context.Context, orgID string) (*doma return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if orgID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-KKd4X", "Errors.ResourceOwnerMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4N3nf", "Errors.Assets.EmptyKey") - } existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -165,8 +163,12 @@ func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID, storageKey str if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-23BMs", "Errors.Org.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "IAM-4N3nf", "Errors.Assets.Object.PutFailed") + } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoAddedEvent(ctx, orgAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoAddedEvent(ctx, orgAgg, asset.Name)) if err != nil { return nil, err } @@ -189,7 +191,7 @@ func (c *Commands) RemoveLogoLabelPolicy(ctx context.Context, orgID string) (*do if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-4MVsf", "Errors.Org.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, orgID, existingPolicy.LogoKey) + err = c.removeAsset(ctx, orgID, existingPolicy.LogoKey) if err != nil { return nil, err } @@ -205,13 +207,10 @@ func (c *Commands) RemoveLogoLabelPolicy(ctx context.Context, orgID string) (*do return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if orgID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-hMDs3", "Errors.ResourceOwnerMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4BS7f", "Errors.Assets.EmptyKey") - } existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -220,8 +219,12 @@ func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID, storageKey str if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-4nq2f", "Errors.Org.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "IAM-4BS7f", "Errors.Assets.Object.PutFailed") + } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconAddedEvent(ctx, orgAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconAddedEvent(ctx, orgAgg, asset.Name)) if err != nil { return nil, err } @@ -245,7 +248,7 @@ func (c *Commands) RemoveIconLabelPolicy(ctx context.Context, orgID string) (*do return nil, caos_errs.ThrowNotFound(nil, "ORG-1nd9f", "Errors.Org.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, orgID, existingPolicy.IconKey) + err = c.removeAsset(ctx, orgID, existingPolicy.IconKey) if err != nil { return nil, err } @@ -261,13 +264,10 @@ func (c *Commands) RemoveIconLabelPolicy(ctx context.Context, orgID string) (*do return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if orgID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-67Ms2", "Errors.ResourceOwnerMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3S7fN", "Errors.Assets.EmptyKey") - } existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -276,8 +276,12 @@ func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID, storageKey if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-QSqcd", "Errors.Org.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "IAM-3S7fN", "Errors.Assets.Object.PutFailed") + } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoDarkAddedEvent(ctx, orgAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoDarkAddedEvent(ctx, orgAgg, asset.Name)) if err != nil { return nil, err } @@ -300,7 +304,7 @@ func (c *Commands) RemoveLogoDarkLabelPolicy(ctx context.Context, orgID string) if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-0peQw", "Errors.Org.LabelPolicy.NotFound") } - err = c.RemoveAsset(ctx, orgID, existingPolicy.LogoDarkKey) + err = c.removeAsset(ctx, orgID, existingPolicy.LogoDarkKey) if err != nil { return nil, err } @@ -316,13 +320,10 @@ func (c *Commands) RemoveLogoDarkLabelPolicy(ctx context.Context, orgID string) return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if orgID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-tzBfs", "Errors.ResourceOwnerMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4B7cs", "Errors.Assets.EmptyKey") - } existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -331,8 +332,12 @@ func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID, storageKey if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-4Nf8s", "Errors.Org.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "IAM-4B7cs", "Errors.Assets.Object.PutFailed") + } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconDarkAddedEvent(ctx, orgAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconDarkAddedEvent(ctx, orgAgg, asset.Name)) if err != nil { return nil, err } @@ -367,13 +372,10 @@ func (c *Commands) RemoveIconDarkLabelPolicy(ctx context.Context, orgID string) return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil } -func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if orgID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "Org-1Nf9s", "Errors.ResourceOwnerMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2f9fw", "Errors.Assets.EmptyKey") - } existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID) if err != nil { return nil, err @@ -382,8 +384,12 @@ func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID, storageKey str if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { return nil, caos_errs.ThrowNotFound(nil, "ORG-2M9fs", "Errors.Org.LabelPolicy.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "ORG-2f9fw", "Errors.Assets.Object.PutFailed") + } orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyFontAddedEvent(ctx, orgAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyFontAddedEvent(ctx, orgAgg, asset.Name)) if err != nil { return nil, err } @@ -447,7 +453,7 @@ func (c *Commands) removeLabelPolicy(ctx context.Context, existingPolicy *OrgLab return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LabelPolicy.NotFound") } - err = c.RemoveAssetsFolder(ctx, existingPolicy.AggregateID, domain.LabelPolicyPrefix+"/", true) + err = c.removeAssetsFolder(ctx, existingPolicy.AggregateID, static.ObjectTypeStyling) if err != nil { return nil, err } @@ -463,7 +469,7 @@ func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string) if existingPolicy.State != domain.PolicyStateActive { return nil, nil } - err = c.RemoveAssetsFolder(ctx, orgID, domain.LabelPolicyPrefix+"/", true) + err = c.removeAssetsFolder(ctx, orgID, static.ObjectTypeStyling) if err != nil { return nil, err } @@ -472,7 +478,7 @@ func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string) } func (c *Commands) removeLabelPolicyAssets(ctx context.Context, existingPolicy *OrgLabelPolicyWriteModel) (*org.LabelPolicyAssetsRemovedEvent, error) { - err := c.RemoveAssetsFolder(ctx, existingPolicy.AggregateID, domain.LabelPolicyPrefix+"/", true) + err := c.removeAssetsFolder(ctx, existingPolicy.AggregateID, static.ObjectTypeStyling) if err != nil { return nil, err } diff --git a/internal/command/org_policy_label_test.go b/internal/command/org_policy_label_test.go index eeb628e388..a977e7f2a8 100644 --- a/internal/command/org_policy_label_test.go +++ b/internal/command/org_policy_label_test.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "context" "testing" @@ -782,9 +783,9 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { storage static.Storage } type args struct { - ctx context.Context - orgID string - storageKey string + ctx context.Context + orgID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -803,24 +804,17 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { t, ), }, - args: args{ - ctx: context.Background(), - storageKey: "key", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, args: args{ ctx: context.Background(), - orgID: "org1", + orgID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -835,14 +829,63 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "logo added, ok", fields: fields{ @@ -871,17 +914,25 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyLogoAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, - "key", + "logo", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -896,7 +947,7 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) { eventstore: tt.fields.eventstore, static: tt.fields.storage, } - got, err := r.AddLogoLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.storageKey) + got, err := r.AddLogoLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1039,11 +1090,12 @@ func TestCommandSide_RemoveLogoLabelPolicy(t *testing.T) { func TestCommandSide_AddIconLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - orgID string - storageKey string + ctx context.Context + orgID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1062,24 +1114,17 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { t, ), }, - args: args{ - ctx: context.Background(), - storageKey: "key", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, args: args{ ctx: context.Background(), - orgID: "org1", + orgID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -1094,14 +1139,63 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "icon added, ok", fields: fields{ @@ -1130,17 +1224,25 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyIconAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, - "key", + "icon", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1153,8 +1255,9 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddIconLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.storageKey) + got, err := r.AddIconLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1294,11 +1397,12 @@ func TestCommandSide_RemoveIconLabelPolicy(t *testing.T) { func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - orgID string - storageKey string + ctx context.Context + orgID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1319,22 +1423,15 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { }, args: args{ ctx: context.Background(), - orgID: "org1", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, - args: args{ - ctx: context.Background(), - orgID: "org1", + orgID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -1349,14 +1446,63 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "logo dark added, ok", fields: fields{ @@ -1385,17 +1531,25 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyLogoDarkAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, - "key", + "logo", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "logo", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1408,8 +1562,9 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddLogoDarkLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.storageKey) + got, err := r.AddLogoDarkLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1555,9 +1710,9 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { storage static.Storage } type args struct { - ctx context.Context - orgID string - storageKey string + ctx context.Context + orgID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1576,24 +1731,17 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { t, ), }, - args: args{ - ctx: context.Background(), - storageKey: "key", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, args: args{ ctx: context.Background(), - orgID: "org1", + orgID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -1608,14 +1756,63 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "icon dark added, ok", fields: fields{ @@ -1644,17 +1841,25 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyIconDarkAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, - "key", + "icon", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "icon", + ContentType: "image", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1667,8 +1872,9 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddIconDarkLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.storageKey) + got, err := r.AddIconDarkLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } @@ -1806,11 +2012,12 @@ func TestCommandSide_RemoveIconDarkLabelPolicy(t *testing.T) { func TestCommandSide_AddFontLabelPolicy(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - orgID string - storageKey string + ctx context.Context + orgID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -1829,24 +2036,17 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { t, ), }, - args: args{ - ctx: context.Background(), - storageKey: "key", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, args: args{ ctx: context.Background(), - orgID: "org1", + orgID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -1861,14 +2061,55 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", }, res: res{ err: caos_errs.IsNotFound, }, }, + { + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + org.NewLabelPolicyAddedEvent(context.Background(), + &org.NewAggregate("org1", "org1").Aggregate, + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + "#ffffff", + true, + true, + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, { name: "font added, ok", fields: fields{ @@ -1897,17 +2138,25 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { eventFromEventPusher( org.NewLabelPolicyFontAddedEvent(context.Background(), &org.NewAggregate("org1", "org1").Aggregate, - "key", + "font", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "font", + ContentType: "ttf", + ObjectType: static.ObjectTypeStyling, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -1920,8 +2169,9 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddFontLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.storageKey) + got, err := r.AddFontLabelPolicy(tt.args.ctx, tt.args.orgID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/statics.go b/internal/command/statics.go index cfe9c9fd4a..84150b5474 100644 --- a/internal/command/statics.go +++ b/internal/command/statics.go @@ -1,27 +1,70 @@ package command import ( + "bytes" "context" "io" + "strings" - "github.com/caos/zitadel/internal/domain" + "github.com/superseriousbusiness/exifremove/pkg/exifremove" + + "github.com/caos/zitadel/internal/api/authz" + "github.com/caos/zitadel/internal/static" ) -func (c *Commands) UploadAsset(ctx context.Context, bucketName, objectName, contentType string, file io.Reader, size int64) (*domain.AssetInfo, error) { +type AssetUpload struct { + ResourceOwner string + ObjectName string + ContentType string + ObjectType static.ObjectType + File io.Reader + Size int64 +} + +func (c *Commands) uploadAsset(ctx context.Context, upload *AssetUpload) (*static.Asset, error) { + //TODO: handle location as soon as possible + file, size, err := removeExif(upload.File, upload.Size, upload.ContentType) + if err != nil { + return nil, err + } return c.static.PutObject(ctx, - bucketName, - objectName, - contentType, + authz.GetInstance(ctx).InstanceID(), + "", + upload.ResourceOwner, + upload.ObjectName, + upload.ContentType, + upload.ObjectType, file, size, - true, ) } -func (c *Commands) RemoveAsset(ctx context.Context, bucketName, storeKey string) error { - return c.static.RemoveObject(ctx, bucketName, storeKey) +func (c *Commands) removeAsset(ctx context.Context, resourceOwner, storeKey string) error { + return c.static.RemoveObject(ctx, authz.GetInstance(ctx).InstanceID(), resourceOwner, storeKey) } -func (c *Commands) RemoveAssetsFolder(ctx context.Context, bucketName, path string, recursive bool) error { - return c.static.RemoveObjects(ctx, bucketName, path, recursive) +func (c *Commands) removeAssetsFolder(ctx context.Context, resourceOwner string, objectType static.ObjectType) error { + return c.static.RemoveObjects(ctx, authz.GetInstance(ctx).InstanceID(), resourceOwner, objectType) +} + +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") } diff --git a/internal/command/user_human_avatar.go b/internal/command/user_human_avatar.go index 8fcc92e56d..c6ac4acff9 100644 --- a/internal/command/user_human_avatar.go +++ b/internal/command/user_human_avatar.go @@ -8,13 +8,10 @@ import ( "github.com/caos/zitadel/internal/repository/user" ) -func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID, storageKey string) (*domain.ObjectDetails, error) { +func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID string, upload *AssetUpload) (*domain.ObjectDetails, error) { if userID == "" { return nil, caos_errs.ThrowInvalidArgument(nil, "USER-Ba5Ds", "Errors.IDMissing") } - if storageKey == "" { - return nil, caos_errs.ThrowInvalidArgument(nil, "USER-1Xyud", "Errors.Assets.EmptyKey") - } existingUser, err := c.userWriteModelByID(ctx, userID, orgID) if err != nil { return nil, err @@ -23,8 +20,12 @@ func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID, storageKey if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted { return nil, caos_errs.ThrowNotFound(nil, "USER-vJ3fS", "Errors.Users.NotFound") } + asset, err := c.uploadAsset(ctx, upload) + if err != nil { + return nil, caos_errs.ThrowInternal(err, "USER-1Xyud", "Errors.Assets.Object.PutFailed") + } userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel) - pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanAvatarAddedEvent(ctx, userAgg, storageKey)) + pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanAvatarAddedEvent(ctx, userAgg, asset.Name)) if err != nil { return nil, err } @@ -46,7 +47,7 @@ func (c *Commands) RemoveHumanAvatar(ctx context.Context, orgID, userID string) if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted { return nil, caos_errs.ThrowNotFound(nil, "USER-35N8f", "Errors.Users.NotFound") } - err = c.RemoveAsset(ctx, orgID, existingUser.Avatar) + err = c.removeAsset(ctx, orgID, existingUser.Avatar) if err != nil { return nil, err } diff --git a/internal/command/user_human_avatar_test.go b/internal/command/user_human_avatar_test.go index ea2d31e327..2875ecd62a 100644 --- a/internal/command/user_human_avatar_test.go +++ b/internal/command/user_human_avatar_test.go @@ -1,6 +1,7 @@ package command import ( + "bytes" "context" "testing" @@ -20,12 +21,13 @@ import ( func TestCommandSide_AddHumanAvatar(t *testing.T) { type fields struct { eventstore *eventstore.Eventstore + storage static.Storage } type args struct { - ctx context.Context - orgID string - userID string - storageKey string + ctx context.Context + orgID string + userID string + upload *AssetUpload } type res struct { want *domain.ObjectDetails @@ -44,25 +46,18 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) { t, ), }, - args: args{ - ctx: context.Background(), - storageKey: "key", - }, - res: res{ - err: caos_errs.IsErrorInvalidArgument, - }, - }, - { - name: "storage key empty, invalid argument error", - fields: fields{ - eventstore: eventstoreExpect( - t, - ), - }, args: args{ ctx: context.Background(), - orgID: "org1", - userID: "user1", + orgID: "", + userID: "", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "avatar", + ContentType: "image", + ObjectType: static.ObjectTypeUserAvatar, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsErrorInvalidArgument, @@ -77,17 +72,65 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) { ), }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + userID: "user1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "avatar", + ContentType: "image", + ObjectType: static.ObjectTypeUserAvatar, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ err: caos_errs.IsNotFound, }, }, { - name: "logo added, ok", + name: "upload failed, error", + fields: fields{ + eventstore: eventstoreExpect( + t, + expectFilter( + eventFromEventPusher( + user.NewHumanAddedEvent(context.Background(), + &user.NewAggregate("user1", "org1").Aggregate, + "username", + "firstname", + "lastname", + "nickname", + "displayname", + language.Und, + domain.GenderUnspecified, + "email@test.ch", + true, + ), + ), + ), + ), + storage: mock.NewStorage(t).ExpectPutObjectError(), + }, + args: args{ + ctx: context.Background(), + orgID: "org1", + userID: "user1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "avatar", + ContentType: "image", + ObjectType: static.ObjectTypeUserAvatar, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, + }, + res: res{ + err: caos_errs.IsInternal, + }, + }, + { + name: "avatar added, ok", fields: fields{ eventstore: eventstoreExpect( t, @@ -112,18 +155,26 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) { eventFromEventPusher( user.NewHumanAvatarAddedEvent(context.Background(), &user.NewAggregate("user1", "org1").Aggregate, - "key", + "avatar", ), ), }, ), ), + storage: mock.NewStorage(t).ExpectPutObject(), }, args: args{ - ctx: context.Background(), - orgID: "org1", - userID: "user1", - storageKey: "key", + ctx: context.Background(), + orgID: "org1", + userID: "user1", + upload: &AssetUpload{ + ResourceOwner: "org1", + ObjectName: "avatar", + ContentType: "image", + ObjectType: static.ObjectTypeUserAvatar, + File: bytes.NewReader([]byte("test")), + Size: 4, + }, }, res: res{ want: &domain.ObjectDetails{ @@ -136,8 +187,9 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := &Commands{ eventstore: tt.fields.eventstore, + static: tt.fields.storage, } - got, err := r.AddHumanAvatar(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.storageKey) + got, err := r.AddHumanAvatar(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.upload) if tt.res.err == nil { assert.NoError(t, err) } diff --git a/internal/command/v2/instance.go b/internal/command/v2/instance.go index 60b2f66055..ba8fdfc6d9 100644 --- a/internal/command/v2/instance.go +++ b/internal/command/v2/instance.go @@ -27,8 +27,32 @@ const ( ) type InstanceSetup struct { - Org OrgSetup - Zitadel ZitadelConfig + Org OrgSetup + Zitadel ZitadelConfig + Features struct { + TierName string + TierDescription string + Retention time.Duration + State domain.FeaturesState + StateDescription string + LoginPolicyFactors bool + LoginPolicyIDP bool + LoginPolicyPasswordless bool + LoginPolicyRegistration bool + LoginPolicyUsernameLogin bool + LoginPolicyPasswordReset bool + PasswordComplexityPolicy bool + LabelPolicyPrivateLabel bool + LabelPolicyWatermark bool + CustomDomain bool + PrivacyPolicy bool + MetadataUser bool + CustomTextMessage bool + CustomTextLogin bool + LockoutPolicy bool + ActionsAllowed domain.ActionsAllowed + MaxActions int + } PasswordComplexityPolicy struct { MinLength uint64 HasLowercase bool @@ -170,6 +194,31 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup) projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID) validations := []preparation.Validation{ + SetDefaultFeatures( + instanceAgg, + setup.Features.TierName, + setup.Features.TierDescription, + setup.Features.State, + setup.Features.StateDescription, + setup.Features.Retention, + setup.Features.LoginPolicyFactors, + setup.Features.LoginPolicyIDP, + setup.Features.LoginPolicyPasswordless, + setup.Features.LoginPolicyRegistration, + setup.Features.LoginPolicyUsernameLogin, + setup.Features.LoginPolicyPasswordReset, + setup.Features.PasswordComplexityPolicy, + setup.Features.LabelPolicyPrivateLabel, + setup.Features.LabelPolicyWatermark, + setup.Features.CustomDomain, + setup.Features.PrivacyPolicy, + setup.Features.MetadataUser, + setup.Features.CustomTextMessage, + setup.Features.CustomTextLogin, + setup.Features.LockoutPolicy, + setup.Features.ActionsAllowed, + setup.Features.MaxActions, + ), AddPasswordComplexityPolicy( instanceAgg, setup.PasswordComplexityPolicy.MinLength, diff --git a/internal/command/v2/instance_features.go b/internal/command/v2/instance_features.go new file mode 100644 index 0000000000..c175b2aab6 --- /dev/null +++ b/internal/command/v2/instance_features.go @@ -0,0 +1,95 @@ +package command + +import ( + "context" + "time" + + "github.com/caos/zitadel/internal/command" + "github.com/caos/zitadel/internal/command/v2/preparation" + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/instance" +) + +func SetDefaultFeatures( + a *instance.Aggregate, + tierName, + tierDescription string, + state domain.FeaturesState, + stateDescription string, + retention time.Duration, + loginPolicyFactors, + loginPolicyIDP, + loginPolicyPasswordless, + loginPolicyRegistration, + loginPolicyUsernameLogin, + loginPolicyPasswordReset, + passwordComplexityPolicy, + labelPolicyPrivateLabel, + labelPolicyWatermark, + customDomain, + privacyPolicy, + metadataUser, + customTextMessage, + customTextLogin, + lockoutPolicy bool, + actionsAllowed domain.ActionsAllowed, + maxActions int, +) preparation.Validation { + return func() (preparation.CreateCommands, error) { + if !state.Valid() || state == domain.FeaturesStateUnspecified || state == domain.FeaturesStateRemoved { + return nil, errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument") + } + return func(ctx context.Context, filter preparation.FilterToQueryReducer) ([]eventstore.Command, error) { + writeModel, err := defaultFeatures(ctx, filter) + if err != nil { + return nil, err + } + event, hasChanged := writeModel.NewSetEvent(ctx, &a.Aggregate, + tierName, + tierDescription, + state, + stateDescription, + retention, + loginPolicyFactors, + loginPolicyIDP, + loginPolicyPasswordless, + loginPolicyRegistration, + loginPolicyUsernameLogin, + loginPolicyPasswordReset, + passwordComplexityPolicy, + labelPolicyPrivateLabel, + labelPolicyWatermark, + customDomain, + privacyPolicy, + metadataUser, + customTextMessage, + customTextLogin, + lockoutPolicy, + actionsAllowed, + maxActions, + ) + if !hasChanged { + return nil, errors.ThrowPreconditionFailed(nil, "INSTA-GE4h2", "Errors.Features.NotChanged") + } + return []eventstore.Command{ + event, + }, nil + }, nil + } +} + +func defaultFeatures(ctx context.Context, filter preparation.FilterToQueryReducer) (*command.InstanceFeaturesWriteModel, error) { + features := command.NewInstanceFeaturesWriteModel(ctx) + events, err := filter(ctx, features.Query()) + if err != nil { + return nil, err + } + if len(events) == 0 { + return features, nil + } + features.AppendEvents(events...) + err = features.Reduce() + return features, err +} diff --git a/internal/command/v2/instance_features_test.go b/internal/command/v2/instance_features_test.go new file mode 100644 index 0000000000..277ba521d2 --- /dev/null +++ b/internal/command/v2/instance_features_test.go @@ -0,0 +1,152 @@ +package command + +import ( + "context" + "testing" + "time" + + "github.com/caos/zitadel/internal/domain" + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/eventstore" + "github.com/caos/zitadel/internal/repository/features" + "github.com/caos/zitadel/internal/repository/instance" +) + +func TestSetDefaultFeatures(t *testing.T) { + type args struct { + a *instance.Aggregate + tierName string + tierDescription string + state domain.FeaturesState + stateDescription string + retention time.Duration + loginPolicyFactors bool + loginPolicyIDP bool + loginPolicyPasswordless bool + loginPolicyRegistration bool + loginPolicyUsernameLogin bool + loginPolicyPasswordReset bool + passwordComplexityPolicy bool + labelPolicyPrivateLabel bool + labelPolicyWatermark bool + customDomain bool + privacyPolicy bool + metadataUser bool + customTextMessage bool + customTextLogin bool + lockoutPolicy bool + actionsAllowed domain.ActionsAllowed + maxActions int + } + tests := []struct { + name string + args args + want Want + }{ + { + name: "invalid state", + args: args{ + a: instance.NewAggregate("INSTANCE"), + tierName: "", + tierDescription: "", + state: 0, + stateDescription: "", + retention: 0, + loginPolicyFactors: false, + loginPolicyIDP: false, + loginPolicyPasswordless: false, + loginPolicyRegistration: false, + loginPolicyUsernameLogin: false, + loginPolicyPasswordReset: false, + passwordComplexityPolicy: false, + labelPolicyPrivateLabel: false, + labelPolicyWatermark: false, + customDomain: false, + privacyPolicy: false, + metadataUser: false, + customTextMessage: false, + customTextLogin: false, + lockoutPolicy: false, + actionsAllowed: 0, + maxActions: 0, + }, + want: Want{ + ValidationErr: errors.ThrowInvalidArgument(nil, "INSTA-d3r1s", "Errors.Invalid.Argument"), + }, + }, + { + name: "correct", + args: args{ + a: instance.NewAggregate("INSTANCE"), + tierName: "", + tierDescription: "", + state: domain.FeaturesStateActive, + stateDescription: "", + retention: 0, + loginPolicyFactors: false, + loginPolicyIDP: false, + loginPolicyPasswordless: false, + loginPolicyRegistration: false, + loginPolicyUsernameLogin: false, + loginPolicyPasswordReset: false, + passwordComplexityPolicy: false, + labelPolicyPrivateLabel: false, + labelPolicyWatermark: false, + customDomain: false, + privacyPolicy: false, + metadataUser: false, + customTextMessage: false, + customTextLogin: false, + lockoutPolicy: false, + actionsAllowed: 0, + maxActions: 0, + }, + want: Want{ + Commands: []eventstore.Command{ + func() *instance.FeaturesSetEvent { + event, _ := instance.NewFeaturesSetEvent(context.Background(), &instance.NewAggregate("INSTANCE").Aggregate, + []features.FeaturesChanges{ + features.ChangeState(domain.FeaturesStateActive), + }, + ) + return event + }(), + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + AssertValidation(t, SetDefaultFeatures( + tt.args.a, + tt.args.tierName, + tt.args.tierDescription, + tt.args.state, + tt.args.stateDescription, + tt.args.retention, + tt.args.loginPolicyFactors, + tt.args.loginPolicyIDP, + tt.args.loginPolicyPasswordless, + tt.args.loginPolicyRegistration, + tt.args.loginPolicyUsernameLogin, + tt.args.loginPolicyPasswordReset, + tt.args.passwordComplexityPolicy, + tt.args.labelPolicyPrivateLabel, + tt.args.labelPolicyWatermark, + tt.args.customDomain, + tt.args.privacyPolicy, + tt.args.metadataUser, + tt.args.customTextMessage, + tt.args.customTextLogin, + tt.args.lockoutPolicy, + tt.args.actionsAllowed, + tt.args.maxActions, + ), NewMultiFilter(). + Append(func(ctx context.Context, queryFactory *eventstore.SearchQueryBuilder) ([]eventstore.Event, error) { + return nil, nil + }). + Filter(), + tt.want) + }) + } +} diff --git a/internal/query/projection/feature.go b/internal/query/projection/feature.go index 94de9a8b81..145d365df4 100644 --- a/internal/query/projection/feature.go +++ b/internal/query/projection/feature.go @@ -62,9 +62,9 @@ func NewFeatureProjection(ctx context.Context, config crdb.StatementHandlerConfi crdb.NewColumn(FeatureSequenceCol, crdb.ColumnTypeInt64), crdb.NewColumn(FeatureIsDefaultCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(FeatureTierNameCol, crdb.ColumnTypeText), - crdb.NewColumn(FeatureTierDescriptionCol, crdb.ColumnTypeText), + crdb.NewColumn(FeatureTierDescriptionCol, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(FeatureStateCol, crdb.ColumnTypeEnum, crdb.Default(0)), - crdb.NewColumn(FeatureStateDescriptionCol, crdb.ColumnTypeText), + crdb.NewColumn(FeatureStateDescriptionCol, crdb.ColumnTypeText, crdb.Nullable()), crdb.NewColumn(FeatureAuditLogRetentionCol, crdb.ColumnTypeInt64, crdb.Default(0)), crdb.NewColumn(FeatureLoginPolicyFactorsCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(FeatureLoginPolicyIDPCol, crdb.ColumnTypeBool, crdb.Default(false)), diff --git a/internal/static/config/config.go b/internal/static/config/config.go index d5d62c6f56..0e4a069e66 100644 --- a/internal/static/config/config.go +++ b/internal/static/config/config.go @@ -1,114 +1,30 @@ package config import ( - "context" - "encoding/json" - "io" - "net/url" - "time" + "database/sql" - "github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/static" + "github.com/caos/zitadel/internal/static/database" "github.com/caos/zitadel/internal/static/s3" ) type AssetStorageConfig struct { Type string - Config static.Config + Config map[string]interface{} `mapstructure:",remain"` } -var storage = map[string]func() static.Config{ - "s3": func() static.Config { return &s3.Config{} }, - "none": func() static.Config { return &NoStorage{} }, - "": func() static.Config { return &NoStorage{} }, -} - -func (c *AssetStorageConfig) UnmarshalJSON(data []byte) error { - var rc struct { - Type string - Config json.RawMessage - } - - if err := json.Unmarshal(data, &rc); err != nil { - return errors.ThrowInternal(err, "STATIC-Bfn5r", "error parsing config") - } - - c.Type = rc.Type - - var err error - c.Config, err = newStorageConfig(c.Type, rc.Config) - if err != nil { - return err - } - - return nil -} - -func newStorageConfig(storageType string, configData []byte) (static.Config, error) { - t, ok := storage[storageType] +func (a *AssetStorageConfig) NewStorage(client *sql.DB) (static.Storage, error) { + t, ok := storage[a.Type] if !ok { - return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", storageType) + return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", a.Type) } - staticConfig := t() - if len(configData) == 0 { - return staticConfig, nil - } - - if err := json.Unmarshal(configData, staticConfig); err != nil { - return nil, errors.ThrowInternal(err, "STATIC-GB4nw", "Could not read config: %v") - } - - return staticConfig, nil + return t(client, a.Config) } -var ( - errNoStorage = errors.ThrowInternal(nil, "STATIC-ashg4", "Errors.Assets.Store.NotConfigured") -) - -type NoStorage struct{} - -func (_ *NoStorage) NewStorage() (static.Storage, error) { - return &NoStorage{}, nil -} - -func (_ *NoStorage) CreateBucket(ctx context.Context, name, location string) error { - return errNoStorage -} - -func (_ *NoStorage) RemoveBucket(ctx context.Context, name string) error { - return errNoStorage -} - -func (_ *NoStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) { - return nil, errNoStorage -} - -func (_ *NoStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) { - return nil, errNoStorage -} - -func (_ *NoStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) { - return nil, errNoStorage -} - -func (_ *NoStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) { - return nil, nil, errNoStorage -} - -func (_ *NoStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) { - return nil, errNoStorage -} - -func (_ *NoStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) { - return nil, errNoStorage -} - -func (_ *NoStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error { - return errNoStorage -} - -func (_ *NoStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error { - return errNoStorage +var storage = map[string]static.CreateStorage{ + "db": database.NewStorage, + "": database.NewStorage, + "s3": s3.NewStorage, } diff --git a/internal/static/database/crdb.go b/internal/static/database/crdb.go new file mode 100644 index 0000000000..811a29aabf --- /dev/null +++ b/internal/static/database/crdb.go @@ -0,0 +1,182 @@ +package database + +import ( + "context" + "database/sql" + errs "errors" + "fmt" + "io" + "time" + + "github.com/Masterminds/squirrel" + + caos_errors "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/static" +) + +var _ static.Storage = (*crdbStorage)(nil) + +const ( + assetsTable = "system.assets" + AssetColInstanceID = "instance_id" + AssetColType = "asset_type" + AssetColLocation = "location" + AssetColResourceOwner = "resource_owner" + AssetColName = "name" + AssetColData = "data" + AssetColContentType = "content_type" + AssetColHash = "hash" + AssetColUpdatedAt = "updated_at" +) + +type crdbStorage struct { + client *sql.DB +} + +func NewStorage(client *sql.DB, _ map[string]interface{}) (static.Storage, error) { + return &crdbStorage{client: client}, nil +} + +func (c *crdbStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { + data, err := io.ReadAll(object) + if err != nil { + return nil, caos_errors.ThrowInternal(err, "DATAB-Dfwvq", "Errors.Internal") + } + stmt, args, err := squirrel.Insert(assetsTable). + Columns(AssetColInstanceID, AssetColResourceOwner, AssetColName, AssetColType, AssetColContentType, AssetColData, AssetColUpdatedAt). + Values(instanceID, resourceOwner, name, objectType, contentType, data, "now()"). + Suffix(fmt.Sprintf( + "ON CONFLICT (%s, %s, %s) DO UPDATE"+ + " SET %s = $5, %s = $6"+ + " RETURNING %s, %s", AssetColInstanceID, AssetColResourceOwner, AssetColName, AssetColContentType, AssetColData, AssetColHash, AssetColUpdatedAt)). + PlaceholderFormat(squirrel.Dollar). + ToSql() + if err != nil { + return nil, caos_errors.ThrowInternal(err, "DATAB-32DG1", "Errors.Internal") + } + var hash string + var updatedAt time.Time + err = c.client.QueryRowContext(ctx, stmt, args...).Scan(&hash, &updatedAt) + if err != nil { + return nil, caos_errors.ThrowInternal(err, "DATAB-D2g2q", "Errors.Internal") + } + return &static.Asset{ + InstanceID: instanceID, + Name: name, + Hash: hash, + Size: objectSize, + LastModified: updatedAt, + Location: location, + ContentType: contentType, + }, nil +} + +func (c *crdbStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { + query, args, err := squirrel.Select(AssetColData, AssetColContentType, AssetColHash, AssetColUpdatedAt). + From(assetsTable). + Where(squirrel.Eq{ + AssetColInstanceID: instanceID, + AssetColResourceOwner: resourceOwner, + AssetColName: name, + }). + PlaceholderFormat(squirrel.Dollar). + ToSql() + if err != nil { + return nil, nil, caos_errors.ThrowInternal(err, "DATAB-GE3hz", "Errors.Internal") + } + var data []byte + asset := &static.Asset{ + InstanceID: instanceID, + ResourceOwner: resourceOwner, + Name: name, + } + err = c.client.QueryRowContext(ctx, query, args...). + Scan( + &data, + &asset.ContentType, + &asset.Hash, + &asset.LastModified, + ) + if err != nil { + if errs.Is(err, sql.ErrNoRows) { + return nil, nil, caos_errors.ThrowNotFound(err, "DATAB-pCP8P", "Errors.Assets.Object.NotFound") + } + return nil, nil, caos_errors.ThrowInternal(err, "DATAB-Sfgb3", "Errors.Assets.Object.GetFailed") + } + asset.Size = int64(len(data)) + return data, + func() (*static.Asset, error) { + return asset, nil + }, + nil +} + +func (c *crdbStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { + query, args, err := squirrel.Select(AssetColContentType, AssetColLocation, "length("+AssetColData+")", AssetColHash, AssetColUpdatedAt). + From(assetsTable). + Where(squirrel.Eq{ + AssetColInstanceID: instanceID, + AssetColResourceOwner: resourceOwner, + AssetColName: name, + }). + PlaceholderFormat(squirrel.Dollar). + ToSql() + if err != nil { + return nil, caos_errors.ThrowInternal(err, "DATAB-rggt2", "Errors.Internal") + } + asset := &static.Asset{ + InstanceID: instanceID, + ResourceOwner: resourceOwner, + Name: name, + } + err = c.client.QueryRowContext(ctx, query, args...). + Scan( + &asset.ContentType, + &asset.Location, + &asset.Size, + &asset.Hash, + &asset.LastModified, + ) + if err != nil { + return nil, caos_errors.ThrowInternal(err, "DATAB-Dbh2s", "Errors.Internal") + } + return asset, nil +} + +func (c *crdbStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { + stmt, args, err := squirrel.Delete(assetsTable). + Where(squirrel.Eq{ + AssetColInstanceID: instanceID, + AssetColResourceOwner: resourceOwner, + AssetColName: name, + }). + PlaceholderFormat(squirrel.Dollar). + ToSql() + if err != nil { + return caos_errors.ThrowInternal(err, "DATAB-Sgvwq", "Errors.Internal") + } + _, err = c.client.ExecContext(ctx, stmt, args...) + if err != nil { + return caos_errors.ThrowInternal(err, "DATAB-RHNgf", "Errors.Assets.Object.RemoveFailed") + } + return nil +} + +func (c *crdbStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { + stmt, args, err := squirrel.Delete(assetsTable). + Where(squirrel.Eq{ + AssetColInstanceID: instanceID, + AssetColResourceOwner: resourceOwner, + AssetColType: objectType, + }). + PlaceholderFormat(squirrel.Dollar). + ToSql() + if err != nil { + return caos_errors.ThrowInternal(err, "DATAB-Sfgeq", "Errors.Internal") + } + _, err = c.client.ExecContext(ctx, stmt, args...) + if err != nil { + return caos_errors.ThrowInternal(err, "DATAB-Efgt2", "Errors.Assets.Object.RemoveFailed") + } + return nil +} diff --git a/internal/static/database/crdb_test.go b/internal/static/database/crdb_test.go new file mode 100644 index 0000000000..e9f001b82e --- /dev/null +++ b/internal/static/database/crdb_test.go @@ -0,0 +1,203 @@ +package database + +import ( + "bytes" + "context" + "database/sql" + "database/sql/driver" + "io" + "reflect" + "regexp" + "testing" + "time" + + "github.com/DATA-DOG/go-sqlmock" + + "github.com/caos/zitadel/internal/static" +) + +var ( + testNow = time.Now() +) + +const ( + objectStmt = "INSERT INTO system.assets" + + " (instance_id,resource_owner,name,asset_type,content_type,data,updated_at)" + + " VALUES ($1,$2,$3,$4,$5,$6,$7)" + + " ON CONFLICT (instance_id, resource_owner, name) DO UPDATE SET" + + " content_type = $5, data = $6" + + " RETURNING hash" +) + +func Test_crdbStorage_CreateObject(t *testing.T) { + type fields struct { + client db + } + type args struct { + ctx context.Context + instanceID string + location string + resourceOwner string + name string + contentType string + objectType static.ObjectType + data io.Reader + objectSize int64 + } + tests := []struct { + name string + fields fields + args args + want *static.Asset + wantErr bool + }{ + { + "create ok", + fields{ + client: prepareDB(t, + expectQuery( + objectStmt, + []string{ + "hash", + "updated_at", + }, + [][]driver.Value{ + { + "md5Hash", + testNow, + }, + }, + "instanceID", + "resourceOwner", + "name", + static.ObjectTypeUserAvatar, + "contentType", + []byte("test"), + "now()", + )), + }, + args{ + ctx: context.Background(), + instanceID: "instanceID", + location: "location", + resourceOwner: "resourceOwner", + name: "name", + contentType: "contentType", + data: bytes.NewReader([]byte("test")), + objectSize: 4, + }, + &static.Asset{ + InstanceID: "instanceID", + Name: "name", + Hash: "md5Hash", + Size: 4, + LastModified: testNow, + Location: "location", + ContentType: "contentType", + }, + false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := &crdbStorage{ + client: tt.fields.client.db, + } + got, err := c.PutObject(tt.args.ctx, tt.args.instanceID, tt.args.location, tt.args.resourceOwner, tt.args.name, tt.args.contentType, tt.args.objectType, tt.args.data, tt.args.objectSize) + if (err != nil) != tt.wantErr { + t.Errorf("CreateObject() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("CreateObject() got = %v, want %v", got, tt.want) + } + }) + } +} + +type db struct { + mock sqlmock.Sqlmock + db *sql.DB +} + +func prepareDB(t *testing.T, expectations ...expectation) db { + t.Helper() + client, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("unable to create sql mock: %v", err) + } + for _, expectation := range expectations { + expectation(mock) + } + return db{ + mock: mock, + db: client, + } +} + +type expectation func(m sqlmock.Sqlmock) + +func expectExists(query string, value bool, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(value)) + } +} + +func expectQueryErr(query string, err error, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + m.ExpectQuery(regexp.QuoteMeta(query)).WithArgs(args...).WillReturnError(err) + } +} +func expectQuery(stmt string, cols []string, rows [][]driver.Value, args ...driver.Value) func(m sqlmock.Sqlmock) { + return func(m sqlmock.Sqlmock) { + q := m.ExpectQuery(regexp.QuoteMeta(stmt)).WithArgs(args...) + result := sqlmock.NewRows(cols) + count := uint64(len(rows)) + for _, row := range rows { + if cols[len(cols)-1] == "count" { + row = append(row, count) + } + result.AddRow(row...) + } + q.WillReturnRows(result) + q.RowsWillBeClosed() + } +} + +func expectExec(stmt string, err error, args ...driver.Value) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectExec(regexp.QuoteMeta(stmt)).WithArgs(args...) + if err != nil { + query.WillReturnError(err) + return + } + query.WillReturnResult(sqlmock.NewResult(1, 1)) + } +} + +func expectBegin(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectBegin() + if err != nil { + query.WillReturnError(err) + } + } +} + +func expectCommit(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectCommit() + if err != nil { + query.WillReturnError(err) + } + } +} + +func expectRollback(err error) expectation { + return func(m sqlmock.Sqlmock) { + query := m.ExpectRollback() + if err != nil { + query.WillReturnError(err) + } + } +} diff --git a/internal/static/i18n/de.yaml b/internal/static/i18n/de.yaml index 6f08e9886f..9cefe06191 100644 --- a/internal/static/i18n/de.yaml +++ b/internal/static/i18n/de.yaml @@ -22,6 +22,7 @@ Errors: Object: PutFailed: Objekt konnte nicht erstellt werden GetFailed: Objekt konnte nicht gelesen werden + NotFound: Objekt konnte nicht gefunden werden PresignedTokenFailed: Signiertes Token konnte nicht erstellt werden ListFailed: Objektliste konnte nicht gelesen werden RemoveFailed: Objekt konnte nicht gelöscht werden diff --git a/internal/static/i18n/en.yaml b/internal/static/i18n/en.yaml index 3d3b4eab0b..117c1e51c8 100644 --- a/internal/static/i18n/en.yaml +++ b/internal/static/i18n/en.yaml @@ -22,6 +22,7 @@ Errors: Object: PutFailed: Object not created GetFailed: Object could not be read + NotFound: Object could not be found PresignedTokenFailed: Signed token could not be created ListFailed: Objectlist could not be read RemoveFailed: Object could not be removed diff --git a/internal/static/i18n/it.yaml b/internal/static/i18n/it.yaml index 17a9efd03b..2caa8ff87b 100644 --- a/internal/static/i18n/it.yaml +++ b/internal/static/i18n/it.yaml @@ -22,6 +22,7 @@ Errors: Object: PutFailed: Oggetto non creato GetFailed: Oggetto non può essere letto + NotFound: Oggetto non trovato PresignedTokenFailed: Il token non può essere creato ListFailed: La lista degli oggetti non può essere letta RemoveFailed: L'oggetto non può essere rimosso diff --git a/internal/static/mock/storage_mock.go b/internal/static/mock/storage_mock.go index e5cf1806b6..c844c45540 100644 --- a/internal/static/mock/storage_mock.go +++ b/internal/static/mock/storage_mock.go @@ -7,11 +7,8 @@ package mock import ( context "context" io "io" - url "net/url" reflect "reflect" - time "time" - domain "github.com/caos/zitadel/internal/domain" static "github.com/caos/zitadel/internal/static" gomock "github.com/golang/mock/gomock" ) @@ -39,187 +36,76 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder { return m.recorder } -// CreateBucket mocks base method. -func (m *MockStorage) CreateBucket(ctx context.Context, name, location string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateBucket", ctx, name, location) - ret0, _ := ret[0].(error) - return ret0 -} - -// CreateBucket indicates an expected call of CreateBucket. -func (mr *MockStorageMockRecorder) CreateBucket(ctx, name, location interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockStorage)(nil).CreateBucket), ctx, name, location) -} - // GetObject mocks base method. -func (m *MockStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) { +func (m *MockStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetObject", ctx, bucketName, objectName) - ret0, _ := ret[0].(io.Reader) - ret1, _ := ret[1].(func() (*domain.AssetInfo, error)) + ret := m.ctrl.Call(m, "GetObject", ctx, instanceID, resourceOwner, name) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(func() (*static.Asset, error)) ret2, _ := ret[2].(error) return ret0, ret1, ret2 } // GetObject indicates an expected call of GetObject. -func (mr *MockStorageMockRecorder) GetObject(ctx, bucketName, objectName interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) GetObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, bucketName, objectName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, instanceID, resourceOwner, name) } // GetObjectInfo mocks base method. -func (m *MockStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) { +func (m *MockStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetObjectInfo", ctx, bucketName, objectName) - ret0, _ := ret[0].(*domain.AssetInfo) + ret := m.ctrl.Call(m, "GetObjectInfo", ctx, instanceID, resourceOwner, name) + ret0, _ := ret[0].(*static.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // GetObjectInfo indicates an expected call of GetObjectInfo. -func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, bucketName, objectName interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, bucketName, objectName) -} - -// GetObjectPresignedURL mocks base method. -func (m *MockStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetObjectPresignedURL", ctx, bucketName, objectName, expiration) - ret0, _ := ret[0].(*url.URL) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetObjectPresignedURL indicates an expected call of GetObjectPresignedURL. -func (mr *MockStorageMockRecorder) GetObjectPresignedURL(ctx, bucketName, objectName, expiration interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectPresignedURL", reflect.TypeOf((*MockStorage)(nil).GetObjectPresignedURL), ctx, bucketName, objectName, expiration) -} - -// ListBuckets mocks base method. -func (m *MockStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListBuckets", ctx) - ret0, _ := ret[0].([]*domain.BucketInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListBuckets indicates an expected call of ListBuckets. -func (mr *MockStorageMockRecorder) ListBuckets(ctx interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockStorage)(nil).ListBuckets), ctx) -} - -// ListObjectInfos mocks base method. -func (m *MockStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "ListObjectInfos", ctx, bucketName, prefix, recursive) - ret0, _ := ret[0].([]*domain.AssetInfo) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// ListObjectInfos indicates an expected call of ListObjectInfos. -func (mr *MockStorageMockRecorder) ListObjectInfos(ctx, bucketName, prefix, recursive interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectInfos", reflect.TypeOf((*MockStorage)(nil).ListObjectInfos), ctx, bucketName, prefix, recursive) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, instanceID, resourceOwner, name) } // PutObject mocks base method. -func (m *MockStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) { +func (m *MockStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "PutObject", ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting) - ret0, _ := ret[0].(*domain.AssetInfo) + ret := m.ctrl.Call(m, "PutObject", ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize) + ret0, _ := ret[0].(*static.Asset) ret1, _ := ret[1].(error) return ret0, ret1 } // PutObject indicates an expected call of PutObject. -func (mr *MockStorageMockRecorder) PutObject(ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) PutObject(ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting) -} - -// RemoveBucket mocks base method. -func (m *MockStorage) RemoveBucket(ctx context.Context, name string) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveBucket", ctx, name) - ret0, _ := ret[0].(error) - return ret0 -} - -// RemoveBucket indicates an expected call of RemoveBucket. -func (mr *MockStorageMockRecorder) RemoveBucket(ctx, name interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveBucket", reflect.TypeOf((*MockStorage)(nil).RemoveBucket), ctx, name) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize) } // RemoveObject mocks base method. -func (m *MockStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error { +func (m *MockStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveObject", ctx, bucketName, objectName) + ret := m.ctrl.Call(m, "RemoveObject", ctx, instanceID, resourceOwner, name) ret0, _ := ret[0].(error) return ret0 } // RemoveObject indicates an expected call of RemoveObject. -func (mr *MockStorageMockRecorder) RemoveObject(ctx, bucketName, objectName interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) RemoveObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, bucketName, objectName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, instanceID, resourceOwner, name) } // RemoveObjects mocks base method. -func (m *MockStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error { +func (m *MockStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "RemoveObjects", ctx, bucketName, path, recursive) + ret := m.ctrl.Call(m, "RemoveObjects", ctx, instanceID, resourceOwner, objectType) ret0, _ := ret[0].(error) return ret0 } // RemoveObjects indicates an expected call of RemoveObjects. -func (mr *MockStorageMockRecorder) RemoveObjects(ctx, bucketName, path, recursive interface{}) *gomock.Call { +func (mr *MockStorageMockRecorder) RemoveObjects(ctx, instanceID, resourceOwner, objectType interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, bucketName, path, recursive) -} - -// MockConfig is a mock of Config interface. -type MockConfig struct { - ctrl *gomock.Controller - recorder *MockConfigMockRecorder -} - -// MockConfigMockRecorder is the mock recorder for MockConfig. -type MockConfigMockRecorder struct { - mock *MockConfig -} - -// NewMockConfig creates a new mock instance. -func NewMockConfig(ctrl *gomock.Controller) *MockConfig { - mock := &MockConfig{ctrl: ctrl} - mock.recorder = &MockConfigMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockConfig) EXPECT() *MockConfigMockRecorder { - return m.recorder -} - -// NewStorage mocks base method. -func (m *MockConfig) NewStorage() (static.Storage, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "NewStorage") - ret0, _ := ret[0].(static.Storage) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// NewStorage indicates an expected call of NewStorage. -func (mr *MockConfigMockRecorder) NewStorage() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStorage", reflect.TypeOf((*MockConfig)(nil).NewStorage)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, instanceID, resourceOwner, objectType) } diff --git a/internal/static/mock/storage_mock.impl.go b/internal/static/mock/storage_mock.impl.go index ed760c9d0a..3022763746 100644 --- a/internal/static/mock/storage_mock.impl.go +++ b/internal/static/mock/storage_mock.impl.go @@ -1,34 +1,49 @@ package mock import ( + "context" + "io" "testing" + "time" "github.com/golang/mock/gomock" caos_errors "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/static" ) func NewStorage(t *testing.T) *MockStorage { return NewMockStorage(gomock.NewController(t)) } -func (m *MockStorage) ExpectAddObjectNoError() *MockStorage { +func (m *MockStorage) ExpectPutObject() *MockStorage { m.EXPECT(). - PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). - Return(nil, nil) + PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + DoAndReturn(func(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { + hash, _ := io.ReadAll(object) + return &static.Asset{ + InstanceID: instanceID, + Name: name, + Hash: string(hash), + Size: objectSize, + LastModified: time.Now(), + Location: location, + ContentType: contentType, + }, nil + }) return m } -func (m *MockStorage) ExpectAddObjectError() *MockStorage { +func (m *MockStorage) ExpectPutObjectError() *MockStorage { m.EXPECT(). - PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil, caos_errors.ThrowInternal(nil, "", "")) return m } func (m *MockStorage) ExpectRemoveObjectNoError() *MockStorage { m.EXPECT(). - RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()). + RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(nil) return m } @@ -42,7 +57,7 @@ func (m *MockStorage) ExpectRemoveObjectsNoError() *MockStorage { func (m *MockStorage) ExpectRemoveObjectError() *MockStorage { m.EXPECT(). - RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()). + RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). Return(caos_errors.ThrowInternal(nil, "", "")) return m } diff --git a/internal/static/s3/config.go b/internal/static/s3/config.go index 5669ad6d90..e2556e8c8f 100644 --- a/internal/static/s3/config.go +++ b/internal/static/s3/config.go @@ -1,9 +1,13 @@ package s3 import ( + "database/sql" + "encoding/json" + "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" + "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/static" ) @@ -34,3 +38,15 @@ func (c *Config) NewStorage() (static.Storage, error) { MultiDelete: c.MultiDelete, }, nil } + +func NewStorage(_ *sql.DB, rawConfig map[string]interface{}) (static.Storage, error) { + configData, err := json.Marshal(rawConfig) + if err != nil { + return nil, errors.ThrowInternal(err, "MINIO-Ef2f2", "could not map config") + } + c := new(Config) + if err := json.Unmarshal(configData, c); err != nil { + return nil, errors.ThrowInternal(err, "MINIO-GB4nw", "could not map config") + } + return c.NewStorage() +} diff --git a/internal/static/s3/minio.go b/internal/static/s3/minio.go index 94f6b4054d..b8ffd59602 100644 --- a/internal/static/s3/minio.go +++ b/internal/static/s3/minio.go @@ -2,11 +2,10 @@ package s3 import ( "context" + "fmt" "io" "net/http" - "net/url" "strings" - "time" "github.com/caos/logging" "github.com/minio/minio-go/v7" @@ -14,8 +13,11 @@ import ( "github.com/caos/zitadel/internal/domain" caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/static" ) +var _ static.Storage = (*Minio)(nil) + type Minio struct { Client *minio.Client Location string @@ -23,129 +25,66 @@ type Minio struct { MultiDelete bool } -func (m *Minio) CreateBucket(ctx context.Context, name, location string) error { - if location == "" { - location = m.Location +func (m *Minio) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) { + err := m.createBucket(ctx, instanceID, location) + if err != nil && !caos_errs.IsErrorAlreadyExists(err) { + return nil, err } - name = m.prefixBucketName(name) - exists, err := m.Client.BucketExists(ctx, name) - if err != nil { - logging.LogWithFields("MINIO-ADvf3", "bucketname", name).WithError(err).Error("cannot check if bucket exists") - return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal") - } - if exists { - return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists") - } - err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location}) - if err != nil { - return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed") - } - return nil -} - -func (m *Minio) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) { - infos, err := m.Client.ListBuckets(ctx) - if err != nil { - return nil, caos_errs.ThrowInternal(err, "MINIO-390OP", "Errors.Assets.Bucket.ListFailed") - } - buckets := make([]*domain.BucketInfo, len(infos)) - for i, info := range infos { - buckets[i] = &domain.BucketInfo{ - Name: info.Name, - CreationDate: info.CreationDate, - } - } - return buckets, nil -} - -func (m *Minio) RemoveBucket(ctx context.Context, name string) error { - name = m.prefixBucketName(name) - err := m.Client.RemoveBucket(ctx, name) - if err != nil { - return caos_errs.ThrowInternal(err, "MINIO-338Hs", "Errors.Assets.Bucket.RemoveFailed") - } - return nil -} - -func (m *Minio) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) { - if createBucketIfNotExisting { - err := m.CreateBucket(ctx, bucketName, "") - if err != nil && !caos_errs.IsErrorAlreadyExists(err) { - return nil, err - } - } - bucketName = m.prefixBucketName(bucketName) + bucketName := m.prefixBucketName(instanceID) + objectName := fmt.Sprintf("%s/%s", resourceOwner, name) info, err := m.Client.PutObject(ctx, bucketName, objectName, object, objectSize, minio.PutObjectOptions{ContentType: contentType}) if err != nil { return nil, caos_errs.ThrowInternal(err, "MINIO-590sw", "Errors.Assets.Object.PutFailed") } - return &domain.AssetInfo{ - Bucket: info.Bucket, - Key: info.Key, - ETag: info.ETag, - Size: info.Size, - LastModified: info.LastModified, - Location: info.Location, - VersionID: info.VersionID, + return &static.Asset{ + InstanceID: info.Bucket, + ResourceOwner: resourceOwner, + Name: info.Key, + Hash: info.ETag, + Size: info.Size, + LastModified: info.LastModified, + Location: info.Location, + ContentType: contentType, }, nil } -func (m *Minio) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) { - bucketName = m.prefixBucketName(bucketName) - objectinfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{}) +func (m *Minio) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) { + bucketName := m.prefixBucketName(instanceID) + objectName := fmt.Sprintf("%s/%s", resourceOwner, name) + object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) + if err != nil { + return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed") + } + info := func() (*static.Asset, error) { + info, err := object.Stat() + if err != nil { + return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed") + } + return m.objectToAssetInfo(instanceID, resourceOwner, info), nil + } + asset, err := io.ReadAll(object) + if err != nil { + return nil, nil, caos_errs.ThrowInternal(err, "MINIO-SFef1", "Errors.Assets.Object.GetFailed") + } + return asset, info, nil +} + +func (m *Minio) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) { + bucketName := m.prefixBucketName(instanceID) + objectName := fmt.Sprintf("%s/%s", resourceOwner, name) + objectInfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{}) if err != nil { if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound { return nil, caos_errs.ThrowNotFound(err, "MINIO-Gdfh4", "Errors.Assets.Object.GetFailed") } return nil, caos_errs.ThrowInternal(err, "MINIO-1vySX", "Errors.Assets.Object.GetFailed") } - return m.objectToAssetInfo(bucketName, objectinfo), nil + return m.objectToAssetInfo(instanceID, resourceOwner, objectInfo), nil } -func (m *Minio) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) { - bucketName = m.prefixBucketName(bucketName) - object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) - if err != nil { - return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed") - } - info := func() (*domain.AssetInfo, error) { - info, err := object.Stat() - if err != nil { - return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed") - } - return m.objectToAssetInfo(bucketName, info), nil - } - return object, info, nil -} - -func (m *Minio) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) { - bucketName = m.prefixBucketName(bucketName) - reqParams := make(url.Values) - presignedURL, err := m.Client.PresignedGetObject(ctx, bucketName, objectName, expiration, reqParams) - if err != nil { - return nil, caos_errs.ThrowInternal(err, "MINIO-19Mp0", "Errors.Assets.Object.PresignedTokenFailed") - } - return presignedURL, nil -} - -func (m *Minio) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) { - bucketName = m.prefixBucketName(bucketName) - assetInfos := make([]*domain.AssetInfo, 0) - - objects, cancel := m.listObjects(ctx, bucketName, prefix, recursive) - defer cancel() - for object := range objects { - if object.Err != nil { - logging.LogWithFields("MINIO-wC8sd", "bucket-name", bucketName, "prefix", prefix).WithError(object.Err).Debug("unable to get object") - return nil, caos_errs.ThrowInternal(object.Err, "MINIO-1m09S", "Errors.Assets.Object.ListFailed") - } - assetInfos = append(assetInfos, m.objectToAssetInfo(bucketName, object)) - } - return assetInfos, nil -} - -func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string) error { - bucketName = m.prefixBucketName(bucketName) +func (m *Minio) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error { + bucketName := m.prefixBucketName(instanceID) + objectName := fmt.Sprintf("%s/%s", resourceOwner, name) err := m.Client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{}) if err != nil { return caos_errs.ThrowInternal(err, "MINIO-x85RT", "Errors.Assets.Object.RemoveFailed") @@ -153,19 +92,27 @@ func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string) return nil } -func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error { - bucketName = m.prefixBucketName(bucketName) +func (m *Minio) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error { + bucketName := m.prefixBucketName(instanceID) objectsCh := make(chan minio.ObjectInfo) g := new(errgroup.Group) + var path string + switch objectType { + case static.ObjectTypeStyling: + path = domain.LabelPolicyPrefix + "/" + default: + return nil + } + g.Go(func() error { defer close(objectsCh) - objects, cancel := m.listObjects(ctx, bucketName, path, recursive) + objects, cancel := m.listObjects(ctx, bucketName, resourceOwner, true) for object := range objects { if err := object.Err; err != nil { cancel() if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound { - logging.LogWithFields("MINIO-ss8va", "bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found") + logging.WithFields("bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found") continue } return caos_errs.ThrowInternal(object.Err, "MINIO-WQF32", "Errors.Assets.Object.ListFailed") @@ -189,6 +136,26 @@ func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recu return g.Wait() } +func (m *Minio) createBucket(ctx context.Context, name, location string) error { + if location == "" { + location = m.Location + } + name = m.prefixBucketName(name) + exists, err := m.Client.BucketExists(ctx, name) + if err != nil { + logging.WithFields("bucketname", name).WithError(err).Error("cannot check if bucket exists") + return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal") + } + if exists { + return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists") + } + err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location}) + if err != nil { + return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed") + } + return nil +} + func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recursive bool) (<-chan minio.ObjectInfo, context.CancelFunc) { ctxCancel, cancel := context.WithCancel(ctx) @@ -198,17 +165,15 @@ func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recu }), cancel } -func (m *Minio) objectToAssetInfo(bucketName string, object minio.ObjectInfo) *domain.AssetInfo { - return &domain.AssetInfo{ - Bucket: bucketName, - Key: object.Key, - ETag: object.ETag, - Size: object.Size, - LastModified: object.LastModified, - VersionID: object.VersionID, - Expiration: object.Expires, - ContentType: object.ContentType, - AutheticatedURL: m.Client.EndpointURL().String() + "/" + bucketName + "/" + object.Key, +func (m *Minio) objectToAssetInfo(bucketName string, resourceOwner string, object minio.ObjectInfo) *static.Asset { + return &static.Asset{ + InstanceID: bucketName, + ResourceOwner: resourceOwner, + Name: object.Key, + Hash: object.ETag, + Size: object.Size, + LastModified: object.LastModified, + ContentType: object.ContentType, } } diff --git a/internal/static/storage.go b/internal/static/storage.go index d97074493d..a8520320e8 100644 --- a/internal/static/storage.go +++ b/internal/static/storage.go @@ -2,25 +2,36 @@ package static import ( "context" + "database/sql" "io" - "net/url" "time" - - "github.com/caos/zitadel/internal/domain" ) +type CreateStorage func(client *sql.DB, rawConfig map[string]interface{}) (Storage, error) + type Storage interface { - CreateBucket(ctx context.Context, name, location string) error - RemoveBucket(ctx context.Context, name string) error - ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) - PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) - GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) - GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) - ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) - GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) - RemoveObject(ctx context.Context, bucketName, objectName string) error - RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error + PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType ObjectType, object io.Reader, objectSize int64) (*Asset, error) + GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*Asset, error), error) + GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*Asset, error) + RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error + RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType ObjectType) error + //TODO: add functionality to move asset location } -type Config interface { - NewStorage() (Storage, error) + +type ObjectType int32 + +const ( + ObjectTypeUserAvatar = iota + ObjectTypeStyling +) + +type Asset struct { + InstanceID string + ResourceOwner string + Name string + Hash string + Size int64 + LastModified time.Time + Location string + ContentType string } diff --git a/internal/user/model/user_view.go b/internal/user/model/user_view.go index f05492c1d9..d6b36475e8 100644 --- a/internal/user/model/user_view.go +++ b/internal/user/model/user_view.go @@ -1,8 +1,6 @@ package model import ( - "context" - "net/url" "time" "golang.org/x/text/language" @@ -11,7 +9,6 @@ import ( "github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/eventstore/v1/models" iam_model "github.com/caos/zitadel/internal/iam/model" - "github.com/caos/zitadel/internal/static" ) type UserView struct { @@ -41,7 +38,6 @@ type HumanView struct { DisplayName string AvatarKey string AvatarURL string - PreSignedAvatar *url.URL PreferredLanguage string Gender Gender Email string @@ -257,23 +253,6 @@ func (u *UserView) GetProfile() (*Profile, error) { }, nil } -func (u *UserView) FillUserAvatar(ctx context.Context, static static.Storage, expiration time.Duration) error { - if u.HumanView == nil { - return errors.ThrowPreconditionFailed(nil, "MODEL-2k8da", "Errors.User.NotHuman") - } - if static != nil { - if ctx == nil { - ctx = context.Background() - } - presignesAvatarURL, err := static.GetObjectPresignedURL(ctx, u.ResourceOwner, u.AvatarKey, expiration) - if err != nil { - return err - } - u.PreSignedAvatar = presignesAvatarURL - } - return nil -} - func (u *UserView) GetPhone() (*Phone, error) { if u.HumanView == nil { return nil, errors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman")