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
This commit is contained in:
Livio Amstutz 2022-04-06 08:13:40 +02:00 committed by GitHub
parent b949b8fc65
commit 4a0d61d75a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 2016 additions and 967 deletions

View File

@ -2,21 +2,36 @@ package setup
import ( import (
"context" "context"
"database/sql"
command "github.com/caos/zitadel/internal/command/v2" _ "embed"
) )
type DefaultInstance struct { const (
cmd *command.Command createAssets = `
InstanceSetup command.InstanceSetup 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 { func (mig *AssetTable) Execute(ctx context.Context) error {
_, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup) _, err := mig.dbClient.ExecContext(ctx, createAssets)
return err return err
} }
func (mig *DefaultInstance) String() string { func (mig *AssetTable) String() string {
return "02_default_instance" return "02_assets"
} }

22
cmd/admin/setup/03.go Normal file
View File

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

View File

@ -35,8 +35,9 @@ func MustNewConfig(v *viper.Viper) *Config {
} }
type Steps struct { type Steps struct {
S1ProjectionTable *ProjectionTable s1ProjectionTable *ProjectionTable
S2DefaultInstance *DefaultInstance s2AssetsTable *AssetTable
S3DefaultInstance *DefaultInstance
} }
func MustNewSteps(v *viper.Viper) *Steps { func MustNewSteps(v *viper.Viper) *Steps {

View File

@ -9,7 +9,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
http_util "github.com/caos/zitadel/internal/api/http" 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/database"
"github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/migration" "github.com/caos/zitadel/internal/migration"
@ -46,12 +46,14 @@ func Setup(config *Config, steps *Steps) {
cmd := command.New(eventstoreClient, "localhost", config.SystemDefaults) cmd := command.New(eventstoreClient, "localhost", config.SystemDefaults)
steps.S2DefaultInstance.cmd = cmd steps.s1ProjectionTable = &ProjectionTable{dbClient: dbClient}
steps.S1ProjectionTable = &ProjectionTable{dbClient: dbClient} steps.s2AssetsTable = &AssetTable{dbClient: dbClient}
steps.S2DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure steps.S3DefaultInstance.cmd = cmd
steps.S2DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) steps.S3DefaultInstance.InstanceSetup.Zitadel.IsDevMode = !config.ExternalSecure
steps.S3DefaultInstance.InstanceSetup.Zitadel.BaseURL = http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
ctx := context.Background() ctx := context.Background()
migration.Migrate(ctx, eventstoreClient, steps.S1ProjectionTable) migration.Migrate(ctx, eventstoreClient, steps.s1ProjectionTable)
migration.Migrate(ctx, eventstoreClient, steps.S2DefaultInstance) migration.Migrate(ctx, eventstoreClient, steps.s2AssetsTable)
migration.Migrate(ctx, eventstoreClient, steps.S3DefaultInstance)
} }

View File

@ -1,4 +1,4 @@
S2DefaultInstance: S3DefaultInstance:
InstanceSetup: InstanceSetup:
Org: Org:
Name: ZITADEL Name: ZITADEL
@ -13,6 +13,29 @@ S2DefaultInstance:
Gender: Gender:
Phone: Phone:
Password: Password1! 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: PasswordComplexityPolicy:
MinLength: 8 MinLength: 8
HasLowercase: true HasLowercase: true

View File

@ -94,12 +94,6 @@ func startZitadel(config *Config, masterKey string) error {
return err 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) eventstoreClient, err := eventstore.Start(dbClient)
if err != nil { if err != nil {
return fmt.Errorf("cannot start eventstore for queries: %w", err) return fmt.Errorf("cannot start eventstore for queries: %w", err)
@ -114,6 +108,11 @@ func startZitadel(config *Config, masterKey string) error {
if err != nil { if err != nil {
return fmt.Errorf("error starting authz repo: %w", err) 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{ webAuthNConfig := webauthn.Config{
ID: config.ExternalDomain, ID: config.ExternalDomain,
Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure), 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 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) userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure)
if err != nil { if err != nil {
return err return err
} }
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader)
issuer := oidc.Issuer(config.ExternalDomain, config.ExternalPort, config.ExternalSecure) 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) 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)

View File

@ -163,7 +163,7 @@ func (m *Styling) generateStylingFile(policy *iam_model.LabelPolicyView) error {
if err != nil { if err != nil {
return err 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) { 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 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 return err
} }

View File

@ -1,24 +1,21 @@
package assets package assets
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"io/ioutil"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/caos/logging" "github.com/caos/logging"
sentryhttp "github.com/getsentry/sentry-go/http" sentryhttp "github.com/getsentry/sentry-go/http"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/superseriousbusiness/exifremove/pkg/exifremove"
"github.com/caos/zitadel/internal/api/authz" "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" http_mw "github.com/caos/zitadel/internal/api/http/middleware"
"github.com/caos/zitadel/internal/command" "github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/static" "github.com/caos/zitadel/internal/static"
@ -54,26 +51,27 @@ func (h *Handler) Storage() static.Storage {
} }
type Uploader interface { 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) ObjectName(data authz.CtxData) (string, error)
BucketName(data authz.CtxData) string ResourceOwner(instance authz.Instance, data authz.CtxData) string
ContentTypeAllowed(contentType string) bool ContentTypeAllowed(contentType string) bool
MaxFileSize() int64 MaxFileSize() int64
ObjectType() static.ObjectType
} }
type Downloader interface { type Downloader interface {
ObjectName(ctx context.Context, path string) (string, error) 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) type ErrorHandler func(http.ResponseWriter, *http.Request, error, int)
func DefaultErrorHandler(w http.ResponseWriter, r *http.Request, err error, code 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) 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{ h := &Handler{
commands: commands, commands: commands,
errorHandler: DefaultErrorHandler, 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? verifier.RegisterServer("Management-API", "assets", AssetsService_AuthMethods) //TODO: separate api?
router := mux.NewRouter() router := mux.NewRouter()
router.Use(sentryhttp.New(sentryhttp.Options{}).Handle) router.Use(sentryhttp.New(sentryhttp.Options{}).Handle, instanceInterceptor)
RegisterRoutes(router, h) 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 return router
} }
@ -101,8 +99,8 @@ func (l *publicFileDownloader) ObjectName(_ context.Context, path string) (strin
return path, nil return path, nil
} }
func (l *publicFileDownloader) BucketName(_ context.Context, id string) string { func (l *publicFileDownloader) ResourceOwner(_ context.Context, ownerPath string) string {
return id return ownerPath
} }
const maxMemory = 2 << 20 const maxMemory = 2 << 20
@ -120,7 +118,7 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit
} }
defer func() { defer func() {
err = file.Close() 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") contentType := handler.Header.Get("content-type")
size := handler.Size size := handler.Size
@ -133,24 +131,21 @@ func UploadHandleFunc(s AssetsService, uploader Uploader) func(http.ResponseWrit
return return
} }
bucketName := uploader.BucketName(ctxData) resourceOwner := uploader.ResourceOwner(authz.GetInstance(ctx), ctxData)
objectName, err := uploader.ObjectName(ctxData) objectName, err := uploader.ObjectName(ctxData)
if err != nil { if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError) s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
return return
} }
cleanedFile, cleanedSize, err := removeExif(file, size, contentType) uploadInfo := &command.AssetUpload{
if err != nil { ResourceOwner: resourceOwner,
s.ErrorHandler()(w, r, fmt.Errorf("remove exif error: %v", err), http.StatusInternalServerError) ObjectName: objectName,
return ContentType: contentType,
ObjectType: uploader.ObjectType(),
File: file,
Size: size,
} }
err = uploader.UploadAsset(ctx, ctxData.OrgID, uploadInfo, s.Commands())
info, err := s.Commands().UploadAsset(ctx, bucketName, objectName, contentType, cleanedFile, cleanedSize)
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
return
}
err = uploader.Callback(ctx, info, ctxData.OrgID, s.Commands())
if err != nil { if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError) s.ErrorHandler()(w, r, fmt.Errorf("upload failed: %v", err), http.StatusInternalServerError)
return return
@ -164,11 +159,11 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
return return
} }
ctx := r.Context() ctx := r.Context()
id := mux.Vars(r)["id"] ownerPath := mux.Vars(r)["owner"]
bucketName := downloader.BucketName(ctx, id) resourceOwner := downloader.ResourceOwner(ctx, ownerPath)
path := "" path := ""
if id != "" { if ownerPath != "" {
path = strings.Split(r.RequestURI, id+"/")[1] path = strings.Split(r.RequestURI, ownerPath+"/")[1]
} }
objectName, err := downloader.ObjectName(ctx, path) objectName, err := downloader.ObjectName(ctx, path)
if err != nil { if err != nil {
@ -176,49 +171,33 @@ func DownloadHandleFunc(s AssetsService, downloader Downloader) func(http.Respon
return return
} }
if objectName == "" { 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 return
} }
reader, getInfo, err := s.Storage().GetObject(ctx, bucketName, objectName) if err = GetAsset(w, r, resourceOwner, objectName, s.Storage()); err != nil {
if err != nil { s.ErrorHandler()(w, r, err, http.StatusInternalServerError)
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
return
} }
data, err := ioutil.ReadAll(reader)
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
return
}
info, err := getInfo()
if err != nil {
s.ErrorHandler()(w, r, fmt.Errorf("download failed: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("content-length", strconv.FormatInt(info.Size, 10))
w.Header().Set("content-type", info.ContentType)
w.Header().Set("ETag", info.ETag)
w.Write(data)
} }
} }
func removeExif(file io.Reader, size int64, contentType string) (io.Reader, int64, error) { func GetAsset(w http.ResponseWriter, r *http.Request, resourceOwner, objectName string, storage static.Storage) error {
if !isAllowedContentType(contentType) { data, getInfo, err := storage.GetObject(r.Context(), authz.GetInstance(r.Context()).InstanceID(), resourceOwner, objectName)
return file, size, nil
}
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(file)
if err != nil { 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 { if err != nil {
return nil, 0, err return fmt.Errorf("download failed: %v", err)
} }
return bytes.NewReader(data), int64(len(data)), nil if info.Hash == r.Header.Get(http_util.IfNoneMatch) {
} w.WriteHeader(304)
return nil
func isAllowedContentType(contentType string) bool { }
return strings.HasSuffix(contentType, "png") || w.Header().Set(http_util.ContentLength, strconv.FormatInt(info.Size, 10))
strings.HasSuffix(contentType, "jpg") || w.Header().Set(http_util.ContentType, info.ContentType)
strings.HasSuffix(contentType, "jpeg") 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
} }

View File

@ -1,6 +1,6 @@
Services: Services:
IAM: IAM:
Prefix: "/iam" Prefix: "/instance"
Methods: Methods:
DefaultLabelPolicyLogo: DefaultLabelPolicyLogo:
Path: "/policy/label/logo" Path: "/policy/label/logo"
@ -116,4 +116,4 @@ Services:
- Name: Get - Name: Get
Comment: Comment:
Type: download Type: download
Permission: authenticated Permission: authenticated

View File

@ -9,6 +9,7 @@ import (
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"
"github.com/caos/zitadel/internal/query" "github.com/caos/zitadel/internal/query"
"github.com/caos/zitadel/internal/static"
) )
func (h *Handler) UploadDefaultLabelPolicyLogo() Uploader { func (h *Handler) UploadDefaultLabelPolicyLogo() Uploader {
@ -44,6 +45,10 @@ func (l *labelPolicyLogoUploader) ContentTypeAllowed(contentType string) bool {
return false return false
} }
func (l *labelPolicyLogoUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyLogoUploader) MaxFileSize() int64 { func (l *labelPolicyLogoUploader) MaxFileSize() int64 {
return l.maxSize return l.maxSize
} }
@ -60,27 +65,27 @@ func (l *labelPolicyLogoUploader) ObjectName(_ authz.CtxData) (string, error) {
return prefix + "-" + suffixID, nil 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 { if l.defaultPolicy {
return domain.IAMID return instance.InstanceID()
} }
return ctxData.OrgID 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.defaultPolicy {
if l.darkMode { if l.darkMode {
_, err := commands.AddLogoDarkDefaultLabelPolicy(ctx, info.Key) _, err := commands.AddLogoDarkDefaultLabelPolicy(ctx, upload)
return err return err
} }
_, err := commands.AddLogoDefaultLabelPolicy(ctx, info.Key) _, err := commands.AddLogoDefaultLabelPolicy(ctx, upload)
return err return err
} }
if l.darkMode { if l.darkMode {
_, err := commands.AddLogoDarkLabelPolicy(ctx, orgID, info.Key) _, err := commands.AddLogoDarkLabelPolicy(ctx, orgID, upload)
return err return err
} }
_, err := commands.AddLogoLabelPolicy(ctx, orgID, info.Key) _, err := commands.AddLogoLabelPolicy(ctx, orgID, upload)
return err return err
} }
@ -134,8 +139,8 @@ func (l *labelPolicyLogoDownloader) ObjectName(ctx context.Context, path string)
return policy.Light.LogoURL, nil return policy.Light.LogoURL, nil
} }
func (l *labelPolicyLogoDownloader) BucketName(ctx context.Context, id string) string { func (l *labelPolicyLogoDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
} }
func (h *Handler) UploadDefaultLabelPolicyIcon() Uploader { func (h *Handler) UploadDefaultLabelPolicyIcon() Uploader {
@ -171,6 +176,10 @@ func (l *labelPolicyIconUploader) ContentTypeAllowed(contentType string) bool {
return false return false
} }
func (l *labelPolicyIconUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyIconUploader) MaxFileSize() int64 { func (l *labelPolicyIconUploader) MaxFileSize() int64 {
return l.maxSize return l.maxSize
} }
@ -187,28 +196,28 @@ func (l *labelPolicyIconUploader) ObjectName(_ authz.CtxData) (string, error) {
return prefix + "-" + suffixID, nil 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 { if l.defaultPolicy {
return domain.IAMID return instance.InstanceID()
} }
return ctxData.OrgID 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.defaultPolicy {
if l.darkMode { if l.darkMode {
_, err := commands.AddIconDarkDefaultLabelPolicy(ctx, info.Key) _, err := commands.AddIconDarkDefaultLabelPolicy(ctx, upload)
return err return err
} }
_, err := commands.AddIconDefaultLabelPolicy(ctx, info.Key) _, err := commands.AddIconDefaultLabelPolicy(ctx, upload)
return err return err
} }
if l.darkMode { if l.darkMode {
_, err := commands.AddIconDarkLabelPolicy(ctx, orgID, info.Key) _, err := commands.AddIconDarkLabelPolicy(ctx, orgID, upload)
return err return err
} }
_, err := commands.AddIconLabelPolicy(ctx, orgID, info.Key) _, err := commands.AddIconLabelPolicy(ctx, orgID, upload)
return err return err
} }
@ -262,8 +271,8 @@ func (l *labelPolicyIconDownloader) ObjectName(ctx context.Context, path string)
return policy.Light.IconURL, nil return policy.Light.IconURL, nil
} }
func (l *labelPolicyIconDownloader) BucketName(ctx context.Context, id string) string { func (l *labelPolicyIconDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
} }
func (h *Handler) UploadDefaultLabelPolicyFont() Uploader { func (h *Handler) UploadDefaultLabelPolicyFont() Uploader {
@ -290,6 +299,10 @@ func (l *labelPolicyFontUploader) ContentTypeAllowed(contentType string) bool {
return false return false
} }
func (l *labelPolicyFontUploader) ObjectType() static.ObjectType {
return static.ObjectTypeStyling
}
func (l *labelPolicyFontUploader) MaxFileSize() int64 { func (l *labelPolicyFontUploader) MaxFileSize() int64 {
return l.maxSize return l.maxSize
} }
@ -303,19 +316,19 @@ func (l *labelPolicyFontUploader) ObjectName(_ authz.CtxData) (string, error) {
return prefix + "-" + suffixID, nil 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 { if l.defaultPolicy {
return domain.IAMID return instance.InstanceID()
} }
return ctxData.OrgID 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 { if l.defaultPolicy {
_, err := commands.AddFontDefaultLabelPolicy(ctx, info.Key) _, err := commands.AddFontDefaultLabelPolicy(ctx, upload)
return err return err
} }
_, err := commands.AddFontLabelPolicy(ctx, orgID, info.Key) _, err := commands.AddFontLabelPolicy(ctx, orgID, upload)
return err return err
} }
@ -349,8 +362,8 @@ func (l *labelPolicyFontDownloader) ObjectName(ctx context.Context, path string)
return policy.FontURL, nil return policy.FontURL, nil
} }
func (l *labelPolicyFontDownloader) BucketName(ctx context.Context, id string) string { func (l *labelPolicyFontDownloader) ResourceOwner(ctx context.Context, _ string) string {
return getLabelPolicyBucketName(ctx, l.defaultPolicy, l.preview, l.query) return getLabelPolicyResourceOwner(ctx, l.defaultPolicy, l.preview, l.query)
} }
func getLabelPolicy(ctx context.Context, defaultPolicy, preview bool, queries *query.Queries) (*query.LabelPolicy, error) { 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) 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 { if defaultPolicy {
return domain.IAMID return authz.GetInstance(ctx).InstanceID()
} }
policy, err := getLabelPolicy(ctx, defaultPolicy, preview, queries) policy, err := getLabelPolicy(ctx, defaultPolicy, preview, queries)
if err != nil { if err != nil {
return "" return ""
} }
if policy.IsDefault { if policy.IsDefault {
return domain.IAMID return authz.GetInstance(ctx).InstanceID()
} }
return authz.GetCtxData(ctx).OrgID return authz.GetCtxData(ctx).OrgID
} }

View File

@ -7,6 +7,7 @@ import (
"github.com/caos/zitadel/internal/api/authz" "github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/command" "github.com/caos/zitadel/internal/command"
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/static"
) )
func (h *Handler) UploadMyUserAvatar() Uploader { func (h *Handler) UploadMyUserAvatar() Uploader {
@ -27,6 +28,10 @@ func (l *myHumanAvatarUploader) ContentTypeAllowed(contentType string) bool {
return false return false
} }
func (l *myHumanAvatarUploader) ObjectType() static.ObjectType {
return static.ObjectTypeUserAvatar
}
func (l *myHumanAvatarUploader) MaxFileSize() int64 { func (l *myHumanAvatarUploader) MaxFileSize() int64 {
return l.maxSize return l.maxSize
} }
@ -35,12 +40,12 @@ func (l *myHumanAvatarUploader) ObjectName(ctxData authz.CtxData) (string, error
return domain.GetHumanAvatarAssetPath(ctxData.UserID), nil return domain.GetHumanAvatarAssetPath(ctxData.UserID), nil
} }
func (l *myHumanAvatarUploader) BucketName(ctxData authz.CtxData) string { func (l *myHumanAvatarUploader) ResourceOwner(_ authz.Instance, ctxData authz.CtxData) string {
return ctxData.OrgID return ctxData.ResourceOwner
} }
func (l *myHumanAvatarUploader) Callback(ctx context.Context, info *domain.AssetInfo, orgID string, commands *command.Commands) error { 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, info.Key) _, err := commands.AddHumanAvatar(ctx, orgID, authz.GetCtxData(ctx).UserID, upload)
return err return err
} }
@ -54,6 +59,6 @@ func (l *myHumanAvatarDownloader) ObjectName(ctx context.Context, path string) (
return domain.GetHumanAvatarAssetPath(authz.GetCtxData(ctx).UserID), nil return domain.GetHumanAvatarAssetPath(authz.GetCtxData(ctx).UserID), nil
} }
func (l *myHumanAvatarDownloader) BucketName(ctx context.Context, id string) string { func (l *myHumanAvatarDownloader) ResourceOwner(ctx context.Context, _ string) string {
return authz.GetCtxData(ctx).OrgID return authz.GetCtxData(ctx).ResourceOwner
} }

View File

@ -22,6 +22,9 @@ const (
ForwardedFor = "x-forwarded-for" ForwardedFor = "x-forwarded-for"
XUserAgent = "x-user-agent" XUserAgent = "x-user-agent"
XGrpcWeb = "x-grpc-web" XGrpcWeb = "x-grpc-web"
IfNoneMatch = "If-None-Match"
LastModified = "Last-Modified"
Etag = "Etag"
ContentSecurityPolicy = "content-security-policy" ContentSecurityPolicy = "content-security-policy"
XXSSProtection = "x-xss-protection" XXSSProtection = "x-xss-protection"

View File

@ -1,11 +1,12 @@
package login package login
import ( import (
"context"
"net/http" "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/api/authz"
"github.com/caos/zitadel/internal/domain"
) )
type dynamicResourceData struct { type dynamicResourceData struct {
@ -25,74 +26,11 @@ func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
return return
} }
bucketName := authz.GetInstance(r.Context()).InstanceID() resourceOwner := authz.GetInstance(r.Context()).InstanceID()
if data.OrgID != "" && !data.DefaultPolicy { if data.OrgID != "" && !data.DefaultPolicy {
bucketName = data.OrgID resourceOwner = data.OrgID
} }
etag := r.Header.Get("If-None-Match") err = assets.GetAsset(w, r, resourceOwner, data.FileName, l.staticStorage)
asset, info, err := l.getStatic(r.Context(), bucketName, data.FileName) logging.WithFields("file", data.FileName, "org", resourceOwner).OnError(err).Warn("asset in login could not be served")
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
} }

View File

@ -124,10 +124,7 @@ func (c *Commands) ActivateDefaultLabelPolicy(ctx context.Context) (*domain.Obje
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
} }
func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-3m20c", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Qw0pd", "Errors.IAM.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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") 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 { if err != nil {
return nil, err return nil, err
} }
@ -174,10 +175,7 @@ func (c *Commands) RemoveLogoDefaultLabelPolicy(ctx context.Context) (*domain.Ob
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
} }
func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-yxE4f", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1yMx0", "Errors.IAM.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-4M0qw", "Errors.IAM.LabelPolicy.NotFound") 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 { if err != nil {
return nil, err return nil, err
} }
@ -223,20 +225,21 @@ func (c *Commands) RemoveIconDefaultLabelPolicy(ctx context.Context) (*domain.Ob
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
} }
func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-4fMs9", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { 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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -254,9 +257,9 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai
} }
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { 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 { if err != nil {
return nil, err return nil, err
} }
@ -272,20 +275,21 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
} }
func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1cxM3", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { 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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -303,9 +307,9 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai
} }
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { 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 { if err != nil {
return nil, err return nil, err
} }
@ -321,20 +325,21 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
} }
func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) { func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1N8fs", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx) existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved { 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) 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 { if err != nil {
return nil, err 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 { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,13 +1,14 @@
package command package command
import ( import (
"bytes"
"context" "context"
"testing" "testing"
"github.com/caos/zitadel/internal/api/authz"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore" "github.com/caos/zitadel/internal/eventstore"
@ -452,8 +453,8 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) {
storage static.Storage storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -465,20 +466,6 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) {
args args args args
res res 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", name: "label policy not existing, not found error",
fields: fields{ fields: fields{
@ -488,13 +475,61 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "logo",
ContentType: "text/css",
ObjectType: static.ObjectTypeStyling,
File: nil,
Size: 0,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "logo added, ok",
fields: fields{ fields: fields{
@ -523,16 +558,24 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
instance.NewLabelPolicyLogoAddedEvent(context.Background(), instance.NewLabelPolicyLogoAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
"key", "logo",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -545,8 +588,9 @@ func TestCommandSide_AddLogoDefaultLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -593,7 +637,6 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) {
err: caos_errs.IsNotFound, err: caos_errs.IsNotFound,
}, },
}, },
{ {
name: "asset remove error, internal error", name: "asset remove error, internal error",
fields: fields{ fields: fields{
@ -708,10 +751,11 @@ func TestCommandSide_RemoveLogoDefaultLabelPolicy(t *testing.T) {
func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -723,20 +767,6 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) {
args args args args
res res 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", name: "label policy not existing, not found error",
fields: fields{ fields: fields{
@ -746,13 +776,61 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "icon added, ok",
fields: fields{ fields: fields{
@ -781,16 +859,24 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
instance.NewLabelPolicyIconAddedEvent(context.Background(), instance.NewLabelPolicyIconAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
"key", "icon",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -803,8 +889,9 @@ func TestCommandSide_AddIconDefaultLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -926,11 +1013,12 @@ func TestCommandSide_RemoveIconDefaultLabelPolicy(t *testing.T) {
func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
instanceID string instanceID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -942,21 +1030,6 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) {
args args args args
res res 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", name: "label policy not existing, not found error",
fields: fields{ fields: fields{
@ -968,12 +1041,61 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) {
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
instanceID: "INSTANCE", instanceID: "INSTANCE",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "logo dark added, ok",
fields: fields{ fields: fields{
@ -1002,16 +1124,24 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
instance.NewLabelPolicyLogoDarkAddedEvent(context.Background(), instance.NewLabelPolicyLogoDarkAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
"key", "logo",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1024,8 +1154,9 @@ func TestCommandSide_AddLogoDarkDefaultLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1186,10 +1317,11 @@ func TestCommandSide_RemoveLogoDarkDefaultLabelPolicy(t *testing.T) {
func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1201,20 +1333,6 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) {
args args args args
res res 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", name: "label policy not existing, not found error",
fields: fields{ fields: fields{
@ -1224,13 +1342,61 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "icon dark added, ok",
fields: fields{ fields: fields{
@ -1259,16 +1425,24 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
instance.NewLabelPolicyIconDarkAddedEvent(context.Background(), instance.NewLabelPolicyIconDarkAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
"key", "icon",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1281,8 +1455,9 @@ func TestCommandSide_AddIconDarkDefaultLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1443,10 +1618,11 @@ func TestCommandSide_RemoveIconDarkDefaultLabelPolicy(t *testing.T) {
func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) { func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1458,20 +1634,6 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) {
args args args args
res res 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", name: "label policy not existing, not found error",
fields: fields{ fields: fields{
@ -1481,13 +1643,61 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "font",
ContentType: "ttf",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "font added, ok",
fields: fields{ fields: fields{
@ -1516,16 +1726,24 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
instance.NewLabelPolicyFontAddedEvent(context.Background(), instance.NewLabelPolicyFontAddedEvent(context.Background(),
&instance.NewAggregate("INSTANCE").Aggregate, &instance.NewAggregate("INSTANCE").Aggregate,
"key", "font",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "IAM",
ObjectName: "font",
ContentType: "ttf",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1538,8 +1756,9 @@ func TestCommandSide_AddFontDefaultLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -6,6 +6,7 @@ import (
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/org" "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) { 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 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 == "" { if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-KKd4X", "Errors.ResourceOwnerMissing") 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) existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-23BMs", "Errors.Org.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4MVsf", "Errors.Org.LabelPolicy.NotFound") 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 { if err != nil {
return nil, err return nil, err
} }
@ -205,13 +207,10 @@ func (c *Commands) RemoveLogoLabelPolicy(ctx context.Context, orgID string) (*do
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil 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 == "" { if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-hMDs3", "Errors.ResourceOwnerMissing") 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) existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4nq2f", "Errors.Org.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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") 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 { if err != nil {
return nil, err return nil, err
} }
@ -261,13 +264,10 @@ func (c *Commands) RemoveIconLabelPolicy(ctx context.Context, orgID string) (*do
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil 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 == "" { if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-67Ms2", "Errors.ResourceOwnerMissing") 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) existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-QSqcd", "Errors.Org.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-0peQw", "Errors.Org.LabelPolicy.NotFound") 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 { if err != nil {
return nil, err return nil, err
} }
@ -316,13 +320,10 @@ func (c *Commands) RemoveLogoDarkLabelPolicy(ctx context.Context, orgID string)
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil 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 == "" { if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-tzBfs", "Errors.ResourceOwnerMissing") 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) existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4Nf8s", "Errors.Org.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err return nil, err
} }
@ -367,13 +372,10 @@ func (c *Commands) RemoveIconDarkLabelPolicy(ctx context.Context, orgID string)
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil 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 == "" { if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-1Nf9s", "Errors.ResourceOwnerMissing") 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) existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-2M9fs", "Errors.Org.LabelPolicy.NotFound") 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) 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 { if err != nil {
return nil, err 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") 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 { if err != nil {
return nil, err return nil, err
} }
@ -463,7 +469,7 @@ func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string)
if existingPolicy.State != domain.PolicyStateActive { if existingPolicy.State != domain.PolicyStateActive {
return nil, nil return nil, nil
} }
err = c.RemoveAssetsFolder(ctx, orgID, domain.LabelPolicyPrefix+"/", true) err = c.removeAssetsFolder(ctx, orgID, static.ObjectTypeStyling)
if err != nil { if err != nil {
return nil, err 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) { 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"bytes"
"context" "context"
"testing" "testing"
@ -782,9 +783,9 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) {
storage static.Storage storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -803,24 +804,17 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) {
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{ args: args{
ctx: context.Background(), 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{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -835,14 +829,63 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "logo added, ok",
fields: fields{ fields: fields{
@ -871,17 +914,25 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
org.NewLabelPolicyLogoAddedEvent(context.Background(), org.NewLabelPolicyLogoAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, &org.NewAggregate("org1", "org1").Aggregate,
"key", "logo",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -896,7 +947,7 @@ func TestCommandSide_AddLogoLabelPolicy(t *testing.T) {
eventstore: tt.fields.eventstore, eventstore: tt.fields.eventstore,
static: tt.fields.storage, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1039,11 +1090,12 @@ func TestCommandSide_RemoveLogoLabelPolicy(t *testing.T) {
func TestCommandSide_AddIconLabelPolicy(t *testing.T) { func TestCommandSide_AddIconLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1062,24 +1114,17 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) {
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{ args: args{
ctx: context.Background(), 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{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -1094,14 +1139,63 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "icon added, ok",
fields: fields{ fields: fields{
@ -1130,17 +1224,25 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
org.NewLabelPolicyIconAddedEvent(context.Background(), org.NewLabelPolicyIconAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, &org.NewAggregate("org1", "org1").Aggregate,
"key", "icon",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1153,8 +1255,9 @@ func TestCommandSide_AddIconLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1294,11 +1397,12 @@ func TestCommandSide_RemoveIconLabelPolicy(t *testing.T) {
func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) { func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1319,22 +1423,15 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) {
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "",
}, upload: &AssetUpload{
res: res{ ResourceOwner: "org1",
err: caos_errs.IsErrorInvalidArgument, ObjectName: "logo",
}, ContentType: "image",
}, ObjectType: static.ObjectTypeStyling,
{ File: bytes.NewReader([]byte("test")),
name: "storage key empty, invalid argument error", Size: 4,
fields: fields{ },
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
}, },
res: res{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -1349,14 +1446,63 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "logo dark added, ok",
fields: fields{ fields: fields{
@ -1385,17 +1531,25 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
org.NewLabelPolicyLogoDarkAddedEvent(context.Background(), org.NewLabelPolicyLogoDarkAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, &org.NewAggregate("org1", "org1").Aggregate,
"key", "logo",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "logo",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1408,8 +1562,9 @@ func TestCommandSide_AddLogoDarkLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1555,9 +1710,9 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) {
storage static.Storage storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1576,24 +1731,17 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) {
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{ args: args{
ctx: context.Background(), 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{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -1608,14 +1756,63 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "icon dark added, ok",
fields: fields{ fields: fields{
@ -1644,17 +1841,25 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
org.NewLabelPolicyIconDarkAddedEvent(context.Background(), org.NewLabelPolicyIconDarkAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, &org.NewAggregate("org1", "org1").Aggregate,
"key", "icon",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "icon",
ContentType: "image",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1667,8 +1872,9 @@ func TestCommandSide_AddIconDarkLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }
@ -1806,11 +2012,12 @@ func TestCommandSide_RemoveIconDarkLabelPolicy(t *testing.T) {
func TestCommandSide_AddFontLabelPolicy(t *testing.T) { func TestCommandSide_AddFontLabelPolicy(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -1829,24 +2036,17 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) {
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{ args: args{
ctx: context.Background(), 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{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -1861,14 +2061,55 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key",
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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", name: "font added, ok",
fields: fields{ fields: fields{
@ -1897,17 +2138,25 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
org.NewLabelPolicyFontAddedEvent(context.Background(), org.NewLabelPolicyFontAddedEvent(context.Background(),
&org.NewAggregate("org1", "org1").Aggregate, &org.NewAggregate("org1", "org1").Aggregate,
"key", "font",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "font",
ContentType: "ttf",
ObjectType: static.ObjectTypeStyling,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -1920,8 +2169,9 @@ func TestCommandSide_AddFontLabelPolicy(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -1,27 +1,70 @@
package command package command
import ( import (
"bytes"
"context" "context"
"io" "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, return c.static.PutObject(ctx,
bucketName, authz.GetInstance(ctx).InstanceID(),
objectName, "",
contentType, upload.ResourceOwner,
upload.ObjectName,
upload.ContentType,
upload.ObjectType,
file, file,
size, size,
true,
) )
} }
func (c *Commands) RemoveAsset(ctx context.Context, bucketName, storeKey string) error { func (c *Commands) removeAsset(ctx context.Context, resourceOwner, storeKey string) error {
return c.static.RemoveObject(ctx, bucketName, storeKey) return c.static.RemoveObject(ctx, authz.GetInstance(ctx).InstanceID(), resourceOwner, storeKey)
} }
func (c *Commands) RemoveAssetsFolder(ctx context.Context, bucketName, path string, recursive bool) error { func (c *Commands) removeAssetsFolder(ctx context.Context, resourceOwner string, objectType static.ObjectType) error {
return c.static.RemoveObjects(ctx, bucketName, path, recursive) 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")
} }

View File

@ -8,13 +8,10 @@ import (
"github.com/caos/zitadel/internal/repository/user" "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 == "" { if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "USER-Ba5Ds", "Errors.IDMissing") 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) existingUser, err := c.userWriteModelByID(ctx, userID, orgID)
if err != nil { if err != nil {
return nil, err 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 { if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "USER-vJ3fS", "Errors.Users.NotFound") 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) 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 { if err != nil {
return nil, err 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 { if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "USER-35N8f", "Errors.Users.NotFound") 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 { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,6 +1,7 @@
package command package command
import ( import (
"bytes"
"context" "context"
"testing" "testing"
@ -20,12 +21,13 @@ import (
func TestCommandSide_AddHumanAvatar(t *testing.T) { func TestCommandSide_AddHumanAvatar(t *testing.T) {
type fields struct { type fields struct {
eventstore *eventstore.Eventstore eventstore *eventstore.Eventstore
storage static.Storage
} }
type args struct { type args struct {
ctx context.Context ctx context.Context
orgID string orgID string
userID string userID string
storageKey string upload *AssetUpload
} }
type res struct { type res struct {
want *domain.ObjectDetails want *domain.ObjectDetails
@ -44,25 +46,18 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
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{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "",
userID: "user1", userID: "",
upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsErrorInvalidArgument, err: caos_errs.IsErrorInvalidArgument,
@ -77,17 +72,65 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
), ),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
userID: "user1", userID: "user1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
err: caos_errs.IsNotFound, 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{ fields: fields{
eventstore: eventstoreExpect( eventstore: eventstoreExpect(
t, t,
@ -112,18 +155,26 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
eventFromEventPusher( eventFromEventPusher(
user.NewHumanAvatarAddedEvent(context.Background(), user.NewHumanAvatarAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate, &user.NewAggregate("user1", "org1").Aggregate,
"key", "avatar",
), ),
), ),
}, },
), ),
), ),
storage: mock.NewStorage(t).ExpectPutObject(),
}, },
args: args{ args: args{
ctx: context.Background(), ctx: context.Background(),
orgID: "org1", orgID: "org1",
userID: "user1", userID: "user1",
storageKey: "key", upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
}, },
res: res{ res: res{
want: &domain.ObjectDetails{ want: &domain.ObjectDetails{
@ -136,8 +187,9 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
r := &Commands{ r := &Commands{
eventstore: tt.fields.eventstore, 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 { if tt.res.err == nil {
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -27,8 +27,32 @@ const (
) )
type InstanceSetup struct { type InstanceSetup struct {
Org OrgSetup Org OrgSetup
Zitadel ZitadelConfig 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 { PasswordComplexityPolicy struct {
MinLength uint64 MinLength uint64
HasLowercase bool HasLowercase bool
@ -170,6 +194,31 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup)
projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID) projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID)
validations := []preparation.Validation{ 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( AddPasswordComplexityPolicy(
instanceAgg, instanceAgg,
setup.PasswordComplexityPolicy.MinLength, setup.PasswordComplexityPolicy.MinLength,

View File

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

View File

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

View File

@ -62,9 +62,9 @@ func NewFeatureProjection(ctx context.Context, config crdb.StatementHandlerConfi
crdb.NewColumn(FeatureSequenceCol, crdb.ColumnTypeInt64), crdb.NewColumn(FeatureSequenceCol, crdb.ColumnTypeInt64),
crdb.NewColumn(FeatureIsDefaultCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(FeatureIsDefaultCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(FeatureTierNameCol, crdb.ColumnTypeText), 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(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(FeatureAuditLogRetentionCol, crdb.ColumnTypeInt64, crdb.Default(0)),
crdb.NewColumn(FeatureLoginPolicyFactorsCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(FeatureLoginPolicyFactorsCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(FeatureLoginPolicyIDPCol, crdb.ColumnTypeBool, crdb.Default(false)), crdb.NewColumn(FeatureLoginPolicyIDPCol, crdb.ColumnTypeBool, crdb.Default(false)),

View File

@ -1,114 +1,30 @@
package config package config
import ( import (
"context" "database/sql"
"encoding/json"
"io"
"net/url"
"time"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static" "github.com/caos/zitadel/internal/static"
"github.com/caos/zitadel/internal/static/database"
"github.com/caos/zitadel/internal/static/s3" "github.com/caos/zitadel/internal/static/s3"
) )
type AssetStorageConfig struct { type AssetStorageConfig struct {
Type string Type string
Config static.Config Config map[string]interface{} `mapstructure:",remain"`
} }
var storage = map[string]func() static.Config{ func (a *AssetStorageConfig) NewStorage(client *sql.DB) (static.Storage, error) {
"s3": func() static.Config { return &s3.Config{} }, t, ok := storage[a.Type]
"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]
if !ok { 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() return t(client, a.Config)
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
} }
var ( var storage = map[string]static.CreateStorage{
errNoStorage = errors.ThrowInternal(nil, "STATIC-ashg4", "Errors.Assets.Store.NotConfigured") "db": database.NewStorage,
) "": database.NewStorage,
"s3": s3.NewStorage,
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
} }

View File

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

View File

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

View File

@ -22,6 +22,7 @@ Errors:
Object: Object:
PutFailed: Objekt konnte nicht erstellt werden PutFailed: Objekt konnte nicht erstellt werden
GetFailed: Objekt konnte nicht gelesen werden GetFailed: Objekt konnte nicht gelesen werden
NotFound: Objekt konnte nicht gefunden werden
PresignedTokenFailed: Signiertes Token konnte nicht erstellt werden PresignedTokenFailed: Signiertes Token konnte nicht erstellt werden
ListFailed: Objektliste konnte nicht gelesen werden ListFailed: Objektliste konnte nicht gelesen werden
RemoveFailed: Objekt konnte nicht gelöscht werden RemoveFailed: Objekt konnte nicht gelöscht werden

View File

@ -22,6 +22,7 @@ Errors:
Object: Object:
PutFailed: Object not created PutFailed: Object not created
GetFailed: Object could not be read GetFailed: Object could not be read
NotFound: Object could not be found
PresignedTokenFailed: Signed token could not be created PresignedTokenFailed: Signed token could not be created
ListFailed: Objectlist could not be read ListFailed: Objectlist could not be read
RemoveFailed: Object could not be removed RemoveFailed: Object could not be removed

View File

@ -22,6 +22,7 @@ Errors:
Object: Object:
PutFailed: Oggetto non creato PutFailed: Oggetto non creato
GetFailed: Oggetto non può essere letto GetFailed: Oggetto non può essere letto
NotFound: Oggetto non trovato
PresignedTokenFailed: Il token non può essere creato PresignedTokenFailed: Il token non può essere creato
ListFailed: La lista degli oggetti non può essere letta ListFailed: La lista degli oggetti non può essere letta
RemoveFailed: L'oggetto non può essere rimosso RemoveFailed: L'oggetto non può essere rimosso

View File

@ -7,11 +7,8 @@ package mock
import ( import (
context "context" context "context"
io "io" io "io"
url "net/url"
reflect "reflect" reflect "reflect"
time "time"
domain "github.com/caos/zitadel/internal/domain"
static "github.com/caos/zitadel/internal/static" static "github.com/caos/zitadel/internal/static"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
) )
@ -39,187 +36,76 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder 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. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetObject", ctx, bucketName, objectName) ret := m.ctrl.Call(m, "GetObject", ctx, instanceID, resourceOwner, name)
ret0, _ := ret[0].(io.Reader) ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(func() (*domain.AssetInfo, error)) ret1, _ := ret[1].(func() (*static.Asset, error))
ret2, _ := ret[2].(error) ret2, _ := ret[2].(error)
return ret0, ret1, ret2 return ret0, ret1, ret2
} }
// GetObject indicates an expected call of GetObject. // 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() 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. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetObjectInfo", ctx, bucketName, objectName) ret := m.ctrl.Call(m, "GetObjectInfo", ctx, instanceID, resourceOwner, name)
ret0, _ := ret[0].(*domain.AssetInfo) ret0, _ := ret[0].(*static.Asset)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// GetObjectInfo indicates an expected call of GetObjectInfo. // 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() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, bucketName, objectName) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, instanceID, resourceOwner, name)
}
// 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)
} }
// PutObject mocks base method. // 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() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutObject", ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting) ret := m.ctrl.Call(m, "PutObject", ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
ret0, _ := ret[0].(*domain.AssetInfo) ret0, _ := ret[0].(*static.Asset)
ret1, _ := ret[1].(error) ret1, _ := ret[1].(error)
return ret0, ret1 return ret0, ret1
} }
// PutObject indicates an expected call of PutObject. // 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() 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) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
}
// 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)
} }
// RemoveObject mocks base method. // 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() 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) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// RemoveObject indicates an expected call of RemoveObject. // 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() 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. // 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() 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) ret0, _ := ret[0].(error)
return ret0 return ret0
} }
// RemoveObjects indicates an expected call of RemoveObjects. // 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() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, bucketName, path, recursive) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, instanceID, resourceOwner, objectType)
}
// 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))
} }

View File

@ -1,34 +1,49 @@
package mock package mock
import ( import (
"context"
"io"
"testing" "testing"
"time"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
caos_errors "github.com/caos/zitadel/internal/errors" caos_errors "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
) )
func NewStorage(t *testing.T) *MockStorage { func NewStorage(t *testing.T) *MockStorage {
return NewMockStorage(gomock.NewController(t)) return NewMockStorage(gomock.NewController(t))
} }
func (m *MockStorage) ExpectAddObjectNoError() *MockStorage { func (m *MockStorage) ExpectPutObject() *MockStorage {
m.EXPECT(). 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, nil) 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 return m
} }
func (m *MockStorage) ExpectAddObjectError() *MockStorage { func (m *MockStorage) ExpectPutObjectError() *MockStorage {
m.EXPECT(). 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(nil, caos_errors.ThrowInternal(nil, "", ""))
return m return m
} }
func (m *MockStorage) ExpectRemoveObjectNoError() *MockStorage { func (m *MockStorage) ExpectRemoveObjectNoError() *MockStorage {
m.EXPECT(). m.EXPECT().
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()). RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil) Return(nil)
return m return m
} }
@ -42,7 +57,7 @@ func (m *MockStorage) ExpectRemoveObjectsNoError() *MockStorage {
func (m *MockStorage) ExpectRemoveObjectError() *MockStorage { func (m *MockStorage) ExpectRemoveObjectError() *MockStorage {
m.EXPECT(). m.EXPECT().
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()). RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(caos_errors.ThrowInternal(nil, "", "")) Return(caos_errors.ThrowInternal(nil, "", ""))
return m return m
} }

View File

@ -1,9 +1,13 @@
package s3 package s3
import ( import (
"database/sql"
"encoding/json"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static" "github.com/caos/zitadel/internal/static"
) )
@ -34,3 +38,15 @@ func (c *Config) NewStorage() (static.Storage, error) {
MultiDelete: c.MultiDelete, MultiDelete: c.MultiDelete,
}, nil }, 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()
}

View File

@ -2,11 +2,10 @@ package s3
import ( import (
"context" "context"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time"
"github.com/caos/logging" "github.com/caos/logging"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@ -14,8 +13,11 @@ import (
"github.com/caos/zitadel/internal/domain" "github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors" caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
) )
var _ static.Storage = (*Minio)(nil)
type Minio struct { type Minio struct {
Client *minio.Client Client *minio.Client
Location string Location string
@ -23,129 +25,66 @@ type Minio struct {
MultiDelete bool MultiDelete bool
} }
func (m *Minio) CreateBucket(ctx context.Context, name, location string) error { func (m *Minio) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
if location == "" { err := m.createBucket(ctx, instanceID, location)
location = m.Location if err != nil && !caos_errs.IsErrorAlreadyExists(err) {
return nil, err
} }
name = m.prefixBucketName(name) bucketName := m.prefixBucketName(instanceID)
exists, err := m.Client.BucketExists(ctx, name) objectName := fmt.Sprintf("%s/%s", resourceOwner, 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)
info, err := m.Client.PutObject(ctx, bucketName, objectName, object, objectSize, minio.PutObjectOptions{ContentType: contentType}) info, err := m.Client.PutObject(ctx, bucketName, objectName, object, objectSize, minio.PutObjectOptions{ContentType: contentType})
if err != nil { if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-590sw", "Errors.Assets.Object.PutFailed") return nil, caos_errs.ThrowInternal(err, "MINIO-590sw", "Errors.Assets.Object.PutFailed")
} }
return &domain.AssetInfo{ return &static.Asset{
Bucket: info.Bucket, InstanceID: info.Bucket,
Key: info.Key, ResourceOwner: resourceOwner,
ETag: info.ETag, Name: info.Key,
Size: info.Size, Hash: info.ETag,
LastModified: info.LastModified, Size: info.Size,
Location: info.Location, LastModified: info.LastModified,
VersionID: info.VersionID, Location: info.Location,
ContentType: contentType,
}, nil }, nil
} }
func (m *Minio) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) { func (m *Minio) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
bucketName = m.prefixBucketName(bucketName) bucketName := m.prefixBucketName(instanceID)
objectinfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{}) 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 err != nil {
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound { 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.ThrowNotFound(err, "MINIO-Gdfh4", "Errors.Assets.Object.GetFailed")
} }
return nil, caos_errs.ThrowInternal(err, "MINIO-1vySX", "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) { func (m *Minio) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
bucketName = m.prefixBucketName(bucketName) bucketName := m.prefixBucketName(instanceID)
object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{}) objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
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)
err := m.Client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{}) err := m.Client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{})
if err != nil { if err != nil {
return caos_errs.ThrowInternal(err, "MINIO-x85RT", "Errors.Assets.Object.RemoveFailed") 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 return nil
} }
func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error { func (m *Minio) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
bucketName = m.prefixBucketName(bucketName) bucketName := m.prefixBucketName(instanceID)
objectsCh := make(chan minio.ObjectInfo) objectsCh := make(chan minio.ObjectInfo)
g := new(errgroup.Group) g := new(errgroup.Group)
var path string
switch objectType {
case static.ObjectTypeStyling:
path = domain.LabelPolicyPrefix + "/"
default:
return nil
}
g.Go(func() error { g.Go(func() error {
defer close(objectsCh) defer close(objectsCh)
objects, cancel := m.listObjects(ctx, bucketName, path, recursive) objects, cancel := m.listObjects(ctx, bucketName, resourceOwner, true)
for object := range objects { for object := range objects {
if err := object.Err; err != nil { if err := object.Err; err != nil {
cancel() cancel()
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound { 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 continue
} }
return caos_errs.ThrowInternal(object.Err, "MINIO-WQF32", "Errors.Assets.Object.ListFailed") 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() 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) { func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recursive bool) (<-chan minio.ObjectInfo, context.CancelFunc) {
ctxCancel, cancel := context.WithCancel(ctx) ctxCancel, cancel := context.WithCancel(ctx)
@ -198,17 +165,15 @@ func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recu
}), cancel }), cancel
} }
func (m *Minio) objectToAssetInfo(bucketName string, object minio.ObjectInfo) *domain.AssetInfo { func (m *Minio) objectToAssetInfo(bucketName string, resourceOwner string, object minio.ObjectInfo) *static.Asset {
return &domain.AssetInfo{ return &static.Asset{
Bucket: bucketName, InstanceID: bucketName,
Key: object.Key, ResourceOwner: resourceOwner,
ETag: object.ETag, Name: object.Key,
Size: object.Size, Hash: object.ETag,
LastModified: object.LastModified, Size: object.Size,
VersionID: object.VersionID, LastModified: object.LastModified,
Expiration: object.Expires, ContentType: object.ContentType,
ContentType: object.ContentType,
AutheticatedURL: m.Client.EndpointURL().String() + "/" + bucketName + "/" + object.Key,
} }
} }

View File

@ -2,25 +2,36 @@ package static
import ( import (
"context" "context"
"database/sql"
"io" "io"
"net/url"
"time" "time"
"github.com/caos/zitadel/internal/domain"
) )
type CreateStorage func(client *sql.DB, rawConfig map[string]interface{}) (Storage, error)
type Storage interface { type Storage interface {
CreateBucket(ctx context.Context, name, location string) error PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType ObjectType, object io.Reader, objectSize int64) (*Asset, error)
RemoveBucket(ctx context.Context, name string) error GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*Asset, error), error)
ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*Asset, error)
PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error
GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType ObjectType) error
GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) //TODO: add functionality to move asset location
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
} }
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
} }

View File

@ -1,8 +1,6 @@
package model package model
import ( import (
"context"
"net/url"
"time" "time"
"golang.org/x/text/language" "golang.org/x/text/language"
@ -11,7 +9,6 @@ import (
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models" "github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/model" iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/static"
) )
type UserView struct { type UserView struct {
@ -41,7 +38,6 @@ type HumanView struct {
DisplayName string DisplayName string
AvatarKey string AvatarKey string
AvatarURL string AvatarURL string
PreSignedAvatar *url.URL
PreferredLanguage string PreferredLanguage string
Gender Gender Gender Gender
Email string Email string
@ -257,23 +253,6 @@ func (u *UserView) GetProfile() (*Profile, error) {
}, nil }, 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) { func (u *UserView) GetPhone() (*Phone, error) {
if u.HumanView == nil { if u.HumanView == nil {
return nil, errors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman") return nil, errors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman")