zitadel/internal/query/message_text.go

352 lines
10 KiB
Go
Raw Normal View History

package query
import (
"context"
"database/sql"
"encoding/json"
errs "errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"time"
sq "github.com/Masterminds/squirrel"
"golang.org/x/text/language"
"sigs.k8s.io/yaml"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/call"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/query/projection"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
type MessageTexts struct {
InitCode MessageText
PasswordReset MessageText
VerifyEmail MessageText
VerifyPhone MessageText
VerifySMSOTP MessageText
VerifyEmailOTP MessageText
DomainClaimed MessageText
PasswordlessRegistration MessageText
PasswordChange MessageText
}
type MessageText struct {
AggregateID string
Sequence uint64
CreationDate time.Time
ChangeDate time.Time
State domain.PolicyState
IsDefault bool
Type string
Language language.Tag
Title string
PreHeader string
Subject string
Greeting string
Text string
ButtonText string
Footer string
}
var (
messageTextTable = table{
name: projection.MessageTextTable,
instanceIDCol: projection.MessageTextInstanceIDCol,
}
MessageTextColAggregateID = Column{
name: projection.MessageTextAggregateIDCol,
table: messageTextTable,
}
MessageTextColInstanceID = Column{
name: projection.MessageTextInstanceIDCol,
table: messageTextTable,
}
MessageTextColSequence = Column{
name: projection.MessageTextSequenceCol,
table: messageTextTable,
}
MessageTextColCreationDate = Column{
name: projection.MessageTextCreationDateCol,
table: messageTextTable,
}
MessageTextColChangeDate = Column{
name: projection.MessageTextChangeDateCol,
table: messageTextTable,
}
MessageTextColState = Column{
name: projection.MessageTextStateCol,
table: messageTextTable,
}
MessageTextColType = Column{
name: projection.MessageTextTypeCol,
table: messageTextTable,
}
MessageTextColLanguage = Column{
name: projection.MessageTextLanguageCol,
table: messageTextTable,
}
MessageTextColTitle = Column{
name: projection.MessageTextTitleCol,
table: messageTextTable,
}
MessageTextColPreHeader = Column{
name: projection.MessageTextPreHeaderCol,
table: messageTextTable,
}
MessageTextColSubject = Column{
name: projection.MessageTextSubjectCol,
table: messageTextTable,
}
MessageTextColGreeting = Column{
name: projection.MessageTextGreetingCol,
table: messageTextTable,
}
MessageTextColText = Column{
name: projection.MessageTextTextCol,
table: messageTextTable,
}
MessageTextColButtonText = Column{
name: projection.MessageTextButtonTextCol,
table: messageTextTable,
}
MessageTextColFooter = Column{
name: projection.MessageTextFooterCol,
table: messageTextTable,
}
feat: remove org (#4148) * feat(command): remove org * refactor: imports, unused code, error handling * reduce org removed in action * add org deletion to projections * add org removal to projections * add org removal to projections * org removed projection * lint import * projections * fix: table names in tests * fix: table names in tests * logging * add org state * fix(domain): add Owner removed to object details * feat(ListQuery): add with owner removed * fix(org-delete): add bool to functions to select with owner removed * fix(org-delete): add bools to user grants with events to determine if dependencies lost owner * fix(org-delete): add unit tests for owner removed and org removed events * fix(org-delete): add handling of org remove for grants and members * fix(org-delete): correction of unit tests for owner removed * fix(org-delete): update projections, unit tests and get functions * fix(org-delete): add change date to authnkeys and owner removed to org metadata * fix(org-delete): include owner removed for login names * fix(org-delete): some column fixes in projections and build for queries with owner removed * indexes * fix(org-delete): include review changes * fix(org-delete): change user projection name after merge * fix(org-delete): include review changes for project grant where no project owner is necessary * fix(org-delete): include auth and adminapi tables with owner removed information * fix(org-delete): cleanup username and orgdomain uniqueconstraints when org is removed * fix(org-delete): add permissions for org.remove * remove unnecessary unique constraints * fix column order in primary keys * fix(org-delete): include review changes * fix(org-delete): add owner removed indexes and chang setup step to create tables * fix(org-delete): move PK order of instance_id and change added user_grant from review * fix(org-delete): no params for prepareUserQuery * change to step 6 * merge main * fix(org-delete): OldUserName rename to private * fix linting * cleanup * fix: remove org test * create prerelease * chore: delete org-delete as prerelease Co-authored-by: Stefan Benz <stefan@caos.ch> Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2022-11-30 17:01:17 +01:00
MessageTextColOwnerRemoved = Column{
name: projection.MessageTextOwnerRemovedCol,
table: messageTextTable,
}
)
func (q *Queries) DefaultMessageText(ctx context.Context) (text *MessageText, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := prepareMessageTextQuery(ctx, q.client)
query, args, err := stmt.Where(sq.Eq{
MessageTextColAggregateID.identifier(): authz.GetInstance(ctx).InstanceID(),
feat: handle instance from context (#3382) * commander * commander * selber! * move to packages * fix(errors): implement Is interface * test: command * test: commands * add init steps * setup tenant * add default step yaml * possibility to set password * merge v2 into v2-commander * fix: rename iam command side to instance * fix: rename iam command side to instance * fix: rename iam command side to instance * fix: rename iam command side to instance * fix: search query builder can filter events in memory * fix: filters for add member * fix(setup): add `ExternalSecure` to config * chore: name iam to instance * fix: matching * remove unsued func * base url * base url * test(command): filter funcs * test: commands * fix: rename orgiampolicy to domain policy * start from init * commands * config * fix indexes and add constraints * fixes * fix: merge conflicts * fix: protos * fix: md files * setup * add deprecated org iam policy again * typo * fix search query * fix filter * Apply suggestions from code review * remove custom org from org setup * add todos for verification * change apps creation * simplify package structure * fix error * move preparation helper for tests * fix unique constraints * fix config mapping in setup * fix error handling in encryption_keys.go * fix projection config * fix query from old views to projection * fix setup of mgmt api * set iam project and fix instance projection * fix tokens view * fix steps.yaml and defaults.yaml * fix projections * change instance context to interface * instance interceptors and additional events in setup * cleanup * tests for interceptors * fix label policy * add todo * single api endpoint in environment.json Co-authored-by: adlerhurst <silvan.reusser@gmail.com> Co-authored-by: fabi <fabienne.gerschwiler@gmail.com>
2022-03-29 11:53:19 +02:00
MessageTextColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}).
Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-1b9mf", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
text, err = scan(row)
return err
}, query, args...)
return text, err
}
func (q *Queries) DefaultMessageTextByTypeAndLanguageFromFileSystem(ctx context.Context, messageType, language string) (_ *MessageText, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
contents, err := q.readNotificationTextMessages(ctx, language)
if err != nil {
return nil, err
}
messageTexts := new(MessageTexts)
if err := yaml.Unmarshal(contents, messageTexts); err != nil {
return nil, errors.ThrowInternal(err, "TEXT-3N9fs", "Errors.TranslationFile.ReadError")
}
return messageTexts.GetMessageTextByType(messageType), nil
}
func (q *Queries) CustomMessageTextByTypeAndLanguage(ctx context.Context, aggregateID, messageType, language string, withOwnerRemoved bool) (msg *MessageText, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
stmt, scan := prepareMessageTextQuery(ctx, q.client)
feat: remove org (#4148) * feat(command): remove org * refactor: imports, unused code, error handling * reduce org removed in action * add org deletion to projections * add org removal to projections * add org removal to projections * org removed projection * lint import * projections * fix: table names in tests * fix: table names in tests * logging * add org state * fix(domain): add Owner removed to object details * feat(ListQuery): add with owner removed * fix(org-delete): add bool to functions to select with owner removed * fix(org-delete): add bools to user grants with events to determine if dependencies lost owner * fix(org-delete): add unit tests for owner removed and org removed events * fix(org-delete): add handling of org remove for grants and members * fix(org-delete): correction of unit tests for owner removed * fix(org-delete): update projections, unit tests and get functions * fix(org-delete): add change date to authnkeys and owner removed to org metadata * fix(org-delete): include owner removed for login names * fix(org-delete): some column fixes in projections and build for queries with owner removed * indexes * fix(org-delete): include review changes * fix(org-delete): change user projection name after merge * fix(org-delete): include review changes for project grant where no project owner is necessary * fix(org-delete): include auth and adminapi tables with owner removed information * fix(org-delete): cleanup username and orgdomain uniqueconstraints when org is removed * fix(org-delete): add permissions for org.remove * remove unnecessary unique constraints * fix column order in primary keys * fix(org-delete): include review changes * fix(org-delete): add owner removed indexes and chang setup step to create tables * fix(org-delete): move PK order of instance_id and change added user_grant from review * fix(org-delete): no params for prepareUserQuery * change to step 6 * merge main * fix(org-delete): OldUserName rename to private * fix linting * cleanup * fix: remove org test * create prerelease * chore: delete org-delete as prerelease Co-authored-by: Stefan Benz <stefan@caos.ch> Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2022-11-30 17:01:17 +01:00
eq := sq.Eq{
MessageTextColLanguage.identifier(): language,
MessageTextColType.identifier(): messageType,
MessageTextColAggregateID.identifier(): aggregateID,
MessageTextColInstanceID.identifier(): authz.GetInstance(ctx).InstanceID(),
}
if !withOwnerRemoved {
eq[MessageTextColOwnerRemoved.identifier()] = false
}
query, args, err := stmt.Where(eq).OrderBy(MessageTextColAggregateID.identifier()).Limit(1).ToSql()
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-1b9mf", "Errors.Query.SQLStatement")
}
err = q.client.QueryRowContext(ctx, func(row *sql.Row) error {
msg, err = scan(row)
return err
}, query, args...)
if errors.IsNotFound(err) {
return q.IAMMessageTextByTypeAndLanguage(ctx, messageType, language)
}
return msg, err
}
func (q *Queries) IAMMessageTextByTypeAndLanguage(ctx context.Context, messageType, language string) (_ *MessageText, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
contents, err := q.readNotificationTextMessages(ctx, language)
if err != nil {
return nil, err
}
notificationTextMap := make(map[string]interface{})
if err := yaml.Unmarshal(contents, &notificationTextMap); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-ekjFF", "Errors.TranslationFile.ReadError")
}
feat: remove org (#4148) * feat(command): remove org * refactor: imports, unused code, error handling * reduce org removed in action * add org deletion to projections * add org removal to projections * add org removal to projections * org removed projection * lint import * projections * fix: table names in tests * fix: table names in tests * logging * add org state * fix(domain): add Owner removed to object details * feat(ListQuery): add with owner removed * fix(org-delete): add bool to functions to select with owner removed * fix(org-delete): add bools to user grants with events to determine if dependencies lost owner * fix(org-delete): add unit tests for owner removed and org removed events * fix(org-delete): add handling of org remove for grants and members * fix(org-delete): correction of unit tests for owner removed * fix(org-delete): update projections, unit tests and get functions * fix(org-delete): add change date to authnkeys and owner removed to org metadata * fix(org-delete): include owner removed for login names * fix(org-delete): some column fixes in projections and build for queries with owner removed * indexes * fix(org-delete): include review changes * fix(org-delete): change user projection name after merge * fix(org-delete): include review changes for project grant where no project owner is necessary * fix(org-delete): include auth and adminapi tables with owner removed information * fix(org-delete): cleanup username and orgdomain uniqueconstraints when org is removed * fix(org-delete): add permissions for org.remove * remove unnecessary unique constraints * fix column order in primary keys * fix(org-delete): include review changes * fix(org-delete): add owner removed indexes and chang setup step to create tables * fix(org-delete): move PK order of instance_id and change added user_grant from review * fix(org-delete): no params for prepareUserQuery * change to step 6 * merge main * fix(org-delete): OldUserName rename to private * fix linting * cleanup * fix: remove org test * create prerelease * chore: delete org-delete as prerelease Co-authored-by: Stefan Benz <stefan@caos.ch> Co-authored-by: Livio Spring <livio.a@gmail.com> Co-authored-by: Fabi <38692350+hifabienne@users.noreply.github.com> Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>
2022-11-30 17:01:17 +01:00
texts, err := q.CustomTextList(ctx, authz.GetInstance(ctx).InstanceID(), messageType, language, false)
if err != nil {
return nil, err
}
for _, text := range texts.CustomTexts {
messageTextMap, ok := notificationTextMap[messageType].(map[string]interface{})
if !ok {
continue
}
messageTextMap[text.Key] = text.Text
}
jsonbody, err := json.Marshal(notificationTextMap)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-3m8fJ", "Errors.TranslationFile.MergeError")
}
notificationText := new(MessageTexts)
if err := json.Unmarshal(jsonbody, &notificationText); err != nil {
return nil, errors.ThrowInternal(err, "QUERY-9MkfD", "Errors.TranslationFile.MergeError")
}
result := notificationText.GetMessageTextByType(messageType)
result.IsDefault = true
result.AggregateID = authz.GetInstance(ctx).InstanceID()
return result, nil
}
func (q *Queries) readNotificationTextMessages(ctx context.Context, language string) ([]byte, error) {
q.mutex.Lock()
defer q.mutex.Unlock()
var err error
contents, ok := q.NotificationTranslationFileContents[language]
if !ok {
contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", language))
if errors.IsNotFound(err) {
contents, err = q.readTranslationFile(q.NotificationDir, fmt.Sprintf("/i18n/%s.yaml", authz.GetInstance(ctx).DefaultLanguage().String()))
}
if err != nil {
return nil, err
}
q.NotificationTranslationFileContents[language] = contents
}
return contents, nil
}
func prepareMessageTextQuery(ctx context.Context, db prepareDatabase) (sq.SelectBuilder, func(*sql.Row) (*MessageText, error)) {
return sq.Select(
MessageTextColAggregateID.identifier(),
MessageTextColSequence.identifier(),
MessageTextColCreationDate.identifier(),
MessageTextColChangeDate.identifier(),
MessageTextColState.identifier(),
MessageTextColType.identifier(),
MessageTextColLanguage.identifier(),
MessageTextColTitle.identifier(),
MessageTextColPreHeader.identifier(),
MessageTextColSubject.identifier(),
MessageTextColGreeting.identifier(),
MessageTextColText.identifier(),
MessageTextColButtonText.identifier(),
MessageTextColFooter.identifier(),
).
From(messageTextTable.identifier() + db.Timetravel(call.Took(ctx))).
PlaceholderFormat(sq.Dollar),
func(row *sql.Row) (*MessageText, error) {
msg := new(MessageText)
lang := ""
title := sql.NullString{}
preHeader := sql.NullString{}
subject := sql.NullString{}
greeting := sql.NullString{}
text := sql.NullString{}
buttonText := sql.NullString{}
footer := sql.NullString{}
err := row.Scan(
&msg.AggregateID,
&msg.Sequence,
&msg.CreationDate,
&msg.ChangeDate,
&msg.State,
&msg.Type,
&lang,
&title,
&preHeader,
&subject,
&greeting,
&text,
&buttonText,
&footer,
)
if err != nil {
if errs.Is(err, sql.ErrNoRows) {
return nil, errors.ThrowNotFound(err, "QUERY-3nlrS", "Errors.MessageText.NotFound")
}
return nil, errors.ThrowInternal(err, "QUERY-499gJ", "Errors.Internal")
}
msg.Language = language.Make(lang)
msg.Title = title.String
msg.PreHeader = preHeader.String
msg.Subject = subject.String
msg.Greeting = greeting.String
msg.Text = text.String
msg.ButtonText = buttonText.String
msg.Footer = footer.String
return msg, nil
}
}
func (q *Queries) readTranslationFile(dir http.FileSystem, filename string) ([]byte, error) {
r, err := dir.Open(filename)
if os.IsNotExist(err) {
return nil, errors.ThrowNotFound(err, "QUERY-sN9wg", "Errors.TranslationFile.NotFound")
}
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-93njw", "Errors.TranslationFile.ReadError")
}
contents, err := ioutil.ReadAll(r)
if err != nil {
return nil, errors.ThrowInternal(err, "QUERY-l0fse", "Errors.TranslationFile.ReadError")
}
return contents, nil
}
func (m *MessageTexts) GetMessageTextByType(msgType string) *MessageText {
switch msgType {
case domain.InitCodeMessageType:
return &m.InitCode
case domain.PasswordResetMessageType:
return &m.PasswordReset
case domain.VerifyEmailMessageType:
return &m.VerifyEmail
case domain.VerifyPhoneMessageType:
return &m.VerifyPhone
case domain.VerifySMSOTPMessageType:
return &m.VerifySMSOTP
case domain.VerifyEmailOTPMessageType:
return &m.VerifyEmailOTP
case domain.DomainClaimedMessageType:
return &m.DomainClaimed
case domain.PasswordlessRegistrationMessageType:
return &m.PasswordlessRegistration
case domain.PasswordChangeMessageType:
return &m.PasswordChange
}
return nil
}