mirror of
https://github.com/zitadel/zitadel.git
synced 2025-08-12 04:07:31 +00:00
chore: move the go code into a subfolder
This commit is contained in:
96
apps/api/internal/domain/action.go
Normal file
96
apps/api/internal/domain/action.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type Action struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Name string
|
||||
Script string
|
||||
Timeout time.Duration
|
||||
AllowedToFail bool
|
||||
State ActionState
|
||||
}
|
||||
|
||||
func (a *Action) IsValid() bool {
|
||||
return a.Name != ""
|
||||
}
|
||||
|
||||
type ActionState int32
|
||||
|
||||
const (
|
||||
ActionStateUnspecified ActionState = iota
|
||||
ActionStateActive
|
||||
ActionStateInactive
|
||||
ActionStateRemoved
|
||||
actionStateCount
|
||||
)
|
||||
|
||||
func (s ActionState) Valid() bool {
|
||||
return s >= 0 && s < actionStateCount
|
||||
}
|
||||
|
||||
func (s ActionState) Exists() bool {
|
||||
return s != ActionStateUnspecified && s != ActionStateRemoved
|
||||
}
|
||||
|
||||
type ActionsAllowed int32
|
||||
|
||||
const (
|
||||
ActionsNotAllowed ActionsAllowed = iota
|
||||
ActionsMaxAllowed
|
||||
ActionsAllowedUnlimited
|
||||
)
|
||||
|
||||
type ActionFunction int32
|
||||
|
||||
const (
|
||||
ActionFunctionUnspecified ActionFunction = iota
|
||||
ActionFunctionPreUserinfo
|
||||
ActionFunctionPreAccessToken
|
||||
ActionFunctionPreSAMLResponse
|
||||
actionFunctionCount
|
||||
)
|
||||
|
||||
func (s ActionFunction) Valid() bool {
|
||||
return s >= 0 && s < actionFunctionCount
|
||||
}
|
||||
|
||||
func (s ActionFunction) LocalizationKey() string {
|
||||
if !s.Valid() {
|
||||
return ActionFunctionUnspecified.LocalizationKey()
|
||||
}
|
||||
|
||||
switch s {
|
||||
case ActionFunctionPreUserinfo:
|
||||
return "preuserinfo"
|
||||
case ActionFunctionPreAccessToken:
|
||||
return "preaccesstoken"
|
||||
case ActionFunctionPreSAMLResponse:
|
||||
return "presamlresponse"
|
||||
case ActionFunctionUnspecified, actionFunctionCount:
|
||||
fallthrough
|
||||
default:
|
||||
return "unspecified"
|
||||
}
|
||||
}
|
||||
|
||||
func AllActionFunctions() []string {
|
||||
return []string{
|
||||
ActionFunctionPreUserinfo.LocalizationKey(),
|
||||
ActionFunctionPreAccessToken.LocalizationKey(),
|
||||
ActionFunctionPreSAMLResponse.LocalizationKey(),
|
||||
}
|
||||
}
|
||||
|
||||
func ActionFunctionExists() func(string) bool {
|
||||
functions := AllActionFunctions()
|
||||
return func(s string) bool {
|
||||
return slices.Contains(functions, s)
|
||||
}
|
||||
}
|
49
apps/api/internal/domain/application.go
Normal file
49
apps/api/internal/domain/application.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
type Application interface {
|
||||
GetAppID() string
|
||||
GetApplicationName() string
|
||||
GetState() AppState
|
||||
//GetSequence() uint64
|
||||
//GetChangeDate() time.Time
|
||||
//GetResourceOwner() string
|
||||
}
|
||||
|
||||
type AppState int32
|
||||
|
||||
const (
|
||||
AppStateUnspecified AppState = iota
|
||||
AppStateActive
|
||||
AppStateInactive
|
||||
AppStateRemoved
|
||||
)
|
||||
|
||||
func (a AppState) Exists() bool {
|
||||
return !(a == AppStateUnspecified || a == AppStateRemoved)
|
||||
}
|
||||
|
||||
type ChangeApp struct {
|
||||
AppID string
|
||||
AppName string
|
||||
State AppState
|
||||
}
|
||||
|
||||
func (a *ChangeApp) GetAppID() string {
|
||||
return a.AppID
|
||||
}
|
||||
|
||||
func (a *ChangeApp) GetApplicationName() string {
|
||||
return a.AppName
|
||||
}
|
||||
|
||||
func (a *ChangeApp) GetState() AppState {
|
||||
return a.State
|
||||
}
|
||||
|
||||
type LoginVersion int32
|
||||
|
||||
const (
|
||||
LoginVersionUnspecified LoginVersion = iota
|
||||
LoginVersion1
|
||||
LoginVersion2
|
||||
)
|
61
apps/api/internal/domain/application_api.go
Normal file
61
apps/api/internal/domain/application_api.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type APIApp struct {
|
||||
models.ObjectRoot
|
||||
|
||||
AppID string
|
||||
AppName string
|
||||
ClientID string
|
||||
EncodedHash string
|
||||
ClientSecretString string
|
||||
AuthMethodType APIAuthMethodType
|
||||
|
||||
State AppState
|
||||
}
|
||||
|
||||
func (a *APIApp) GetApplicationName() string {
|
||||
return a.AppName
|
||||
}
|
||||
|
||||
func (a *APIApp) GetState() AppState {
|
||||
return a.State
|
||||
}
|
||||
|
||||
type APIAuthMethodType int32
|
||||
|
||||
const (
|
||||
APIAuthMethodTypeBasic APIAuthMethodType = iota
|
||||
APIAuthMethodTypePrivateKeyJWT
|
||||
)
|
||||
|
||||
func (a *APIApp) IsValid() bool {
|
||||
return a.AppName != ""
|
||||
}
|
||||
|
||||
func (a *APIApp) setClientID(clientID string) {
|
||||
a.ClientID = clientID
|
||||
}
|
||||
|
||||
func (a *APIApp) setClientSecret(encodedHash string) {
|
||||
a.EncodedHash = encodedHash
|
||||
}
|
||||
|
||||
func (a *APIApp) requiresClientSecret() bool {
|
||||
return a.AuthMethodType == APIAuthMethodTypeBasic
|
||||
}
|
||||
|
||||
func (a *APIApp) GenerateClientSecretIfNeeded(generator *crypto.HashGenerator) (plain string, err error) {
|
||||
if a.AuthMethodType == APIAuthMethodTypePrivateKeyJWT {
|
||||
return "", nil
|
||||
}
|
||||
a.EncodedHash, plain, err = generator.NewCode()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return plain, nil
|
||||
}
|
60
apps/api/internal/domain/application_key.go
Normal file
60
apps/api/internal/domain/application_key.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type ApplicationKey struct {
|
||||
models.ObjectRoot
|
||||
|
||||
ApplicationID string
|
||||
ClientID string
|
||||
KeyID string
|
||||
Type AuthNKeyType
|
||||
ExpirationDate time.Time
|
||||
PrivateKey []byte
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) SetPublicKey(publicKey []byte) {
|
||||
k.PublicKey = publicKey
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) SetPrivateKey(privateKey []byte) {
|
||||
k.PrivateKey = privateKey
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) GetExpirationDate() time.Time {
|
||||
return k.ExpirationDate
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) SetExpirationDate(expiration time.Time) {
|
||||
k.ExpirationDate = expiration
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) Detail() ([]byte, error) {
|
||||
if k.Type == AuthNKeyTypeJSON {
|
||||
return k.MarshalJSON()
|
||||
}
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "KEY-dsg52", "Errors.Internal")
|
||||
}
|
||||
|
||||
func (k *ApplicationKey) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
KeyID string `json:"keyId"`
|
||||
Key string `json:"key"`
|
||||
AppID string `json:"appId"`
|
||||
ClientID string `json:"clientId"`
|
||||
}{
|
||||
Type: "application",
|
||||
KeyID: k.KeyID,
|
||||
Key: string(k.PrivateKey),
|
||||
AppID: k.ApplicationID,
|
||||
ClientID: k.ClientID,
|
||||
})
|
||||
}
|
34
apps/api/internal/domain/application_oauth.go
Normal file
34
apps/api/internal/domain/application_oauth.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/id"
|
||||
)
|
||||
|
||||
type oAuthApplication interface {
|
||||
setClientID(clientID string)
|
||||
setClientSecret(encodedHash string)
|
||||
requiresClientSecret() bool
|
||||
}
|
||||
|
||||
// ClientID random_number (eg. 495894098234)
|
||||
func SetNewClientID(a oAuthApplication, idGenerator id.Generator) error {
|
||||
clientID, err := idGenerator.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.setClientID(clientID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetNewClientSecretIfNeeded(a oAuthApplication, generate func() (encodedHash, plain string, err error)) (string, error) {
|
||||
if !a.requiresClientSecret() {
|
||||
return "", nil
|
||||
}
|
||||
encodedHash, plain, err := generate()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
a.setClientSecret(encodedHash)
|
||||
return plain, nil
|
||||
}
|
424
apps/api/internal/domain/application_oidc.go
Normal file
424
apps/api/internal/domain/application_oidc.go
Normal file
@@ -0,0 +1,424 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
const (
|
||||
http = "http://"
|
||||
httpLocalhostWithPort = "http://localhost:"
|
||||
httpLocalhostWithoutPort = "http://localhost/"
|
||||
httpLoopbackV4WithPort = "http://127.0.0.1:"
|
||||
httpLoopbackV4WithoutPort = "http://127.0.0.1/"
|
||||
httpLoopbackV6WithPort = "http://[::1]:"
|
||||
httpLoopbackV6WithoutPort = "http://[::1]/"
|
||||
httpLoopbackV6LongWithPort = "http://[0:0:0:0:0:0:0:1]:"
|
||||
httpLoopbackV6LongWithoutPort = "http://[0:0:0:0:0:0:0:1]/"
|
||||
https = "https://"
|
||||
)
|
||||
|
||||
type OIDCApp struct {
|
||||
models.ObjectRoot
|
||||
|
||||
AppID string
|
||||
AppName string
|
||||
ClientID string
|
||||
EncodedHash string
|
||||
ClientSecretString string
|
||||
RedirectUris []string
|
||||
ResponseTypes []OIDCResponseType
|
||||
GrantTypes []OIDCGrantType
|
||||
ApplicationType *OIDCApplicationType
|
||||
AuthMethodType *OIDCAuthMethodType
|
||||
PostLogoutRedirectUris []string
|
||||
OIDCVersion *OIDCVersion
|
||||
Compliance *Compliance
|
||||
DevMode *bool
|
||||
AccessTokenType *OIDCTokenType
|
||||
AccessTokenRoleAssertion *bool
|
||||
IDTokenRoleAssertion *bool
|
||||
IDTokenUserinfoAssertion *bool
|
||||
ClockSkew *time.Duration
|
||||
AdditionalOrigins []string
|
||||
SkipNativeAppSuccessPage *bool
|
||||
BackChannelLogoutURI *string
|
||||
LoginVersion *LoginVersion
|
||||
LoginBaseURI *string
|
||||
|
||||
State AppState
|
||||
}
|
||||
|
||||
func (a *OIDCApp) GetApplicationName() string {
|
||||
return a.AppName
|
||||
}
|
||||
|
||||
func (a *OIDCApp) GetState() AppState {
|
||||
return a.State
|
||||
}
|
||||
|
||||
func (a *OIDCApp) setClientID(clientID string) {
|
||||
a.ClientID = clientID
|
||||
}
|
||||
|
||||
func (a *OIDCApp) setClientSecret(encodedHash string) {
|
||||
a.EncodedHash = encodedHash
|
||||
}
|
||||
|
||||
func (a *OIDCApp) requiresClientSecret() bool {
|
||||
return a.AuthMethodType != nil && (*a.AuthMethodType == OIDCAuthMethodTypeBasic || *a.AuthMethodType == OIDCAuthMethodTypePost)
|
||||
}
|
||||
|
||||
type OIDCVersion int32
|
||||
|
||||
const (
|
||||
OIDCVersionV1 OIDCVersion = iota
|
||||
)
|
||||
|
||||
type OIDCResponseType int32
|
||||
|
||||
const (
|
||||
OIDCResponseTypeUnspecified OIDCResponseType = iota - 1 // Negative offset not to break existing configs.
|
||||
OIDCResponseTypeCode
|
||||
OIDCResponseTypeIDToken
|
||||
OIDCResponseTypeIDTokenToken
|
||||
)
|
||||
|
||||
//go:generate enumer -type OIDCResponseMode -transform snake -trimprefix OIDCResponseMode
|
||||
type OIDCResponseMode int
|
||||
|
||||
const (
|
||||
OIDCResponseModeUnspecified OIDCResponseMode = iota
|
||||
OIDCResponseModeQuery
|
||||
OIDCResponseModeFragment
|
||||
OIDCResponseModeFormPost
|
||||
)
|
||||
|
||||
type OIDCGrantType int32
|
||||
|
||||
const (
|
||||
OIDCGrantTypeAuthorizationCode OIDCGrantType = iota
|
||||
OIDCGrantTypeImplicit
|
||||
OIDCGrantTypeRefreshToken
|
||||
OIDCGrantTypeDeviceCode
|
||||
OIDCGrantTypeTokenExchange
|
||||
)
|
||||
|
||||
type OIDCApplicationType int32
|
||||
|
||||
const (
|
||||
OIDCApplicationTypeWeb OIDCApplicationType = iota
|
||||
OIDCApplicationTypeUserAgent
|
||||
OIDCApplicationTypeNative
|
||||
)
|
||||
|
||||
type OIDCAuthMethodType int32
|
||||
|
||||
const (
|
||||
OIDCAuthMethodTypeBasic OIDCAuthMethodType = iota
|
||||
OIDCAuthMethodTypePost
|
||||
OIDCAuthMethodTypeNone
|
||||
OIDCAuthMethodTypePrivateKeyJWT
|
||||
)
|
||||
|
||||
type Compliance struct {
|
||||
NoneCompliant bool
|
||||
Problems []string
|
||||
}
|
||||
|
||||
type OIDCTokenType int32
|
||||
|
||||
const (
|
||||
OIDCTokenTypeBearer OIDCTokenType = iota
|
||||
OIDCTokenTypeJWT
|
||||
)
|
||||
|
||||
func (a *OIDCApp) IsValid() bool {
|
||||
if (a.ClockSkew != nil && (*a.ClockSkew > time.Second*5 || *a.ClockSkew < time.Second*0)) || !a.OriginsValid() {
|
||||
return false
|
||||
}
|
||||
grantTypes := a.getRequiredGrantTypes()
|
||||
if len(grantTypes) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, grantType := range grantTypes {
|
||||
ok := containsOIDCGrantType(a.GrantTypes, grantType)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *OIDCApp) OriginsValid() bool {
|
||||
for _, origin := range a.AdditionalOrigins {
|
||||
if !http_util.IsOrigin(strings.TrimSpace(origin)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func ContainsRequiredGrantTypes(responseTypes []OIDCResponseType, grantTypes []OIDCGrantType) bool {
|
||||
required := RequiredOIDCGrantTypes(responseTypes, grantTypes)
|
||||
return ContainsOIDCGrantTypes(required, grantTypes)
|
||||
}
|
||||
|
||||
func RequiredOIDCGrantTypes(responseTypes []OIDCResponseType, grantTypesSet []OIDCGrantType) (grantTypes []OIDCGrantType) {
|
||||
var implicit bool
|
||||
|
||||
for _, r := range responseTypes {
|
||||
switch r {
|
||||
case OIDCResponseTypeCode:
|
||||
// #5684 when "Device Code" is selected, "Authorization Code" is no longer a hard requirement
|
||||
if !containsOIDCGrantType(grantTypesSet, OIDCGrantTypeDeviceCode) {
|
||||
grantTypes = append(grantTypes, OIDCGrantTypeAuthorizationCode)
|
||||
} else {
|
||||
grantTypes = append(grantTypes, OIDCGrantTypeDeviceCode)
|
||||
}
|
||||
case OIDCResponseTypeIDToken, OIDCResponseTypeIDTokenToken:
|
||||
if !implicit {
|
||||
implicit = true
|
||||
grantTypes = append(grantTypes, OIDCGrantTypeImplicit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return grantTypes
|
||||
}
|
||||
|
||||
func (a *OIDCApp) getRequiredGrantTypes() []OIDCGrantType {
|
||||
return RequiredOIDCGrantTypes(a.ResponseTypes, a.GrantTypes)
|
||||
}
|
||||
|
||||
func ContainsOIDCGrantTypes(shouldContain, list []OIDCGrantType) bool {
|
||||
for _, should := range shouldContain {
|
||||
if !containsOIDCGrantType(list, should) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func containsOIDCGrantType(grantTypes []OIDCGrantType, grantType OIDCGrantType) bool {
|
||||
return slices.Contains(grantTypes, grantType)
|
||||
}
|
||||
|
||||
func (a *OIDCApp) FillCompliance() {
|
||||
a.Compliance = GetOIDCCompliance(a.OIDCVersion, a.ApplicationType, a.GrantTypes, a.ResponseTypes, a.AuthMethodType, a.RedirectUris)
|
||||
}
|
||||
|
||||
func GetOIDCCompliance(version *OIDCVersion, appType *OIDCApplicationType, grantTypes []OIDCGrantType, responseTypes []OIDCResponseType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance {
|
||||
if version != nil && *version == OIDCVersionV1 {
|
||||
return GetOIDCV1Compliance(appType, grantTypes, authMethod, redirectUris)
|
||||
}
|
||||
|
||||
return &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.UnsupportedVersion"},
|
||||
}
|
||||
}
|
||||
|
||||
func GetOIDCV1Compliance(appType *OIDCApplicationType, grantTypes []OIDCGrantType, authMethod *OIDCAuthMethodType, redirectUris []string) *Compliance {
|
||||
compliance := &Compliance{NoneCompliant: false}
|
||||
|
||||
checkGrantTypesCombination(compliance, grantTypes)
|
||||
checkRedirectURIs(compliance, grantTypes, appType, redirectUris)
|
||||
checkApplicationType(compliance, appType, authMethod)
|
||||
|
||||
if compliance.NoneCompliant {
|
||||
compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...)
|
||||
}
|
||||
return compliance
|
||||
}
|
||||
|
||||
func checkGrantTypesCombination(compliance *Compliance, grantTypes []OIDCGrantType) {
|
||||
if !containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) && containsOIDCGrantType(grantTypes, OIDCGrantTypeRefreshToken) && !containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.GrantType.Refresh.NoAuthCode")
|
||||
}
|
||||
}
|
||||
|
||||
func checkRedirectURIs(compliance *Compliance, grantTypes []OIDCGrantType, appType *OIDCApplicationType, redirectUris []string) {
|
||||
// See #5684 for OIDCGrantTypeDeviceCode and redirectUris further explanation
|
||||
if len(redirectUris) == 0 && (!containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) || (containsOIDCGrantType(grantTypes, OIDCGrantTypeDeviceCode) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode))) {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append([]string{"Application.OIDC.V1.NoRedirectUris"}, compliance.Problems...)
|
||||
}
|
||||
|
||||
if containsOIDCGrantType(grantTypes, OIDCGrantTypeImplicit) && containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) {
|
||||
CheckRedirectUrisImplicitAndCode(compliance, appType, redirectUris)
|
||||
} else {
|
||||
if containsOIDCGrantType(grantTypes, OIDCGrantTypeImplicit) {
|
||||
CheckRedirectUrisImplicit(compliance, appType, redirectUris)
|
||||
}
|
||||
if containsOIDCGrantType(grantTypes, OIDCGrantTypeAuthorizationCode) {
|
||||
CheckRedirectUrisCode(compliance, appType, redirectUris)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func checkApplicationType(compliance *Compliance, appType *OIDCApplicationType, authMethod *OIDCAuthMethodType) {
|
||||
if appType != nil {
|
||||
switch *appType {
|
||||
case OIDCApplicationTypeNative:
|
||||
GetOIDCV1NativeApplicationCompliance(compliance, authMethod)
|
||||
case OIDCApplicationTypeUserAgent:
|
||||
GetOIDCV1UserAgentApplicationCompliance(compliance, authMethod)
|
||||
case OIDCApplicationTypeWeb:
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if compliance.NoneCompliant {
|
||||
compliance.Problems = append([]string{"Application.OIDC.V1.NotCompliant"}, compliance.Problems...)
|
||||
}
|
||||
}
|
||||
|
||||
func GetOIDCV1NativeApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) {
|
||||
if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.AuthMethodType.NotNone")
|
||||
}
|
||||
}
|
||||
|
||||
func GetOIDCV1UserAgentApplicationCompliance(compliance *Compliance, authMethod *OIDCAuthMethodType) {
|
||||
if authMethod != nil && *authMethod != OIDCAuthMethodTypeNone {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.UserAgent.AuthMethodType.NotNone")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckRedirectUrisCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
|
||||
if urlsAreHttps(redirectUris) {
|
||||
return
|
||||
}
|
||||
if urlContainsPrefix(redirectUris, http) {
|
||||
if appType != nil && *appType == OIDCApplicationTypeUserAgent {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb")
|
||||
}
|
||||
if appType != nil && *appType == OIDCApplicationTypeNative && !onlyLocalhostIsHttp(redirectUris) {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
|
||||
}
|
||||
}
|
||||
if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckRedirectUrisImplicit(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
|
||||
if urlsAreHttps(redirectUris) {
|
||||
return
|
||||
}
|
||||
if containsCustom(redirectUris) {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed")
|
||||
}
|
||||
if urlContainsPrefix(redirectUris, http) {
|
||||
if appType != nil && *appType == OIDCApplicationTypeNative {
|
||||
if !onlyLocalhostIsHttp(redirectUris) {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
|
||||
}
|
||||
return
|
||||
}
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.HttpNotAllowed")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckRedirectUrisImplicitAndCode(compliance *Compliance, appType *OIDCApplicationType, redirectUris []string) {
|
||||
if urlsAreHttps(redirectUris) {
|
||||
return
|
||||
}
|
||||
if containsCustom(redirectUris) && appType != nil && *appType != OIDCApplicationTypeNative {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed")
|
||||
}
|
||||
if urlContainsPrefix(redirectUris, http) {
|
||||
if appType != nil && *appType == OIDCApplicationTypeUserAgent {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb")
|
||||
}
|
||||
if !onlyLocalhostIsHttp(redirectUris) && appType != nil && *appType == OIDCApplicationTypeNative {
|
||||
compliance.NoneCompliant = true
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost")
|
||||
}
|
||||
}
|
||||
if !compliance.NoneCompliant {
|
||||
compliance.Problems = append(compliance.Problems, "Application.OIDC.V1.NotAllCombinationsAreAllowed")
|
||||
}
|
||||
}
|
||||
|
||||
func urlsAreHttps(uris []string) bool {
|
||||
for _, uri := range uris {
|
||||
if !strings.HasPrefix(uri, https) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func urlContainsPrefix(uris []string, prefix string) bool {
|
||||
for _, uri := range uris {
|
||||
if strings.HasPrefix(uri, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsCustom(uris []string) bool {
|
||||
for _, uri := range uris {
|
||||
if !strings.HasPrefix(uri, http) && !strings.HasPrefix(uri, https) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func onlyLocalhostIsHttp(uris []string) bool {
|
||||
for _, uri := range uris {
|
||||
if strings.HasPrefix(uri, http) && !isHTTPLoopbackLocalhost(uri) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func isHTTPLoopbackLocalhost(uri string) bool {
|
||||
return strings.HasPrefix(uri, httpLocalhostWithoutPort) ||
|
||||
strings.HasPrefix(uri, httpLocalhostWithPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV4WithoutPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV4WithPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV6WithoutPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV6WithPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV6LongWithoutPort) ||
|
||||
strings.HasPrefix(uri, httpLoopbackV6LongWithPort)
|
||||
}
|
||||
|
||||
func OIDCOriginAllowList(redirectURIs, additionalOrigins []string) ([]string, error) {
|
||||
allowList := make([]string, 0)
|
||||
for _, redirect := range redirectURIs {
|
||||
origin, err := http_util.GetOriginFromURLString(redirect)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !http_util.IsOriginAllowed(allowList, origin) {
|
||||
allowList = append(allowList, origin)
|
||||
}
|
||||
}
|
||||
for _, origin := range additionalOrigins {
|
||||
if !http_util.IsOriginAllowed(allowList, origin) {
|
||||
allowList = append(allowList, origin)
|
||||
}
|
||||
}
|
||||
return allowList, nil
|
||||
}
|
671
apps/api/internal/domain/application_oidc_test.go
Normal file
671
apps/api/internal/domain/application_oidc_test.go
Normal file
@@ -0,0 +1,671 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/muhlemmer/gu"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
func TestApplicationValid(t *testing.T) {
|
||||
type args struct {
|
||||
app *OIDCApp
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
name: "invalid clock skew",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "AppName",
|
||||
ClockSkew: gu.Ptr(time.Minute * 1),
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "invalid clock skew minus",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "AppName",
|
||||
ClockSkew: gu.Ptr(time.Minute * -1),
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype code",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "invalid oidc application: responsetype code",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "invalid oidc application: responsetype id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype token_id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "invalid oidc application: responsetype token_id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype code & id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype code & token_id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "valid oidc application: responsetype code & id_token & token_id_token",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken, OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit},
|
||||
},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "invalid oidc application: invalid origin",
|
||||
args: args{
|
||||
app: &OIDCApp{
|
||||
ObjectRoot: models.ObjectRoot{AggregateID: "AggregateID"},
|
||||
AppID: "AppID",
|
||||
AppName: "Name",
|
||||
ResponseTypes: []OIDCResponseType{OIDCResponseTypeCode, OIDCResponseTypeIDToken, OIDCResponseTypeIDTokenToken},
|
||||
GrantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeImplicit},
|
||||
AdditionalOrigins: []string{"https://test.com/test"},
|
||||
},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.args.app.IsValid()
|
||||
if result != tt.result {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOIDCV1Compliance(t *testing.T) {
|
||||
type args struct {
|
||||
appType *OIDCApplicationType
|
||||
grantTypes []OIDCGrantType
|
||||
authMethod *OIDCAuthMethodType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "none compliant",
|
||||
args: args{},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := GetOIDCV1Compliance(tt.args.appType, tt.args.grantTypes, tt.args.authMethod, tt.args.redirectUris)
|
||||
if !got.NoneCompliant {
|
||||
t.Error("compliance should be none compliant")
|
||||
}
|
||||
if len(got.Problems) == 0 || got.Problems[0] != "Application.OIDC.V1.NotCompliant" {
|
||||
t.Errorf("first entry of problems should be \"Application.OIDC.V1.NotCompliant\" but got %v", got.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkGrantTypesCombination(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Compliance
|
||||
grantTypes []OIDCGrantType
|
||||
}{
|
||||
{
|
||||
name: "implicit",
|
||||
want: new(Compliance),
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
|
||||
},
|
||||
{
|
||||
name: "refresh token and implicit",
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.GrantType.Refresh.NoAuthCode"},
|
||||
},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit, OIDCGrantTypeRefreshToken},
|
||||
},
|
||||
{
|
||||
name: "device code flow and refresh token doesnt require OIDCGrantTypeImplicit",
|
||||
want: &Compliance{},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeDeviceCode, OIDCGrantTypeRefreshToken},
|
||||
},
|
||||
{
|
||||
name: "refresh token and authorization code",
|
||||
want: &Compliance{},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode, OIDCGrantTypeRefreshToken},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compliance := new(Compliance)
|
||||
|
||||
checkGrantTypesCombination(compliance, tt.grantTypes)
|
||||
|
||||
if tt.want.NoneCompliant != compliance.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkRedirectURIs(t *testing.T) {
|
||||
type args struct {
|
||||
grantTypes []OIDCGrantType
|
||||
appType *OIDCApplicationType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Compliance
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "no redirect uris",
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{
|
||||
"Application.OIDC.V1.NoRedirectUris",
|
||||
},
|
||||
},
|
||||
args: args{},
|
||||
},
|
||||
{
|
||||
name: "implicit and authorization code",
|
||||
want: &Compliance{
|
||||
NoneCompliant: false,
|
||||
Problems: []string{"Application.OIDC.V1.NotAllCombinationsAreAllowed"},
|
||||
},
|
||||
args: args{
|
||||
redirectUris: []string{"http://redirect.to/me"},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit, OIDCGrantTypeAuthorizationCode},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only implicit",
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.HttpNotAllowed"},
|
||||
},
|
||||
args: args{
|
||||
redirectUris: []string{"http://redirect.to/me"},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeImplicit},
|
||||
appType: gu.Ptr(OIDCApplicationTypeUserAgent),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only authorization code",
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"},
|
||||
},
|
||||
args: args{
|
||||
redirectUris: []string{"http://redirect.to/me"},
|
||||
grantTypes: []OIDCGrantType{OIDCGrantTypeAuthorizationCode},
|
||||
appType: gu.Ptr(OIDCApplicationTypeUserAgent),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compliance := new(Compliance)
|
||||
|
||||
checkRedirectURIs(compliance, tt.args.grantTypes, tt.args.appType, tt.args.redirectUris)
|
||||
|
||||
if tt.want.NoneCompliant != compliance.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CheckRedirectUrisImplicitAndCode(t *testing.T) {
|
||||
type args struct {
|
||||
appType *OIDCApplicationType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
want *Compliance
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "implicit and code https",
|
||||
want: &Compliance{
|
||||
NoneCompliant: false,
|
||||
Problems: nil,
|
||||
},
|
||||
args: args{
|
||||
redirectUris: []string{"https://redirect.to/me"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
compliance := new(Compliance)
|
||||
|
||||
CheckRedirectUrisImplicitAndCode(compliance, tt.args.appType, tt.args.redirectUris)
|
||||
|
||||
if tt.want.NoneCompliant != compliance.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, compliance.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, compliance.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, compliance.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRedirectUrisImplicitAndCode(t *testing.T) {
|
||||
type args struct {
|
||||
appType *OIDCApplicationType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Compliance
|
||||
}{
|
||||
{
|
||||
name: "only https",
|
||||
args: args{},
|
||||
want: &Compliance{},
|
||||
},
|
||||
{
|
||||
name: "custom protocol not native app",
|
||||
args: args{
|
||||
appType: gu.Ptr(OIDCApplicationTypeWeb),
|
||||
redirectUris: []string{"custom://nirvana.com"},
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http localhost user agent app",
|
||||
args: args{
|
||||
appType: gu.Ptr(OIDCApplicationTypeUserAgent),
|
||||
redirectUris: []string{"http://localhost:9009"},
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "http, not only localhost native app",
|
||||
args: args{
|
||||
appType: gu.Ptr(OIDCApplicationTypeNative),
|
||||
redirectUris: []string{"http://nirvana.com", "http://localhost:9009"},
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "not allowed combination",
|
||||
args: args{
|
||||
appType: gu.Ptr(OIDCApplicationTypeNative),
|
||||
redirectUris: []string{"https://nirvana.com", "cutom://nirvana.com"},
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: false,
|
||||
Problems: []string{"Application.OIDC.V1.NotAllCombinationsAreAllowed"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := new(Compliance)
|
||||
CheckRedirectUrisImplicitAndCode(got, tt.args.appType, tt.args.redirectUris)
|
||||
|
||||
if tt.want.NoneCompliant != got.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, got.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRedirectUrisImplicit(t *testing.T) {
|
||||
type args struct {
|
||||
appType *OIDCApplicationType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Compliance
|
||||
}{
|
||||
{
|
||||
name: "only https",
|
||||
args: args{},
|
||||
want: &Compliance{},
|
||||
},
|
||||
{
|
||||
name: "custom protocol",
|
||||
args: args{
|
||||
redirectUris: []string{"custom://nirvana.com"},
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.CustomNotAllowed"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only http protocol, app type native, not only localhost",
|
||||
args: args{
|
||||
redirectUris: []string{"http://nirvana.com"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeNative),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only http protocol, app type native, only localhost",
|
||||
args: args{
|
||||
redirectUris: []string{"http://localhost:8080"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeNative),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: false,
|
||||
Problems: nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only http protocol, app type web",
|
||||
args: args{
|
||||
redirectUris: []string{"http://nirvana.com"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeWeb),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Implicit.RedirectUris.HttpNotAllowed"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := new(Compliance)
|
||||
CheckRedirectUrisImplicit(got, tt.args.appType, tt.args.redirectUris)
|
||||
|
||||
if tt.want.NoneCompliant != got.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, got.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckRedirectUrisCode(t *testing.T) {
|
||||
type args struct {
|
||||
appType *OIDCApplicationType
|
||||
redirectUris []string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *Compliance
|
||||
}{
|
||||
{
|
||||
name: "only https",
|
||||
args: args{},
|
||||
want: &Compliance{},
|
||||
},
|
||||
{
|
||||
name: "custom prefix, app type web",
|
||||
args: args{
|
||||
redirectUris: []string{"custom://nirvana.com"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeWeb),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only http protocol, app type user agent",
|
||||
args: args{
|
||||
redirectUris: []string{"http://nirvana.com"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeUserAgent),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Code.RedirectUris.HttpOnlyForWeb"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "only http protocol, app type native, only localhost",
|
||||
args: args{
|
||||
redirectUris: []string{"http://localhost:8080", "http://nirvana.com:8080"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeNative),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Native.RedirectUris.MustBeHttpLocalhost"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "custom protocol, not native",
|
||||
args: args{
|
||||
redirectUris: []string{"custom://nirvana.com"},
|
||||
appType: gu.Ptr(OIDCApplicationTypeWeb),
|
||||
},
|
||||
want: &Compliance{
|
||||
NoneCompliant: true,
|
||||
Problems: []string{"Application.OIDC.V1.Code.RedirectUris.CustomOnlyForNative"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := new(Compliance)
|
||||
CheckRedirectUrisCode(got, tt.args.appType, tt.args.redirectUris)
|
||||
|
||||
if tt.want.NoneCompliant != got.NoneCompliant {
|
||||
t.Errorf("NoneCompliant: expected: %v, got %v", tt.want.NoneCompliant, got.NoneCompliant)
|
||||
}
|
||||
if !reflect.DeepEqual(tt.want.Problems, got.Problems) {
|
||||
t.Errorf("Problems: expected: %v, got %v", tt.want.Problems, got.Problems)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOIDCOriginAllowList(t *testing.T) {
|
||||
type args struct {
|
||||
redirectUris []string
|
||||
additionalOrigins []string
|
||||
}
|
||||
type want struct {
|
||||
allowed []string
|
||||
err func(error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
name: "no uris, no origins",
|
||||
args: args{},
|
||||
want: want{
|
||||
allowed: []string{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "redirects invalid schema",
|
||||
args: args{
|
||||
redirectUris: []string{"https:// localhost:8080"},
|
||||
},
|
||||
want: want{
|
||||
allowed: nil,
|
||||
err: func(e error) bool {
|
||||
return strings.HasPrefix(e.Error(), "invalid chavalid character")
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "redirects additional",
|
||||
args: args{
|
||||
redirectUris: []string{"https://localhost:8080"},
|
||||
},
|
||||
want: want{
|
||||
allowed: []string{"https://localhost:8080"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "additional origin",
|
||||
args: args{
|
||||
additionalOrigins: []string{"https://localhost:8080"},
|
||||
},
|
||||
want: want{
|
||||
allowed: []string{"https://localhost:8080"},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
allowed, err := OIDCOriginAllowList(tt.args.redirectUris, tt.args.additionalOrigins)
|
||||
|
||||
if tt.want.err == nil && err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
} else if tt.want.err == nil && err == nil {
|
||||
//ok
|
||||
} else if tt.want.err(err) {
|
||||
t.Errorf("unexpected err got %v", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(allowed, tt.want.allowed) {
|
||||
t.Errorf("expected list: %v, got: %v", tt.want.allowed, allowed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
45
apps/api/internal/domain/application_saml.go
Normal file
45
apps/api/internal/domain/application_saml.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type SAMLApp struct {
|
||||
models.ObjectRoot
|
||||
|
||||
AppID string
|
||||
AppName string
|
||||
EntityID string
|
||||
Metadata []byte
|
||||
MetadataURL *string
|
||||
LoginVersion *LoginVersion
|
||||
LoginBaseURI *string
|
||||
|
||||
State AppState
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetApplicationName() string {
|
||||
return a.AppName
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetState() AppState {
|
||||
return a.State
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetMetadata() []byte {
|
||||
return a.Metadata
|
||||
}
|
||||
|
||||
func (a *SAMLApp) GetMetadataURL() string {
|
||||
if a.MetadataURL != nil {
|
||||
return *a.MetadataURL
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *SAMLApp) IsValid() bool {
|
||||
if (a.MetadataURL == nil || *a.MetadataURL == "") && a.Metadata == nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
46
apps/api/internal/domain/asset.go
Normal file
46
apps/api/internal/domain/asset.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
UsersAssetPath = "users"
|
||||
AvatarAssetPath = "/avatar"
|
||||
|
||||
policyPrefix = "policy"
|
||||
LabelPolicyPrefix = policyPrefix + "/label"
|
||||
labelPolicyLogoPrefix = LabelPolicyPrefix + "/logo"
|
||||
labelPolicyIconPrefix = LabelPolicyPrefix + "/icon"
|
||||
labelPolicyFontPrefix = LabelPolicyPrefix + "/font"
|
||||
Dark = "dark"
|
||||
|
||||
CssPath = LabelPolicyPrefix + "/css"
|
||||
CssVariablesFileName = "variables.css"
|
||||
|
||||
LabelPolicyLogoPath = labelPolicyLogoPrefix
|
||||
LabelPolicyIconPath = labelPolicyIconPrefix
|
||||
LabelPolicyFontPath = labelPolicyFontPrefix
|
||||
)
|
||||
|
||||
type AssetInfo struct {
|
||||
Bucket string
|
||||
Key string
|
||||
ETag string
|
||||
Size int64
|
||||
LastModified time.Time
|
||||
Location string
|
||||
VersionID string
|
||||
Expiration time.Time
|
||||
AutheticatedURL string
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func GetHumanAvatarAssetPath(userID string) string {
|
||||
return UsersAssetPath + "/" + userID + AvatarAssetPath
|
||||
}
|
||||
|
||||
func AssetURL(prefix, resourceOwner, key string) string {
|
||||
if prefix == "" || resourceOwner == "" || key == "" {
|
||||
return ""
|
||||
}
|
||||
return prefix + "/" + resourceOwner + "/" + key
|
||||
}
|
285
apps/api/internal/domain/auth_request.go
Normal file
285
apps/api/internal/domain/auth_request.go
Normal file
@@ -0,0 +1,285 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type AuthRequest struct {
|
||||
ID string
|
||||
AgentID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
BrowserInfo *BrowserInfo
|
||||
ApplicationID string
|
||||
CallbackURI string
|
||||
TransferState string
|
||||
Prompt []Prompt
|
||||
PossibleLOAs []LevelOfAssurance
|
||||
UiLocales []string
|
||||
LoginHint string
|
||||
MaxAuthAge *time.Duration
|
||||
InstanceID string
|
||||
Request Request
|
||||
|
||||
levelOfAssurance LevelOfAssurance
|
||||
UserID string
|
||||
UserName string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
AvatarKey string
|
||||
PresignedAvatar string
|
||||
UserOrgID string
|
||||
PreferredLanguage *language.Tag
|
||||
RequestedOrgID string
|
||||
RequestedOrgName string
|
||||
RequestedPrimaryDomain string
|
||||
RequestedOrgDomain bool
|
||||
ApplicationResourceOwner string
|
||||
PrivateLabelingSetting PrivateLabelingSetting
|
||||
SelectedIDPConfigID string
|
||||
SelectedIDPConfigArgs map[string]any
|
||||
LinkingUsers []*ExternalUser
|
||||
PossibleSteps []NextStep `json:"-"`
|
||||
PasswordVerified bool
|
||||
IDPLoginChecked bool
|
||||
MFAsVerified []MFAType
|
||||
Audience []string
|
||||
AuthTime time.Time
|
||||
Code string
|
||||
LoginPolicy *LoginPolicy
|
||||
AllowedExternalIDPs []*IDPProvider
|
||||
LabelPolicy *LabelPolicy
|
||||
PrivacyPolicy *PrivacyPolicy
|
||||
LockoutPolicy *LockoutPolicy
|
||||
PasswordAgePolicy *PasswordAgePolicy
|
||||
DefaultTranslations []*CustomText
|
||||
OrgTranslations []*CustomText
|
||||
SAMLRequestID string
|
||||
RequestLocalAuth bool
|
||||
// orgID the policies were last loaded with
|
||||
policyOrgID string
|
||||
// SessionID is set to the computed sessionID of the login session table
|
||||
SessionID string
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetPolicyOrgID(id string) {
|
||||
a.policyOrgID = id
|
||||
}
|
||||
|
||||
func (a *AuthRequest) PolicyOrgID() string {
|
||||
return a.policyOrgID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) AuthMethods() []UserAuthMethodType {
|
||||
list := make([]UserAuthMethodType, 0, len(a.MFAsVerified)+2)
|
||||
if a.PasswordVerified {
|
||||
list = append(list, UserAuthMethodTypePassword)
|
||||
}
|
||||
if a.IDPLoginChecked {
|
||||
list = append(list, UserAuthMethodTypeIDP)
|
||||
}
|
||||
for _, mfa := range a.MFAsVerified {
|
||||
list = append(list, mfa.UserAuthMethodType())
|
||||
}
|
||||
return slices.Compact(list)
|
||||
}
|
||||
|
||||
type ExternalUser struct {
|
||||
IDPConfigID string
|
||||
ExternalUserID string
|
||||
DisplayName string
|
||||
PreferredUsername string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
Email EmailAddress
|
||||
IsEmailVerified bool
|
||||
PreferredLanguage language.Tag
|
||||
Phone PhoneNumber
|
||||
IsPhoneVerified bool
|
||||
Metadatas []*Metadata
|
||||
}
|
||||
|
||||
type Prompt int32
|
||||
|
||||
const (
|
||||
PromptUnspecified Prompt = iota
|
||||
PromptNone
|
||||
PromptLogin
|
||||
PromptConsent
|
||||
PromptSelectAccount
|
||||
PromptCreate
|
||||
)
|
||||
|
||||
func IsPrompt(prompt []Prompt, requestedPrompt Prompt) bool {
|
||||
for _, p := range prompt {
|
||||
if p == requestedPrompt {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type LevelOfAssurance int
|
||||
|
||||
const (
|
||||
LevelOfAssuranceNone LevelOfAssurance = iota
|
||||
)
|
||||
|
||||
type MFAType int
|
||||
|
||||
const (
|
||||
MFATypeTOTP MFAType = iota
|
||||
MFATypeU2F
|
||||
MFATypeU2FUserVerification
|
||||
MFATypeOTPSMS
|
||||
MFATypeOTPEmail
|
||||
)
|
||||
|
||||
func (m MFAType) UserAuthMethodType() UserAuthMethodType {
|
||||
switch m {
|
||||
case MFATypeTOTP:
|
||||
return UserAuthMethodTypeTOTP
|
||||
case MFATypeU2F:
|
||||
return UserAuthMethodTypeU2F
|
||||
case MFATypeU2FUserVerification:
|
||||
return UserAuthMethodTypePasswordless
|
||||
case MFATypeOTPSMS:
|
||||
return UserAuthMethodTypeOTPSMS
|
||||
case MFATypeOTPEmail:
|
||||
return UserAuthMethodTypeOTPEmail
|
||||
default:
|
||||
return UserAuthMethodTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
type MFALevel int
|
||||
|
||||
const (
|
||||
MFALevelNotSetUp MFALevel = iota
|
||||
MFALevelSecondFactor
|
||||
MFALevelMultiFactor
|
||||
MFALevelMultiFactorCertified
|
||||
)
|
||||
|
||||
type AuthRequestState int
|
||||
|
||||
const (
|
||||
AuthRequestStateUnspecified AuthRequestState = iota
|
||||
AuthRequestStateAdded
|
||||
AuthRequestStateCodeAdded
|
||||
AuthRequestStateCodeExchanged
|
||||
AuthRequestStateFailed
|
||||
AuthRequestStateSucceeded
|
||||
)
|
||||
|
||||
func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
|
||||
switch requestType {
|
||||
case AuthRequestTypeOIDC:
|
||||
return &AuthRequest{Request: &AuthRequestOIDC{}}, nil
|
||||
case AuthRequestTypeSAML:
|
||||
return &AuthRequest{Request: &AuthRequestSAML{}}, nil
|
||||
case AuthRequestTypeDevice:
|
||||
return &AuthRequest{Request: &AuthRequestDevice{}}, nil
|
||||
}
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "DOMAIN-ds2kl", "invalid request type")
|
||||
}
|
||||
|
||||
func (a *AuthRequest) WithCurrentInfo(info *BrowserInfo) *AuthRequest {
|
||||
a.BrowserInfo = info
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetUserInfo(userID, userName, loginName, displayName, avatar, userOrgID string) {
|
||||
a.UserID = userID
|
||||
a.UserName = userName
|
||||
a.LoginName = loginName
|
||||
a.DisplayName = displayName
|
||||
a.AvatarKey = avatar
|
||||
a.UserOrgID = userOrgID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) SetOrgInformation(id, name, primaryDomain string, requestedByDomain bool) {
|
||||
a.RequestedOrgID = id
|
||||
a.RequestedOrgName = name
|
||||
a.RequestedPrimaryDomain = primaryDomain
|
||||
a.RequestedOrgDomain = requestedByDomain
|
||||
}
|
||||
|
||||
func (a *AuthRequest) MFALevel() MFALevel {
|
||||
return -1
|
||||
//PLANNED: check a.PossibleLOAs (and Prompt Login?)
|
||||
}
|
||||
|
||||
func (a *AuthRequest) AppendAudIfNotExisting(aud string) {
|
||||
for _, a := range a.Audience {
|
||||
if a == aud {
|
||||
return
|
||||
}
|
||||
}
|
||||
a.Audience = append(a.Audience, aud)
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetScopeOrgPrimaryDomain() string {
|
||||
switch request := a.Request.(type) {
|
||||
case *AuthRequestOIDC:
|
||||
for _, scope := range request.Scopes {
|
||||
if strings.HasPrefix(scope, OrgDomainPrimaryScope) {
|
||||
return strings.TrimPrefix(scope, OrgDomainPrimaryScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AuthRequest) GetScopeOrgID() string {
|
||||
switch request := a.Request.(type) {
|
||||
case *AuthRequestOIDC:
|
||||
for _, scope := range request.Scopes {
|
||||
if strings.HasPrefix(scope, OrgIDScope) {
|
||||
return strings.TrimPrefix(scope, OrgIDScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (a *AuthRequest) Done() bool {
|
||||
for _, step := range a.PossibleSteps {
|
||||
if step.Type() == NextStepRedirectToCallback {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *AuthRequest) PrivateLabelingOrgID(defaultID string) string {
|
||||
if a.RequestedOrgID != "" {
|
||||
return a.RequestedOrgID
|
||||
}
|
||||
if (a.PrivateLabelingSetting == PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy || a.PrivateLabelingSetting == PrivateLabelingSettingUnspecified) &&
|
||||
a.UserOrgID != "" {
|
||||
return a.UserOrgID
|
||||
}
|
||||
if a.PrivateLabelingSetting != PrivateLabelingSettingUnspecified {
|
||||
return a.ApplicationResourceOwner
|
||||
}
|
||||
return defaultID
|
||||
}
|
||||
|
||||
func (a *AuthRequest) UserAuthMethodTypes() []UserAuthMethodType {
|
||||
list := make([]UserAuthMethodType, 0, len(a.MFAsVerified)+1)
|
||||
if a.PasswordVerified {
|
||||
list = append(list, UserAuthMethodTypePassword)
|
||||
}
|
||||
for _, mfa := range a.MFAsVerified {
|
||||
list = append(list, mfa.UserAuthMethodType())
|
||||
}
|
||||
return list
|
||||
}
|
108
apps/api/internal/domain/auth_request_test.go
Normal file
108
apps/api/internal/domain/auth_request_test.go
Normal file
@@ -0,0 +1,108 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestMFAType_UserAuthMethodType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
m MFAType
|
||||
want UserAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "totp",
|
||||
m: MFATypeTOTP,
|
||||
want: UserAuthMethodTypeTOTP,
|
||||
},
|
||||
{
|
||||
name: "u2f",
|
||||
m: MFATypeU2F,
|
||||
want: UserAuthMethodTypeU2F,
|
||||
},
|
||||
{
|
||||
name: "passwordless",
|
||||
m: MFATypeU2FUserVerification,
|
||||
want: UserAuthMethodTypePasswordless,
|
||||
},
|
||||
{
|
||||
name: "otp sms",
|
||||
m: MFATypeOTPSMS,
|
||||
want: UserAuthMethodTypeOTPSMS,
|
||||
},
|
||||
{
|
||||
name: "otp email",
|
||||
m: MFATypeOTPEmail,
|
||||
want: UserAuthMethodTypeOTPEmail,
|
||||
},
|
||||
{
|
||||
name: "unspecified",
|
||||
m: 99,
|
||||
want: UserAuthMethodTypeUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.m.UserAuthMethodType()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthRequest_UserAuthMethodTypes(t *testing.T) {
|
||||
type fields struct {
|
||||
PasswordVerified bool
|
||||
MFAsVerified []MFAType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want []UserAuthMethodType
|
||||
}{
|
||||
{
|
||||
name: "no auth methods",
|
||||
fields: fields{
|
||||
PasswordVerified: false,
|
||||
MFAsVerified: nil,
|
||||
},
|
||||
want: []UserAuthMethodType{},
|
||||
},
|
||||
{
|
||||
name: "only password",
|
||||
fields: fields{
|
||||
PasswordVerified: true,
|
||||
MFAsVerified: nil,
|
||||
},
|
||||
want: []UserAuthMethodType{
|
||||
UserAuthMethodTypePassword,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "password, with mfa",
|
||||
fields: fields{
|
||||
PasswordVerified: true,
|
||||
MFAsVerified: []MFAType{
|
||||
MFATypeTOTP,
|
||||
MFATypeU2F,
|
||||
},
|
||||
},
|
||||
want: []UserAuthMethodType{
|
||||
UserAuthMethodTypePassword,
|
||||
UserAuthMethodTypeTOTP,
|
||||
UserAuthMethodTypeU2F,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
a := &AuthRequest{
|
||||
PasswordVerified: tt.fields.PasswordVerified,
|
||||
MFAsVerified: tt.fields.MFAsVerified,
|
||||
}
|
||||
got := a.UserAuthMethodTypes()
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
65
apps/api/internal/domain/authn_key.go
Normal file
65
apps/api/internal/domain/authn_key.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/logging"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type authNKey interface {
|
||||
SetPublicKey([]byte)
|
||||
SetPrivateKey([]byte)
|
||||
expiration
|
||||
}
|
||||
|
||||
type AuthNKeyType int32
|
||||
|
||||
const (
|
||||
AuthNKeyTypeNONE AuthNKeyType = iota
|
||||
AuthNKeyTypeJSON
|
||||
|
||||
keyCount
|
||||
)
|
||||
|
||||
func (k AuthNKeyType) Valid() bool {
|
||||
return k >= 0 && k < keyCount
|
||||
}
|
||||
|
||||
func (key *MachineKey) GenerateNewMachineKeyPair(keySize int) error {
|
||||
privateKey, publicKey, err := crypto.GenerateKeyPair(keySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.PublicKey, err = crypto.PublicKeyToBytes(publicKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.PrivateKey = crypto.PrivateKeyToBytes(privateKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func SetNewAuthNKeyPair(key authNKey, keySize int) error {
|
||||
privateKey, publicKey, err := NewAuthNKeyPair(keySize)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.SetPrivateKey(privateKey)
|
||||
key.SetPublicKey(publicKey)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAuthNKeyPair(keySize int) (privateKey, publicKey []byte, err error) {
|
||||
private, public, err := crypto.GenerateKeyPair(keySize)
|
||||
if err != nil {
|
||||
logging.Log("AUTHN-Ud51I").WithError(err).Error("unable to create authn key pair")
|
||||
return nil, nil, zerrors.ThrowInternal(err, "AUTHN-gdg2l", "Errors.Project.CouldNotGenerateClientSecret")
|
||||
}
|
||||
publicKey, err = crypto.PublicKeyToBytes(public)
|
||||
if err != nil {
|
||||
logging.Log("AUTHN-Dbb35").WithError(err).Error("unable to convert public key")
|
||||
return nil, nil, zerrors.ThrowInternal(err, "AUTHN-Bne3f", "Errors.Project.CouldNotGenerateClientSecret")
|
||||
}
|
||||
privateKey = crypto.PrivateKeyToBytes(private)
|
||||
return privateKey, publicKey, nil
|
||||
}
|
37
apps/api/internal/domain/browser_info.go
Normal file
37
apps/api/internal/domain/browser_info.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net"
|
||||
net_http "net/http"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
)
|
||||
|
||||
type BrowserInfo struct {
|
||||
UserAgent string
|
||||
AcceptLanguage string
|
||||
RemoteIP net.IP
|
||||
Header net_http.Header
|
||||
}
|
||||
|
||||
func BrowserInfoFromRequest(r *net_http.Request) *BrowserInfo {
|
||||
return &BrowserInfo{
|
||||
UserAgent: r.Header.Get(http_util.UserAgentHeader),
|
||||
AcceptLanguage: r.Header.Get(http_util.AcceptLanguage),
|
||||
RemoteIP: http_util.RemoteIPFromRequest(r),
|
||||
Header: r.Header,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *AuthRequest) ToUserAgent() *UserAgent {
|
||||
agent := &UserAgent{
|
||||
FingerprintID: &a.AgentID,
|
||||
}
|
||||
if a.BrowserInfo == nil {
|
||||
return agent
|
||||
}
|
||||
agent.IP = a.BrowserInfo.RemoteIP
|
||||
agent.Description = &a.BrowserInfo.UserAgent
|
||||
agent.Header = a.BrowserInfo.Header
|
||||
return agent
|
||||
}
|
8
apps/api/internal/domain/bucket.go
Normal file
8
apps/api/internal/domain/bucket.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type BucketInfo struct {
|
||||
Name string
|
||||
CreationDate time.Time
|
||||
}
|
9
apps/api/internal/domain/count_trigger.go
Normal file
9
apps/api/internal/domain/count_trigger.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
//go:generate enumer -type CountParentType -transform lower -trimprefix CountParentType -sql
|
||||
type CountParentType int
|
||||
|
||||
const (
|
||||
CountParentTypeInstance CountParentType = iota
|
||||
CountParentTypeOrganization
|
||||
)
|
109
apps/api/internal/domain/countparenttype_enumer.go
Normal file
109
apps/api/internal/domain/countparenttype_enumer.go
Normal file
@@ -0,0 +1,109 @@
|
||||
// Code generated by "enumer -type CountParentType -transform lower -trimprefix CountParentType -sql"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _CountParentTypeName = "instanceorganization"
|
||||
|
||||
var _CountParentTypeIndex = [...]uint8{0, 8, 20}
|
||||
|
||||
const _CountParentTypeLowerName = "instanceorganization"
|
||||
|
||||
func (i CountParentType) String() string {
|
||||
if i < 0 || i >= CountParentType(len(_CountParentTypeIndex)-1) {
|
||||
return fmt.Sprintf("CountParentType(%d)", i)
|
||||
}
|
||||
return _CountParentTypeName[_CountParentTypeIndex[i]:_CountParentTypeIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _CountParentTypeNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[CountParentTypeInstance-(0)]
|
||||
_ = x[CountParentTypeOrganization-(1)]
|
||||
}
|
||||
|
||||
var _CountParentTypeValues = []CountParentType{CountParentTypeInstance, CountParentTypeOrganization}
|
||||
|
||||
var _CountParentTypeNameToValueMap = map[string]CountParentType{
|
||||
_CountParentTypeName[0:8]: CountParentTypeInstance,
|
||||
_CountParentTypeLowerName[0:8]: CountParentTypeInstance,
|
||||
_CountParentTypeName[8:20]: CountParentTypeOrganization,
|
||||
_CountParentTypeLowerName[8:20]: CountParentTypeOrganization,
|
||||
}
|
||||
|
||||
var _CountParentTypeNames = []string{
|
||||
_CountParentTypeName[0:8],
|
||||
_CountParentTypeName[8:20],
|
||||
}
|
||||
|
||||
// CountParentTypeString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func CountParentTypeString(s string) (CountParentType, error) {
|
||||
if val, ok := _CountParentTypeNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _CountParentTypeNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to CountParentType values", s)
|
||||
}
|
||||
|
||||
// CountParentTypeValues returns all values of the enum
|
||||
func CountParentTypeValues() []CountParentType {
|
||||
return _CountParentTypeValues
|
||||
}
|
||||
|
||||
// CountParentTypeStrings returns a slice of all String values of the enum
|
||||
func CountParentTypeStrings() []string {
|
||||
strs := make([]string, len(_CountParentTypeNames))
|
||||
copy(strs, _CountParentTypeNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsACountParentType returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i CountParentType) IsACountParentType() bool {
|
||||
for _, v := range _CountParentTypeValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (i CountParentType) Value() (driver.Value, error) {
|
||||
return i.String(), nil
|
||||
}
|
||||
|
||||
func (i *CountParentType) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var str string
|
||||
switch v := value.(type) {
|
||||
case []byte:
|
||||
str = string(v)
|
||||
case string:
|
||||
str = v
|
||||
case fmt.Stringer:
|
||||
str = v.String()
|
||||
default:
|
||||
return fmt.Errorf("invalid value of CountParentType: %[1]T(%[1]v)", value)
|
||||
}
|
||||
|
||||
val, err := CountParentTypeString(str)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i = val
|
||||
return nil
|
||||
}
|
675
apps/api/internal/domain/custom_login_text.go
Normal file
675
apps/api/internal/domain/custom_login_text.go
Normal file
@@ -0,0 +1,675 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
const (
|
||||
LoginCustomText = "Login"
|
||||
|
||||
LoginKeyLogin = "Login."
|
||||
LoginKeyLoginTitle = LoginKeyLogin + "Title"
|
||||
LoginKeyLoginDescription = LoginKeyLogin + "Description"
|
||||
LoginKeyLoginTitleLinkingProcess = LoginKeyLogin + "TitleLinking"
|
||||
LoginKeyLoginDescriptionLinkingProcess = LoginKeyLogin + "DescriptionLinking"
|
||||
LoginKeyLoginNameLabel = LoginKeyLogin + "LoginNameLabel"
|
||||
LoginKeyLoginUsernamePlaceHolder = LoginKeyLogin + "UsernamePlaceHolder"
|
||||
LoginKeyLoginLoginnamePlaceHolder = LoginKeyLogin + "LoginnamePlaceHolder"
|
||||
LoginKeyLoginRegisterButtonText = LoginKeyLogin + "RegisterButtonText"
|
||||
LoginKeyLoginNextButtonText = LoginKeyLogin + "NextButtonText"
|
||||
LoginKeyLoginExternalUserDescription = LoginKeyLogin + "ExternalUserDescription"
|
||||
LoginKeyLoginUserMustBeMemberOfOrg = LoginKeyLogin + "MustBeMemberOfOrg"
|
||||
|
||||
LoginKeySelectAccount = "SelectAccount."
|
||||
LoginKeySelectAccountTitle = LoginKeySelectAccount + "Title"
|
||||
LoginKeySelectAccountDescription = LoginKeySelectAccount + "Description"
|
||||
LoginKeySelectAccountTitleLinkingProcess = LoginKeySelectAccount + "TitleLinking"
|
||||
LoginKeySelectAccountDescriptionLinkingProcess = LoginKeySelectAccount + "DescriptionLinking"
|
||||
LoginKeySelectAccountOtherUser = LoginKeySelectAccount + "OtherUser"
|
||||
LoginKeySelectAccountSessionStateActive = LoginKeySelectAccount + "SessionState0"
|
||||
LoginKeySelectAccountSessionStateInactive = LoginKeySelectAccount + "SessionState1"
|
||||
LoginKeySelectAccountUserMustBeMemberOfOrg = LoginKeySelectAccount + "MustBeMemberOfOrg"
|
||||
|
||||
LoginKeyPassword = "Password."
|
||||
LoginKeyPasswordTitle = LoginKeyPassword + "Title"
|
||||
LoginKeyPasswordDescription = LoginKeyPassword + "Description"
|
||||
LoginKeyPasswordLabel = LoginKeyPassword + "PasswordLabel"
|
||||
LoginKeyPasswordMinLength = LoginKeyPassword + "MinLength"
|
||||
LoginKeyPasswordHasUppercase = LoginKeyPassword + "HasUppercase"
|
||||
LoginKeyPasswordHasLowercase = LoginKeyPassword + "HasLowercase"
|
||||
LoginKeyPasswordHasNumber = LoginKeyPassword + "HasNumber"
|
||||
LoginKeyPasswordHasSymbol = LoginKeyPassword + "HasSymbol"
|
||||
LoginKeyPasswordConfirmation = LoginKeyPassword + "Confirmation"
|
||||
LoginKeyPasswordResetLinkText = LoginKeyPassword + "ResetLinkText"
|
||||
LoginKeyPasswordBackButtonText = LoginKeyPassword + "BackButtonText"
|
||||
LoginKeyPasswordNextButtonText = LoginKeyPassword + "NextButtonText"
|
||||
|
||||
LoginKeyUsernameChange = "UsernameChange."
|
||||
LoginKeyUsernameChangeTitle = LoginKeyUsernameChange + "Title"
|
||||
LoginKeyUsernameChangeDescription = LoginKeyUsernameChange + "Description"
|
||||
LoginKeyUsernameChangeUsernameLabel = LoginKeyUsernameChange + "UsernameLabel"
|
||||
LoginKeyUsernameChangeCancelButtonText = LoginKeyUsernameChange + "CancelButtonText"
|
||||
LoginKeyUsernameChangeNextButtonText = LoginKeyUsernameChange + "NextButtonText"
|
||||
|
||||
LoginKeyUsernameChangeDone = "UsernameChangeDone."
|
||||
LoginKeyUsernameChangeDoneTitle = LoginKeyUsernameChangeDone + "Title"
|
||||
LoginKeyUsernameChangeDoneDescription = LoginKeyUsernameChangeDone + "Description"
|
||||
LoginKeyUsernameChangeDoneNextButtonText = LoginKeyUsernameChangeDone + "NextButtonText"
|
||||
|
||||
LoginKeyInitPassword = "InitPassword."
|
||||
LoginKeyInitPasswordTitle = LoginKeyInitPassword + "Title"
|
||||
LoginKeyInitPasswordDescription = LoginKeyInitPassword + "Description"
|
||||
LoginKeyInitPasswordCodeLabel = LoginKeyInitPassword + "CodeLabel"
|
||||
LoginKeyInitPasswordNewPasswordLabel = LoginKeyInitPassword + "NewPasswordLabel"
|
||||
LoginKeyInitPasswordNewPasswordConfirmLabel = LoginKeyInitPassword + "NewPasswordConfirmLabel"
|
||||
LoginKeyInitPasswordNextButtonText = LoginKeyInitPassword + "NextButtonText"
|
||||
LoginKeyInitPasswordResendButtonText = LoginKeyInitPassword + "ResendButtonText"
|
||||
|
||||
LoginKeyInitPasswordDone = "InitPasswordDone."
|
||||
LoginKeyInitPasswordDoneTitle = LoginKeyInitPasswordDone + "Title"
|
||||
LoginKeyInitPasswordDoneDescription = LoginKeyInitPasswordDone + "Description"
|
||||
LoginKeyInitPasswordDoneNextButtonText = LoginKeyInitPasswordDone + "NextButtonText"
|
||||
LoginKeyInitPasswordDoneCancelButtonText = LoginKeyInitPasswordDone + "CancelButtonText"
|
||||
|
||||
LoginKeyEmailVerification = "EmailVerification."
|
||||
LoginKeyEmailVerificationTitle = LoginKeyEmailVerification + "Title"
|
||||
LoginKeyEmailVerificationDescription = LoginKeyEmailVerification + "Description"
|
||||
LoginKeyEmailVerificationCodeLabel = LoginKeyEmailVerification + "CodeLabel"
|
||||
LoginKeyEmailVerificationNextButtonText = LoginKeyEmailVerification + "NextButtonText"
|
||||
LoginKeyEmailVerificationResendButtonText = LoginKeyEmailVerification + "ResendButtonText"
|
||||
|
||||
LoginKeyEmailVerificationDone = "EmailVerificationDone."
|
||||
LoginKeyEmailVerificationDoneTitle = LoginKeyEmailVerificationDone + "Title"
|
||||
LoginKeyEmailVerificationDoneDescription = LoginKeyEmailVerificationDone + "Description"
|
||||
LoginKeyEmailVerificationDoneNextButtonText = LoginKeyEmailVerificationDone + "NextButtonText"
|
||||
LoginKeyEmailVerificationDoneCancelButtonText = LoginKeyEmailVerificationDone + "CancelButtonText"
|
||||
LoginKeyEmailVerificationDoneLoginButtonText = LoginKeyEmailVerificationDone + "LoginButtonText"
|
||||
|
||||
LoginKeyInitializeUser = "InitUser."
|
||||
LoginKeyInitializeUserTitle = LoginKeyInitializeUser + "Title"
|
||||
LoginKeyInitializeUserDescription = LoginKeyInitializeUser + "Description"
|
||||
LoginKeyInitializeUserCodeLabel = LoginKeyInitializeUser + "CodeLabel"
|
||||
LoginKeyInitializeUserNewPasswordLabel = LoginKeyInitializeUser + "NewPasswordLabel"
|
||||
LoginKeyInitializeUserNewPasswordConfirmLabel = LoginKeyInitializeUser + "NewPasswordConfirm"
|
||||
LoginKeyInitializeUserResendButtonText = LoginKeyInitializeUser + "ResendButtonText"
|
||||
LoginKeyInitializeUserNextButtonText = LoginKeyInitializeUser + "NextButtonText"
|
||||
|
||||
LoginKeyInitUserDone = "InitUserDone."
|
||||
LoginKeyInitUserDoneTitle = LoginKeyInitUserDone + "Title"
|
||||
LoginKeyInitUserDoneDescription = LoginKeyInitUserDone + "Description"
|
||||
LoginKeyInitUserDoneCancelButtonText = LoginKeyInitUserDone + "CancelButtonText"
|
||||
LoginKeyInitUserDoneNextButtonText = LoginKeyInitUserDone + "NextButtonText"
|
||||
|
||||
LoginKeyInitMFAPrompt = "InitMFAPrompt."
|
||||
LoginKeyInitMFAPromptTitle = LoginKeyInitMFAPrompt + "Title"
|
||||
LoginKeyInitMFAPromptDescription = LoginKeyInitMFAPrompt + "Description"
|
||||
LoginKeyInitMFAPromptOTPOption = LoginKeyInitMFAPrompt + "Provider0"
|
||||
LoginKeyInitMFAPromptU2FOption = LoginKeyInitMFAPrompt + "Provider1"
|
||||
LoginKeyInitMFAPromptSkipButtonText = LoginKeyInitMFAPrompt + "SkipButtonText"
|
||||
LoginKeyInitMFAPromptNextButtonText = LoginKeyInitMFAPrompt + "NextButtonText"
|
||||
|
||||
LoginKeyInitMFAOTP = "InitMFAOTP."
|
||||
LoginKeyInitMFAOTPTitle = LoginKeyInitMFAOTP + "Title"
|
||||
LoginKeyInitMFAOTPDescription = LoginKeyInitMFAOTP + "Description"
|
||||
LoginKeyInitMFAOTPDescriptionOTP = LoginKeyInitMFAOTP + "OTPDescription"
|
||||
LoginKeyInitMFAOTPSecretLabel = LoginKeyInitMFAOTP + "SecretLabel"
|
||||
LoginKeyInitMFAOTPCodeLabel = LoginKeyInitMFAOTP + "CodeLabel"
|
||||
LoginKeyInitMFAOTPNextButtonText = LoginKeyInitMFAOTP + "NextButtonText"
|
||||
LoginKeyInitMFAOTPCancelButtonText = LoginKeyInitMFAOTP + "CancelButtonText"
|
||||
|
||||
LoginKeyInitMFAU2F = "InitMFAU2F."
|
||||
LoginKeyInitMFAU2FTitle = LoginKeyInitMFAU2F + "Title"
|
||||
LoginKeyInitMFAU2FDescription = LoginKeyInitMFAU2F + "Description"
|
||||
LoginKeyInitMFAU2FTokenNameLabel = LoginKeyInitMFAU2F + "TokenNameLabel"
|
||||
LoginKeyInitMFAU2FNotSupported = LoginKeyInitMFAU2F + "NotSupported"
|
||||
LoginKeyInitMFAU2FRegisterTokenButtonText = LoginKeyInitMFAU2F + "RegisterTokenButtonText"
|
||||
LoginKeyInitMFAU2FErrorRetry = LoginKeyInitMFAU2F + "ErrorRetry"
|
||||
|
||||
LoginKeyInitMFADone = "InitMFADone."
|
||||
LoginKeyInitMFADoneTitle = LoginKeyInitMFADone + "Title"
|
||||
LoginKeyInitMFADoneDescription = LoginKeyInitMFADone + "Description"
|
||||
LoginKeyInitMFADoneCancelButtonText = LoginKeyInitMFADone + "CancelButtonText"
|
||||
LoginKeyInitMFADoneNextButtonText = LoginKeyInitMFADone + "NextButtonText"
|
||||
|
||||
LoginKeyMFAProviders = "MFAProvider."
|
||||
LoginKeyMFAProvidersChooseOther = LoginKeyMFAProviders + "ChooseOther"
|
||||
LoginKeyMFAProvidersOTP = LoginKeyMFAProviders + "Provider0"
|
||||
LoginKeyMFAProvidersU2F = LoginKeyMFAProviders + "Provider1"
|
||||
|
||||
LoginKeyVerifyMFAOTP = "VerifyMFAOTP."
|
||||
LoginKeyVerifyMFAOTPTitle = LoginKeyVerifyMFAOTP + "Title"
|
||||
LoginKeyVerifyMFAOTPDescription = LoginKeyVerifyMFAOTP + "Description"
|
||||
LoginKeyVerifyMFAOTPCodeLabel = LoginKeyVerifyMFAOTP + "CodeLabel"
|
||||
LoginKeyVerifyMFAOTPNextButtonText = LoginKeyVerifyMFAOTP + "NextButtonText"
|
||||
|
||||
LoginKeyVerifyMFAU2F = "VerifyMFAU2F."
|
||||
LoginKeyVerifyMFAU2FTitle = LoginKeyVerifyMFAU2F + "Title"
|
||||
LoginKeyVerifyMFAU2FDescription = LoginKeyVerifyMFAU2F + "Description"
|
||||
LoginKeyVerifyMFAU2FNotSupported = LoginKeyVerifyMFAU2F + "NotSupported"
|
||||
LoginKeyVerifyMFAU2FValidateTokenText = LoginKeyVerifyMFAU2F + "ValidateTokenButtonText"
|
||||
LoginKeyVerifyMFAU2FErrorRetry = LoginKeyVerifyMFAU2F + "ErrorRetry"
|
||||
|
||||
LoginKeyPasswordless = "Passwordless."
|
||||
LoginKeyPasswordlessTitle = LoginKeyPasswordless + "Title"
|
||||
LoginKeyPasswordlessDescription = LoginKeyPasswordless + "Description"
|
||||
LoginKeyPasswordlessLoginWithPwButtonText = LoginKeyPasswordless + "LoginWithPwButtonText"
|
||||
LoginKeyPasswordlessValidateTokenButtonText = LoginKeyPasswordless + "ValidateTokenButtonText"
|
||||
LoginKeyPasswordlessNotSupported = LoginKeyPasswordless + "NotSupported"
|
||||
LoginKeyPasswordlessErrorRetry = LoginKeyPasswordless + "ErrorRetry"
|
||||
|
||||
LoginKeyPasswordlessPrompt = "PasswordlessPrompt."
|
||||
LoginKeyPasswordlessPromptTitle = LoginKeyPasswordlessPrompt + "Title"
|
||||
LoginKeyPasswordlessPromptDescription = LoginKeyPasswordlessPrompt + "Description"
|
||||
LoginKeyPasswordlessPromptDescriptionInit = LoginKeyPasswordlessPrompt + "DescriptionInit"
|
||||
LoginKeyPasswordlessPromptPasswordlessButtonText = LoginKeyPasswordlessPrompt + "PasswordlessButtonText"
|
||||
LoginKeyPasswordlessPromptNextButtonText = LoginKeyPasswordlessPrompt + "NextButtonText"
|
||||
LoginKeyPasswordlessPromptSkipButtonText = LoginKeyPasswordlessPrompt + "SkipButtonText"
|
||||
|
||||
LoginKeyPasswordlessRegistration = "PasswordlessRegistration."
|
||||
LoginKeyPasswordlessRegistrationTitle = LoginKeyPasswordlessRegistration + "Title"
|
||||
LoginKeyPasswordlessRegistrationDescription = LoginKeyPasswordlessRegistration + "Description"
|
||||
LoginKeyPasswordlessRegistrationRegisterTokenButtonText = LoginKeyPasswordlessRegistration + "RegisterTokenButtonText"
|
||||
LoginKeyPasswordlessRegistrationTokenNameLabel = LoginKeyPasswordlessRegistration + "TokenNameLabel"
|
||||
LoginKeyPasswordlessRegistrationNotSupported = LoginKeyPasswordlessRegistration + "NotSupported"
|
||||
LoginKeyPasswordlessRegistrationErrorRetry = LoginKeyPasswordlessRegistration + "ErrorRetry"
|
||||
|
||||
LoginKeyPasswordlessRegistrationDone = "PasswordlessRegistrationDone."
|
||||
LoginKeyPasswordlessRegistrationDoneTitle = LoginKeyPasswordlessRegistrationDone + "Title"
|
||||
LoginKeyPasswordlessRegistrationDoneDescription = LoginKeyPasswordlessRegistrationDone + "Description"
|
||||
LoginKeyPasswordlessRegistrationDoneDescriptionClose = LoginKeyPasswordlessRegistrationDone + "DescriptionClose"
|
||||
LoginKeyPasswordlessRegistrationDoneNextButtonText = LoginKeyPasswordlessRegistrationDone + "NextButtonText"
|
||||
LoginKeyPasswordlessRegistrationDoneCancelButtonText = LoginKeyPasswordlessRegistrationDone + "CancelButtonText"
|
||||
|
||||
LoginKeyPasswordChange = "PasswordChange."
|
||||
LoginKeyPasswordChangeTitle = LoginKeyPasswordChange + "Title"
|
||||
LoginKeyPasswordChangeDescription = LoginKeyPasswordChange + "Description"
|
||||
LoginKeyPasswordChangeExpiredDescription = LoginKeyPasswordChange + "ExpiredDescription"
|
||||
LoginKeyPasswordChangeOldPasswordLabel = LoginKeyPasswordChange + "OldPasswordLabel"
|
||||
LoginKeyPasswordChangeNewPasswordLabel = LoginKeyPasswordChange + "NewPasswordLabel"
|
||||
LoginKeyPasswordChangeNewPasswordConfirmLabel = LoginKeyPasswordChange + "NewPasswordConfirmLabel"
|
||||
LoginKeyPasswordChangeCancelButtonText = LoginKeyPasswordChange + "CancelButtonText"
|
||||
LoginKeyPasswordChangeNextButtonText = LoginKeyPasswordChange + "NextButtonText"
|
||||
|
||||
LoginKeyPasswordChangeDone = "PasswordChangeDone."
|
||||
LoginKeyPasswordChangeDoneTitle = LoginKeyPasswordChangeDone + "Title"
|
||||
LoginKeyPasswordChangeDoneDescription = LoginKeyPasswordChangeDone + "Description"
|
||||
LoginKeyPasswordChangeDoneNextButtonText = LoginKeyPasswordChangeDone + "NextButtonText"
|
||||
|
||||
LoginKeyPasswordResetDone = "PasswordResetDone."
|
||||
LoginKeyPasswordResetDoneTitle = LoginKeyPasswordResetDone + "Title"
|
||||
LoginKeyPasswordResetDoneDescription = LoginKeyPasswordResetDone + "Description"
|
||||
LoginKeyPasswordResetDoneNextButtonText = LoginKeyPasswordResetDone + "NextButtonText"
|
||||
|
||||
LoginKeyRegistrationOption = "RegisterOption."
|
||||
LoginKeyRegistrationOptionTitle = LoginKeyRegistrationOption + "Title"
|
||||
LoginKeyRegistrationOptionDescription = LoginKeyRegistrationOption + "Description"
|
||||
LoginKeyRegistrationOptionUserNameButtonText = LoginKeyRegistrationOption + "RegisterUsernamePasswordButtonText"
|
||||
LoginKeyRegistrationOptionExternalLoginDescription = LoginKeyRegistrationOption + "ExternalLoginDescription"
|
||||
LoginKeyRegistrationOptionLoginButtonText = LoginKeyRegistrationOption + "LoginButtonText"
|
||||
|
||||
LoginKeyRegistrationUser = "RegistrationUser."
|
||||
LoginKeyRegistrationUserTitle = LoginKeyRegistrationUser + "Title"
|
||||
LoginKeyRegistrationUserDescription = LoginKeyRegistrationUser + "Description"
|
||||
LoginKeyRegistrationUserDescriptionOrgRegister = LoginKeyRegistrationUser + "DescriptionOrgRegister"
|
||||
LoginKeyRegistrationUserFirstnameLabel = LoginKeyRegistrationUser + "FirstnameLabel"
|
||||
LoginKeyRegistrationUserLastnameLabel = LoginKeyRegistrationUser + "LastnameLabel"
|
||||
LoginKeyRegistrationUserEmailLabel = LoginKeyRegistrationUser + "EmailLabel"
|
||||
LoginKeyRegistrationUserUsernameLabel = LoginKeyRegistrationUser + "UsernameLabel"
|
||||
LoginKeyRegistrationUserLanguageLabel = LoginKeyRegistrationUser + "LanguageLabel"
|
||||
LoginKeyRegistrationUserGenderLabel = LoginKeyRegistrationUser + "GenderLabel"
|
||||
LoginKeyRegistrationUserPasswordLabel = LoginKeyRegistrationUser + "PasswordLabel"
|
||||
LoginKeyRegistrationUserPasswordConfirmLabel = LoginKeyRegistrationUser + "PasswordConfirmLabel"
|
||||
LoginKeyRegistrationUserTOSAndPrivacyLabel = LoginKeyRegistrationUser + "TosAndPrivacyLabel"
|
||||
LoginKeyRegistrationUserTOSConfirm = LoginKeyRegistrationUser + "TosConfirm"
|
||||
LoginKeyRegistrationUserTOSLinkText = LoginKeyRegistrationUser + "TosLinkText"
|
||||
LoginKeyRegistrationUserPrivacyConfirm = LoginKeyRegistrationUser + "PrivacyConfirm"
|
||||
LoginKeyRegistrationUserPrivacyLinkText = LoginKeyRegistrationUser + "PrivacyLinkText"
|
||||
LoginKeyRegistrationUserNextButtonText = LoginKeyRegistrationUser + "NextButtonText"
|
||||
LoginKeyRegistrationUserBackButtonText = LoginKeyRegistrationUser + "BackButtonText"
|
||||
|
||||
LoginKeyExternalRegistrationUserOverview = "ExternalRegistrationUserOverview."
|
||||
LoginKeyExternalRegistrationUserOverviewTitle = LoginKeyExternalRegistrationUserOverview + "Title"
|
||||
LoginKeyExternalRegistrationUserOverviewDescription = LoginKeyExternalRegistrationUserOverview + "Description"
|
||||
LoginKeyExternalRegistrationUserOverviewEmailLabel = LoginKeyExternalRegistrationUserOverview + "EmailLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewUsernameLabel = LoginKeyExternalRegistrationUserOverview + "UsernameLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewFirstnameLabel = LoginKeyExternalRegistrationUserOverview + "FirstnameLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewLastnameLabel = LoginKeyExternalRegistrationUserOverview + "LastnameLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewNicknameLabel = LoginKeyExternalRegistrationUserOverview + "NicknameLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewPhoneLabel = LoginKeyExternalRegistrationUserOverview + "PhoneLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewLanguageLabel = LoginKeyExternalRegistrationUserOverview + "LanguageLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewTOSAndPrivacyLabel = LoginKeyExternalRegistrationUserOverview + "TosAndPrivacyLabel"
|
||||
LoginKeyExternalRegistrationUserOverviewTOSConfirm = LoginKeyExternalRegistrationUserOverview + "TosConfirm"
|
||||
LoginKeyExternalRegistrationUserOverviewTOSLinkText = LoginKeyExternalRegistrationUserOverview + "TosLinkText"
|
||||
LoginKeyExternalRegistrationUserOverviewPrivacyConfirm = LoginKeyExternalRegistrationUserOverview + "PrivacyConfirm"
|
||||
LoginKeyExternalRegistrationUserOverviewPrivacyLinkText = LoginKeyExternalRegistrationUserOverview + "PrivacyLinkText"
|
||||
LoginKeyExternalRegistrationUserOverviewBackButtonText = LoginKeyExternalRegistrationUserOverview + "BackButtonText"
|
||||
LoginKeyExternalRegistrationUserOverviewNextButtonText = LoginKeyExternalRegistrationUserOverview + "NextButtonText"
|
||||
|
||||
LoginKeyRegistrationOrg = "RegistrationOrg."
|
||||
LoginKeyRegisterOrgTitle = LoginKeyRegistrationOrg + "Title"
|
||||
LoginKeyRegisterOrgDescription = LoginKeyRegistrationOrg + "Description"
|
||||
LoginKeyRegisterOrgOrgNameLabel = LoginKeyRegistrationOrg + "OrgNameLabel"
|
||||
LoginKeyRegisterOrgFirstnameLabel = LoginKeyRegistrationOrg + "FirstnameLabel"
|
||||
LoginKeyRegisterOrgLastnameLabel = LoginKeyRegistrationOrg + "LastnameLabel"
|
||||
LoginKeyRegisterOrgUsernameLabel = LoginKeyRegistrationOrg + "UsernameLabel"
|
||||
LoginKeyRegisterOrgEmailLabel = LoginKeyRegistrationOrg + "EmailLabel"
|
||||
LoginKeyRegisterOrgPasswordLabel = LoginKeyRegistrationOrg + "PasswordLabel"
|
||||
LoginKeyRegisterOrgPasswordConfirmLabel = LoginKeyRegistrationOrg + "PasswordConfirmLabel"
|
||||
LoginKeyRegisterOrgTOSAndPrivacyLabel = LoginKeyRegistrationOrg + "TosAndPrivacyLabel"
|
||||
LoginKeyRegisterOrgTOSConfirm = LoginKeyRegistrationOrg + "TosConfirm"
|
||||
LoginKeyRegisterOrgTOSLinkText = LoginKeyRegistrationOrg + "TosLinkText"
|
||||
LoginKeyRegisterOrgPrivacyConfirm = LoginKeyRegistrationOrg + "PrivacyConfirm"
|
||||
LoginKeyRegisterOrgPrivacyLinkText = LoginKeyRegistrationOrg + "PrivacyLinkText"
|
||||
LoginKeyRegisterOrgSaveButtonText = LoginKeyRegistrationOrg + "SaveButtonText"
|
||||
LoginKeyRegisterOrgBackButtonText = LoginKeyRegistrationOrg + "BackButtonText"
|
||||
|
||||
LoginKeyLinkingUserDone = "LinkingUsersDone."
|
||||
LoginKeyLinkingUserDoneTitle = LoginKeyLinkingUserDone + "Title"
|
||||
LoginKeyLinkingUserDoneDescription = LoginKeyLinkingUserDone + "Description"
|
||||
LoginKeyLinkingUserDoneCancelButtonText = LoginKeyLinkingUserDone + "CancelButtonText"
|
||||
LoginKeyLinkingUserDoneNextButtonText = LoginKeyLinkingUserDone + "NextButtonText"
|
||||
|
||||
LoginKeyExternalNotFound = "ExternalNotFound."
|
||||
LoginKeyExternalNotFoundTitle = LoginKeyExternalNotFound + "Title"
|
||||
LoginKeyExternalNotFoundDescription = LoginKeyExternalNotFound + "Description"
|
||||
LoginKeyExternalNotFoundLinkButtonText = LoginKeyExternalNotFound + "LinkButtonText"
|
||||
LoginKeyExternalNotFoundAutoRegisterButtonText = LoginKeyExternalNotFound + "AutoRegisterButtonText"
|
||||
LoginKeyExternalNotFoundTOSAndPrivacyLabel = LoginKeyExternalNotFound + "TosAndPrivacyLabel"
|
||||
LoginKeyExternalNotFoundTOSConfirm = LoginKeyExternalNotFound + "TosConfirm"
|
||||
LoginKeyExternalNotFoundTOSLinkText = LoginKeyExternalNotFound + "TosLinkText"
|
||||
LoginKeyExternalNotFoundPrivacyConfirm = LoginKeyExternalNotFound + "PrivacyConfirm"
|
||||
LoginKeyExternalNotFoundPrivacyLinkText = LoginKeyExternalNotFound + "PrivacyLinkText"
|
||||
|
||||
LoginKeySuccessLogin = "LoginSuccess."
|
||||
LoginKeySuccessLoginTitle = LoginKeySuccessLogin + "Title"
|
||||
LoginKeySuccessLoginAutoRedirectDescription = LoginKeySuccessLogin + "AutoRedirectDescription"
|
||||
LoginKeySuccessLoginRedirectedDescription = LoginKeySuccessLogin + "RedirectedDescription"
|
||||
LoginKeySuccessLoginNextButtonText = LoginKeySuccessLogin + "NextButtonText"
|
||||
|
||||
LoginKeyLogoutDone = "LogoutDone."
|
||||
LoginKeyLogoutDoneTitle = LoginKeyLogoutDone + "Title"
|
||||
LoginKeyLogoutDoneDescription = LoginKeyLogoutDone + "Description"
|
||||
LoginKeyLogoutDoneLoginButtonText = LoginKeyLogoutDone + "LoginButtonText"
|
||||
|
||||
LoginKeyFooter = "Footer."
|
||||
LoginKeyFooterTOS = LoginKeyFooter + "Tos"
|
||||
LoginKeyFooterPrivacyPolicy = LoginKeyFooter + "PrivacyPolicy"
|
||||
LoginKeyFooterHelp = LoginKeyFooter + "Help"
|
||||
LoginKeyFooterSupportEmail = LoginKeyFooter + "SupportEmail"
|
||||
)
|
||||
|
||||
type CustomLoginText struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State PolicyState
|
||||
Default bool
|
||||
Language language.Tag
|
||||
IsDefault bool
|
||||
|
||||
SelectAccount SelectAccountScreenText
|
||||
Login LoginScreenText
|
||||
Password PasswordScreenText
|
||||
UsernameChange UsernameChangeScreenText
|
||||
UsernameChangeDone UsernameChangeDoneScreenText
|
||||
InitPassword InitPasswordScreenText
|
||||
InitPasswordDone InitPasswordDoneScreenText
|
||||
EmailVerification EmailVerificationScreenText
|
||||
EmailVerificationDone EmailVerificationDoneScreenText
|
||||
InitUser InitializeUserScreenText
|
||||
InitUserDone InitializeUserDoneScreenText
|
||||
InitMFAPrompt InitMFAPromptScreenText
|
||||
InitMFAOTP InitMFAOTPScreenText
|
||||
InitMFAU2F InitMFAU2FScreenText
|
||||
InitMFADone InitMFADoneScreenText
|
||||
MFAProvider MFAProvidersText
|
||||
VerifyMFAOTP VerifyMFAOTPScreenText
|
||||
VerifyMFAU2F VerifyMFAU2FScreenText
|
||||
Passwordless PasswordlessScreenText
|
||||
PasswordlessPrompt PasswordlessPromptScreenText
|
||||
PasswordlessRegistration PasswordlessRegistrationScreenText
|
||||
PasswordlessRegistrationDone PasswordlessRegistrationDoneScreenText
|
||||
PasswordChange PasswordChangeScreenText
|
||||
PasswordChangeDone PasswordChangeDoneScreenText
|
||||
PasswordResetDone PasswordResetDoneScreenText
|
||||
RegisterOption RegistrationOptionScreenText
|
||||
RegistrationUser RegistrationUserScreenText
|
||||
ExternalRegistrationUserOverview ExternalRegistrationUserOverviewScreenText
|
||||
RegistrationOrg RegistrationOrgScreenText
|
||||
LinkingUsersDone LinkingUserDoneScreenText
|
||||
ExternalNotFound ExternalUserNotFoundScreenText
|
||||
LoginSuccess SuccessLoginScreenText
|
||||
LogoutDone LogoutDoneScreenText
|
||||
Footer FooterText
|
||||
}
|
||||
|
||||
func (m *CustomLoginText) IsValid(supportedLanguages []language.Tag) error {
|
||||
if err := LanguageIsDefined(m.Language); err != nil {
|
||||
return err
|
||||
}
|
||||
return LanguagesAreSupported(supportedLanguages, m.Language)
|
||||
}
|
||||
|
||||
type SelectAccountScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
TitleLinking string
|
||||
DescriptionLinking string
|
||||
OtherUser string
|
||||
SessionState0 string //active
|
||||
SessionState1 string //inactive
|
||||
MustBeMemberOfOrg string
|
||||
}
|
||||
|
||||
type LoginScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
TitleLinking string
|
||||
DescriptionLinking string
|
||||
LoginNameLabel string
|
||||
UsernamePlaceholder string
|
||||
LoginnamePlaceholder string
|
||||
RegisterButtonText string
|
||||
NextButtonText string
|
||||
ExternalUserDescription string
|
||||
MustBeMemberOfOrg string
|
||||
}
|
||||
|
||||
type PasswordScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
PasswordLabel string
|
||||
ResetLinkText string
|
||||
BackButtonText string
|
||||
NextButtonText string
|
||||
MinLength string
|
||||
HasUppercase string
|
||||
HasLowercase string
|
||||
HasNumber string
|
||||
HasSymbol string
|
||||
Confirmation string
|
||||
}
|
||||
|
||||
type UsernameChangeScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
UsernameLabel string
|
||||
CancelButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type UsernameChangeDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type InitPasswordScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CodeLabel string
|
||||
NewPasswordLabel string
|
||||
NewPasswordConfirmLabel string
|
||||
NextButtonText string
|
||||
ResendButtonText string
|
||||
}
|
||||
|
||||
type InitPasswordDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
NextButtonText string
|
||||
CancelButtonText string
|
||||
}
|
||||
|
||||
type EmailVerificationScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CodeLabel string
|
||||
NextButtonText string
|
||||
ResendButtonText string
|
||||
}
|
||||
|
||||
type EmailVerificationDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
NextButtonText string
|
||||
CancelButtonText string
|
||||
LoginButtonText string
|
||||
}
|
||||
|
||||
type InitializeUserScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CodeLabel string
|
||||
NewPasswordLabel string
|
||||
NewPasswordConfirmLabel string
|
||||
ResendButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type InitializeUserDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CancelButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type InitMFAPromptScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
Provider0 string //OTP
|
||||
Provider1 string //U2F
|
||||
SkipButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type InitMFAOTPScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
OTPDescription string
|
||||
SecretLabel string
|
||||
CodeLabel string
|
||||
NextButtonText string
|
||||
CancelButtonText string
|
||||
}
|
||||
|
||||
type InitMFAU2FScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
TokenNameLabel string
|
||||
RegisterTokenButtonText string
|
||||
NotSupported string
|
||||
ErrorRetry string
|
||||
}
|
||||
|
||||
type InitMFADoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CancelButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type MFAProvidersText struct {
|
||||
ChooseOther string
|
||||
Provider0 string //OTP
|
||||
Provider1 string //U2F
|
||||
}
|
||||
|
||||
type VerifyMFAOTPScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CodeLabel string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type VerifyMFAU2FScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
ValidateTokenButtonText string
|
||||
NotSupported string
|
||||
ErrorRetry string
|
||||
}
|
||||
|
||||
type PasswordlessScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
LoginWithPwButtonText string
|
||||
ValidateTokenButtonText string
|
||||
NotSupported string
|
||||
ErrorRetry string
|
||||
}
|
||||
|
||||
type PasswordChangeScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
ExpiredDescription string
|
||||
OldPasswordLabel string
|
||||
NewPasswordLabel string
|
||||
NewPasswordConfirmLabel string
|
||||
CancelButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type PasswordChangeDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type PasswordResetDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type RegistrationOptionScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
RegisterUsernamePasswordButtonText string
|
||||
ExternalLoginDescription string
|
||||
LoginButtonText string
|
||||
}
|
||||
|
||||
type RegistrationUserScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
DescriptionOrgRegister string
|
||||
FirstnameLabel string
|
||||
LastnameLabel string
|
||||
EmailLabel string
|
||||
UsernameLabel string
|
||||
LanguageLabel string
|
||||
GenderLabel string
|
||||
PasswordLabel string
|
||||
PasswordConfirmLabel string
|
||||
TOSAndPrivacyLabel string
|
||||
TOSConfirm string
|
||||
TOSLinkText string
|
||||
PrivacyConfirm string
|
||||
PrivacyLinkText string
|
||||
NextButtonText string
|
||||
BackButtonText string
|
||||
}
|
||||
|
||||
type ExternalRegistrationUserOverviewScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
EmailLabel string
|
||||
UsernameLabel string
|
||||
FirstnameLabel string
|
||||
LastnameLabel string
|
||||
NicknameLabel string
|
||||
LanguageLabel string
|
||||
PhoneLabel string
|
||||
TOSAndPrivacyLabel string
|
||||
TOSConfirm string
|
||||
TOSLinkText string
|
||||
PrivacyConfirm string
|
||||
PrivacyLinkText string
|
||||
BackButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type RegistrationOrgScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
OrgNameLabel string
|
||||
FirstnameLabel string
|
||||
LastnameLabel string
|
||||
UsernameLabel string
|
||||
EmailLabel string
|
||||
PasswordLabel string
|
||||
PasswordConfirmLabel string
|
||||
TOSAndPrivacyLabel string
|
||||
TOSConfirm string
|
||||
TOSLinkText string
|
||||
PrivacyConfirm string
|
||||
PrivacyLinkText string
|
||||
SaveButtonText string
|
||||
}
|
||||
|
||||
type LinkingUserDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
CancelButtonText string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type ExternalUserNotFoundScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
LinkButtonText string
|
||||
AutoRegisterButtonText string
|
||||
TOSAndPrivacyLabel string
|
||||
TOSConfirm string
|
||||
TOSLinkText string
|
||||
PrivacyConfirm string
|
||||
PrivacyLinkText string
|
||||
}
|
||||
|
||||
type SuccessLoginScreenText struct {
|
||||
Title string
|
||||
AutoRedirectDescription string
|
||||
RedirectedDescription string
|
||||
NextButtonText string
|
||||
}
|
||||
|
||||
type LogoutDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
LoginButtonText string
|
||||
}
|
||||
|
||||
type FooterText struct {
|
||||
TOS string
|
||||
PrivacyPolicy string
|
||||
Help string
|
||||
SupportEmail string
|
||||
}
|
||||
|
||||
type PasswordlessPromptScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
DescriptionInit string
|
||||
PasswordlessButtonText string
|
||||
NextButtonText string
|
||||
SkipButtonText string
|
||||
}
|
||||
|
||||
type PasswordlessRegistrationScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
RegisterTokenButtonText string
|
||||
TokenNameLabel string
|
||||
NotSupported string
|
||||
ErrorRetry string
|
||||
}
|
||||
|
||||
type PasswordlessRegistrationDoneScreenText struct {
|
||||
Title string
|
||||
Description string
|
||||
DescriptionClose string
|
||||
NextButtonText string
|
||||
CancelButtonText string
|
||||
}
|
67
apps/api/internal/domain/custom_message_text.go
Normal file
67
apps/api/internal/domain/custom_message_text.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const (
|
||||
InitCodeMessageType = "InitCode"
|
||||
PasswordResetMessageType = "PasswordReset"
|
||||
VerifyEmailMessageType = "VerifyEmail"
|
||||
VerifyPhoneMessageType = "VerifyPhone"
|
||||
VerifySMSOTPMessageType = "VerifySMSOTP"
|
||||
VerifyEmailOTPMessageType = "VerifyEmailOTP"
|
||||
DomainClaimedMessageType = "DomainClaimed"
|
||||
PasswordlessRegistrationMessageType = "PasswordlessRegistration"
|
||||
PasswordChangeMessageType = "PasswordChange"
|
||||
InviteUserMessageType = "InviteUser"
|
||||
MessageTitle = "Title"
|
||||
MessagePreHeader = "PreHeader"
|
||||
MessageSubject = "Subject"
|
||||
MessageGreeting = "Greeting"
|
||||
MessageText = "Text"
|
||||
MessageButtonText = "ButtonText"
|
||||
MessageFooterText = "Footer"
|
||||
)
|
||||
|
||||
type CustomMessageText struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State PolicyState
|
||||
Default bool
|
||||
MessageTextType string
|
||||
Language language.Tag
|
||||
Title string
|
||||
PreHeader string
|
||||
Subject string
|
||||
Greeting string
|
||||
Text string
|
||||
ButtonText string
|
||||
FooterText string
|
||||
}
|
||||
|
||||
func (m *CustomMessageText) IsValid(supportedLanguages []language.Tag) error {
|
||||
if m.MessageTextType == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "INSTANCE-kd9fs", "Errors.CustomMessageText.Invalid")
|
||||
}
|
||||
if err := LanguageIsDefined(m.Language); err != nil {
|
||||
return err
|
||||
}
|
||||
return LanguagesAreSupported(supportedLanguages, m.Language)
|
||||
}
|
||||
|
||||
func IsMessageTextType(textType string) bool {
|
||||
return textType == InitCodeMessageType ||
|
||||
textType == PasswordResetMessageType ||
|
||||
textType == VerifyEmailMessageType ||
|
||||
textType == VerifyPhoneMessageType ||
|
||||
textType == VerifySMSOTPMessageType ||
|
||||
textType == VerifyEmailOTPMessageType ||
|
||||
textType == DomainClaimedMessageType ||
|
||||
textType == PasswordlessRegistrationMessageType ||
|
||||
textType == PasswordChangeMessageType ||
|
||||
textType == InviteUserMessageType
|
||||
}
|
32
apps/api/internal/domain/custom_text.go
Normal file
32
apps/api/internal/domain/custom_text.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type CustomText struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State CustomTextState
|
||||
Default bool
|
||||
Template string
|
||||
Key string
|
||||
Language language.Tag
|
||||
Text string
|
||||
}
|
||||
|
||||
type CustomTextState int32
|
||||
|
||||
const (
|
||||
CustomTextStateUnspecified CustomTextState = iota
|
||||
CustomTextStateActive
|
||||
CustomTextStateRemoved
|
||||
|
||||
customTextStateCount
|
||||
)
|
||||
|
||||
func (m *CustomText) IsValid() bool {
|
||||
return m.Key != "" && m.Language != language.Und && m.Text != ""
|
||||
}
|
14
apps/api/internal/domain/debug_events.go
Normal file
14
apps/api/internal/domain/debug_events.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package domain
|
||||
|
||||
type DebugEventsState int
|
||||
|
||||
const (
|
||||
DebugEventsStateUnspecified DebugEventsState = iota
|
||||
DebugEventsStateInitial
|
||||
DebugEventsStateChanged
|
||||
DebugEventsStateRemoved
|
||||
)
|
||||
|
||||
func (state DebugEventsState) Exists() bool {
|
||||
return state == DebugEventsStateInitial || state == DebugEventsStateChanged
|
||||
}
|
68
apps/api/internal/domain/device_auth.go
Normal file
68
apps/api/internal/domain/device_auth.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// DeviceAuthState describes the step the
|
||||
// the device authorization process is in.
|
||||
// We generate the Stringer implementation for prettier
|
||||
// log output.
|
||||
//
|
||||
//go:generate stringer -type=DeviceAuthState -linecomment
|
||||
type DeviceAuthState uint
|
||||
|
||||
const (
|
||||
DeviceAuthStateUndefined DeviceAuthState = iota // undefined
|
||||
DeviceAuthStateInitiated // initiated
|
||||
DeviceAuthStateApproved // approved
|
||||
DeviceAuthStateDenied // denied
|
||||
DeviceAuthStateExpired // expired
|
||||
DeviceAuthStateDone // done
|
||||
|
||||
deviceAuthStateCount // invalid
|
||||
)
|
||||
|
||||
// Exists returns true when not Undefined and
|
||||
// any status lower than deviceAuthStateCount.
|
||||
func (s DeviceAuthState) Exists() bool {
|
||||
return s > DeviceAuthStateUndefined && s < deviceAuthStateCount
|
||||
}
|
||||
|
||||
// Done returns true when DeviceAuthState is Approved.
|
||||
// This implements the OIDC interface requirement of "Done"
|
||||
func (s DeviceAuthState) Done() bool {
|
||||
return s == DeviceAuthStateApproved
|
||||
}
|
||||
|
||||
// Denied returns true when DeviceAuthState is Denied, Expired or Removed.
|
||||
// This implements the OIDC interface requirement of "Denied".
|
||||
func (s DeviceAuthState) Denied() bool {
|
||||
return s >= DeviceAuthStateDenied
|
||||
}
|
||||
|
||||
func (s DeviceAuthState) GoString() string {
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
// DeviceAuthCanceled is a subset of DeviceAuthState, allowed to
|
||||
// be used in the deviceauth.CanceledEvent.
|
||||
// The string type is used to make the eventstore more readable
|
||||
// on the reason of cancelation.
|
||||
type DeviceAuthCanceled string
|
||||
|
||||
const (
|
||||
DeviceAuthCanceledDenied = "denied"
|
||||
DeviceAuthCanceledExpired = "expired"
|
||||
)
|
||||
|
||||
func (c DeviceAuthCanceled) State() DeviceAuthState {
|
||||
switch c {
|
||||
case DeviceAuthCanceledDenied:
|
||||
return DeviceAuthStateDenied
|
||||
case DeviceAuthCanceledExpired:
|
||||
return DeviceAuthStateExpired
|
||||
default:
|
||||
return DeviceAuthStateUndefined
|
||||
}
|
||||
}
|
150
apps/api/internal/domain/device_auth_test.go
Normal file
150
apps/api/internal/domain/device_auth_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceAuthState_Exists(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: deviceAuthStateCount,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Exists(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Exists() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Done(t *testing.T) {
|
||||
tests := []struct {
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.s.String(), func(t *testing.T) {
|
||||
if got := tt.s.Done(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Done() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthState_Denied(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s DeviceAuthState
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
s: DeviceAuthStateUndefined,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateInitiated,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateApproved,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateDenied,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
s: DeviceAuthStateExpired,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.s.Denied(); got != tt.want {
|
||||
t.Errorf("DeviceAuthState.Denied() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeviceAuthCanceled_State(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
c DeviceAuthCanceled
|
||||
want DeviceAuthState
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
c: "foo",
|
||||
want: DeviceAuthStateUndefined,
|
||||
},
|
||||
{
|
||||
name: "denied",
|
||||
c: DeviceAuthCanceledDenied,
|
||||
want: DeviceAuthStateDenied,
|
||||
},
|
||||
{
|
||||
name: "expired",
|
||||
c: DeviceAuthCanceledExpired,
|
||||
want: DeviceAuthStateExpired,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.c.State(); got != tt.want {
|
||||
t.Errorf("DeviceAuthCanceled.State() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
29
apps/api/internal/domain/deviceauthstate_string.go
Normal file
29
apps/api/internal/domain/deviceauthstate_string.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Code generated by "stringer -type=DeviceAuthState -linecomment"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[DeviceAuthStateUndefined-0]
|
||||
_ = x[DeviceAuthStateInitiated-1]
|
||||
_ = x[DeviceAuthStateApproved-2]
|
||||
_ = x[DeviceAuthStateDenied-3]
|
||||
_ = x[DeviceAuthStateExpired-4]
|
||||
_ = x[DeviceAuthStateDone-5]
|
||||
_ = x[deviceAuthStateCount-6]
|
||||
}
|
||||
|
||||
const _DeviceAuthState_name = "undefinedinitiatedapproveddeniedexpireddoneinvalid"
|
||||
|
||||
var _DeviceAuthState_index = [...]uint8{0, 9, 18, 26, 32, 39, 43, 50}
|
||||
|
||||
func (i DeviceAuthState) String() string {
|
||||
if i >= DeviceAuthState(len(_DeviceAuthState_index)-1) {
|
||||
return "DeviceAuthState(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _DeviceAuthState_name[_DeviceAuthState_index[i]:_DeviceAuthState_index[i+1]]
|
||||
}
|
47
apps/api/internal/domain/execution.go
Normal file
47
apps/api/internal/domain/execution.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package domain
|
||||
|
||||
type ExecutionType uint
|
||||
|
||||
func (s ExecutionType) Valid() bool {
|
||||
return s < executionTypeStateCount
|
||||
}
|
||||
|
||||
const (
|
||||
ExecutionTypeUnspecified ExecutionType = iota
|
||||
ExecutionTypeRequest
|
||||
ExecutionTypeResponse
|
||||
ExecutionTypeFunction
|
||||
ExecutionTypeEvent
|
||||
|
||||
executionTypeStateCount
|
||||
)
|
||||
|
||||
func (e ExecutionType) String() string {
|
||||
switch e {
|
||||
case ExecutionTypeUnspecified, executionTypeStateCount:
|
||||
return ""
|
||||
case ExecutionTypeRequest:
|
||||
return "request"
|
||||
case ExecutionTypeResponse:
|
||||
return "response"
|
||||
case ExecutionTypeFunction:
|
||||
return "function"
|
||||
case ExecutionTypeEvent:
|
||||
return "event"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ExecutionTargetType uint
|
||||
|
||||
func (s ExecutionTargetType) Valid() bool {
|
||||
return s < executionTargetTypeStateCount
|
||||
}
|
||||
|
||||
const (
|
||||
ExecutionTargetTypeUnspecified ExecutionTargetType = iota
|
||||
ExecutionTargetTypeInclude
|
||||
ExecutionTargetTypeTarget
|
||||
|
||||
executionTargetTypeStateCount
|
||||
)
|
36
apps/api/internal/domain/expiration.go
Normal file
36
apps/api/internal/domain/expiration.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
//most of us won't survive until 12-31-9999 23:59:59, maybe ZITADEL does
|
||||
defaultExpDate = time.Date(9999, time.December, 31, 23, 59, 59, 0, time.UTC)
|
||||
)
|
||||
|
||||
type expiration interface {
|
||||
GetExpirationDate() time.Time
|
||||
SetExpirationDate(time.Time)
|
||||
}
|
||||
|
||||
func EnsureValidExpirationDate(key expiration) error {
|
||||
date, err := ValidateExpirationDate(key.GetExpirationDate())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key.SetExpirationDate(date)
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateExpirationDate(date time.Time) (time.Time, error) {
|
||||
if date.IsZero() {
|
||||
return defaultExpDate, nil
|
||||
}
|
||||
if date.Before(time.Now()) {
|
||||
return time.Time{}, zerrors.ThrowInvalidArgument(nil, "DOMAIN-dv3t5", "Errors.AuthNKey.ExpireBeforeNow")
|
||||
}
|
||||
return date, nil
|
||||
}
|
44
apps/api/internal/domain/factors.go
Normal file
44
apps/api/internal/domain/factors.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package domain
|
||||
|
||||
type SecondFactorType int32
|
||||
|
||||
const (
|
||||
SecondFactorTypeUnspecified SecondFactorType = iota
|
||||
SecondFactorTypeTOTP
|
||||
SecondFactorTypeU2F
|
||||
SecondFactorTypeOTPEmail
|
||||
SecondFactorTypeOTPSMS
|
||||
|
||||
secondFactorCount
|
||||
)
|
||||
|
||||
type MultiFactorType int32
|
||||
|
||||
const (
|
||||
MultiFactorTypeUnspecified MultiFactorType = iota
|
||||
MultiFactorTypeU2FWithPIN
|
||||
|
||||
multiFactorCount
|
||||
)
|
||||
|
||||
type FactorState int32
|
||||
|
||||
const (
|
||||
FactorStateUnspecified FactorState = iota
|
||||
FactorStateActive
|
||||
FactorStateRemoved
|
||||
|
||||
factorStateCount
|
||||
)
|
||||
|
||||
func (f SecondFactorType) Valid() bool {
|
||||
return f > 0 && f < secondFactorCount
|
||||
}
|
||||
|
||||
func (f MultiFactorType) Valid() bool {
|
||||
return f > 0 && f < multiFactorCount
|
||||
}
|
||||
|
||||
func (f FactorState) Valid() bool {
|
||||
return f >= 0 && f < factorStateCount
|
||||
}
|
26
apps/api/internal/domain/feature.go
Normal file
26
apps/api/internal/domain/feature.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package domain
|
||||
|
||||
type Feature int
|
||||
|
||||
func (f Feature) Type() FeatureType {
|
||||
switch f {
|
||||
case FeatureUnspecified:
|
||||
return FeatureTypeUnspecified
|
||||
case FeatureLoginDefaultOrg:
|
||||
return FeatureTypeBoolean
|
||||
default:
|
||||
return FeatureTypeUnspecified
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
FeatureTypeUnspecified FeatureType = iota
|
||||
FeatureTypeBoolean
|
||||
)
|
||||
|
||||
type FeatureType int
|
||||
|
||||
const (
|
||||
FeatureUnspecified Feature = iota
|
||||
FeatureLoginDefaultOrg
|
||||
)
|
37
apps/api/internal/domain/federatedlogout/logout.go
Normal file
37
apps/api/internal/domain/federatedlogout/logout.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package federatedlogout
|
||||
|
||||
type Index int
|
||||
|
||||
const (
|
||||
IndexUnspecified Index = iota
|
||||
IndexRequestID
|
||||
)
|
||||
|
||||
type FederatedLogout struct {
|
||||
InstanceID string
|
||||
FingerPrintID string
|
||||
SessionID string
|
||||
IDPID string
|
||||
UserID string
|
||||
PostLogoutRedirectURI string
|
||||
State State
|
||||
}
|
||||
|
||||
// Keys implements cache.Entry
|
||||
func (c *FederatedLogout) Keys(i Index) []string {
|
||||
if i == IndexRequestID {
|
||||
return []string{Key(c.InstanceID, c.SessionID)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Key(instanceID, sessionID string) string {
|
||||
return instanceID + "-" + sessionID
|
||||
}
|
||||
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateCreated State = iota
|
||||
StateRedirected
|
||||
)
|
151
apps/api/internal/domain/flow.go
Normal file
151
apps/api/internal/domain/flow.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type FlowState int32
|
||||
|
||||
const (
|
||||
FlowStateActive FlowState = iota
|
||||
FlowStateInactive
|
||||
flowStateCount
|
||||
)
|
||||
|
||||
func (s FlowState) Valid() bool {
|
||||
return s >= 0 && s < flowStateCount
|
||||
}
|
||||
|
||||
type FlowType int32
|
||||
|
||||
const (
|
||||
FlowTypeUnspecified FlowType = iota
|
||||
FlowTypeExternalAuthentication
|
||||
FlowTypeCustomiseToken
|
||||
FlowTypeInternalAuthentication
|
||||
FlowTypeCustomizeSAMLResponse
|
||||
flowTypeCount
|
||||
)
|
||||
|
||||
func AllFlowTypes() []FlowType {
|
||||
return []FlowType{
|
||||
FlowTypeExternalAuthentication,
|
||||
FlowTypeCustomiseToken,
|
||||
FlowTypeInternalAuthentication,
|
||||
FlowTypeCustomizeSAMLResponse,
|
||||
}
|
||||
}
|
||||
|
||||
func (s FlowType) Valid() bool {
|
||||
return s > 0 && s < flowTypeCount
|
||||
}
|
||||
|
||||
func (s FlowType) HasTrigger(triggerType TriggerType) bool {
|
||||
for _, trigger := range s.TriggerTypes() {
|
||||
if trigger == triggerType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s FlowType) TriggerTypes() []TriggerType {
|
||||
switch s {
|
||||
case FlowTypeExternalAuthentication:
|
||||
return []TriggerType{
|
||||
TriggerTypePostAuthentication,
|
||||
TriggerTypePreCreation,
|
||||
TriggerTypePostCreation,
|
||||
}
|
||||
case FlowTypeCustomiseToken:
|
||||
return []TriggerType{
|
||||
TriggerTypePreUserinfoCreation,
|
||||
TriggerTypePreAccessTokenCreation,
|
||||
}
|
||||
case FlowTypeInternalAuthentication:
|
||||
return []TriggerType{
|
||||
TriggerTypePostAuthentication,
|
||||
TriggerTypePreCreation,
|
||||
TriggerTypePostCreation,
|
||||
}
|
||||
case FlowTypeCustomizeSAMLResponse:
|
||||
return []TriggerType{
|
||||
TriggerTypePreSAMLResponseCreation,
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (s FlowType) ID() string {
|
||||
if s < 0 && s >= flowTypeCount {
|
||||
return FlowTypeUnspecified.ID()
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
func (s FlowType) LocalizationKey() string {
|
||||
if s < 0 && s >= flowTypeCount {
|
||||
return FlowTypeUnspecified.LocalizationKey()
|
||||
}
|
||||
|
||||
switch s {
|
||||
case FlowTypeExternalAuthentication:
|
||||
return "Action.Flow.Type.ExternalAuthentication"
|
||||
case FlowTypeCustomiseToken:
|
||||
return "Action.Flow.Type.CustomiseToken"
|
||||
case FlowTypeInternalAuthentication:
|
||||
return "Action.Flow.Type.InternalAuthentication"
|
||||
case FlowTypeCustomizeSAMLResponse:
|
||||
return "Action.Flow.Type.CustomizeSAMLResponse"
|
||||
default:
|
||||
return "Action.Flow.Type.Unspecified"
|
||||
}
|
||||
}
|
||||
|
||||
type TriggerType int32
|
||||
|
||||
const (
|
||||
TriggerTypeUnspecified TriggerType = iota
|
||||
TriggerTypePostAuthentication
|
||||
TriggerTypePreCreation
|
||||
TriggerTypePostCreation
|
||||
TriggerTypePreUserinfoCreation
|
||||
TriggerTypePreAccessTokenCreation
|
||||
TriggerTypePreSAMLResponseCreation
|
||||
triggerTypeCount
|
||||
)
|
||||
|
||||
func (s TriggerType) Valid() bool {
|
||||
return s >= 0 && s < triggerTypeCount
|
||||
}
|
||||
|
||||
func (s TriggerType) ID() string {
|
||||
if !s.Valid() {
|
||||
return TriggerTypeUnspecified.ID()
|
||||
}
|
||||
return strconv.Itoa(int(s))
|
||||
}
|
||||
|
||||
func (s TriggerType) LocalizationKey() string {
|
||||
if !s.Valid() {
|
||||
return FlowTypeUnspecified.LocalizationKey()
|
||||
}
|
||||
|
||||
switch s {
|
||||
case TriggerTypePostAuthentication:
|
||||
return "Action.TriggerType.PostAuthentication"
|
||||
case TriggerTypePreCreation:
|
||||
return "Action.TriggerType.PreCreation"
|
||||
case TriggerTypePostCreation:
|
||||
return "Action.TriggerType.PostCreation"
|
||||
case TriggerTypePreUserinfoCreation:
|
||||
return "Action.TriggerType.PreUserinfoCreation"
|
||||
case TriggerTypePreAccessTokenCreation:
|
||||
return "Action.TriggerType.PreAccessTokenCreation"
|
||||
case TriggerTypePreSAMLResponseCreation:
|
||||
return "Action.TriggerType.PreSAMLResponseCreation"
|
||||
default:
|
||||
return "Action.TriggerType.Unspecified"
|
||||
}
|
||||
}
|
142
apps/api/internal/domain/human.go
Normal file
142
apps/api/internal/domain/human.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Human struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Username string
|
||||
State UserState
|
||||
*Password
|
||||
HashedPassword string
|
||||
*Profile
|
||||
*Email
|
||||
*Phone
|
||||
*Address
|
||||
}
|
||||
|
||||
func (h Human) GetUsername() string {
|
||||
return h.Username
|
||||
}
|
||||
|
||||
func (h Human) GetState() UserState {
|
||||
return h.State
|
||||
}
|
||||
|
||||
type InitUserCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
type Gender int32
|
||||
|
||||
const (
|
||||
GenderUnspecified Gender = iota
|
||||
GenderFemale
|
||||
GenderMale
|
||||
GenderDiverse
|
||||
|
||||
genderCount
|
||||
)
|
||||
|
||||
func (f Gender) Valid() bool {
|
||||
return f >= 0 && f < genderCount
|
||||
}
|
||||
|
||||
func (f Gender) Specified() bool {
|
||||
return f > GenderUnspecified && f < genderCount
|
||||
}
|
||||
|
||||
func (u *Human) Normalize() error {
|
||||
if u.Username == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "COMMAND-00p2b", "Errors.User.Username.Empty")
|
||||
}
|
||||
if err := u.Profile.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := u.Email.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Phone != nil && u.Phone.PhoneNumber != "" {
|
||||
if err := u.Phone.Normalize(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) CheckDomainPolicy(policy *DomainPolicy) error {
|
||||
if policy == nil {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "DOMAIN-zSH7j", "Errors.Users.DomainPolicyNil")
|
||||
}
|
||||
if !policy.UserLoginMustBeDomain && u.Profile != nil && u.Username == "" && u.Email != nil {
|
||||
u.Username = string(u.EmailAddress)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) EnsureDisplayName() {
|
||||
if u.Profile == nil {
|
||||
u.Profile = new(Profile)
|
||||
}
|
||||
if u.DisplayName != "" {
|
||||
return
|
||||
}
|
||||
if u.FirstName != "" && u.LastName != "" {
|
||||
u.DisplayName = u.FirstName + " " + u.LastName
|
||||
return
|
||||
}
|
||||
if u.Email != nil && strings.TrimSpace(string(u.Email.EmailAddress)) != "" {
|
||||
u.DisplayName = string(u.Email.EmailAddress)
|
||||
return
|
||||
}
|
||||
u.DisplayName = u.Username
|
||||
}
|
||||
|
||||
func (u *Human) HashPasswordIfExisting(ctx context.Context, policy *PasswordComplexityPolicy, hasher *crypto.Hasher, onetime bool) (err error) {
|
||||
ctx, span := tracing.NewSpan(ctx)
|
||||
defer func() { span.EndWithError(err) }()
|
||||
|
||||
if u.Password != nil {
|
||||
u.Password.ChangeRequired = onetime
|
||||
return u.Password.HashPasswordIfExisting(ctx, policy, hasher)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *Human) IsInitialState(passwordless, externalIDPs bool) bool {
|
||||
if externalIDPs {
|
||||
return false
|
||||
}
|
||||
return u.Email == nil || !u.IsEmailVerified || !passwordless && (u.Password == nil || u.Password.SecretString == "") && u.HashedPassword == ""
|
||||
}
|
||||
|
||||
func NewInitUserCode(generator crypto.Generator) (*InitUserCode, error) {
|
||||
initCodeCrypto, _, err := crypto.NewCode(generator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &InitUserCode{
|
||||
Code: initCodeCrypto,
|
||||
Expiry: generator.Expiry(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func GenerateLoginName(username, domain string, appendDomain bool) string {
|
||||
if !appendDomain {
|
||||
return username
|
||||
}
|
||||
return username + "@" + domain
|
||||
}
|
27
apps/api/internal/domain/human_address.go
Normal file
27
apps/api/internal/domain/human_address.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package domain
|
||||
|
||||
import es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
|
||||
type Address struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Country string
|
||||
Locality string
|
||||
PostalCode string
|
||||
Region string
|
||||
StreetAddress string
|
||||
}
|
||||
|
||||
type AddressState int32
|
||||
|
||||
const (
|
||||
AddressStateUnspecified AddressState = iota
|
||||
AddressStateActive
|
||||
AddressStateRemoved
|
||||
|
||||
addressStateCount
|
||||
)
|
||||
|
||||
func (s AddressState) Valid() bool {
|
||||
return s >= 0 && s < addressStateCount
|
||||
}
|
78
apps/api/internal/domain/human_email.go
Normal file
78
apps/api/internal/domain/human_email.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")
|
||||
)
|
||||
|
||||
type EmailAddress string
|
||||
|
||||
func (e EmailAddress) Validate() error {
|
||||
if e == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")
|
||||
}
|
||||
if !emailRegex.MatchString(string(e)) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "EMAIL-599BI", "Errors.User.Email.Invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e EmailAddress) Normalize() EmailAddress {
|
||||
return EmailAddress(strings.TrimSpace(string(e)))
|
||||
}
|
||||
|
||||
type Email struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
EmailAddress EmailAddress
|
||||
IsEmailVerified bool
|
||||
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||
PlainCode *string
|
||||
}
|
||||
|
||||
type EmailCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func (e *Email) Validate() error {
|
||||
if e == nil {
|
||||
return zerrors.ThrowInvalidArgument(nil, "EMAIL-spblu", "Errors.User.Email.Empty")
|
||||
}
|
||||
return e.EmailAddress.Validate()
|
||||
}
|
||||
|
||||
func NewEmailCode(emailGenerator crypto.Generator) (*EmailCode, string, error) {
|
||||
emailCodeCrypto, code, err := crypto.NewCode(emailGenerator)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return &EmailCode{
|
||||
Code: emailCodeCrypto,
|
||||
Expiry: emailGenerator.Expiry(),
|
||||
}, code, nil
|
||||
}
|
||||
|
||||
type ConfirmURLData struct {
|
||||
UserID string
|
||||
Code string
|
||||
OrgID string
|
||||
}
|
||||
|
||||
// RenderConfirmURLTemplate parses and renders tmpl.
|
||||
// userID, code and orgID are passed into the [ConfirmURLData].
|
||||
func RenderConfirmURLTemplate(w io.Writer, tmpl, userID, code, orgID string) error {
|
||||
return renderURLTemplate(w, tmpl, &ConfirmURLData{userID, code, orgID})
|
||||
}
|
134
apps/api/internal/domain/human_email_test.go
Normal file
134
apps/api/internal/domain/human_email_test.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestEmailValid(t *testing.T) {
|
||||
type args struct {
|
||||
email *Email
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result bool
|
||||
}{
|
||||
{
|
||||
name: "empty email, invalid",
|
||||
args: args{
|
||||
email: &Email{},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "only letters email, invalid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "testemail"},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "nothing after @, invalid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "testemail@"},
|
||||
},
|
||||
result: false,
|
||||
},
|
||||
{
|
||||
name: "email, valid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "testemail@gmail.com"},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "email, valid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "test.email@gmail.com"},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "email, valid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "test/email@gmail.com"},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
{
|
||||
name: "email, valid",
|
||||
args: args{
|
||||
email: &Email{EmailAddress: "test/email@gmail.com"},
|
||||
},
|
||||
result: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := tt.args.email.Validate() == nil
|
||||
if result != tt.result {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderConfirmURLTemplate(t *testing.T) {
|
||||
type args struct {
|
||||
tmpl string
|
||||
userID string
|
||||
code string
|
||||
orgID string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid template",
|
||||
args: args{
|
||||
tmpl: "{{",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-oGh5e", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "execution error",
|
||||
args: args{
|
||||
tmpl: "{{.Foo}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-ieYa7", "Errors.User.InvalidURLTemplate"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{
|
||||
tmpl: "https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}",
|
||||
userID: "user1",
|
||||
code: "123",
|
||||
orgID: "org1",
|
||||
},
|
||||
want: "https://example.com/email/verify?userID=user1&code=123&orgID=org1",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var w strings.Builder
|
||||
err := RenderConfirmURLTemplate(&w, tt.args.tmpl, tt.args.userID, tt.args.code, tt.args.orgID)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.want, w.String())
|
||||
})
|
||||
}
|
||||
}
|
37
apps/api/internal/domain/human_otp.go
Normal file
37
apps/api/internal/domain/human_otp.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type TOTP struct {
|
||||
*ObjectDetails
|
||||
|
||||
Secret string
|
||||
URI string
|
||||
}
|
||||
|
||||
func NewTOTPKey(issuer, accountName string) (*otp.Key, error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{Issuer: issuer, AccountName: accountName})
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInternal(err, "TOTP-ieY3o", "Errors.Internal")
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func VerifyTOTP(code string, secret *crypto.CryptoValue, cryptoAlg crypto.EncryptionAlgorithm) error {
|
||||
decrypt, err := crypto.DecryptString(secret, cryptoAlg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := totp.Validate(code, decrypt)
|
||||
if !valid {
|
||||
return zerrors.ThrowInvalidArgument(nil, "EVENT-8isk2", "Errors.User.MFA.OTP.InvalidCode")
|
||||
}
|
||||
return nil
|
||||
}
|
64
apps/api/internal/domain/human_password.go
Normal file
64
apps/api/internal/domain/human_password.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/telemetry/tracing"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Password struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
SecretString string
|
||||
EncodedSecret string
|
||||
ChangeRequired bool
|
||||
}
|
||||
|
||||
func NewPassword(password string) *Password {
|
||||
return &Password{
|
||||
SecretString: password,
|
||||
}
|
||||
}
|
||||
|
||||
type PasswordCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
NotificationType NotificationType
|
||||
}
|
||||
|
||||
func (p *Password) HashPasswordIfExisting(ctx context.Context, policy *PasswordComplexityPolicy, hasher *crypto.Hasher) error {
|
||||
if p.SecretString == "" {
|
||||
return nil
|
||||
}
|
||||
if policy == nil {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "DOMAIN-s8ifS", "Errors.User.PasswordComplexityPolicy.NotFound")
|
||||
}
|
||||
if err := policy.Check(p.SecretString); err != nil {
|
||||
return err
|
||||
}
|
||||
_, spanHash := tracing.NewNamedSpan(ctx, "passwap.Hash")
|
||||
encoded, err := hasher.Hash(p.SecretString)
|
||||
spanHash.EndWithError(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.EncodedSecret = encoded
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewPasswordCode(passwordGenerator crypto.Generator) (*PasswordCode, error) {
|
||||
passwordCodeCrypto, _, err := crypto.NewCode(passwordGenerator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PasswordCode{
|
||||
Code: passwordCodeCrypto,
|
||||
Expiry: passwordGenerator.Expiry(),
|
||||
}, nil
|
||||
}
|
73
apps/api/internal/domain/human_phone.go
Normal file
73
apps/api/internal/domain/human_phone.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/ttacon/libphonenumber"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
const defaultRegion = "CH"
|
||||
|
||||
type PhoneNumber string
|
||||
|
||||
func (p PhoneNumber) Normalize() (PhoneNumber, error) {
|
||||
if p == "" {
|
||||
return p, zerrors.ThrowInvalidArgument(nil, "PHONE-Zt0NV", "Errors.User.Phone.Empty")
|
||||
}
|
||||
phoneNr, err := libphonenumber.Parse(string(p), defaultRegion)
|
||||
if err != nil {
|
||||
return p, zerrors.ThrowInvalidArgument(err, "PHONE-so0wa", "Errors.User.Phone.Invalid")
|
||||
}
|
||||
return PhoneNumber(libphonenumber.Format(phoneNr, libphonenumber.E164)), nil
|
||||
}
|
||||
|
||||
type Phone struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
PhoneNumber PhoneNumber
|
||||
IsPhoneVerified bool
|
||||
// PlainCode is set by the command and can be used to return it to the caller (API)
|
||||
PlainCode *string
|
||||
}
|
||||
|
||||
type PhoneCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Code *crypto.CryptoValue
|
||||
Expiry time.Duration
|
||||
}
|
||||
|
||||
func (p *Phone) Normalize() error {
|
||||
if p == nil {
|
||||
return zerrors.ThrowInvalidArgument(nil, "PHONE-YlbwO", "Errors.User.Phone.Empty")
|
||||
}
|
||||
normalizedNumber, err := p.PhoneNumber.Normalize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Issue for avoiding mutating state: https://github.com/zitadel/zitadel/issues/5412
|
||||
p.PhoneNumber = normalizedNumber
|
||||
return nil
|
||||
}
|
||||
|
||||
type PhoneState int32
|
||||
|
||||
const (
|
||||
PhoneStateUnspecified PhoneState = iota
|
||||
PhoneStateActive
|
||||
PhoneStateRemoved
|
||||
|
||||
phoneStateCount
|
||||
)
|
||||
|
||||
func (s PhoneState) Valid() bool {
|
||||
return s >= 0 && s < phoneStateCount
|
||||
}
|
||||
|
||||
func (s PhoneState) Exists() bool {
|
||||
return s == PhoneStateActive
|
||||
}
|
106
apps/api/internal/domain/human_phone_test.go
Normal file
106
apps/api/internal/domain/human_phone_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestFormatPhoneNumber(t *testing.T) {
|
||||
type args struct {
|
||||
phone *Phone
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Phone
|
||||
errFunc func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "invalid phone number",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
},
|
||||
errFunc: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "format phone 071...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 0041...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0041711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 071 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "071 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone +4171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "+4171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 004171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format non swiss phone 004371 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004371 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+43711234567",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
normalized, err := tt.args.phone.PhoneNumber.Normalize()
|
||||
if tt.errFunc == nil && tt.result.PhoneNumber != normalized {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result.PhoneNumber, normalized)
|
||||
}
|
||||
if tt.errFunc != nil && !tt.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
41
apps/api/internal/domain/human_profile.go
Normal file
41
apps/api/internal/domain/human_profile.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"golang.org/x/text/language"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Profile struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage language.Tag
|
||||
Gender Gender
|
||||
PreferredLoginName string
|
||||
LoginNames []string
|
||||
}
|
||||
|
||||
func (p *Profile) Validate() error {
|
||||
if p == nil {
|
||||
return zerrors.ThrowInvalidArgument(nil, "PROFILE-GPY3p", "Errors.User.Profile.Empty")
|
||||
}
|
||||
if p.FirstName == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "PROFILE-RF5z2", "Errors.User.Profile.FirstNameEmpty")
|
||||
}
|
||||
if p.LastName == "" {
|
||||
return zerrors.ThrowInvalidArgument(nil, "PROFILE-DSUkN", "Errors.User.Profile.LastNameEmpty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func AvatarURL(prefix, resourceOwner, key string) string {
|
||||
if prefix == "" || resourceOwner == "" || key == "" {
|
||||
return ""
|
||||
}
|
||||
return prefix + "/" + resourceOwner + "/" + key
|
||||
}
|
78
apps/api/internal/domain/human_test.go
Normal file
78
apps/api/internal/domain/human_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHuman_EnsureDisplayName(t *testing.T) {
|
||||
type fields struct {
|
||||
Username string
|
||||
Profile *Profile
|
||||
Email *Email
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"display name set",
|
||||
fields{
|
||||
Username: "username",
|
||||
Profile: &Profile{
|
||||
FirstName: "firstName",
|
||||
LastName: "lastName",
|
||||
DisplayName: "displayName",
|
||||
},
|
||||
Email: &Email{
|
||||
EmailAddress: "email",
|
||||
},
|
||||
},
|
||||
"displayName",
|
||||
},
|
||||
{
|
||||
"first and lastname set",
|
||||
fields{
|
||||
Username: "username",
|
||||
Profile: &Profile{
|
||||
FirstName: "firstName",
|
||||
LastName: "lastName",
|
||||
},
|
||||
Email: &Email{
|
||||
EmailAddress: "email",
|
||||
},
|
||||
},
|
||||
"firstName lastName",
|
||||
},
|
||||
{
|
||||
"profile nil, email set",
|
||||
fields{
|
||||
Username: "username",
|
||||
Email: &Email{
|
||||
EmailAddress: "email",
|
||||
},
|
||||
},
|
||||
"email",
|
||||
},
|
||||
{
|
||||
"email nil, username set",
|
||||
fields{
|
||||
Username: "username",
|
||||
},
|
||||
"username",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u := &Human{
|
||||
Username: tt.fields.Username,
|
||||
Profile: tt.fields.Profile,
|
||||
Email: tt.fields.Email,
|
||||
}
|
||||
u.EnsureDisplayName()
|
||||
assert.Equal(t, tt.want, u.DisplayName)
|
||||
})
|
||||
}
|
||||
}
|
102
apps/api/internal/domain/human_web_auth_n.go
Normal file
102
apps/api/internal/domain/human_web_auth_n.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type WebAuthNToken struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
WebAuthNTokenID string
|
||||
CredentialCreationData []byte
|
||||
State MFAState
|
||||
Challenge string
|
||||
AllowedCredentialIDs [][]byte
|
||||
UserVerification UserVerificationRequirement
|
||||
KeyID []byte
|
||||
PublicKey []byte
|
||||
AttestationType string
|
||||
AAGUID []byte
|
||||
SignCount uint32
|
||||
WebAuthNTokenName string
|
||||
RPID string
|
||||
}
|
||||
|
||||
type WebAuthNLogin struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
CredentialAssertionData []byte
|
||||
Challenge string
|
||||
AllowedCredentialIDs [][]byte
|
||||
UserVerification UserVerificationRequirement
|
||||
RPID string
|
||||
}
|
||||
|
||||
type UserVerificationRequirement int32
|
||||
|
||||
const (
|
||||
UserVerificationRequirementUnspecified UserVerificationRequirement = iota
|
||||
UserVerificationRequirementRequired
|
||||
UserVerificationRequirementPreferred
|
||||
UserVerificationRequirementDiscouraged
|
||||
)
|
||||
|
||||
type AuthenticatorAttachment int32
|
||||
|
||||
const (
|
||||
AuthenticatorAttachmentUnspecified AuthenticatorAttachment = iota
|
||||
AuthenticatorAttachmentPlattform
|
||||
AuthenticatorAttachmentCrossPlattform
|
||||
)
|
||||
|
||||
func GetTokenToVerify(tokens []*WebAuthNToken) (int, *WebAuthNToken) {
|
||||
for i, u2f := range tokens {
|
||||
if u2f.State == MFAStateNotReady {
|
||||
return i, u2f
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func GetTokenByKeyID(tokens []*WebAuthNToken, keyID []byte) (int, *WebAuthNToken) {
|
||||
for i, token := range tokens {
|
||||
if bytes.Compare(token.KeyID, keyID) == 0 {
|
||||
return i, token
|
||||
}
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
type PasswordlessInitCodeState int32
|
||||
|
||||
const (
|
||||
PasswordlessInitCodeStateUnspecified PasswordlessInitCodeState = iota
|
||||
PasswordlessInitCodeStateRequested
|
||||
PasswordlessInitCodeStateActive
|
||||
PasswordlessInitCodeStateRemoved
|
||||
)
|
||||
|
||||
type PasswordlessInitCode struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
CodeID string
|
||||
Code string
|
||||
Expiration time.Duration
|
||||
State PasswordlessInitCodeState
|
||||
}
|
||||
|
||||
func (p *PasswordlessInitCode) Link(baseURL string) string {
|
||||
return PasswordlessInitCodeLink(baseURL, p.AggregateID, p.ResourceOwner, p.CodeID, p.Code)
|
||||
}
|
||||
|
||||
func PasswordlessInitCodeLink(baseURL, userID, resourceOwner, codeID, code string) string {
|
||||
return fmt.Sprintf("%s?userID=%s&orgID=%s&codeID=%s&code=%s", baseURL, userID, resourceOwner, codeID, code)
|
||||
}
|
||||
|
||||
func PasswordlessInitCodeLinkTemplate(baseURL, userID, resourceOwner, codeID string) string {
|
||||
return PasswordlessInitCodeLink(baseURL, userID, resourceOwner, codeID, "{{.Code}}")
|
||||
}
|
146
apps/api/internal/domain/idp.go
Normal file
146
apps/api/internal/domain/idp.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/logging"
|
||||
|
||||
type IDPState int32
|
||||
|
||||
const (
|
||||
IDPStateUnspecified IDPState = iota
|
||||
IDPStateActive
|
||||
IDPStateInactive
|
||||
IDPStateRemoved
|
||||
IDPStateMigrated
|
||||
|
||||
idpStateCount
|
||||
)
|
||||
|
||||
func (s IDPState) Valid() bool {
|
||||
return s >= 0 && s < idpStateCount
|
||||
}
|
||||
|
||||
func (s IDPState) Exists() bool {
|
||||
return s != IDPStateUnspecified && s != IDPStateRemoved && s != IDPStateMigrated
|
||||
}
|
||||
|
||||
type IDPType int32
|
||||
|
||||
const (
|
||||
IDPTypeUnspecified IDPType = iota
|
||||
IDPTypeOIDC
|
||||
IDPTypeJWT
|
||||
IDPTypeOAuth
|
||||
IDPTypeLDAP
|
||||
IDPTypeAzureAD
|
||||
IDPTypeGitHub
|
||||
IDPTypeGitHubEnterprise
|
||||
IDPTypeGitLab
|
||||
IDPTypeGitLabSelfHosted
|
||||
IDPTypeGoogle
|
||||
IDPTypeApple
|
||||
IDPTypeSAML
|
||||
)
|
||||
|
||||
func (t IDPType) GetCSSClass() string {
|
||||
switch t {
|
||||
case IDPTypeGoogle:
|
||||
return "google"
|
||||
case IDPTypeGitHub,
|
||||
IDPTypeGitHubEnterprise:
|
||||
return "github"
|
||||
case IDPTypeGitLab,
|
||||
IDPTypeGitLabSelfHosted:
|
||||
return "gitlab"
|
||||
case IDPTypeAzureAD:
|
||||
return "azure"
|
||||
case IDPTypeApple:
|
||||
return "apple"
|
||||
case IDPTypeUnspecified,
|
||||
IDPTypeOIDC,
|
||||
IDPTypeJWT,
|
||||
IDPTypeOAuth,
|
||||
IDPTypeLDAP,
|
||||
IDPTypeSAML:
|
||||
fallthrough
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func IDPName(name string, idpType IDPType) string {
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
return idpType.DisplayName()
|
||||
}
|
||||
|
||||
// DisplayName returns the name or a default
|
||||
// to be used when always a name must be displayed (e.g. login)
|
||||
func (t IDPType) DisplayName() string {
|
||||
switch t {
|
||||
case IDPTypeGitHub:
|
||||
return "GitHub"
|
||||
case IDPTypeGitLab:
|
||||
return "GitLab"
|
||||
case IDPTypeGoogle:
|
||||
return "Google"
|
||||
case IDPTypeApple:
|
||||
return "Apple"
|
||||
case IDPTypeUnspecified,
|
||||
IDPTypeOIDC,
|
||||
IDPTypeJWT,
|
||||
IDPTypeOAuth,
|
||||
IDPTypeLDAP,
|
||||
IDPTypeAzureAD,
|
||||
IDPTypeGitHubEnterprise,
|
||||
IDPTypeGitLabSelfHosted,
|
||||
IDPTypeSAML:
|
||||
fallthrough
|
||||
default:
|
||||
// we should never get here, so log it
|
||||
logging.Errorf("name of provider (type %d) is empty", t)
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// IsSignInButton returns if the button should be displayed with a translated
|
||||
// "Sign in with {{.DisplayName}}", e.g. "Sign in with Apple"
|
||||
func (t IDPType) IsSignInButton() bool {
|
||||
return t == IDPTypeApple
|
||||
}
|
||||
|
||||
type IDPIntentState int32
|
||||
|
||||
const (
|
||||
IDPIntentStateUnspecified IDPIntentState = iota
|
||||
IDPIntentStateStarted
|
||||
IDPIntentStateSucceeded
|
||||
IDPIntentStateFailed
|
||||
IDPIntentStateConsumed
|
||||
|
||||
idpIntentStateCount
|
||||
)
|
||||
|
||||
func (s IDPIntentState) Valid() bool {
|
||||
return s >= 0 && s < idpIntentStateCount
|
||||
}
|
||||
|
||||
func (s IDPIntentState) Exists() bool {
|
||||
return s != IDPIntentStateUnspecified && s != IDPIntentStateFailed //TODO: ?
|
||||
}
|
||||
|
||||
type AutoLinkingOption uint8
|
||||
|
||||
const (
|
||||
AutoLinkingOptionUnspecified AutoLinkingOption = iota
|
||||
AutoLinkingOptionUsername
|
||||
AutoLinkingOptionEmail
|
||||
)
|
||||
|
||||
type SAMLNameIDFormat uint8
|
||||
|
||||
const (
|
||||
SAMLNameIDFormatUnspecified SAMLNameIDFormat = iota
|
||||
SAMLNameIDFormatEmailAddress
|
||||
SAMLNameIDFormatPersistent
|
||||
SAMLNameIDFormatTransient
|
||||
)
|
133
apps/api/internal/domain/idp_config.go
Normal file
133
apps/api/internal/domain/idp_config.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type IDPConfig struct {
|
||||
es_models.ObjectRoot
|
||||
IDPConfigID string
|
||||
Type IDPConfigType
|
||||
Name string
|
||||
StylingType IDPConfigStylingType
|
||||
State IDPConfigState
|
||||
OIDCConfig *OIDCIDPConfig
|
||||
JWTConfig *JWTIDPConfig
|
||||
AutoRegister bool
|
||||
}
|
||||
|
||||
type IDPConfigView struct {
|
||||
AggregateID string
|
||||
IDPConfigID string
|
||||
Name string
|
||||
StylingType IDPConfigStylingType
|
||||
State IDPConfigState
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
Sequence uint64
|
||||
IDPProviderType IdentityProviderType
|
||||
AutoRegister bool
|
||||
|
||||
IsOIDC bool
|
||||
OIDCClientID string
|
||||
OIDCClientSecret *crypto.CryptoValue
|
||||
OIDCIssuer string
|
||||
OIDCScopes []string
|
||||
OIDCIDPDisplayNameMapping OIDCMappingField
|
||||
OIDCUsernameMapping OIDCMappingField
|
||||
OAuthAuthorizationEndpoint string
|
||||
OAuthTokenEndpoint string
|
||||
|
||||
JWTEndpoint string
|
||||
JWTIssuer string
|
||||
JWTKeysEndpoint string
|
||||
}
|
||||
|
||||
type OIDCIDPConfig struct {
|
||||
es_models.ObjectRoot
|
||||
IDPConfigID string
|
||||
ClientID string
|
||||
ClientSecret *crypto.CryptoValue
|
||||
ClientSecretString string
|
||||
Issuer string
|
||||
AuthorizationEndpoint string
|
||||
TokenEndpoint string
|
||||
Scopes []string
|
||||
IDPDisplayNameMapping OIDCMappingField
|
||||
UsernameMapping OIDCMappingField
|
||||
}
|
||||
|
||||
type JWTIDPConfig struct {
|
||||
es_models.ObjectRoot
|
||||
IDPConfigID string
|
||||
JWTEndpoint string
|
||||
Issuer string
|
||||
KeysEndpoint string
|
||||
HeaderName string
|
||||
}
|
||||
|
||||
// IDPConfigType
|
||||
// Deprecated: use [IDPType]
|
||||
type IDPConfigType int32
|
||||
|
||||
const (
|
||||
IDPConfigTypeOIDC IDPConfigType = iota
|
||||
IDPConfigTypeSAML
|
||||
IDPConfigTypeJWT
|
||||
|
||||
//count is for validation
|
||||
idpConfigTypeCount
|
||||
IDPConfigTypeUnspecified IDPConfigType = -1
|
||||
)
|
||||
|
||||
func (f IDPConfigType) Valid() bool {
|
||||
return f >= 0 && f < idpConfigTypeCount
|
||||
}
|
||||
|
||||
// IDPConfigState
|
||||
// Deprecated: use [IDPStateType]
|
||||
type IDPConfigState int32
|
||||
|
||||
const (
|
||||
IDPConfigStateUnspecified IDPConfigState = iota
|
||||
IDPConfigStateActive
|
||||
IDPConfigStateInactive
|
||||
IDPConfigStateRemoved
|
||||
|
||||
idpConfigStateCount
|
||||
)
|
||||
|
||||
func (s IDPConfigState) Valid() bool {
|
||||
return s >= 0 && s < idpConfigStateCount
|
||||
}
|
||||
|
||||
func (s IDPConfigState) Exists() bool {
|
||||
return s != IDPConfigStateUnspecified && s != IDPConfigStateRemoved
|
||||
}
|
||||
|
||||
// IDPConfigStylingType
|
||||
// Deprecated: use a concrete provider
|
||||
type IDPConfigStylingType int32
|
||||
|
||||
const (
|
||||
IDPConfigStylingTypeUnspecified IDPConfigStylingType = iota
|
||||
IDPConfigStylingTypeGoogle
|
||||
|
||||
idpConfigStylingTypeCount
|
||||
)
|
||||
|
||||
func (f IDPConfigStylingType) Valid() bool {
|
||||
return f >= 0 && f < idpConfigStylingTypeCount
|
||||
}
|
||||
|
||||
func (st IDPConfigStylingType) GetCSSClass() string {
|
||||
switch st {
|
||||
case IDPConfigStylingTypeGoogle:
|
||||
return "google"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
23
apps/api/internal/domain/instance.go
Normal file
23
apps/api/internal/domain/instance.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
IAMID = "IAM"
|
||||
)
|
||||
|
||||
type InstanceState int32
|
||||
|
||||
const (
|
||||
InstanceStateUnspecified InstanceState = iota
|
||||
InstanceStateActive
|
||||
InstanceStateRemoved
|
||||
|
||||
instanceStateCount
|
||||
)
|
||||
|
||||
func (s InstanceState) Valid() bool {
|
||||
return s >= 0 && s < instanceStateCount
|
||||
}
|
||||
|
||||
func (s InstanceState) Exists() bool {
|
||||
return s != InstanceStateUnspecified && s != InstanceStateRemoved
|
||||
}
|
38
apps/api/internal/domain/instance_domain.go
Normal file
38
apps/api/internal/domain/instance_domain.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
)
|
||||
|
||||
var (
|
||||
domainRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
|
||||
)
|
||||
|
||||
type InstanceDomainState int32
|
||||
|
||||
const (
|
||||
InstanceDomainStateUnspecified InstanceDomainState = iota
|
||||
InstanceDomainStateActive
|
||||
InstanceDomainStateRemoved
|
||||
|
||||
instanceDomainStateCount
|
||||
)
|
||||
|
||||
func (f InstanceDomainState) Valid() bool {
|
||||
return f >= 0 && f < instanceDomainStateCount
|
||||
}
|
||||
|
||||
func (f InstanceDomainState) Exists() bool {
|
||||
return f == InstanceDomainStateActive
|
||||
}
|
||||
|
||||
func NewGeneratedInstanceDomain(instanceName, iamDomain string) (string, error) {
|
||||
randomString, err := crypto.GenerateRandomString(6, domainRunes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
instanceName = strings.TrimSpace(instanceName)
|
||||
return strings.ToLower(strings.ReplaceAll(instanceName, " ", "-") + "-" + randomString + "." + iamDomain), nil
|
||||
}
|
34
apps/api/internal/domain/key_pair.go
Normal file
34
apps/api/internal/domain/key_pair.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type KeyPair struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
Usage crypto.KeyUsage
|
||||
Algorithm string
|
||||
PrivateKey *Key
|
||||
PublicKey *Key
|
||||
Certificate *Key
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
Key *crypto.CryptoValue
|
||||
Expiry time.Time
|
||||
}
|
||||
|
||||
func (k *KeyPair) IsValid() bool {
|
||||
return k.Algorithm != "" &&
|
||||
k.PrivateKey != nil && k.PrivateKey.IsValid() &&
|
||||
k.PublicKey != nil && k.PublicKey.IsValid() &&
|
||||
k.Certificate != nil && k.Certificate.IsValid()
|
||||
}
|
||||
|
||||
func (k *Key) IsValid() bool {
|
||||
return k.Key != nil
|
||||
}
|
130
apps/api/internal/domain/language.go
Normal file
130
apps/api/internal/domain/language.go
Normal file
@@ -0,0 +1,130 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func StringsToLanguages(langs []string) []language.Tag {
|
||||
return GenericMapSlice(langs, language.Make)
|
||||
}
|
||||
|
||||
func LanguagesToStrings(langs []language.Tag) []string {
|
||||
return GenericMapSlice(langs, func(lang language.Tag) string { return lang.String() })
|
||||
}
|
||||
|
||||
func GenericMapSlice[T any, U any](from []T, mapTo func(T) U) []U {
|
||||
if from == nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]U, len(from))
|
||||
for i, lang := range from {
|
||||
result[i] = mapTo(lang)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LanguagesDiffer returns true if the languages differ.
|
||||
func LanguagesDiffer(left, right []language.Tag) bool {
|
||||
if left == nil && right == nil {
|
||||
return false
|
||||
}
|
||||
if left == nil || right == nil || len(left) != len(right) {
|
||||
return true
|
||||
}
|
||||
return !languagesAreContained(left, right)
|
||||
}
|
||||
|
||||
func LanguageIsAllowed(allowUndefined bool, allowedLanguages []language.Tag, lang language.Tag) error {
|
||||
err := LanguageIsDefined(lang)
|
||||
if err != nil && allowUndefined {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(allowedLanguages) > 0 && !languageIsContained(allowedLanguages, lang) {
|
||||
return zerrors.ThrowPreconditionFailed(nil, "LANG-2M9fs", "Errors.Language.NotAllowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LanguagesAreSupported(supportedLanguages []language.Tag, lang ...language.Tag) error {
|
||||
unsupported := make([]language.Tag, 0)
|
||||
for _, l := range lang {
|
||||
if l.IsRoot() {
|
||||
continue
|
||||
}
|
||||
if !languageIsContained(supportedLanguages, l) {
|
||||
unsupported = append(unsupported, l)
|
||||
}
|
||||
}
|
||||
if len(unsupported) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(unsupported) == 1 {
|
||||
return zerrors.ThrowInvalidArgument(nil, "LANG-lg4DP", "Errors.Language.NotSupported")
|
||||
}
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.NotSupported: %s", LanguagesToStrings(unsupported))
|
||||
}
|
||||
|
||||
func LanguageIsDefined(lang language.Tag) error {
|
||||
if lang.IsRoot() {
|
||||
return zerrors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Undefined")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LanguagesHaveDuplicates returns an error if the passed slices contains duplicates.
|
||||
// The error lists the duplicates.
|
||||
func LanguagesHaveDuplicates(langs []language.Tag) error {
|
||||
unique := make(map[language.Tag]struct{})
|
||||
duplicates := make([]language.Tag, 0)
|
||||
for _, lang := range langs {
|
||||
if _, ok := unique[lang]; ok {
|
||||
duplicates = append(duplicates, lang)
|
||||
}
|
||||
unique[lang] = struct{}{}
|
||||
}
|
||||
if len(duplicates) == 0 {
|
||||
return nil
|
||||
}
|
||||
if len(duplicates) > 1 {
|
||||
return zerrors.ThrowInvalidArgument(nil, "LANG-3M9f2", "Errors.Language.Duplicate")
|
||||
}
|
||||
return zerrors.ThrowInvalidArgumentf(nil, "LANG-XHiK5", "Errors.Languages.Duplicate: %s", LanguagesToStrings(duplicates))
|
||||
}
|
||||
|
||||
func ParseLanguage(lang ...string) (tags []language.Tag, err error) {
|
||||
tags = make([]language.Tag, len(lang))
|
||||
for i := range lang {
|
||||
var parseErr error
|
||||
tags[i], parseErr = language.Parse(lang[i])
|
||||
err = errors.Join(err, parseErr)
|
||||
}
|
||||
if err != nil {
|
||||
err = zerrors.ThrowInvalidArgument(err, "LANG-jc8Sq", "Errors.Language.NotParsed")
|
||||
}
|
||||
return tags, err
|
||||
}
|
||||
|
||||
func languagesAreContained(languages, search []language.Tag) bool {
|
||||
for _, s := range search {
|
||||
if !languageIsContained(languages, s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func languageIsContained(languages []language.Tag, search language.Tag) bool {
|
||||
for _, lang := range languages {
|
||||
if lang == search {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
24
apps/api/internal/domain/machine.go
Normal file
24
apps/api/internal/domain/machine.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
|
||||
type Machine struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Username string
|
||||
State UserState
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (m Machine) GetUsername() string {
|
||||
return m.Username
|
||||
}
|
||||
|
||||
func (m Machine) GetState() UserState {
|
||||
return m.State
|
||||
}
|
||||
|
||||
func (sa *Machine) IsValid() bool {
|
||||
return sa.Name != "" && sa.Username != ""
|
||||
}
|
76
apps/api/internal/domain/machine_key.go
Normal file
76
apps/api/internal/domain/machine_key.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type MachineKey struct {
|
||||
models.ObjectRoot
|
||||
|
||||
KeyID string
|
||||
Type AuthNKeyType
|
||||
ExpirationDate time.Time
|
||||
PrivateKey []byte
|
||||
PublicKey []byte
|
||||
}
|
||||
|
||||
func (key *MachineKey) setPublicKey(publicKey []byte) {
|
||||
key.PublicKey = publicKey
|
||||
}
|
||||
|
||||
func (key *MachineKey) setPrivateKey(privateKey []byte) {
|
||||
key.PrivateKey = privateKey
|
||||
}
|
||||
|
||||
func (key *MachineKey) expirationDate() time.Time {
|
||||
return key.ExpirationDate
|
||||
}
|
||||
|
||||
func (key *MachineKey) setExpirationDate(expiration time.Time) {
|
||||
key.ExpirationDate = expiration
|
||||
}
|
||||
|
||||
func (key *MachineKey) Detail() ([]byte, error) {
|
||||
if key.Type == AuthNKeyTypeJSON {
|
||||
return key.MarshalJSON()
|
||||
}
|
||||
return nil, zerrors.ThrowPreconditionFailed(nil, "KEY-dsg52", "Errors.Internal")
|
||||
}
|
||||
|
||||
func (key *MachineKey) MarshalJSON() ([]byte, error) {
|
||||
return MachineKeyMarshalJSON(key.KeyID, key.PrivateKey, key.ExpirationDate, key.AggregateID)
|
||||
}
|
||||
|
||||
type MachineKeyState int32
|
||||
|
||||
const (
|
||||
MachineKeyStateUnspecified MachineKeyState = iota
|
||||
MachineKeyStateActive
|
||||
MachineKeyStateRemoved
|
||||
|
||||
machineKeyStateCount
|
||||
)
|
||||
|
||||
func (f MachineKeyState) Valid() bool {
|
||||
return f >= 0 && f < machineKeyStateCount
|
||||
}
|
||||
|
||||
func MachineKeyMarshalJSON(keyID string, privateKey []byte, expirationDate time.Time, userID string) ([]byte, error) {
|
||||
return json.Marshal(struct {
|
||||
Type string `json:"type"`
|
||||
KeyID string `json:"keyId"`
|
||||
Key string `json:"key"`
|
||||
ExpirationDate time.Time `json:"expirationDate"`
|
||||
UserID string `json:"userId"`
|
||||
}{
|
||||
Type: "serviceaccount",
|
||||
KeyID: keyID,
|
||||
Key: string(privateKey),
|
||||
ExpirationDate: expirationDate,
|
||||
UserID: userID,
|
||||
})
|
||||
}
|
44
apps/api/internal/domain/member.go
Normal file
44
apps/api/internal/domain/member.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type Member struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
UserID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func NewMember(aggregateID, userID string, roles ...string) *Member {
|
||||
return &Member{
|
||||
ObjectRoot: es_models.ObjectRoot{
|
||||
AggregateID: aggregateID,
|
||||
},
|
||||
UserID: userID,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
|
||||
func (i *Member) IsValid() bool {
|
||||
return i.AggregateID != "" && i.UserID != "" && len(i.Roles) != 0
|
||||
}
|
||||
|
||||
type MemberState int32
|
||||
|
||||
const (
|
||||
MemberStateUnspecified MemberState = iota
|
||||
MemberStateActive
|
||||
MemberStateRemoved
|
||||
|
||||
memberStateCount
|
||||
)
|
||||
|
||||
func (f MemberState) Valid() bool {
|
||||
return f >= 0 && f < memberStateCount
|
||||
}
|
||||
|
||||
func (f MemberState) Exists() bool {
|
||||
return f != MemberStateRemoved && f != MemberStateUnspecified
|
||||
}
|
83
apps/api/internal/domain/metadata.go
Normal file
83
apps/api/internal/domain/metadata.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type Metadata struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
State MetadataState
|
||||
Key string
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type MetadataState int32
|
||||
|
||||
const (
|
||||
MetadataStateUnspecified MetadataState = iota
|
||||
MetadataStateActive
|
||||
MetadataStateRemoved
|
||||
)
|
||||
|
||||
func (m *Metadata) IsValid() bool {
|
||||
return m.Key != "" && len(m.Value) > 0
|
||||
}
|
||||
|
||||
func (s MetadataState) Exists() bool {
|
||||
return s != MetadataStateUnspecified && s != MetadataStateRemoved
|
||||
}
|
||||
|
||||
type MetadataSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn MetadataSearchKey
|
||||
Asc bool
|
||||
Queries []*MetadataSearchQuery
|
||||
}
|
||||
|
||||
type MetadataSearchKey int32
|
||||
|
||||
const (
|
||||
MetadataSearchKeyUnspecified MetadataSearchKey = iota
|
||||
MetadataSearchKeyAggregateID
|
||||
MetadataSearchKeyResourceOwner
|
||||
MetadataSearchKeyKey
|
||||
MetadataSearchKeyValue
|
||||
)
|
||||
|
||||
type MetadataSearchQuery struct {
|
||||
Key MetadataSearchKey
|
||||
Method SearchMethod
|
||||
Value interface{}
|
||||
}
|
||||
|
||||
type MetadataSearchResponse struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*Metadata
|
||||
Sequence uint64
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
func (r *MetadataSearchRequest) EnsureLimit(limit uint64) error {
|
||||
if r.Limit > limit {
|
||||
return zerrors.ThrowInvalidArgument(nil, "SEARCH-0ds32", "Errors.Limit.ExceedsDefault")
|
||||
}
|
||||
if r.Limit == 0 {
|
||||
r.Limit = limit
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *MetadataSearchRequest) AppendAggregateIDQuery(aggregateID string) {
|
||||
r.Queries = append(r.Queries, &MetadataSearchQuery{Key: MetadataSearchKeyAggregateID, Method: SearchMethodEquals, Value: aggregateID})
|
||||
}
|
||||
|
||||
func (r *MetadataSearchRequest) AppendResourceOwnerQuery(resourceOwner string) {
|
||||
r.Queries = append(r.Queries, &MetadataSearchQuery{Key: MetadataSearchKeyResourceOwner, Method: SearchMethodEquals, Value: resourceOwner})
|
||||
}
|
23
apps/api/internal/domain/mfa.go
Normal file
23
apps/api/internal/domain/mfa.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/crypto"
|
||||
|
||||
type MFAState int32
|
||||
|
||||
const (
|
||||
MFAStateUnspecified MFAState = iota
|
||||
MFAStateNotReady
|
||||
MFAStateReady
|
||||
MFAStateRemoved
|
||||
|
||||
stateCount
|
||||
)
|
||||
|
||||
type MultifactorConfigs struct {
|
||||
OTP OTPConfig
|
||||
}
|
||||
|
||||
type OTPConfig struct {
|
||||
Issuer string
|
||||
CryptoMFA crypto.EncryptionAlgorithm
|
||||
}
|
22
apps/api/internal/domain/mock/permission.go
Normal file
22
apps/api/internal/domain/mock/permission.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package permissionmock
|
||||
|
||||
import (
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/domain"
|
||||
)
|
||||
|
||||
// MockPermissionCheckErr returns a permission check function that will fail
|
||||
// and return the input error
|
||||
func MockPermissionCheckErr(err error) domain.PermissionCheck {
|
||||
return func(_ context.Context, _, _, _ string) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// MockPermissionCheckOK returns a permission check function that will succeed
|
||||
func MockPermissionCheckOK() domain.PermissionCheck {
|
||||
return func(_ context.Context, _, _, _ string) (err error) {
|
||||
return nil
|
||||
}
|
||||
}
|
200
apps/api/internal/domain/next_step.go
Normal file
200
apps/api/internal/domain/next_step.go
Normal file
@@ -0,0 +1,200 @@
|
||||
package domain
|
||||
|
||||
type NextStep interface {
|
||||
Type() NextStepType
|
||||
}
|
||||
|
||||
type NextStepType int32
|
||||
|
||||
const (
|
||||
NextStepUnspecified NextStepType = iota
|
||||
NextStepLogin
|
||||
NextStepUserSelection
|
||||
NextStepInitUser
|
||||
NextStepPassword
|
||||
NextStepChangePassword
|
||||
NextStepInitPassword
|
||||
NextStepVerifyEmail
|
||||
NextStepMFAPrompt
|
||||
NextStepMFAVerify
|
||||
NextStepRedirectToCallback
|
||||
NextStepChangeUsername
|
||||
NextStepLinkUsers
|
||||
NextStepExternalNotFoundOption
|
||||
NextStepExternalLogin
|
||||
NextStepGrantRequired
|
||||
NextStepPasswordless
|
||||
NextStepPasswordlessRegistrationPrompt
|
||||
NextStepRegistration
|
||||
NextStepProjectRequired
|
||||
NextStepRedirectToExternalIDP
|
||||
NextStepLoginSucceeded
|
||||
NextStepVerifyInvite
|
||||
)
|
||||
|
||||
type LoginStep struct{}
|
||||
|
||||
func (s *LoginStep) Type() NextStepType {
|
||||
return NextStepLogin
|
||||
}
|
||||
|
||||
type RegistrationStep struct{}
|
||||
|
||||
func (s *RegistrationStep) Type() NextStepType {
|
||||
return NextStepRegistration
|
||||
}
|
||||
|
||||
type SelectUserStep struct {
|
||||
Users []UserSelection
|
||||
}
|
||||
|
||||
func (s *SelectUserStep) Type() NextStepType {
|
||||
return NextStepUserSelection
|
||||
}
|
||||
|
||||
type UserSelection struct {
|
||||
UserID string
|
||||
UserName string
|
||||
DisplayName string
|
||||
LoginName string
|
||||
UserSessionState UserSessionState
|
||||
SelectionPossible bool
|
||||
AvatarKey string
|
||||
ResourceOwner string
|
||||
}
|
||||
|
||||
type UserSessionState int32
|
||||
|
||||
const (
|
||||
UserSessionStateActive UserSessionState = iota
|
||||
UserSessionStateTerminated
|
||||
)
|
||||
|
||||
type RedirectToExternalIDPStep struct{}
|
||||
|
||||
func (s *RedirectToExternalIDPStep) Type() NextStepType {
|
||||
return NextStepRedirectToExternalIDP
|
||||
}
|
||||
|
||||
type InitUserStep struct {
|
||||
PasswordSet bool
|
||||
}
|
||||
|
||||
func (s *InitUserStep) Type() NextStepType {
|
||||
return NextStepInitUser
|
||||
}
|
||||
|
||||
type ExternalNotFoundOptionStep struct{}
|
||||
|
||||
func (s *ExternalNotFoundOptionStep) Type() NextStepType {
|
||||
return NextStepExternalNotFoundOption
|
||||
}
|
||||
|
||||
type PasswordStep struct{}
|
||||
|
||||
func (s *PasswordStep) Type() NextStepType {
|
||||
return NextStepPassword
|
||||
}
|
||||
|
||||
type ExternalLoginStep struct {
|
||||
SelectedIDPConfigID string
|
||||
}
|
||||
|
||||
func (s *ExternalLoginStep) Type() NextStepType {
|
||||
return NextStepExternalLogin
|
||||
}
|
||||
|
||||
type PasswordlessStep struct {
|
||||
PasswordSet bool
|
||||
}
|
||||
|
||||
func (s *PasswordlessStep) Type() NextStepType {
|
||||
return NextStepPasswordless
|
||||
}
|
||||
|
||||
type PasswordlessRegistrationPromptStep struct{}
|
||||
|
||||
func (s *PasswordlessRegistrationPromptStep) Type() NextStepType {
|
||||
return NextStepPasswordlessRegistrationPrompt
|
||||
}
|
||||
|
||||
type ChangePasswordStep struct {
|
||||
Expired bool
|
||||
}
|
||||
|
||||
func (s *ChangePasswordStep) Type() NextStepType {
|
||||
return NextStepChangePassword
|
||||
}
|
||||
|
||||
type InitPasswordStep struct{}
|
||||
|
||||
func (s *InitPasswordStep) Type() NextStepType {
|
||||
return NextStepInitPassword
|
||||
}
|
||||
|
||||
type ChangeUsernameStep struct{}
|
||||
|
||||
func (s *ChangeUsernameStep) Type() NextStepType {
|
||||
return NextStepChangeUsername
|
||||
}
|
||||
|
||||
type VerifyEMailStep struct {
|
||||
InitPassword bool
|
||||
}
|
||||
|
||||
func (s *VerifyEMailStep) Type() NextStepType {
|
||||
return NextStepVerifyEmail
|
||||
}
|
||||
|
||||
type MFAPromptStep struct {
|
||||
Required bool
|
||||
MFAProviders []MFAType
|
||||
}
|
||||
|
||||
func (s *MFAPromptStep) Type() NextStepType {
|
||||
return NextStepMFAPrompt
|
||||
}
|
||||
|
||||
type MFAVerificationStep struct {
|
||||
MFAProviders []MFAType
|
||||
}
|
||||
|
||||
func (s *MFAVerificationStep) Type() NextStepType {
|
||||
return NextStepMFAVerify
|
||||
}
|
||||
|
||||
type LinkUsersStep struct{}
|
||||
|
||||
func (s *LinkUsersStep) Type() NextStepType {
|
||||
return NextStepLinkUsers
|
||||
}
|
||||
|
||||
type GrantRequiredStep struct{}
|
||||
|
||||
func (s *GrantRequiredStep) Type() NextStepType {
|
||||
return NextStepGrantRequired
|
||||
}
|
||||
|
||||
type ProjectRequiredStep struct{}
|
||||
|
||||
func (s *ProjectRequiredStep) Type() NextStepType {
|
||||
return NextStepProjectRequired
|
||||
}
|
||||
|
||||
type RedirectToCallbackStep struct{}
|
||||
|
||||
func (s *RedirectToCallbackStep) Type() NextStepType {
|
||||
return NextStepRedirectToCallback
|
||||
}
|
||||
|
||||
type LoginSucceededStep struct{}
|
||||
|
||||
func (s *LoginSucceededStep) Type() NextStepType {
|
||||
return NextStepLoginSucceeded
|
||||
}
|
||||
|
||||
type VerifyInviteStep struct{}
|
||||
|
||||
func (s *VerifyInviteStep) Type() NextStepType {
|
||||
return NextStepVerifyInvite
|
||||
}
|
66
apps/api/internal/domain/notification.go
Normal file
66
apps/api/internal/domain/notification.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type NotificationType int32
|
||||
|
||||
const (
|
||||
NotificationTypeEmail NotificationType = iota
|
||||
NotificationTypeSms
|
||||
|
||||
notificationCount
|
||||
)
|
||||
|
||||
type NotificationProviderState int32
|
||||
|
||||
const (
|
||||
NotificationProviderStateUnspecified NotificationProviderState = iota
|
||||
NotificationProviderStateActive
|
||||
NotificationProviderStateRemoved
|
||||
|
||||
notificationProviderCount
|
||||
)
|
||||
|
||||
func (s NotificationProviderState) Exists() bool {
|
||||
return s == NotificationProviderStateActive
|
||||
}
|
||||
|
||||
type NotificationProviderType int32
|
||||
|
||||
const (
|
||||
NotificationProviderTypeFile NotificationProviderType = iota
|
||||
NotificationProviderTypeLog
|
||||
|
||||
notificationProviderTypeCount
|
||||
)
|
||||
|
||||
type NotificationArguments struct {
|
||||
Origin string `json:"origin,omitempty"`
|
||||
Domain string `json:"domain,omitempty"`
|
||||
Expiry time.Duration `json:"expiry,omitempty"`
|
||||
TempUsername string `json:"tempUsername,omitempty"`
|
||||
ApplicationName string `json:"applicationName,omitempty"`
|
||||
CodeID string `json:"codeID,omitempty"`
|
||||
SessionID string `json:"sessionID,omitempty"`
|
||||
AuthRequestID string `json:"authRequestID,omitempty"`
|
||||
}
|
||||
|
||||
// ToMap creates a type safe map of the notification arguments.
|
||||
// Since these arguments are used in text template, all keys must be PascalCase and types must remain the same (e.g. Duration).
|
||||
func (n *NotificationArguments) ToMap() map[string]interface{} {
|
||||
m := make(map[string]interface{})
|
||||
if n == nil {
|
||||
return m
|
||||
}
|
||||
m["Origin"] = n.Origin
|
||||
m["Domain"] = n.Domain
|
||||
m["Expiry"] = n.Expiry
|
||||
m["TempUsername"] = n.TempUsername
|
||||
m["ApplicationName"] = n.ApplicationName
|
||||
m["CodeID"] = n.CodeID
|
||||
m["SessionID"] = n.SessionID
|
||||
m["AuthRequestID"] = n.AuthRequestID
|
||||
return m
|
||||
}
|
16
apps/api/internal/domain/object.go
Normal file
16
apps/api/internal/domain/object.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type ObjectDetails struct {
|
||||
Sequence uint64
|
||||
// EventDate is the date of the last event that changed the object
|
||||
EventDate time.Time
|
||||
// CreationDate is the date of the first event that created the object
|
||||
CreationDate time.Time
|
||||
ResourceOwner string
|
||||
// ID is the Aggregate ID of the resource
|
||||
ID string
|
||||
}
|
17
apps/api/internal/domain/oidc_code_challenge.go
Normal file
17
apps/api/internal/domain/oidc_code_challenge.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package domain
|
||||
|
||||
type OIDCCodeChallenge struct {
|
||||
Challenge string
|
||||
Method OIDCCodeChallengeMethod
|
||||
}
|
||||
|
||||
func (c *OIDCCodeChallenge) IsValid() bool {
|
||||
return c.Challenge != ""
|
||||
}
|
||||
|
||||
type OIDCCodeChallengeMethod int32
|
||||
|
||||
const (
|
||||
CodeChallengeMethodPlain OIDCCodeChallengeMethod = iota
|
||||
CodeChallengeMethodS256
|
||||
)
|
48
apps/api/internal/domain/oidc_error_reason.go
Normal file
48
apps/api/internal/domain/oidc_error_reason.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type OIDCErrorReason int32
|
||||
|
||||
const (
|
||||
OIDCErrorReasonUnspecified OIDCErrorReason = iota
|
||||
OIDCErrorReasonInvalidRequest
|
||||
OIDCErrorReasonUnauthorizedClient
|
||||
OIDCErrorReasonAccessDenied
|
||||
OIDCErrorReasonUnsupportedResponseType
|
||||
OIDCErrorReasonInvalidScope
|
||||
OIDCErrorReasonServerError
|
||||
OIDCErrorReasonTemporaryUnavailable
|
||||
OIDCErrorReasonInteractionRequired
|
||||
OIDCErrorReasonLoginRequired
|
||||
OIDCErrorReasonAccountSelectionRequired
|
||||
OIDCErrorReasonConsentRequired
|
||||
OIDCErrorReasonInvalidRequestURI
|
||||
OIDCErrorReasonInvalidRequestObject
|
||||
OIDCErrorReasonRequestNotSupported
|
||||
OIDCErrorReasonRequestURINotSupported
|
||||
OIDCErrorReasonRegistrationNotSupported
|
||||
OIDCErrorReasonInvalidGrant
|
||||
)
|
||||
|
||||
func OIDCErrorReasonFromError(err error) OIDCErrorReason {
|
||||
if errors.Is(err, oidc.ErrInvalidRequest()) {
|
||||
return OIDCErrorReasonInvalidRequest
|
||||
}
|
||||
if errors.Is(err, oidc.ErrInvalidGrant()) {
|
||||
return OIDCErrorReasonInvalidGrant
|
||||
}
|
||||
if zerrors.IsPreconditionFailed(err) {
|
||||
return OIDCErrorReasonAccessDenied
|
||||
}
|
||||
if zerrors.IsInternal(err) {
|
||||
return OIDCErrorReasonServerError
|
||||
}
|
||||
return OIDCErrorReasonUnspecified
|
||||
}
|
51
apps/api/internal/domain/oidc_error_reason_test.go
Normal file
51
apps/api/internal/domain/oidc_error_reason_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestOIDCErrorReasonFromError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want OIDCErrorReason
|
||||
}{
|
||||
{
|
||||
name: "invalid request",
|
||||
err: oidc.ErrInvalidRequest().WithDescription("foo"),
|
||||
want: OIDCErrorReasonInvalidRequest,
|
||||
},
|
||||
{
|
||||
name: "invalid grant",
|
||||
err: oidc.ErrInvalidGrant().WithDescription("foo"),
|
||||
want: OIDCErrorReasonInvalidGrant,
|
||||
},
|
||||
{
|
||||
name: "precondition failed",
|
||||
err: zerrors.ThrowPreconditionFailed(nil, "123", "bar"),
|
||||
want: OIDCErrorReasonAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "internal",
|
||||
err: zerrors.ThrowInternal(nil, "123", "bar"),
|
||||
want: OIDCErrorReasonServerError,
|
||||
},
|
||||
{
|
||||
name: "unspecified",
|
||||
err: io.ErrClosedPipe,
|
||||
want: OIDCErrorReasonUnspecified,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := OIDCErrorReasonFromError(tt.err)
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
15
apps/api/internal/domain/oidc_mapping_field.go
Normal file
15
apps/api/internal/domain/oidc_mapping_field.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
type OIDCMappingField int32
|
||||
|
||||
const (
|
||||
OIDCMappingFieldUnspecified OIDCMappingField = iota
|
||||
OIDCMappingFieldPreferredLoginName
|
||||
OIDCMappingFieldEmail
|
||||
// count is for validation purposes
|
||||
oidcMappingFieldCount
|
||||
)
|
||||
|
||||
func (f OIDCMappingField) Valid() bool {
|
||||
return f > 0 && f < oidcMappingFieldCount
|
||||
}
|
9
apps/api/internal/domain/oidc_session.go
Normal file
9
apps/api/internal/domain/oidc_session.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
type OIDCSessionState int32
|
||||
|
||||
const (
|
||||
OIDCSessionStateUnspecified OIDCSessionState = iota
|
||||
OIDCSessionStateActive
|
||||
OIDCSessionStateTerminated
|
||||
)
|
37
apps/api/internal/domain/oidc_settings.go
Normal file
37
apps/api/internal/domain/oidc_settings.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type OIDCSettings struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State OIDCSettingsState
|
||||
Default bool
|
||||
|
||||
AccessTokenLifetime time.Duration
|
||||
IdTokenLifetime time.Duration
|
||||
RefreshTokenIdleExpiration time.Duration
|
||||
RefreshTokenExpiration time.Duration
|
||||
}
|
||||
|
||||
type OIDCSettingsState int32
|
||||
|
||||
const (
|
||||
OIDCSettingsStateUnspecified OIDCSettingsState = iota
|
||||
OIDCSettingsStateActive
|
||||
OIDCSettingsStateRemoved
|
||||
|
||||
oidcSettingsStateCount
|
||||
)
|
||||
|
||||
func (c OIDCSettingsState) Valid() bool {
|
||||
return c >= 0 && c < oidcSettingsStateCount
|
||||
}
|
||||
|
||||
func (s OIDCSettingsState) Exists() bool {
|
||||
return s != OIDCSettingsStateUnspecified && s != OIDCSettingsStateRemoved
|
||||
}
|
86
apps/api/internal/domain/oidcresponsemode_enumer.go
Normal file
86
apps/api/internal/domain/oidcresponsemode_enumer.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// Code generated by "enumer -type OIDCResponseMode -transform snake -trimprefix OIDCResponseMode"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _OIDCResponseModeName = "unspecifiedqueryfragmentform_post"
|
||||
|
||||
var _OIDCResponseModeIndex = [...]uint8{0, 11, 16, 24, 33}
|
||||
|
||||
const _OIDCResponseModeLowerName = "unspecifiedqueryfragmentform_post"
|
||||
|
||||
func (i OIDCResponseMode) String() string {
|
||||
if i < 0 || i >= OIDCResponseMode(len(_OIDCResponseModeIndex)-1) {
|
||||
return fmt.Sprintf("OIDCResponseMode(%d)", i)
|
||||
}
|
||||
return _OIDCResponseModeName[_OIDCResponseModeIndex[i]:_OIDCResponseModeIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _OIDCResponseModeNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[OIDCResponseModeUnspecified-(0)]
|
||||
_ = x[OIDCResponseModeQuery-(1)]
|
||||
_ = x[OIDCResponseModeFragment-(2)]
|
||||
_ = x[OIDCResponseModeFormPost-(3)]
|
||||
}
|
||||
|
||||
var _OIDCResponseModeValues = []OIDCResponseMode{OIDCResponseModeUnspecified, OIDCResponseModeQuery, OIDCResponseModeFragment, OIDCResponseModeFormPost}
|
||||
|
||||
var _OIDCResponseModeNameToValueMap = map[string]OIDCResponseMode{
|
||||
_OIDCResponseModeName[0:11]: OIDCResponseModeUnspecified,
|
||||
_OIDCResponseModeLowerName[0:11]: OIDCResponseModeUnspecified,
|
||||
_OIDCResponseModeName[11:16]: OIDCResponseModeQuery,
|
||||
_OIDCResponseModeLowerName[11:16]: OIDCResponseModeQuery,
|
||||
_OIDCResponseModeName[16:24]: OIDCResponseModeFragment,
|
||||
_OIDCResponseModeLowerName[16:24]: OIDCResponseModeFragment,
|
||||
_OIDCResponseModeName[24:33]: OIDCResponseModeFormPost,
|
||||
_OIDCResponseModeLowerName[24:33]: OIDCResponseModeFormPost,
|
||||
}
|
||||
|
||||
var _OIDCResponseModeNames = []string{
|
||||
_OIDCResponseModeName[0:11],
|
||||
_OIDCResponseModeName[11:16],
|
||||
_OIDCResponseModeName[16:24],
|
||||
_OIDCResponseModeName[24:33],
|
||||
}
|
||||
|
||||
// OIDCResponseModeString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func OIDCResponseModeString(s string) (OIDCResponseMode, error) {
|
||||
if val, ok := _OIDCResponseModeNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _OIDCResponseModeNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to OIDCResponseMode values", s)
|
||||
}
|
||||
|
||||
// OIDCResponseModeValues returns all values of the enum
|
||||
func OIDCResponseModeValues() []OIDCResponseMode {
|
||||
return _OIDCResponseModeValues
|
||||
}
|
||||
|
||||
// OIDCResponseModeStrings returns a slice of all String values of the enum
|
||||
func OIDCResponseModeStrings() []string {
|
||||
strs := make([]string, len(_OIDCResponseModeNames))
|
||||
copy(strs, _OIDCResponseModeNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsAOIDCResponseMode returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i OIDCResponseMode) IsAOIDCResponseMode() bool {
|
||||
for _, v := range _OIDCResponseModeValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
49
apps/api/internal/domain/org.go
Normal file
49
apps/api/internal/domain/org.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type Org struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State OrgState
|
||||
Name string
|
||||
|
||||
PrimaryDomain string
|
||||
Domains []*OrgDomain
|
||||
}
|
||||
|
||||
func (o *Org) IsValid() bool {
|
||||
if o == nil {
|
||||
return false
|
||||
}
|
||||
o.Name = strings.TrimSpace(o.Name)
|
||||
return o.Name != ""
|
||||
}
|
||||
|
||||
func (o *Org) AddIAMDomain(iamDomain string) {
|
||||
orgDomain, _ := NewIAMDomainName(o.Name, iamDomain)
|
||||
o.Domains = append(o.Domains, &OrgDomain{Domain: orgDomain, Verified: true, Primary: true})
|
||||
}
|
||||
|
||||
type OrgState int32
|
||||
|
||||
const (
|
||||
OrgStateUnspecified OrgState = iota
|
||||
OrgStateActive
|
||||
OrgStateInactive
|
||||
OrgStateRemoved
|
||||
|
||||
orgStateMax
|
||||
)
|
||||
|
||||
func (s OrgState) Valid() bool {
|
||||
return s > OrgStateUnspecified && s < orgStateMax
|
||||
}
|
||||
|
||||
func (s OrgState) Exists() bool {
|
||||
return s != OrgStateRemoved && s != OrgStateUnspecified
|
||||
}
|
110
apps/api/internal/domain/org_domain.go
Normal file
110
apps/api/internal/domain/org_domain.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
http_util "github.com/zitadel/zitadel/internal/api/http"
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type OrgDomain struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Domain string
|
||||
Primary bool
|
||||
Verified bool
|
||||
ValidationType OrgDomainValidationType
|
||||
ValidationCode *crypto.CryptoValue
|
||||
}
|
||||
|
||||
func (domain *OrgDomain) IsValid() bool {
|
||||
return domain.Domain != ""
|
||||
}
|
||||
|
||||
func (domain *OrgDomain) GenerateVerificationCode(codeGenerator crypto.Generator) (string, error) {
|
||||
validationCodeCrypto, validationCode, err := crypto.NewCode(codeGenerator)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
domain.ValidationCode = validationCodeCrypto
|
||||
return validationCode, nil
|
||||
}
|
||||
|
||||
func NewIAMDomainName(orgName, iamDomain string) (string, error) {
|
||||
// Reference: label domain requirements https://www.nic.ad.jp/timeline/en/20th/appendix1.html
|
||||
|
||||
// Replaces spaces in org name with hyphens
|
||||
label := strings.ReplaceAll(orgName, " ", "-")
|
||||
|
||||
// The label must only contains alphanumeric characters and hyphens
|
||||
// Invalid characters are replaced with and empty space but as #6471,
|
||||
// as these domains are not used to host ZITADEL, but only for user names,
|
||||
// the characters shouldn't matter that much so we'll accept unicode
|
||||
// characters, accented characters (\p{L}\p{M}), numbers and hyphens.
|
||||
label = string(regexp.MustCompile(`[^\p{L}\p{M}0-9-]`).ReplaceAll([]byte(label), []byte("")))
|
||||
|
||||
// The label cannot exceed 63 characters
|
||||
if len(label) > 63 {
|
||||
label = label[:63]
|
||||
}
|
||||
|
||||
// The total length of the resulting domain can't exceed 253 characters
|
||||
domain := label + "." + iamDomain
|
||||
if len(domain) > 253 {
|
||||
truncateNChars := len(domain) - 253
|
||||
label = label[:len(label)-truncateNChars]
|
||||
}
|
||||
|
||||
// Label (maybe truncated) can't start with a hyphen
|
||||
if len(label) > 0 && label[0:1] == "-" {
|
||||
label = label[1:]
|
||||
}
|
||||
|
||||
// Label (maybe truncated) can't end with a hyphen
|
||||
if len(label) > 0 && label[len(label)-1:] == "-" {
|
||||
label = label[:len(label)-1]
|
||||
}
|
||||
|
||||
// Empty string should be invalid
|
||||
if len(label) > 0 {
|
||||
return strings.ToLower(label + "." + iamDomain), nil
|
||||
}
|
||||
|
||||
return "", zerrors.ThrowInvalidArgument(nil, "ORG-RrfXY", "Errors.Org.Domain.EmptyString")
|
||||
}
|
||||
|
||||
type OrgDomainValidationType int32
|
||||
|
||||
const (
|
||||
OrgDomainValidationTypeUnspecified OrgDomainValidationType = iota
|
||||
OrgDomainValidationTypeHTTP
|
||||
OrgDomainValidationTypeDNS
|
||||
)
|
||||
|
||||
func (t OrgDomainValidationType) CheckType() (http_util.CheckType, bool) {
|
||||
switch t {
|
||||
case OrgDomainValidationTypeHTTP:
|
||||
return http_util.CheckTypeHTTP, true
|
||||
case OrgDomainValidationTypeDNS:
|
||||
return http_util.CheckTypeDNS, true
|
||||
default:
|
||||
return -1, false
|
||||
}
|
||||
}
|
||||
|
||||
type OrgDomainState int32
|
||||
|
||||
const (
|
||||
OrgDomainStateUnspecified OrgDomainState = iota
|
||||
OrgDomainStateActive
|
||||
OrgDomainStateRemoved
|
||||
|
||||
orgDomainStateCount
|
||||
)
|
||||
|
||||
func (f OrgDomainState) Valid() bool {
|
||||
return f >= 0 && f < orgDomainStateCount
|
||||
}
|
98
apps/api/internal/domain/org_domain_test.go
Normal file
98
apps/api/internal/domain/org_domain_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewIAMDomainName(t *testing.T) {
|
||||
type args struct {
|
||||
orgName string
|
||||
iamDomain string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result string
|
||||
}{
|
||||
{
|
||||
name: "Single word domain is already valid",
|
||||
args: args{
|
||||
orgName: "single-word-domain",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "single-word-domain.localhost",
|
||||
},
|
||||
{
|
||||
name: "resulting domain should be in lowercase",
|
||||
args: args{
|
||||
orgName: "Uppercase org Name",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "uppercase-org-name.localhost",
|
||||
},
|
||||
{
|
||||
name: "replace spaces with hyphens",
|
||||
args: args{
|
||||
orgName: "my org name",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "my-org-name.localhost",
|
||||
},
|
||||
{
|
||||
name: "replace invalid characters [^a-zA-Z0-9-] with empty spaces",
|
||||
args: args{
|
||||
orgName: "mí >**name?",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "mí-name.localhost",
|
||||
},
|
||||
{
|
||||
name: "label created from org name size is not greater than 63 chars",
|
||||
args: args{
|
||||
orgName: "my organization name must not exceed sixty-three characters 1234",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "my-organization-name-must-not-exceed-sixty-three-characters-123.localhost",
|
||||
},
|
||||
{
|
||||
name: "resulting domain cannot exceed 253 chars",
|
||||
args: args{
|
||||
orgName: "Lorem ipsum dolor sit amet",
|
||||
iamDomain: "llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
|
||||
},
|
||||
result: "lorem-ipsum-dolor-sit.llgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch.co.uk",
|
||||
},
|
||||
{
|
||||
name: "label based on org name should not end with a hyphen",
|
||||
args: args{
|
||||
orgName: "my super long organization name with many many many characters ",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
|
||||
},
|
||||
{
|
||||
name: "label based on org name should not start with a hyphen",
|
||||
args: args{
|
||||
orgName: " my super long organization name with many many many characters",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "my-super-long-organization-name-with-many-many-many-characters.localhost",
|
||||
},
|
||||
{
|
||||
name: "string full with invalid characters returns empty",
|
||||
args: args{
|
||||
orgName: "*¿=@^[])",
|
||||
iamDomain: "localhost",
|
||||
},
|
||||
result: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
domain, _ := NewIAMDomainName(tt.args.orgName, tt.args.iamDomain)
|
||||
if tt.result != domain {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.result, domain)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
19
apps/api/internal/domain/organization_settings.go
Normal file
19
apps/api/internal/domain/organization_settings.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
type OrganizationSettingsState int32
|
||||
|
||||
const (
|
||||
OrganizationSettingsStateUnspecified OrganizationSettingsState = iota
|
||||
OrganizationSettingsStateActive
|
||||
OrganizationSettingsStateRemoved
|
||||
|
||||
organizationSettingsStateCount
|
||||
)
|
||||
|
||||
func (c OrganizationSettingsState) Valid() bool {
|
||||
return c >= 0 && c < organizationSettingsStateCount
|
||||
}
|
||||
|
||||
func (s OrganizationSettingsState) Exists() bool {
|
||||
return s.Valid() && s != OrganizationSettingsStateUnspecified && s != OrganizationSettingsStateRemoved
|
||||
}
|
75
apps/api/internal/domain/permission.go
Normal file
75
apps/api/internal/domain/permission.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package domain
|
||||
|
||||
import "context"
|
||||
|
||||
type Permissions struct {
|
||||
Permissions []string
|
||||
}
|
||||
|
||||
func (p *Permissions) AppendPermissions(ctxID string, permissions ...string) {
|
||||
for _, permission := range permissions {
|
||||
p.appendPermission(ctxID, permission)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Permissions) appendPermission(ctxID, permission string) {
|
||||
if ctxID != "" {
|
||||
permission = permission + ":" + ctxID
|
||||
}
|
||||
for _, existingPermission := range p.Permissions {
|
||||
if existingPermission == permission {
|
||||
return
|
||||
}
|
||||
}
|
||||
p.Permissions = append(p.Permissions, permission)
|
||||
}
|
||||
|
||||
type PermissionCheck func(ctx context.Context, permission, resourceOwnerID, aggregateID string) (err error)
|
||||
|
||||
const (
|
||||
PermissionUserWrite = "user.write"
|
||||
PermissionUserRead = "user.read"
|
||||
PermissionUserDelete = "user.delete"
|
||||
PermissionUserCredentialWrite = "user.credential.write"
|
||||
PermissionSessionWrite = "session.write"
|
||||
PermissionSessionRead = "session.read"
|
||||
PermissionSessionLink = "session.link"
|
||||
PermissionSessionDelete = "session.delete"
|
||||
PermissionOrgRead = "org.read"
|
||||
PermissionIDPRead = "iam.idp.read"
|
||||
PermissionOrgIDPRead = "org.idp.read"
|
||||
PermissionProjectWrite = "project.write"
|
||||
PermissionProjectRead = "project.read"
|
||||
PermissionProjectDelete = "project.delete"
|
||||
PermissionProjectGrantWrite = "project.grant.write"
|
||||
PermissionProjectGrantRead = "project.grant.read"
|
||||
PermissionProjectGrantDelete = "project.grant.delete"
|
||||
PermissionProjectRoleWrite = "project.role.write"
|
||||
PermissionProjectRoleRead = "project.role.read"
|
||||
PermissionProjectRoleDelete = "project.role.delete"
|
||||
PermissionProjectAppWrite = "project.app.write"
|
||||
PermissionProjectAppDelete = "project.app.delete"
|
||||
PermissionProjectAppRead = "project.app.read"
|
||||
PermissionInstanceMemberWrite = "iam.member.write"
|
||||
PermissionInstanceMemberDelete = "iam.member.delete"
|
||||
PermissionInstanceMemberRead = "iam.member.read"
|
||||
PermissionOrgMemberWrite = "org.member.write"
|
||||
PermissionOrgMemberDelete = "org.member.delete"
|
||||
PermissionOrgMemberRead = "org.member.read"
|
||||
PermissionProjectMemberWrite = "project.member.write"
|
||||
PermissionProjectMemberDelete = "project.member.delete"
|
||||
PermissionProjectMemberRead = "project.member.read"
|
||||
PermissionProjectGrantMemberWrite = "project.grant.member.write"
|
||||
PermissionProjectGrantMemberDelete = "project.grant.member.delete"
|
||||
PermissionProjectGrantMemberRead = "project.grant.member.read"
|
||||
PermissionUserGrantWrite = "user.grant.write"
|
||||
PermissionUserGrantRead = "user.grant.read"
|
||||
PermissionUserGrantDelete = "user.grant.delete"
|
||||
PermissionIAMPolicyWrite = "iam.policy.write"
|
||||
PermissionIAMPolicyDelete = "iam.policy.delete"
|
||||
PermissionPolicyRead = "policy.read"
|
||||
)
|
||||
|
||||
// ProjectPermissionCheck is used as a check for preconditions dependent on application, project, user resourceowner and usergrants.
|
||||
// Configurable on the project the application belongs to through the flags related to authentication.
|
||||
type ProjectPermissionCheck func(ctx context.Context, clientID, userID string) (err error)
|
15
apps/api/internal/domain/policy.go
Normal file
15
apps/api/internal/domain/policy.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
type PolicyState int32
|
||||
|
||||
const (
|
||||
PolicyStateUnspecified PolicyState = iota
|
||||
PolicyStateActive
|
||||
PolicyStateRemoved
|
||||
|
||||
policyStateCount
|
||||
)
|
||||
|
||||
func (s PolicyState) Exists() bool {
|
||||
return s != PolicyStateUnspecified && s != PolicyStateRemoved
|
||||
}
|
14
apps/api/internal/domain/policy_domain.go
Normal file
14
apps/api/internal/domain/policy_domain.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type DomainPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
UserLoginMustBeDomain bool
|
||||
ValidateOrgDomains bool
|
||||
SMTPSenderAddressMatchesInstanceDomain bool
|
||||
Default bool
|
||||
}
|
85
apps/api/internal/domain/policy_label.go
Normal file
85
apps/api/internal/domain/policy_label.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var colorRegex = regexp.MustCompile("^$|^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$")
|
||||
|
||||
type LabelPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State LabelPolicyState
|
||||
Default bool
|
||||
|
||||
PrimaryColor string
|
||||
BackgroundColor string
|
||||
WarnColor string
|
||||
FontColor string
|
||||
LogoURL string
|
||||
IconURL string
|
||||
|
||||
PrimaryColorDark string
|
||||
BackgroundColorDark string
|
||||
WarnColorDark string
|
||||
FontColorDark string
|
||||
LogoDarkURL string
|
||||
IconDarkURL string
|
||||
|
||||
Font string
|
||||
|
||||
HideLoginNameSuffix bool
|
||||
ErrorMsgPopup bool
|
||||
DisableWatermark bool
|
||||
ThemeMode LabelPolicyThemeMode
|
||||
}
|
||||
|
||||
type LabelPolicyState int32
|
||||
|
||||
const (
|
||||
LabelPolicyStateUnspecified LabelPolicyState = iota
|
||||
LabelPolicyStateActive
|
||||
LabelPolicyStateRemoved
|
||||
LabelPolicyStatePreview
|
||||
|
||||
labelPolicyStateCount
|
||||
)
|
||||
|
||||
type LabelPolicyThemeMode int32
|
||||
|
||||
const (
|
||||
LabelPolicyThemeAuto LabelPolicyThemeMode = iota
|
||||
LabelPolicyThemeLight
|
||||
LabelPolicyThemeDark
|
||||
)
|
||||
|
||||
func (f LabelPolicy) IsValid() error {
|
||||
if !colorRegex.MatchString(f.PrimaryColor) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-391dG", "Errors.Policy.Label.Invalid.PrimaryColor")
|
||||
}
|
||||
if !colorRegex.MatchString(f.BackgroundColor) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-502F1", "Errors.Policy.Label.Invalid.BackgroundColor")
|
||||
}
|
||||
if !colorRegex.MatchString(f.WarnColor) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-nvw33", "Errors.Policy.Label.Invalid.WarnColor")
|
||||
}
|
||||
if !colorRegex.MatchString(f.FontColor) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-93mSf", "Errors.Policy.Label.Invalid.FontColor")
|
||||
}
|
||||
if !colorRegex.MatchString(f.PrimaryColorDark) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-391dG", "Errors.Policy.Label.Invalid.PrimaryColorDark")
|
||||
}
|
||||
if !colorRegex.MatchString(f.BackgroundColorDark) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-llsp2", "Errors.Policy.Label.Invalid.BackgroundColorDark")
|
||||
}
|
||||
if !colorRegex.MatchString(f.WarnColorDark) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-2b6sf", "Errors.Policy.Label.Invalid.WarnColorDark")
|
||||
}
|
||||
if !colorRegex.MatchString(f.FontColorDark) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "POLICY-3M0fs", "Errors.Policy.Label.Invalid.FontColorDark")
|
||||
}
|
||||
return nil
|
||||
}
|
505
apps/api/internal/domain/policy_label_test.go
Normal file
505
apps/api/internal/domain/policy_label_test.go
Normal file
@@ -0,0 +1,505 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestLabelPolicyPrimaryColorValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty primary, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyBackgroundColorValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty background, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyWarnColorValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty warn, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyFontColorValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty font, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColor: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyPrimaryColorDarkValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty primary dark, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{PrimaryColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyBackgroundColorDarkValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty background dark, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{BackgroundColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyWarnColorDarkValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty warn dark, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{WarnColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelPolicyFontColorDarkValid(t *testing.T) {
|
||||
type args struct {
|
||||
policy *LabelPolicy
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
err func(error) bool
|
||||
}{
|
||||
{
|
||||
name: "empty font dark, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: ""},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 6 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: "#ffffff"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with 3 characters, valid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: "#000"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "color code with wrong characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: "#0f9wfm"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with wrong count of characters, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "color code with no #, invalid",
|
||||
args: args{
|
||||
policy: &LabelPolicy{FontColorDark: "#00"},
|
||||
},
|
||||
err: zerrors.IsErrorInvalidArgument,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.policy.IsValid()
|
||||
if tt.err == nil {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
if tt.err != nil && !tt.err(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
97
apps/api/internal/domain/policy_login.go
Normal file
97
apps/api/internal/domain/policy_login.go
Normal file
@@ -0,0 +1,97 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type LoginPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Default bool
|
||||
AllowUsernamePassword bool
|
||||
AllowRegister bool
|
||||
AllowExternalIDP bool
|
||||
IDPProviders []*IDPProvider
|
||||
ForceMFA bool
|
||||
ForceMFALocalOnly bool
|
||||
SecondFactors []SecondFactorType
|
||||
MultiFactors []MultiFactorType
|
||||
PasswordlessType PasswordlessType
|
||||
HidePasswordReset bool
|
||||
IgnoreUnknownUsernames bool
|
||||
AllowDomainDiscovery bool
|
||||
DefaultRedirectURI string
|
||||
PasswordCheckLifetime time.Duration
|
||||
ExternalLoginCheckLifetime time.Duration
|
||||
MFAInitSkipLifetime time.Duration
|
||||
SecondFactorCheckLifetime time.Duration
|
||||
MultiFactorCheckLifetime time.Duration
|
||||
DisableLoginWithEmail bool
|
||||
DisableLoginWithPhone bool
|
||||
}
|
||||
|
||||
func ValidateDefaultRedirectURI(rawURL string) bool {
|
||||
if rawURL == "" {
|
||||
return true
|
||||
}
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch parsedURL.Scheme {
|
||||
case "":
|
||||
return false
|
||||
case "http", "https":
|
||||
return parsedURL.Host != ""
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
type IDPProvider struct {
|
||||
models.ObjectRoot
|
||||
Type IdentityProviderType
|
||||
IDPConfigID string
|
||||
|
||||
Name string
|
||||
StylingType IDPConfigStylingType // deprecated
|
||||
IDPType IDPType
|
||||
IDPState IDPConfigState
|
||||
}
|
||||
|
||||
func (p IDPProvider) IsValid() bool {
|
||||
return p.IDPConfigID != ""
|
||||
}
|
||||
|
||||
// DisplayName returns the name or a default
|
||||
// It's used for html rendering
|
||||
// to be used when always a name must be displayed (e.g. login)
|
||||
func (p IDPProvider) DisplayName() string {
|
||||
return IDPName(p.Name, p.IDPType)
|
||||
}
|
||||
|
||||
type PasswordlessType int32
|
||||
|
||||
const (
|
||||
PasswordlessTypeNotAllowed PasswordlessType = iota
|
||||
PasswordlessTypeAllowed
|
||||
|
||||
passwordlessCount
|
||||
)
|
||||
|
||||
func (f PasswordlessType) Valid() bool {
|
||||
return f >= 0 && f < passwordlessCount
|
||||
}
|
||||
|
||||
// HasSecondFactors is used in html rendering
|
||||
func (p *LoginPolicy) HasSecondFactors() bool {
|
||||
return len(p.SecondFactors) > 0
|
||||
}
|
||||
|
||||
// HasMultiFactors is used in html rendering
|
||||
func (p *LoginPolicy) HasMultiFactors() bool {
|
||||
return len(p.MultiFactors) > 0
|
||||
}
|
73
apps/api/internal/domain/policy_login_test.go
Normal file
73
apps/api/internal/domain/policy_login_test.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateDefaultRedirectURI(t *testing.T) {
|
||||
type args struct {
|
||||
rawURL string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"invalid url, false",
|
||||
args{
|
||||
rawURL: string('\n'),
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"empty schema, false",
|
||||
args{
|
||||
rawURL: "url",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"empty http host, false",
|
||||
args{
|
||||
rawURL: "http://",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"empty https host, false",
|
||||
args{
|
||||
rawURL: "https://",
|
||||
},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"https, ok",
|
||||
args{
|
||||
rawURL: "https://test",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"custom schema, ok",
|
||||
args{
|
||||
rawURL: "custom://",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty url, ok",
|
||||
args{
|
||||
rawURL: "",
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, ValidateDefaultRedirectURI(tt.args.rawURL), "ValidateDefaultRedirectURI(%v)", tt.args.rawURL)
|
||||
})
|
||||
}
|
||||
}
|
15
apps/api/internal/domain/policy_mail_template.go
Normal file
15
apps/api/internal/domain/policy_mail_template.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package domain
|
||||
|
||||
import "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
|
||||
type MailTemplate struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State PolicyState
|
||||
Default bool
|
||||
Template []byte
|
||||
}
|
||||
|
||||
func (m *MailTemplate) IsValid() bool {
|
||||
return m.Template != nil
|
||||
}
|
12
apps/api/internal/domain/policy_password_age.go
Normal file
12
apps/api/internal/domain/policy_password_age.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type PasswordAgePolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
MaxAgeDays uint64
|
||||
ExpireWarnDays uint64
|
||||
}
|
57
apps/api/internal/domain/policy_password_complexity.go
Normal file
57
apps/api/internal/domain/policy_password_complexity.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
hasStringLowerCase = regexp.MustCompile(`[a-z]`).MatchString
|
||||
hasStringUpperCase = regexp.MustCompile(`[A-Z]`).MatchString
|
||||
hasNumber = regexp.MustCompile(`[0-9]`).MatchString
|
||||
hasSymbol = regexp.MustCompile(`[^A-Za-z0-9]`).MatchString
|
||||
)
|
||||
|
||||
type PasswordComplexityPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
MinLength uint64
|
||||
HasLowercase bool
|
||||
HasUppercase bool
|
||||
HasNumber bool
|
||||
HasSymbol bool
|
||||
|
||||
Default bool
|
||||
}
|
||||
|
||||
func (p *PasswordComplexityPolicy) IsValid() error {
|
||||
if p.MinLength == 0 || p.MinLength > 72 {
|
||||
return zerrors.ThrowInvalidArgument(nil, "MODEL-Lsp0e", "Errors.User.PasswordComplexityPolicy.MinLengthNotAllowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PasswordComplexityPolicy) Check(password string) error {
|
||||
if p.MinLength != 0 && uint64(len(password)) < p.MinLength {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOMAIN-HuJf6", "Errors.User.PasswordComplexityPolicy.MinLength")
|
||||
}
|
||||
|
||||
if p.HasLowercase && !hasStringLowerCase(password) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOMAIN-co3Xw", "Errors.User.PasswordComplexityPolicy.HasLower")
|
||||
}
|
||||
|
||||
if p.HasUppercase && !hasStringUpperCase(password) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOMAIN-VoaRj", "Errors.User.PasswordComplexityPolicy.HasUpper")
|
||||
}
|
||||
|
||||
if p.HasNumber && !hasNumber(password) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOMAIN-ZBv4H", "Errors.User.PasswordComplexityPolicy.HasNumber")
|
||||
}
|
||||
|
||||
if p.HasSymbol && !hasSymbol(password) {
|
||||
return zerrors.ThrowInvalidArgument(nil, "DOMAIN-ZDLwA", "Errors.User.PasswordComplexityPolicy.HasSymbol")
|
||||
}
|
||||
return nil
|
||||
}
|
14
apps/api/internal/domain/policy_password_lockout.go
Normal file
14
apps/api/internal/domain/policy_password_lockout.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type LockoutPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Default bool
|
||||
MaxPasswordAttempts uint64
|
||||
MaxOTPAttempts uint64
|
||||
ShowLockOutFailures bool
|
||||
}
|
20
apps/api/internal/domain/policy_privacy.go
Normal file
20
apps/api/internal/domain/policy_privacy.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type PrivacyPolicy struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State PolicyState
|
||||
Default bool
|
||||
|
||||
TOSLink string
|
||||
PrivacyLink string
|
||||
HelpLink string
|
||||
SupportEmail EmailAddress
|
||||
DocsLink string
|
||||
CustomLink string
|
||||
CustomLinkText string
|
||||
}
|
49
apps/api/internal/domain/project.go
Normal file
49
apps/api/internal/domain/project.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type Project struct {
|
||||
models.ObjectRoot
|
||||
|
||||
State ProjectState
|
||||
Name string
|
||||
ProjectRoleAssertion bool
|
||||
ProjectRoleCheck bool
|
||||
HasProjectCheck bool
|
||||
PrivateLabelingSetting PrivateLabelingSetting
|
||||
}
|
||||
|
||||
type ProjectState int32
|
||||
|
||||
const (
|
||||
ProjectStateUnspecified ProjectState = iota
|
||||
ProjectStateActive
|
||||
ProjectStateInactive
|
||||
ProjectStateRemoved
|
||||
|
||||
projectStateMax
|
||||
)
|
||||
|
||||
func (s ProjectState) Valid() bool {
|
||||
return s > ProjectStateUnspecified && s < projectStateMax
|
||||
}
|
||||
|
||||
type PrivateLabelingSetting int32
|
||||
|
||||
const (
|
||||
PrivateLabelingSettingUnspecified PrivateLabelingSetting = iota
|
||||
PrivateLabelingSettingEnforceProjectResourceOwnerPolicy
|
||||
PrivateLabelingSettingAllowLoginUserResourceOwnerPolicy
|
||||
|
||||
privateLabelingSettingMax
|
||||
)
|
||||
|
||||
func (s PrivateLabelingSetting) Valid() bool {
|
||||
return s >= PrivateLabelingSettingUnspecified && s < privateLabelingSettingMax
|
||||
}
|
||||
|
||||
func (o *Project) IsValid() bool {
|
||||
return o.Name != ""
|
||||
}
|
45
apps/api/internal/domain/project_grant.go
Normal file
45
apps/api/internal/domain/project_grant.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package domain
|
||||
|
||||
import es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
|
||||
type ProjectGrant struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
GrantID string
|
||||
GrantedOrgID string
|
||||
State ProjectGrantState
|
||||
RoleKeys []string
|
||||
}
|
||||
|
||||
type ProjectGrantState int32
|
||||
|
||||
const (
|
||||
ProjectGrantStateUnspecified ProjectGrantState = iota
|
||||
ProjectGrantStateActive
|
||||
ProjectGrantStateInactive
|
||||
ProjectGrantStateRemoved
|
||||
|
||||
projectGrantStateMax
|
||||
)
|
||||
|
||||
func (s ProjectGrantState) Valid() bool {
|
||||
return s > ProjectGrantStateUnspecified && s < projectGrantStateMax
|
||||
}
|
||||
|
||||
func (s ProjectGrantState) Exists() bool {
|
||||
return s != ProjectGrantStateUnspecified && s != ProjectGrantStateRemoved
|
||||
}
|
||||
|
||||
func (p *ProjectGrant) IsValid() bool {
|
||||
return p.GrantedOrgID != ""
|
||||
}
|
||||
|
||||
func GetRemovedRoles(existingRoles, newRoles []string) []string {
|
||||
removed := make([]string, 0)
|
||||
for _, role := range existingRoles {
|
||||
if !containsRoleKey(role, newRoles) {
|
||||
removed = append(removed, role)
|
||||
}
|
||||
}
|
||||
return removed
|
||||
}
|
17
apps/api/internal/domain/project_grant_member.go
Normal file
17
apps/api/internal/domain/project_grant_member.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
es_models "github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type ProjectGrantMember struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
GrantID string
|
||||
UserID string
|
||||
Roles []string
|
||||
}
|
||||
|
||||
func (i *ProjectGrantMember) IsValid() bool {
|
||||
return i.AggregateID != "" && i.GrantID != "" && i.UserID != "" && len(i.Roles) != 0
|
||||
}
|
47
apps/api/internal/domain/project_role.go
Normal file
47
apps/api/internal/domain/project_role.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/zitadel/internal/eventstore/v1/models"
|
||||
)
|
||||
|
||||
type ProjectRole struct {
|
||||
models.ObjectRoot
|
||||
|
||||
Key string
|
||||
DisplayName string
|
||||
Group string
|
||||
}
|
||||
|
||||
type ProjectRoleState int32
|
||||
|
||||
const (
|
||||
ProjectRoleStateUnspecified ProjectRoleState = iota
|
||||
ProjectRoleStateActive
|
||||
ProjectRoleStateRemoved
|
||||
)
|
||||
|
||||
func (s ProjectRoleState) Exists() bool {
|
||||
return s != ProjectRoleStateUnspecified && s != ProjectRoleStateRemoved
|
||||
}
|
||||
|
||||
func (p *ProjectRole) IsValid() bool {
|
||||
return p.AggregateID != "" && p.Key != ""
|
||||
}
|
||||
|
||||
func HasInvalidRoles(validRoles, roles []string) bool {
|
||||
for _, roleKey := range roles {
|
||||
if !containsRoleKey(roleKey, validRoles) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func containsRoleKey(roleKey string, validRoles []string) bool {
|
||||
for _, validRole := range validRoles {
|
||||
if roleKey == validRole {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
20
apps/api/internal/domain/provider.go
Normal file
20
apps/api/internal/domain/provider.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package domain
|
||||
|
||||
type IdentityProviderType int8
|
||||
|
||||
const (
|
||||
IdentityProviderTypeSystem IdentityProviderType = iota
|
||||
IdentityProviderTypeOrg
|
||||
|
||||
identityProviderCount
|
||||
)
|
||||
|
||||
type IdentityProviderState int32
|
||||
|
||||
const (
|
||||
IdentityProviderStateUnspecified IdentityProviderState = iota
|
||||
IdentityProviderStateActive
|
||||
IdentityProviderStateRemoved
|
||||
|
||||
idpProviderState
|
||||
)
|
37
apps/api/internal/domain/refresh_token.go
Normal file
37
apps/api/internal/domain/refresh_token.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func NewRefreshToken(userID, tokenID string, algorithm crypto.EncryptionAlgorithm) (string, error) {
|
||||
return RefreshToken(userID, tokenID, tokenID, algorithm)
|
||||
}
|
||||
|
||||
func RefreshToken(userID, tokenID, token string, algorithm crypto.EncryptionAlgorithm) (string, error) {
|
||||
encrypted, err := algorithm.Encrypt([]byte(userID + ":" + tokenID + ":" + token))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(encrypted), nil
|
||||
}
|
||||
|
||||
func FromRefreshToken(refreshToken string, algorithm crypto.EncryptionAlgorithm) (userID, tokenID, token string, err error) {
|
||||
decoded, err := base64.RawURLEncoding.DecodeString(refreshToken)
|
||||
if err != nil {
|
||||
return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid")
|
||||
}
|
||||
decrypted, err := algorithm.DecryptString(decoded, algorithm.EncryptionKeyID())
|
||||
if err != nil {
|
||||
return "", "", "", zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid")
|
||||
}
|
||||
split := strings.Split(decrypted, ":")
|
||||
if len(split) != 3 {
|
||||
return "", "", "", zerrors.ThrowInvalidArgument(nil, "DOMAIN-Se8oh", "Errors.User.RefreshToken.Invalid")
|
||||
}
|
||||
return split[0], split[1], split[2], nil
|
||||
}
|
129
apps/api/internal/domain/refresh_token_test.go
Normal file
129
apps/api/internal/domain/refresh_token_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/crypto"
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
type mockKeyStorage struct {
|
||||
keys crypto.Keys
|
||||
}
|
||||
|
||||
func (s *mockKeyStorage) ReadKeys() (crypto.Keys, error) {
|
||||
return s.keys, nil
|
||||
}
|
||||
|
||||
func (s *mockKeyStorage) ReadKey(id string) (*crypto.Key, error) {
|
||||
return &crypto.Key{
|
||||
ID: id,
|
||||
Value: s.keys[id],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (*mockKeyStorage) CreateKeys(context.Context, ...*crypto.Key) error {
|
||||
return errors.New("mockKeyStorage.CreateKeys not implemented")
|
||||
}
|
||||
|
||||
func TestFromRefreshToken(t *testing.T) {
|
||||
const (
|
||||
userID = "userID"
|
||||
tokenID = "tokenID"
|
||||
)
|
||||
|
||||
keyConfig := &crypto.KeyConfig{
|
||||
EncryptionKeyID: "keyID",
|
||||
DecryptionKeyIDs: []string{"keyID"},
|
||||
}
|
||||
keys := crypto.Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"}
|
||||
algorithm, err := crypto.NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys})
|
||||
require.NoError(t, err)
|
||||
|
||||
refreshToken, err := NewRefreshToken(userID, tokenID, algorithm)
|
||||
require.NoError(t, err)
|
||||
|
||||
invalidRefreshToken, err := algorithm.Encrypt([]byte(userID + ":" + tokenID))
|
||||
require.NoError(t, err)
|
||||
|
||||
type args struct {
|
||||
refreshToken string
|
||||
algorithm crypto.EncryptionAlgorithm
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantUserID string
|
||||
wantTokenID string
|
||||
wantToken string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "invalid base64",
|
||||
args: args{"~~~", algorithm},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-BGDhn", "Errors.User.RefreshToken.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "short cipher text",
|
||||
args: args{"DEADBEEF", algorithm},
|
||||
wantErr: zerrors.ThrowInvalidArgument(err, "DOMAIN-rie9A", "Errors.User.RefreshToken.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "incorrect amount of segments",
|
||||
args: args{base64.RawURLEncoding.EncodeToString(invalidRefreshToken), algorithm},
|
||||
wantErr: zerrors.ThrowInvalidArgument(nil, "DOMAIN-Se8oh", "Errors.User.RefreshToken.Invalid"),
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
args: args{refreshToken, algorithm},
|
||||
wantUserID: userID,
|
||||
wantTokenID: tokenID,
|
||||
wantToken: tokenID,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotUserID, gotTokenID, gotToken, err := FromRefreshToken(tt.args.refreshToken, tt.args.algorithm)
|
||||
require.ErrorIs(t, err, tt.wantErr)
|
||||
assert.Equal(t, tt.wantUserID, gotUserID)
|
||||
assert.Equal(t, tt.wantTokenID, gotTokenID)
|
||||
assert.Equal(t, tt.wantToken, gotToken)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Fuzz test invalid inputs. None of the inputs should result in a success.
|
||||
func FuzzFromRefreshToken(f *testing.F) {
|
||||
keyConfig := &crypto.KeyConfig{
|
||||
EncryptionKeyID: "keyID",
|
||||
DecryptionKeyIDs: []string{"keyID"},
|
||||
}
|
||||
keys := crypto.Keys{"keyID": "ThisKeyNeedsToHave32Characters!!"}
|
||||
algorithm, err := crypto.NewAESCrypto(keyConfig, &mockKeyStorage{keys: keys})
|
||||
require.NoError(f, err)
|
||||
|
||||
invalidRefreshToken, err := algorithm.Encrypt([]byte("userID:tokenID"))
|
||||
require.NoError(f, err)
|
||||
|
||||
tests := []string{
|
||||
"~~~", // invalid base64
|
||||
"DEADBEEF", // short cipher text
|
||||
base64.RawURLEncoding.EncodeToString(invalidRefreshToken), // incorrect amount of segments
|
||||
}
|
||||
for _, tc := range tests {
|
||||
f.Add(tc)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, refreshToken string) {
|
||||
gotUserID, gotTokenID, gotToken, err := FromRefreshToken(refreshToken, algorithm)
|
||||
target := zerrors.InvalidArgumentError{ZitadelError: new(zerrors.ZitadelError)}
|
||||
t.Log(gotUserID, gotTokenID, gotToken)
|
||||
require.ErrorAs(t, err, &target)
|
||||
})
|
||||
}
|
80
apps/api/internal/domain/request.go
Normal file
80
apps/api/internal/domain/request.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package domain
|
||||
|
||||
const (
|
||||
OrgDomainPrimaryScope = "urn:zitadel:iam:org:domain:primary:"
|
||||
OrgIDScope = "urn:zitadel:iam:org:id:"
|
||||
OrgRoleIDScope = "urn:zitadel:iam:org:roles:id:"
|
||||
OrgDomainPrimaryClaim = "urn:zitadel:iam:org:domain:primary"
|
||||
OrgIDClaim = "urn:zitadel:iam:org:id"
|
||||
ProjectIDScope = "urn:zitadel:iam:org:project:id:"
|
||||
ProjectIDScopeZITADEL = "zitadel"
|
||||
AudSuffix = ":aud"
|
||||
ProjectScopeZITADEL = ProjectIDScope + ProjectIDScopeZITADEL + AudSuffix
|
||||
SelectIDPScope = "urn:zitadel:iam:org:idp:id:"
|
||||
)
|
||||
|
||||
// TODO: Change AuthRequest to interface and let oidcauthreqesut implement it
|
||||
type Request interface {
|
||||
Type() AuthRequestType
|
||||
IsValid() bool
|
||||
}
|
||||
|
||||
type AuthRequestType int32
|
||||
|
||||
const (
|
||||
AuthRequestTypeOIDC AuthRequestType = iota
|
||||
AuthRequestTypeSAML
|
||||
AuthRequestTypeDevice
|
||||
)
|
||||
|
||||
type AuthRequestOIDC struct {
|
||||
Scopes []string
|
||||
ResponseType OIDCResponseType
|
||||
ResponseMode OIDCResponseMode
|
||||
Nonce string
|
||||
CodeChallenge *OIDCCodeChallenge
|
||||
}
|
||||
|
||||
func (a *AuthRequestOIDC) Type() AuthRequestType {
|
||||
return AuthRequestTypeOIDC
|
||||
}
|
||||
|
||||
func (a *AuthRequestOIDC) IsValid() bool {
|
||||
return len(a.Scopes) > 0 &&
|
||||
a.CodeChallenge == nil || a.CodeChallenge != nil && a.CodeChallenge.IsValid()
|
||||
}
|
||||
|
||||
type AuthRequestSAML struct {
|
||||
ID string
|
||||
BindingType string
|
||||
Code string
|
||||
Issuer string
|
||||
IssuerName string
|
||||
Destination string
|
||||
}
|
||||
|
||||
func (a *AuthRequestSAML) Type() AuthRequestType {
|
||||
return AuthRequestTypeSAML
|
||||
}
|
||||
|
||||
func (a *AuthRequestSAML) IsValid() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
type AuthRequestDevice struct {
|
||||
ClientID string
|
||||
DeviceCode string
|
||||
UserCode string
|
||||
Scopes []string
|
||||
Audience []string
|
||||
AppName string
|
||||
ProjectName string
|
||||
}
|
||||
|
||||
func (*AuthRequestDevice) Type() AuthRequestType {
|
||||
return AuthRequestTypeDevice
|
||||
}
|
||||
|
||||
func (a *AuthRequestDevice) IsValid() bool {
|
||||
return a.DeviceCode != "" && a.UserCode != ""
|
||||
}
|
41
apps/api/internal/domain/roles.go
Normal file
41
apps/api/internal/domain/roles.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
)
|
||||
|
||||
const (
|
||||
IAMRolePrefix = "IAM"
|
||||
OrgRolePrefix = "ORG"
|
||||
ProjectRolePrefix = "PROJECT"
|
||||
ProjectGrantRolePrefix = "PROJECT_GRANT"
|
||||
RoleOrgOwner = "ORG_OWNER"
|
||||
RoleOrgProjectCreator = "ORG_PROJECT_CREATOR"
|
||||
RoleIAMOwner = "IAM_OWNER"
|
||||
RoleIAMLoginClient = "IAM_LOGIN_CLIENT"
|
||||
RoleProjectOwner = "PROJECT_OWNER"
|
||||
RoleProjectOwnerGlobal = "PROJECT_OWNER_GLOBAL"
|
||||
RoleProjectGrantOwner = "PROJECT_GRANT_OWNER"
|
||||
RoleSelfManagementGlobal = "SELF_MANAGEMENT_GLOBAL"
|
||||
)
|
||||
|
||||
func CheckForInvalidRoles(roles []string, rolePrefix string, validRoles []authz.RoleMapping) []string {
|
||||
invalidRoles := make([]string, 0)
|
||||
for _, role := range roles {
|
||||
if !containsRole(role, rolePrefix, validRoles) {
|
||||
invalidRoles = append(invalidRoles, role)
|
||||
}
|
||||
}
|
||||
return invalidRoles
|
||||
}
|
||||
|
||||
func containsRole(role, rolePrefix string, validRoles []authz.RoleMapping) bool {
|
||||
for _, validRole := range validRoles {
|
||||
if role == validRole.Role && strings.HasPrefix(role, rolePrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
41
apps/api/internal/domain/saml_error_reason.go
Normal file
41
apps/api/internal/domain/saml_error_reason.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"github.com/zitadel/saml/pkg/provider"
|
||||
)
|
||||
|
||||
type SAMLErrorReason int32
|
||||
|
||||
const (
|
||||
SAMLErrorReasonUnspecified SAMLErrorReason = iota
|
||||
SAMLErrorReasonVersionMissmatch
|
||||
SAMLErrorReasonAuthNFailed
|
||||
SAMLErrorReasonInvalidAttrNameOrValue
|
||||
SAMLErrorReasonInvalidNameIDPolicy
|
||||
SAMLErrorReasonRequestDenied
|
||||
SAMLErrorReasonRequestUnsupported
|
||||
SAMLErrorReasonUnsupportedBinding
|
||||
)
|
||||
|
||||
func SAMLErrorReasonToString(reason SAMLErrorReason) string {
|
||||
switch reason {
|
||||
case SAMLErrorReasonUnspecified:
|
||||
return "unspecified error"
|
||||
case SAMLErrorReasonVersionMissmatch:
|
||||
return provider.StatusCodeVersionMissmatch
|
||||
case SAMLErrorReasonAuthNFailed:
|
||||
return provider.StatusCodeAuthNFailed
|
||||
case SAMLErrorReasonInvalidAttrNameOrValue:
|
||||
return provider.StatusCodeInvalidAttrNameOrValue
|
||||
case SAMLErrorReasonInvalidNameIDPolicy:
|
||||
return provider.StatusCodeInvalidNameIDPolicy
|
||||
case SAMLErrorReasonRequestDenied:
|
||||
return provider.StatusCodeRequestDenied
|
||||
case SAMLErrorReasonRequestUnsupported:
|
||||
return provider.StatusCodeRequestUnsupported
|
||||
case SAMLErrorReasonUnsupportedBinding:
|
||||
return provider.StatusCodeUnsupportedBinding
|
||||
default:
|
||||
return "unspecified error"
|
||||
}
|
||||
}
|
10
apps/api/internal/domain/saml_request.go
Normal file
10
apps/api/internal/domain/saml_request.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package domain
|
||||
|
||||
type SAMLRequestState int
|
||||
|
||||
const (
|
||||
SAMLRequestStateUnspecified SAMLRequestState = iota
|
||||
SAMLRequestStateAdded
|
||||
SAMLRequestStateFailed
|
||||
SAMLRequestStateSucceeded
|
||||
)
|
9
apps/api/internal/domain/saml_session.go
Normal file
9
apps/api/internal/domain/saml_session.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package domain
|
||||
|
||||
type SAMLSessionState int32
|
||||
|
||||
const (
|
||||
SAMLSessionStateUnspecified SAMLSessionState = iota
|
||||
SAMLSessionStateActive
|
||||
SAMLSessionStateTerminated
|
||||
)
|
120
apps/api/internal/domain/schema/permission.go
Normal file
120
apps/api/internal/domain/schema/permission.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed permission.schema.v1.json
|
||||
permissionJSON string
|
||||
|
||||
permissionSchema = jsonschema.MustCompileString(PermissionSchemaID, permissionJSON)
|
||||
)
|
||||
|
||||
const (
|
||||
PermissionSchemaID = "urn:zitadel:schema:permission-schema:v1"
|
||||
PermissionProperty = "urn:zitadel:schema:permission"
|
||||
)
|
||||
|
||||
type Role int32
|
||||
|
||||
const (
|
||||
RoleUnspecified Role = iota
|
||||
RoleSelf
|
||||
RoleOwner
|
||||
)
|
||||
|
||||
type permissionExtension struct {
|
||||
role Role
|
||||
}
|
||||
|
||||
// Compile implements the [jsonschema.ExtCompiler] interface.
|
||||
// It parses the permission schema extension / annotation of the passed field.
|
||||
func (c permissionExtension) Compile(ctx jsonschema.CompilerContext, m map[string]interface{}) (_ jsonschema.ExtSchema, err error) {
|
||||
perm, ok := m[PermissionProperty]
|
||||
if !ok {
|
||||
return nil, nil
|
||||
}
|
||||
p, ok := perm.(map[string]interface{})
|
||||
if !ok {
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-WR5gs", "invalid permission")
|
||||
}
|
||||
perms := new(permissions)
|
||||
for key, value := range p {
|
||||
switch key {
|
||||
case "self":
|
||||
perms.self, err = mapPermission(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
case "owner":
|
||||
perms.owner, err = mapPermission(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role")
|
||||
}
|
||||
}
|
||||
return permissionExtensionConfig{c.role, perms}, nil
|
||||
}
|
||||
|
||||
type permissionExtensionConfig struct {
|
||||
role Role
|
||||
permissions *permissions
|
||||
}
|
||||
|
||||
// Validate implements the [jsonschema.ExtSchema] interface.
|
||||
// It validates the fields of the json instance according to the permission schema.
|
||||
func (s permissionExtensionConfig) Validate(ctx jsonschema.ValidationContext, v interface{}) error {
|
||||
switch s.role {
|
||||
case RoleSelf:
|
||||
if s.permissions.self == nil || !s.permissions.self.write {
|
||||
return ctx.Error("permission", "missing required permission")
|
||||
}
|
||||
return nil
|
||||
case RoleOwner:
|
||||
if s.permissions.owner == nil || !s.permissions.owner.write {
|
||||
return ctx.Error("permission", "missing required permission")
|
||||
}
|
||||
return nil
|
||||
case RoleUnspecified:
|
||||
fallthrough
|
||||
default:
|
||||
return ctx.Error("permission", "missing required permission")
|
||||
}
|
||||
}
|
||||
|
||||
func mapPermission(value any) (*permission, error) {
|
||||
p := new(permission)
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
for _, s := range v {
|
||||
switch s {
|
||||
case 'r':
|
||||
p.read = true
|
||||
case 'w':
|
||||
p.write = true
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "SCHEMA-EZ5zjh", "invalid permission pattern: `%s` in (%s)", string(s), v)
|
||||
}
|
||||
}
|
||||
return p, nil
|
||||
default:
|
||||
return nil, zerrors.ThrowInvalidArgumentf(nil, "SCHEMA-E5h31", "invalid permission type %T (%v)", v, v)
|
||||
}
|
||||
}
|
||||
|
||||
type permissions struct {
|
||||
self *permission
|
||||
owner *permission
|
||||
}
|
||||
|
||||
type permission struct {
|
||||
read bool
|
||||
write bool
|
||||
}
|
28
apps/api/internal/domain/schema/permission.schema.v1.json
Normal file
28
apps/api/internal/domain/schema/permission.schema.v1.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"$id": "urn:zitadel:schema:permission-schema:v1",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$defs": {
|
||||
"urn:zitadel:schema:property-permission": {
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"pattern": "^[rw]$"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"urn:zitadel:schema:permission": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"owner": {
|
||||
"$ref": "#/$defs/urn:zitadel:schema:property-permission"
|
||||
},
|
||||
"self": {
|
||||
"$ref": "#/$defs/urn:zitadel:schema:property-permission"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
253
apps/api/internal/domain/schema/permission_test.go
Normal file
253
apps/api/internal/domain/schema/permission_test.go
Normal file
@@ -0,0 +1,253 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
func TestPermissionExtension(t *testing.T) {
|
||||
type args struct {
|
||||
role Role
|
||||
schema string
|
||||
instance string
|
||||
}
|
||||
type want struct {
|
||||
compilationErr error
|
||||
validationErr bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want want
|
||||
}{
|
||||
{
|
||||
"invalid permission, compilation err",
|
||||
args{
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": "read"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
want{
|
||||
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-WR5gs", "invalid permission"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid permission string, compilation err",
|
||||
args{
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"self": "read"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
want{
|
||||
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-EZ5zjh", "invalid permission pattern: `e` in (read)"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid permission type, compilation err",
|
||||
args{
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
want{
|
||||
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-E5h31", "invalid permission type bool (true)"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid Role, compilation err",
|
||||
args{
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"IAM_OWNER": "rw"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
},
|
||||
want{
|
||||
compilationErr: zerrors.ThrowInvalidArgument(nil, "SCHEMA-GFjio", "invalid permission Role"),
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid permission self, validation err",
|
||||
args{
|
||||
role: RoleSelf,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "r"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"invalid permission owner, validation err",
|
||||
args{
|
||||
role: RoleOwner,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "r",
|
||||
"self": "r"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid permission self, ok",
|
||||
args{
|
||||
role: RoleSelf,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "r",
|
||||
"self": "rw"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"valid permission owner, ok",
|
||||
args{
|
||||
role: RoleOwner,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "r"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no Role, validation err",
|
||||
args{
|
||||
role: RoleUnspecified,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"urn:zitadel:schema:permission": {
|
||||
"owner": "rw",
|
||||
"self": "rw"
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
"no permission required, ok",
|
||||
args{
|
||||
role: RoleSelf,
|
||||
schema: `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}`,
|
||||
instance: `{ "name": "test"}`,
|
||||
},
|
||||
want{
|
||||
validationErr: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
schema, err := NewSchema(tt.args.role, strings.NewReader(tt.args.schema))
|
||||
require.ErrorIs(t, err, tt.want.compilationErr)
|
||||
if tt.want.compilationErr != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var v interface{}
|
||||
err = json.Unmarshal([]byte(tt.args.instance), &v)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = schema.Validate(v)
|
||||
if tt.want.validationErr {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
41
apps/api/internal/domain/schema/schema.go
Normal file
41
apps/api/internal/domain/schema/schema.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/santhosh-tekuri/jsonschema/v5"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/zerrors"
|
||||
)
|
||||
|
||||
var (
|
||||
//go:embed zitadel.schema.v1.json
|
||||
zitadelJSON string
|
||||
)
|
||||
|
||||
const (
|
||||
MetaSchemaID = "urn:zitadel:schema:v1"
|
||||
)
|
||||
|
||||
func NewSchema(role Role, r io.Reader) (*jsonschema.Schema, error) {
|
||||
c := jsonschema.NewCompiler()
|
||||
if err := c.AddResource(PermissionSchemaID, strings.NewReader(permissionJSON)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.AddResource(MetaSchemaID, strings.NewReader(zitadelJSON)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.RegisterExtension(PermissionSchemaID, permissionSchema, permissionExtension{
|
||||
role,
|
||||
})
|
||||
if err := c.AddResource("schema.json", r); err != nil {
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-Frh42", "Errors.UserSchema.Invalid")
|
||||
}
|
||||
schema, err := c.Compile("schema.json")
|
||||
if err != nil {
|
||||
return nil, zerrors.ThrowInvalidArgument(err, "COMMA-W21tg", "Errors.UserSchema.Invalid")
|
||||
}
|
||||
return schema, nil
|
||||
}
|
13
apps/api/internal/domain/schema/zitadel.schema.v1.json
Normal file
13
apps/api/internal/domain/schema/zitadel.schema.v1.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$id": "urn:zitadel:schema:v1",
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"type": "object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "https://json-schema.org/draft/2020-12/schema"
|
||||
},
|
||||
{
|
||||
"$ref": "urn:zitadel:schema:permission-schema:v1"
|
||||
}
|
||||
]
|
||||
}
|
19
apps/api/internal/domain/search_method.go
Normal file
19
apps/api/internal/domain/search_method.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package domain
|
||||
|
||||
type SearchMethod int32
|
||||
|
||||
const (
|
||||
SearchMethodEquals SearchMethod = iota
|
||||
SearchMethodStartsWith
|
||||
SearchMethodContains
|
||||
SearchMethodEqualsIgnoreCase
|
||||
SearchMethodStartsWithIgnoreCase
|
||||
SearchMethodContainsIgnoreCase
|
||||
SearchMethodNotEquals
|
||||
SearchMethodGreaterThan
|
||||
SearchMethodLessThan
|
||||
SearchMethodIsOneOf
|
||||
SearchMethodListContains
|
||||
SearchMethodEndsWith
|
||||
SearchMethodEndsWithIgnoreCase
|
||||
)
|
33
apps/api/internal/domain/secret_generator.go
Normal file
33
apps/api/internal/domain/secret_generator.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package domain
|
||||
|
||||
//go:generate enumer -type SecretGeneratorType -transform snake -trimprefix SecretGeneratorType
|
||||
type SecretGeneratorType int32
|
||||
|
||||
const (
|
||||
SecretGeneratorTypeUnspecified SecretGeneratorType = iota
|
||||
SecretGeneratorTypeInitCode
|
||||
SecretGeneratorTypeVerifyEmailCode
|
||||
SecretGeneratorTypeVerifyPhoneCode
|
||||
SecretGeneratorTypeVerifyDomain
|
||||
SecretGeneratorTypePasswordResetCode
|
||||
SecretGeneratorTypePasswordlessInitCode
|
||||
SecretGeneratorTypeAppSecret
|
||||
SecretGeneratorTypeOTPSMS
|
||||
SecretGeneratorTypeOTPEmail
|
||||
SecretGeneratorTypeInviteCode
|
||||
SecretGeneratorTypeSigningKey
|
||||
|
||||
secretGeneratorTypeCount
|
||||
)
|
||||
|
||||
func (t SecretGeneratorType) Valid() bool {
|
||||
return t > SecretGeneratorTypeUnspecified && t < secretGeneratorTypeCount
|
||||
}
|
||||
|
||||
type SecretGeneratorState int32
|
||||
|
||||
const (
|
||||
SecretGeneratorStateUnspecified SecretGeneratorState = iota
|
||||
SecretGeneratorStateActive
|
||||
SecretGeneratorStateRemoved
|
||||
)
|
122
apps/api/internal/domain/secretgeneratortype_enumer.go
Normal file
122
apps/api/internal/domain/secretgeneratortype_enumer.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Code generated by "enumer -type SecretGeneratorType -transform snake -trimprefix SecretGeneratorType"; DO NOT EDIT.
|
||||
|
||||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const _SecretGeneratorTypeName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count"
|
||||
|
||||
var _SecretGeneratorTypeIndex = [...]uint8{0, 11, 20, 37, 54, 67, 86, 108, 118, 124, 133, 144, 155, 182}
|
||||
|
||||
const _SecretGeneratorTypeLowerName = "unspecifiedinit_codeverify_email_codeverify_phone_codeverify_domainpassword_reset_codepasswordless_init_codeapp_secretotpsmsotp_emailinvite_codesigning_keysecret_generator_type_count"
|
||||
|
||||
func (i SecretGeneratorType) String() string {
|
||||
if i < 0 || i >= SecretGeneratorType(len(_SecretGeneratorTypeIndex)-1) {
|
||||
return fmt.Sprintf("SecretGeneratorType(%d)", i)
|
||||
}
|
||||
return _SecretGeneratorTypeName[_SecretGeneratorTypeIndex[i]:_SecretGeneratorTypeIndex[i+1]]
|
||||
}
|
||||
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
func _SecretGeneratorTypeNoOp() {
|
||||
var x [1]struct{}
|
||||
_ = x[SecretGeneratorTypeUnspecified-(0)]
|
||||
_ = x[SecretGeneratorTypeInitCode-(1)]
|
||||
_ = x[SecretGeneratorTypeVerifyEmailCode-(2)]
|
||||
_ = x[SecretGeneratorTypeVerifyPhoneCode-(3)]
|
||||
_ = x[SecretGeneratorTypeVerifyDomain-(4)]
|
||||
_ = x[SecretGeneratorTypePasswordResetCode-(5)]
|
||||
_ = x[SecretGeneratorTypePasswordlessInitCode-(6)]
|
||||
_ = x[SecretGeneratorTypeAppSecret-(7)]
|
||||
_ = x[SecretGeneratorTypeOTPSMS-(8)]
|
||||
_ = x[SecretGeneratorTypeOTPEmail-(9)]
|
||||
_ = x[SecretGeneratorTypeInviteCode-(10)]
|
||||
_ = x[SecretGeneratorTypeSigningKey-(11)]
|
||||
_ = x[secretGeneratorTypeCount-(12)]
|
||||
}
|
||||
|
||||
var _SecretGeneratorTypeValues = []SecretGeneratorType{SecretGeneratorTypeUnspecified, SecretGeneratorTypeInitCode, SecretGeneratorTypeVerifyEmailCode, SecretGeneratorTypeVerifyPhoneCode, SecretGeneratorTypeVerifyDomain, SecretGeneratorTypePasswordResetCode, SecretGeneratorTypePasswordlessInitCode, SecretGeneratorTypeAppSecret, SecretGeneratorTypeOTPSMS, SecretGeneratorTypeOTPEmail, SecretGeneratorTypeInviteCode, SecretGeneratorTypeSigningKey, secretGeneratorTypeCount}
|
||||
|
||||
var _SecretGeneratorTypeNameToValueMap = map[string]SecretGeneratorType{
|
||||
_SecretGeneratorTypeName[0:11]: SecretGeneratorTypeUnspecified,
|
||||
_SecretGeneratorTypeLowerName[0:11]: SecretGeneratorTypeUnspecified,
|
||||
_SecretGeneratorTypeName[11:20]: SecretGeneratorTypeInitCode,
|
||||
_SecretGeneratorTypeLowerName[11:20]: SecretGeneratorTypeInitCode,
|
||||
_SecretGeneratorTypeName[20:37]: SecretGeneratorTypeVerifyEmailCode,
|
||||
_SecretGeneratorTypeLowerName[20:37]: SecretGeneratorTypeVerifyEmailCode,
|
||||
_SecretGeneratorTypeName[37:54]: SecretGeneratorTypeVerifyPhoneCode,
|
||||
_SecretGeneratorTypeLowerName[37:54]: SecretGeneratorTypeVerifyPhoneCode,
|
||||
_SecretGeneratorTypeName[54:67]: SecretGeneratorTypeVerifyDomain,
|
||||
_SecretGeneratorTypeLowerName[54:67]: SecretGeneratorTypeVerifyDomain,
|
||||
_SecretGeneratorTypeName[67:86]: SecretGeneratorTypePasswordResetCode,
|
||||
_SecretGeneratorTypeLowerName[67:86]: SecretGeneratorTypePasswordResetCode,
|
||||
_SecretGeneratorTypeName[86:108]: SecretGeneratorTypePasswordlessInitCode,
|
||||
_SecretGeneratorTypeLowerName[86:108]: SecretGeneratorTypePasswordlessInitCode,
|
||||
_SecretGeneratorTypeName[108:118]: SecretGeneratorTypeAppSecret,
|
||||
_SecretGeneratorTypeLowerName[108:118]: SecretGeneratorTypeAppSecret,
|
||||
_SecretGeneratorTypeName[118:124]: SecretGeneratorTypeOTPSMS,
|
||||
_SecretGeneratorTypeLowerName[118:124]: SecretGeneratorTypeOTPSMS,
|
||||
_SecretGeneratorTypeName[124:133]: SecretGeneratorTypeOTPEmail,
|
||||
_SecretGeneratorTypeLowerName[124:133]: SecretGeneratorTypeOTPEmail,
|
||||
_SecretGeneratorTypeName[133:144]: SecretGeneratorTypeInviteCode,
|
||||
_SecretGeneratorTypeLowerName[133:144]: SecretGeneratorTypeInviteCode,
|
||||
_SecretGeneratorTypeName[144:155]: SecretGeneratorTypeSigningKey,
|
||||
_SecretGeneratorTypeLowerName[144:155]: SecretGeneratorTypeSigningKey,
|
||||
_SecretGeneratorTypeName[155:182]: secretGeneratorTypeCount,
|
||||
_SecretGeneratorTypeLowerName[155:182]: secretGeneratorTypeCount,
|
||||
}
|
||||
|
||||
var _SecretGeneratorTypeNames = []string{
|
||||
_SecretGeneratorTypeName[0:11],
|
||||
_SecretGeneratorTypeName[11:20],
|
||||
_SecretGeneratorTypeName[20:37],
|
||||
_SecretGeneratorTypeName[37:54],
|
||||
_SecretGeneratorTypeName[54:67],
|
||||
_SecretGeneratorTypeName[67:86],
|
||||
_SecretGeneratorTypeName[86:108],
|
||||
_SecretGeneratorTypeName[108:118],
|
||||
_SecretGeneratorTypeName[118:124],
|
||||
_SecretGeneratorTypeName[124:133],
|
||||
_SecretGeneratorTypeName[133:144],
|
||||
_SecretGeneratorTypeName[144:155],
|
||||
_SecretGeneratorTypeName[155:182],
|
||||
}
|
||||
|
||||
// SecretGeneratorTypeString retrieves an enum value from the enum constants string name.
|
||||
// Throws an error if the param is not part of the enum.
|
||||
func SecretGeneratorTypeString(s string) (SecretGeneratorType, error) {
|
||||
if val, ok := _SecretGeneratorTypeNameToValueMap[s]; ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
if val, ok := _SecretGeneratorTypeNameToValueMap[strings.ToLower(s)]; ok {
|
||||
return val, nil
|
||||
}
|
||||
return 0, fmt.Errorf("%s does not belong to SecretGeneratorType values", s)
|
||||
}
|
||||
|
||||
// SecretGeneratorTypeValues returns all values of the enum
|
||||
func SecretGeneratorTypeValues() []SecretGeneratorType {
|
||||
return _SecretGeneratorTypeValues
|
||||
}
|
||||
|
||||
// SecretGeneratorTypeStrings returns a slice of all String values of the enum
|
||||
func SecretGeneratorTypeStrings() []string {
|
||||
strs := make([]string, len(_SecretGeneratorTypeNames))
|
||||
copy(strs, _SecretGeneratorTypeNames)
|
||||
return strs
|
||||
}
|
||||
|
||||
// IsASecretGeneratorType returns "true" if the value is listed in the enum definition. "false" otherwise
|
||||
func (i SecretGeneratorType) IsASecretGeneratorType() bool {
|
||||
for _, v := range _SecretGeneratorTypeValues {
|
||||
if i == v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
37
apps/api/internal/domain/session.go
Normal file
37
apps/api/internal/domain/session.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type SessionState int32
|
||||
|
||||
const (
|
||||
SessionStateUnspecified SessionState = iota
|
||||
SessionStateActive
|
||||
SessionStateTerminated
|
||||
)
|
||||
|
||||
type OTPEmailURLData struct {
|
||||
Code string
|
||||
UserID string
|
||||
LoginName string
|
||||
DisplayName string
|
||||
PreferredLanguage language.Tag
|
||||
SessionID string
|
||||
}
|
||||
|
||||
// RenderOTPEmailURLTemplate parses and renders tmpl.
|
||||
// code, userID, (preferred) loginName, displayName and preferredLanguage are passed into the [OTPEmailURLData].
|
||||
func RenderOTPEmailURLTemplate(w io.Writer, tmpl, code, userID, loginName, displayName, sessionID string, preferredLanguage language.Tag) error {
|
||||
return renderURLTemplate(w, tmpl, &OTPEmailURLData{
|
||||
Code: code,
|
||||
UserID: userID,
|
||||
LoginName: loginName,
|
||||
DisplayName: displayName,
|
||||
PreferredLanguage: preferredLanguage,
|
||||
SessionID: sessionID,
|
||||
})
|
||||
}
|
14
apps/api/internal/domain/sms.go
Normal file
14
apps/api/internal/domain/sms.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package domain
|
||||
|
||||
type SMSConfigState int32
|
||||
|
||||
const (
|
||||
SMSConfigStateUnspecified SMSConfigState = iota
|
||||
SMSConfigStateActive
|
||||
SMSConfigStateInactive
|
||||
SMSConfigStateRemoved
|
||||
)
|
||||
|
||||
func (s SMSConfigState) Exists() bool {
|
||||
return s != SMSConfigStateUnspecified && s != SMSConfigStateRemoved
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user