Merge branch 'main' into grcp-server-reflect

This commit is contained in:
Tim Möhlmann
2023-04-30 14:40:13 +03:00
311 changed files with 15961 additions and 3609 deletions

View File

@@ -103,6 +103,12 @@ func (a *API) RegisterService(ctx context.Context, grpcServer server.Server) err
return nil
}
// HandleFunc allows registering a [http.HandlerFunc] on an exact
// path, instead of prefix like RegisterHandlerOnPrefix.
func (a *API) HandleFunc(path string, f http.HandlerFunc) {
a.router.HandleFunc(path, f)
}
// RegisterHandlerOnPrefix registers a http handler on a path prefix
// the prefix will not be passed to the actual handler
func (a *API) RegisterHandlerOnPrefix(prefix string, handler http.Handler) {

View File

@@ -14,11 +14,11 @@ const (
authenticated = "authenticated"
)
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgIDHeader string, verifier *TokenVerifier, authConfig Config, requiredAuthOption Option, method string) (ctxSetter func(context.Context) context.Context, err error) {
ctx, span := tracing.NewServerInterceptorSpan(ctx)
defer func() { span.EndWithError(err) }()
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgID, verifier, method)
ctxData, err := VerifyTokenAndCreateCtxData(ctx, token, orgIDHeader, verifier, method)
if err != nil {
return nil, err
}
@@ -29,7 +29,7 @@ func CheckUserAuthorization(ctx context.Context, req interface{}, token, orgID s
}, nil
}
requestedPermissions, allPermissions, err := getUserMethodPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig, ctxData)
requestedPermissions, allPermissions, err := getUserPermissions(ctx, verifier, requiredAuthOption.Permission, authConfig.RolePermissionMappings, ctxData, ctxData.OrgID)
if err != nil {
return nil, err
}
@@ -110,18 +110,6 @@ func HasGlobalPermission(perms []string) bool {
return false
}
func HasGlobalExplicitPermission(perms []string, permToCheck string) bool {
for _, perm := range perms {
p, ctxID := SplitPermission(perm)
if p == permToCheck {
if ctxID == "" {
return true
}
}
}
return false
}
func GetAllPermissionCtxIDs(perms []string) []string {
ctxIDs := make([]string, 0)
for _, perm := range perms {
@@ -132,16 +120,3 @@ func GetAllPermissionCtxIDs(perms []string) []string {
}
return ctxIDs
}
func GetExplicitPermissionCtxIDs(perms []string, searchPerm string) []string {
ctxIDs := make([]string, 0)
for _, perm := range perms {
p, ctxID := SplitPermission(perm)
if p == searchPerm {
if ctxID != "" {
ctxIDs = append(ctxIDs, ctxID)
}
}
}
return ctxIDs
}

View File

@@ -14,11 +14,11 @@ type MethodMapping map[string]Option
type Option struct {
Permission string
CheckParam string
Feature string
AllowSelf bool
}
func (a *Config) getPermissionsFromRole(role string) []string {
for _, roleMap := range a.RolePermissionMappings {
func getPermissionsFromRole(rolePermissionMappings []RoleMapping, role string) []string {
for _, roleMap := range rolePermissionMappings {
if roleMap.Role == role {
return roleMap.Permissions
}

View File

@@ -7,7 +7,28 @@ import (
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPerm string, authConfig Config, ctxData CtxData) (requestedPermissions, allPermissions []string, err error) {
func CheckPermission(ctx context.Context, resolver MembershipsResolver, roleMappings []RoleMapping, permission, orgID, resourceID string, allowSelf bool) (err error) {
ctxData := GetCtxData(ctx)
if allowSelf && ctxData.UserID == resourceID {
return nil
}
requestedPermissions, _, err := getUserPermissions(ctx, resolver, permission, roleMappings, ctxData, orgID)
if err != nil {
return err
}
_, userPermissionSpan := tracing.NewNamedSpan(ctx, "checkUserPermissions")
err = checkUserResourcePermissions(requestedPermissions, resourceID)
userPermissionSpan.EndWithError(err)
if err != nil {
return err
}
return nil
}
// getUserPermissions retrieves the memberships of the authenticated user (on instance and provided organisation level),
// and maps them to permissions. It will return the requested permission(s) and all other granted permissions separately.
func getUserPermissions(ctx context.Context, resolver MembershipsResolver, requiredPerm string, roleMappings []RoleMapping, ctxData CtxData, orgID string) (requestedPermissions, allPermissions []string, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
@@ -16,13 +37,13 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
}
ctx = context.WithValue(ctx, dataKey, ctxData)
memberships, err := t.SearchMyMemberships(ctx)
memberships, err := resolver.SearchMyMemberships(ctx, orgID)
if err != nil {
return nil, nil, err
}
if len(memberships) == 0 {
err = retry(func() error {
memberships, err = t.SearchMyMemberships(ctx)
memberships, err = resolver.SearchMyMemberships(ctx, orgID)
if err != nil {
return err
}
@@ -35,24 +56,56 @@ func getUserMethodPermissions(ctx context.Context, t *TokenVerifier, requiredPer
return nil, nil, nil
}
}
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, authConfig)
requestedPermissions, allPermissions = mapMembershipsToPermissions(requiredPerm, memberships, roleMappings)
return requestedPermissions, allPermissions, nil
}
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, authConfig Config) (requestPermissions, allPermissions []string) {
// checkUserResourcePermissions checks that if a user i granted either the requested permission globally (project.write)
// or the specific resource (project.write:123)
func checkUserResourcePermissions(userPerms []string, resourceID string) error {
if len(userPerms) == 0 {
return errors.ThrowPermissionDenied(nil, "AUTH-AWfge", "No matching permissions found")
}
if resourceID == "" {
return nil
}
if HasGlobalPermission(userPerms) {
return nil
}
if hasContextResourcePermission(userPerms, resourceID) {
return nil
}
return errors.ThrowPermissionDenied(nil, "AUTH-Swrgg2", "No matching permissions found")
}
func hasContextResourcePermission(permissions []string, resourceID string) bool {
for _, perm := range permissions {
_, ctxID := SplitPermission(perm)
if resourceID == ctxID {
return true
}
}
return false
}
func mapMembershipsToPermissions(requiredPerm string, memberships []*Membership, roleMappings []RoleMapping) (requestPermissions, allPermissions []string) {
requestPermissions = make([]string, 0)
allPermissions = make([]string, 0)
for _, membership := range memberships {
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, authConfig, requestPermissions, allPermissions)
requestPermissions, allPermissions = mapMembershipToPerm(requiredPerm, membership, roleMappings, requestPermissions, allPermissions)
}
return requestPermissions, allPermissions
}
func mapMembershipToPerm(requiredPerm string, membership *Membership, authConfig Config, requestPermissions, allPermissions []string) ([]string, []string) {
func mapMembershipToPerm(requiredPerm string, membership *Membership, roleMappings []RoleMapping, requestPermissions, allPermissions []string) ([]string, []string) {
roleNames, roleContextID := roleWithContext(membership)
for _, roleName := range roleNames {
perms := authConfig.getPermissionsFromRole(roleName)
perms := getPermissionsFromRole(roleMappings, roleName)
for _, p := range perms {
permWithCtx := addRoleContextIDToPerm(p, roleContextID)

View File

@@ -18,7 +18,7 @@ type testVerifier struct {
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "userID", "agentID", "clientID", "de", "orgID", nil
}
func (v *testVerifier) SearchMyMemberships(ctx context.Context) ([]*Membership, error) {
func (v *testVerifier) SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error) {
return v.memberships, nil
}
@@ -46,7 +46,7 @@ func equalStringArray(a, b []string) bool {
return true
}
func Test_GetUserMethodPermissions(t *testing.T) {
func Test_GetUserPermissions(t *testing.T) {
type args struct {
ctxData CtxData
verifier *TokenVerifier
@@ -139,7 +139,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, perms, err := getUserMethodPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig, tt.args.ctxData)
_, perms, err := getUserPermissions(context.Background(), tt.args.verifier, tt.args.requiredPerm, tt.args.authConfig.RolePermissionMappings, tt.args.ctxData, tt.args.ctxData.OrgID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
@@ -295,7 +295,7 @@ func Test_MapMembershipToPermissions(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig)
requestPerms, allPerms := mapMembershipsToPermissions(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings)
if !equalStringArray(requestPerms, tt.requestPerms) {
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
}
@@ -435,7 +435,7 @@ func Test_MapMembershipToPerm(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig, tt.args.requestPerms, tt.args.allPerms)
requestPerms, allPerms := mapMembershipToPerm(tt.args.requiredPerm, tt.args.membership, tt.args.authConfig.RolePermissionMappings, tt.args.requestPerms, tt.args.allPerms)
if !equalStringArray(requestPerms, tt.requestPerms) {
t.Errorf("got wrong requestPerms, expecting: %v, actual: %v ", tt.requestPerms, requestPerms)
}
@@ -519,3 +519,109 @@ func Test_ExistisPerm(t *testing.T) {
})
}
}
func Test_CheckUserResourcePermissions(t *testing.T) {
type args struct {
perms []string
resourceID string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "no permissions",
args: args{
perms: []string{},
resourceID: "",
},
wantErr: true,
},
{
name: "has permission and no context requested",
args: args{
perms: []string{"project.read"},
resourceID: "",
},
wantErr: false,
},
{
name: "context requested and has global permission",
args: args{
perms: []string{"project.read", "project.read:1"},
resourceID: "Test",
},
wantErr: false,
},
{
name: "context requested and has specific permission",
args: args{
perms: []string{"project.read:Test"},
resourceID: "Test",
},
wantErr: false,
},
{
name: "context requested and has no permission",
args: args{
perms: []string{"project.read:Test"},
resourceID: "Hodor",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := checkUserResourcePermissions(tt.args.perms, tt.args.resourceID)
if tt.wantErr && err == nil {
t.Errorf("got wrong result, should get err: actual: %v ", err)
}
if !tt.wantErr && err != nil {
t.Errorf("shouldn't get err: %v ", err)
}
if tt.wantErr && !caos_errs.IsPermissionDenied(err) {
t.Errorf("got wrong err: %v ", err)
}
})
}
}
func Test_HasContextResourcePermission(t *testing.T) {
type args struct {
perms []string
resourceID string
}
tests := []struct {
name string
args args
result bool
}{
{
name: "existing context permission",
args: args{
perms: []string{"test:wrong", "test:right"},
resourceID: "right",
},
result: true,
},
{
name: "not existing context permission",
args: args{
perms: []string{"test:wrong", "test:wrong2"},
resourceID: "test",
},
result: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := hasContextResourcePermission(tt.args.perms, tt.args.resourceID)
if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
}
})
}
}

View File

@@ -27,10 +27,14 @@ type TokenVerifier struct {
systemJWTProfile op.JWTProfileVerifier
}
type MembershipsResolver interface {
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
}
type authZRepo interface {
VerifyAccessToken(ctx context.Context, token, verifierClientID, projectID string) (userID, agentID, clientID, prefLang, resourceOwner string, err error)
VerifierClientID(ctx context.Context, name string) (clientID, projectID string, err error)
SearchMyMemberships(ctx context.Context) ([]*Membership, error)
SearchMyMemberships(ctx context.Context, orgID string) ([]*Membership, error)
ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (projectID string, origins []string, err error)
ExistsOrg(ctx context.Context, orgID string) error
}
@@ -127,10 +131,10 @@ func (v *TokenVerifier) RegisterServer(appName, methodPrefix string, mappings Me
}
}
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context) (_ []*Membership, err error) {
func (v *TokenVerifier) SearchMyMemberships(ctx context.Context, orgID string) (_ []*Membership, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
return v.authZRepo.SearchMyMemberships(ctx)
return v.authZRepo.SearchMyMemberships(ctx, orgID)
}
func (v *TokenVerifier) ProjectIDAndOriginsByClientID(ctx context.Context, clientID string) (_ string, _ []string, err error) {

View File

@@ -71,7 +71,7 @@ func (s *Server) AppName() string {
}
func (s *Server) MethodPrefix() string {
return admin.AdminService_MethodPrefix
return admin.AdminService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {

View File

@@ -69,7 +69,7 @@ func (s *Server) AppName() string {
}
func (s *Server) MethodPrefix() string {
return auth.AuthService_MethodPrefix
return auth.AuthService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {

View File

@@ -63,7 +63,7 @@ func (s *Server) AppName() string {
}
func (s *Server) MethodPrefix() string {
return management.ManagementService_MethodPrefix
return management.ManagementService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {

View File

@@ -210,17 +210,14 @@ func (s *Server) BulkRemoveUserMetadata(ctx context.Context, req *mgmt_pb.BulkRe
}
func (s *Server) AddHumanUser(ctx context.Context, req *mgmt_pb.AddHumanUserRequest) (*mgmt_pb.AddHumanUserResponse, error) {
details, err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, AddHumanUserRequestToAddHuman(req))
human := AddHumanUserRequestToAddHuman(req)
err := s.command.AddHuman(ctx, authz.GetCtxData(ctx).OrgID, human, true)
if err != nil {
return nil, err
}
return &mgmt_pb.AddHumanUserResponse{
UserId: details.ID,
Details: obj_grpc.AddToDetailsPb(
details.Sequence,
details.EventDate,
details.ResourceOwner,
),
UserId: human.ID,
Details: obj_grpc.DomainToAddDetailsPb(human.Details),
}, nil
}

View File

@@ -0,0 +1,19 @@
package object
import (
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
)
func DomainToDetailsPb(objectDetail *domain.ObjectDetails) *object.Details {
details := &object.Details{
Sequence: objectDetail.Sequence,
ResourceOwner: objectDetail.ResourceOwner,
}
if !objectDetail.EventDate.IsZero() {
details.ChangeDate = timestamppb.New(objectDetail.EventDate)
}
return details
}

View File

@@ -136,6 +136,8 @@ func OIDCGrantTypesFromModel(grantTypes []domain.OIDCGrantType) []app_pb.OIDCGra
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_IMPLICIT
case domain.OIDCGrantTypeRefreshToken:
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN
case domain.OIDCGrantTypeDeviceCode:
oidcGrantTypes[i] = app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE
}
}
return oidcGrantTypes
@@ -154,6 +156,8 @@ func OIDCGrantTypesToDomain(grantTypes []app_pb.OIDCGrantType) []domain.OIDCGran
oidcGrantTypes[i] = domain.OIDCGrantTypeImplicit
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_REFRESH_TOKEN:
oidcGrantTypes[i] = domain.OIDCGrantTypeRefreshToken
case app_pb.OIDCGrantType_OIDC_GRANT_TYPE_DEVICE_CODE:
oidcGrantTypes[i] = domain.OIDCGrantTypeDeviceCode
}
}
return oidcGrantTypes

View File

@@ -34,6 +34,9 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
}
orgID := grpc_util.GetHeader(authCtx, http.ZitadelOrgID)
if o, ok := req.(AuthContext); ok {
orgID = o.AuthContext()
}
ctxSetter, err := authz.CheckUserAuthorization(authCtx, req, authToken, orgID, verifier, authConfig, authOpt, info.FullMethod)
if err != nil {
@@ -42,3 +45,7 @@ func authorize(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo,
span.End()
return handler(ctxSetter(ctx), req)
}
type AuthContext interface {
AuthContext() string
}

View File

@@ -24,7 +24,7 @@ type verifierMock struct{}
func (v *verifierMock) VerifyAccessToken(ctx context.Context, token, clientID, projectID string) (string, string, string, string, string, error) {
return "", "", "", "", "", nil
}
func (v *verifierMock) SearchMyMemberships(ctx context.Context) ([]*authz.Membership, error) {
func (v *verifierMock) SearchMyMemberships(ctx context.Context, orgID string) ([]*authz.Membership, error) {
return nil, nil
}

View File

@@ -50,13 +50,13 @@ func CreateServer(
middleware.MetricsHandler(metricTypes, grpc_api.Probes...),
middleware.NoCacheInterceptor(),
middleware.ErrorHandler(),
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_MethodPrefix, healthpb.Health_ServiceDesc.ServiceName),
middleware.InstanceInterceptor(queries, hostHeaderName, system_pb.SystemService_ServiceDesc.ServiceName, healthpb.Health_ServiceDesc.ServiceName),
middleware.AccessStorageInterceptor(accessSvc),
middleware.AuthorizationInterceptor(verifier, authConfig),
middleware.TranslationHandler(),
middleware.ValidationHandler(),
middleware.ServiceHandler(),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_MethodPrefix),
middleware.QuotaExhaustedInterceptor(accessSvc, system_pb.SystemService_ServiceDesc.ServiceName),
),
),
}

View File

@@ -4,7 +4,6 @@ import (
"google.golang.org/grpc"
"github.com/zitadel/zitadel/internal/admin/repository"
"github.com/zitadel/zitadel/internal/admin/repository/eventsourcing"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
@@ -60,7 +59,7 @@ func (s *Server) AppName() string {
}
func (s *Server) MethodPrefix() string {
return system.SystemService_MethodPrefix
return system.SystemService_ServiceDesc.ServiceName
}
func (s *Server) AuthMethods() authz.MethodMapping {

View File

@@ -0,0 +1,65 @@
package user
import (
"context"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) SetEmail(ctx context.Context, req *user.SetEmailRequest) (resp *user.SetEmailResponse, err error) {
var resourceOwner string // TODO: check if still needed
var email *domain.Email
switch v := req.GetVerification().(type) {
case *user.SetEmailRequest_SendCode:
email, err = s.command.ChangeUserEmailURLTemplate(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg, v.SendCode.GetUrlTemplate())
case *user.SetEmailRequest_ReturnCode:
email, err = s.command.ChangeUserEmailReturnCode(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
case *user.SetEmailRequest_IsVerified:
if v.IsVerified {
email, err = s.command.ChangeUserEmailVerified(ctx, req.GetUserId(), resourceOwner, req.GetEmail())
} else {
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
}
case nil:
email, err = s.command.ChangeUserEmail(ctx, req.GetUserId(), resourceOwner, req.GetEmail(), s.userCodeAlg)
default:
err = caos_errs.ThrowUnimplementedf(nil, "USERv2-Ahng0", "verification oneOf %T in method SetEmail not implemented", v)
}
if err != nil {
return nil, err
}
return &user.SetEmailResponse{
Details: &object.Details{
Sequence: email.Sequence,
ChangeDate: timestamppb.New(email.ChangeDate),
ResourceOwner: email.ResourceOwner,
},
VerificationCode: email.PlainCode,
}, nil
}
func (s *Server) VerifyEmail(ctx context.Context, req *user.VerifyEmailRequest) (*user.VerifyEmailResponse, error) {
details, err := s.command.VerifyUserEmail(ctx,
req.GetUserId(),
"", // TODO: check if still needed
req.GetVerificationCode(),
s.userCodeAlg,
)
if err != nil {
return nil, err
}
return &user.VerifyEmailResponse{
Details: &object.Details{
Sequence: details.Sequence,
ChangeDate: timestamppb.New(details.EventDate),
ResourceOwner: details.ResourceOwner,
},
}, nil
}

View File

@@ -6,27 +6,27 @@ import (
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/server"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/crypto"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
var _ user.UserServiceServer = (*Server)(nil)
type Server struct {
user.UnimplementedUserServiceServer
command *command.Commands
query *query.Queries
command *command.Commands
query *query.Queries
userCodeAlg crypto.EncryptionAlgorithm
}
type Config struct{}
func CreateServer(
command *command.Commands,
query *query.Queries,
) *Server {
func CreateServer(command *command.Commands, query *query.Queries, userCodeAlg crypto.EncryptionAlgorithm) *Server {
return &Server{
command: command,
query: query,
command: command,
query: query,
userCodeAlg: userCodeAlg,
}
}

View File

@@ -1,55 +0,0 @@
package user
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) TestGet(ctx context.Context, req *user.TestGetRequest) (*user.TestGetResponse, error) {
return &user.TestGetResponse{
Ctx: req.Ctx.String(),
}, nil
}
func (s *Server) TestPost(ctx context.Context, req *user.TestPostRequest) (*user.TestPostResponse, error) {
return &user.TestPostResponse{
Ctx: req.Ctx.String(),
}, nil
}
func (s *Server) TestAuth(ctx context.Context, req *user.TestAuthRequest) (*user.TestAuthResponse, error) {
reqCtx, err := authDemo(ctx, req.Ctx)
if err != nil {
return nil, err
}
return &user.TestAuthResponse{
User: &user.User{Id: authz.GetCtxData(ctx).UserID},
Ctx: reqCtx,
}, nil
}
func authDemo(ctx context.Context, reqCtx *user.Context) (*user.Context, error) {
ro := authz.GetCtxData(ctx).ResourceOwner
if reqCtx == nil {
return &user.Context{Ctx: &user.Context_OrgId{OrgId: ro}}, nil
}
switch c := reqCtx.Ctx.(type) {
case *user.Context_OrgId:
if c.OrgId == ro {
return reqCtx, nil
}
return nil, errors.ThrowPermissionDenied(nil, "USER-dg4g", "Errors.User.NotAllowedOrg")
case *user.Context_OrgDomain:
if c.OrgDomain == "forbidden.com" {
return nil, errors.ThrowPermissionDenied(nil, "USER-SDg4g", "Errors.User.NotAllowedOrg")
}
return reqCtx, nil
case *user.Context_Instance:
return reqCtx, nil
default:
return reqCtx, nil
}
}

View File

@@ -0,0 +1,112 @@
package user
import (
"context"
"io"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/command"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) AddHumanUser(ctx context.Context, req *user.AddHumanUserRequest) (_ *user.AddHumanUserResponse, err error) {
human, err := addUserRequestToAddHuman(req)
if err != nil {
return nil, err
}
orgID := req.GetOrganisation().GetOrgId()
if orgID == "" {
orgID = authz.GetCtxData(ctx).OrgID
}
err = s.command.AddHuman(ctx, orgID, human, false)
if err != nil {
return nil, err
}
return &user.AddHumanUserResponse{
UserId: human.ID,
Details: object.DomainToDetailsPb(human.Details),
EmailCode: human.EmailCode,
}, nil
}
func addUserRequestToAddHuman(req *user.AddHumanUserRequest) (*command.AddHuman, error) {
username := req.GetUsername()
if username == "" {
username = req.GetEmail().GetEmail()
}
var urlTemplate string
if req.GetEmail().GetSendCode() != nil {
urlTemplate = req.GetEmail().GetSendCode().GetUrlTemplate()
// test the template execution so the async notification will not fail because of it and the user won't realize
if err := domain.RenderConfirmURLTemplate(io.Discard, urlTemplate, req.GetUserId(), "code", "orgID"); err != nil {
return nil, err
}
}
bcryptedPassword, err := hashedPasswordToCommand(req.GetHashedPassword())
if err != nil {
return nil, err
}
passwordChangeRequired := req.GetPassword().GetChangeRequired() || req.GetHashedPassword().GetChangeRequired()
metadata := make([]*command.AddMetadataEntry, len(req.Metadata))
for i, metadataEntry := range req.Metadata {
metadata[i] = &command.AddMetadataEntry{
Key: metadataEntry.GetKey(),
Value: metadataEntry.GetValue(),
}
}
return &command.AddHuman{
ID: req.GetUserId(),
Username: username,
FirstName: req.GetProfile().GetFirstName(),
LastName: req.GetProfile().GetLastName(),
NickName: req.GetProfile().GetNickName(),
DisplayName: req.GetProfile().GetDisplayName(),
Email: command.Email{
Address: domain.EmailAddress(req.GetEmail().GetEmail()),
Verified: req.GetEmail().GetIsVerified(),
ReturnCode: req.GetEmail().GetReturnCode() != nil,
URLTemplate: urlTemplate,
},
PreferredLanguage: language.Make(req.GetProfile().GetPreferredLanguage()),
Gender: genderToDomain(req.GetProfile().GetGender()),
Phone: command.Phone{}, // TODO: add as soon as possible
Password: req.GetPassword().GetPassword(),
BcryptedPassword: bcryptedPassword,
PasswordChangeRequired: passwordChangeRequired,
Passwordless: false,
ExternalIDP: false,
Register: false,
Metadata: metadata,
}, nil
}
func genderToDomain(gender user.Gender) domain.Gender {
switch gender {
case user.Gender_GENDER_UNSPECIFIED:
return domain.GenderUnspecified
case user.Gender_GENDER_FEMALE:
return domain.GenderFemale
case user.Gender_GENDER_MALE:
return domain.GenderMale
case user.Gender_GENDER_DIVERSE:
return domain.GenderDiverse
default:
return domain.GenderUnspecified
}
}
func hashedPasswordToCommand(hashed *user.HashedPassword) (string, error) {
if hashed == nil {
return "", nil
}
// we currently only handle bcrypt
if hashed.GetAlgorithm() != "bcrypt" {
return "", errors.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument")
}
return hashed.GetHash(), nil
}

View File

@@ -0,0 +1,80 @@
package user
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
caos_errs "github.com/zitadel/zitadel/internal/errors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func Test_hashedPasswordToCommand(t *testing.T) {
type args struct {
hashed *user.HashedPassword
}
type res struct {
want string
err func(error) bool
}
tests := []struct {
name string
args args
res res
}{
{
"not hashed",
args{
hashed: nil,
},
res{
"",
nil,
},
},
{
"hashed, not bcrypt",
args{
hashed: &user.HashedPassword{
Hash: "hash",
Algorithm: "custom",
},
},
res{
"",
func(err error) bool {
return errors.Is(err, caos_errs.ThrowInvalidArgument(nil, "USER-JDk4t", "Errors.InvalidArgument"))
},
},
},
{
"hashed, bcrypt",
args{
hashed: &user.HashedPassword{
Hash: "hash",
Algorithm: "bcrypt",
},
},
res{
"hash",
nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := hashedPasswordToCommand(tt.args.hashed)
if tt.res.err == nil {
require.NoError(t, err)
}
if tt.res.err != nil && !tt.res.err(err) {
t.Errorf("got wrong err: %v ", err)
}
if tt.res.err == nil {
assert.Equal(t, tt.res.want, got)
}
})
}
}

View File

@@ -99,15 +99,6 @@ func (a *AuthRequest) GetSubject() string {
return a.UserID
}
func (a *AuthRequest) Done() bool {
for _, step := range a.PossibleSteps {
if step.Type() == domain.NextStepRedirectToCallback {
return true
}
}
return false
}
func (a *AuthRequest) oidc() *domain.AuthRequestOIDC {
return a.Request.(*domain.AuthRequestOIDC)
}

View File

@@ -200,6 +200,8 @@ func grantTypeToOIDC(grantType domain.OIDCGrantType) oidc.GrantType {
return oidc.GrantTypeImplicit
case domain.OIDCGrantTypeRefreshToken:
return oidc.GrantTypeRefreshToken
case domain.OIDCGrantTypeDeviceCode:
return oidc.GrantTypeDeviceCode
default:
return oidc.GrantTypeCode
}

View File

@@ -0,0 +1,176 @@
package oidc
import (
"context"
"time"
"github.com/zitadel/logging"
"github.com/zitadel/oidc/v2/pkg/oidc"
"github.com/zitadel/oidc/v2/pkg/op"
"github.com/zitadel/zitadel/internal/api/ui/login"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
)
const (
DeviceAuthDefaultLifetime = 5 * time.Minute
DeviceAuthDefaultPollInterval = 5 * time.Second
)
type DeviceAuthorizationConfig struct {
Lifetime time.Duration
PollInterval time.Duration
UserCode *UserCodeConfig
}
type UserCodeConfig struct {
CharSet string
CharAmount int
DashInterval int
}
// toOPConfig converts DeviceAuthorizationConfig to a [op.DeviceAuthorizationConfig],
// setting sane defaults for empty values.
// Safe to call when c is nil.
func (c *DeviceAuthorizationConfig) toOPConfig() op.DeviceAuthorizationConfig {
out := op.DeviceAuthorizationConfig{
Lifetime: DeviceAuthDefaultLifetime,
PollInterval: DeviceAuthDefaultPollInterval,
UserFormPath: login.EndpointDeviceAuth,
UserCode: op.UserCodeBase20,
}
if c == nil {
return out
}
if c.Lifetime != 0 {
out.Lifetime = c.Lifetime
}
if c.PollInterval != 0 {
out.PollInterval = c.PollInterval
}
if c.UserCode == nil {
return out
}
if c.UserCode.CharSet != "" {
out.UserCode.CharSet = c.UserCode.CharSet
}
if c.UserCode.CharAmount != 0 {
out.UserCode.CharAmount = c.UserCode.CharAmount
}
if c.UserCode.DashInterval != 0 {
out.UserCode.DashInterval = c.UserCode.CharAmount
}
return out
}
// StoreDeviceAuthorization creates a new Device Authorization request.
// Implements the op.DeviceAuthorizationStorage interface.
func (o *OPStorage) StoreDeviceAuthorization(ctx context.Context, clientID, deviceCode, userCode string, expires time.Time, scopes []string) (err error) {
const logMsg = "store device authorization"
logger := logging.WithFields("client_id", clientID, "device_code", deviceCode, "user_code", userCode, "expires", expires, "scopes", scopes)
ctx, span := tracing.NewSpan(ctx)
defer func() {
logger.OnError(err).Error(logMsg)
span.EndWithError(err)
}()
// TODO(muhlemmer): Remove the following code block with oidc v3
// https://github.com/zitadel/oidc/issues/370
client, err := o.GetClientByClientID(ctx, clientID)
if err != nil {
return err
}
if !op.ValidateGrantType(client, oidc.GrantTypeDeviceCode) {
return errors.ThrowPermissionDeniedf(nil, "OIDC-et1Ae", "grant type %q not allowed for client", oidc.GrantTypeDeviceCode)
}
scopes, err = o.assertProjectRoleScopes(ctx, clientID, scopes)
if err != nil {
return errors.ThrowPreconditionFailed(err, "OIDC-She4t", "Errors.Internal")
}
aggrID, details, err := o.command.AddDeviceAuth(ctx, clientID, deviceCode, userCode, expires, scopes)
if err == nil {
logger.SetFields("aggregate_id", aggrID, "details", details).Debug(logMsg)
}
return err
}
func newDeviceAuthorizationState(d *domain.DeviceAuth) *op.DeviceAuthorizationState {
return &op.DeviceAuthorizationState{
ClientID: d.ClientID,
Scopes: d.Scopes,
Expires: d.Expires,
Done: d.State.Done(),
Subject: d.Subject,
Denied: d.State.Denied(),
}
}
// GetDeviceAuthorizatonState retieves the current state of the Device Authorization process.
// It implements the [op.DeviceAuthorizationStorage] interface and is used by devices that
// are polling until they successfully receive a token or we indicate a denied or expired state.
// As generated user codes are of low entropy, this implementation also takes care or
// device authorization request cleanup, when it has been Approved, Denied or Expired.
func (o *OPStorage) GetDeviceAuthorizatonState(ctx context.Context, clientID, deviceCode string) (state *op.DeviceAuthorizationState, err error) {
const logMsg = "get device authorization state"
logger := logging.WithFields("client_id", clientID, "device_code", deviceCode)
ctx, span := tracing.NewSpan(ctx)
defer func() {
if err != nil {
logger.WithError(err).Error(logMsg)
}
span.EndWithError(err)
}()
deviceAuth, err := o.query.DeviceAuthByDeviceCode(ctx, clientID, deviceCode)
if err != nil {
return nil, err
}
logger.SetFields(
"expires", deviceAuth.Expires, "scopes", deviceAuth.Scopes,
"subject", deviceAuth.Subject, "state", deviceAuth.State,
).Debug("device authorization state")
// Cancel the request if it is expired, only if it wasn't Done meanwhile
if !deviceAuth.State.Done() && deviceAuth.Expires.Before(time.Now()) {
_, err = o.command.CancelDeviceAuth(ctx, deviceAuth.AggregateID, domain.DeviceAuthCanceledExpired)
if err != nil {
return nil, err
}
deviceAuth.State = domain.DeviceAuthStateExpired
}
// When the request is more then initiated, it has been either Approved, Denied or Expired.
// At this point we should remove it from the DB to avoid user code conflicts.
if deviceAuth.State > domain.DeviceAuthStateInitiated {
_, err = o.command.RemoveDeviceAuth(ctx, deviceAuth.AggregateID)
if err != nil {
return nil, err
}
}
return newDeviceAuthorizationState(deviceAuth), nil
}
// TODO(muhlemmer): remove the following methods with oidc v3.
// They are actually not used, but are required by the oidc device storage interface.
// https://github.com/zitadel/oidc/issues/371
func (o *OPStorage) GetDeviceAuthorizationByUserCode(ctx context.Context, userCode string) (*op.DeviceAuthorizationState, error) {
return nil, nil
}
func (o *OPStorage) CompleteDeviceAuthorization(ctx context.Context, userCode, subject string) (err error) {
return nil
}
func (o *OPStorage) DenyDeviceAuthorization(ctx context.Context, userCode string) (err error) {
return nil
}
// TODO end.

View File

@@ -176,7 +176,7 @@ func (o *OPStorage) lockAndGenerateSigningKeyPair(ctx context.Context, algorithm
if errors.IsErrorAlreadyExists(err) {
return nil
}
logging.OnError(err).Warn("initial lock failed")
logging.OnError(err).Debug("initial lock failed")
return err
}

View File

@@ -40,6 +40,7 @@ type Config struct {
UserAgentCookieConfig *middleware.UserAgentCookieConfig
Cache *middleware.CacheConfig
CustomEndpoints *EndpointConfig
DeviceAuth *DeviceAuthorizationConfig
}
type EndpointConfig struct {
@@ -50,6 +51,7 @@ type EndpointConfig struct {
Revocation *Endpoint
EndSession *Endpoint
Keys *Endpoint
DeviceAuth *Endpoint
}
type Endpoint struct {
@@ -108,6 +110,7 @@ func createOPConfig(config Config, defaultLogoutRedirectURI string, cryptoKey []
GrantTypeRefreshToken: config.GrantTypeRefreshToken,
RequestObjectSupported: config.RequestObjectSupported,
SupportedUILocales: supportedLanguages,
DeviceAuthorization: config.DeviceAuth.toOPConfig(),
}
if cryptoLength := len(cryptoKey); cryptoLength != 32 {
return nil, caos_errs.ThrowInternalf(nil, "OIDC-D43gf", "crypto key must be 32 bytes, but is %d", cryptoLength)
@@ -165,6 +168,9 @@ func customEndpoints(endpointConfig *EndpointConfig) []op.Option {
if endpointConfig.Keys != nil {
options = append(options, op.WithCustomKeysEndpoint(op.NewEndpointWithURL(endpointConfig.Keys.Path, endpointConfig.Keys.URL)))
}
if endpointConfig.DeviceAuth != nil {
options = append(options, op.WithCustomDeviceAuthorizationEndpoint(op.NewEndpointWithURL(endpointConfig.DeviceAuth.Path, endpointConfig.DeviceAuth.URL)))
}
return options
}

View File

@@ -63,14 +63,6 @@ func (a *AuthRequest) GetUserID() string {
func (a *AuthRequest) GetUserName() string {
return a.UserName
}
func (a *AuthRequest) Done() bool {
for _, step := range a.PossibleSteps {
if step.Type() == domain.NextStepRedirectToCallback {
return true
}
}
return false
}
func AuthRequestFromBusiness(authReq *domain.AuthRequest) (_ models.AuthRequestInt, err error) {
if _, ok := authReq.Request.(*domain.AuthRequestSAML); !ok {

View File

@@ -123,7 +123,7 @@ func (p *Storage) lockAndGenerateCertificateAndKey(ctx context.Context, usage do
if errors.IsErrorAlreadyExists(err) {
return nil
}
logging.OnError(err).Warn("initial lock failed")
logging.OnError(err).Debug("initial lock failed")
return err
}

View File

@@ -0,0 +1,201 @@
package login
import (
errs "errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/gorilla/mux"
"github.com/muhlemmer/gu"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/http/middleware"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/errors"
)
const (
tmplDeviceAuthUserCode = "device-usercode"
tmplDeviceAuthAction = "device-action"
)
func (l *Login) renderDeviceAuthUserCode(w http.ResponseWriter, r *http.Request, err error) {
var errID, errMessage string
if err != nil {
logging.WithError(err).Error()
errID, errMessage = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, nil, "DeviceAuth.Title", "DeviceAuth.UserCode.Description", errID, errMessage)
translator := l.getTranslator(r.Context(), nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthUserCode], data, nil)
}
func (l *Login) renderDeviceAuthAction(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, scopes []string) {
data := &struct {
baseData
AuthRequestID string
Username string
ClientID string
Scopes []string
}{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Action.Description", "", ""),
AuthRequestID: authReq.ID,
Username: authReq.UserName,
ClientID: authReq.ApplicationID,
Scopes: scopes,
}
translator := l.getTranslator(r.Context(), authReq)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplDeviceAuthAction], data, nil)
}
const (
deviceAuthAllowed = "allowed"
deviceAuthDenied = "denied"
)
// renderDeviceAuthDone renders success.html when the action was allowed and error.html when it was denied.
func (l *Login) renderDeviceAuthDone(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, action string) {
data := &struct {
baseData
Message string
}{
baseData: l.getBaseData(r, authReq, "DeviceAuth.Title", "DeviceAuth.Done.Description", "", ""),
}
translator := l.getTranslator(r.Context(), authReq)
switch action {
case deviceAuthAllowed:
data.Message = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Approved", nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplSuccess], data, nil)
case deviceAuthDenied:
data.ErrMessage = translator.LocalizeFromRequest(r, "DeviceAuth.Done.Denied", nil)
l.renderer.RenderTemplate(w, r, translator, l.renderer.Templates[tmplError], data, nil)
}
}
// handleDeviceUserCode serves the Device Authorization user code submission form.
// The "user_code" may be submitted by URL (GET) or form (POST).
// When a "user_code" is received and found through query,
// handleDeviceAuthUserCode will create a new AuthRequest in the repository.
// The user is then redirected to the /login endpoint to complete authentication.
//
// The agent ID from the context is set to the authentication request
// to ensure the complete login flow is completed from the same browser.
func (l *Login) handleDeviceAuthUserCode(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
err := r.ParseForm()
if err != nil {
w.WriteHeader(http.StatusBadRequest)
l.renderDeviceAuthUserCode(w, r, err)
return
}
userCode := r.Form.Get("user_code")
if userCode == "" {
if prompt, _ := url.QueryUnescape(r.Form.Get("prompt")); prompt != "" {
err = errs.New(prompt)
}
l.renderDeviceAuthUserCode(w, r, err)
return
}
deviceAuth, err := l.query.DeviceAuthByUserCode(ctx, userCode)
if err != nil {
l.renderDeviceAuthUserCode(w, r, err)
return
}
userAgentID, ok := middleware.UserAgentIDFromCtx(ctx)
if !ok {
l.renderDeviceAuthUserCode(w, r, errs.New("internal error: agent ID missing"))
return
}
authRequest, err := l.authRepo.CreateAuthRequest(ctx, &domain.AuthRequest{
CreationDate: time.Now(),
AgentID: userAgentID,
ApplicationID: deviceAuth.ClientID,
InstanceID: authz.GetInstance(ctx).InstanceID(),
Request: &domain.AuthRequestDevice{
ID: deviceAuth.AggregateID,
DeviceCode: deviceAuth.DeviceCode,
UserCode: deviceAuth.UserCode,
Scopes: deviceAuth.Scopes,
},
})
if err != nil {
l.renderDeviceAuthUserCode(w, r, err)
return
}
http.Redirect(w, r, l.renderer.pathPrefix+EndpointLogin+"?authRequestID="+authRequest.ID, http.StatusFound)
}
// redirectDeviceAuthStart redirects the user to the start point of
// the device authorization flow. A prompt can be set to inform the user
// of the reason why they are redirected back.
func (l *Login) redirectDeviceAuthStart(w http.ResponseWriter, r *http.Request, prompt string) {
values := make(url.Values)
values.Set("prompt", url.QueryEscape(prompt))
url := url.URL{
Path: l.renderer.pathPrefix + EndpointDeviceAuth,
RawQuery: values.Encode(),
}
http.Redirect(w, r, url.String(), http.StatusSeeOther)
}
// handleDeviceAuthAction is the handler where the user is redirected after login.
// The authRequest is checked if the login was indeed completed.
// When the action of "allowed" or "denied", the device authorization is updated accordingly.
// Else the user is presented with a page where they can choose / submit either action.
func (l *Login) handleDeviceAuthAction(w http.ResponseWriter, r *http.Request) {
authReq, err := l.getAuthRequest(r)
if authReq == nil {
err = errors.ThrowInvalidArgument(err, "LOGIN-OLah8", "invalid or missing auth request")
l.redirectDeviceAuthStart(w, r, err.Error())
return
}
if !authReq.Done() {
l.redirectDeviceAuthStart(w, r, "authentication not completed")
return
}
authDev, ok := authReq.Request.(*domain.AuthRequestDevice)
if !ok {
l.redirectDeviceAuthStart(w, r, fmt.Sprintf("wrong auth request type: %T", authReq.Request))
return
}
action := mux.Vars(r)["action"]
switch action {
case deviceAuthAllowed:
_, err = l.command.ApproveDeviceAuth(r.Context(), authDev.ID, authReq.UserID)
case deviceAuthDenied:
_, err = l.command.CancelDeviceAuth(r.Context(), authDev.ID, domain.DeviceAuthCanceledDenied)
default:
l.renderDeviceAuthAction(w, r, authReq, authDev.Scopes)
return
}
if err != nil {
l.redirectDeviceAuthStart(w, r, err.Error())
return
}
l.renderDeviceAuthDone(w, r, authReq, action)
}
// deviceAuthCallbackURL creates the callback URL with which the user
// is redirected back to the device authorization flow.
func (l *Login) deviceAuthCallbackURL(authRequestID string) string {
return l.renderer.pathPrefix + EndpointDeviceAuthAction + "?authRequestID=" + authRequestID
}
// RedirectDeviceAuthToPrefix allows users to use https://domain.com/device without the /ui/login prefix
// and redirects them to the prefixed endpoint.
// [rfc 8628](https://www.rfc-editor.org/rfc/rfc8628#section-3.2) recommends the URL to be as short as possible.
func RedirectDeviceAuthToPrefix(w http.ResponseWriter, r *http.Request) {
target := gu.PtrCopy(r.URL)
target.Path = HandlerPrefix + EndpointDeviceAuth
http.Redirect(w, r, target.String(), http.StatusFound)
}

View File

@@ -69,6 +69,8 @@ func (l *Login) authRequestCallback(ctx context.Context, authReq *domain.AuthReq
return l.oidcAuthCallbackURL(ctx, authReq.ID), nil
case *domain.AuthRequestSAML:
return l.samlAuthCallbackURL(ctx, authReq.ID), nil
case *domain.AuthRequestDevice:
return l.deviceAuthCallbackURL(authReq.ID), nil
default:
return "", caos_errs.ThrowInternal(nil, "LOGIN-rhjQF", "Errors.AuthRequest.RequestTypeNotSupported")
}

View File

@@ -25,7 +25,8 @@ import (
)
const (
tmplError = "error"
tmplError = "error"
tmplSuccess = "success"
)
type Renderer struct {
@@ -45,6 +46,7 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
}
tmplMapping := map[string]string{
tmplError: "error.html",
tmplSuccess: "success.html",
tmplLogin: "login.html",
tmplUserSelection: "select_user.html",
tmplPassword: "password.html",
@@ -77,6 +79,8 @@ func CreateRenderer(pathPrefix string, staticDir http.FileSystem, staticStorage
tmplExternalNotFoundOption: "external_not_found_option.html",
tmplLoginSuccess: "login_success.html",
tmplLDAPLogin: "ldap_login.html",
tmplDeviceAuthUserCode: "device_usercode.html",
tmplDeviceAuthAction: "device_action.html",
}
funcs := map[string]interface{}{
"resourceUrl": func(file string) string {
@@ -323,6 +327,7 @@ func (l *Login) chooseNextStep(w http.ResponseWriter, r *http.Request, authReq *
func (l *Login) renderInternalError(w http.ResponseWriter, r *http.Request, authReq *domain.AuthRequest, err error) {
var msg string
if err != nil {
logging.WithError(err).WithField("auth_req_id", authReq.ID).Error()
_, msg = l.getErrorMessage(r, err)
}
data := l.getBaseData(r, authReq, "Errors.Internal", "", "Internal", msg)

View File

@@ -46,6 +46,9 @@ const (
EndpointResources = "/resources"
EndpointDynamicResources = "/resources/dynamic"
EndpointDeviceAuth = "/device"
EndpointDeviceAuthAction = "/device/{action}"
)
var (
@@ -107,5 +110,7 @@ func CreateRouter(login *Login, staticDir http.FileSystem, interceptors ...mux.M
router.HandleFunc(EndpointLDAPLogin, login.handleLDAP).Methods(http.MethodGet)
router.HandleFunc(EndpointLDAPCallback, login.handleLDAPCallback).Methods(http.MethodPost)
router.SkipClean(true).Handle("", http.RedirectHandler(HandlerPrefix+"/", http.StatusMovedPermanently))
router.HandleFunc(EndpointDeviceAuth, login.handleDeviceAuthUserCode).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc(EndpointDeviceAuthAction, login.handleDeviceAuthAction).Methods(http.MethodGet, http.MethodPost)
return router
}

View File

@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Geschlecht
Female: weiblich
Male: männlich
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: Allgemeine Geschäftsbedingungen und Datenschutz
TosConfirm: Ich akzeptiere die
TosLinkText: AGBs
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: Geräteautorisierung
UserCode:
Label: Benutzercode
Description: Geben Sie den auf dem Gerät angezeigten Benutzercode ein
ButtonNext: weiter
Action:
Description: Gerätezugriff erlauben
GrantDevice: Sie sind dabei, das Gerät zu erlauben
AccessToScopes: Zugriff auf die folgenden Daten
Button:
Allow: erlauben
Deny: verweigern
Done:
Description: Abgeschlossen
Approved: Gerätezulassung genehmigt. Sie können jetzt zum Gerät zurückkehren.
Denied: Geräteautorisierung verweigert. Sie können jetzt zum Gerät zurückkehren.
Footer:
PoweredBy: Powered By
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: Registrierung ist nicht erlaubt
DeviceAuth:
NotExisting: Benutzercode existiert nicht
optional: (optional)

View File

@@ -7,7 +7,7 @@ Login:
UsernamePlaceHolder: username
LoginnamePlaceHolder: username@domain
ExternalUserDescription: Login with an external user.
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organization.
RegisterButtonText: register
NextButtonText: next
@@ -26,7 +26,7 @@ SelectAccount:
OtherUser: Other User
SessionState0: active
SessionState1: inactive
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organisation.
MustBeMemberOfOrg: The user must be member of the {{.OrgName}} organization.
Password:
Title: Password
@@ -118,7 +118,7 @@ InitMFADone:
MFAProvider:
Provider0: Authenticator App (e.g Google/Microsoft Authenticator, Authy)
Provider1: Device dependent (e.g FaceID, Windows Hello, Fingerprint)
ChooseOther: or choose an other option
ChooseOther: or choose another option
VerifyMFAOTP:
Title: Verify 2-Factor
@@ -143,7 +143,7 @@ Passwordless:
PasswordlessPrompt:
Title: Passwordless setup
Description: Would you like to setup passwordless login? (Authenticationmethods of your device like FaceID, Windows Hello or Fingerprint)
Description: Would you like to setup passwordless login? (Authentication methods of your device like FaceID, Windows Hello or Fingerprint)
DescriptionInit: You need to set up passwordless login. Use the link you were given to register your device.
PasswordlessButtonText: Go passwordless
NextButtonText: next
@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Gender
Female: Female
Male: Male
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Japanese: 日本語
Polish: Polski
Spanish: Español
TosAndPrivacyLabel: Terms and conditions
TosConfirm: I accept the
TosLinkText: TOS
@@ -263,9 +265,9 @@ ExternalRegistrationUserOverview:
NextButtonText: save
RegistrationOrg:
Title: Organisation Registration
Description: Enter your organisationname and userdata.
OrgNameLabel: Organisationname
Title: Organization Registration
Description: Enter your organization name and userdata.
OrgNameLabel: Organization name
EmailLabel: E-Mail
UsernameLabel: Username
FirstnameLabel: First name
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: Device Authorization
UserCode:
Label: User Code
Description: Enter the user code presented on the device.
ButtonNext: next
Action:
Description: Grant device access.
GrantDevice: you are about to grant device
AccessToScopes: access to the following scopes
Button:
Allow: allow
Deny: deny
Done:
Description: Done.
Approved: Device authorization approved. You can now return to the device.
Denied: Device authorization denied. You can now return to the device.
Footer:
PoweredBy: Powered By
@@ -334,8 +355,8 @@ Errors:
NotFound: User could not be found
AlreadyExists: User already exists
Inactive: User is inactive
NotFoundOnOrg: User could not be found on chosen organisation
NotAllowedOrg: User is no member of the required organisation
NotFoundOnOrg: User could not be found on chosen organization
NotAllowedOrg: User is no member of the required organization
NotMatchingUserID: User and user in authrequest don't match
UserIDMissing: UserID is empty
Invalid: Invalid userdata
@@ -365,11 +386,11 @@ Errors:
NotFound: Address not found
NotChanged: Address not changed
Username:
AlreadyExists: Username already taken
AlreadyExists: Username already taken
Reserved: Username is already taken
Empty: Username is empty
Password:
ConfirmationWrong: Passwordconfirmation is wrong
ConfirmationWrong: Password confirmation is wrong
Empty: Password is empty
Invalid: Password is invalid
InvalidAndLocked: Password is invalid and user is locked, contact your administrator.
@@ -377,7 +398,7 @@ Errors:
Invalid: Username or Password is invalid
PasswordComplexityPolicy:
NotFound: Password policy not found
MinLength: Password is to short
MinLength: Password is too short
HasLower: Password must contain lower letter
HasUpper: Password must contain upper letter
HasNumber: Password must contain number
@@ -413,7 +434,7 @@ Errors:
CreationNotAllowed: Creation of a new user is not allowed on this Provider
LinkingNotAllowed: Linking of a user is not allowed on this Provider
GrantRequired: Login not possible. The user is required to have at least one grant on the application. Please contact your administrator.
ProjectRequired: Login not possible. The organisation of the user must be granted to the project. Please contact your administrator.
ProjectRequired: Login not possible. The organization of the user must be granted to the project. Please contact your administrator.
IdentityProvider:
InvalidConfig: Identity Provider configuration is invalid
IAM:
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: Registration is not allowed
DeviceAuth:
NotExisting: User Code doesn't exist
optional: (optional)

View File

@@ -0,0 +1,429 @@
Login:
Title: ¡Hola de nuevo!
Description: Introduce tus datos de inicio de sesión.
TitleLinking: Inicio de sesión para vincular un usuario
DescriptionLinking: Introduce tus datos de inicio de sesión para vincular tu usuario externo con un usuario ZITADEL.
LoginNameLabel: Nombre de inicio de sesión
UsernamePlaceHolder: username
LoginnamePlaceHolder: username@dominio
ExternalUserDescription: Inicia sesión con un usuario externo.
MustBeMemberOfOrg: El usuario debe ser miembro de la organización {{.OrgName}}.
RegisterButtonText: registrar
NextButtonText: siguiente
LDAP:
Title: Inicio de sesión
Description: Introduce tus datos de inicio de sesión.
LoginNameLabel: Nombre de inicio de sesión
PasswordLabel: Contraseña
NextButtonText: siguiente
SelectAccount:
Title: Seleccionar cuenta
Description: Utiliza tu cuenta ZITADEL
TitleLinking: Selecciona tu cuenta para vincular el usuario
DescriptionLinking: Selecciona tu cuenta para vincular con tu usuario externo.
OtherUser: Otro Usuario
SessionState0: activo
SessionState1: inactivo
MustBeMemberOfOrg: El usuario debe ser miembro de la organización {{.OrgName}}.
Password:
Title: Contraseña
Description: Introduce tus datos de inicio de sesión.
PasswordLabel: Contraseña
MinLength: Longitud mínima
HasUppercase: Una letra mayúscula
HasLowercase: Una letra minúscula
HasNumber: Número
HasSymbol: Símbolo
Confirmation: Las contraseñas coinciden
ResetLinkText: restablecer contraseña
BackButtonText: atrás
NextButtonText: siguiente
UsernameChange:
Title: Cambiar nombre de usuario
Description: Introduce tu nuevo nombre de usuario
UsernameLabel: Nombre de usuario
CancelButtonText: cancelar
NextButtonText: siguiente
UsernameChangeDone:
Title: Nombre de usuario cambiado
Description: Tu nombre de usuario se cambió correctamente.
NextButtonText: siguiente
InitPassword:
Title: Establecer contraseña
Description: Habrás recibido un código, que tendrás que introducir en el siguiente campo, para establecer tu nueva contraseña.
CodeLabel: Código
NewPasswordLabel: Nueva contraseña
NewPasswordConfirmLabel: Confirmar contraseña
ResendButtonText: reenviar código
NextButtonText: siguiente
InitPasswordDone:
Title: Contraseña establecida
Description: La contraseña se estableció correctamente
NextButtonText: siguiente
CancelButtonText: cancelar
InitUser:
Title: Activar usuario
Description: Verifica tu email con el siguiente código y establece tu contraseña.
CodeLabel: Código
NewPasswordLabel: Nueva contraseña
NewPasswordConfirm: Confirmar contraseña
NextButtonText: siguiente
ResendButtonText: reenviar código
InitUserDone:
Title: Usuario activado
Description: Email verificado y contraseña establecida correctamente
NextButtonText: siguiente
CancelButtonText: cancelar
InitMFAPrompt:
Title: Configuración de doble factor
Description: La autenticación de doble factor te proporciona seguridad adicional para tu cuenta de usuario. Ésta asegura que solo tú tienes acceso a tu cuenta.
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
NextButtonText: siguiente
SkipButtonText: saltar
InitMFAOTP:
Title: Verificación de doble factor
Description: Crea tu doble factor de autenticación. Descarga una aplicación autenticadora si todavía no tienes una.
OTPDescription: Escanea el código con tu app autenticadora (p.e Google/Microsoft Authenticator, Authy) o copia el secreto e inserta el código generado más abajo.
SecretLabel: Secreto
CodeLabel: Código
NextButtonText: siguiente
CancelButtonText: cancelar
InitMFAU2F:
Title: Añadir clave de seguridad
Description: Una clave de seguridad es un método de verificación que puede integrarse en tu teléfono móvil, con Bluetooth, o conectándolo directamente en el puerto USB de tu ordenador.
TokenNameLabel: Nombre de la clave de seguridad / dispositivo
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
RegisterTokenButtonText: Añadir clave de seguridad
ErrorRetry: Reintentar, crear un nuevo challenge o elegir un método diferente.
InitMFADone:
Title: Clave de seguridad verificada
Description: ¡Genial! Acabas de configurar satisfactoriamente tu doble factor y has hecho que tu cuenta sea más segura. El doble factor tendrá que introducirse en cada inicio de sesión.
NextButtonText: siguiente
CancelButtonText: cancelar
MFAProvider:
Provider0: App autenticadora (p.e Google/Microsoft Authenticator, Authy)
Provider1: Dependiente de un dispositivo (p.e FaceID, Windows Hello, Huella dactilar)
ChooseOther: o elige otra opción
VerifyMFAOTP:
Title: Verificar doble factor
Description: Verifica tu doble factor
CodeLabel: Código
NextButtonText: siguiente
VerifyMFAU2F:
Title: Verificación de doble factor
Description: Verifica tu doble factor de autenticación con el dispositivo registrado (p.e FaceID, Windows Hello, Huella dactilar)
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
ErrorRetry: Inténtalo nuevamente, crea una nueva petición o elige otro método.
ValidateTokenButtonText: Verificar doble factor
Passwordless:
Title: Inicio de sesión sin contraseña
Description: Iniciar sesión con métodos de autenticación proporcionados por tu dispositivo como FaceID, Windows Hello o tu huella dactilar.
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
ErrorRetry: Inténtalo nuevamente, crea un nuevo reto (challenge) o elige un método diferente.
LoginWithPwButtonText: Inicio de sesión con contraseña
ValidateTokenButtonText: Inicio de sesión sin contraseña
PasswordlessPrompt:
Title: Configuración de acceso sin contraseña
Description: ¿Te gustaría configurar tu inicio de sesión sin contraseña? (métodos de autenticación de tu dispositivo como FaceID, Windows Hello o tu huella dactilar)
DescriptionInit: Necesitas configurar tu inicio de sesión sin contraseña. Utiliza el enlace que se te ha proporcionado para registrar tu dispositivo.
PasswordlessButtonText: Adelante con el inicio sin contraseñas
NextButtonText: siguiente
SkipButtonText: saltar
PasswordlessRegistration:
Title: Configuración de acceso sin contraseña
Description: Añade tu medio de autenticación proporcionando un nombre (p.e MyMobilePhone, MacBook, etc) y después haz clic en el botón 'Registrar acceso sin contraseña'.
TokenNameLabel: Nombre del dispositivo
NotSupported: WebAuthN no está soportado por tu navegador. Por favor asegúrate de que está actualizado o utiliza uno diferente (p.e. Chrome, Safari, Firefox)
RegisterTokenButtonText: Registrar acceso sin contraseña
ErrorRetry: Inténtalo nuevamente, crea un nuevo reto (challenge) o elige un método diferente.
PasswordlessRegistrationDone:
Title: Configuración de acceso sin contraseña
Description: Se añadió con éxito el dispositivo para iniciar sesión sin contraseña.
DescriptionClose: Ya puedes cerrar esta ventana.
NextButtonText: siguiente
CancelButtonText: cancelar
PasswordChange:
Title: Cambiar contraseña
Description: Cambia tu contraseña. Introduce tu contraseña anterior y la nueva.
OldPasswordLabel: Contraseña anterior
NewPasswordLabel: Nueva contraseña
NewPasswordConfirmLabel: Confirmación de contraseña
CancelButtonText: cancelar
NextButtonText: siguiente
Footer: Pie
PasswordChangeDone:
Title: Cambiar contraseña
Description: Tu contraseña se cambió correctamente.
NextButtonText: siguiente
PasswordResetDone:
Title: Se ha enviado un enlace para restablecer la contraseña
Description: Comprueba tu email para restablecer la contraseña.
NextButtonText: siguiente
EmailVerification:
Title: Verificación de email
Description: Te hemos enviado un email para verificar tu dirección. Por favor introduce el código en el siguiente campo.
CodeLabel: Código
NextButtonText: siguiente
ResendButtonText: reenviar código
EmailVerificationDone:
Title: Verificación de email
Description: Tu dirección de email se ha verificado correctamente.
NextButtonText: siguiente
CancelButtonText: cancelar
LoginButtonText: iniciar sesión
RegisterOption:
Title: Opciones de registro
Description: Elige cómo te gustaría registrarte
RegisterUsernamePasswordButtonText: Con nombre de usuario y contraseña
ExternalLoginDescription: o regístrate con un usuario externo
LoginButtonText: iniciar sesión
RegistrationUser:
Title: Registro
Description: Introduce tus datos de usuario. Tu email se utilizará como nombre de inicio de sesión.
DescriptionOrgRegister: Introduce tus datos de usuario.
EmailLabel: Email
UsernameLabel: Nombre de usuario
FirstnameLabel: Nombre
LastnameLabel: Apellidos
LanguageLabel: Idioma
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Género
Female: Mujer
Male: Hombre
Diverse: Diverso / X
PasswordLabel: Contraseña
PasswordConfirmLabel: Confirmación de contraseña
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
PrivacyConfirm: Acepto la
PrivacyLinkText: política de privacidad
ExternalLogin: o regístrate con un usuario externo
BackButtonText: inicio de sesión
NextButtonText: siguiente
ExternalRegistrationUserOverview:
Title: Registro de usuarios externos
Description: Hemos tomado los detalles de tu usuario del proveedor seleccionado. Ahora puedes cambiarlos o completarlos.
EmailLabel: Email
UsernameLabel: Nombre de usuario
FirstnameLabel: Nombre
LastnameLabel: Apellidos
NicknameLabel: Apodo
PhoneLabel: Número de teléfono
LanguageLabel: Idioma
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Japanese: 日本語
Polish: Polski
Spanish: Español
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
PrivacyConfirm: Acepto la
PrivacyLinkText: política de privacidad
ExternalLogin: o regístrate con un usuario externo
BackButtonText: atrás
NextButtonText: guardar
RegistrationOrg:
Title: Registro de organización
Description: Introduce el nombre de tu organización y tus datos de usuario.
OrgNameLabel: Nombre de organización
EmailLabel: Email
UsernameLabel: Nombre de usuario
FirstnameLabel: Nombre
LastnameLabel: Apellidos
PasswordLabel: Contraseña
PasswordConfirmLabel: Confirmación de contraseña
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
PrivacyConfirm: Acepto la
PrivacyLinkText: política de privacidad
SaveButtonText: Crear organización
LoginSuccess:
Title: Se inició sesión con éxito
AutoRedirectDescription: Se te redirigirá a tu aplicación automáticamente. Si no fuera así, haz clic en el botón siguiente. Puedes cerrar esta ventana posteriormente.
RedirectedDescription: Ya puedes cerrar esta ventana.
NextButtonText: siguiente
LogoutDone:
Title: Cerraste sesión
Description: Cerraste la sesión con éxito.
LoginButtonText: iniciar sesión
LinkingUsersDone:
Title: Vinculación de usuario
Description: usuario vinculado con éxito.
CancelButtonText: cancelar
NextButtonText: siguiente
ExternalNotFound:
Title: Usuario externo no encontrado
Description: Usuario externo no encontrado. ¿Quieres vincular tu usuario o autoregistrar uno nuevo?
LinkButtonText: Vincular
AutoRegisterButtonText: registrar
TosAndPrivacyLabel: Términos y condiciones
TosConfirm: Acepto los
TosLinkText: TDS
PrivacyConfirm: Acepto la
PrivacyLinkText: política de privacidad
German: Deutsch
English: English
Italian: Italiano
French: Français
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
Footer:
PoweredBy: Powered By
Tos: TDS
PrivacyPolicy: Política de privacidad
Help: Ayuda
SupportEmail: Email de soporte
Errors:
Internal: Se produjo un error interno
AuthRequest:
NotFound: No pude encontrar la petición de autenticación (authrequest)
UserAgentNotCorresponding: El User Agent no se corresponde
UserAgentNotFound: No se encontró el ID del User Agent
TokenNotFound: No se encontró el Token
RequestTypeNotSupported: El tipo de petición no está soportado
MissingParameters: Faltan parámetros requeridos
User:
NotFound: El usuario no pudo ser encontrado
AlreadyExists: El usuario ya existe
Inactive: El usuario está inactivo
NotFoundOnOrg: El usuario no pudo encontrarse en la organización elegida
NotAllowedOrg: El usuario no es miembro de la organización requerida
NotMatchingUserID: El usuario y el usuario contenido en la petición de autenticación (authrequest) no coinciden
UserIDMissing: El ID de usuario está vacío
Invalid: Datos de usuario no válidos
DomainNotAllowedAsUsername: El dominio ya está reservado y no puede usarse
NotAllowedToLink: El usuario no está autorizado para vincularse con un proveedor de inicio de sesión externo
Profile:
NotFound: Perfil no encontrado
NotChanged: El perfil no ha cambiado
Empty: El perfil está vacío
FirstNameEmpty: El nombre del perfil está vacío
LastNameEmpty: Los apellidos del perfil están vacíos
IDMissing: Falta el ID del perfil
Email:
NotFound: Email no encontrado
Invalid: El email no es válido
AlreadyVerified: El email ya ha sido verificado
NotChanged: El email no ha cambiado
Empty: El email está vacío
IDMissing: Falta el ID del email
Phone:
NotFound: Teléfono no encontrado
Invalid: El teléfono no es válido
AlreadyVerified: El teléfono ya ha sido verificado
Empty: El teléfono está vacío
NotChanged: El teléfono no ha cambiado
Address:
NotFound: Dirección no encontrada
NotChanged: La dirección no cambió
Username:
AlreadyExists: El nombre de usuario ya está cogido
Reserved: El nombre de usuario ya está cogido
Empty: El nombre de usuario está vacío
Password:
ConfirmationWrong: La confirmación de la contraseña es incorrecta
Empty: La contraseña está vacía
Invalid: La contraseña no es válida
InvalidAndLocked: La contraseña no es válida y el usuario está bloqueado, contacta con tu administrador.
UsernameOrPassword:
Invalid: El nombre de usuario o la contraseña no son válidos
PasswordComplexityPolicy:
NotFound: No se encontró una política de contraseñas
MinLength: La contraseña es demasiada corta
HasLower: La contraseña debe contener una letra minúscula
HasUpper: La contraseña debe contener una letra mayúscula
HasNumber: La contraseña debe contener un número
HasSymbol: La contraseña debe contener un símbolo
Code:
Expired: El código ha caducado
Invalid: El código no es válido
Empty: El código está vacío
CryptoCodeNil: El código criptográfico es nulo
NotFound: No pude encontrar el código
GeneratorAlgNotSupported: Algoritmo de generación no soportado
EmailVerify:
UserIDEmpty: El ID de usuario está vacío
ExternalData:
CouldNotRead: Los datos externos no pudieron leerse correctamente
MFA:
NoProviders: No hay proveedores multifactor disponibles
OTP:
AlreadyReady: El multifactor OTP (OneTimePassword) ya está configurado
NotExisting: El multifactor OTP (OneTimePassword) no existe
InvalidCode: Código no válido
NotReady: El multifactor OTP (OneTimePassword) no está listo
Locked: El usuario está bloqueado
SomethingWentWrong: Algo fue mal
NotActive: El usuario no está activo
ExternalIDP:
IDPTypeNotImplemented: El tipo de IDP no está implementado
NotAllowed: El proveedor de inicio de sesión externo no está permitido
IDPConfigIDEmpty: El ID del proveedor de identidad está vacío
ExternalUserIDEmpty: El ID de usuario externo está vacío
UserDisplayNameEmpty: El nombre mostrado del usuario está vacío
NoExternalUserData: No se recibieron datos del usuario externo
CreationNotAllowed: La creación de un nuevo usuario no está permitida para este proveedor
LinkingNotAllowed: La vinculación de un usuario no está permitida para este proveedor
GrantRequired: El inicio de sesión no es posible. Se requiere que el usuario tenga al menos una concesión sobre la aplicación. Por favor contacta con tu administrador.
ProjectRequired: El inicio de sesión no es posible. La organización del usuario debe tener el acceso concedido para el proyecto. Por favor contacta con tu administrador.
IdentityProvider:
InvalidConfig: La configuración del proveedor de identidades no es válida
IAM:
LockoutPolicy:
NotExisting: No existe política de bloqueo
Org:
LoginPolicy:
RegistrationNotAllowed: El registro no está permitido
optional: (opcional)

View File

@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Genre
Female: Femme
Male: Homme
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: Termes et conditions
TosConfirm: J'accepte les
TosLinkText: TOS
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: Autorisation de l'appareil
UserCode:
Label: Code d'utilisateur
Description: Saisissez le code utilisateur présenté sur l'appareil.
ButtonNext: suivant
Action:
Description: Accordez l'accès à l'appareil.
GrantDevice: vous êtes sur le point d'accorder un appareil
AccessToScopes: accès aux périmètres suivants
Button:
Allow: permettre
Deny: refuser
Done:
Description: Fait.
Approved: Autorisation de l'appareil approuvée. Vous pouvez maintenant retourner à l'appareil.
Denied: Autorisation de l'appareil refusée. Vous pouvez maintenant retourner à l'appareil.
Footer:
PoweredBy: Promulgué par
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: L'enregistrement n'est pas autorisé
DeviceAuth:
NotExisting: Le code utilisateur n'existe pas
optional: (facultatif)

View File

@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Genere
Female: Femminile
Male: Maschile
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: Termini di servizio
TosConfirm: Accetto i
TosLinkText: Termini di servizio
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: Autorizzazione del dispositivo
UserCode:
Label: Codice utente
Description: Inserire il codice utente presentato sul dispositivo.
ButtonNext: prossimo
Action:
Description: Concedi l'accesso al dispositivo.
GrantDevice: stai per concedere il dispositivo
AccessToScopes: accesso ai seguenti ambiti
Button:
Allow: permettere
Deny: negare
Done:
Description: Fatto.
Approved: Autorizzazione del dispositivo approvata. Ora puoi tornare al dispositivo.
Denied: Autorizzazione dispositivo negata. Ora puoi tornare al dispositivo.
Footer:
PoweredBy: Alimentato da
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: la registrazione non è consentita.
DeviceAuth:
NotExisting: Il codice utente non esiste
optional: (opzionale)

View File

@@ -213,6 +213,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: 性別
Female: 女性
Male: 男性
@@ -245,6 +246,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: 利用規約
TosConfirm: 私は利用規約を承諾します。
TosLinkText: TOS
@@ -305,6 +307,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: デバイス認証
UserCode:
Label: ユーザーコード
Description: デバイスに表示されたユーザー コードを入力します。
ButtonNext:
Action:
Description: デバイスへのアクセスを許可します。
GrantDevice: デバイスを許可しようとしています
AccessToScopes: 次のスコープへのアクセス
Button:
Allow: 許可する
Deny: 拒否
Done:
Description: 終わり。
Approved: デバイス認証が承認されました。 これで、デバイスに戻ることができます。
Denied: デバイス認証が拒否されました。 これで、デバイスに戻ることができます。
Footer:
PoweredBy: Powered By
@@ -382,5 +403,7 @@ Errors:
IAM:
LockoutPolicy:
NotExisting: ロックアウトポリシーが存在しません
DeviceAuth:
NotExisting: ユーザーコードが存在しません
optional: "(オプション)"

View File

@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: Płeć
Female: Kobieta
Male: Mężczyzna
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: Warunki i zasady
TosConfirm: Akceptuję
TosLinkText: Warunki korzystania
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: Autoryzacja urządzenia
UserCode:
Label: Kod użytkownika
Description: Wprowadź kod użytkownika prezentowany na urządzeniu.
ButtonNext: Następny
Action:
Description: Przyznaj dostęp do urządzenia.
GrantDevice: zamierzasz przyznać urządzenie
AccessToScopes: dostęp do następujących zakresów
Button:
Allow: umożliwić
Deny: zaprzeczyć
Done:
Description: Zrobione.
Approved: Zatwierdzono autoryzację urządzenia. Możesz teraz wrócić do urządzenia.
Denied: Odmowa autoryzacji urządzenia. Możesz teraz wrócić do urządzenia.
Footer:
PoweredBy: Obsługiwane przez
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: Rejestracja nie jest dozwolona
DeviceAuth:
NotExisting: Kod użytkownika nie istnieje
optional: (opcjonalny)

View File

@@ -221,6 +221,7 @@ RegistrationUser:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
GenderLabel: 性别
Female: 女性
Male: 男性
@@ -253,6 +254,7 @@ ExternalRegistrationUserOverview:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
TosAndPrivacyLabel: 条款和条款
TosConfirm: 我接受
TosLinkText: 服务条款
@@ -313,6 +315,25 @@ ExternalNotFound:
Chinese: 简体中文
Polish: Polski
Japanese: 日本語
Spanish: Español
DeviceAuth:
Title: 设备授权
UserCode:
Label: 用户代码
Description: 输入设备上显示的用户代码。
ButtonNext: 下一个
Action:
Description: 授予设备访问权限。
GrantDevice: 您即将授予设备
AccessToScopes: 访问以下范围
Button:
Allow: 允许
Deny: 否定
Done:
Description: 完毕。
Approved: 设备授权已批准。 您现在可以返回设备。
Denied: 设备授权被拒绝。 您现在可以返回设备。
Footer:
PoweredBy: Powered By
@@ -422,5 +443,7 @@ Errors:
Org:
LoginPolicy:
RegistrationNotAllowed: 不允许注册
DeviceAuth:
NotExisting: 用户代码不存在
optional: (可选)

View File

@@ -0,0 +1,18 @@
{{template "main-top" .}}
<h1>{{.Title}}</h1>
<p>
{{.Username}}, {{t "DeviceAuth.Action.GrantDevice"}} {{.ClientID}} {{t "DeviceAuth.Action.AccessToScopes"}}: {{.Scopes}}.
</p>
<form method="POST">
{{ .CSRF }}
<input type="hidden" name="authRequestID" value="{{.AuthRequestID}}">
<button class="lgn-raised-button lgn-primary left" type="submit" formaction="./allowed">
{{t "DeviceAuth.Action.Button.Allow"}}
</button>
<button class="lgn-raised-button lgn-warn right" type="submit" formaction="./denied">
{{t "DeviceAuth.Action.Button.Deny"}}
</button>
</form>
{{template "main-bottom" .}}

View File

@@ -0,0 +1,21 @@
{{template "main-top" .}}
<h1>{{.Title}}</h1>
<form method="POST">
{{ .CSRF }}
<div class="fields">
<label class="lgn-label" for="user_code">{{t "DeviceAuth.UserCode.Label"}}</label>
<input class="lgn-input" id="user_code" name="user_code" autofocus required{{if .ErrMessage}} shake{{end}}>
</div>
{{template "error-message" .}}
<div class="lgn-actions">
<span class="fill-space"></span>
<button id="submit-button" class="lgn-raised-button lgn-primary right" type="submit">{{t "DeviceAuth.UserCode.ButtonNext"}}</button>
</div>
</form>
{{template "main-bottom" .}}

View File

@@ -64,6 +64,8 @@
</option>
<option value="en" id="en" {{if (selectedLanguage "en")}} selected {{end}}>{{t "ExternalNotFound.English"}}
</option>
<option value="es" id="es" {{if (selectedLanguage "es")}} selected {{end}}>{{t "ExternalNotFound.Spanish"}}
</option>
<option value="fr" id="fr" {{if (selectedLanguage "fr")}} selected {{end}}>{{t "ExternalNotFound.French"}}
</option>
<option value="it" id="it" {{if (selectedLanguage "it")}} selected {{end}}>{{t "ExternalNotFound.Italian"}}

View File

@@ -0,0 +1,12 @@
{{template "main-top" .}}
<div class="lgn-head">
<div class="lgn-actions">
<i class="lgn-icon-check-solid lgn-primary"></i>
<p class="lgn-error-message">
{{ .Message }}
</p>
</div>
</div>
{{template "main-bottom" .}}

View File

@@ -1,3 +1,3 @@
package statik
//go:generate statik -src=../static -dest=.. -ns=login
//go:generate statik -f -src=../static -dest=.. -ns=login