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 (
"context"
command "github.com/caos/zitadel/internal/command/v2"
"database/sql"
_ "embed"
)
type DefaultInstance struct {
cmd *command.Command
InstanceSetup command.InstanceSetup
const (
createAssets = `
CREATE TABLE system.assets (
instance_id TEXT,
asset_type TEXT,
resource_owner TEXT,
name TEXT,
content_type TEXT,
hash TEXT AS (md5(data)) STORED,
data BYTES,
updated_at TIMESTAMPTZ,
PRIMARY KEY (instance_id, resource_owner, name)
);
`
)
type AssetTable struct {
dbClient *sql.DB
}
func (mig *DefaultInstance) Execute(ctx context.Context) error {
_, err := mig.cmd.SetUpInstance(ctx, &mig.InstanceSetup)
func (mig *AssetTable) Execute(ctx context.Context) error {
_, err := mig.dbClient.ExecContext(ctx, createAssets)
return err
}
func (mig *DefaultInstance) String() string {
return "02_default_instance"
func (mig *AssetTable) String() string {
return "02_assets"
}

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 {
S1ProjectionTable *ProjectionTable
S2DefaultInstance *DefaultInstance
s1ProjectionTable *ProjectionTable
s2AssetsTable *AssetTable
S3DefaultInstance *DefaultInstance
}
func MustNewSteps(v *viper.Viper) *Steps {

View File

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

View File

@ -1,4 +1,4 @@
S2DefaultInstance:
S3DefaultInstance:
InstanceSetup:
Org:
Name: ZITADEL
@ -13,6 +13,29 @@ S2DefaultInstance:
Gender:
Phone:
Password: Password1!
Features:
TierName: Default Tier
TierDescription: ""
State: 1 #active
StateDescription: ""
Retention: 8760h #1year
LoginPolicyFactors: true
LoginPolicyIDP: true
LoginPolicyPasswordless: true
LoginPolicyRegistration: true
LoginPolicyUsernameLogin: true
LoginPolicyPasswordReset: true
PasswordComplexityPolicy: true
LabelPolicyPrivateLabel: true
LabelPolicyWatermark: true
CustomDomain: true
PrivacyPolicy: true
MetadataUser: true
CustomTextMessage: true
CustomTextLogin: true
LockoutPolicy: true
ActionsAllowed: 2 #ActionsAllowedUnlimited
MaxActions: #not necessary because of ActionsAllowedUnlimited
PasswordComplexityPolicy:
MinLength: 8
HasLowercase: true

View File

@ -94,12 +94,6 @@ func startZitadel(config *Config, masterKey string) error {
return err
}
var storage static.Storage
//TODO: enable when storage is implemented again
//if *assetsEnabled {
//storage, err = config.AssetStorage.Config.NewStorage()
//logging.Log("MAIN-Bfhe2").OnError(err).Fatal("Unable to start asset storage")
//}
eventstoreClient, err := eventstore.Start(dbClient)
if err != nil {
return fmt.Errorf("cannot start eventstore for queries: %w", err)
@ -114,6 +108,11 @@ func startZitadel(config *Config, masterKey string) error {
if err != nil {
return fmt.Errorf("error starting authz repo: %w", err)
}
storage, err := config.AssetStorage.NewStorage(dbClient)
if err != nil {
return fmt.Errorf("cannot start asset storage client: %w", err)
}
webAuthNConfig := webauthn.Config{
ID: config.ExternalDomain,
Origin: http_util.BuildHTTP(config.ExternalDomain, config.ExternalPort, config.ExternalSecure),
@ -163,13 +162,13 @@ func startAPIs(ctx context.Context, router *mux.Router, commands *command.Comman
return err
}
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries))
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader)
apis.RegisterHandler(assets.HandlerPrefix, assets.NewHandler(commands, verifier, config.InternalAuthZ, id.SonyFlakeGenerator, store, queries, instanceInterceptor.Handler))
userAgentInterceptor, err := middleware.NewUserAgentHandler(config.UserAgentCookie, keys.UserAgentCookieKey, config.ExternalDomain, id.SonyFlakeGenerator, config.ExternalSecure)
if err != nil {
return err
}
instanceInterceptor := middleware.InstanceInterceptor(queries, config.HTTP1HostHeader)
issuer := oidc.Issuer(config.ExternalDomain, config.ExternalPort, config.ExternalSecure)
oidcProvider, err := oidc.NewProvider(ctx, config.OIDC, issuer, login.DefaultLoggedOutPath, commands, queries, authRepo, config.SystemDefaults.KeyConfig, keys.OIDC, keys.OIDCKey, eventstore, dbClient, keyChan, userAgentInterceptor, instanceInterceptor.Handler)

View File

@ -163,7 +163,7 @@ func (m *Styling) generateStylingFile(policy *iam_model.LabelPolicyView) error {
if err != nil {
return err
}
return m.uploadFilesToBucket(policy.AggregateID, "text/css", reader, size)
return m.uploadFilesToStorage(policy.InstanceID, policy.AggregateID, "text/css", reader, size)
}
func (m *Styling) writeFile(policy *iam_model.LabelPolicyView) (io.Reader, int64, error) {
@ -245,9 +245,10 @@ const fontFaceTemplate = `
}
`
func (m *Styling) uploadFilesToBucket(aggregateID, contentType string, reader io.Reader, size int64) error {
func (m *Styling) uploadFilesToStorage(instanceID, aggregateID, contentType string, reader io.Reader, size int64) error {
fileName := domain.CssPath + "/" + domain.CssVariablesFileName
_, err := m.static.PutObject(context.Background(), aggregateID, fileName, contentType, reader, size, true)
//TODO: handle location as soon as possible
_, err := m.static.PutObject(context.Background(), instanceID, "", aggregateID, fileName, contentType, static.ObjectTypeStyling, reader, size)
return err
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,12 @@
package login
import (
"context"
"net/http"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/api/assets"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/domain"
)
type dynamicResourceData struct {
@ -25,74 +26,11 @@ func (l *Login) handleDynamicResources(w http.ResponseWriter, r *http.Request) {
return
}
bucketName := authz.GetInstance(r.Context()).InstanceID()
resourceOwner := authz.GetInstance(r.Context()).InstanceID()
if data.OrgID != "" && !data.DefaultPolicy {
bucketName = data.OrgID
resourceOwner = data.OrgID
}
etag := r.Header.Get("If-None-Match")
asset, info, err := l.getStatic(r.Context(), bucketName, data.FileName)
if info != nil && info.ETag == etag {
w.WriteHeader(304)
return
}
if err != nil {
return
}
//TODO: enable again when assets are implemented again
_ = asset
//w.Header().Set("content-length", strconv.FormatInt(info.Size, 10))
//w.Header().Set("content-type", info.ContentType)
//w.Header().Set("ETag", info.ETag)
//w.Write(asset)
}
func (l *Login) getStatic(ctx context.Context, bucketName, fileName string) ([]byte, *domain.AssetInfo, error) {
s := new(staticAsset)
//TODO: enable again when assets are implemented again
//key := bucketName + "-" + fileName
//err := l.staticCache.Get(key, s)
//if err == nil && s.Info != nil && (s.Info.Expiration.After(time.Now().Add(-1 * time.Minute))) { //TODO: config?
// return s.Data, s.Info, nil
//}
//info, err := l.staticStorage.GetObjectInfo(ctx, bucketName, fileName)
//if err != nil {
// if caos_errs.IsNotFound(err) {
// return nil, nil, err
// }
// return s.Data, s.Info, err
//}
//if s.Info != nil && s.Info.ETag == info.ETag {
// if info.Expiration.After(s.Info.Expiration) {
// s.Info = info
// //l.cacheStatic(bucketName, fileName, s)
// }
// return s.Data, s.Info, nil
//}
//
//reader, _, err := l.staticStorage.GetObject(ctx, bucketName, fileName)
//if err != nil {
// return s.Data, s.Info, err
//}
//s.Data, err = ioutil.ReadAll(reader)
//if err != nil {
// return nil, nil, err
//}
//s.Info = info
//l.cacheStatic(bucketName, fileName, s)
return s.Data, s.Info, nil
}
//TODO: enable again when assets are implemented again
//
//func (l *Login) cacheStatic(bucketName, fileName string, s *staticAsset) {
// key := bucketName + "-" + fileName
// err := l.staticCache.Set(key, &s)
// logging.Log("HANDLER-dfht2").OnError(err).Warnf("caching of asset %s: %s failed", bucketName, fileName)
//}
type staticAsset struct {
Data []byte
Info *domain.AssetInfo
err = assets.GetAsset(w, r, resourceOwner, data.FileName, l.staticStorage)
logging.WithFields("file", data.FileName, "org", resourceOwner).OnError(err).Warn("asset in login could not be served")
}

View File

@ -124,10 +124,7 @@ func (c *Commands) ActivateDefaultLabelPolicy(ctx context.Context) (*domain.Obje
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-3m20c", "Errors.Assets.EmptyKey")
}
func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
@ -136,8 +133,12 @@ func (c *Commands) AddLogoDefaultLabelPolicy(ctx context.Context, storageKey str
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Qw0pd", "Errors.IAM.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "INSTANCE-3m20c", "Errors.Assets.Object.PutFailed")
}
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoAddedEvent(ctx, instanceAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoAddedEvent(ctx, instanceAgg, asset.Name))
if err != nil {
return nil, err
}
@ -158,7 +159,7 @@ func (c *Commands) RemoveLogoDefaultLabelPolicy(ctx context.Context) (*domain.Ob
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Xc8Kf", "Errors.IAM.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoKey)
err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoKey)
if err != nil {
return nil, err
}
@ -174,10 +175,7 @@ func (c *Commands) RemoveLogoDefaultLabelPolicy(ctx context.Context) (*domain.Ob
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-yxE4f", "Errors.Assets.EmptyKey")
}
func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
@ -186,8 +184,12 @@ func (c *Commands) AddIconDefaultLabelPolicy(ctx context.Context, storageKey str
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1yMx0", "Errors.IAM.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "INSTANCE-yxE4f", "Errors.Assets.Object.PutFailed")
}
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconAddedEvent(ctx, instanceAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconAddedEvent(ctx, instanceAgg, asset.Name))
if err != nil {
return nil, err
}
@ -207,7 +209,7 @@ func (c *Commands) RemoveIconDefaultLabelPolicy(ctx context.Context) (*domain.Ob
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-4M0qw", "Errors.IAM.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconKey)
err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconKey)
if err != nil {
return nil, err
}
@ -223,20 +225,21 @@ func (c *Commands) RemoveIconDefaultLabelPolicy(ctx context.Context) (*domain.Ob
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-4fMs9", "Errors.Assets.EmptyKey")
}
func (c *Commands) AddLogoDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-ZR9fs", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-ZR9fs", "Errors.Instance.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "INSTANCE-4fMs9", "Errors.Assets.Object.PutFailed")
}
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoDarkAddedEvent(ctx, instanceAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyLogoDarkAddedEvent(ctx, instanceAgg, asset.Name))
if err != nil {
return nil, err
}
@ -254,9 +257,9 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-3FGds", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-3FGds", "Errors.Instance.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoDarkKey)
err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.LogoDarkKey)
if err != nil {
return nil, err
}
@ -272,20 +275,21 @@ func (c *Commands) RemoveLogoDarkDefaultLabelPolicy(ctx context.Context) (*domai
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1cxM3", "Errors.Assets.EmptyKey")
}
func (c *Commands) AddIconDarkDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-vMsf9", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-vMsf9", "Errors.Instance.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "INSTANCE-1cxM3", "Errors.Assets.Object.PutFailed")
}
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconDarkAddedEvent(ctx, instanceAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyIconDarkAddedEvent(ctx, instanceAgg, asset.Name))
if err != nil {
return nil, err
}
@ -303,9 +307,9 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-2nc7F", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-2nc7F", "Errors.Instance.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconDarkKey)
err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.IconDarkKey)
if err != nil {
return nil, err
}
@ -321,20 +325,21 @@ func (c *Commands) RemoveIconDarkDefaultLabelPolicy(ctx context.Context) (*domai
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, storageKey string) (*domain.ObjectDetails, error) {
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "INSTANCE-1N8fs", "Errors.Assets.EmptyKey")
}
func (c *Commands) AddFontDefaultLabelPolicy(ctx context.Context, upload *AssetUpload) (*domain.ObjectDetails, error) {
existingPolicy, err := c.defaultLabelPolicyWriteModelByID(ctx)
if err != nil {
return nil, err
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1N8fE", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-1N8fE", "Errors.Instance.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(nil, "INSTANCE-1N8fs", "Errors.Assets.Object.PutFailed")
}
instanceAgg := InstanceAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyFontAddedEvent(ctx, instanceAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, instance.NewLabelPolicyFontAddedEvent(ctx, instanceAgg, asset.Name))
if err != nil {
return nil, err
}
@ -352,9 +357,9 @@ func (c *Commands) RemoveFontDefaultLabelPolicy(ctx context.Context) (*domain.Ob
}
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Tk0gw", "Errors.IAM.LabelPolicy.NotFound")
return nil, caos_errs.ThrowNotFound(nil, "INSTANCE-Tk0gw", "Errors.Instance.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.FontKey)
err = c.removeAsset(ctx, authz.GetInstance(ctx).InstanceID(), existingPolicy.FontKey)
if err != nil {
return nil, err
}

View File

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

View File

@ -6,6 +6,7 @@ import (
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/repository/org"
"github.com/caos/zitadel/internal/static"
)
func (c *Commands) AddLabelPolicy(ctx context.Context, resourceOwner string, policy *domain.LabelPolicy) (*domain.LabelPolicy, error) {
@ -150,13 +151,10 @@ func (c *Commands) ActivateLabelPolicy(ctx context.Context, orgID string) (*doma
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-KKd4X", "Errors.ResourceOwnerMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4N3nf", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
@ -165,8 +163,12 @@ func (c *Commands) AddLogoLabelPolicy(ctx context.Context, orgID, storageKey str
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-23BMs", "Errors.Org.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "IAM-4N3nf", "Errors.Assets.Object.PutFailed")
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoAddedEvent(ctx, orgAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoAddedEvent(ctx, orgAgg, asset.Name))
if err != nil {
return nil, err
}
@ -189,7 +191,7 @@ func (c *Commands) RemoveLogoLabelPolicy(ctx context.Context, orgID string) (*do
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4MVsf", "Errors.Org.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, orgID, existingPolicy.LogoKey)
err = c.removeAsset(ctx, orgID, existingPolicy.LogoKey)
if err != nil {
return nil, err
}
@ -205,13 +207,10 @@ func (c *Commands) RemoveLogoLabelPolicy(ctx context.Context, orgID string) (*do
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-hMDs3", "Errors.ResourceOwnerMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4BS7f", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
@ -220,8 +219,12 @@ func (c *Commands) AddIconLabelPolicy(ctx context.Context, orgID, storageKey str
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4nq2f", "Errors.Org.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "IAM-4BS7f", "Errors.Assets.Object.PutFailed")
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconAddedEvent(ctx, orgAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconAddedEvent(ctx, orgAgg, asset.Name))
if err != nil {
return nil, err
}
@ -245,7 +248,7 @@ func (c *Commands) RemoveIconLabelPolicy(ctx context.Context, orgID string) (*do
return nil, caos_errs.ThrowNotFound(nil, "ORG-1nd9f", "Errors.Org.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, orgID, existingPolicy.IconKey)
err = c.removeAsset(ctx, orgID, existingPolicy.IconKey)
if err != nil {
return nil, err
}
@ -261,13 +264,10 @@ func (c *Commands) RemoveIconLabelPolicy(ctx context.Context, orgID string) (*do
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-67Ms2", "Errors.ResourceOwnerMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-3S7fN", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
@ -276,8 +276,12 @@ func (c *Commands) AddLogoDarkLabelPolicy(ctx context.Context, orgID, storageKey
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-QSqcd", "Errors.Org.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "IAM-3S7fN", "Errors.Assets.Object.PutFailed")
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoDarkAddedEvent(ctx, orgAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyLogoDarkAddedEvent(ctx, orgAgg, asset.Name))
if err != nil {
return nil, err
}
@ -300,7 +304,7 @@ func (c *Commands) RemoveLogoDarkLabelPolicy(ctx context.Context, orgID string)
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-0peQw", "Errors.Org.LabelPolicy.NotFound")
}
err = c.RemoveAsset(ctx, orgID, existingPolicy.LogoDarkKey)
err = c.removeAsset(ctx, orgID, existingPolicy.LogoDarkKey)
if err != nil {
return nil, err
}
@ -316,13 +320,10 @@ func (c *Commands) RemoveLogoDarkLabelPolicy(ctx context.Context, orgID string)
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-tzBfs", "Errors.ResourceOwnerMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "IAM-4B7cs", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
@ -331,8 +332,12 @@ func (c *Commands) AddIconDarkLabelPolicy(ctx context.Context, orgID, storageKey
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-4Nf8s", "Errors.Org.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "IAM-4B7cs", "Errors.Assets.Object.PutFailed")
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconDarkAddedEvent(ctx, orgAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyIconDarkAddedEvent(ctx, orgAgg, asset.Name))
if err != nil {
return nil, err
}
@ -367,13 +372,10 @@ func (c *Commands) RemoveIconDarkLabelPolicy(ctx context.Context, orgID string)
return writeModelToObjectDetails(&existingPolicy.LabelPolicyWriteModel.WriteModel), nil
}
func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if orgID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "Org-1Nf9s", "Errors.ResourceOwnerMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "ORG-2f9fw", "Errors.Assets.EmptyKey")
}
existingPolicy, err := c.orgLabelPolicyWriteModelByID(ctx, orgID)
if err != nil {
return nil, err
@ -382,8 +384,12 @@ func (c *Commands) AddFontLabelPolicy(ctx context.Context, orgID, storageKey str
if existingPolicy.State == domain.PolicyStateUnspecified || existingPolicy.State == domain.PolicyStateRemoved {
return nil, caos_errs.ThrowNotFound(nil, "ORG-2M9fs", "Errors.Org.LabelPolicy.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "ORG-2f9fw", "Errors.Assets.Object.PutFailed")
}
orgAgg := OrgAggregateFromWriteModel(&existingPolicy.LabelPolicyWriteModel.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyFontAddedEvent(ctx, orgAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, org.NewLabelPolicyFontAddedEvent(ctx, orgAgg, asset.Name))
if err != nil {
return nil, err
}
@ -447,7 +453,7 @@ func (c *Commands) removeLabelPolicy(ctx context.Context, existingPolicy *OrgLab
return nil, caos_errs.ThrowNotFound(nil, "Org-3M9df", "Errors.Org.LabelPolicy.NotFound")
}
err = c.RemoveAssetsFolder(ctx, existingPolicy.AggregateID, domain.LabelPolicyPrefix+"/", true)
err = c.removeAssetsFolder(ctx, existingPolicy.AggregateID, static.ObjectTypeStyling)
if err != nil {
return nil, err
}
@ -463,7 +469,7 @@ func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string)
if existingPolicy.State != domain.PolicyStateActive {
return nil, nil
}
err = c.RemoveAssetsFolder(ctx, orgID, domain.LabelPolicyPrefix+"/", true)
err = c.removeAssetsFolder(ctx, orgID, static.ObjectTypeStyling)
if err != nil {
return nil, err
}
@ -472,7 +478,7 @@ func (c *Commands) removeLabelPolicyIfExists(ctx context.Context, orgID string)
}
func (c *Commands) removeLabelPolicyAssets(ctx context.Context, existingPolicy *OrgLabelPolicyWriteModel) (*org.LabelPolicyAssetsRemovedEvent, error) {
err := c.RemoveAssetsFolder(ctx, existingPolicy.AggregateID, domain.LabelPolicyPrefix+"/", true)
err := c.removeAssetsFolder(ctx, existingPolicy.AggregateID, static.ObjectTypeStyling)
if err != nil {
return nil, err
}

View File

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

View File

@ -1,27 +1,70 @@
package command
import (
"bytes"
"context"
"io"
"strings"
"github.com/caos/zitadel/internal/domain"
"github.com/superseriousbusiness/exifremove/pkg/exifremove"
"github.com/caos/zitadel/internal/api/authz"
"github.com/caos/zitadel/internal/static"
)
func (c *Commands) UploadAsset(ctx context.Context, bucketName, objectName, contentType string, file io.Reader, size int64) (*domain.AssetInfo, error) {
type AssetUpload struct {
ResourceOwner string
ObjectName string
ContentType string
ObjectType static.ObjectType
File io.Reader
Size int64
}
func (c *Commands) uploadAsset(ctx context.Context, upload *AssetUpload) (*static.Asset, error) {
//TODO: handle location as soon as possible
file, size, err := removeExif(upload.File, upload.Size, upload.ContentType)
if err != nil {
return nil, err
}
return c.static.PutObject(ctx,
bucketName,
objectName,
contentType,
authz.GetInstance(ctx).InstanceID(),
"",
upload.ResourceOwner,
upload.ObjectName,
upload.ContentType,
upload.ObjectType,
file,
size,
true,
)
}
func (c *Commands) RemoveAsset(ctx context.Context, bucketName, storeKey string) error {
return c.static.RemoveObject(ctx, bucketName, storeKey)
func (c *Commands) removeAsset(ctx context.Context, resourceOwner, storeKey string) error {
return c.static.RemoveObject(ctx, authz.GetInstance(ctx).InstanceID(), resourceOwner, storeKey)
}
func (c *Commands) RemoveAssetsFolder(ctx context.Context, bucketName, path string, recursive bool) error {
return c.static.RemoveObjects(ctx, bucketName, path, recursive)
func (c *Commands) removeAssetsFolder(ctx context.Context, resourceOwner string, objectType static.ObjectType) error {
return c.static.RemoveObjects(ctx, authz.GetInstance(ctx).InstanceID(), resourceOwner, objectType)
}
func removeExif(file io.Reader, size int64, contentType string) (io.Reader, int64, error) {
if !isAllowedContentType(contentType) {
return file, size, nil
}
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(file)
if err != nil {
return file, 0, err
}
data, err := exifremove.Remove(buf.Bytes())
if err != nil {
return nil, 0, err
}
return bytes.NewReader(data), int64(len(data)), nil
}
func isAllowedContentType(contentType string) bool {
return strings.HasSuffix(contentType, "png") ||
strings.HasSuffix(contentType, "jpg") ||
strings.HasSuffix(contentType, "jpeg")
}

View File

@ -8,13 +8,10 @@ import (
"github.com/caos/zitadel/internal/repository/user"
)
func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID, storageKey string) (*domain.ObjectDetails, error) {
func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID string, upload *AssetUpload) (*domain.ObjectDetails, error) {
if userID == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "USER-Ba5Ds", "Errors.IDMissing")
}
if storageKey == "" {
return nil, caos_errs.ThrowInvalidArgument(nil, "USER-1Xyud", "Errors.Assets.EmptyKey")
}
existingUser, err := c.userWriteModelByID(ctx, userID, orgID)
if err != nil {
return nil, err
@ -23,8 +20,12 @@ func (c *Commands) AddHumanAvatar(ctx context.Context, orgID, userID, storageKey
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "USER-vJ3fS", "Errors.Users.NotFound")
}
asset, err := c.uploadAsset(ctx, upload)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "USER-1Xyud", "Errors.Assets.Object.PutFailed")
}
userAgg := UserAggregateFromWriteModel(&existingUser.WriteModel)
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanAvatarAddedEvent(ctx, userAgg, storageKey))
pushedEvents, err := c.eventstore.Push(ctx, user.NewHumanAvatarAddedEvent(ctx, userAgg, asset.Name))
if err != nil {
return nil, err
}
@ -46,7 +47,7 @@ func (c *Commands) RemoveHumanAvatar(ctx context.Context, orgID, userID string)
if existingUser.UserState == domain.UserStateUnspecified || existingUser.UserState == domain.UserStateDeleted {
return nil, caos_errs.ThrowNotFound(nil, "USER-35N8f", "Errors.Users.NotFound")
}
err = c.RemoveAsset(ctx, orgID, existingUser.Avatar)
err = c.removeAsset(ctx, orgID, existingUser.Avatar)
if err != nil {
return nil, err
}

View File

@ -1,6 +1,7 @@
package command
import (
"bytes"
"context"
"testing"
@ -20,12 +21,13 @@ import (
func TestCommandSide_AddHumanAvatar(t *testing.T) {
type fields struct {
eventstore *eventstore.Eventstore
storage static.Storage
}
type args struct {
ctx context.Context
orgID string
userID string
storageKey string
ctx context.Context
orgID string
userID string
upload *AssetUpload
}
type res struct {
want *domain.ObjectDetails
@ -44,25 +46,18 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
t,
),
},
args: args{
ctx: context.Background(),
storageKey: "key",
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
},
},
{
name: "storage key empty, invalid argument error",
fields: fields{
eventstore: eventstoreExpect(
t,
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
orgID: "",
userID: "",
upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
},
res: res{
err: caos_errs.IsErrorInvalidArgument,
@ -77,17 +72,65 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
storageKey: "key",
ctx: context.Background(),
orgID: "org1",
userID: "user1",
upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
},
res: res{
err: caos_errs.IsNotFound,
},
},
{
name: "logo added, ok",
name: "upload failed, error",
fields: fields{
eventstore: eventstoreExpect(
t,
expectFilter(
eventFromEventPusher(
user.NewHumanAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"username",
"firstname",
"lastname",
"nickname",
"displayname",
language.Und,
domain.GenderUnspecified,
"email@test.ch",
true,
),
),
),
),
storage: mock.NewStorage(t).ExpectPutObjectError(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
},
res: res{
err: caos_errs.IsInternal,
},
},
{
name: "avatar added, ok",
fields: fields{
eventstore: eventstoreExpect(
t,
@ -112,18 +155,26 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
eventFromEventPusher(
user.NewHumanAvatarAddedEvent(context.Background(),
&user.NewAggregate("user1", "org1").Aggregate,
"key",
"avatar",
),
),
},
),
),
storage: mock.NewStorage(t).ExpectPutObject(),
},
args: args{
ctx: context.Background(),
orgID: "org1",
userID: "user1",
storageKey: "key",
ctx: context.Background(),
orgID: "org1",
userID: "user1",
upload: &AssetUpload{
ResourceOwner: "org1",
ObjectName: "avatar",
ContentType: "image",
ObjectType: static.ObjectTypeUserAvatar,
File: bytes.NewReader([]byte("test")),
Size: 4,
},
},
res: res{
want: &domain.ObjectDetails{
@ -136,8 +187,9 @@ func TestCommandSide_AddHumanAvatar(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
r := &Commands{
eventstore: tt.fields.eventstore,
static: tt.fields.storage,
}
got, err := r.AddHumanAvatar(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.storageKey)
got, err := r.AddHumanAvatar(tt.args.ctx, tt.args.orgID, tt.args.userID, tt.args.upload)
if tt.res.err == nil {
assert.NoError(t, err)
}

View File

@ -27,8 +27,32 @@ const (
)
type InstanceSetup struct {
Org OrgSetup
Zitadel ZitadelConfig
Org OrgSetup
Zitadel ZitadelConfig
Features struct {
TierName string
TierDescription string
Retention time.Duration
State domain.FeaturesState
StateDescription string
LoginPolicyFactors bool
LoginPolicyIDP bool
LoginPolicyPasswordless bool
LoginPolicyRegistration bool
LoginPolicyUsernameLogin bool
LoginPolicyPasswordReset bool
PasswordComplexityPolicy bool
LabelPolicyPrivateLabel bool
LabelPolicyWatermark bool
CustomDomain bool
PrivacyPolicy bool
MetadataUser bool
CustomTextMessage bool
CustomTextLogin bool
LockoutPolicy bool
ActionsAllowed domain.ActionsAllowed
MaxActions int
}
PasswordComplexityPolicy struct {
MinLength uint64
HasLowercase bool
@ -170,6 +194,31 @@ func (command *Command) SetUpInstance(ctx context.Context, setup *InstanceSetup)
projectAgg := project.NewAggregate(setup.Zitadel.projectID, orgID)
validations := []preparation.Validation{
SetDefaultFeatures(
instanceAgg,
setup.Features.TierName,
setup.Features.TierDescription,
setup.Features.State,
setup.Features.StateDescription,
setup.Features.Retention,
setup.Features.LoginPolicyFactors,
setup.Features.LoginPolicyIDP,
setup.Features.LoginPolicyPasswordless,
setup.Features.LoginPolicyRegistration,
setup.Features.LoginPolicyUsernameLogin,
setup.Features.LoginPolicyPasswordReset,
setup.Features.PasswordComplexityPolicy,
setup.Features.LabelPolicyPrivateLabel,
setup.Features.LabelPolicyWatermark,
setup.Features.CustomDomain,
setup.Features.PrivacyPolicy,
setup.Features.MetadataUser,
setup.Features.CustomTextMessage,
setup.Features.CustomTextLogin,
setup.Features.LockoutPolicy,
setup.Features.ActionsAllowed,
setup.Features.MaxActions,
),
AddPasswordComplexityPolicy(
instanceAgg,
setup.PasswordComplexityPolicy.MinLength,

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(FeatureIsDefaultCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(FeatureTierNameCol, crdb.ColumnTypeText),
crdb.NewColumn(FeatureTierDescriptionCol, crdb.ColumnTypeText),
crdb.NewColumn(FeatureTierDescriptionCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(FeatureStateCol, crdb.ColumnTypeEnum, crdb.Default(0)),
crdb.NewColumn(FeatureStateDescriptionCol, crdb.ColumnTypeText),
crdb.NewColumn(FeatureStateDescriptionCol, crdb.ColumnTypeText, crdb.Nullable()),
crdb.NewColumn(FeatureAuditLogRetentionCol, crdb.ColumnTypeInt64, crdb.Default(0)),
crdb.NewColumn(FeatureLoginPolicyFactorsCol, crdb.ColumnTypeBool, crdb.Default(false)),
crdb.NewColumn(FeatureLoginPolicyIDPCol, crdb.ColumnTypeBool, crdb.Default(false)),

View File

@ -1,114 +1,30 @@
package config
import (
"context"
"encoding/json"
"io"
"net/url"
"time"
"database/sql"
"github.com/caos/zitadel/internal/domain"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
"github.com/caos/zitadel/internal/static/database"
"github.com/caos/zitadel/internal/static/s3"
)
type AssetStorageConfig struct {
Type string
Config static.Config
Config map[string]interface{} `mapstructure:",remain"`
}
var storage = map[string]func() static.Config{
"s3": func() static.Config { return &s3.Config{} },
"none": func() static.Config { return &NoStorage{} },
"": func() static.Config { return &NoStorage{} },
}
func (c *AssetStorageConfig) UnmarshalJSON(data []byte) error {
var rc struct {
Type string
Config json.RawMessage
}
if err := json.Unmarshal(data, &rc); err != nil {
return errors.ThrowInternal(err, "STATIC-Bfn5r", "error parsing config")
}
c.Type = rc.Type
var err error
c.Config, err = newStorageConfig(c.Type, rc.Config)
if err != nil {
return err
}
return nil
}
func newStorageConfig(storageType string, configData []byte) (static.Config, error) {
t, ok := storage[storageType]
func (a *AssetStorageConfig) NewStorage(client *sql.DB) (static.Storage, error) {
t, ok := storage[a.Type]
if !ok {
return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", storageType)
return nil, errors.ThrowInternalf(nil, "STATIC-dsbjh", "config type %s not supported", a.Type)
}
staticConfig := t()
if len(configData) == 0 {
return staticConfig, nil
}
if err := json.Unmarshal(configData, staticConfig); err != nil {
return nil, errors.ThrowInternal(err, "STATIC-GB4nw", "Could not read config: %v")
}
return staticConfig, nil
return t(client, a.Config)
}
var (
errNoStorage = errors.ThrowInternal(nil, "STATIC-ashg4", "Errors.Assets.Store.NotConfigured")
)
type NoStorage struct{}
func (_ *NoStorage) NewStorage() (static.Storage, error) {
return &NoStorage{}, nil
}
func (_ *NoStorage) CreateBucket(ctx context.Context, name, location string) error {
return errNoStorage
}
func (_ *NoStorage) RemoveBucket(ctx context.Context, name string) error {
return errNoStorage
}
func (_ *NoStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
return nil, errNoStorage
}
func (_ *NoStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
return nil, errNoStorage
}
func (_ *NoStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
return nil, errNoStorage
}
func (_ *NoStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
return nil, nil, errNoStorage
}
func (_ *NoStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
return nil, errNoStorage
}
func (_ *NoStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
return nil, errNoStorage
}
func (_ *NoStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error {
return errNoStorage
}
func (_ *NoStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
return errNoStorage
var storage = map[string]static.CreateStorage{
"db": database.NewStorage,
"": database.NewStorage,
"s3": s3.NewStorage,
}

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:
PutFailed: Objekt konnte nicht erstellt werden
GetFailed: Objekt konnte nicht gelesen werden
NotFound: Objekt konnte nicht gefunden werden
PresignedTokenFailed: Signiertes Token konnte nicht erstellt werden
ListFailed: Objektliste konnte nicht gelesen werden
RemoveFailed: Objekt konnte nicht gelöscht werden

View File

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

View File

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

View File

@ -7,11 +7,8 @@ package mock
import (
context "context"
io "io"
url "net/url"
reflect "reflect"
time "time"
domain "github.com/caos/zitadel/internal/domain"
static "github.com/caos/zitadel/internal/static"
gomock "github.com/golang/mock/gomock"
)
@ -39,187 +36,76 @@ func (m *MockStorage) EXPECT() *MockStorageMockRecorder {
return m.recorder
}
// CreateBucket mocks base method.
func (m *MockStorage) CreateBucket(ctx context.Context, name, location string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "CreateBucket", ctx, name, location)
ret0, _ := ret[0].(error)
return ret0
}
// CreateBucket indicates an expected call of CreateBucket.
func (mr *MockStorageMockRecorder) CreateBucket(ctx, name, location interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockStorage)(nil).CreateBucket), ctx, name, location)
}
// GetObject mocks base method.
func (m *MockStorage) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
func (m *MockStorage) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetObject", ctx, bucketName, objectName)
ret0, _ := ret[0].(io.Reader)
ret1, _ := ret[1].(func() (*domain.AssetInfo, error))
ret := m.ctrl.Call(m, "GetObject", ctx, instanceID, resourceOwner, name)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(func() (*static.Asset, error))
ret2, _ := ret[2].(error)
return ret0, ret1, ret2
}
// GetObject indicates an expected call of GetObject.
func (mr *MockStorageMockRecorder) GetObject(ctx, bucketName, objectName interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) GetObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, bucketName, objectName)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockStorage)(nil).GetObject), ctx, instanceID, resourceOwner, name)
}
// GetObjectInfo mocks base method.
func (m *MockStorage) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
func (m *MockStorage) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetObjectInfo", ctx, bucketName, objectName)
ret0, _ := ret[0].(*domain.AssetInfo)
ret := m.ctrl.Call(m, "GetObjectInfo", ctx, instanceID, resourceOwner, name)
ret0, _ := ret[0].(*static.Asset)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetObjectInfo indicates an expected call of GetObjectInfo.
func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, bucketName, objectName interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) GetObjectInfo(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, bucketName, objectName)
}
// GetObjectPresignedURL mocks base method.
func (m *MockStorage) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetObjectPresignedURL", ctx, bucketName, objectName, expiration)
ret0, _ := ret[0].(*url.URL)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetObjectPresignedURL indicates an expected call of GetObjectPresignedURL.
func (mr *MockStorageMockRecorder) GetObjectPresignedURL(ctx, bucketName, objectName, expiration interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectPresignedURL", reflect.TypeOf((*MockStorage)(nil).GetObjectPresignedURL), ctx, bucketName, objectName, expiration)
}
// ListBuckets mocks base method.
func (m *MockStorage) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListBuckets", ctx)
ret0, _ := ret[0].([]*domain.BucketInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListBuckets indicates an expected call of ListBuckets.
func (mr *MockStorageMockRecorder) ListBuckets(ctx interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockStorage)(nil).ListBuckets), ctx)
}
// ListObjectInfos mocks base method.
func (m *MockStorage) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ListObjectInfos", ctx, bucketName, prefix, recursive)
ret0, _ := ret[0].([]*domain.AssetInfo)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ListObjectInfos indicates an expected call of ListObjectInfos.
func (mr *MockStorageMockRecorder) ListObjectInfos(ctx, bucketName, prefix, recursive interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectInfos", reflect.TypeOf((*MockStorage)(nil).ListObjectInfos), ctx, bucketName, prefix, recursive)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectInfo", reflect.TypeOf((*MockStorage)(nil).GetObjectInfo), ctx, instanceID, resourceOwner, name)
}
// PutObject mocks base method.
func (m *MockStorage) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
func (m *MockStorage) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "PutObject", ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting)
ret0, _ := ret[0].(*domain.AssetInfo)
ret := m.ctrl.Call(m, "PutObject", ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
ret0, _ := ret[0].(*static.Asset)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// PutObject indicates an expected call of PutObject.
func (mr *MockStorageMockRecorder) PutObject(ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) PutObject(ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, bucketName, objectName, contentType, object, objectSize, createBucketIfNotExisting)
}
// RemoveBucket mocks base method.
func (m *MockStorage) RemoveBucket(ctx context.Context, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveBucket", ctx, name)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveBucket indicates an expected call of RemoveBucket.
func (mr *MockStorageMockRecorder) RemoveBucket(ctx, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveBucket", reflect.TypeOf((*MockStorage)(nil).RemoveBucket), ctx, name)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockStorage)(nil).PutObject), ctx, instanceID, location, resourceOwner, name, contentType, objectType, object, objectSize)
}
// RemoveObject mocks base method.
func (m *MockStorage) RemoveObject(ctx context.Context, bucketName, objectName string) error {
func (m *MockStorage) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveObject", ctx, bucketName, objectName)
ret := m.ctrl.Call(m, "RemoveObject", ctx, instanceID, resourceOwner, name)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveObject indicates an expected call of RemoveObject.
func (mr *MockStorageMockRecorder) RemoveObject(ctx, bucketName, objectName interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) RemoveObject(ctx, instanceID, resourceOwner, name interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, bucketName, objectName)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObject", reflect.TypeOf((*MockStorage)(nil).RemoveObject), ctx, instanceID, resourceOwner, name)
}
// RemoveObjects mocks base method.
func (m *MockStorage) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
func (m *MockStorage) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "RemoveObjects", ctx, bucketName, path, recursive)
ret := m.ctrl.Call(m, "RemoveObjects", ctx, instanceID, resourceOwner, objectType)
ret0, _ := ret[0].(error)
return ret0
}
// RemoveObjects indicates an expected call of RemoveObjects.
func (mr *MockStorageMockRecorder) RemoveObjects(ctx, bucketName, path, recursive interface{}) *gomock.Call {
func (mr *MockStorageMockRecorder) RemoveObjects(ctx, instanceID, resourceOwner, objectType interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, bucketName, path, recursive)
}
// MockConfig is a mock of Config interface.
type MockConfig struct {
ctrl *gomock.Controller
recorder *MockConfigMockRecorder
}
// MockConfigMockRecorder is the mock recorder for MockConfig.
type MockConfigMockRecorder struct {
mock *MockConfig
}
// NewMockConfig creates a new mock instance.
func NewMockConfig(ctrl *gomock.Controller) *MockConfig {
mock := &MockConfig{ctrl: ctrl}
mock.recorder = &MockConfigMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockConfig) EXPECT() *MockConfigMockRecorder {
return m.recorder
}
// NewStorage mocks base method.
func (m *MockConfig) NewStorage() (static.Storage, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewStorage")
ret0, _ := ret[0].(static.Storage)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// NewStorage indicates an expected call of NewStorage.
func (mr *MockConfigMockRecorder) NewStorage() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewStorage", reflect.TypeOf((*MockConfig)(nil).NewStorage))
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveObjects", reflect.TypeOf((*MockStorage)(nil).RemoveObjects), ctx, instanceID, resourceOwner, objectType)
}

View File

@ -1,34 +1,49 @@
package mock
import (
"context"
"io"
"testing"
"time"
"github.com/golang/mock/gomock"
caos_errors "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
)
func NewStorage(t *testing.T) *MockStorage {
return NewMockStorage(gomock.NewController(t))
}
func (m *MockStorage) ExpectAddObjectNoError() *MockStorage {
func (m *MockStorage) ExpectPutObject() *MockStorage {
m.EXPECT().
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, nil)
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
hash, _ := io.ReadAll(object)
return &static.Asset{
InstanceID: instanceID,
Name: name,
Hash: string(hash),
Size: objectSize,
LastModified: time.Now(),
Location: location,
ContentType: contentType,
}, nil
})
return m
}
func (m *MockStorage) ExpectAddObjectError() *MockStorage {
func (m *MockStorage) ExpectPutObjectError() *MockStorage {
m.EXPECT().
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
PutObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, caos_errors.ThrowInternal(nil, "", ""))
return m
}
func (m *MockStorage) ExpectRemoveObjectNoError() *MockStorage {
m.EXPECT().
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()).
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil)
return m
}
@ -42,7 +57,7 @@ func (m *MockStorage) ExpectRemoveObjectsNoError() *MockStorage {
func (m *MockStorage) ExpectRemoveObjectError() *MockStorage {
m.EXPECT().
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any()).
RemoveObject(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(caos_errors.ThrowInternal(nil, "", ""))
return m
}

View File

@ -1,9 +1,13 @@
package s3
import (
"database/sql"
"encoding/json"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
)
@ -34,3 +38,15 @@ func (c *Config) NewStorage() (static.Storage, error) {
MultiDelete: c.MultiDelete,
}, nil
}
func NewStorage(_ *sql.DB, rawConfig map[string]interface{}) (static.Storage, error) {
configData, err := json.Marshal(rawConfig)
if err != nil {
return nil, errors.ThrowInternal(err, "MINIO-Ef2f2", "could not map config")
}
c := new(Config)
if err := json.Unmarshal(configData, c); err != nil {
return nil, errors.ThrowInternal(err, "MINIO-GB4nw", "could not map config")
}
return c.NewStorage()
}

View File

@ -2,11 +2,10 @@ package s3
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/caos/logging"
"github.com/minio/minio-go/v7"
@ -14,8 +13,11 @@ import (
"github.com/caos/zitadel/internal/domain"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/static"
)
var _ static.Storage = (*Minio)(nil)
type Minio struct {
Client *minio.Client
Location string
@ -23,129 +25,66 @@ type Minio struct {
MultiDelete bool
}
func (m *Minio) CreateBucket(ctx context.Context, name, location string) error {
if location == "" {
location = m.Location
func (m *Minio) PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType static.ObjectType, object io.Reader, objectSize int64) (*static.Asset, error) {
err := m.createBucket(ctx, instanceID, location)
if err != nil && !caos_errs.IsErrorAlreadyExists(err) {
return nil, err
}
name = m.prefixBucketName(name)
exists, err := m.Client.BucketExists(ctx, name)
if err != nil {
logging.LogWithFields("MINIO-ADvf3", "bucketname", name).WithError(err).Error("cannot check if bucket exists")
return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal")
}
if exists {
return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists")
}
err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location})
if err != nil {
return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed")
}
return nil
}
func (m *Minio) ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error) {
infos, err := m.Client.ListBuckets(ctx)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-390OP", "Errors.Assets.Bucket.ListFailed")
}
buckets := make([]*domain.BucketInfo, len(infos))
for i, info := range infos {
buckets[i] = &domain.BucketInfo{
Name: info.Name,
CreationDate: info.CreationDate,
}
}
return buckets, nil
}
func (m *Minio) RemoveBucket(ctx context.Context, name string) error {
name = m.prefixBucketName(name)
err := m.Client.RemoveBucket(ctx, name)
if err != nil {
return caos_errs.ThrowInternal(err, "MINIO-338Hs", "Errors.Assets.Bucket.RemoveFailed")
}
return nil
}
func (m *Minio) PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error) {
if createBucketIfNotExisting {
err := m.CreateBucket(ctx, bucketName, "")
if err != nil && !caos_errs.IsErrorAlreadyExists(err) {
return nil, err
}
}
bucketName = m.prefixBucketName(bucketName)
bucketName := m.prefixBucketName(instanceID)
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
info, err := m.Client.PutObject(ctx, bucketName, objectName, object, objectSize, minio.PutObjectOptions{ContentType: contentType})
if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-590sw", "Errors.Assets.Object.PutFailed")
}
return &domain.AssetInfo{
Bucket: info.Bucket,
Key: info.Key,
ETag: info.ETag,
Size: info.Size,
LastModified: info.LastModified,
Location: info.Location,
VersionID: info.VersionID,
return &static.Asset{
InstanceID: info.Bucket,
ResourceOwner: resourceOwner,
Name: info.Key,
Hash: info.ETag,
Size: info.Size,
LastModified: info.LastModified,
Location: info.Location,
ContentType: contentType,
}, nil
}
func (m *Minio) GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error) {
bucketName = m.prefixBucketName(bucketName)
objectinfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
func (m *Minio) GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*static.Asset, error), error) {
bucketName := m.prefixBucketName(instanceID)
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed")
}
info := func() (*static.Asset, error) {
info, err := object.Stat()
if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed")
}
return m.objectToAssetInfo(instanceID, resourceOwner, info), nil
}
asset, err := io.ReadAll(object)
if err != nil {
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-SFef1", "Errors.Assets.Object.GetFailed")
}
return asset, info, nil
}
func (m *Minio) GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*static.Asset, error) {
bucketName := m.prefixBucketName(instanceID)
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
objectInfo, err := m.Client.StatObject(ctx, bucketName, objectName, minio.StatObjectOptions{})
if err != nil {
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound {
return nil, caos_errs.ThrowNotFound(err, "MINIO-Gdfh4", "Errors.Assets.Object.GetFailed")
}
return nil, caos_errs.ThrowInternal(err, "MINIO-1vySX", "Errors.Assets.Object.GetFailed")
}
return m.objectToAssetInfo(bucketName, objectinfo), nil
return m.objectToAssetInfo(instanceID, resourceOwner, objectInfo), nil
}
func (m *Minio) GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error) {
bucketName = m.prefixBucketName(bucketName)
object, err := m.Client.GetObject(ctx, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
return nil, nil, caos_errs.ThrowInternal(err, "MINIO-VGDgv", "Errors.Assets.Object.GetFailed")
}
info := func() (*domain.AssetInfo, error) {
info, err := object.Stat()
if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-F96xF", "Errors.Assets.Object.GetFailed")
}
return m.objectToAssetInfo(bucketName, info), nil
}
return object, info, nil
}
func (m *Minio) GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error) {
bucketName = m.prefixBucketName(bucketName)
reqParams := make(url.Values)
presignedURL, err := m.Client.PresignedGetObject(ctx, bucketName, objectName, expiration, reqParams)
if err != nil {
return nil, caos_errs.ThrowInternal(err, "MINIO-19Mp0", "Errors.Assets.Object.PresignedTokenFailed")
}
return presignedURL, nil
}
func (m *Minio) ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error) {
bucketName = m.prefixBucketName(bucketName)
assetInfos := make([]*domain.AssetInfo, 0)
objects, cancel := m.listObjects(ctx, bucketName, prefix, recursive)
defer cancel()
for object := range objects {
if object.Err != nil {
logging.LogWithFields("MINIO-wC8sd", "bucket-name", bucketName, "prefix", prefix).WithError(object.Err).Debug("unable to get object")
return nil, caos_errs.ThrowInternal(object.Err, "MINIO-1m09S", "Errors.Assets.Object.ListFailed")
}
assetInfos = append(assetInfos, m.objectToAssetInfo(bucketName, object))
}
return assetInfos, nil
}
func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string) error {
bucketName = m.prefixBucketName(bucketName)
func (m *Minio) RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error {
bucketName := m.prefixBucketName(instanceID)
objectName := fmt.Sprintf("%s/%s", resourceOwner, name)
err := m.Client.RemoveObject(ctx, bucketName, objectName, minio.RemoveObjectOptions{})
if err != nil {
return caos_errs.ThrowInternal(err, "MINIO-x85RT", "Errors.Assets.Object.RemoveFailed")
@ -153,19 +92,27 @@ func (m *Minio) RemoveObject(ctx context.Context, bucketName, objectName string)
return nil
}
func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error {
bucketName = m.prefixBucketName(bucketName)
func (m *Minio) RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType static.ObjectType) error {
bucketName := m.prefixBucketName(instanceID)
objectsCh := make(chan minio.ObjectInfo)
g := new(errgroup.Group)
var path string
switch objectType {
case static.ObjectTypeStyling:
path = domain.LabelPolicyPrefix + "/"
default:
return nil
}
g.Go(func() error {
defer close(objectsCh)
objects, cancel := m.listObjects(ctx, bucketName, path, recursive)
objects, cancel := m.listObjects(ctx, bucketName, resourceOwner, true)
for object := range objects {
if err := object.Err; err != nil {
cancel()
if errResp := minio.ToErrorResponse(err); errResp.StatusCode == http.StatusNotFound {
logging.LogWithFields("MINIO-ss8va", "bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found")
logging.WithFields("bucketName", bucketName, "path", path).Warn("list objects for remove failed with not found")
continue
}
return caos_errs.ThrowInternal(object.Err, "MINIO-WQF32", "Errors.Assets.Object.ListFailed")
@ -189,6 +136,26 @@ func (m *Minio) RemoveObjects(ctx context.Context, bucketName, path string, recu
return g.Wait()
}
func (m *Minio) createBucket(ctx context.Context, name, location string) error {
if location == "" {
location = m.Location
}
name = m.prefixBucketName(name)
exists, err := m.Client.BucketExists(ctx, name)
if err != nil {
logging.WithFields("bucketname", name).WithError(err).Error("cannot check if bucket exists")
return caos_errs.ThrowInternal(err, "MINIO-1b8fs", "Errors.Assets.Bucket.Internal")
}
if exists {
return caos_errs.ThrowAlreadyExists(nil, "MINIO-9n3MK", "Errors.Assets.Bucket.AlreadyExists")
}
err = m.Client.MakeBucket(ctx, name, minio.MakeBucketOptions{Region: location})
if err != nil {
return caos_errs.ThrowInternal(err, "MINIO-4m90d", "Errors.Assets.Bucket.CreateFailed")
}
return nil
}
func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recursive bool) (<-chan minio.ObjectInfo, context.CancelFunc) {
ctxCancel, cancel := context.WithCancel(ctx)
@ -198,17 +165,15 @@ func (m *Minio) listObjects(ctx context.Context, bucketName, prefix string, recu
}), cancel
}
func (m *Minio) objectToAssetInfo(bucketName string, object minio.ObjectInfo) *domain.AssetInfo {
return &domain.AssetInfo{
Bucket: bucketName,
Key: object.Key,
ETag: object.ETag,
Size: object.Size,
LastModified: object.LastModified,
VersionID: object.VersionID,
Expiration: object.Expires,
ContentType: object.ContentType,
AutheticatedURL: m.Client.EndpointURL().String() + "/" + bucketName + "/" + object.Key,
func (m *Minio) objectToAssetInfo(bucketName string, resourceOwner string, object minio.ObjectInfo) *static.Asset {
return &static.Asset{
InstanceID: bucketName,
ResourceOwner: resourceOwner,
Name: object.Key,
Hash: object.ETag,
Size: object.Size,
LastModified: object.LastModified,
ContentType: object.ContentType,
}
}

View File

@ -2,25 +2,36 @@ package static
import (
"context"
"database/sql"
"io"
"net/url"
"time"
"github.com/caos/zitadel/internal/domain"
)
type CreateStorage func(client *sql.DB, rawConfig map[string]interface{}) (Storage, error)
type Storage interface {
CreateBucket(ctx context.Context, name, location string) error
RemoveBucket(ctx context.Context, name string) error
ListBuckets(ctx context.Context) ([]*domain.BucketInfo, error)
PutObject(ctx context.Context, bucketName, objectName, contentType string, object io.Reader, objectSize int64, createBucketIfNotExisting bool) (*domain.AssetInfo, error)
GetObjectInfo(ctx context.Context, bucketName, objectName string) (*domain.AssetInfo, error)
GetObject(ctx context.Context, bucketName, objectName string) (io.Reader, func() (*domain.AssetInfo, error), error)
ListObjectInfos(ctx context.Context, bucketName, prefix string, recursive bool) ([]*domain.AssetInfo, error)
GetObjectPresignedURL(ctx context.Context, bucketName, objectName string, expiration time.Duration) (*url.URL, error)
RemoveObject(ctx context.Context, bucketName, objectName string) error
RemoveObjects(ctx context.Context, bucketName, path string, recursive bool) error
PutObject(ctx context.Context, instanceID, location, resourceOwner, name, contentType string, objectType ObjectType, object io.Reader, objectSize int64) (*Asset, error)
GetObject(ctx context.Context, instanceID, resourceOwner, name string) ([]byte, func() (*Asset, error), error)
GetObjectInfo(ctx context.Context, instanceID, resourceOwner, name string) (*Asset, error)
RemoveObject(ctx context.Context, instanceID, resourceOwner, name string) error
RemoveObjects(ctx context.Context, instanceID, resourceOwner string, objectType ObjectType) error
//TODO: add functionality to move asset location
}
type Config interface {
NewStorage() (Storage, error)
type ObjectType int32
const (
ObjectTypeUserAvatar = iota
ObjectTypeStyling
)
type Asset struct {
InstanceID string
ResourceOwner string
Name string
Hash string
Size int64
LastModified time.Time
Location string
ContentType string
}

View File

@ -1,8 +1,6 @@
package model
import (
"context"
"net/url"
"time"
"golang.org/x/text/language"
@ -11,7 +9,6 @@ import (
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore/v1/models"
iam_model "github.com/caos/zitadel/internal/iam/model"
"github.com/caos/zitadel/internal/static"
)
type UserView struct {
@ -41,7 +38,6 @@ type HumanView struct {
DisplayName string
AvatarKey string
AvatarURL string
PreSignedAvatar *url.URL
PreferredLanguage string
Gender Gender
Email string
@ -257,23 +253,6 @@ func (u *UserView) GetProfile() (*Profile, error) {
}, nil
}
func (u *UserView) FillUserAvatar(ctx context.Context, static static.Storage, expiration time.Duration) error {
if u.HumanView == nil {
return errors.ThrowPreconditionFailed(nil, "MODEL-2k8da", "Errors.User.NotHuman")
}
if static != nil {
if ctx == nil {
ctx = context.Background()
}
presignesAvatarURL, err := static.GetObjectPresignedURL(ctx, u.ResourceOwner, u.AvatarKey, expiration)
if err != nil {
return err
}
u.PreSignedAvatar = presignesAvatarURL
}
return nil
}
func (u *UserView) GetPhone() (*Phone, error) {
if u.HumanView == nil {
return nil, errors.ThrowPreconditionFailed(nil, "MODEL-him4a", "Errors.User.NotHuman")