fix: translation (#647)

* fix: translation

* fix: translation

* fix: translation

* fix: remove unused code

* fix: log err
This commit is contained in:
Fabi 2020-08-28 09:44:43 +02:00 committed by GitHub
parent 34ec2508d3
commit 7295383621
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 184 additions and 104 deletions

View File

@ -43,7 +43,7 @@ func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t *Tok
}
}
userID, clientID, agentID, err := verifyAccessToken(ctx, token, t, method)
userID, clientID, agentID, prefLang, err := verifyAccessToken(ctx, token, t, method)
if err != nil {
return nil, err
}
@ -54,7 +54,7 @@ func VerifyTokenAndWriteCtxData(ctx context.Context, token, orgID string, t *Tok
if err := checkOrigin(ctx, origins); err != nil {
return nil, err
}
return context.WithValue(ctx, dataKey, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID}), nil
return context.WithValue(ctx, dataKey, CtxData{UserID: userID, OrgID: orgID, ProjectID: projectID, AgentID: agentID, PreferredLanguage: prefLang}), nil
}
func SetCtxData(ctx context.Context, ctxData CtxData) context.Context {

View File

@ -15,8 +15,8 @@ type testVerifier struct {
grant *Grant
}
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, error) {
return "userID", "agentID", nil
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, error) {
return "userID", "agentID", "de", nil
}
func (v *testVerifier) ResolveGrants(ctx context.Context) (*Grant, error) {

View File

@ -19,7 +19,7 @@ type TokenVerifier struct {
}
type authZRepo interface {
VerifyAccessToken(ctx context.Context, token, clientID string) (userID, agentID string, err error)
VerifyAccessToken(ctx context.Context, token, clientID string) (userID, agentID, prefLang string, err error)
VerifierClientID(ctx context.Context, name string) (clientID string, err error)
ResolveGrants(ctx context.Context) (grant *Grant, err error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
@ -30,13 +30,13 @@ func Start(authZRepo authZRepo) (v *TokenVerifier) {
return &TokenVerifier{authZRepo: authZRepo}
}
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID string, err error) {
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string, method string) (userID, clientID, agentID, prefLang string, err error) {
clientID, err = v.clientIDFromMethod(ctx, method)
if err != nil {
return "", "", "", err
return "", "", "", "", err
}
userID, agentID, err = v.authZRepo.VerifyAccessToken(ctx, token, clientID)
return userID, clientID, agentID, err
userID, agentID, prefLang, err = v.authZRepo.VerifyAccessToken(ctx, token, clientID)
return userID, clientID, agentID, prefLang, err
}
type client struct {
@ -101,10 +101,10 @@ func (v *TokenVerifier) CheckAuthMethod(method string) (Option, bool) {
return authOpt, ok
}
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID string, err error) {
func verifyAccessToken(ctx context.Context, token string, t *TokenVerifier, method string) (userID, clientID, agentID, prefLang string, err error) {
parts := strings.Split(token, BearerPrefix)
if len(parts) != 2 {
return "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
return "", "", "", "", caos_errs.ThrowUnauthenticated(nil, "AUTH-7fs1e", "invalid auth header")
}
return t.VerifyAccessToken(ctx, parts[1], method)
}

View File

@ -58,7 +58,7 @@ func Test_VerifyAccessToken(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier, tt.args.method)
_, _, _, _, err := verifyAccessToken(tt.args.ctx, tt.args.token, tt.args.verifier, tt.args.method)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}

View File

@ -1,16 +1,15 @@
package grpc
package errors
import (
"context"
"github.com/caos/logging"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/i18n"
"github.com/caos/zitadel/pkg/grpc/message"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
func CaosToGRPCError(ctx context.Context, err error, translator *i18n.Translator) error {
func CaosToGRPCError(ctx context.Context, err error) error {
if err == nil {
return nil
}
@ -19,10 +18,8 @@ func CaosToGRPCError(ctx context.Context, err error, translator *i18n.Translator
return status.Convert(err).Err()
}
msg := key
if translator != nil {
msg = translator.LocalizeFromCtx(ctx, key, nil)
msg += " (" + id + ")"
}
msg += " (" + id + ")"
s, err := status.New(code, msg).WithDetails(&message.ErrorDetail{Id: id, Message: key})
if err != nil {
logging.Log("GRPC-gIeRw").WithError(err).Debug("unable to add detail")

View File

@ -1,4 +1,4 @@
package grpc
package errors
import (
"context"
@ -8,13 +8,11 @@ import (
"google.golang.org/grpc/codes"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/i18n"
)
func TestCaosToGRPCError(t *testing.T) {
type args struct {
err error
translator *i18n.Translator
err error
}
tests := []struct {
name string
@ -28,18 +26,18 @@ func TestCaosToGRPCError(t *testing.T) {
},
{
"unknown error",
args{errors.New("unknown"), nil},
args{errors.New("unknown")},
true,
},
{
"caos error",
args{caos_errs.ThrowInternal(nil, "", "message"), nil},
args{caos_errs.ThrowInternal(nil, "", "message")},
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := CaosToGRPCError(context.Background(), tt.args.err, tt.args.translator); (err != nil) != tt.wantErr {
if err := CaosToGRPCError(context.Background(), tt.args.err); (err != nil) != tt.wantErr {
t.Errorf("CaosToGRPCError() error = %v, wantErr %v", err, tt.wantErr)
}
})

View File

@ -21,8 +21,8 @@ var (
type verifierMock struct{}
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, error) {
return "", "", nil
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID string) (string, string, string, error) {
return "", "", "", nil
}
func (v *verifierMock) ResolveGrants(ctx context.Context) (*authz.Grant, error) {
return nil, nil

View File

@ -2,24 +2,20 @@ package middleware
import (
"context"
"github.com/caos/zitadel/internal/api/grpc/errors"
"golang.org/x/text/language"
"google.golang.org/grpc"
grpc_util "github.com/caos/zitadel/internal/api/grpc"
"github.com/caos/zitadel/internal/i18n"
_ "github.com/caos/zitadel/internal/statik"
)
func ErrorHandler(defaultLanguage language.Tag) grpc.UnaryServerInterceptor {
translator := newZitadelTranslator(defaultLanguage)
func ErrorHandler() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
return toGRPCError(ctx, req, handler, translator)
return toGRPCError(ctx, req, handler)
}
}
func toGRPCError(ctx context.Context, req interface{}, handler grpc.UnaryHandler, translator *i18n.Translator) (interface{}, error) {
func toGRPCError(ctx context.Context, req interface{}, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
return resp, grpc_util.CaosToGRPCError(ctx, err, translator)
return resp, errors.CaosToGRPCError(ctx, err)
}

View File

@ -50,7 +50,7 @@ func Test_toGRPCError(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := toGRPCError(tt.args.ctx, tt.args.req, tt.args.handler, nil)
got, err := toGRPCError(tt.args.ctx, tt.args.req, tt.args.handler)
if (err != nil) != tt.res.wantErr {
t.Errorf("toGRPCError() error = %v, wantErr %v", err, tt.res.wantErr)
return

View File

@ -18,6 +18,9 @@ func TranslationHandler(defaultLanguage language.Tag) func(ctx context.Context,
if loc, ok := resp.(localizers); ok && resp != nil {
translateFields(ctx, loc, translator)
}
if err != nil {
err = translateError(ctx, err, translator)
}
return resp, err
}
}

View File

@ -2,6 +2,8 @@ package middleware
import (
"context"
"errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/i18n"
@ -26,6 +28,17 @@ func translateFields(ctx context.Context, object localizers, translator *i18n.Tr
}
}
func translateError(ctx context.Context, err error, translator *i18n.Translator) error {
if translator == nil || err == nil {
return err
}
caosErr := new(caos_errs.CaosError)
if errors.As(err, &caosErr) {
caosErr.SetMessage(translator.LocalizeFromCtx(ctx, caosErr.GetMessage(), nil))
}
return caosErr
}
func newZitadelTranslator(defaultLanguage language.Tag) *i18n.Translator {
return translatorFromNamespace("zitadel", defaultLanguage)
}

View File

@ -30,14 +30,13 @@ func CreateServer(verifier *authz.TokenVerifier, authConfig authz.Config, lang l
middleware.TracingStatsServer(http.Healthz, http.Readiness, http.Validation),
grpc.UnaryInterceptor(
grpc_middleware.ChainUnaryServer(
middleware.ErrorHandler(lang),
middleware.ErrorHandler(),
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.TranslationHandler(lang),
grpc_middleware.ChainUnaryServer(
middleware.AuthorizationInterceptor(verifier, authConfig),
),
),
),
)
}
func Serve(ctx context.Context, server *grpc.Server, port string) {

View File

@ -2,6 +2,8 @@ package oidc
import (
"context"
"github.com/caos/logging"
"golang.org/x/text/language"
"github.com/caos/oidc/pkg/oidc"
"github.com/caos/oidc/pkg/op"
@ -78,6 +80,8 @@ func (o *OPStorage) GetUserinfoFromScopes(ctx context.Context, userID string, sc
userInfo.PreferredUsername = user.PreferredLoginName
userInfo.UpdatedAt = user.ChangeDate
userInfo.Gender = oidc.Gender(getGender(user.Gender))
userInfo.Locale, err = language.Parse(user.PreferredLanguage)
logging.Log("OIDC-4ks9F").OnError(err).Debug("unable to parse locale")
case scopePhone:
userInfo.PhoneNumber = user.Phone
userInfo.PhoneNumberVerified = user.IsPhoneVerified

View File

@ -14,7 +14,12 @@ type TokenRepo struct {
}
func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*token_model.Token, error) {
token, err := repo.View.CreateToken(agentID, applicationID, userID, audience, scopes, lifetime)
preferredLanguage := ""
user, _ := repo.View.UserByID(userID)
if user != nil {
preferredLanguage = user.PreferredLanguage
}
token, err := repo.View.CreateToken(agentID, applicationID, userID, preferredLanguage, audience, scopes, lifetime)
if err != nil {
return nil, err
}

View File

@ -3,6 +3,7 @@ package handler
import (
"context"
"encoding/json"
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
"github.com/caos/logging"
@ -43,6 +44,17 @@ func (u *Token) EventQuery() (*models.SearchQuery, error) {
func (u *Token) Reduce(event *models.Event) (err error) {
switch event.Type {
case user_es_model.UserProfileChanged:
user := new(view_model.UserView)
user.AppendEvent(event)
tokens, err := u.view.TokensByUserID(event.AggregateID)
if err != nil {
return err
}
for _, token := range tokens {
token.PreferredLanguage = user.PreferredLanguage
}
return u.view.PutTokens(tokens, event.Sequence)
case user_es_model.SignedOut:
id, err := agentIDFromSession(event)
if err != nil {

View File

@ -16,25 +16,30 @@ func (v *View) TokenByID(tokenID string) (*model.Token, error) {
return view.TokenByID(v.Db, tokenTable, tokenID)
}
func (v *View) TokensByUserID(userID string) ([]*model.Token, error) {
return view.TokensByUserID(v.Db, tokenTable, userID)
}
func (v *View) IsTokenValid(tokenID string) (bool, error) {
return view.IsTokenValid(v.Db, tokenTable, tokenID)
}
func (v *View) CreateToken(agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*model.Token, error) {
func (v *View) CreateToken(agentID, applicationID, userID, preferredLanguage string, audience, scopes []string, lifetime time.Duration) (*model.Token, error) {
id, err := v.idGenerator.Next()
if err != nil {
return nil, err
}
now := time.Now().UTC()
token := &model.Token{
ID: id,
CreationDate: now,
UserID: userID,
ApplicationID: applicationID,
UserAgentID: agentID,
Scopes: scopes,
Audience: audience,
Expiration: now.Add(lifetime),
ID: id,
CreationDate: now,
UserID: userID,
ApplicationID: applicationID,
UserAgentID: agentID,
Scopes: scopes,
Audience: audience,
Expiration: now.Add(lifetime),
PreferredLanguage: preferredLanguage,
}
if err := view.PutToken(v.Db, tokenTable, token); err != nil {
return nil, err
@ -50,6 +55,14 @@ func (v *View) PutToken(token *model.Token) error {
return v.ProcessedTokenSequence(token.Sequence)
}
func (v *View) PutTokens(token []*model.Token, sequence uint64) error {
err := view.PutTokens(v.Db, tokenTable, token...)
if err != nil {
return err
}
return v.ProcessedTokenSequence(sequence)
}
func (v *View) DeleteToken(tokenID string, eventSequence uint64) error {
err := view.DeleteToken(v.Db, tokenTable, tokenID)
if err != nil {

View File

@ -19,26 +19,26 @@ type TokenVerifierRepo struct {
View *view.View
}
func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenString, clientID string) (userID string, agentID string, err error) {
func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenString, clientID string) (userID string, agentID string, prefLang string, err error) {
//TODO: use real key
tokenID, err := crypto.DecryptAESString(tokenString, string(repo.TokenVerificationKey[:32]))
if err != nil {
return "", "", caos_errs.ThrowUnauthenticated(nil, "APP-8EF0zZ", "invalid token")
return "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-8EF0zZ", "invalid token")
}
token, err := repo.View.TokenByID(tokenID)
if err != nil {
return "", "", caos_errs.ThrowUnauthenticated(err, "APP-BxUSiL", "invalid token")
return "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-BxUSiL", "invalid token")
}
if !token.Expiration.After(time.Now().UTC()) {
return "", "", caos_errs.ThrowUnauthenticated(err, "APP-k9KS0", "invalid token")
return "", "", "", caos_errs.ThrowUnauthenticated(err, "APP-k9KS0", "invalid token")
}
for _, aud := range token.Audience {
if clientID == aud {
return token.UserID, token.UserAgentID, nil
return token.UserID, token.UserAgentID, token.PreferredLanguage, nil
}
}
return "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
return "", "", "", caos_errs.ThrowUnauthenticated(nil, "APP-Zxfako", "invalid audience")
}
func (repo *TokenVerifierRepo) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error) {

View File

@ -44,6 +44,10 @@ func (err *CaosError) GetMessage() string {
return err.Message
}
func (err *CaosError) SetMessage(msg string) {
err.Message = msg
}
func (err *CaosError) GetID() string {
return err.ID
}

View File

@ -9,6 +9,7 @@ import (
type Error interface {
GetParent() error
GetMessage() string
SetMessage(string)
GetID() string
}

View File

@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"github.com/BurntSushi/toml"
"github.com/caos/zitadel/internal/api/authz"
"github.com/grpc-ecosystem/go-grpc-middleware/util/metautils"
"io/ioutil"
"net/http"
@ -130,6 +131,10 @@ func (t *Translator) langsFromRequest(r *http.Request) []string {
func (t *Translator) langsFromCtx(ctx context.Context) []string {
langs := make([]string, 0)
if ctx != nil {
ctxData := authz.GetCtxData(ctx)
if ctxData.PreferredLanguage != "" {
langs = append(langs, authz.GetCtxData(ctx).PreferredLanguage)
}
langs = append(langs, getAcceptLanguageHeader(ctx))
}
return langs

View File

@ -7,17 +7,18 @@ import (
)
type Token struct {
ID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
UserID string
ApplicationID string
UserAgentID string
Audience []string
Expiration time.Time
Scopes []string
Sequence uint64
ID string
CreationDate time.Time
ChangeDate time.Time
ResourceOwner string
UserID string
ApplicationID string
UserAgentID string
Audience []string
Expiration time.Time
Scopes []string
Sequence uint64
PreferredLanguage string
}
type TokenSearchRequest struct {

View File

@ -18,47 +18,50 @@ const (
)
type Token struct {
ID string `json:"-" gorm:"column:id;primary_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
UserID string `json:"-" gorm:"column:user_id"`
ApplicationID string `json:"-" gorm:"column:application_id"`
UserAgentID string `json:"-" gorm:"column:user_agent_id"`
Audience pq.StringArray `json:"-" gorm:"column:audience"`
Scopes pq.StringArray `json:"-" gorm:"column:scopes"`
Expiration time.Time `json:"-" gorm:"column:expiration"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
ID string `json:"-" gorm:"column:id;primary_key"`
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
UserID string `json:"-" gorm:"column:user_id"`
ApplicationID string `json:"-" gorm:"column:application_id"`
UserAgentID string `json:"-" gorm:"column:user_agent_id"`
Audience pq.StringArray `json:"-" gorm:"column:audience"`
Scopes pq.StringArray `json:"-" gorm:"column:scopes"`
Expiration time.Time `json:"-" gorm:"column:expiration"`
Sequence uint64 `json:"-" gorm:"column:sequence"`
PreferredLanguage string `json:"-" gorm:"column:preferred_language"`
}
func TokenFromModel(token *model.Token) *Token {
return &Token{
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
UserID: token.UserID,
ApplicationID: token.ApplicationID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
Expiration: token.Expiration,
Sequence: token.Sequence,
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
UserID: token.UserID,
ApplicationID: token.ApplicationID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
Expiration: token.Expiration,
Sequence: token.Sequence,
PreferredLanguage: token.PreferredLanguage,
}
}
func TokenToModel(token *Token) *model.Token {
return &model.Token{
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
UserID: token.UserID,
ApplicationID: token.ApplicationID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
Expiration: token.Expiration,
Sequence: token.Sequence,
ID: token.ID,
CreationDate: token.CreationDate,
ChangeDate: token.ChangeDate,
ResourceOwner: token.ResourceOwner,
UserID: token.UserID,
ApplicationID: token.ApplicationID,
UserAgentID: token.UserAgentID,
Audience: token.Audience,
Scopes: token.Scopes,
Expiration: token.Expiration,
Sequence: token.Sequence,
PreferredLanguage: token.PreferredLanguage,
}
}

View File

@ -1,6 +1,7 @@
package view
import (
global_model "github.com/caos/zitadel/internal/model"
"time"
"github.com/jinzhu/gorm"
@ -22,6 +23,20 @@ func TokenByID(db *gorm.DB, table, tokenID string) (*model.Token, error) {
return token, err
}
func TokensByUserID(db *gorm.DB, table, userID string) ([]*model.Token, error) {
tokens := make([]*model.Token, 0)
userIDQuery := &token_model.TokenSearchQuery{
Key: token_model.TokenSearchKeyUserID,
Method: global_model.SearchMethodEquals,
Value: userID,
}
query := repository.PrepareSearchQuery(table, model.TokenSearchRequest{
Queries: []*token_model.TokenSearchQuery{userIDQuery},
})
_, err := query(db, &tokens)
return tokens, err
}
func IsTokenValid(db *gorm.DB, table, tokenID string) (bool, error) {
token, err := TokenByID(db, table, tokenID)
if err == nil {
@ -38,6 +53,15 @@ func PutToken(db *gorm.DB, table string, token *model.Token) error {
return save(db, token)
}
func PutTokens(db *gorm.DB, table string, tokens ...*model.Token) error {
save := repository.PrepareBulkSave(table)
t := make([]interface{}, len(tokens))
for i, token := range tokens {
t[i] = token
}
return save(db, t...)
}
func DeleteToken(db *gorm.DB, table, tokenID string) error {
delete := repository.PrepareDeleteByKey(table, model.TokenSearchKey(token_model.TokenSearchKeyTokenID), tokenID)
return delete(db)

View File

@ -2,11 +2,11 @@ package tracing
import (
"fmt"
errors2 "github.com/caos/zitadel/internal/api/grpc/errors"
"strconv"
"go.opencensus.io/trace"
"github.com/caos/zitadel/internal/api/grpc"
"github.com/caos/zitadel/internal/errors"
)
@ -40,7 +40,7 @@ func (s *Span) SetStatusByError(err error) {
}
func statusFromError(err error) trace.Status {
code, msg, _, _ := grpc.ExtractCaosError(err)
code, msg, _, _ := errors2.ExtractCaosError(err)
return trace.Status{Code: int32(code), Message: msg}
}

View File

@ -0,0 +1,2 @@
ALTER TABLE auth.tokens ADD COLUMN preferred_language TEXT;