feat: Login, OP Support and Auth Queries (#177)

* fix: change oidc config

* fix: change oidc config secret

* begin models

* begin repo

* fix: implement grpc app funcs

* fix: add application requests

* fix: converter

* fix: converter

* fix: converter and generate clientid

* fix: tests

* feat: project grant aggregate

* feat: project grant

* fix: project grant check if role existing

* fix: project grant requests

* fix: project grant fixes

* fix: project grant member model

* fix: project grant member aggregate

* fix: project grant member eventstore

* fix: project grant member requests

* feat: user model

* begin repo

* repo models and more

* feat: user command side

* lots of functions

* user command side

* profile requests

* commit before rebase on user

* save

* local config with gopass and more

* begin new auth command (user centric)

* Update internal/user/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/address.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/email.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/mfa.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/password.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/phone.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/model/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/usergrant/repository/eventsourcing/user_grant.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/user_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* Update internal/user/repository/eventsourcing/eventstore_mock_test.go

Co-Authored-By: Livio Amstutz <livio.a@gmail.com>

* changes from mr review

* save files into basedir

* changes from mr review

* changes from mr review

* move to auth request

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/usergrant/repository/eventsourcing/cache.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* changes requested on mr

* fix generate codes

* fix return if no events

* password code

* email verification step

* more steps

* lot of mfa

* begin tests

* more next steps

* auth api

* auth api (user)

* auth api (user)

* auth api (user)

* differ requests

* merge

* tests

* fix compilation error

* mock for id generator

* Update internal/user/repository/eventsourcing/model/password.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* Update internal/user/repository/eventsourcing/model/user.go

Co-authored-by: Silvan <silvan.reusser@gmail.com>

* requests of mr

* check email

* begin separation of command and query

* otp

* change packages

* some cleanup and fixes

* tests for auth request / next steps

* add VerificationLifetimes to config and make it run

* tests

* fix code challenge validation

* cleanup

* fix merge

* begin view

* repackaging tests and configs

* fix startup config for auth

* add migration

* add PromptSelectAccount

* fix copy / paste

* remove user_agent files

* fixes

* fix sequences in user_session

* token commands

* token queries and signout

* fix

* fix set password test

* add token handler and table

* handle session init

* add session state

* add user view test cases

* change VerifyMyMfaOTP

* some fixes

* fix user repo in auth api

* cleanup

* add user session view test

* fix merge

* begin oidc

* user agent and more

* config

* keys

* key command and query

* add login statics

* key handler

* start login

* login handlers

* lot of fixes

* merge oidc

* add missing exports

* add missing exports

* fix some bugs

* authrequestid in htmls

* getrequest

* update auth request

* fix userid check

* add username to authrequest

* fix user session and auth request handling

* fix UserSessionsByAgentID

* fix auth request tests

* fix user session on UserPasswordChanged and MfaOtpRemoved

* fix MfaTypesSetupPossible

* handle mfa

* fill username

* auth request query checks new events

* fix userSessionByIDs

* fix tokens

* fix userSessionByIDs test

* add user selection

* init code

* user code creation date

* add init user step

* add verification failed types

* add verification failures

* verify init code

* user init code handle

* user init code handle

* fix userSessionByIDs

* update logging

* user agent cookie

* browserinfo from request

* add DeleteAuthRequest

* add static login files to binary

* add login statik to build

* move generate to separate file and remove statik.go files

* remove static dirs from startup.yaml

* generate into separate namespaces

* merge master

* auth request code

* auth request type mapping

* fix keys

* improve tokens

* improve register and basic styling

* fix ailerons font

* improve password reset

* add audience to token

* all oidc apps as audience

* fix test nextStep

* fix email texts

* remove "not set"

* lot of style changes

* improve copy to clipboard

* fix footer

* add cookie handler

* remove placeholders

* fix compilation after merge

* fix auth config

* remove comments

* typo

* use new secrets store

* change default pws to match default policy

* fixes

* add todo

* enable login

* fix db name

* Auth queries (#179)

* my usersession

* org structure/ auth handlers

* working user grant spooler

* auth internal user grants

* search my project orgs

* remove permissions file

* my zitadel permissions

* my zitadel permissions

* remove unused code

* authz

* app searches in view

* token verification

* fix user grant load

* fix tests

* fix tests

* read configs

* remove unused const

* remove todos

* env variables

* app_name

* working authz

* search projects

* global resourceowner

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* Update internal/api/auth/permissions.go

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* model2 rename

* at least it works

* check token expiry

* search my user grants

* remove token table from authz

Co-authored-by: Livio Amstutz <livio.a@gmail.com>

* fix test

* fix ports and enable console

Co-authored-by: Fabiennne <fabienne.gerschwiler@gmail.com>
Co-authored-by: Fabi <38692350+fgerschwiler@users.noreply.github.com>
Co-authored-by: Silvan <silvan.reusser@gmail.com>
This commit is contained in:
Livio Amstutz 2020-06-05 07:50:04 +02:00 committed by GitHub
parent 46b60a6968
commit 8a5badddf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
293 changed files with 14189 additions and 3176 deletions

View File

@ -101,6 +101,8 @@ jobs:
- run: go get github.com/rakyll/statik - run: go get github.com/rakyll/statik
- run: ./build/console/generate-static.sh - run: ./build/console/generate-static.sh
- run: cat pkg/console/statik/statik.go - run: cat pkg/console/statik/statik.go
- run: ./build/login/generate-static.sh
- run: cat internal/login/statik/statik.go
- run: CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel-${{ matrix.goos }}-${{ matrix.goarch }} cmd/zitadel/main.go - run: CGO_ENABLED=0 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o zitadel-${{ matrix.goos }}-${{ matrix.goarch }} cmd/zitadel/main.go
- uses: actions/upload-artifact@v1 - uses: actions/upload-artifact@v1
with: with:

2
.gitignore vendored
View File

@ -32,4 +32,4 @@ cockroach-data/*
#binaries #binaries
cmd/zitadel/zitadel cmd/zitadel/zitadel
zitadel **/statik/statik.go

View File

@ -2,4 +2,4 @@
set -eux set -eux
go generate pkg/console/console.go go generate pkg/console/statik/generate.go

5
build/login/generate-static.sh Executable file
View File

@ -0,0 +1,5 @@
#! /bin/sh
set -eux
go generate internal/login/statik/generate.go

View File

@ -1,4 +1,4 @@
AuthZ: InternalAuthZ:
RolePermissionMappings: RolePermissionMappings:
- Role: 'IAM_OWNER' - Role: 'IAM_OWNER'
Permissions: Permissions:

26
cmd/zitadel/caos_local.sh Normal file → Executable file
View File

@ -1,7 +1,9 @@
BASEDIR=$(dirname "$0") BASEDIR=$(dirname "$0")
gopass sync --store zitadel-secrets
# Tracing # Tracing
gopass citadel-secrets/citadel/developer/default/citadel-svc-account-eventstore-local | base64 -D > "$BASEDIR/local_svc-account-tracing.json" gopass zitadel-secrets/zitadel/developer/default/zitadel-svc-account-eventstore-local | base64 -D > "$BASEDIR/local_svc-account-tracing.json"
export GOOGLE_APPLICATION_CREDENTIALS="$BASEDIR/local_svc-account-tracing.json" export GOOGLE_APPLICATION_CREDENTIALS="$BASEDIR/local_svc-account-tracing.json"
export ZITADEL_TRACING_PROJECT_ID=caos-citadel-test export ZITADEL_TRACING_PROJECT_ID=caos-citadel-test
@ -15,24 +17,32 @@ export ZITADEL_EVENTSTORE_HOST=localhost
export ZITADEL_EVENTSTORE_PORT=26257 export ZITADEL_EVENTSTORE_PORT=26257
# Keys # Keys
gopass citadel-secrets/citadel/developer/default/keys.yaml > "$BASEDIR/local_keys.yaml" gopass zitadel-secrets/zitadel/developer/default/keys.yaml > "$BASEDIR/local_keys.yaml"
export ZITADEL_KEY_PATH="$BASEDIR/local_keys.yaml" export ZITADEL_KEY_PATH="$BASEDIR/local_keys.yaml"
export ZITADEL_USER_VERIFICATION_KEY=UserVerificationKey_1 export ZITADEL_USER_VERIFICATION_KEY=UserVerificationKey_1
export ZITADEL_OTP_VERIFICATION_KEY=OTPVerificationKey_1 export ZITADEL_OTP_VERIFICATION_KEY=OTPVerificationKey_1
export ZITADEL_OIDC_KEYS_ID=OIDCKey_1
export ZITADEL_COOKIE_KEY=CookieKey_1
# Notifications # Notifications
export DEBUG_MODE=TRUE export DEBUG_MODE=TRUE
export TWILIO_SERVICE_SID=$(gopass citadel-secrets/citadel/developer/default/twilio-sid) export TWILIO_SERVICE_SID=$(gopass zitadel-secrets/zitadel/dev/twilio-sid)
export TWILIO_TOKEN=$(gopass citadel-secrets/citadel/developer/default/twilio-auth-token) export TWILIO_TOKEN=$(gopass zitadel-secrets/zitadel/dev/twilio-auth-token)
export TWILIO_SENDER_NAME=CAOS AG export TWILIO_SENDER_NAME=CAOS AG
export SMTP_HOST=smtp.gmail.com:465 export SMTP_HOST=smtp.gmail.com:465
export SMTP_USER=zitadel@caos.ch export SMTP_USER=zitadel@caos.ch
export SMTP_PASSWORD=$(gopass citadel-secrets/citadel/google/emailappkey) export SMTP_PASSWORD=$(gopass zitadel-secrets/zitadel/google/emailappkey)
export EMAIL_SENDER_ADDRESS=noreply@caos.ch export EMAIL_SENDER_ADDRESS=noreply@caos.ch
export EMAIL_SENDER_NAME=CAOS AG export EMAIL_SENDER_NAME=CAOS AG
export SMTP_TLS=TRUE export SMTP_TLS=TRUE
export CHAT_URL=$(gopass citadel-secrets/citadel/developer/default/google-chat-url | base64 -D) export CHAT_URL=$(gopass zitadel-secrets/zitadel/dev/google-chat-url | base64 -D)
# Zitadel #OIDC
export ZITADEL_ACCOUNTS=http://localhost:61121 export ZITADEL_ISSUER=http://localhost:50022
export ZITADEL_ACCOUNTS=http://localhost:50031
export ZITADEL_AUTHORIZE=http://localhost:50022
export ZITADEL_OAUTH=http://localhost:50022
export ZITADEL_CONSOLE=http://localhost:4200
export CAOS_OIDC_DEV=true
export ZITADEL_COOKIE_DOMAIN=localhost

View File

@ -3,18 +3,20 @@ package main
import ( import (
"context" "context"
"flag" "flag"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing"
"github.com/caos/zitadel/internal/authz"
sd "github.com/caos/zitadel/internal/config/systemdefaults" sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/login"
"github.com/caos/logging" "github.com/caos/logging"
authz "github.com/caos/zitadel/internal/api/auth" internal_authz "github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/config" "github.com/caos/zitadel/internal/config"
"github.com/caos/zitadel/internal/notification" "github.com/caos/zitadel/internal/notification"
tracing "github.com/caos/zitadel/internal/tracing/config" tracing "github.com/caos/zitadel/internal/tracing/config"
"github.com/caos/zitadel/pkg/admin" "github.com/caos/zitadel/pkg/admin"
"github.com/caos/zitadel/pkg/auth" "github.com/caos/zitadel/pkg/auth"
"github.com/caos/zitadel/pkg/console" "github.com/caos/zitadel/pkg/console"
"github.com/caos/zitadel/pkg/login"
"github.com/caos/zitadel/pkg/management" "github.com/caos/zitadel/pkg/management"
) )
@ -22,13 +24,14 @@ type Config struct {
Mgmt management.Config Mgmt management.Config
Auth auth.Config Auth auth.Config
Login login.Config Login login.Config
AuthZ authz.Config
Admin admin.Config Admin admin.Config
Console console.Config Console console.Config
Notification notification.Config Notification notification.Config
Log logging.Config Log logging.Config
Tracing tracing.TracingConfig Tracing tracing.TracingConfig
AuthZ authz.Config InternalAuthZ internal_authz.Config
SystemDefaults sd.SystemDefaults SystemDefaults sd.SystemDefaults
} }
@ -48,18 +51,25 @@ func main() {
logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config") logging.Log("MAIN-FaF2r").OnError(err).Fatal("cannot read config")
ctx := context.Background() ctx := context.Background()
authZRepo, err := authz.Start(ctx, conf.AuthZ, conf.InternalAuthZ, conf.SystemDefaults)
logging.Log("MAIN-s9KOw").OnError(err).Fatal("error starting authz repo")
if *adminEnabled { if *adminEnabled {
admin.Start(ctx, conf.Admin, conf.AuthZ, conf.SystemDefaults) admin.Start(ctx, conf.Admin, authZRepo, conf.InternalAuthZ, conf.SystemDefaults)
} }
if *managementEnabled { if *managementEnabled {
management.Start(ctx, conf.Mgmt, conf.AuthZ, conf.SystemDefaults) management.Start(ctx, conf.Mgmt, authZRepo, conf.InternalAuthZ, conf.SystemDefaults)
}
var authRepo *eventsourcing.EsRepository
if *authEnabled || *loginEnabled {
authRepo, err = eventsourcing.Start(conf.Auth.Repository, conf.InternalAuthZ, conf.SystemDefaults, authZRepo)
logging.Log("MAIN-9oRw6").OnError(err).Fatal("error starting auth repo")
} }
if *authEnabled { if *authEnabled {
auth.Start(ctx, conf.Auth, conf.AuthZ, conf.SystemDefaults) auth.Start(ctx, conf.Auth, authZRepo, conf.InternalAuthZ, conf.SystemDefaults, authRepo)
} }
if *loginEnabled { if *loginEnabled {
err = login.Start(ctx, conf.Login) login.Start(ctx, conf.Login, conf.SystemDefaults, authRepo)
logging.Log("MAIN-53RF2").OnError(err).Fatal("error starting login ui")
} }
if *notificationEnabled { if *notificationEnabled {
notification.Start(ctx, conf.Notification, conf.SystemDefaults) notification.Start(ctx, conf.Notification, conf.SystemDefaults)

View File

@ -50,6 +50,37 @@ Auth:
GatewayPort: 50021 GatewayPort: 50021
CustomHeaders: CustomHeaders:
- x-zitadel- - x-zitadel-
OIDC:
OPConfig:
Issuer: $ZITADEL_ISSUER
DefaultLogoutRedirectURI: $ZITADEL_ACCOUNTS/logout/done
Port: 50022
StorageConfig:
DefaultLoginURL: $ZITADEL_ACCOUNTS/login?authRequestID=
DefaultAccessTokenLifetime: 12h
DefaultIdTokenLifetime: 12h
SigningKeyAlgorithm: RS256
UserAgentCookieConfig:
Name: caos.zitadel.useragent
Domain: $ZITADEL_COOKIE_DOMAIN
Key:
EncryptionKeyID: $ZITADEL_COOKIE_KEY
Endpoints:
Auth:
Path: 'authorize'
URL: '$ZITADEL_AUTHORIZE/authorize'
Token:
Path: 'token'
URL: '$ZITADEL_OAUTH/token'
EndSession:
Path: 'endsession'
URL: '$ZITADEL_AUTHORIZE/endsession'
Userinfo:
Path: 'userinfo'
URL: '$ZITADEL_OAUTH/userinfo'
Keys:
Path: 'keys'
URL: '$ZITADEL_OAUTH/keys'
Repository: Repository:
SearchLimit: 100 SearchLimit: 100
Eventstore: Eventstore:
@ -66,11 +97,12 @@ Auth:
Config: Config:
MaxCacheSizeInByte: 10485760 #10mb MaxCacheSizeInByte: 10485760 #10mb
AuthRequest: AuthRequest:
Host: $ZITADEL_EVENTSTORE_HOST Connection:
Port: $ZITADEL_EVENTSTORE_PORT Host: $ZITADEL_EVENTSTORE_HOST
User: 'auth' Port: $ZITADEL_EVENTSTORE_PORT
Database: 'auth' User: 'auth'
SSLmode: disable Database: 'auth'
SSLmode: disable
View: View:
Host: $ZITADEL_EVENTSTORE_HOST Host: $ZITADEL_EVENTSTORE_HOST
Port: $ZITADEL_EVENTSTORE_PORT Port: $ZITADEL_EVENTSTORE_PORT
@ -81,9 +113,48 @@ Auth:
ConcurrentTasks: 4 ConcurrentTasks: 4
BulkLimit: 100 BulkLimit: 100
FailureCountUntilSkip: 5 FailureCountUntilSkip: 5
KeyConfig:
Size: 2048
PrivateKeyLifetime: 6h
PublicKeyLifetime: 30h
EncryptionConfig:
EncryptionKeyID: $ZITADEL_OIDC_KEYS_ID
SigningKeyRotation: 10s
Login: Login:
# will get port range 5003x Handler:
Port: 50031
OidcAuthCallbackURL: '$ZITADEL_AUTHORIZE/authorize/'
ZitadelURL: '$ZITADEL_CONSOLE'
LanguageCookieName: 'caos.zitadel.login.lang'
DefaultLanguage: 'de'
AuthZ:
Repository:
Eventstore:
ServiceName: 'AuthZ'
Repository:
SQL:
Host: $ZITADEL_EVENTSTORE_HOST
Port: $ZITADEL_EVENTSTORE_PORT
User: 'authz'
Database: 'eventstore'
SSLmode: disable
Cache:
Type: 'fastcache'
Config:
MaxCacheSizeInByte: 10485760 #10mb
View:
Host: $ZITADEL_EVENTSTORE_HOST
Port: $ZITADEL_EVENTSTORE_PORT
User: 'authz'
Database: 'authz'
SSLmode: disable
Spooler:
ConcurrentTasks: 1
BulkLimit: 100
FailureCountUntilSkip: 5
Admin: Admin:
API: API:
@ -120,7 +191,6 @@ Admin:
Console: Console:
Port: 50050 Port: 50050
StaticDir: /app/console/dist
Notification: Notification:

View File

@ -76,7 +76,7 @@ SystemDefaults:
LastName: 'Administrator' LastName: 'Administrator'
UserName: 'zitadel-global-org-admin@caos.ch' UserName: 'zitadel-global-org-admin@caos.ch'
Email: 'zitadel-global-org-admin@caos.ch' Email: 'zitadel-global-org-admin@caos.ch'
Password: 'Password' Password: 'Password1!'
Owners: Owners:
- 'zitadel-global-org-admin@caos.ch' - 'zitadel-global-org-admin@caos.ch'
- Name: 'CAOS AG' - Name: 'CAOS AG'
@ -86,7 +86,7 @@ SystemDefaults:
LastName: 'Administrator' LastName: 'Administrator'
UserName: 'zitadel-admin@caos.ch' UserName: 'zitadel-admin@caos.ch'
Email: 'zitadel-admin@caos.ch' Email: 'zitadel-admin@caos.ch'
Password: 'Password' Password: 'Password1!'
Owners: Owners:
- 'zitadel-admin@caos.ch' - 'zitadel-admin@caos.ch'
Projects: Projects:
@ -97,9 +97,9 @@ SystemDefaults:
- Name: 'Admin-API' - Name: 'Admin-API'
- Name: 'Zitadel Console' - Name: 'Zitadel Console'
RedirectUris: RedirectUris:
- '$CITADEL_CONSOLE/auth/callback' - '$ZITADEL_CONSOLE/auth/callback'
PostLogoutRedirectUris: PostLogoutRedirectUris:
- '$CITADEL_CONSOLE/signedout' - '$ZITADEL_CONSOLE/signedout'
ResponseTypes: ResponseTypes:
- 'CODE' - 'CODE'
GrantTypes: GrantTypes:
@ -133,9 +133,9 @@ SystemDefaults:
From: $TWILIO_SENDER_NAME From: $TWILIO_SENDER_NAME
TemplateData: TemplateData:
InitCode: InitCode:
Title: 'Zitadel - User Initialisieren' Title: 'Zitadel - User initialisieren'
PreHeader: 'User Initialisieren' PreHeader: 'User initialisieren'
Subject: 'User Initialisieren' Subject: 'User initialisieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},' Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Dieser Benutzer wurde soeben im Zitadel erstellt. Du kannst den Button unten verwenden, um die Initialisierung abzuschliesen. Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.' Text: 'Dieser Benutzer wurde soeben im Zitadel erstellt. Du kannst den Button unten verwenden, um die Initialisierung abzuschliesen. Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.'
ButtonText: 'Initialisierung abschliessen' ButtonText: 'Initialisierung abschliessen'
@ -147,16 +147,16 @@ SystemDefaults:
Text: 'Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen. Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.' Text: 'Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen. Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.'
ButtonText: 'Passwort zurücksetzen' ButtonText: 'Passwort zurücksetzen'
VerifyEmail: VerifyEmail:
Title: 'Zitadel - Email Verifizieren' Title: 'Zitadel - Email verifizieren'
PreHeader: 'Email verifizieren' PreHeader: 'Email verifizieren'
Subject: 'Email verifizieren' Subject: 'Email verifizieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},' Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren. Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.' Text: 'Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren. Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.'
ButtonText: 'Passwort zurücksetzen' ButtonText: 'Email verifizieren'
VerifyPhone: VerifyPhone:
Title: 'Zitadel - Telefonnummer Verifizieren' Title: 'Zitadel - Telefonnummer verifizieren'
PreHeader: 'Telefonnummer Verifizieren' PreHeader: 'Telefonnummer verifizieren'
Subject: 'Telefonnummer Verifizieren' Subject: 'Telefonnummer verifizieren'
Greeting: 'Hallo {{.FirstName}} {{.LastName}},' Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
Text: 'Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst: {{.Code}}' Text: 'Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst: {{.Code}}'
ButtonText: 'Telefon verifizieren' ButtonText: 'Telefon verifizieren'

12
go.mod
View File

@ -11,16 +11,20 @@ require (
github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible github.com/Masterminds/sprig v2.22.0+incompatible
github.com/VictoriaMetrics/fastcache v1.5.7 github.com/VictoriaMetrics/fastcache v1.5.7
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca
github.com/allegro/bigcache v1.2.1 github.com/allegro/bigcache v1.2.1
github.com/aws/aws-sdk-go v1.30.25 // indirect github.com/aws/aws-sdk-go v1.30.25 // indirect
github.com/caos/logging v0.0.1 github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc
github.com/caos/logging v0.0.2
github.com/caos/oidc v0.6.2
github.com/cockroachdb/cockroach-go v0.0.0-20200504194139-73ffeee90b62 github.com/cockroachdb/cockroach-go v0.0.0-20200504194139-73ffeee90b62
github.com/envoyproxy/protoc-gen-validate v0.1.0 github.com/envoyproxy/protoc-gen-validate v0.1.0
github.com/ghodss/yaml v1.0.0 github.com/ghodss/yaml v1.0.0
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
github.com/golang/mock v1.4.3 github.com/golang/mock v1.4.3
github.com/golang/protobuf v1.4.1 github.com/golang/protobuf v1.4.1
github.com/google/uuid v1.1.1 // indirect github.com/gorilla/mux v1.7.4
github.com/gorilla/schema v1.1.0 github.com/gorilla/schema v1.1.0
github.com/gorilla/securecookie v1.1.1 github.com/gorilla/securecookie v1.1.1
github.com/grpc-ecosystem/go-grpc-middleware v1.2.0 github.com/grpc-ecosystem/go-grpc-middleware v1.2.0
@ -41,7 +45,6 @@ require (
github.com/pquerna/otp v1.2.0 github.com/pquerna/otp v1.2.0
github.com/rakyll/statik v0.1.7 github.com/rakyll/statik v0.1.7
github.com/rs/cors v1.7.0 github.com/rs/cors v1.7.0
github.com/sirupsen/logrus v1.6.0 // indirect
github.com/sony/sonyflake v1.0.0 github.com/sony/sonyflake v1.0.0
github.com/stretchr/testify v1.5.1 github.com/stretchr/testify v1.5.1
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
@ -49,12 +52,13 @@ require (
go.opencensus.io v0.22.3 go.opencensus.io v0.22.3
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25 // indirect golang.org/x/sys v0.0.0-20200523222454-059865788121 // indirect
golang.org/x/text v0.3.2 golang.org/x/text v0.3.2
golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a golang.org/x/tools v0.0.0-20200512001501-aaeff5de670a
google.golang.org/api v0.24.0 // indirect google.golang.org/api v0.24.0 // indirect
google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380 google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380
google.golang.org/grpc v1.29.1 google.golang.org/grpc v1.29.1
google.golang.org/protobuf v1.22.0 google.golang.org/protobuf v1.22.0
gopkg.in/square/go-jose.v2 v2.5.1
gopkg.in/yaml.v2 v2.2.8 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect
) )

30
go.sum
View File

@ -45,6 +45,10 @@ github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZC
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o=
github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw= github.com/VictoriaMetrics/fastcache v1.5.7 h1:4y6y0G8PRzszQUYIQHHssv/jgPHAb5qQuuDNdCbyAgw=
github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8= github.com/VictoriaMetrics/fastcache v1.5.7/go.mod h1:ptDBkNMQI4RtmVo8VS/XwRY6RoTu1dAWCbrk+6WsEM8=
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681 h1:eZrVcUgy0P6+B6Vu7SKPh3UZQS5nEuyjhbkFyfz7I2I=
github.com/aaronarduino/goqrsvg v0.0.0-20170617203649-603647895681/go.mod h1:dytw+5qs+pdi61fO/S4OmXR7AuEq/HvNCuG03KxQHT4=
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca h1:kWzLcty5V2rzOqJM7Tp/MfSX0RMSI1x4IOLApEefYxA=
github.com/ajstarks/svgo v0.0.0-20200320125537-f189e35d30ca/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.1-0.20190218064605-e24eb225f156/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc= github.com/allegro/bigcache v1.2.1 h1:hg1sY1raCwic3Vnsvje6TT7/pnZba83LeFck5NrFKSc=
github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM= github.com/allegro/bigcache v1.2.1/go.mod h1:Cb/ax3seSYIx7SuZdm2G2xzfwmv3TPSk2ucNfQESPXM=
@ -56,10 +60,11 @@ github.com/aws/aws-sdk-go v1.30.25 h1:89NXJkfpjnMEnsxkP8MVX+LDsoiLCSqevraLb5y4Mj
github.com/aws/aws-sdk-go v1.30.25/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0= github.com/aws/aws-sdk-go v1.30.25/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/caos/logging v0.0.1 h1:YSGtO2/+5OWdwilBCou50akoDHAT/OhkbrolkVlR6U0= github.com/caos/logging v0.0.0-20191210002624-b3260f690a6a/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
github.com/caos/logging v0.0.1 h1:YSGtO2/+5OWdwilBCou50akoDHAT/OhkbrolkVlR6U0= github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo=
github.com/caos/logging v0.0.1/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
github.com/caos/logging v0.0.1/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0= github.com/caos/oidc v0.6.2 h1:ejp6uTepVSlgLoPhXIAqFe8SbCdoxqO4vZv1H4fF0Yk=
github.com/caos/oidc v0.6.2/go.mod h1:ozoi3b+aY33gzdvjz4w90VZShIHGsmDa0goruuV0arQ=
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk= github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@ -138,6 +143,10 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -152,6 +161,10 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gorilla/handlers v1.4.2 h1:0QniY0USkHQ1RGCLfKxeNHK9bkDHGRYGNDFBCS+YARg=
github.com/gorilla/handlers v1.4.2/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY= github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@ -273,6 +286,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
@ -320,6 +334,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@ -333,6 +348,7 @@ golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191122200657-5d9234df094c/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -372,8 +388,8 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA=
golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25 h1:OKbAoGs4fGM5cPLlVQLZGYkFC8OnOfgo6tt0Smf9XhM= golang.org/x/sys v0.0.0-20200523222454-059865788121 h1:rITEj+UZHYC927n8GT97eC3zrpzXdb/voyeOuVKS46o=
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -498,6 +514,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w=
gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -3,23 +3,34 @@ package auth
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/api/auth" "github.com/caos/zitadel/internal/api/auth"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
)
const (
adminName = "Admin-API"
) )
type TokenVerifier struct { type TokenVerifier struct {
adminID string
authZRepo *authz_repo.EsRepository
} }
func Start() (v *TokenVerifier) { func Start(authZRepo *authz_repo.EsRepository) (v *TokenVerifier) {
return new(TokenVerifier) return &TokenVerifier{authZRepo: authZRepo}
} }
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) { func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
return "", "", "", nil userID, clientID, agentID, err := v.authZRepo.VerifyAccessToken(ctx, token, adminName, v.adminID)
if clientID != "" {
v.adminID = clientID
}
return userID, clientID, agentID, err
} }
func (v *TokenVerifier) ResolveGrants(ctx context.Context, userID, orgID string) ([]*auth.Grant, error) { func (v *TokenVerifier) ResolveGrant(ctx context.Context) (*auth.Grant, error) {
return nil, nil return v.authZRepo.ResolveGrants(ctx)
} }
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) { func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
return "", nil return v.authZRepo.ProjectIDByClientID(ctx, clientID)
} }

View File

@ -3,13 +3,12 @@ package eventstore
import ( import (
admin_model "github.com/caos/zitadel/internal/admin/model" admin_model "github.com/caos/zitadel/internal/admin/model"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
org_model "github.com/caos/zitadel/internal/org/model" "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing/model" usr_es "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
) )
type Setup struct { type Setup struct {
*org_es.Org *model.Org
*usr_es.User *usr_es.User
} }
@ -17,7 +16,7 @@ func (s *Setup) AppendEvents(events ...*es_models.Event) error {
for _, event := range events { for _, event := range events {
var err error var err error
switch event.AggregateType { switch event.AggregateType {
case org_model.OrgAggregate: case model.OrgAggregate:
err = s.Org.AppendEvent(event) err = s.Org.AppendEvent(event)
case usr_es.UserAggregate: case usr_es.UserAggregate:
err = s.User.AppendEvent(event) err = s.User.AppendEvent(event)
@ -31,7 +30,7 @@ func (s *Setup) AppendEvents(events ...*es_models.Event) error {
func SetupToModel(setup *Setup) *admin_model.SetupOrg { func SetupToModel(setup *Setup) *admin_model.SetupOrg {
return &admin_model.SetupOrg{ return &admin_model.SetupOrg{
Org: org_es.OrgToModel(setup.Org), Org: model.OrgToModel(setup.Org),
User: usr_es.UserToModel(setup.User), User: usr_es.UserToModel(setup.User),
} }
} }

View File

@ -1,12 +1,12 @@
package handler package handler
import ( import (
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"time" "time"
"github.com/caos/logging" "github.com/caos/logging"
es_models "github.com/caos/zitadel/internal/eventstore/models" es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler" "github.com/caos/zitadel/internal/eventstore/spooler"
org_model "github.com/caos/zitadel/internal/org/model"
"github.com/caos/zitadel/internal/org/repository/eventsourcing" "github.com/caos/zitadel/internal/org/repository/eventsourcing"
"github.com/caos/zitadel/internal/org/repository/view" "github.com/caos/zitadel/internal/org/repository/view"
) )
@ -37,9 +37,9 @@ func (o *Org) Process(event *es_models.Event) error {
org := new(view.OrgView) org := new(view.OrgView)
switch event.Type { switch event.Type {
case org_model.OrgAdded: case model.OrgAdded:
org.AppendEvent(event) org.AppendEvent(event)
case org_model.OrgChanged: case model.OrgChanged:
err := org.SetData(event) err := org.SetData(event)
if err != nil { if err != nil {
return err return err

View File

@ -125,7 +125,7 @@ func (s *Setup) Execute(ctx context.Context) error {
err = setUp.setIamProject(ctx) err = setUp.setIamProject(ctx)
if err != nil { if err != nil {
logging.Log("SETUP-kaWjq").WithError(err).Error("unable to set citadel project") logging.Log("SETUP-kaWjq").WithError(err).Error("unable to set zitadel project")
return err return err
} }

View File

@ -11,9 +11,9 @@ import (
type key int type key int
var ( const (
permissionsKey key permissionsKey key = 1
dataKey key dataKey key = 2
) )
type CtxData struct { type CtxData struct {
@ -36,7 +36,7 @@ type Grant struct {
type TokenVerifier interface { type TokenVerifier interface {
VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error)
ResolveGrants(ctx context.Context, sub, orgID string) ([]*Grant, error) ResolveGrant(ctx context.Context) (*Grant, error)
GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error)
} }

View File

@ -11,21 +11,20 @@ func getUserMethodPermissions(ctx context.Context, t TokenVerifier, requiredPerm
if ctxData.IsZero() { if ctxData.IsZero() {
return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing") return nil, nil, errors.ThrowUnauthenticated(nil, "AUTH-rKLWEH", "context missing")
} }
grants, err := t.ResolveGrants(ctx, ctxData.UserID, ctxData.OrgID) grant, err := t.ResolveGrant(ctx)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
permissions := mapGrantsToPermissions(requiredPerm, grants, authConfig) permissions := mapGrantToPermissions(requiredPerm, grant, authConfig)
return context.WithValue(ctx, permissionsKey, permissions), permissions, nil return context.WithValue(ctx, permissionsKey, permissions), permissions, nil
} }
func mapGrantsToPermissions(requiredPerm string, grants []*Grant, authConfig *Config) []string { func mapGrantToPermissions(requiredPerm string, grant *Grant, authConfig *Config) []string {
resolvedPermissions := make([]string, 0) resolvedPermissions := make([]string, 0)
for _, grant := range grants { for _, role := range grant.Roles {
for _, role := range grant.Roles { resolvedPermissions = mapRoleToPerm(requiredPerm, role, authConfig, resolvedPermissions)
resolvedPermissions = mapRoleToPerm(requiredPerm, role, authConfig, resolvedPermissions)
}
} }
return resolvedPermissions return resolvedPermissions
} }
@ -36,7 +35,7 @@ func mapRoleToPerm(requiredPerm, actualRole string, authConfig *Config, resolved
for _, p := range perms { for _, p := range perms {
if p == requiredPerm { if p == requiredPerm {
p = addRoleContextIDToPerm(p, roleContextID) p = addRoleContextIDToPerm(p, roleContextID)
if !existsPerm(resolvedPermissions, p) { if !ExistsPerm(resolvedPermissions, p) {
resolvedPermissions = append(resolvedPermissions, p) resolvedPermissions = append(resolvedPermissions, p)
} }
} }
@ -51,7 +50,7 @@ func addRoleContextIDToPerm(perm, roleContextID string) string {
return perm return perm
} }
func existsPerm(existing []string, perm string) bool { func ExistsPerm(existing []string, perm string) bool {
for _, e := range existing { for _, e := range existing {
if e == perm { if e == perm {
return true return true

View File

@ -12,15 +12,15 @@ func getTestCtx(userID, orgID string) context.Context {
} }
type testVerifier struct { type testVerifier struct {
grants []*Grant grant *Grant
} }
func (v *testVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) { func (v *testVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
return "userID", "clientID", "agentID", nil return "userID", "clientID", "agentID", nil
} }
func (v *testVerifier) ResolveGrants(ctx context.Context, sub, orgID string) ([]*Grant, error) { func (v *testVerifier) ResolveGrant(ctx context.Context) (*Grant, error) {
return v.grants, nil return v.grant, nil
} }
func (v *testVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) { func (v *testVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
@ -57,8 +57,8 @@ func Test_GetUserMethodPermissions(t *testing.T) {
name: "Empty Context", name: "Empty Context",
args: args{ args: args{
ctx: getTestCtx("", ""), ctx: getTestCtx("", ""),
verifier: &testVerifier{grants: []*Grant{&Grant{ verifier: &testVerifier{grant: &Grant{
Roles: []string{"ORG_OWNER"}}}}, Roles: []string{"ORG_OWNER"}}},
requiredPerm: "project.read", requiredPerm: "project.read",
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
@ -81,7 +81,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
name: "No Grants", name: "No Grants",
args: args{ args: args{
ctx: getTestCtx("", ""), ctx: getTestCtx("", ""),
verifier: &testVerifier{grants: []*Grant{}}, verifier: &testVerifier{grant: &Grant{}},
requiredPerm: "project.read", requiredPerm: "project.read",
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
@ -102,8 +102,8 @@ func Test_GetUserMethodPermissions(t *testing.T) {
name: "Get Permissions", name: "Get Permissions",
args: args{ args: args{
ctx: getTestCtx("userID", "orgID"), ctx: getTestCtx("userID", "orgID"),
verifier: &testVerifier{grants: []*Grant{&Grant{ verifier: &testVerifier{grant: &Grant{
Roles: []string{"ORG_OWNER"}}}}, Roles: []string{"ORG_OWNER"}}},
requiredPerm: "project.read", requiredPerm: "project.read",
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
@ -143,7 +143,7 @@ func Test_GetUserMethodPermissions(t *testing.T) {
func Test_MapGrantsToPermissions(t *testing.T) { func Test_MapGrantsToPermissions(t *testing.T) {
type args struct { type args struct {
requiredPerm string requiredPerm string
grants []*Grant grant *Grant
authConfig *Config authConfig *Config
} }
tests := []struct { tests := []struct {
@ -155,8 +155,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
name: "One Role existing perm", name: "One Role existing perm",
args: args{ args: args{
requiredPerm: "project.read", requiredPerm: "project.read",
grants: []*Grant{&Grant{ grant: &Grant{Roles: []string{"ORG_OWNER"}},
Roles: []string{"ORG_OWNER"}}},
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
RoleMapping{ RoleMapping{
@ -176,8 +175,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
name: "One Role not existing perm", name: "One Role not existing perm",
args: args{ args: args{
requiredPerm: "project.write", requiredPerm: "project.write",
grants: []*Grant{&Grant{ grant: &Grant{Roles: []string{"ORG_OWNER"}},
Roles: []string{"ORG_OWNER"}}},
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
RoleMapping{ RoleMapping{
@ -197,8 +195,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
name: "Multiple Roles one existing", name: "Multiple Roles one existing",
args: args{ args: args{
requiredPerm: "project.read", requiredPerm: "project.read",
grants: []*Grant{&Grant{ grant: &Grant{Roles: []string{"ORG_OWNER", "IAM_OWNER"}},
Roles: []string{"ORG_OWNER", "IAM_OWNER"}}},
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
RoleMapping{ RoleMapping{
@ -218,8 +215,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
name: "Multiple Roles, global and specific", name: "Multiple Roles, global and specific",
args: args{ args: args{
requiredPerm: "project.read", requiredPerm: "project.read",
grants: []*Grant{&Grant{ grant: &Grant{Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}},
Roles: []string{"ORG_OWNER", "PROJECT_OWNER:1"}}},
authConfig: &Config{ authConfig: &Config{
RolePermissionMappings: []RoleMapping{ RolePermissionMappings: []RoleMapping{
RoleMapping{ RoleMapping{
@ -238,7 +234,7 @@ func Test_MapGrantsToPermissions(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := mapGrantsToPermissions(tt.args.requiredPerm, tt.args.grants, tt.args.authConfig) result := mapGrantToPermissions(tt.args.requiredPerm, tt.args.grant, tt.args.authConfig)
if !equalStringArray(result, tt.result) { if !equalStringArray(result, tt.result) {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
} }
@ -419,7 +415,7 @@ func Test_ExistisPerm(t *testing.T) {
} }
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
result := existsPerm(tt.args.existing, tt.args.perm) result := ExistsPerm(tt.args.existing, tt.args.perm)
if result != tt.result { if result != tt.result {
t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result) t.Errorf("got wrong result, expecting: %v, actual: %v ", tt.result, result)
} }

View File

@ -7,6 +7,8 @@ const (
ContentType = "content-type" ContentType = "content-type"
Location = "location" Location = "location"
Origin = "origin" Origin = "origin"
UserAgent = "user-agent"
ForwardedFor = "x-forwarded-for"
ZitadelOrgID = "x-zitadel-orgid" ZitadelOrgID = "x-zitadel-orgid"
//TODO: Remove as soon an authentification is implemented //TODO: Remove as soon an authentification is implemented

View File

@ -0,0 +1,71 @@
package http
import (
"context"
"net"
"net/http"
"strings"
"github.com/caos/zitadel/internal/api"
)
type key int
var (
httpHeaders key
remoteAddr key
)
func CopyHeadersToContext(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), httpHeaders, r.Header)
ctx = context.WithValue(ctx, remoteAddr, r.RemoteAddr)
r = r.WithContext(ctx)
h(w, r)
}
}
func HeadersFromCtx(ctx context.Context) (http.Header, bool) {
headers, ok := ctx.Value(httpHeaders).(http.Header)
return headers, ok
}
func RemoteIPFromCtx(ctx context.Context) string {
ctxHeaders, ok := HeadersFromCtx(ctx)
if !ok {
return RemoteAddrFromCtx(ctx)
}
forwarded, ok := ForwardedFor(ctxHeaders)
if ok {
return forwarded
}
return RemoteAddrFromCtx(ctx)
}
func RemoteIPFromRequest(r *http.Request) net.IP {
return net.ParseIP(RemoteIPStringFromRequest(r))
}
func RemoteIPStringFromRequest(r *http.Request) string {
ip, ok := ForwardedFor(r.Header)
if ok {
return ip
}
return r.RemoteAddr
}
func ForwardedFor(headers http.Header) (string, bool) {
forwarded, ok := headers[api.ForwardedFor]
if ok {
ip := strings.Split(forwarded[0], ", ")[0]
if ip != "" {
return ip, true
}
}
return "", false
}
func RemoteAddrFromCtx(ctx context.Context) string {
ctxRemoteAddr, _ := ctx.Value(remoteAddr).(string)
return ctxRemoteAddr
}

View File

@ -0,0 +1,68 @@
package http
import (
"net/http"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/id"
)
type UserAgent struct {
ID string
}
type UserAgentHandler struct {
handler *CookieHandler
cookieName string
idGenerator id.Generator
}
type UserAgentCookieConfig struct {
Name string
Domain string
Key *crypto.KeyConfig
}
func NewUserAgentHandler(config *UserAgentCookieConfig, idGenerator id.Generator) (*UserAgentHandler, error) {
keys, _, err := crypto.LoadKeys(config.Key)
if err != nil {
return nil, err
}
cookieKey := []byte(keys[config.Key.EncryptionKeyID])
handler := NewCookieHandler(
WithEncryption(cookieKey, cookieKey),
WithDomain(config.Domain),
WithUnsecure(),
)
return &UserAgentHandler{
cookieName: config.Name,
handler: handler,
idGenerator: idGenerator,
}, nil
}
func (ua *UserAgentHandler) NewUserAgent() (*UserAgent, error) {
agentID, err := ua.idGenerator.Next()
if err != nil {
return nil, err
}
return &UserAgent{ID: agentID}, nil
}
func (ua *UserAgentHandler) GetUserAgent(r *http.Request) (*UserAgent, error) {
userAgent := new(UserAgent)
err := ua.handler.GetEncryptedCookieValue(r, ua.cookieName, userAgent)
if err != nil {
return nil, errors.ThrowPermissionDenied(err, "HTTP-YULqH4", "cannot read user agent cookie")
}
return userAgent, nil
}
func (ua *UserAgentHandler) SetUserAgent(w http.ResponseWriter, agent *UserAgent) error {
err := ua.handler.SetEncryptedCookie(w, ua.cookieName, agent)
if err != nil {
return errors.ThrowPermissionDenied(err, "HTTP-AqgqdA", "cannot set user agent cookie")
}
return nil
}

View File

@ -3,23 +3,34 @@ package auth
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/api/auth" "github.com/caos/zitadel/internal/api/auth"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
)
const (
authName = "Auth-API"
) )
type TokenVerifier struct { type TokenVerifier struct {
authID string
authZRepo *authz_repo.EsRepository
} }
func Start() (v *TokenVerifier) { func Start(authZRepo *authz_repo.EsRepository) (v *TokenVerifier) {
return new(TokenVerifier) return &TokenVerifier{authZRepo: authZRepo}
} }
func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) { func (v *TokenVerifier) VerifyAccessToken(ctx context.Context, token string) (string, string, string, error) {
return "", "", "", nil userID, clientID, agentID, err := v.authZRepo.VerifyAccessToken(ctx, token, authName, v.authID)
if clientID != "" {
v.authID = clientID
}
return userID, clientID, agentID, err
} }
func (v *TokenVerifier) ResolveGrants(ctx context.Context, userID, orgID string) ([]*auth.Grant, error) { func (v *TokenVerifier) ResolveGrant(ctx context.Context) (*auth.Grant, error) {
return nil, nil return v.authZRepo.ResolveGrants(ctx)
} }
func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) { func (v *TokenVerifier) GetProjectIDByClientID(ctx context.Context, clientID string) (string, error) {
return "", nil return v.authZRepo.ProjectIDByClientID(ctx, clientID)
} }

View File

@ -1,4 +0,0 @@
package auth
type Config struct {
}

View File

@ -0,0 +1,12 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/project/model"
)
type ApplicationRepository interface {
ApplicationByClientID(ctx context.Context, clientID string) (*model.ApplicationView, error)
AuthorizeOIDCApplication(ctx context.Context, clientID, secret string) error
}

View File

@ -9,7 +9,12 @@ import (
type AuthRequestRepository interface { type AuthRequestRepository interface {
CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*model.AuthRequest, error) CreateAuthRequest(ctx context.Context, request *model.AuthRequest) (*model.AuthRequest, error)
AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error)
AuthRequestByIDCheckLoggedIn(ctx context.Context, id string) (*model.AuthRequest, error)
AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error)
SaveAuthCode(ctx context.Context, id, code string) error
DeleteAuthRequest(ctx context.Context, id string) error
CheckUsername(ctx context.Context, id, username string) error CheckUsername(ctx context.Context, id, username string) error
SelectUser(ctx context.Context, id, userID string) error
VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error
VerifyMfaOTP(ctx context.Context, agentID, authRequestID string, code string, info *model.BrowserInfo) error VerifyMfaOTP(ctx context.Context, agentID, authRequestID string, code string, info *model.BrowserInfo) error
} }

View File

@ -0,0 +1,31 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/project/model"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_view_model "github.com/caos/zitadel/internal/project/repository/view/model"
)
type ApplicationRepo struct {
View *view.View
ProjectEvents *proj_event.ProjectEventstore
}
func (a *ApplicationRepo) ApplicationByClientID(ctx context.Context, clientID string) (*model.ApplicationView, error) {
app, err := a.View.ApplicationByClientID(ctx, clientID)
if err != nil {
return nil, err
}
return proj_view_model.ApplicationViewToModel(app), nil
}
func (a *ApplicationRepo) AuthorizeOIDCApplication(ctx context.Context, clientID, secret string) error {
app, err := a.View.ApplicationByClientID(ctx, clientID)
if err != nil {
return err
}
return a.ProjectEvents.VerifyOIDCClientSecret(ctx, app.ProjectID, app.ID, secret)
}

View File

@ -4,23 +4,28 @@ import (
"context" "context"
"time" "time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/auth_request/repository/cache" cache "github.com/caos/zitadel/internal/auth_request/repository"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"
user_model "github.com/caos/zitadel/internal/user/model" user_model "github.com/caos/zitadel/internal/user/model"
user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model" view_model "github.com/caos/zitadel/internal/user/repository/view/model"
) )
type AuthRequestRepo struct { type AuthRequestRepo struct {
UserEvents *user_event.UserEventstore UserEvents *user_event.UserEventstore
AuthRequests *cache.AuthRequestCache AuthRequests cache.AuthRequestCache
View *view.View View *view.View
UserSessionViewProvider userSessionViewProvider UserSessionViewProvider userSessionViewProvider
UserViewProvider userViewProvider UserViewProvider userViewProvider
UserEventProvider userEventProvider
IdGenerator id.Generator IdGenerator id.Generator
@ -38,6 +43,10 @@ type userViewProvider interface {
UserByID(string) (*view_model.UserView, error) UserByID(string) (*view_model.UserView, error)
} }
type userEventProvider interface {
UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error)
}
func (repo *AuthRequestRepo) Health(ctx context.Context) error { func (repo *AuthRequestRepo) Health(ctx context.Context) error {
if err := repo.UserEvents.Health(ctx); err != nil { if err := repo.UserEvents.Health(ctx); err != nil {
return err return err
@ -51,6 +60,11 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod
return nil, err return nil, err
} }
request.ID = reqID request.ID = reqID
ids, err := repo.View.AppIDsFromProjectByClientID(ctx, request.ApplicationID)
if err != nil {
return nil, err
}
request.Audience = ids
err = repo.AuthRequests.SaveAuthRequest(ctx, request) err = repo.AuthRequests.SaveAuthRequest(ctx, request)
if err != nil { if err != nil {
return nil, err return nil, err
@ -59,11 +73,28 @@ func (repo *AuthRequestRepo) CreateAuthRequest(ctx context.Context, request *mod
} }
func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) { func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) {
return repo.getAuthRequest(ctx, id, false)
}
func (repo *AuthRequestRepo) AuthRequestByIDCheckLoggedIn(ctx context.Context, id string) (*model.AuthRequest, error) {
return repo.getAuthRequest(ctx, id, true)
}
func (repo *AuthRequestRepo) SaveAuthCode(ctx context.Context, id, code string) error {
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
if err != nil {
return err
}
request.Code = code
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) AuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error) {
request, err := repo.AuthRequests.GetAuthRequestByCode(ctx, code)
if err != nil { if err != nil {
return nil, err return nil, err
} }
steps, err := repo.nextSteps(request) steps, err := repo.nextSteps(ctx, request, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,6 +102,10 @@ func (repo *AuthRequestRepo) AuthRequestByID(ctx context.Context, id string) (*m
return request, nil return request, nil
} }
func (repo *AuthRequestRepo) DeleteAuthRequest(ctx context.Context, id string) error {
return repo.AuthRequests.DeleteAuthRequest(ctx, id)
}
func (repo *AuthRequestRepo) CheckUsername(ctx context.Context, id, username string) error { func (repo *AuthRequestRepo) CheckUsername(ctx context.Context, id, username string) error {
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id) request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
if err != nil { if err != nil {
@ -80,8 +115,21 @@ func (repo *AuthRequestRepo) CheckUsername(ctx context.Context, id, username str
if err != nil { if err != nil {
return err return err
} }
request.UserID = user.ID request.SetUserInfo(user.ID, user.UserName, user.ResourceOwner)
return repo.AuthRequests.SaveAuthRequest(ctx, request) return repo.AuthRequests.UpdateAuthRequest(ctx, request)
}
func (repo *AuthRequestRepo) SelectUser(ctx context.Context, id, userID string) error {
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
if err != nil {
return err
}
user, err := repo.View.UserByID(userID)
if err != nil {
return err
}
request.SetUserInfo(user.ID, user.UserName, user.ResourceOwner)
return repo.AuthRequests.UpdateAuthRequest(ctx, request)
} }
func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error { func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, password string, info *model.BrowserInfo) error {
@ -89,8 +137,8 @@ func (repo *AuthRequestRepo) VerifyPassword(ctx context.Context, id, userID, pas
if err != nil { if err != nil {
return err return err
} }
if request.UserID == userID { if request.UserID != userID {
return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "user id does not match request id ") return errors.ThrowPreconditionFailed(nil, "EVENT-ds35D", "user id does not match request id")
} }
return repo.UserEvents.CheckPassword(ctx, userID, password, request.WithCurrentInfo(info)) return repo.UserEvents.CheckPassword(ctx, userID, password, request.WithCurrentInfo(info))
} }
@ -106,15 +154,29 @@ func (repo *AuthRequestRepo) VerifyMfaOTP(ctx context.Context, authRequestID, us
return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info)) return repo.UserEvents.CheckMfaOTP(ctx, userID, code, request.WithCurrentInfo(info))
} }
func (repo *AuthRequestRepo) nextSteps(request *model.AuthRequest) ([]model.NextStep, error) { func (repo *AuthRequestRepo) getAuthRequest(ctx context.Context, id string, checkLoggedIn bool) (*model.AuthRequest, error) {
request, err := repo.AuthRequests.GetAuthRequestByID(ctx, id)
if err != nil {
return nil, err
}
steps, err := repo.nextSteps(ctx, request, checkLoggedIn)
if err != nil {
return nil, err
}
request.PossibleSteps = steps
return request, nil
}
func (repo *AuthRequestRepo) nextSteps(ctx context.Context, request *model.AuthRequest, checkLoggedIn bool) ([]model.NextStep, error) {
if request == nil { if request == nil {
return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "request must not be nil") return nil, errors.ThrowInvalidArgument(nil, "EVENT-ds27a", "request must not be nil")
} }
steps := make([]model.NextStep, 0) steps := make([]model.NextStep, 0)
if !checkLoggedIn && request.Prompt == model.PromptNone {
return append(steps, &model.RedirectToCallbackStep{}), nil
}
if request.UserID == "" { if request.UserID == "" {
if request.Prompt != model.PromptNone { steps = append(steps, &model.LoginStep{})
steps = append(steps, &model.LoginStep{})
}
if request.Prompt == model.PromptSelectAccount { if request.Prompt == model.PromptSelectAccount {
users, err := repo.usersForUserSelection(request) users, err := repo.usersForUserSelection(request)
if err != nil { if err != nil {
@ -124,15 +186,18 @@ func (repo *AuthRequestRepo) nextSteps(request *model.AuthRequest) ([]model.Next
} }
return steps, nil return steps, nil
} }
userSession, err := userSessionByIDs(repo.UserSessionViewProvider, request.AgentID, request.UserID) user, err := userByID(ctx, repo.UserViewProvider, repo.UserEventProvider, request.UserID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
user, err := userByID(repo.UserViewProvider, request.UserID) userSession, err := userSessionByIDs(ctx, repo.UserSessionViewProvider, repo.UserEventProvider, request.AgentID, user)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if user.InitRequired {
return append(steps, &model.InitUserStep{PasswordSet: user.PasswordSet}), nil
}
if !user.PasswordSet { if !user.PasswordSet {
return append(steps, &model.InitPasswordStep{}), nil return append(steps, &model.InitPasswordStep{}), nil
} }
@ -140,6 +205,8 @@ func (repo *AuthRequestRepo) nextSteps(request *model.AuthRequest) ([]model.Next
if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) { if !checkVerificationTime(userSession.PasswordVerification, repo.PasswordCheckLifeTime) {
return append(steps, &model.PasswordStep{}), nil return append(steps, &model.PasswordStep{}), nil
} }
request.PasswordVerified = true
request.AuthTime = userSession.PasswordVerification
if step, ok := repo.mfaChecked(userSession, request, user); !ok { if step, ok := repo.mfaChecked(userSession, request, user); !ok {
return append(steps, step), nil return append(steps, step), nil
@ -178,23 +245,32 @@ func (repo *AuthRequestRepo) usersForUserSelection(request *model.AuthRequest) (
func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *model.AuthRequest, user *user_model.UserView) (model.NextStep, bool) { func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView, request *model.AuthRequest, user *user_model.UserView) (model.NextStep, bool) {
mfaLevel := request.MfaLevel() mfaLevel := request.MfaLevel()
required := user.MfaMaxSetUp < mfaLevel promptRequired := user.MfaMaxSetUp < mfaLevel
if required || !repo.mfaSkippedOrSetUp(user) { if promptRequired || !repo.mfaSkippedOrSetUp(user) {
return &model.MfaPromptStep{ return &model.MfaPromptStep{
Required: required, Required: promptRequired,
MfaProviders: user.MfaTypesSetupPossible(mfaLevel), MfaProviders: user.MfaTypesSetupPossible(mfaLevel),
}, false }, false
} }
switch mfaLevel { switch mfaLevel {
default: default:
fallthrough fallthrough
case model.MfaLevelNotSetUp:
if user.MfaMaxSetUp == model.MfaLevelNotSetUp {
return nil, true
}
fallthrough
case model.MfaLevelSoftware: case model.MfaLevelSoftware:
if checkVerificationTime(userSession.MfaSoftwareVerification, repo.MfaSoftwareCheckLifeTime) { if checkVerificationTime(userSession.MfaSoftwareVerification, repo.MfaSoftwareCheckLifeTime) {
request.MfasVerified = append(request.MfasVerified, userSession.MfaSoftwareVerificationType)
request.AuthTime = userSession.MfaSoftwareVerification
return nil, true return nil, true
} }
fallthrough fallthrough
case model.MfaLevelHardware: case model.MfaLevelHardware:
if checkVerificationTime(userSession.MfaHardwareVerification, repo.MfaHardwareCheckLifeTime) { if checkVerificationTime(userSession.MfaHardwareVerification, repo.MfaHardwareCheckLifeTime) {
request.MfasVerified = append(request.MfasVerified, userSession.MfaHardwareVerificationType)
request.AuthTime = userSession.MfaHardwareVerification
return nil, true return nil, true
} }
} }
@ -204,7 +280,7 @@ func (repo *AuthRequestRepo) mfaChecked(userSession *user_model.UserSessionView,
} }
func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool { func (repo *AuthRequestRepo) mfaSkippedOrSetUp(user *user_model.UserView) bool {
if user.MfaMaxSetUp >= 0 { if user.MfaMaxSetUp > model.MfaLevelNotSetUp {
return true return true
} }
return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime) return checkVerificationTime(user.MfaInitSkipped, repo.MfaInitSkippedLifeTime)
@ -222,18 +298,55 @@ func userSessionsByUserAgentID(provider userSessionViewProvider, agentID string)
return view_model.UserSessionsToModel(session), nil return view_model.UserSessionsToModel(session), nil
} }
func userSessionByIDs(provider userSessionViewProvider, agentID, userID string) (*user_model.UserSessionView, error) { func userSessionByIDs(ctx context.Context, provider userSessionViewProvider, eventProvider userEventProvider, agentID string, user *user_model.UserView) (*user_model.UserSessionView, error) {
session, err := provider.UserSessionByIDs(agentID, userID) session, err := provider.UserSessionByIDs(agentID, user.ID)
if err != nil { if err != nil {
return nil, err if !errors.IsNotFound(err) {
return nil, err
}
session = &view_model.UserSessionView{}
} }
return view_model.UserSessionToModel(session), nil events, err := eventProvider.UserEventsByID(ctx, user.ID, session.Sequence)
if err != nil {
logging.Log("EVENT-Hse6s").WithError(err).Debug("error retrieving new events")
return view_model.UserSessionToModel(session), nil
}
sessionCopy := *session
for _, event := range events {
switch event.Type {
case es_model.UserPasswordCheckSucceeded,
es_model.UserPasswordCheckFailed,
es_model.MfaOtpCheckSucceeded,
es_model.MfaOtpCheckFailed:
eventData, err := view_model.UserSessionFromEvent(event)
if err != nil {
logging.Log("EVENT-sdgT3").WithError(err).Debug("error getting event data")
return view_model.UserSessionToModel(session), nil
}
if eventData.UserAgentID != agentID {
continue
}
}
sessionCopy.AppendEvent(event)
}
return view_model.UserSessionToModel(&sessionCopy), nil
} }
func userByID(provider userViewProvider, userID string) (*user_model.UserView, error) { func userByID(ctx context.Context, viewProvider userViewProvider, eventProvider userEventProvider, userID string) (*user_model.UserView, error) {
user, err := provider.UserByID(userID) user, err := viewProvider.UserByID(userID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return view_model.UserToModel(user), nil events, err := eventProvider.UserEventsByID(ctx, userID, user.Sequence)
if err != nil {
logging.Log("EVENT-dfg42").WithError(err).Debug("error retrieving new events")
return view_model.UserToModel(user), nil
}
userCopy := *user
for _, event := range events {
if err := userCopy.AppendEvent(event); err != nil {
return view_model.UserToModel(user), nil
}
}
return view_model.UserToModel(&userCopy), nil
} }

View File

@ -1,6 +1,8 @@
package eventstore package eventstore
import ( import (
"context"
"encoding/json"
"testing" "testing"
"time" "time"
@ -10,8 +12,10 @@ import (
"github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/auth_request/repository/cache" "github.com/caos/zitadel/internal/auth_request/repository/cache"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
user_model "github.com/caos/zitadel/internal/user/model" user_model "github.com/caos/zitadel/internal/user/model"
user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" user_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
user_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/user/repository/view/model" view_model "github.com/caos/zitadel/internal/user/repository/view/model"
) )
@ -25,6 +29,16 @@ func (m *mockViewNoUserSession) UserSessionsByAgentID(string) ([]*view_model.Use
return nil, errors.ThrowInternal(nil, "id", "internal error") return nil, errors.ThrowInternal(nil, "id", "internal error")
} }
type mockViewErrUserSession struct{}
func (m *mockViewErrUserSession) UserSessionByIDs(string, string) (*view_model.UserSessionView, error) {
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
func (m *mockViewErrUserSession) UserSessionsByAgentID(string) ([]*view_model.UserSessionView, error) {
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
type mockViewUserSession struct { type mockViewUserSession struct {
PasswordVerification time.Time PasswordVerification time.Time
MfaSoftwareVerification time.Time MfaSoftwareVerification time.Time
@ -60,7 +74,26 @@ func (m *mockViewNoUser) UserByID(string) (*view_model.UserView, error) {
return nil, errors.ThrowNotFound(nil, "id", "user not found") return nil, errors.ThrowNotFound(nil, "id", "user not found")
} }
type mockEventUser struct {
Event *es_models.Event
}
func (m *mockEventUser) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) {
events := make([]*es_models.Event, 0)
if m.Event != nil {
events = append(events, m.Event)
}
return events, nil
}
type mockEventErrUser struct{}
func (m *mockEventErrUser) UserEventsByID(ctx context.Context, id string, sequence uint64) ([]*es_models.Event, error) {
return nil, errors.ThrowInternal(nil, "id", "internal error")
}
type mockViewUser struct { type mockViewUser struct {
InitRequired bool
PasswordSet bool PasswordSet bool
PasswordChangeRequired bool PasswordChangeRequired bool
IsEmailVerified bool IsEmailVerified bool
@ -71,6 +104,7 @@ type mockViewUser struct {
func (m *mockViewUser) UserByID(string) (*view_model.UserView, error) { func (m *mockViewUser) UserByID(string) (*view_model.UserView, error) {
return &view_model.UserView{ return &view_model.UserView{
InitRequired: m.InitRequired,
PasswordSet: m.PasswordSet, PasswordSet: m.PasswordSet,
PasswordChangeRequired: m.PasswordChangeRequired, PasswordChangeRequired: m.PasswordChangeRequired,
IsEmailVerified: m.IsEmailVerified, IsEmailVerified: m.IsEmailVerified,
@ -87,13 +121,15 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
View *view.View View *view.View
userSessionViewProvider userSessionViewProvider userSessionViewProvider userSessionViewProvider
userViewProvider userViewProvider userViewProvider userViewProvider
userEventProvider userEventProvider
PasswordCheckLifeTime time.Duration PasswordCheckLifeTime time.Duration
MfaInitSkippedLifeTime time.Duration MfaInitSkippedLifeTime time.Duration
MfaSoftwareCheckLifeTime time.Duration MfaSoftwareCheckLifeTime time.Duration
MfaHardwareCheckLifeTime time.Duration MfaHardwareCheckLifeTime time.Duration
} }
type args struct { type args struct {
request *model.AuthRequest request *model.AuthRequest
checkLoggedIn bool
} }
tests := []struct { tests := []struct {
name string name string
@ -105,22 +141,22 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
{ {
"request nil, error", "request nil, error",
fields{}, fields{},
args{nil}, args{nil, false},
nil, nil,
errors.IsErrorInvalidArgument, errors.IsErrorInvalidArgument,
}, },
{ {
"user not set, login step", "prompt none and checkLoggedIn false, callback step",
fields{}, fields{},
args{&model.AuthRequest{}}, args{&model.AuthRequest{Prompt: model.PromptNone}, false},
[]model.NextStep{&model.LoginStep{}}, []model.NextStep{&model.RedirectToCallbackStep{}},
nil, nil,
}, },
{ {
"user not set and prompt none, no step", "user not set, login step",
fields{}, fields{},
args{&model.AuthRequest{Prompt: model.PromptNone}}, args{&model.AuthRequest{}, false},
[]model.NextStep{}, []model.NextStep{&model.LoginStep{}},
nil, nil,
}, },
{ {
@ -128,7 +164,7 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
fields{ fields{
userSessionViewProvider: &mockViewNoUserSession{}, userSessionViewProvider: &mockViewNoUserSession{},
}, },
args{&model.AuthRequest{Prompt: model.PromptSelectAccount}}, args{&model.AuthRequest{Prompt: model.PromptSelectAccount}, false},
nil, nil,
errors.IsInternal, errors.IsInternal,
}, },
@ -147,8 +183,9 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
}, },
}, },
userEventProvider: &mockEventUser{},
}, },
args{&model.AuthRequest{Prompt: model.PromptSelectAccount}}, args{&model.AuthRequest{Prompt: model.PromptSelectAccount}, false},
[]model.NextStep{ []model.NextStep{
&model.LoginStep{}, &model.LoginStep{},
&model.SelectUserStep{ &model.SelectUserStep{
@ -166,31 +203,63 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
nil, nil,
}, },
{ {
"usersession not found, not found error", "user not not found, not found error",
fields{ fields{
userSessionViewProvider: &mockViewNoUserSession{}, userViewProvider: &mockViewNoUser{},
userEventProvider: &mockEventUser{},
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
nil, nil,
errors.IsNotFound, errors.IsNotFound,
}, },
{ {
"user not not found, not found error", "usersession not found, new user session, password step",
fields{
userSessionViewProvider: &mockViewNoUserSession{},
userViewProvider: &mockViewUser{
PasswordSet: true,
},
userEventProvider: &mockEventUser{},
},
args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.PasswordStep{}},
nil,
},
{
"usersession error, internal error",
fields{
userSessionViewProvider: &mockViewErrUserSession{},
userViewProvider: &mockViewUser{},
userEventProvider: &mockEventUser{},
},
args{&model.AuthRequest{UserID: "UserID"}, false},
nil,
errors.IsInternal,
},
{
"user not initialized, init user step",
fields{ fields{
userSessionViewProvider: &mockViewUserSession{}, userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewNoUser{}, userViewProvider: &mockViewUser{
InitRequired: true,
PasswordSet: true,
},
userEventProvider: &mockEventUser{},
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.InitUserStep{
PasswordSet: true,
}},
nil, nil,
errors.IsNotFound,
}, },
{ {
"password not set, init password step", "password not set, init password step",
fields{ fields{
userSessionViewProvider: &mockViewUserSession{}, userSessionViewProvider: &mockViewUserSession{},
userViewProvider: &mockViewUser{}, userViewProvider: &mockViewUser{},
userEventProvider: &mockEventUser{},
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.InitPasswordStep{}}, []model.NextStep{&model.InitPasswordStep{}},
nil, nil,
}, },
@ -201,9 +270,10 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.PasswordStep{}}, []model.NextStep{&model.PasswordStep{}},
nil, nil,
}, },
@ -218,10 +288,11 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
OTPState: int32(user_model.MFASTATE_READY), OTPState: int32(user_model.MFASTATE_READY),
MfaMaxSetUp: int32(model.MfaLevelSoftware), MfaMaxSetUp: int32(model.MfaLevelSoftware),
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour, MfaSoftwareCheckLifeTime: 18 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.MfaVerificationStep{ []model.NextStep{&model.MfaVerificationStep{
MfaProviders: []model.MfaType{model.MfaTypeOTP}, MfaProviders: []model.MfaType{model.MfaTypeOTP},
}}, }},
@ -238,11 +309,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
PasswordSet: true, PasswordSet: true,
PasswordChangeRequired: true, PasswordChangeRequired: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour, MfaSoftwareCheckLifeTime: 18 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.ChangePasswordStep{}}, []model.NextStep{&model.ChangePasswordStep{}},
nil, nil,
}, },
@ -255,11 +328,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
}, },
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour, MfaSoftwareCheckLifeTime: 18 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.VerifyEMailStep{}}, []model.NextStep{&model.VerifyEMailStep{}},
nil, nil,
}, },
@ -273,11 +348,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
PasswordChangeRequired: true, PasswordChangeRequired: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour, MfaSoftwareCheckLifeTime: 18 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.ChangePasswordStep{}, &model.VerifyEMailStep{}}, []model.NextStep{&model.ChangePasswordStep{}, &model.VerifyEMailStep{}},
nil, nil,
}, },
@ -291,11 +368,33 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
userViewProvider: &mockViewUser{ userViewProvider: &mockViewUser{
PasswordSet: true, PasswordSet: true,
IsEmailVerified: true, IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
}, },
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour, PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour, MfaSoftwareCheckLifeTime: 18 * time.Hour,
}, },
args{&model.AuthRequest{UserID: "UserID"}}, args{&model.AuthRequest{UserID: "UserID"}, false},
[]model.NextStep{&model.RedirectToCallbackStep{}},
nil,
},
{
"prompt none, checkLoggedIn true and authenticated, redirect to callback step",
fields{
userSessionViewProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Add(-5 * time.Minute),
MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Minute),
},
userViewProvider: &mockViewUser{
PasswordSet: true,
IsEmailVerified: true,
MfaMaxSetUp: int32(model.MfaLevelSoftware),
},
userEventProvider: &mockEventUser{},
PasswordCheckLifeTime: 10 * 24 * time.Hour,
MfaSoftwareCheckLifeTime: 18 * time.Hour,
},
args{&model.AuthRequest{UserID: "UserID", Prompt: model.PromptNone}, true},
[]model.NextStep{&model.RedirectToCallbackStep{}}, []model.NextStep{&model.RedirectToCallbackStep{}},
nil, nil,
}, },
@ -308,12 +407,13 @@ func TestAuthRequestRepo_nextSteps(t *testing.T) {
View: tt.fields.View, View: tt.fields.View,
UserSessionViewProvider: tt.fields.userSessionViewProvider, UserSessionViewProvider: tt.fields.userSessionViewProvider,
UserViewProvider: tt.fields.userViewProvider, UserViewProvider: tt.fields.userViewProvider,
UserEventProvider: tt.fields.userEventProvider,
PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime, PasswordCheckLifeTime: tt.fields.PasswordCheckLifeTime,
MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime, MfaInitSkippedLifeTime: tt.fields.MfaInitSkippedLifeTime,
MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime, MfaSoftwareCheckLifeTime: tt.fields.MfaSoftwareCheckLifeTime,
MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime, MfaHardwareCheckLifeTime: tt.fields.MfaHardwareCheckLifeTime,
} }
got, err := repo.nextSteps(tt.args.request) got, err := repo.nextSteps(context.Background(), tt.args.request, tt.args.checkLoggedIn)
if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) { if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) {
t.Errorf("nextSteps() wrong error = %v", err) t.Errorf("nextSteps() wrong error = %v", err)
return return
@ -360,14 +460,31 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{ args{
request: &model.AuthRequest{}, request: &model.AuthRequest{},
user: &user_model.UserView{ user: &user_model.UserView{
MfaMaxSetUp: -1, MfaMaxSetUp: model.MfaLevelNotSetUp,
}, },
}, },
&model.MfaPromptStep{ &model.MfaPromptStep{
MfaProviders: []model.MfaType{}, MfaProviders: []model.MfaType{
model.MfaTypeOTP,
},
}, },
false, false,
}, },
{
"not set up and skipped, true",
fields{
MfaInitSkippedLifeTime: 30 * 24 * time.Hour,
},
args{
request: &model.AuthRequest{},
user: &user_model.UserView{
MfaMaxSetUp: model.MfaLevelNotSetUp,
MfaInitSkipped: time.Now().UTC(),
},
},
nil,
true,
},
{ {
"checked mfa software, true", "checked mfa software, true",
fields{ fields{
@ -376,7 +493,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{ args{
request: &model.AuthRequest{}, request: &model.AuthRequest{},
user: &user_model.UserView{ user: &user_model.UserView{
OTPState: user_model.MFASTATE_READY, MfaMaxSetUp: model.MfaLevelSoftware,
OTPState: user_model.MFASTATE_READY,
}, },
userSession: &user_model.UserSessionView{MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Hour)}, userSession: &user_model.UserSessionView{MfaSoftwareVerification: time.Now().UTC().Add(-5 * time.Hour)},
}, },
@ -391,7 +509,8 @@ func TestAuthRequestRepo_mfaChecked(t *testing.T) {
args{ args{
request: &model.AuthRequest{}, request: &model.AuthRequest{},
user: &user_model.UserView{ user: &user_model.UserView{
OTPState: user_model.MFASTATE_READY, MfaMaxSetUp: model.MfaLevelSoftware,
OTPState: user_model.MFASTATE_READY,
}, },
userSession: &user_model.UserSessionView{}, userSession: &user_model.UserSessionView{},
}, },
@ -473,3 +592,235 @@ func TestAuthRequestRepo_mfaSkippedOrSetUp(t *testing.T) {
}) })
} }
} }
func Test_userSessionByIDs(t *testing.T) {
type args struct {
userProvider userSessionViewProvider
eventProvider userEventProvider
agentID string
user *user_model.UserView
}
tests := []struct {
name string
args args
want *user_model.UserSessionView
wantErr func(error) bool
}{
{
"not found, new session",
args{
userProvider: &mockViewNoUserSession{},
eventProvider: &mockEventErrUser{},
user: &user_model.UserView{ID: "id"},
},
&user_model.UserSessionView{},
nil,
},
{
"internal error, internal error",
args{
userProvider: &mockViewErrUserSession{},
user: &user_model.UserView{ID: "id"},
},
nil,
errors.IsInternal,
},
{
"error user events, old view model state",
args{
userProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
},
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventErrUser{},
},
&user_model.UserSessionView{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
MfaSoftwareVerification: time.Time{},
MfaHardwareVerification: time.Time{},
},
nil,
},
{
"new user events but error, old view model state",
args{
userProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
},
agentID: "agentID",
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventUser{
&es_models.Event{
AggregateType: user_es_model.UserAggregate,
Type: user_es_model.MfaOtpCheckSucceeded,
CreationDate: time.Now().UTC().Round(1 * time.Second),
},
},
},
&user_model.UserSessionView{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
MfaSoftwareVerification: time.Time{},
MfaHardwareVerification: time.Time{},
},
nil,
},
{
"new user events but other agentID, old view model state",
args{
userProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
},
agentID: "agentID",
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventUser{
&es_models.Event{
AggregateType: user_es_model.UserAggregate,
Type: user_es_model.MfaOtpCheckSucceeded,
CreationDate: time.Now().UTC().Round(1 * time.Second),
Data: func() []byte {
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "otherID"})
return data
}(),
},
},
},
&user_model.UserSessionView{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
MfaSoftwareVerification: time.Time{},
MfaHardwareVerification: time.Time{},
},
nil,
},
{
"new user events, new view model state",
args{
userProvider: &mockViewUserSession{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
},
agentID: "agentID",
user: &user_model.UserView{ID: "id"},
eventProvider: &mockEventUser{
&es_models.Event{
AggregateType: user_es_model.UserAggregate,
Type: user_es_model.MfaOtpCheckSucceeded,
CreationDate: time.Now().UTC().Round(1 * time.Second),
Data: func() []byte {
data, _ := json.Marshal(&user_es_model.AuthRequest{UserAgentID: "agentID"})
return data
}(),
},
},
},
&user_model.UserSessionView{
PasswordVerification: time.Now().UTC().Round(1 * time.Second),
MfaSoftwareVerification: time.Now().UTC().Round(1 * time.Second),
ChangeDate: time.Now().UTC().Round(1 * time.Second),
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userSessionByIDs(context.Background(), tt.args.userProvider, tt.args.eventProvider, tt.args.agentID, tt.args.user)
if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) {
t.Errorf("nextSteps() wrong error = %v", err)
return
}
assert.Equal(t, tt.want, got)
})
}
}
func Test_userByID(t *testing.T) {
type args struct {
ctx context.Context
viewProvider userViewProvider
eventProvider userEventProvider
userID string
}
tests := []struct {
name string
args args
want *user_model.UserView
wantErr func(error) bool
}{
{
"not found, not found error",
args{
viewProvider: &mockViewNoUser{},
},
nil,
errors.IsNotFound,
},
{
"error user events, old view model state",
args{
viewProvider: &mockViewUser{
PasswordChangeRequired: true,
},
eventProvider: &mockEventErrUser{},
},
&user_model.UserView{
PasswordChangeRequired: true,
},
nil,
},
{
"new user events but error, old view model state",
args{
viewProvider: &mockViewUser{
PasswordChangeRequired: true,
},
eventProvider: &mockEventUser{
&es_models.Event{
AggregateType: user_es_model.UserAggregate,
Type: user_es_model.UserPasswordChanged,
CreationDate: time.Now().UTC().Round(1 * time.Second),
Data: nil,
},
},
},
&user_model.UserView{
PasswordChangeRequired: true,
},
nil,
},
{
"new user events, new view model state",
args{
viewProvider: &mockViewUser{
PasswordChangeRequired: true,
},
eventProvider: &mockEventUser{
&es_models.Event{
AggregateType: user_es_model.UserAggregate,
Type: user_es_model.UserPasswordChanged,
CreationDate: time.Now().UTC().Round(1 * time.Second),
Data: func() []byte {
data, _ := json.Marshal(user_es_model.Password{ChangeRequired: false})
return data
}(),
},
},
},
&user_model.UserView{
PasswordChangeRequired: false,
ChangeDate: time.Now().UTC().Round(1 * time.Second),
State: user_model.USERSTATE_INITIAL,
PasswordChanged: time.Now().UTC().Round(1 * time.Second),
},
nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := userByID(tt.args.ctx, tt.args.viewProvider, tt.args.eventProvider, tt.args.userID)
if (err != nil && tt.wantErr == nil) || (tt.wantErr != nil && !tt.wantErr(err)) {
t.Errorf("nextSteps() wrong error = %v", err)
return
}
assert.Equal(t, tt.want, got)
})
}
}

View File

@ -0,0 +1,16 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/iam/model"
iam_event "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
)
type IamRepository struct {
IamID string
IamEvents *iam_event.IamEventstore
}
func (repo *IamRepository) GetIam(ctx context.Context) (*model.Iam, error) {
return repo.IamEvents.IamByID(ctx, repo.IamID)
}

View File

@ -0,0 +1,75 @@
package eventstore
import (
"context"
"time"
"gopkg.in/square/go-jose.v2"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/key/model"
key_event "github.com/caos/zitadel/internal/key/repository/eventsourcing"
)
const (
oidcUser = "OIDC"
iamOrg = "IAM"
)
type KeyRepository struct {
KeyEvents *key_event.KeyEventstore
View *view.View
SigningKeyRotation time.Duration
}
func (k *KeyRepository) GenerateSigningKeyPair(ctx context.Context, algorithm string) error {
ctx = setOIDCCtx(ctx)
_, err := k.KeyEvents.GenerateKeyPair(ctx, model.KeyUsageSigning, algorithm)
return err
}
func (k *KeyRepository) GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, renewTimer <-chan time.Time) {
go func() {
for {
select {
case <-ctx.Done():
return
case <-renewTimer:
k.refreshSigningKey(keyCh, errCh)
renewTimer = time.After(k.SigningKeyRotation)
}
}
}()
}
func (k *KeyRepository) GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error) {
keys, err := k.View.GetActiveKeySet()
if err != nil {
return nil, err
}
webKeys := make([]jose.JSONWebKey, len(keys))
for i, key := range keys {
webKeys[i] = jose.JSONWebKey{KeyID: key.ID, Algorithm: key.Algorithm, Use: key.Usage.String(), Key: key.Key}
}
return &jose.JSONWebKeySet{Keys: webKeys}, nil
}
func (k *KeyRepository) refreshSigningKey(keyCh chan<- jose.SigningKey, errCh chan<- error) {
key, err := k.View.GetSigningKey()
if err != nil {
errCh <- err
return
}
keyCh <- jose.SigningKey{
Algorithm: jose.SignatureAlgorithm(key.Algorithm),
Key: jose.JSONWebKey{
KeyID: key.ID,
Key: key.Key,
},
}
}
func setOIDCCtx(ctx context.Context) context.Context {
return auth.SetCtxData(ctx, auth.CtxData{UserID: oidcUser, OrgID: iamOrg})
}

View File

@ -0,0 +1,29 @@
package eventstore
import (
"context"
auth_view "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
org_model "github.com/caos/zitadel/internal/org/model"
org_es "github.com/caos/zitadel/internal/org/repository/eventsourcing"
"github.com/caos/zitadel/internal/org/repository/view"
)
type OrgRepository struct {
SearchLimit uint64
*org_es.OrgEventstore
View *auth_view.View
}
func (repo *OrgRepository) SearchOrgs(ctx context.Context, request *org_model.OrgSearchRequest) (*org_model.OrgSearchResult, error) {
request.EnsureLimit(repo.SearchLimit)
members, count, err := repo.View.SearchOrgs(request)
if err != nil {
return nil, err
}
return &org_model.OrgSearchResult{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: view.OrgsToModel(members),
}, nil
}

View File

@ -13,8 +13,8 @@ type TokenRepo struct {
View *view.View View *view.View
} }
func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, lifetime time.Duration) (*token_model.Token, error) { func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*token_model.Token, error) {
token, err := repo.View.CreateToken(agentID, applicationID, userID, lifetime) token, err := repo.View.CreateToken(agentID, applicationID, userID, audience, scopes, lifetime)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -24,3 +24,11 @@ func (repo *TokenRepo) CreateToken(ctx context.Context, agentID, applicationID,
func (repo *TokenRepo) IsTokenValid(ctx context.Context, tokenID string) (bool, error) { func (repo *TokenRepo) IsTokenValid(ctx context.Context, tokenID string) (bool, error) {
return repo.View.IsTokenValid(tokenID) return repo.View.IsTokenValid(tokenID)
} }
func (repo *TokenRepo) TokenByID(ctx context.Context, tokenID string) (*token_model.Token, error) {
token, err := repo.View.TokenByID(tokenID)
if err != nil {
return nil, err
}
return token_view_model.TokenToModel(token), nil
}

View File

@ -56,10 +56,18 @@ func (repo *UserRepo) ChangeMyEmail(ctx context.Context, email *model.Email) (*m
return repo.UserEvents.ChangeEmail(ctx, email) return repo.UserEvents.ChangeEmail(ctx, email)
} }
func (repo *UserRepo) VerifyEmail(ctx context.Context, userID, code string) error {
return repo.UserEvents.VerifyEmail(ctx, userID, code)
}
func (repo *UserRepo) VerifyMyEmail(ctx context.Context, code string) error { func (repo *UserRepo) VerifyMyEmail(ctx context.Context, code string) error {
return repo.UserEvents.VerifyEmail(ctx, auth.GetCtxData(ctx).UserID, code) return repo.UserEvents.VerifyEmail(ctx, auth.GetCtxData(ctx).UserID, code)
} }
func (repo *UserRepo) ResendEmailVerificationMail(ctx context.Context, userID string) error {
return repo.UserEvents.CreateEmailVerificationCode(ctx, userID)
}
func (repo *UserRepo) ResendMyEmailVerificationMail(ctx context.Context) error { func (repo *UserRepo) ResendMyEmailVerificationMail(ctx context.Context) error {
return repo.UserEvents.CreateEmailVerificationCode(ctx, auth.GetCtxData(ctx).UserID) return repo.UserEvents.CreateEmailVerificationCode(ctx, auth.GetCtxData(ctx).UserID)
} }
@ -103,11 +111,28 @@ func (repo *UserRepo) ChangeMyPassword(ctx context.Context, old, new string) err
return err return err
} }
func (repo *UserRepo) ChangePassword(ctx context.Context, userID, old, new string) error {
policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID)
if err != nil {
return err
}
_, err = repo.UserEvents.ChangePassword(ctx, policy, userID, old, new)
return err
}
func (repo *UserRepo) AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error) {
return repo.UserEvents.AddOTP(ctx, userID)
}
func (repo *UserRepo) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) { func (repo *UserRepo) AddMyMfaOTP(ctx context.Context) (*model.OTP, error) {
return repo.UserEvents.AddOTP(ctx, auth.GetCtxData(ctx).UserID) return repo.UserEvents.AddOTP(ctx, auth.GetCtxData(ctx).UserID)
} }
func (repo *UserRepo) VerifyMyMfaOTP(ctx context.Context, code string) error { func (repo *UserRepo) VerifyMfaOTPSetup(ctx context.Context, userID, code string) error {
return repo.UserEvents.CheckMfaOTPSetup(ctx, userID, code)
}
func (repo *UserRepo) VerifyMyMfaOTPSetup(ctx context.Context, code string) error {
return repo.UserEvents.CheckMfaOTPSetup(ctx, auth.GetCtxData(ctx).UserID, code) return repo.UserEvents.CheckMfaOTPSetup(ctx, auth.GetCtxData(ctx).UserID, code)
} }
@ -115,6 +140,19 @@ func (repo *UserRepo) RemoveMyMfaOTP(ctx context.Context) error {
return repo.UserEvents.RemoveOTP(ctx, auth.GetCtxData(ctx).UserID) return repo.UserEvents.RemoveOTP(ctx, auth.GetCtxData(ctx).UserID)
} }
func (repo *UserRepo) ResendInitVerificationMail(ctx context.Context, userID string) error {
_, err := repo.UserEvents.CreateInitializeUserCodeByID(ctx, userID)
return err
}
func (repo *UserRepo) VerifyInitCode(ctx context.Context, userID, code, password string) error {
policy, err := repo.PolicyEvents.GetPasswordComplexityPolicy(ctx, auth.GetCtxData(ctx).OrgID)
if err != nil {
return err
}
return repo.UserEvents.VerifyInitCode(ctx, policy, userID, code, password)
}
func (repo *UserRepo) SkipMfaInit(ctx context.Context, userID string) error { func (repo *UserRepo) SkipMfaInit(ctx context.Context, userID string) error {
return repo.UserEvents.SkipMfaInit(ctx, userID) return repo.UserEvents.SkipMfaInit(ctx, userID)
} }
@ -139,6 +177,10 @@ func (repo *UserRepo) SignOut(ctx context.Context, agentID, userID string) error
return repo.UserEvents.SignOut(ctx, agentID, userID) return repo.UserEvents.SignOut(ctx, agentID, userID)
} }
func (repo *UserRepo) UserByID(ctx context.Context, userID string) (*model.User, error) {
return repo.UserEvents.UserByID(ctx, userID)
}
func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error { func checkIDs(ctx context.Context, obj es_models.ObjectRoot) error {
if obj.AggregateID != auth.GetCtxData(ctx).UserID { if obj.AggregateID != auth.GetCtxData(ctx).UserID {
return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user") return errors.ThrowPermissionDenied(nil, "EVENT-kFi9w", "object does not belong to user")

View File

@ -0,0 +1,158 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
caos_errs "github.com/caos/zitadel/internal/errors"
global_model "github.com/caos/zitadel/internal/model"
org_model "github.com/caos/zitadel/internal/org/model"
org_view "github.com/caos/zitadel/internal/org/repository/view"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/caos/zitadel/internal/usergrant/repository/view/model"
)
type UserGrantRepo struct {
SearchLimit uint64
View *view.View
IamID string
Auth auth.Config
AuthZRepo *authz_repo.EsRepository
}
func (repo *UserGrantRepo) SearchMyUserGrants(ctx context.Context, request *grant_model.UserGrantSearchRequest) (*grant_model.UserGrantSearchResponse, error) {
request.EnsureLimit(repo.SearchLimit)
request.Queries = append(request.Queries, &grant_model.UserGrantSearchQuery{Key: grant_model.USERGRANTSEARCHKEY_USER_ID, Method: global_model.SEARCHMETHOD_EQUALS, Value: auth.GetCtxData(ctx).UserID})
grants, count, err := repo.View.SearchUserGrants(request)
if err != nil {
return nil, err
}
return &grant_model.UserGrantSearchResponse{
Offset: request.Offset,
Limit: request.Limit,
TotalResult: uint64(count),
Result: model.UserGrantsToModel(grants),
}, nil
}
func (repo *UserGrantRepo) SearchMyProjectOrgs(ctx context.Context, request *grant_model.UserGrantSearchRequest) (*grant_model.ProjectOrgSearchResponse, error) {
request.EnsureLimit(repo.SearchLimit)
ctxData := auth.GetCtxData(ctx)
if ctxData.ProjectID == "" {
return nil, caos_errs.ThrowPreconditionFailed(nil, "APP-7lqva", "Could not get ProjectID")
}
if ctxData.ProjectID == repo.AuthZRepo.IamProjectID {
isAdmin, err := repo.IsIamAdmin(ctx)
if err != nil {
return nil, err
}
if isAdmin {
return repo.SearchAdminOrgs(request)
}
}
request.Queries = append(request.Queries, &grant_model.UserGrantSearchQuery{Key: grant_model.USERGRANTSEARCHKEY_PROJECT_ID, Method: global_model.SEARCHMETHOD_EQUALS, Value: ctxData.ProjectID})
grants, err := repo.SearchMyUserGrants(ctx, request)
if err != nil {
return nil, err
}
return grantRespToOrgResp(grants), nil
}
func (repo *UserGrantRepo) SearchMyZitadelPermissions(ctx context.Context) ([]string, error) {
grant, err := repo.AuthZRepo.ResolveGrants(ctx)
if err != nil {
return nil, err
}
permissions := &grant_model.Permissions{Permissions: []string{}}
for _, role := range grant.Roles {
roleName, ctxID := auth.SplitPermission(role)
for _, mapping := range repo.Auth.RolePermissionMappings {
if mapping.Role == roleName {
permissions.AppendPermissions(ctxID, mapping.Permissions...)
}
}
}
return permissions.Permissions, nil
}
func (repo *UserGrantRepo) SearchAdminOrgs(request *grant_model.UserGrantSearchRequest) (*grant_model.ProjectOrgSearchResponse, error) {
searchRequest := &org_model.OrgSearchRequest{}
if len(request.Queries) > 0 {
for _, q := range request.Queries {
if q.Key == grant_model.USERGRANTSEARCHKEY_ORG_NAME {
searchRequest.Queries = append(searchRequest.Queries, &org_model.OrgSearchQuery{Key: org_model.ORGSEARCHKEY_ORG_NAME, Method: q.Method, Value: q.Value})
}
}
}
orgs, count, err := repo.View.SearchOrgs(searchRequest)
if err != nil {
return nil, err
}
return orgRespToOrgResp(orgs, count), nil
}
func (repo *UserGrantRepo) IsIamAdmin(ctx context.Context) (bool, error) {
grantSearch := &grant_model.UserGrantSearchRequest{
Queries: []*grant_model.UserGrantSearchQuery{
&grant_model.UserGrantSearchQuery{Key: grant_model.USERGRANTSEARCHKEY_RESOURCEOWNER, Method: global_model.SEARCHMETHOD_EQUALS, Value: repo.IamID},
}}
result, err := repo.SearchMyUserGrants(ctx, grantSearch)
if err != nil {
return false, err
}
if result.TotalResult == 0 {
return false, nil
}
return true, nil
}
func grantRespToOrgResp(grants *grant_model.UserGrantSearchResponse) *grant_model.ProjectOrgSearchResponse {
resp := &grant_model.ProjectOrgSearchResponse{
TotalResult: grants.TotalResult,
}
resp.Result = make([]*grant_model.Org, len(grants.Result))
for i, g := range grants.Result {
resp.Result[i] = &grant_model.Org{OrgID: g.ResourceOwner, OrgName: g.OrgName}
}
return resp
}
func orgRespToOrgResp(orgs []*org_view.OrgView, count int) *grant_model.ProjectOrgSearchResponse {
resp := &grant_model.ProjectOrgSearchResponse{
TotalResult: uint64(count),
}
resp.Result = make([]*grant_model.Org, len(orgs))
for i, o := range orgs {
resp.Result[i] = &grant_model.Org{OrgID: o.ID, OrgName: o.Name}
}
return resp
}
func mergeOrgAndAdminGrant(ctxData auth.CtxData, orgGrant, iamAdminGrant *model.UserGrantView) (grant *auth.Grant) {
if orgGrant != nil {
roles := orgGrant.RoleKeys
if iamAdminGrant != nil {
roles = addIamAdminRoles(roles, iamAdminGrant.RoleKeys)
}
grant = &auth.Grant{OrgID: orgGrant.ResourceOwner, Roles: roles}
} else if iamAdminGrant != nil {
grant = &auth.Grant{
OrgID: ctxData.OrgID,
Roles: iamAdminGrant.RoleKeys,
}
}
return grant
}
func addIamAdminRoles(orgRoles, iamAdminRoles []string) []string {
result := make([]string, 0)
result = append(result, iamAdminRoles...)
for _, role := range orgRoles {
if !auth.ExistsPerm(result, role) {
result = append(result, role)
}
}
return result
}

View File

@ -0,0 +1,21 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
usr_model "github.com/caos/zitadel/internal/user/model"
"github.com/caos/zitadel/internal/user/repository/view/model"
)
type UserSessionRepo struct {
View *view.View
}
func (repo *UserSessionRepo) GetMyUserSessions(ctx context.Context) ([]*usr_model.UserSessionView, error) {
userSessions, err := repo.View.UserSessionsByUserID(auth.GetCtxData(ctx).UserID)
if err != nil {
return nil, err
}
return model.UserSessionsToModel(userSessions), nil
}

View File

@ -0,0 +1,74 @@
package handler
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
"time"
)
type Application struct {
handler
projectEvents *proj_event.ProjectEventstore
}
const (
applicationTable = "auth.applications"
)
func (p *Application) MinimumCycleDuration() time.Duration { return p.cycleDuration }
func (p *Application) ViewModel() string {
return applicationTable
}
func (p *Application) EventQuery() (*models.SearchQuery, error) {
sequence, err := p.view.GetLatestApplicationSequence()
if err != nil {
return nil, err
}
return eventsourcing.ProjectQuery(sequence), nil
}
func (p *Application) Process(event *models.Event) (err error) {
app := new(view_model.ApplicationView)
switch event.Type {
case es_model.ApplicationAdded:
app.AppendEvent(event)
case es_model.ApplicationChanged,
es_model.OIDCConfigAdded,
es_model.OIDCConfigChanged,
es_model.ApplicationDeactivated,
es_model.ApplicationReactivated:
err := app.SetData(event)
if err != nil {
return err
}
app, err = p.view.ApplicationByID(app.ID)
if err != nil {
return err
}
app.AppendEvent(event)
case es_model.ApplicationRemoved:
err := app.SetData(event)
if err != nil {
return err
}
return p.view.DeleteApplication(app.ID, event.Sequence)
default:
return p.view.ProcessedApplicationSequence(event.Sequence)
}
if err != nil {
return err
}
return p.view.PutApplication(app)
}
func (p *Application) OnError(event *models.Event, spoolerError error) error {
logging.LogWithFields("SPOOL-ls9ew", "id", event.AggregateID).WithError(spoolerError).Warn("something went wrong in project app handler")
return spooler.HandleError(event, spoolerError, p.view.GetLatestApplicationFailedEvent, p.view.ProcessedApplicationFailedEvent, p.view.ProcessedApplicationSequence, p.errorCountUntilSkip)
}

View File

@ -1,9 +1,15 @@
package handler package handler
import ( import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/eventstore"
iam_events "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
org_events "github.com/caos/zitadel/internal/org/repository/eventsourcing"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
"time" "time"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/eventstore/spooler" "github.com/caos/zitadel/internal/eventstore/spooler"
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing" usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
) )
@ -11,7 +17,7 @@ import (
type Configs map[string]*Config type Configs map[string]*Config
type Config struct { type Config struct {
MinimumCycleDurationMillisecond int MinimumCycleDuration types.Duration
} }
type handler struct { type handler struct {
@ -22,14 +28,28 @@ type handler struct {
} }
type EventstoreRepos struct { type EventstoreRepos struct {
UserEvents *usr_event.UserEventstore UserEvents *usr_event.UserEventstore
ProjectEvents *proj_event.ProjectEventstore
OrgEvents *org_events.OrgEventstore
IamEvents *iam_events.IamEventstore
} }
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, repos EventstoreRepos) []spooler.Handler { func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []spooler.Handler {
return []spooler.Handler{ return []spooler.Handler{
&User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}}, &User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
&UserSession{handler: handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount}, userEvents: repos.UserEvents}, &UserSession{handler: handler{view, bulkLimit, configs.cycleDuration("UserSession"), errorCount}, userEvents: repos.UserEvents},
&Token{handler: handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount}}, &Token{handler: handler{view, bulkLimit, configs.cycleDuration("Token"), errorCount}},
&Key{handler: handler{view, bulkLimit, configs.cycleDuration("Key"), errorCount}},
&Application{handler: handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount}},
&Org{handler: handler{view, bulkLimit, configs.cycleDuration("Org"), errorCount}},
&UserGrant{
handler: handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount},
eventstore: eventstore,
userEvents: repos.UserEvents,
orgEvents: repos.OrgEvents,
projectEvents: repos.ProjectEvents,
iamEvents: repos.IamEvents,
iamID: systemDefaults.IamID},
} }
} }
@ -38,5 +58,5 @@ func (configs Configs) cycleDuration(viewModel string) time.Duration {
if !ok { if !ok {
return 1 * time.Second return 1 * time.Second
} }
return time.Duration(c.MinimumCycleDurationMillisecond) * time.Millisecond return c.MinimumCycleDuration.Duration
} }

View File

@ -0,0 +1,55 @@
package handler
import (
"time"
es_model "github.com/caos/zitadel/internal/key/repository/eventsourcing/model"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/key/repository/eventsourcing"
view_model "github.com/caos/zitadel/internal/key/repository/view/model"
)
type Key struct {
handler
}
const (
keyTable = "auth.keys"
)
func (k *Key) MinimumCycleDuration() time.Duration { return k.cycleDuration }
func (k *Key) ViewModel() string {
return keyTable
}
func (k *Key) EventQuery() (*models.SearchQuery, error) {
sequence, err := k.view.GetLatestKeySequence()
if err != nil {
return nil, err
}
return eventsourcing.KeyPairQuery(sequence), nil
}
func (k *Key) Process(event *models.Event) error {
switch event.Type {
case es_model.KeyPairAdded:
privateKey, publicKey, err := view_model.KeysFromPairEvent(event)
if err != nil {
return err
}
return k.view.PutKeys(privateKey, publicKey, event.Sequence)
default:
return k.view.ProcessedKeySequence(event.Sequence)
}
return nil
}
func (k *Key) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-GHa3a", "id", event.AggregateID).WithError(err).Warn("something went wrong in key handler")
return spooler.HandleError(event, err, k.view.GetLatestKeyFailedEvent, k.view.ProcessedKeyFailedEvent, k.view.ProcessedKeySequence, k.errorCountUntilSkip)
}

View File

@ -0,0 +1,64 @@
package handler
import (
"github.com/caos/logging"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/org/repository/eventsourcing"
"github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
"github.com/caos/zitadel/internal/org/repository/view"
"time"
)
type Org struct {
handler
}
const (
orgTable = "auth.orgs"
)
func (o *Org) MinimumCycleDuration() time.Duration { return o.cycleDuration }
func (o *Org) ViewModel() string {
return orgTable
}
func (o *Org) EventQuery() (*es_models.SearchQuery, error) {
sequence, err := o.view.GetLatestOrgSequence()
if err != nil {
return nil, err
}
return eventsourcing.OrgQuery(sequence), nil
}
func (o *Org) Process(event *es_models.Event) error {
org := new(view.OrgView)
switch event.Type {
case model.OrgAdded:
org.AppendEvent(event)
case model.OrgChanged:
err := org.SetData(event)
if err != nil {
return err
}
org, err = o.view.OrgByID(org.ID)
if err != nil {
return err
}
err = org.AppendEvent(event)
if err != nil {
return err
}
default:
return o.view.ProcessedOrgSequence(event.Sequence)
}
return o.view.PutOrg(org)
}
func (o *Org) OnError(event *es_models.Event, spoolerErr error) error {
logging.LogWithFields("SPOOL-8siWS", "id", event.AggregateID).WithError(spoolerErr).Warn("something went wrong in org handler")
return spooler.HandleError(event, spoolerErr, o.view.GetLatestOrgFailedEvent, o.view.ProcessedOrgFailedEvent, o.view.ProcessedOrgSequence, o.errorCountUntilSkip)
}

View File

@ -54,7 +54,9 @@ func (p *User) Process(event *models.Event) (err error) {
es_model.UserUnlocked, es_model.UserUnlocked,
es_model.MfaOtpAdded, es_model.MfaOtpAdded,
es_model.MfaOtpVerified, es_model.MfaOtpVerified,
es_model.MfaOtpRemoved: es_model.MfaOtpRemoved,
es_model.MfaInitSkipped,
es_model.UserPasswordChanged:
user, err = p.view.UserByID(event.AggregateID) user, err = p.view.UserByID(event.AggregateID)
if err != nil { if err != nil {
return err return err

View File

@ -0,0 +1,344 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_events "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
org_model "github.com/caos/zitadel/internal/org/model"
org_events "github.com/caos/zitadel/internal/org/repository/eventsourcing"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
proj_model "github.com/caos/zitadel/internal/project/model"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
usr_model "github.com/caos/zitadel/internal/user/model"
usr_events "github.com/caos/zitadel/internal/user/repository/eventsourcing"
usr_es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
grant_es_model "github.com/caos/zitadel/internal/usergrant/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
"strings"
"time"
)
type UserGrant struct {
handler
eventstore eventstore.Eventstore
projectEvents *proj_event.ProjectEventstore
userEvents *usr_events.UserEventstore
orgEvents *org_events.OrgEventstore
iamEvents *iam_events.IamEventstore
iamID string
iamProjectID string
}
const (
userGrantTable = "auth.user_grants"
)
func (u *UserGrant) MinimumCycleDuration() time.Duration { return u.cycleDuration }
func (u *UserGrant) ViewModel() string {
return userGrantTable
}
func (u *UserGrant) EventQuery() (*models.SearchQuery, error) {
if u.iamProjectID == "" {
err := u.setIamProjectID()
if err != nil {
return nil, err
}
}
sequence, err := u.view.GetLatestUserGrantSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(grant_es_model.UserGrantAggregate, iam_es_model.IamAggregate, org_es_model.OrgAggregate, usr_es_model.UserAggregate, proj_es_model.ProjectAggregate).
LatestSequenceFilter(sequence), nil
}
func (u *UserGrant) Process(event *models.Event) (err error) {
switch event.AggregateType {
case grant_es_model.UserGrantAggregate:
err = u.processUserGrant(event)
case usr_es_model.UserAggregate:
err = u.processUser(event)
case proj_es_model.ProjectAggregate:
err = u.processProject(event)
case iam_es_model.IamAggregate:
err = u.processIamMember(event, "IAM", false)
case org_es_model.OrgAggregate:
return u.processOrg(event)
}
return err
}
func (u *UserGrant) processUserGrant(event *models.Event) (err error) {
grant := new(view_model.UserGrantView)
switch event.Type {
case grant_es_model.UserGrantAdded:
err = grant.AppendEvent(event)
if err != nil {
return err
}
err = u.fillData(grant, event.ResourceOwner)
case grant_es_model.UserGrantChanged,
grant_es_model.UserGrantDeactivated,
grant_es_model.UserGrantReactivated:
grant, err = u.view.UserGrantByID(event.AggregateID)
if err != nil {
return err
}
err = grant.AppendEvent(event)
case grant_es_model.UserGrantRemoved:
err = u.view.DeleteUserGrant(event.AggregateID, event.Sequence)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
if err != nil {
return err
}
return u.view.PutUserGrant(grant, grant.Sequence)
}
func (u *UserGrant) processUser(event *models.Event) (err error) {
switch event.Type {
case usr_es_model.UserProfileChanged,
usr_es_model.UserEmailChanged:
grants, err := u.view.UserGrantsByUserID(event.AggregateID)
if err != nil {
return err
}
user, err := u.userEvents.UserByID(context.Background(), event.AggregateID)
if err != nil {
return err
}
for _, grant := range grants {
u.fillUserData(grant, user)
err = u.view.PutUserGrant(grant, event.Sequence)
if err != nil {
return err
}
}
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
return nil
}
func (u *UserGrant) processProject(event *models.Event) (err error) {
switch event.Type {
case proj_es_model.ProjectChanged:
grants, err := u.view.UserGrantsByProjectID(event.AggregateID)
if err != nil {
return err
}
project, err := u.projectEvents.ProjectByID(context.Background(), event.AggregateID)
if err != nil {
return err
}
for _, grant := range grants {
u.fillProjectData(grant, project)
return u.view.PutUserGrant(grant, event.Sequence)
}
case proj_es_model.ProjectMemberAdded, proj_es_model.ProjectMemberChanged, proj_es_model.ProjectMemberRemoved:
member := new(proj_es_model.ProjectMember)
member.SetData(event)
return u.processMember(event, "PROJECT", true, member.UserID, member.Roles)
case proj_es_model.ProjectGrantMemberAdded, proj_es_model.ProjectGrantMemberChanged, proj_es_model.ProjectGrantMemberRemoved:
member := new(proj_es_model.ProjectGrantMember)
member.SetData(event)
return u.processMember(event, "PROJECT_GRANT", true, member.UserID, member.Roles)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
return nil
}
func (u *UserGrant) processOrg(event *models.Event) (err error) {
switch event.Type {
case org_es_model.OrgMemberAdded, org_es_model.OrgMemberChanged, org_es_model.OrgMemberRemoved:
member := new(org_es_model.OrgMember)
member.SetData(event)
return u.processMember(event, "ORG", false, member.UserID, member.Roles)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
return nil
}
func (u *UserGrant) processIamMember(event *models.Event, rolePrefix string, suffix bool) error {
member := new(iam_es_model.IamMember)
switch event.Type {
case iam_es_model.IamMemberAdded, iam_es_model.IamMemberChanged:
member.SetData(event)
grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if errors.IsNotFound(err) {
grant = &view_model.UserGrantView{
ID: u.iamProjectID + member.UserID,
ResourceOwner: u.iamID,
OrgName: u.iamID,
OrgDomain: u.iamID,
ProjectID: u.iamProjectID,
UserID: member.UserID,
RoleKeys: member.Roles,
CreationDate: event.CreationDate,
}
if suffix {
grant.RoleKeys = suffixRoles(event.AggregateID, grant.RoleKeys)
}
} else {
newRoles := member.Roles
if grant.RoleKeys != nil {
grant.RoleKeys = mergeExistingRoles(rolePrefix, grant.RoleKeys, newRoles)
} else {
grant.RoleKeys = newRoles
}
}
grant.Sequence = event.Sequence
grant.ChangeDate = event.CreationDate
return u.view.PutUserGrant(grant, grant.Sequence)
case iam_es_model.IamMemberRemoved:
member.SetData(event)
grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID)
if err != nil {
return err
}
return u.view.DeleteUserGrant(grant.ID, event.Sequence)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
}
func (u *UserGrant) processMember(event *models.Event, rolePrefix string, suffix bool, userID string, roleKeys []string) error {
switch event.Type {
case org_es_model.OrgMemberAdded, proj_es_model.ProjectMemberAdded, proj_es_model.ProjectGrantMemberAdded,
org_es_model.OrgMemberChanged, proj_es_model.ProjectMemberChanged, proj_es_model.ProjectGrantMemberChanged:
grant, err := u.view.UserGrantByIDs(event.ResourceOwner, u.iamProjectID, userID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if suffix {
roleKeys = suffixRoles(event.AggregateID, roleKeys)
}
if errors.IsNotFound(err) {
grant = &view_model.UserGrantView{
ID: u.iamProjectID + event.ResourceOwner + userID,
ResourceOwner: event.ResourceOwner,
ProjectID: u.iamProjectID,
UserID: userID,
RoleKeys: roleKeys,
CreationDate: event.CreationDate,
}
u.fillData(grant, event.ResourceOwner)
} else {
newRoles := roleKeys
if grant.RoleKeys != nil {
grant.RoleKeys = mergeExistingRoles(rolePrefix, grant.RoleKeys, newRoles)
} else {
grant.RoleKeys = newRoles
}
}
grant.Sequence = event.Sequence
grant.ChangeDate = event.CreationDate
return u.view.PutUserGrant(grant, event.Sequence)
case org_es_model.OrgMemberRemoved,
proj_es_model.ProjectMemberRemoved,
proj_es_model.ProjectGrantMemberRemoved:
grant, err := u.view.UserGrantByIDs(event.ResourceOwner, u.iamProjectID, userID)
if err != nil {
return err
}
return u.view.DeleteUserGrant(grant.ID, event.Sequence)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
}
func suffixRoles(suffix string, roles []string) []string {
suffixedRoles := make([]string, len(roles))
for i := 0; i < len(roles); i++ {
suffixedRoles[i] = roles[i] + ":" + suffix
}
return suffixedRoles
}
func mergeExistingRoles(rolePrefix string, existingRoles, newRoles []string) []string {
mergedRoles := make([]string, 0)
for _, existing := range existingRoles {
if !strings.HasPrefix(existing, rolePrefix) {
mergedRoles = append(mergedRoles, existing)
}
}
return append(mergedRoles, newRoles...)
}
func (u *UserGrant) setIamProjectID() error {
if u.iamProjectID != "" {
return nil
}
iam, err := u.iamEvents.IamByID(context.Background(), u.iamID)
if err != nil {
return err
}
if !iam.SetUpDone {
return caos_errs.ThrowPreconditionFailed(nil, "HANDL-s5DTs", "Setup not done")
}
u.iamProjectID = iam.IamProjectID
return nil
}
func (u *UserGrant) fillData(grant *view_model.UserGrantView, resourceOwner string) (err error) {
user, err := u.userEvents.UserByID(context.Background(), grant.UserID)
if err != nil {
return err
}
u.fillUserData(grant, user)
project, err := u.projectEvents.ProjectByID(context.Background(), grant.ProjectID)
if err != nil {
return err
}
u.fillProjectData(grant, project)
org, err := u.orgEvents.OrgByID(context.TODO(), org_model.NewOrg(resourceOwner))
if err != nil {
return err
}
u.fillOrgData(grant, org)
return nil
}
func (u *UserGrant) fillUserData(grant *view_model.UserGrantView, user *usr_model.User) {
grant.UserName = user.UserName
grant.FirstName = user.FirstName
grant.LastName = user.LastName
grant.Email = user.EmailAddress
}
func (u *UserGrant) fillProjectData(grant *view_model.UserGrantView, project *proj_model.Project) {
grant.ProjectName = project.Name
}
func (u *UserGrant) fillOrgData(grant *view_model.UserGrantView, org *org_model.Org) {
grant.OrgDomain = org.Domain
grant.OrgName = org.Name
}
func (u *UserGrant) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-8is4s", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler")
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
}

View File

@ -45,10 +45,8 @@ func (u *UserSession) Process(event *models.Event) (err error) {
switch event.Type { switch event.Type {
case es_model.UserPasswordCheckSucceeded, case es_model.UserPasswordCheckSucceeded,
es_model.UserPasswordCheckFailed, es_model.UserPasswordCheckFailed,
es_model.UserPasswordChanged,
es_model.MfaOtpCheckSucceeded, es_model.MfaOtpCheckSucceeded,
es_model.MfaOtpCheckFailed, es_model.MfaOtpCheckFailed:
es_model.MfaOtpRemoved:
eventData, err := view_model.UserSessionFromEvent(event) eventData, err := view_model.UserSessionFromEvent(event)
if err != nil { if err != nil {
return err return err
@ -66,14 +64,22 @@ func (u *UserSession) Process(event *models.Event) (err error) {
State: int32(req_model.UserSessionStateActive), State: int32(req_model.UserSessionStateActive),
} }
} }
session.AppendEvent(event) return u.updateSession(session, event)
case es_model.UserPasswordChanged,
es_model.MfaOtpRemoved:
sessions, err := u.view.UserSessionsByUserID(event.AggregateID)
if err != nil {
return err
}
for _, session := range sessions {
if err := u.updateSession(session, event); err != nil {
return err
}
}
return nil
default: default:
return u.view.ProcessedUserSessionSequence(event.Sequence) return u.view.ProcessedUserSessionSequence(event.Sequence)
} }
if err := u.FillUserInfo(session, event.AggregateID); err != nil {
return err
}
return u.view.PutUserSession(session)
} }
func (u *UserSession) OnError(event *models.Event, err error) error { func (u *UserSession) OnError(event *models.Event, err error) error {
@ -81,7 +87,18 @@ func (u *UserSession) OnError(event *models.Event, err error) error {
return spooler.HandleError(event, err, u.view.GetLatestUserSessionFailedEvent, u.view.ProcessedUserSessionFailedEvent, u.view.ProcessedUserSessionSequence, u.errorCountUntilSkip) return spooler.HandleError(event, err, u.view.GetLatestUserSessionFailedEvent, u.view.ProcessedUserSessionFailedEvent, u.view.ProcessedUserSessionSequence, u.errorCountUntilSkip)
} }
func (u *UserSession) FillUserInfo(session *view_model.UserSessionView, id string) error { func (u *UserSession) updateSession(session *view_model.UserSessionView, event *models.Event) error {
session.Sequence = event.Sequence
session.AppendEvent(event)
if session.UserName == "" {
if err := u.fillUserInfo(session, event.AggregateID); err != nil {
return err
}
}
return u.view.PutUserSession(session)
}
func (u *UserSession) fillUserInfo(session *view_model.UserSessionView, id string) error {
user, err := u.userEvents.UserByID(context.Background(), id) user, err := u.userEvents.UserByID(context.Background(), id)
if err != nil { if err != nil {
return err return err

View File

@ -2,6 +2,10 @@ package eventsourcing
import ( import (
"context" "context"
"github.com/caos/zitadel/internal/api/auth"
authz_repo "github.com/caos/zitadel/internal/authz/repository/eventsourcing"
es_iam "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
es_org "github.com/caos/zitadel/internal/org/repository/eventsourcing"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/eventstore" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/eventstore"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler"
@ -10,18 +14,23 @@ import (
"github.com/caos/zitadel/internal/auth_request/repository/cache" "github.com/caos/zitadel/internal/auth_request/repository/cache"
sd "github.com/caos/zitadel/internal/config/systemdefaults" sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types" "github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/crypto"
es_int "github.com/caos/zitadel/internal/eventstore" es_int "github.com/caos/zitadel/internal/eventstore"
es_spol "github.com/caos/zitadel/internal/eventstore/spooler" es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/id" "github.com/caos/zitadel/internal/id"
es_key "github.com/caos/zitadel/internal/key/repository/eventsourcing"
es_policy "github.com/caos/zitadel/internal/policy/repository/eventsourcing" es_policy "github.com/caos/zitadel/internal/policy/repository/eventsourcing"
es_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
es_user "github.com/caos/zitadel/internal/user/repository/eventsourcing" es_user "github.com/caos/zitadel/internal/user/repository/eventsourcing"
) )
type Config struct { type Config struct {
SearchLimit uint64
Eventstore es_int.Config Eventstore es_int.Config
AuthRequest cache.Config AuthRequest cache.Config
View types.SQL View types.SQL
Spooler spooler.SpoolerConfig Spooler spooler.SpoolerConfig
KeyConfig es_key.KeyConfig
} }
type EsRepository struct { type EsRepository struct {
@ -29,9 +38,15 @@ type EsRepository struct {
eventstore.UserRepo eventstore.UserRepo
eventstore.AuthRequestRepo eventstore.AuthRequestRepo
eventstore.TokenRepo eventstore.TokenRepo
eventstore.KeyRepository
eventstore.ApplicationRepo
eventstore.UserSessionRepo
eventstore.UserGrantRepo
eventstore.OrgRepository
eventstore.IamRepository
} }
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) { func Start(conf Config, authZ auth.Config, systemDefaults sd.SystemDefaults, authZRepo *authz_repo.EsRepository) (*EsRepository, error) {
es, err := es_int.Start(conf.Eventstore) es, err := es_int.Start(conf.Eventstore)
if err != nil { if err != nil {
return nil, err return nil, err
@ -41,7 +56,14 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
view, err := auth_view.StartView(sqlClient)
keyAlgorithm, err := crypto.NewAESCrypto(conf.KeyConfig.EncryptionConfig)
if err != nil {
return nil, err
}
idGenerator := id.SonyFlakeGenerator
view, err := auth_view.StartView(sqlClient, keyAlgorithm, idGenerator)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,8 +92,35 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
return nil, err return nil, err
} }
repos := handler.EventstoreRepos{UserEvents: user} key, err := es_key.StartKey(es, conf.KeyConfig, keyAlgorithm, idGenerator)
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos) if err != nil {
return nil, err
}
project, err := es_proj.StartProject(
es_proj.ProjectConfig{
Cache: conf.Eventstore.Cache,
Eventstore: es,
},
systemDefaults,
)
if err != nil {
return nil, err
}
iam, err := es_iam.StartIam(
es_iam.IamConfig{
Eventstore: es,
Cache: conf.Eventstore.Cache,
},
systemDefaults,
)
if err != nil {
return nil, err
}
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es})
repos := handler.EventstoreRepos{UserEvents: user, ProjectEvents: project, OrgEvents: org, IamEvents: iam}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos, systemDefaults)
return &EsRepository{ return &EsRepository{
spool, spool,
@ -86,13 +135,41 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
View: view, View: view,
UserSessionViewProvider: view, UserSessionViewProvider: view,
UserViewProvider: view, UserViewProvider: view,
IdGenerator: id.SonyFlakeGenerator, UserEventProvider: user,
IdGenerator: idGenerator,
PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration, PasswordCheckLifeTime: systemDefaults.VerificationLifetimes.PasswordCheck.Duration,
MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration, MfaInitSkippedLifeTime: systemDefaults.VerificationLifetimes.MfaInitSkip.Duration,
MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration, MfaSoftwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaSoftwareCheck.Duration,
MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration, MfaHardwareCheckLifeTime: systemDefaults.VerificationLifetimes.MfaHardwareCheck.Duration,
}, },
eventstore.TokenRepo{View: view}, eventstore.TokenRepo{View: view},
eventstore.KeyRepository{
KeyEvents: key,
View: view,
SigningKeyRotation: conf.KeyConfig.SigningKeyRotation.Duration,
},
eventstore.ApplicationRepo{
View: view,
ProjectEvents: project,
},
eventstore.UserSessionRepo{
View: view,
},
eventstore.UserGrantRepo{
SearchLimit: conf.SearchLimit,
View: view,
IamID: systemDefaults.IamID,
Auth: authZ,
AuthZRepo: authZRepo,
},
eventstore.OrgRepository{
SearchLimit: conf.SearchLimit,
View: view,
},
eventstore.IamRepository{
IamEvents: iam,
IamID: systemDefaults.IamID,
},
}, nil }, nil
} }

View File

@ -2,6 +2,7 @@ package spooler
import ( import (
"database/sql" "database/sql"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/handler"
"github.com/caos/zitadel/internal/auth/repository/eventsourcing/view" "github.com/caos/zitadel/internal/auth/repository/eventsourcing/view"
@ -17,12 +18,12 @@ type SpoolerConfig struct {
Handlers handler.Configs Handlers handler.Configs
} }
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, repos handler.EventstoreRepos) *spooler.Spooler { func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, repos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
spoolerConfig := spooler.Config{ spoolerConfig := spooler.Config{
Eventstore: es, Eventstore: es,
Locker: &locker{dbClient: sql}, Locker: &locker{dbClient: sql},
ConcurrentTasks: c.ConcurrentTasks, ConcurrentTasks: c.ConcurrentTasks,
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, repos), ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, repos, systemDefaults),
} }
spool := spoolerConfig.New() spool := spoolerConfig.New()
spool.Start() spool.Start()

View File

@ -0,0 +1,105 @@
package view
import (
"context"
"github.com/caos/zitadel/internal/errors"
global_model "github.com/caos/zitadel/internal/model"
proj_model "github.com/caos/zitadel/internal/project/model"
"github.com/caos/zitadel/internal/project/repository/view"
"github.com/caos/zitadel/internal/project/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
applicationTable = "auth.applications"
)
func (v *View) ApplicationByID(appID string) (*model.ApplicationView, error) {
return view.ApplicationByID(v.Db, applicationTable, appID)
}
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, int, error) {
return view.SearchApplications(v.Db, applicationTable, request)
}
func (v *View) PutApplication(project *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, project)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(project.Sequence)
}
func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
err := view.DeleteApplication(v.Db, applicationTable, appID)
if err != nil {
return nil
}
return v.ProcessedApplicationSequence(eventSequence)
}
func (v *View) GetLatestApplicationSequence() (uint64, error) {
return v.latestSequence(applicationTable)
}
func (v *View) ProcessedApplicationSequence(eventSequence uint64) error {
return v.saveCurrentSequence(applicationTable, eventSequence)
}
func (v *View) GetLatestApplicationFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(applicationTable, sequence)
}
func (v *View) ProcessedApplicationFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}
func (v *View) ApplicationByClientID(_ context.Context, clientID string) (*model.ApplicationView, error) {
req := &proj_model.ApplicationSearchRequest{
Limit: 1,
Queries: []*proj_model.ApplicationSearchQuery{
{
Key: proj_model.APPLICATIONSEARCHKEY_OIDC_CLIENT_ID,
Method: global_model.SEARCHMETHOD_EQUALS,
Value: clientID,
},
},
}
apps, count, err := view.SearchApplications(v.Db, applicationTable, req)
if err != nil {
return nil, errors.ThrowPreconditionFailed(err, "VIEW-sd6JQ", "cannot find client")
}
if count != 1 {
return nil, errors.ThrowPreconditionFailed(nil, "VIEW-dfw3as", "cannot find client")
}
return apps[0], nil
}
func (v *View) AppIDsFromProjectByClientID(ctx context.Context, clientID string) ([]string, error) {
app, err := v.ApplicationByClientID(ctx, clientID)
if err != nil {
return nil, err
}
req := &proj_model.ApplicationSearchRequest{
Queries: []*proj_model.ApplicationSearchQuery{
{
Key: proj_model.APPLICATIONSEARCHKEY_PROJECT_ID,
Method: global_model.SEARCHMETHOD_EQUALS,
Value: app.ProjectID,
},
},
}
apps, _, err := view.SearchApplications(v.Db, applicationTable, req)
if err != nil {
return nil, errors.ThrowPreconditionFailed(err, "VIEW-Gd24q", "cannot find applications")
}
ids := make([]string, 0, len(apps))
for _, app := range apps {
if !app.IsOIDC {
continue
}
ids = append(ids, app.OIDCClientID)
}
return ids, nil
}

View File

@ -0,0 +1,72 @@
package view
import (
key_model "github.com/caos/zitadel/internal/key/model"
"github.com/caos/zitadel/internal/key/repository/view"
"github.com/caos/zitadel/internal/key/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
keyTable = "auth.keys"
)
func (v *View) KeyByIDAndType(keyID string, private bool) (*model.KeyView, error) {
return view.KeyByIDAndType(v.Db, keyTable, keyID, private)
}
func (v *View) GetSigningKey() (*key_model.SigningKey, error) {
key, err := view.GetSigningKey(v.Db, keyTable)
if err != nil {
return nil, err
}
return key_model.SigningKeyFromKeyView(model.KeyViewToModel(key), v.keyAlgorithm)
}
func (v *View) GetActiveKeySet() ([]*key_model.PublicKey, error) {
keys, err := view.GetActivePublicKeys(v.Db, keyTable)
if err != nil {
return nil, err
}
return key_model.PublicKeysFromKeyView(model.KeyViewsToModel(keys), v.keyAlgorithm)
}
func (v *View) PutKeys(privateKey, publicKey *model.KeyView, eventSequence uint64) error {
err := view.PutKeys(v.Db, keyTable, privateKey, publicKey)
if err != nil {
return err
}
return v.ProcessedKeySequence(eventSequence)
}
func (v *View) DeleteKey(keyID string, private bool, eventSequence uint64) error {
err := view.DeleteKey(v.Db, keyTable, keyID, private)
if err != nil {
return nil
}
return v.ProcessedKeySequence(eventSequence)
}
func (v *View) DeleteKeyPair(keyID string, eventSequence uint64) error {
err := view.DeleteKeyPair(v.Db, keyTable, keyID)
if err != nil {
return nil
}
return v.ProcessedKeySequence(eventSequence)
}
func (v *View) GetLatestKeySequence() (uint64, error) {
return v.latestSequence(keyTable)
}
func (v *View) ProcessedKeySequence(eventSequence uint64) error {
return v.saveCurrentSequence(keyTable, eventSequence)
}
func (v *View) GetLatestKeyFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(keyTable, sequence)
}
func (v *View) ProcessedKeyFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,43 @@
package view
import (
"github.com/caos/zitadel/internal/org/model"
org_view "github.com/caos/zitadel/internal/org/repository/view"
"github.com/caos/zitadel/internal/view"
)
const (
orgTable = "auth.orgs"
)
func (v *View) OrgByID(orgID string) (*org_view.OrgView, error) {
return org_view.OrgByID(v.Db, orgTable, orgID)
}
func (v *View) SearchOrgs(req *model.OrgSearchRequest) ([]*org_view.OrgView, int, error) {
return org_view.SearchOrgs(v.Db, orgTable, req)
}
func (v *View) PutOrg(org *org_view.OrgView) error {
err := org_view.PutOrg(v.Db, orgTable, org)
if err != nil {
return err
}
return v.ProcessedOrgSequence(org.Sequence)
}
func (v *View) GetLatestOrgFailedEvent(sequence uint64) (*view.FailedEvent, error) {
return v.latestFailedEvent(orgTable, sequence)
}
func (v *View) ProcessedOrgFailedEvent(failedEvent *view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}
func (v *View) GetLatestOrgSequence() (uint64, error) {
return v.latestSequence(orgTable)
}
func (v *View) ProcessedOrgSequence(eventSequence uint64) error {
return v.saveCurrentSequence(orgTable, eventSequence)
}

View File

@ -20,17 +20,23 @@ func (v *View) IsTokenValid(tokenID string) (bool, error) {
return view.IsTokenValid(v.Db, tokenTable, tokenID) return view.IsTokenValid(v.Db, tokenTable, tokenID)
} }
func (v *View) CreateToken(agentID, applicationID, userID string, lifetime time.Duration) (*model.Token, error) { func (v *View) CreateToken(agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*model.Token, error) {
id, err := v.idGenerator.Next()
if err != nil {
return nil, err
}
now := time.Now().UTC() now := time.Now().UTC()
token := &model.Token{ token := &model.Token{
ID: id,
CreationDate: now, CreationDate: now,
UserID: userID, UserID: userID,
ApplicationID: applicationID, ApplicationID: applicationID,
UserAgentID: agentID, UserAgentID: agentID,
Scopes: scopes,
Audience: audience,
Expiration: now.Add(lifetime), Expiration: now.Add(lifetime),
} }
err := view.PutToken(v.Db, tokenTable, token) if err := view.PutToken(v.Db, tokenTable, token); err != nil {
if err != nil {
return nil, err return nil, err
} }
return token, nil return token, nil

View File

@ -0,0 +1,64 @@
package view
import (
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/caos/zitadel/internal/usergrant/repository/view"
"github.com/caos/zitadel/internal/usergrant/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
userGrantTable = "auth.user_grants"
)
func (v *View) UserGrantByID(grantID string) (*model.UserGrantView, error) {
return view.UserGrantByID(v.Db, userGrantTable, grantID)
}
func (v *View) UserGrantByIDs(resourceOwnerID, projectID, userID string) (*model.UserGrantView, error) {
return view.UserGrantByIDs(v.Db, userGrantTable, resourceOwnerID, projectID, userID)
}
func (v *View) UserGrantsByUserID(userID string) ([]*model.UserGrantView, error) {
return view.UserGrantsByUserID(v.Db, userGrantTable, userID)
}
func (v *View) UserGrantsByProjectID(projectID string) ([]*model.UserGrantView, error) {
return view.UserGrantsByProjectID(v.Db, userGrantTable, projectID)
}
func (v *View) SearchUserGrants(request *grant_model.UserGrantSearchRequest) ([]*model.UserGrantView, int, error) {
return view.SearchUserGrants(v.Db, userGrantTable, request)
}
func (v *View) PutUserGrant(grant *model.UserGrantView, sequence uint64) error {
err := view.PutUserGrant(v.Db, userGrantTable, grant)
if err != nil {
return err
}
return v.ProcessedUserGrantSequence(sequence)
}
func (v *View) DeleteUserGrant(grantID string, eventSequence uint64) error {
err := view.DeleteUserGrant(v.Db, userGrantTable, grantID)
if err != nil {
return nil
}
return v.ProcessedUserGrantSequence(eventSequence)
}
func (v *View) GetLatestUserGrantSequence() (uint64, error) {
return v.latestSequence(userGrantTable)
}
func (v *View) ProcessedUserGrantSequence(eventSequence uint64) error {
return v.saveCurrentSequence(userGrantTable, eventSequence)
}
func (v *View) GetLatestUserGrantFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(userGrantTable, sequence)
}
func (v *View) ProcessedUserGrantFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -10,14 +10,14 @@ const (
userSessionTable = "auth.user_sessions" userSessionTable = "auth.user_sessions"
) )
func (v *View) UserSessionByID(sessionID string) (*model.UserSessionView, error) {
return view.UserSessionByID(v.Db, userSessionTable, sessionID)
}
func (v *View) UserSessionByIDs(agentID, userID string) (*model.UserSessionView, error) { func (v *View) UserSessionByIDs(agentID, userID string) (*model.UserSessionView, error) {
return view.UserSessionByIDs(v.Db, userSessionTable, agentID, userID) return view.UserSessionByIDs(v.Db, userSessionTable, agentID, userID)
} }
func (v *View) UserSessionsByUserID(userID string) ([]*model.UserSessionView, error) {
return view.UserSessionsByUserID(v.Db, userSessionTable, userID)
}
func (v *View) UserSessionsByAgentID(agentID string) ([]*model.UserSessionView, error) { func (v *View) UserSessionsByAgentID(agentID string) ([]*model.UserSessionView, error) {
return view.UserSessionsByAgentID(v.Db, userSessionTable, agentID) return view.UserSessionsByAgentID(v.Db, userSessionTable, agentID)
} }

View File

@ -4,19 +4,26 @@ import (
"database/sql" "database/sql"
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/id"
) )
type View struct { type View struct {
Db *gorm.DB Db *gorm.DB
keyAlgorithm crypto.EncryptionAlgorithm
idGenerator id.Generator
} }
func StartView(sqlClient *sql.DB) (*View, error) { func StartView(sqlClient *sql.DB, keyAlgorithm crypto.EncryptionAlgorithm, idGenerator id.Generator) (*View, error) {
gorm, err := gorm.Open("postgres", sqlClient) gorm, err := gorm.Open("postgres", sqlClient)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &View{ return &View{
Db: gorm, Db: gorm,
keyAlgorithm: keyAlgorithm,
idGenerator: idGenerator,
}, nil }, nil
} }

View File

@ -0,0 +1,11 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/iam/model"
)
type IamRepository interface {
GetIam(ctx context.Context) (*model.Iam, error)
}

View File

@ -0,0 +1,14 @@
package repository
import (
"context"
"time"
"gopkg.in/square/go-jose.v2"
)
type KeyRepository interface {
GenerateSigningKeyPair(ctx context.Context, algorithm string) error
GetSigningKey(ctx context.Context, keyCh chan<- jose.SigningKey, errCh chan<- error, timer <-chan time.Time)
GetKeySet(ctx context.Context) (*jose.JSONWebKeySet, error)
}

View File

@ -9,4 +9,8 @@ type Repository interface {
UserRepository UserRepository
AuthRequestRepository AuthRequestRepository
TokenRepository TokenRepository
ApplicationRepository
KeyRepository
UserSessionRepository
UserGrantRepository
} }

View File

@ -8,6 +8,7 @@ import (
) )
type TokenRepository interface { type TokenRepository interface {
CreateToken(ctx context.Context, agentID, applicationID, userID string, lifetime time.Duration) (*model.Token, error) CreateToken(ctx context.Context, agentID, applicationID, userID string, audience, scopes []string, lifetime time.Duration) (*model.Token, error)
IsTokenValid(ctx context.Context, tokenID string) (bool, error) IsTokenValid(ctx context.Context, tokenID string) (bool, error)
TokenByID(ctx context.Context, tokenID string) (*model.Token, error)
} }

View File

@ -11,10 +11,20 @@ type UserRepository interface {
myUserRepo myUserRepo
SkipMfaInit(ctx context.Context, userID string) error SkipMfaInit(ctx context.Context, userID string) error
RequestPasswordReset(ctx context.Context, username string) error RequestPasswordReset(ctx context.Context, username string) error
SetPassword(ctx context.Context, userID, code, password string) error SetPassword(ctx context.Context, userID, code, password string) error
ChangePassword(ctx context.Context, userID, old, new string) error
VerifyEmail(ctx context.Context, userID, code string) error
ResendEmailVerificationMail(ctx context.Context, userID string) error
AddMfaOTP(ctx context.Context, userID string) (*model.OTP, error)
VerifyMfaOTPSetup(ctx context.Context, userID, code string) error
SignOut(ctx context.Context, agentID, userID string) error SignOut(ctx context.Context, agentID, userID string) error
UserByID(ctx context.Context, userID string) (*model.User, error)
} }
type myUserRepo interface { type myUserRepo interface {
@ -37,6 +47,6 @@ type myUserRepo interface {
ChangeMyPassword(ctx context.Context, old, new string) error ChangeMyPassword(ctx context.Context, old, new string) error
AddMyMfaOTP(ctx context.Context) (*model.OTP, error) AddMyMfaOTP(ctx context.Context) (*model.OTP, error)
VerifyMyMfaOTP(ctx context.Context, code string) error VerifyMyMfaOTPSetup(ctx context.Context, code string) error
RemoveMyMfaOTP(ctx context.Context) error RemoveMyMfaOTP(ctx context.Context) error
} }

View File

@ -0,0 +1,12 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/usergrant/model"
)
type UserGrantRepository interface {
SearchMyUserGrants(ctx context.Context, request *model.UserGrantSearchRequest) (*model.UserGrantSearchResponse, error)
SearchMyProjectOrgs(ctx context.Context, request *model.UserGrantSearchRequest) (*model.ProjectOrgSearchResponse, error)
SearchMyZitadelPermissions(ctx context.Context) ([]string, error)
}

View File

@ -0,0 +1,10 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/user/model"
)
type UserSessionRepository interface {
GetMyUserSessions(ctx context.Context) ([]*model.UserSessionView, error)
}

View File

@ -2,29 +2,36 @@ package model
import ( import (
"time" "time"
"github.com/caos/zitadel/internal/errors"
) )
type AuthRequest struct { type AuthRequest struct {
ID string ID string
AgentID string AgentID string
CreationDate time.Time CreationDate time.Time
ChangeDate time.Time ChangeDate time.Time
BrowserInfo *BrowserInfo BrowserInfo *BrowserInfo
ApplicationID string ApplicationID string
CallbackURI string CallbackURI string
TransferState string TransferState string
Prompt Prompt Prompt Prompt
PossibleLOAs []LevelOfAssurance PossibleLOAs []LevelOfAssurance
UiLocales []string UiLocales []string
LoginHint string LoginHint string
PreselectedUserID string MaxAuthAge uint32
MaxAuthAge uint32 Request Request
Request Request
levelOfAssurance LevelOfAssurance levelOfAssurance LevelOfAssurance
projectApplicationIDs []string UserID string
UserID string UserName string
PossibleSteps []NextStep UserOrgID string
PossibleSteps []NextStep
PasswordVerified bool
MfasVerified []MfaType
Audience []string
AuthTime time.Time
Code string
} }
type Prompt int32 type Prompt int32
@ -46,22 +53,30 @@ const (
func NewAuthRequest(id, agentID string, info *BrowserInfo, applicationID, callbackURI, transferState string, func NewAuthRequest(id, agentID string, info *BrowserInfo, applicationID, callbackURI, transferState string,
prompt Prompt, possibleLOAs []LevelOfAssurance, uiLocales []string, loginHint, preselectedUserID string, maxAuthAge uint32, request Request) *AuthRequest { prompt Prompt, possibleLOAs []LevelOfAssurance, uiLocales []string, loginHint, preselectedUserID string, maxAuthAge uint32, request Request) *AuthRequest {
return &AuthRequest{ return &AuthRequest{
ID: id, ID: id,
AgentID: agentID, AgentID: agentID,
BrowserInfo: info, BrowserInfo: info,
ApplicationID: applicationID, ApplicationID: applicationID,
CallbackURI: callbackURI, CallbackURI: callbackURI,
TransferState: transferState, TransferState: transferState,
Prompt: prompt, Prompt: prompt,
PossibleLOAs: possibleLOAs, PossibleLOAs: possibleLOAs,
UiLocales: uiLocales, UiLocales: uiLocales,
LoginHint: loginHint, LoginHint: loginHint,
PreselectedUserID: preselectedUserID, UserID: preselectedUserID,
MaxAuthAge: maxAuthAge, MaxAuthAge: maxAuthAge,
Request: request, Request: request,
} }
} }
func NewAuthRequestFromType(requestType AuthRequestType) (*AuthRequest, error) {
request, ok := authRequestTypeMapping[requestType]
if !ok {
return nil, errors.ThrowInvalidArgument(nil, "MODEL-ds2kl", "invalid request type")
}
return &AuthRequest{Request: request}, nil
}
func (a *AuthRequest) IsValid() bool { func (a *AuthRequest) IsValid() bool {
return a.ID != "" && return a.ID != "" &&
a.AgentID != "" && a.AgentID != "" &&
@ -80,3 +95,9 @@ func (a *AuthRequest) WithCurrentInfo(info *BrowserInfo) *AuthRequest {
a.BrowserInfo = info a.BrowserInfo = info
return a return a
} }
func (a *AuthRequest) SetUserInfo(userID string, userName string, userOrgID string) {
a.UserID = userID
a.UserName = userName
a.UserOrgID = userOrgID
}

View File

@ -1,6 +1,12 @@
package model package model
import "net" import (
"net"
"net/http"
"github.com/caos/zitadel/internal/api"
http_util "github.com/caos/zitadel/internal/api/http"
)
type BrowserInfo struct { type BrowserInfo struct {
UserAgent string UserAgent string
@ -8,6 +14,14 @@ type BrowserInfo struct {
RemoteIP net.IP RemoteIP net.IP
} }
func BrowserInfoFromRequest(r *http.Request) *BrowserInfo {
return &BrowserInfo{
UserAgent: r.Header.Get(api.UserAgent),
AcceptLanguage: r.Header.Get(api.AcceptLanguage),
RemoteIP: http_util.RemoteIPFromRequest(r),
}
}
func (i *BrowserInfo) IsValid() bool { func (i *BrowserInfo) IsValid() bool {
return i.UserAgent != "" && return i.UserAgent != "" &&
i.AcceptLanguage != "" && i.AcceptLanguage != "" &&

View File

@ -10,6 +10,7 @@ const (
NextStepUnspecified NextStepType = iota NextStepUnspecified NextStepType = iota
NextStepLogin NextStepLogin
NextStepUserSelection NextStepUserSelection
NextStepInitUser
NextStepPassword NextStepPassword
NextStepChangePassword NextStepChangePassword
NextStepInitPassword NextStepInitPassword
@ -26,9 +27,7 @@ const (
UserSessionStateTerminated UserSessionStateTerminated
) )
type LoginStep struct { type LoginStep struct{}
NotFound bool
}
func (s *LoginStep) Type() NextStepType { func (s *LoginStep) Type() NextStepType {
return NextStepLogin return NextStepLogin
@ -48,30 +47,33 @@ type UserSelection struct {
UserSessionState UserSessionState UserSessionState UserSessionState
} }
type PasswordStep struct { type InitUserStep struct {
FailureCount uint16 PasswordSet bool
} }
func (s *InitUserStep) Type() NextStepType {
return NextStepInitUser
}
type PasswordStep struct{}
func (s *PasswordStep) Type() NextStepType { func (s *PasswordStep) Type() NextStepType {
return NextStepPassword return NextStepPassword
} }
type ChangePasswordStep struct { type ChangePasswordStep struct{}
}
func (s *ChangePasswordStep) Type() NextStepType { func (s *ChangePasswordStep) Type() NextStepType {
return NextStepChangePassword return NextStepChangePassword
} }
type InitPasswordStep struct { type InitPasswordStep struct{}
}
func (s *InitPasswordStep) Type() NextStepType { func (s *InitPasswordStep) Type() NextStepType {
return NextStepInitPassword return NextStepInitPassword
} }
type VerifyEMailStep struct { type VerifyEMailStep struct{}
}
func (s *VerifyEMailStep) Type() NextStepType { func (s *VerifyEMailStep) Type() NextStepType {
return NextStepVerifyEmail return NextStepVerifyEmail
@ -87,7 +89,6 @@ func (s *MfaPromptStep) Type() NextStepType {
} }
type MfaVerificationStep struct { type MfaVerificationStep struct {
FailureCount uint16
MfaProviders []MfaType MfaProviders []MfaType
} }
@ -95,8 +96,7 @@ func (s *MfaVerificationStep) Type() NextStepType {
return NextStepMfaVerify return NextStepMfaVerify
} }
type RedirectToCallbackStep struct { type RedirectToCallbackStep struct{}
}
func (s *RedirectToCallbackStep) Type() NextStepType { func (s *RedirectToCallbackStep) Type() NextStepType {
return NextStepRedirectToCallback return NextStepRedirectToCallback
@ -111,7 +111,8 @@ const (
type MfaLevel int type MfaLevel int
const ( const (
MfaLevelSoftware MfaLevel = iota MfaLevelNotSetUp MfaLevel = iota
MfaLevelSoftware
MfaLevelHardware MfaLevelHardware
MfaLevelHardwareCertified MfaLevelHardwareCertified
) )

View File

@ -7,6 +7,12 @@ type Request interface {
type AuthRequestType int32 type AuthRequestType int32
var (
authRequestTypeMapping = map[AuthRequestType]Request{
AuthRequestTypeOIDC: &AuthRequestOIDC{},
}
)
const ( const (
AuthRequestTypeOIDC AuthRequestType = iota AuthRequestTypeOIDC AuthRequestType = iota
AuthRequestTypeSAML AuthRequestTypeSAML

View File

@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/auth_request/model"
"github.com/caos/zitadel/internal/config/types" "github.com/caos/zitadel/internal/config/types"
@ -34,34 +35,62 @@ func (c *AuthRequestCache) Health(ctx context.Context) error {
} }
func (c *AuthRequestCache) GetAuthRequestByID(_ context.Context, id string) (*model.AuthRequest, error) { func (c *AuthRequestCache) GetAuthRequestByID(_ context.Context, id string) (*model.AuthRequest, error) {
return c.getAuthRequest("id", id)
}
func (c *AuthRequestCache) GetAuthRequestByCode(_ context.Context, code string) (*model.AuthRequest, error) {
return c.getAuthRequest("code", code)
}
func (c *AuthRequestCache) SaveAuthRequest(_ context.Context, request *model.AuthRequest) error {
return c.saveAuthRequest(request, "INSERT INTO auth.auth_requests (id, request, request_type) VALUES($1, $2, $3)", request.Request.Type())
}
func (c *AuthRequestCache) UpdateAuthRequest(_ context.Context, request *model.AuthRequest) error {
return c.saveAuthRequest(request, "UPDATE auth.auth_requests SET request = $2, code = $3 WHERE id = $1", request.Code)
}
func (c *AuthRequestCache) DeleteAuthRequest(_ context.Context, id string) error {
_, err := c.client.Exec("DELETE FROM auth.auth_requests WHERE id = $1", id)
if err != nil {
return caos_errs.ThrowInternal(err, "CACHE-dsHw3", "unable to delete auth request")
}
return nil
}
func (c *AuthRequestCache) getAuthRequest(key, value string) (*model.AuthRequest, error) {
var b []byte var b []byte
err := c.client.QueryRow("SELECT request FROM auth.authrequests WHERE id = ?", id).Scan(&b) var requestType model.AuthRequestType
query := fmt.Sprintf("SELECT request, request_type FROM auth.auth_requests WHERE %s = $1", key)
err := c.client.QueryRow(query, value).Scan(&b, &requestType)
if err != nil { if err != nil {
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
return nil, caos_errs.ThrowNotFound(err, "CACHE-d24aD", "auth request not found") return nil, caos_errs.ThrowNotFound(err, "CACHE-d24aD", "auth request not found")
} }
return nil, caos_errs.ThrowInternal(err, "CACHE-as3kj", "unable to get auth request from database") return nil, caos_errs.ThrowInternal(err, "CACHE-as3kj", "unable to get auth request from database")
} }
request := new(model.AuthRequest) request, err := model.NewAuthRequestFromType(requestType)
err = json.Unmarshal(b, &request) if err == nil {
err = json.Unmarshal(b, request)
}
if err != nil { if err != nil {
return nil, caos_errs.ThrowInternal(err, "CACHE-2wshg", "unable to unmarshal auth request") return nil, caos_errs.ThrowInternal(err, "CACHE-2wshg", "unable to unmarshal auth request")
} }
return request, nil return request, nil
} }
func (c *AuthRequestCache) SaveAuthRequest(_ context.Context, request *model.AuthRequest) error { func (c *AuthRequestCache) saveAuthRequest(request *model.AuthRequest, query string, param interface{}) error {
b, err := json.Marshal(request) b, err := json.Marshal(request)
if err != nil { if err != nil {
return caos_errs.ThrowInternal(err, "CACHE-32FH9", "unable to marshal auth request") return caos_errs.ThrowInternal(err, "CACHE-os0GH", "unable to marshal auth request")
} }
stmt, err := c.client.Prepare("INSERT INTO auth.authrequests (id, request) VALUES($1, $2)") stmt, err := c.client.Prepare(query)
if err != nil { if err != nil {
return caos_errs.ThrowInternal(err, "CACHE-dswfF", "sql prepare failed") return caos_errs.ThrowInternal(err, "CACHE-su3GK", "sql prepare failed")
} }
_, err = stmt.Exec(request.ID, b) _, err = stmt.Exec(request.ID, b, param)
if err != nil { if err != nil {
return caos_errs.ThrowInternal(err, "CACHE-sw4af", "unable to save auth request") return caos_errs.ThrowInternal(err, "CACHE-sj8iS", "unable to save auth request")
} }
return nil return nil
} }

View File

@ -1,3 +1,3 @@
package repository package repository
//go:generate mockgen -package mock -destination ./mock/repository.mock.go github.com/caos/zitadel/internal/auth_request/repository Repository //go:generate mockgen -package mock -destination ./mock/repository.mock.go github.com/caos/zitadel/internal/auth_request/repository AuthRequestCache

View File

@ -1,12 +0,0 @@
package mock
import (
"github.com/golang/mock/gomock"
"github.com/caos/zitadel/internal/auth_request/repository"
)
func NewMockAuthRequestRepository(ctrl *gomock.Controller) repository.Repository {
repo := NewMockRepository(ctrl)
return repo
}

View File

@ -1,5 +1,5 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/caos/zitadel/internal/auth_request/repository (interfaces: Repository) // Source: github.com/caos/zitadel/internal/auth_request/repository (interfaces: AuthRequestCache)
// Package mock is a generated GoMock package. // Package mock is a generated GoMock package.
package mock package mock
@ -11,31 +11,60 @@ import (
reflect "reflect" reflect "reflect"
) )
// MockRepository is a mock of Repository interface // MockAuthRequestCache is a mock of AuthRequestCache interface
type MockRepository struct { type MockAuthRequestCache struct {
ctrl *gomock.Controller ctrl *gomock.Controller
recorder *MockRepositoryMockRecorder recorder *MockAuthRequestCacheMockRecorder
} }
// MockRepositoryMockRecorder is the mock recorder for MockRepository // MockAuthRequestCacheMockRecorder is the mock recorder for MockAuthRequestCache
type MockRepositoryMockRecorder struct { type MockAuthRequestCacheMockRecorder struct {
mock *MockRepository mock *MockAuthRequestCache
} }
// NewMockRepository creates a new mock instance // NewMockAuthRequestCache creates a new mock instance
func NewMockRepository(ctrl *gomock.Controller) *MockRepository { func NewMockAuthRequestCache(ctrl *gomock.Controller) *MockAuthRequestCache {
mock := &MockRepository{ctrl: ctrl} mock := &MockAuthRequestCache{ctrl: ctrl}
mock.recorder = &MockRepositoryMockRecorder{mock} mock.recorder = &MockAuthRequestCacheMockRecorder{mock}
return mock return mock
} }
// EXPECT returns an object that allows the caller to indicate expected use // EXPECT returns an object that allows the caller to indicate expected use
func (m *MockRepository) EXPECT() *MockRepositoryMockRecorder { func (m *MockAuthRequestCache) EXPECT() *MockAuthRequestCacheMockRecorder {
return m.recorder return m.recorder
} }
// DeleteAuthRequest mocks base method
func (m *MockAuthRequestCache) DeleteAuthRequest(arg0 context.Context, arg1 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DeleteAuthRequest", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// DeleteAuthRequest indicates an expected call of DeleteAuthRequest
func (mr *MockAuthRequestCacheMockRecorder) DeleteAuthRequest(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAuthRequest", reflect.TypeOf((*MockAuthRequestCache)(nil).DeleteAuthRequest), arg0, arg1)
}
// GetAuthRequestByCode mocks base method
func (m *MockAuthRequestCache) GetAuthRequestByCode(arg0 context.Context, arg1 string) (*model.AuthRequest, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthRequestByCode", arg0, arg1)
ret0, _ := ret[0].(*model.AuthRequest)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAuthRequestByCode indicates an expected call of GetAuthRequestByCode
func (mr *MockAuthRequestCacheMockRecorder) GetAuthRequestByCode(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthRequestByCode", reflect.TypeOf((*MockAuthRequestCache)(nil).GetAuthRequestByCode), arg0, arg1)
}
// GetAuthRequestByID mocks base method // GetAuthRequestByID mocks base method
func (m *MockRepository) GetAuthRequestByID(arg0 context.Context, arg1 string) (*model.AuthRequest, error) { func (m *MockAuthRequestCache) GetAuthRequestByID(arg0 context.Context, arg1 string) (*model.AuthRequest, error) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAuthRequestByID", arg0, arg1) ret := m.ctrl.Call(m, "GetAuthRequestByID", arg0, arg1)
ret0, _ := ret[0].(*model.AuthRequest) ret0, _ := ret[0].(*model.AuthRequest)
@ -44,13 +73,13 @@ func (m *MockRepository) GetAuthRequestByID(arg0 context.Context, arg1 string) (
} }
// GetAuthRequestByID indicates an expected call of GetAuthRequestByID // GetAuthRequestByID indicates an expected call of GetAuthRequestByID
func (mr *MockRepositoryMockRecorder) GetAuthRequestByID(arg0, arg1 interface{}) *gomock.Call { func (mr *MockAuthRequestCacheMockRecorder) GetAuthRequestByID(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthRequestByID", reflect.TypeOf((*MockRepository)(nil).GetAuthRequestByID), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthRequestByID", reflect.TypeOf((*MockAuthRequestCache)(nil).GetAuthRequestByID), arg0, arg1)
} }
// Health mocks base method // Health mocks base method
func (m *MockRepository) Health(arg0 context.Context) error { func (m *MockAuthRequestCache) Health(arg0 context.Context) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Health", arg0) ret := m.ctrl.Call(m, "Health", arg0)
ret0, _ := ret[0].(error) ret0, _ := ret[0].(error)
@ -58,22 +87,35 @@ func (m *MockRepository) Health(arg0 context.Context) error {
} }
// Health indicates an expected call of Health // Health indicates an expected call of Health
func (mr *MockRepositoryMockRecorder) Health(arg0 interface{}) *gomock.Call { func (mr *MockAuthRequestCacheMockRecorder) Health(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockRepository)(nil).Health), arg0) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Health", reflect.TypeOf((*MockAuthRequestCache)(nil).Health), arg0)
} }
// SaveAuthRequest mocks base method // SaveAuthRequest mocks base method
func (m *MockRepository) SaveAuthRequest(arg0 context.Context, arg1 string) (*model.AuthRequest, error) { func (m *MockAuthRequestCache) SaveAuthRequest(arg0 context.Context, arg1 *model.AuthRequest) error {
m.ctrl.T.Helper() m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SaveAuthRequest", arg0, arg1) ret := m.ctrl.Call(m, "SaveAuthRequest", arg0, arg1)
ret0, _ := ret[0].(*model.AuthRequest) ret0, _ := ret[0].(error)
ret1, _ := ret[1].(error) return ret0
return ret0, ret1
} }
// SaveAuthRequest indicates an expected call of SaveAuthRequest // SaveAuthRequest indicates an expected call of SaveAuthRequest
func (mr *MockRepositoryMockRecorder) SaveAuthRequest(arg0, arg1 interface{}) *gomock.Call { func (mr *MockAuthRequestCacheMockRecorder) SaveAuthRequest(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthRequest", reflect.TypeOf((*MockRepository)(nil).SaveAuthRequest), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveAuthRequest", reflect.TypeOf((*MockAuthRequestCache)(nil).SaveAuthRequest), arg0, arg1)
}
// UpdateAuthRequest mocks base method
func (m *MockAuthRequestCache) UpdateAuthRequest(arg0 context.Context, arg1 *model.AuthRequest) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UpdateAuthRequest", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// UpdateAuthRequest indicates an expected call of UpdateAuthRequest
func (mr *MockAuthRequestCacheMockRecorder) UpdateAuthRequest(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAuthRequest", reflect.TypeOf((*MockAuthRequestCache)(nil).UpdateAuthRequest), arg0, arg1)
} }

View File

@ -6,9 +6,12 @@ import (
"github.com/caos/zitadel/internal/auth_request/model" "github.com/caos/zitadel/internal/auth_request/model"
) )
type Repository interface { type AuthRequestCache interface {
Health(ctx context.Context) error Health(ctx context.Context) error
GetAuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error) GetAuthRequestByID(ctx context.Context, id string) (*model.AuthRequest, error)
SaveAuthRequest(ctx context.Context, id string) (*model.AuthRequest, error) GetAuthRequestByCode(ctx context.Context, code string) (*model.AuthRequest, error)
SaveAuthRequest(ctx context.Context, request *model.AuthRequest) error
UpdateAuthRequest(ctx context.Context, request *model.AuthRequest) error
DeleteAuthRequest(ctx context.Context, id string) error
} }

16
internal/authz/authz.go Normal file
View File

@ -0,0 +1,16 @@
package authz
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
)
type Config struct {
Repository eventsourcing.Config
}
func Start(ctx context.Context, config Config, authZ auth.Config, systemDefaults sd.SystemDefaults) (*eventsourcing.EsRepository, error) {
return eventsourcing.Start(config.Repository, authZ, systemDefaults)
}

View File

@ -0,0 +1,20 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/iam/model"
iam_event "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
)
type IamRepo struct {
IamID string
IamEvents *iam_event.IamEventstore
}
func (repo *IamRepo) Health(ctx context.Context) error {
return repo.IamEvents.Health(ctx)
}
func (repo *IamRepo) IamByID(ctx context.Context) (*model.Iam, error) {
return repo.IamEvents.IamByID(ctx, repo.IamID)
}

View File

@ -0,0 +1,68 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
iam_event "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
proj_event "github.com/caos/zitadel/internal/project/repository/eventsourcing"
"time"
)
type TokenVerifierRepo struct {
TokenVerificationKey [32]byte
IamID string
IamEvents *iam_event.IamEventstore
ProjectEvents *proj_event.ProjectEventstore
View *view.View
}
func (repo *TokenVerifierRepo) VerifyAccessToken(ctx context.Context, tokenString, appName, appID string) (userID string, clientID string, agentID string, err error) {
clientID, err = repo.verifierClientID(ctx, appName, appID)
if err != nil {
return "", "", "", caos_errs.ThrowPermissionDenied(nil, "APP-ptTIF2", "invalid token")
}
//TODO: use real key
tokenID, err := crypto.DecryptAESString(tokenString, string(repo.TokenVerificationKey[:32]))
if err != nil {
return "", "", "", caos_errs.ThrowPermissionDenied(nil, "APP-8EF0zZ", "invalid token")
}
token, err := repo.View.TokenByID(tokenID)
if err != nil {
return "", "", "", caos_errs.ThrowPermissionDenied(err, "APP-BxUSiL", "invalid token")
}
if !token.Expiration.After(time.Now().UTC()) {
return "", "", "", caos_errs.ThrowPermissionDenied(err, "APP-k9KS0", "invalid token")
}
for _, aud := range token.Audience {
if clientID == aud {
return token.UserID, clientID, token.UserAgentID, nil
}
}
return "", "", "", caos_errs.ThrowPermissionDenied(nil, "APP-Zxfako", "invalid audience")
}
func (repo *TokenVerifierRepo) ProjectIDByClientID(ctx context.Context, clientID string) (projectID string, err error) {
app, err := repo.View.ApplicationByOIDCClientID(clientID)
if err != nil {
return "", err
}
return app.ID, nil
}
func (repo *TokenVerifierRepo) verifierClientID(ctx context.Context, appName, appClientID string) (string, error) {
if appClientID != "" {
return appClientID, nil
}
iam, err := repo.IamEvents.IamByID(ctx, repo.IamID)
if err != nil {
return "", err
}
app, err := repo.View.ApplicationByProjecIDAndAppName(iam.IamProjectID, appName)
if err != nil {
return "", err
}
return app.OIDCClientID, nil
}

View File

@ -0,0 +1,102 @@
package eventstore
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
caos_errs "github.com/caos/zitadel/internal/errors"
iam_event "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/caos/zitadel/internal/usergrant/repository/view/model"
)
type UserGrantRepo struct {
View *view.View
IamID string
IamProjectID string
Auth auth.Config
IamEvents *iam_event.IamEventstore
}
func (repo *UserGrantRepo) Health() error {
return repo.View.Health()
}
func (repo *UserGrantRepo) ResolveGrants(ctx context.Context) (*auth.Grant, error) {
err := repo.fillIamProjectID(ctx)
if err != nil {
return nil, err
}
ctxData := auth.GetCtxData(ctx)
orgGrant, err := repo.View.UserGrantByIDs(ctxData.OrgID, repo.IamProjectID, ctxData.UserID)
if err != nil && !caos_errs.IsNotFound(err) {
return nil, err
}
iamAdminGrant, err := repo.View.UserGrantByIDs(repo.IamID, repo.IamProjectID, ctxData.UserID)
if err != nil && !caos_errs.IsNotFound(err) {
return nil, err
}
return mergeOrgAndAdminGrant(ctxData, orgGrant, iamAdminGrant), nil
}
func (repo *UserGrantRepo) SearchMyZitadelPermissions(ctx context.Context) ([]string, error) {
grant, err := repo.ResolveGrants(ctx)
if err != nil {
return nil, err
}
permissions := &grant_model.Permissions{Permissions: []string{}}
for _, role := range grant.Roles {
roleName, ctxID := auth.SplitPermission(role)
for _, mapping := range repo.Auth.RolePermissionMappings {
if mapping.Role == roleName {
permissions.AppendPermissions(ctxID, mapping.Permissions...)
}
}
}
return permissions.Permissions, nil
}
func (repo *UserGrantRepo) fillIamProjectID(ctx context.Context) error {
if repo.IamProjectID != "" {
return nil
}
iam, err := repo.IamEvents.IamByID(ctx, repo.IamID)
if err != nil {
return err
}
if !iam.SetUpDone {
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-skiwS", "Setup not done")
}
repo.IamProjectID = iam.IamProjectID
return nil
}
func mergeOrgAndAdminGrant(ctxData auth.CtxData, orgGrant, iamAdminGrant *model.UserGrantView) (grant *auth.Grant) {
if orgGrant != nil {
roles := orgGrant.RoleKeys
if iamAdminGrant != nil {
roles = addIamAdminRoles(roles, iamAdminGrant.RoleKeys)
}
grant = &auth.Grant{OrgID: orgGrant.ResourceOwner, Roles: roles}
} else if iamAdminGrant != nil {
grant = &auth.Grant{
OrgID: ctxData.OrgID,
Roles: iamAdminGrant.RoleKeys,
}
}
return grant
}
func addIamAdminRoles(orgRoles, iamAdminRoles []string) []string {
result := make([]string, 0)
result = append(result, iamAdminRoles...)
for _, role := range orgRoles {
if !auth.ExistsPerm(result, role) {
result = append(result, role)
}
}
return result
}

View File

@ -0,0 +1,72 @@
package handler
import (
"github.com/caos/logging"
"github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
"github.com/caos/zitadel/internal/project/repository/eventsourcing"
es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/project/repository/view/model"
"time"
)
type Application struct {
handler
}
const (
applicationTable = "authz.applications"
)
func (p *Application) MinimumCycleDuration() time.Duration { return p.cycleDuration }
func (p *Application) ViewModel() string {
return applicationTable
}
func (p *Application) EventQuery() (*models.SearchQuery, error) {
sequence, err := p.view.GetLatestApplicationSequence()
if err != nil {
return nil, err
}
return eventsourcing.ProjectQuery(sequence), nil
}
func (p *Application) Process(event *models.Event) (err error) {
app := new(view_model.ApplicationView)
switch event.Type {
case es_model.ApplicationAdded:
app.AppendEvent(event)
case es_model.ApplicationChanged,
es_model.OIDCConfigAdded,
es_model.OIDCConfigChanged,
es_model.ApplicationDeactivated,
es_model.ApplicationReactivated:
err := app.SetData(event)
if err != nil {
return err
}
app, err = p.view.ApplicationByID(app.ID)
if err != nil {
return err
}
app.AppendEvent(event)
case es_model.ApplicationRemoved:
err := app.SetData(event)
if err != nil {
return err
}
return p.view.DeleteApplication(app.ID, event.Sequence)
default:
return p.view.ProcessedApplicationSequence(event.Sequence)
}
if err != nil {
return err
}
return p.view.PutApplication(app)
}
func (p *Application) OnError(event *models.Event, spoolerError error) error {
logging.LogWithFields("SPOOL-sjZw", "id", event.AggregateID).WithError(spoolerError).Warn("something went wrong in project app handler")
return spooler.HandleError(event, spoolerError, p.view.GetLatestApplicationFailedEvent, p.view.ProcessedApplicationFailedEvent, p.view.ProcessedApplicationSequence, p.errorCountUntilSkip)
}

View File

@ -0,0 +1,49 @@
package handler
import (
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/eventstore"
iam_events "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
"time"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/eventstore/spooler"
)
type Configs map[string]*Config
type Config struct {
MinimumCycleDuration types.Duration
}
type handler struct {
view *view.View
bulkLimit uint64
cycleDuration time.Duration
errorCountUntilSkip uint64
}
type EventstoreRepos struct {
IamEvents *iam_events.IamEventstore
}
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []spooler.Handler {
return []spooler.Handler{
&UserGrant{
handler: handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount},
eventstore: eventstore,
iamID: systemDefaults.IamID,
iamEvents: repos.IamEvents,
},
&Application{handler: handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount}},
}
}
func (configs Configs) cycleDuration(viewModel string) time.Duration {
c, ok := configs[viewModel]
if !ok {
return 1 * time.Second
}
return c.MinimumCycleDuration.Duration
}

View File

@ -0,0 +1,226 @@
package handler
import (
"context"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/errors"
caos_errs "github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/models"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/eventstore/spooler"
iam_events "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
iam_es_model "github.com/caos/zitadel/internal/iam/repository/eventsourcing/model"
org_es_model "github.com/caos/zitadel/internal/org/repository/eventsourcing/model"
proj_es_model "github.com/caos/zitadel/internal/project/repository/eventsourcing/model"
view_model "github.com/caos/zitadel/internal/usergrant/repository/view/model"
"strings"
"time"
)
type UserGrant struct {
handler
eventstore eventstore.Eventstore
iamEvents *iam_events.IamEventstore
iamID string
iamProjectID string
}
const (
userGrantTable = "authz.user_grants"
)
func (u *UserGrant) MinimumCycleDuration() time.Duration { return u.cycleDuration }
func (u *UserGrant) ViewModel() string {
return userGrantTable
}
func (u *UserGrant) EventQuery() (*models.SearchQuery, error) {
if u.iamProjectID == "" {
err := u.setIamProjectID()
if err != nil {
return nil, err
}
}
sequence, err := u.view.GetLatestUserGrantSequence()
if err != nil {
return nil, err
}
return es_models.NewSearchQuery().
AggregateTypeFilter(iam_es_model.IamAggregate, org_es_model.OrgAggregate, proj_es_model.ProjectAggregate).
LatestSequenceFilter(sequence), nil
}
func (u *UserGrant) Process(event *models.Event) (err error) {
switch event.AggregateType {
case proj_es_model.ProjectAggregate:
err = u.processProject(event)
case iam_es_model.IamAggregate:
err = u.processIamMember(event, "IAM", false)
case org_es_model.OrgAggregate:
return u.processOrg(event)
}
return err
}
func (u *UserGrant) processProject(event *models.Event) (err error) {
switch event.Type {
case proj_es_model.ProjectMemberAdded, proj_es_model.ProjectMemberChanged, proj_es_model.ProjectMemberRemoved:
member := new(proj_es_model.ProjectMember)
member.SetData(event)
return u.processMember(event, "PROJECT", true, member.UserID, member.Roles)
case proj_es_model.ProjectGrantMemberAdded, proj_es_model.ProjectGrantMemberChanged, proj_es_model.ProjectGrantMemberRemoved:
member := new(proj_es_model.ProjectGrantMember)
member.SetData(event)
return u.processMember(event, "PROJECT_GRANT", true, member.UserID, member.Roles)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
return nil
}
func (u *UserGrant) processOrg(event *models.Event) (err error) {
switch event.Type {
case org_es_model.OrgMemberAdded, org_es_model.OrgMemberChanged, org_es_model.OrgMemberRemoved:
member := new(org_es_model.OrgMember)
member.SetData(event)
return u.processMember(event, "ORG", false, member.UserID, member.Roles)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
return nil
}
func (u *UserGrant) processIamMember(event *models.Event, rolePrefix string, suffix bool) error {
member := new(iam_es_model.IamMember)
switch event.Type {
case iam_es_model.IamMemberAdded, iam_es_model.IamMemberChanged:
member.SetData(event)
grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if errors.IsNotFound(err) {
grant = &view_model.UserGrantView{
ID: u.iamProjectID + member.UserID,
ResourceOwner: u.iamID,
OrgName: u.iamID,
OrgDomain: u.iamID,
ProjectID: u.iamProjectID,
UserID: member.UserID,
RoleKeys: member.Roles,
CreationDate: event.CreationDate,
}
if suffix {
grant.RoleKeys = suffixRoles(event.AggregateID, grant.RoleKeys)
}
} else {
newRoles := member.Roles
if grant.RoleKeys != nil {
grant.RoleKeys = mergeExistingRoles(rolePrefix, grant.RoleKeys, newRoles)
} else {
grant.RoleKeys = newRoles
}
}
grant.Sequence = event.Sequence
grant.ChangeDate = event.CreationDate
return u.view.PutUserGrant(grant, grant.Sequence)
case iam_es_model.IamMemberRemoved:
member.SetData(event)
grant, err := u.view.UserGrantByIDs(u.iamID, u.iamProjectID, member.UserID)
if err != nil {
return err
}
return u.view.DeleteUserGrant(grant.ID, event.Sequence)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
}
func (u *UserGrant) processMember(event *models.Event, rolePrefix string, suffix bool, userID string, roleKeys []string) error {
switch event.Type {
case org_es_model.OrgMemberAdded, proj_es_model.ProjectMemberAdded, proj_es_model.ProjectGrantMemberAdded,
org_es_model.OrgMemberChanged, proj_es_model.ProjectMemberChanged, proj_es_model.ProjectGrantMemberChanged:
grant, err := u.view.UserGrantByIDs(event.ResourceOwner, u.iamProjectID, userID)
if err != nil && !errors.IsNotFound(err) {
return err
}
if suffix {
roleKeys = suffixRoles(event.AggregateID, roleKeys)
}
if errors.IsNotFound(err) {
grant = &view_model.UserGrantView{
ID: u.iamProjectID + event.ResourceOwner + userID,
ResourceOwner: event.ResourceOwner,
ProjectID: u.iamProjectID,
UserID: userID,
RoleKeys: roleKeys,
CreationDate: event.CreationDate,
}
} else {
newRoles := roleKeys
if grant.RoleKeys != nil {
grant.RoleKeys = mergeExistingRoles(rolePrefix, grant.RoleKeys, newRoles)
} else {
grant.RoleKeys = newRoles
}
}
grant.Sequence = event.Sequence
grant.ChangeDate = event.CreationDate
return u.view.PutUserGrant(grant, event.Sequence)
case org_es_model.OrgMemberRemoved,
proj_es_model.ProjectMemberRemoved,
proj_es_model.ProjectGrantMemberRemoved:
grant, err := u.view.UserGrantByIDs(event.ResourceOwner, u.iamProjectID, userID)
if err != nil {
return err
}
return u.view.DeleteUserGrant(grant.ID, event.Sequence)
default:
return u.view.ProcessedUserGrantSequence(event.Sequence)
}
}
func suffixRoles(suffix string, roles []string) []string {
suffixedRoles := make([]string, len(roles))
for i := 0; i < len(roles); i++ {
suffixedRoles[i] = roles[i] + ":" + suffix
}
return suffixedRoles
}
func mergeExistingRoles(rolePrefix string, existingRoles, newRoles []string) []string {
mergedRoles := make([]string, 0)
for _, existing := range existingRoles {
if !strings.HasPrefix(existing, rolePrefix) {
mergedRoles = append(mergedRoles, existing)
}
}
return append(mergedRoles, newRoles...)
}
func (u *UserGrant) setIamProjectID() error {
if u.iamProjectID != "" {
return nil
}
iam, err := u.iamEvents.IamByID(context.Background(), u.iamID)
if err != nil {
return err
}
if !iam.SetUpDone {
return caos_errs.ThrowPreconditionFailed(nil, "HANDL-s5DTs", "Setup not done")
}
u.iamProjectID = iam.IamProjectID
return nil
}
func (u *UserGrant) OnError(event *models.Event, err error) error {
logging.LogWithFields("SPOOL-8is4s", "id", event.AggregateID).WithError(err).Warn("something went wrong in user handler")
return spooler.HandleError(event, err, u.view.GetLatestUserGrantFailedEvent, u.view.ProcessedUserGrantFailedEvent, u.view.ProcessedUserGrantSequence, u.errorCountUntilSkip)
}

View File

@ -0,0 +1,98 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/handler"
es_iam "github.com/caos/zitadel/internal/iam/repository/eventsourcing"
"github.com/caos/zitadel/internal/id"
es_proj "github.com/caos/zitadel/internal/project/repository/eventsourcing"
"github.com/caos/zitadel/internal/auth_request/repository/cache"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/eventstore"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/spooler"
authz_view "github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/config/types"
es_int "github.com/caos/zitadel/internal/eventstore"
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
es_key "github.com/caos/zitadel/internal/key/repository/eventsourcing"
)
type Config struct {
Eventstore es_int.Config
AuthRequest cache.Config
View types.SQL
Spooler spooler.SpoolerConfig
KeyConfig es_key.KeyConfig
}
type EsRepository struct {
spooler *es_spol.Spooler
eventstore.UserGrantRepo
eventstore.IamRepo
eventstore.TokenVerifierRepo
}
func Start(conf Config, authZ auth.Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
es, err := es_int.Start(conf.Eventstore)
if err != nil {
return nil, err
}
sqlClient, err := conf.View.Start()
if err != nil {
return nil, err
}
idGenerator := id.SonyFlakeGenerator
view, err := authz_view.StartView(sqlClient, idGenerator)
if err != nil {
return nil, err
}
iam, err := es_iam.StartIam(es_iam.IamConfig{
Eventstore: es,
Cache: conf.Eventstore.Cache,
}, systemDefaults)
if err != nil {
return nil, err
}
project, err := es_proj.StartProject(es_proj.ProjectConfig{
Eventstore: es,
Cache: conf.Eventstore.Cache,
}, systemDefaults)
if err != nil {
return nil, err
}
repos := handler.EventstoreRepos{IamEvents: iam}
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, repos, systemDefaults)
return &EsRepository{
spool,
eventstore.UserGrantRepo{
View: view,
IamID: systemDefaults.IamID,
Auth: authZ,
IamEvents: iam,
},
eventstore.IamRepo{
IamID: systemDefaults.IamID,
IamEvents: iam,
},
eventstore.TokenVerifierRepo{
//TODO: Add Token Verification Key
IamID: systemDefaults.IamID,
IamEvents: iam,
ProjectEvents: project,
View: view,
},
}, nil
}
func (repo *EsRepository) Health(ctx context.Context) error {
if err := repo.UserGrantRepo.Health(); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,19 @@
package spooler
import (
"database/sql"
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
"time"
)
const (
lockTable = "authz.locks"
)
type locker struct {
dbClient *sql.DB
}
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
}

View File

@ -0,0 +1,127 @@
package spooler
import (
"database/sql"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
)
type dbMock struct {
db *sql.DB
mock sqlmock.Sqlmock
}
func mockDB(t *testing.T) *dbMock {
mockDB := dbMock{}
var err error
mockDB.db, mockDB.mock, err = sqlmock.New()
if err != nil {
t.Fatalf("error occured while creating stub db %v", err)
}
mockDB.mock.MatchExpectationsInOrder(true)
return &mockDB
}
func (db *dbMock) expectCommit() *dbMock {
db.mock.ExpectCommit()
return db
}
func (db *dbMock) expectRollback() *dbMock {
db.mock.ExpectRollback()
return db
}
func (db *dbMock) expectBegin() *dbMock {
db.mock.ExpectBegin()
return db
}
func (db *dbMock) expectSavepoint() *dbMock {
db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
return db
}
func (db *dbMock) expectReleaseSavepoint() *dbMock {
db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
return db
}
func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock {
query := db.mock.
ExpectExec(`INSERT INTO authz\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`).
WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view).
WillReturnResult(sqlmock.NewResult(1, 1))
if affectedRows == 0 {
query.WillReturnResult(sqlmock.NewResult(0, 0))
} else {
query.WillReturnResult(sqlmock.NewResult(1, affectedRows))
}
return db
}
func Test_locker_Renew(t *testing.T) {
type fields struct {
db *dbMock
}
type args struct {
lockerID string
viewModel string
waitTime time.Duration
}
tests := []struct {
name string
fields fields
args args
wantErr bool
}{
{
name: "renew succeeded",
fields: fields{
db: mockDB(t).
expectBegin().
expectSavepoint().
expectRenew("locker", "view", 1).
expectReleaseSavepoint().
expectCommit(),
},
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
wantErr: false,
},
{
name: "renew now rows updated",
fields: fields{
db: mockDB(t).
expectBegin().
expectSavepoint().
expectRenew("locker", "view", 0).
expectRollback(),
},
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
l := &locker{
dbClient: tt.fields.db.db,
}
if err := l.Renew(tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr {
t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr)
}
if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil {
t.Errorf("not all database expectations met: %v", err)
}
})
}
}

View File

@ -0,0 +1,31 @@
package spooler
import (
"database/sql"
sd "github.com/caos/zitadel/internal/config/systemdefaults"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/handler"
"github.com/caos/zitadel/internal/authz/repository/eventsourcing/view"
"github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/spooler"
)
type SpoolerConfig struct {
BulkLimit uint64
FailureCountUntilSkip uint64
ConcurrentTasks int
Handlers handler.Configs
}
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, repos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
spoolerConfig := spooler.Config{
Eventstore: es,
Locker: &locker{dbClient: sql},
ConcurrentTasks: c.ConcurrentTasks,
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, repos, systemDefaults),
}
spool := spoolerConfig.New()
spool.Start()
return spool
}

View File

@ -0,0 +1,60 @@
package view
import (
proj_model "github.com/caos/zitadel/internal/project/model"
"github.com/caos/zitadel/internal/project/repository/view"
"github.com/caos/zitadel/internal/project/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
applicationTable = "authz.applications"
)
func (v *View) ApplicationByID(appID string) (*model.ApplicationView, error) {
return view.ApplicationByID(v.Db, applicationTable, appID)
}
func (v *View) ApplicationByOIDCClientID(clientID string) (*model.ApplicationView, error) {
return view.ApplicationByOIDCClientID(v.Db, applicationTable, clientID)
}
func (v *View) ApplicationByProjecIDAndAppName(projectID, appName string) (*model.ApplicationView, error) {
return view.ApplicationByProjectIDAndAppName(v.Db, applicationTable, projectID, appName)
}
func (v *View) SearchApplications(request *proj_model.ApplicationSearchRequest) ([]*model.ApplicationView, int, error) {
return view.SearchApplications(v.Db, applicationTable, request)
}
func (v *View) PutApplication(project *model.ApplicationView) error {
err := view.PutApplication(v.Db, applicationTable, project)
if err != nil {
return err
}
return v.ProcessedApplicationSequence(project.Sequence)
}
func (v *View) DeleteApplication(appID string, eventSequence uint64) error {
err := view.DeleteApplication(v.Db, applicationTable, appID)
if err != nil {
return nil
}
return v.ProcessedApplicationSequence(eventSequence)
}
func (v *View) GetLatestApplicationSequence() (uint64, error) {
return v.latestSequence(applicationTable)
}
func (v *View) ProcessedApplicationSequence(eventSequence uint64) error {
return v.saveCurrentSequence(applicationTable, eventSequence)
}
func (v *View) GetLatestApplicationFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(applicationTable, sequence)
}
func (v *View) ProcessedApplicationFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,17 @@
package view
import (
"github.com/caos/zitadel/internal/view"
)
const (
errTable = "authz.failed_event"
)
func (v *View) saveFailedEvent(failedEvent *view.FailedEvent) error {
return view.SaveFailedEvent(v.Db, errTable, failedEvent)
}
func (v *View) latestFailedEvent(viewName string, sequence uint64) (*view.FailedEvent, error) {
return view.LatestFailedEvent(v.Db, errTable, viewName, sequence)
}

View File

@ -0,0 +1,17 @@
package view
import (
"github.com/caos/zitadel/internal/view"
)
const (
sequencesTable = "authz.current_sequences"
)
func (v *View) saveCurrentSequence(viewName string, sequence uint64) error {
return view.SaveCurrentSequence(v.Db, sequencesTable, viewName, sequence)
}
func (v *View) latestSequence(viewName string) (uint64, error) {
return view.LatestSequence(v.Db, sequencesTable, viewName)
}

View File

@ -0,0 +1,59 @@
package view
import (
"github.com/caos/zitadel/internal/token/repository/view"
"github.com/caos/zitadel/internal/token/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
tokenTable = "auth.tokens"
)
func (v *View) TokenByID(tokenID string) (*model.Token, error) {
return view.TokenByID(v.Db, tokenTable, tokenID)
}
func (v *View) IsTokenValid(tokenID string) (bool, error) {
return view.IsTokenValid(v.Db, tokenTable, tokenID)
}
func (v *View) PutToken(token *model.Token) error {
err := view.PutToken(v.Db, tokenTable, token)
if err != nil {
return err
}
return v.ProcessedTokenSequence(token.Sequence)
}
func (v *View) DeleteToken(tokenID string, eventSequence uint64) error {
err := view.DeleteToken(v.Db, tokenTable, tokenID)
if err != nil {
return nil
}
return v.ProcessedTokenSequence(eventSequence)
}
func (v *View) DeleteSessionTokens(agentID, userID string, eventSequence uint64) error {
err := view.DeleteTokens(v.Db, tokenTable, agentID, userID)
if err != nil {
return nil
}
return v.ProcessedTokenSequence(eventSequence)
}
func (v *View) GetLatestTokenSequence() (uint64, error) {
return v.latestSequence(tokenTable)
}
func (v *View) ProcessedTokenSequence(eventSequence uint64) error {
return v.saveCurrentSequence(tokenTable, eventSequence)
}
func (v *View) GetLatestTokenFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(tokenTable, sequence)
}
func (v *View) ProcessedTokenFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,64 @@
package view
import (
grant_model "github.com/caos/zitadel/internal/usergrant/model"
"github.com/caos/zitadel/internal/usergrant/repository/view"
"github.com/caos/zitadel/internal/usergrant/repository/view/model"
global_view "github.com/caos/zitadel/internal/view"
)
const (
userGrantTable = "authz.user_grants"
)
func (v *View) UserGrantByID(grantID string) (*model.UserGrantView, error) {
return view.UserGrantByID(v.Db, userGrantTable, grantID)
}
func (v *View) UserGrantByIDs(resourceOwnerID, projectID, userID string) (*model.UserGrantView, error) {
return view.UserGrantByIDs(v.Db, userGrantTable, resourceOwnerID, projectID, userID)
}
func (v *View) UserGrantsByUserID(userID string) ([]*model.UserGrantView, error) {
return view.UserGrantsByUserID(v.Db, userGrantTable, userID)
}
func (v *View) UserGrantsByProjectID(projectID string) ([]*model.UserGrantView, error) {
return view.UserGrantsByProjectID(v.Db, userGrantTable, projectID)
}
func (v *View) SearchUserGrants(request *grant_model.UserGrantSearchRequest) ([]*model.UserGrantView, int, error) {
return view.SearchUserGrants(v.Db, userGrantTable, request)
}
func (v *View) PutUserGrant(grant *model.UserGrantView, sequence uint64) error {
err := view.PutUserGrant(v.Db, userGrantTable, grant)
if err != nil {
return err
}
return v.ProcessedUserGrantSequence(sequence)
}
func (v *View) DeleteUserGrant(grantID string, eventSequence uint64) error {
err := view.DeleteUserGrant(v.Db, userGrantTable, grantID)
if err != nil {
return nil
}
return v.ProcessedUserGrantSequence(eventSequence)
}
func (v *View) GetLatestUserGrantSequence() (uint64, error) {
return v.latestSequence(userGrantTable)
}
func (v *View) ProcessedUserGrantSequence(eventSequence uint64) error {
return v.saveCurrentSequence(userGrantTable, eventSequence)
}
func (v *View) GetLatestUserGrantFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
return v.latestFailedEvent(userGrantTable, sequence)
}
func (v *View) ProcessedUserGrantFailedEvent(failedEvent *global_view.FailedEvent) error {
return v.saveFailedEvent(failedEvent)
}

View File

@ -0,0 +1,28 @@
package view
import (
"database/sql"
"github.com/caos/zitadel/internal/id"
"github.com/jinzhu/gorm"
)
type View struct {
Db *gorm.DB
idGenerator id.Generator
}
func StartView(sqlClient *sql.DB, idGenerator id.Generator) (*View, error) {
gorm, err := gorm.Open("postgres", sqlClient)
if err != nil {
return nil, err
}
return &View{
Db: gorm,
idGenerator: idGenerator,
}, nil
}
func (v *View) Health() (err error) {
return v.Db.DB().Ping()
}

View File

@ -0,0 +1,11 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/iam/model"
)
type IamRepository interface {
Health(ctx context.Context) error
IamByID(ctx context.Context, id string) (*model.Iam, error)
}

View File

@ -0,0 +1,11 @@
package repository
import (
"context"
)
type Repository interface {
Health(context.Context) error
UserGrantRepository
IamRepository
}

View File

@ -0,0 +1,10 @@
package repository
import (
"context"
)
type TokenVerifierRepository interface {
VerifyAccessToken(ctx context.Context, appName string) (string, string, string, error)
ProjectIDByClientID(ctx context.Context, clientID string) (string, error)
}

View File

@ -0,0 +1,11 @@
package repository
import (
"context"
"github.com/caos/zitadel/internal/api/auth"
)
type UserGrantRepository interface {
ResolveGrants(ctx context.Context) (*auth.Grant, error)
SearchMyZitadelPermissions(ctx context.Context) ([]string, error)
}

View File

@ -1,6 +1,9 @@
package crypto package crypto
import ( import (
"database/sql/driver"
"encoding/json"
"github.com/caos/zitadel/internal/errors" "github.com/caos/zitadel/internal/errors"
) )
@ -35,6 +38,23 @@ type CryptoValue struct {
Crypted []byte Crypted []byte
} }
func (c *CryptoValue) Value() (driver.Value, error) {
if c == nil {
return nil, nil
}
return json.Marshal(c)
}
func (c *CryptoValue) Scan(src interface{}) error {
if b, ok := src.([]byte); ok {
return json.Unmarshal(b, c)
}
if s, ok := src.(string); ok {
return json.Unmarshal([]byte(s), c)
}
return nil
}
type CryptoType int type CryptoType int
func Crypt(value []byte, c Crypto) (*CryptoValue, error) { func Crypt(value []byte, c Crypto) (*CryptoValue, error) {

103
internal/crypto/rsa.go Normal file
View File

@ -0,0 +1,103 @@
package crypto
import (
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
)
func GenerateKeyPair(bits int) (*rsa.PrivateKey, *rsa.PublicKey, error) {
privkey, err := rsa.GenerateKey(rand.Reader, bits)
if err != nil {
return nil, nil, err
}
return privkey, &privkey.PublicKey, nil
}
func GenerateEncryptedKeyPair(bits int, alg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, error) {
privateKey, publicKey, err := GenerateKeyPair(bits)
if err != nil {
return nil, nil, err
}
return EncryptKeys(privateKey, publicKey, alg)
}
func PrivateKeyToBytes(priv *rsa.PrivateKey) []byte {
return pem.EncodeToMemory(
&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(priv),
},
)
}
func PublicKeyToBytes(pub *rsa.PublicKey) ([]byte, error) {
pubASN1, err := x509.MarshalPKIXPublicKey(pub)
if err != nil {
return nil, err
}
pubBytes := pem.EncodeToMemory(&pem.Block{
Type: "RSA PUBLIC KEY",
Bytes: pubASN1,
})
return pubBytes, nil
}
func BytesToPrivateKey(priv []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(priv)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
key, err := x509.ParsePKCS1PrivateKey(b)
if err != nil {
return nil, err
}
return key, nil
}
func BytesToPublicKey(pub []byte) (*rsa.PublicKey, error) {
block, _ := pem.Decode(pub)
enc := x509.IsEncryptedPEMBlock(block)
b := block.Bytes
var err error
if enc {
b, err = x509.DecryptPEMBlock(block, nil)
if err != nil {
return nil, err
}
}
ifc, err := x509.ParsePKIXPublicKey(b)
if err != nil {
return nil, err
}
key, ok := ifc.(*rsa.PublicKey)
if !ok {
return nil, err
}
return key, nil
}
func EncryptKeys(privateKey *rsa.PrivateKey, publicKey *rsa.PublicKey, alg EncryptionAlgorithm) (*CryptoValue, *CryptoValue, error) {
encryptedPrivateKey, err := Encrypt(PrivateKeyToBytes(privateKey), alg)
if err != nil {
return nil, nil, err
}
pubKey, err := PublicKeyToBytes(publicKey)
if err != nil {
return nil, nil, err
}
encryptedPublicKey, err := Encrypt(pubKey, alg)
if err != nil {
return nil, nil, err
}
return encryptedPrivateKey, encryptedPublicKey, nil
}

27
internal/form/parser.go Normal file
View File

@ -0,0 +1,27 @@
package form
import (
"github.com/caos/zitadel/internal/errors"
"net/http"
"github.com/gorilla/schema"
)
type Parser struct {
decoder *schema.Decoder
}
func NewParser() *Parser {
d := schema.NewDecoder()
d.IgnoreUnknownKeys(true)
return &Parser{d}
}
func (p *Parser) Parse(r *http.Request, data interface{}) error {
err := r.ParseForm()
if err != nil {
return errors.ThrowInternal(err, "FORM-lCC9zI", "error parsing http form")
}
return p.decoder.Decode(data, r.Form)
}

128
internal/i18n/i18n.go Normal file
View File

@ -0,0 +1,128 @@
package i18n
import (
"encoding/json"
"io/ioutil"
"net/http"
"os"
http_util "github.com/caos/zitadel/internal/api/http"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/logging"
"github.com/ghodss/yaml"
"github.com/nicksnyder/go-i18n/v2/i18n"
"golang.org/x/text/language"
)
const (
i18nPath = "/i18n"
)
type Translator struct {
bundle *i18n.Bundle
cookieName string
cookieHandler *http_util.CookieHandler
}
type TranslatorConfig struct {
DefaultLanguage language.Tag
CookieName string
}
func NewTranslator(dir http.FileSystem, config TranslatorConfig) (*Translator, error) {
t := new(Translator)
var err error
t.bundle, err = newBundle(dir, config.DefaultLanguage)
if err != nil {
return nil, err
}
t.cookieHandler = http_util.NewCookieHandler()
t.cookieName = config.CookieName
return t, nil
}
func newBundle(dir http.FileSystem, defaultLanguage language.Tag) (*i18n.Bundle, error) {
bundle := i18n.NewBundle(defaultLanguage)
bundle.RegisterUnmarshalFunc("yaml", yaml.Unmarshal)
bundle.RegisterUnmarshalFunc("json", json.Unmarshal)
i18nDir, err := dir.Open(i18nPath)
if err != nil {
return nil, errors.ThrowNotFound(err, "I18N-MnXRie", "path not found")
}
defer i18nDir.Close()
files, err := i18nDir.Readdir(0)
if err != nil {
return nil, errors.ThrowNotFound(err, "I18N-Gew23", "cannot read dir")
}
for _, file := range files {
if err := addFileToBundle(dir, bundle, file); err != nil {
return nil, errors.ThrowNotFound(err, "I18N-ZS2AW", "cannot append file to bundle")
}
}
return bundle, nil
}
func addFileToBundle(dir http.FileSystem, bundle *i18n.Bundle, file os.FileInfo) error {
f, err := dir.Open("/i18n/" + file.Name())
if err != nil {
return err
}
defer f.Close()
content, err := ioutil.ReadAll(f)
if err != nil {
return err
}
bundle.MustParseMessageFileBytes(content, file.Name())
return nil
}
func (t *Translator) LocalizeFromRequest(r *http.Request, id string, args map[string]interface{}) string {
s, err := t.localizerFromRequest(r).Localize(&i18n.LocalizeConfig{
MessageID: id,
TemplateData: args,
})
if err != nil {
logging.Log("I18N-MsF5sx").WithError(err).Warnf("missing translation")
return id
}
return s
}
func (t *Translator) Localize(id string, args map[string]interface{}) string {
s, _ := t.localizer().Localize(&i18n.LocalizeConfig{
MessageID: id,
TemplateData: args,
})
return s
}
func (t *Translator) Lang(r *http.Request) language.Tag {
matcher := language.NewMatcher(t.bundle.LanguageTags())
tag, _ := language.MatchStrings(matcher, t.langsFromRequest(r)...)
return tag
}
func (t *Translator) SetLangCookie(w http.ResponseWriter, lang language.Tag) {
t.cookieHandler.SetCookie(w, t.cookieName, lang.String())
}
func (t *Translator) localizerFromRequest(r *http.Request) *i18n.Localizer {
return t.localizer(t.langsFromRequest(r)...)
}
func (t *Translator) localizer(langs ...string) *i18n.Localizer {
return i18n.NewLocalizer(t.bundle, langs...)
}
func (t *Translator) langsFromRequest(r *http.Request) []string {
langs := make([]string, 0)
if r != nil {
lang, err := t.cookieHandler.GetCookieValue(r, t.cookieName)
if err == nil {
langs = append(langs, lang)
}
langs = append(langs, r.Header.Get("Accept-Language"))
}
return langs
}

View File

@ -65,7 +65,7 @@ func (i *Iam) AppendEvent(event *es_models.Event) (err error) {
i.SetUpDone = true i.SetUpDone = true
case IamProjectSet, case IamProjectSet,
GlobalOrgSet: GlobalOrgSet:
err = i.setData(event) err = i.SetData(event)
case IamMemberAdded: case IamMemberAdded:
err = i.appendAddMemberEvent(event) err = i.appendAddMemberEvent(event)
case IamMemberChanged: case IamMemberChanged:
@ -76,7 +76,7 @@ func (i *Iam) AppendEvent(event *es_models.Event) (err error) {
return err return err
} }
func (i *Iam) setData(event *es_models.Event) error { func (i *Iam) SetData(event *es_models.Event) error {
i.ObjectRoot.AppendEvent(event) i.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, i); err != nil { if err := json.Unmarshal(event.Data, i); err != nil {
logging.Log("EVEN-9sie4").WithError(err).Error("could not unmarshal event data") logging.Log("EVEN-9sie4").WithError(err).Error("could not unmarshal event data")

View File

@ -56,7 +56,7 @@ func IamMemberToModel(member *IamMember) *model.IamMember {
func (iam *Iam) appendAddMemberEvent(event *es_models.Event) error { func (iam *Iam) appendAddMemberEvent(event *es_models.Event) error {
member := &IamMember{} member := &IamMember{}
err := member.setData(event) err := member.SetData(event)
if err != nil { if err != nil {
return err return err
} }
@ -67,7 +67,7 @@ func (iam *Iam) appendAddMemberEvent(event *es_models.Event) error {
func (iam *Iam) appendChangeMemberEvent(event *es_models.Event) error { func (iam *Iam) appendChangeMemberEvent(event *es_models.Event) error {
member := &IamMember{} member := &IamMember{}
err := member.setData(event) err := member.SetData(event)
if err != nil { if err != nil {
return err return err
} }
@ -79,7 +79,7 @@ func (iam *Iam) appendChangeMemberEvent(event *es_models.Event) error {
func (iam *Iam) appendRemoveMemberEvent(event *es_models.Event) error { func (iam *Iam) appendRemoveMemberEvent(event *es_models.Event) error {
member := &IamMember{} member := &IamMember{}
err := member.setData(event) err := member.SetData(event)
if err != nil { if err != nil {
return err return err
} }
@ -91,7 +91,7 @@ func (iam *Iam) appendRemoveMemberEvent(event *es_models.Event) error {
return nil return nil
} }
func (m *IamMember) setData(event *es_models.Event) error { func (m *IamMember) SetData(event *es_models.Event) error {
m.ObjectRoot.AppendEvent(event) m.ObjectRoot.AppendEvent(event)
if err := json.Unmarshal(event.Data, m); err != nil { if err := json.Unmarshal(event.Data, m); err != nil {
logging.Log("EVEN-e4dkp").WithError(err).Error("could not unmarshal event data") logging.Log("EVEN-e4dkp").WithError(err).Error("could not unmarshal event data")

46
internal/key/model/key.go Normal file
View File

@ -0,0 +1,46 @@
package model
import (
"time"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
)
type KeyPair struct {
es_models.ObjectRoot
Usage KeyUsage
Algorithm string
PrivateKey *Key
PublicKey *Key
}
type KeyUsage int32
const (
KeyUsageSigning KeyUsage = iota
)
func (u KeyUsage) String() string {
switch u {
case KeyUsageSigning:
return "sig"
}
return ""
}
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()
}
func (k *Key) IsValid() bool {
return k.Key != nil
}

View File

@ -0,0 +1,120 @@
package model
import (
"time"
"github.com/caos/zitadel/internal/crypto"
"github.com/caos/zitadel/internal/errors"
"github.com/caos/zitadel/internal/model"
)
type KeyView struct {
ID string
Private bool
Expiry time.Time
Algorithm string
Usage KeyUsage
Key *crypto.CryptoValue
Sequence uint64
}
type SigningKey struct {
ID string
Algorithm string
Key interface{}
}
type PublicKey struct {
ID string
Algorithm string
Usage KeyUsage
Key interface{}
}
type KeySearchRequest struct {
Offset uint64
Limit uint64
SortingColumn KeySearchKey
Asc bool
Queries []*KeySearchQuery
}
type KeySearchKey int32
const (
KEYSEARCHKEY_UNSPECIFIED KeySearchKey = iota
KEYSEARCHKEY_ID
KEYSEARCHKEY_PRIVATE
KEYSEARCHKEY_EXPIRY
KEYSEARCHKEY_USAGE
)
type KeySearchQuery struct {
Key KeySearchKey
Method model.SearchMethod
Value interface{}
}
type KeySearchResponse struct {
Offset uint64
Limit uint64
TotalResult uint64
Result []*KeyView
}
func (r *KeySearchRequest) EnsureLimit(limit uint64) {
if r.Limit == 0 || r.Limit > limit {
r.Limit = limit
}
}
func SigningKeyFromKeyView(key *KeyView, alg crypto.EncryptionAlgorithm) (*SigningKey, error) {
if key.Usage != KeyUsageSigning || !key.Private {
return nil, errors.ThrowInvalidArgument(nil, "MODEL-5HBdh", "key must be private signing key")
}
keyData, err := crypto.Decrypt(key.Key, alg)
if err != nil {
return nil, err
}
privateKey, err := crypto.BytesToPrivateKey(keyData)
if err != nil {
return nil, err
}
return &SigningKey{
ID: key.ID,
Algorithm: key.Algorithm,
Key: privateKey,
}, nil
}
func PublicKeysFromKeyView(keys []*KeyView, alg crypto.EncryptionAlgorithm) ([]*PublicKey, error) {
converted := make([]*PublicKey, len(keys))
var err error
for i, key := range keys {
converted[i], err = PublicKeyFromKeyView(key, alg)
if err != nil {
return nil, err
}
}
return converted, nil
}
func PublicKeyFromKeyView(key *KeyView, alg crypto.EncryptionAlgorithm) (*PublicKey, error) {
if key.Private {
return nil, errors.ThrowInvalidArgument(nil, "MODEL-dTZa2", "key must be public")
}
keyData, err := crypto.Decrypt(key.Key, alg)
if err != nil {
return nil, err
}
publicKey, err := crypto.BytesToPublicKey(keyData)
if err != nil {
return nil, err
}
return &PublicKey{
ID: key.ID,
Algorithm: key.Algorithm,
Usage: key.Usage,
Key: publicKey,
}, nil
}

View File

@ -0,0 +1,84 @@
package eventsourcing
import (
"context"
"time"
"github.com/caos/zitadel/internal/config/types"
"github.com/caos/zitadel/internal/crypto"
caos_errs "github.com/caos/zitadel/internal/errors"
es_int "github.com/caos/zitadel/internal/eventstore"
"github.com/caos/zitadel/internal/eventstore/models"
es_sdk "github.com/caos/zitadel/internal/eventstore/sdk"
"github.com/caos/zitadel/internal/id"
key_model "github.com/caos/zitadel/internal/key/model"
"github.com/caos/zitadel/internal/key/repository/eventsourcing/model"
)
type KeyEventstore struct {
es_int.Eventstore
keySize int
keyAlgorithm crypto.EncryptionAlgorithm
privateKeyLifetime time.Duration
publicKeyLifetime time.Duration
idGenerator id.Generator
}
type KeyConfig struct {
Size int
PrivateKeyLifetime types.Duration
PublicKeyLifetime types.Duration
EncryptionConfig *crypto.KeyConfig
SigningKeyRotation types.Duration
}
func StartKey(eventstore es_int.Eventstore, config KeyConfig, keyAlgorithm crypto.EncryptionAlgorithm, generator id.Generator) (*KeyEventstore, error) {
return &KeyEventstore{
Eventstore: eventstore,
keySize: config.Size,
keyAlgorithm: keyAlgorithm,
privateKeyLifetime: config.PrivateKeyLifetime.Duration,
publicKeyLifetime: config.PublicKeyLifetime.Duration,
idGenerator: generator,
}, nil
}
func (es *KeyEventstore) GenerateKeyPair(ctx context.Context, usage key_model.KeyUsage, algorithm string) (*key_model.KeyPair, error) {
privateKey, publicKey, err := crypto.GenerateEncryptedKeyPair(es.keySize, es.keyAlgorithm)
if err != nil {
return nil, err
}
privateKeyExp := time.Now().UTC().Add(es.privateKeyLifetime)
publicKeyExp := time.Now().UTC().Add(es.publicKeyLifetime)
return es.CreateKeyPair(ctx, &key_model.KeyPair{
ObjectRoot: models.ObjectRoot{},
Usage: usage,
Algorithm: algorithm,
PrivateKey: &key_model.Key{
Key: privateKey,
Expiry: privateKeyExp,
},
PublicKey: &key_model.Key{
Key: publicKey,
Expiry: publicKeyExp,
},
})
}
func (es *KeyEventstore) CreateKeyPair(ctx context.Context, pair *key_model.KeyPair) (*key_model.KeyPair, error) {
if !pair.IsValid() {
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-G34ga", "Name is required")
}
id, err := es.idGenerator.Next()
if err != nil {
return nil, err
}
pair.AggregateID = id
repoKey := model.KeyPairFromModel(pair)
createAggregate := KeyPairCreateAggregate(es.AggregateCreator(), repoKey)
err = es_sdk.Push(ctx, es.PushAggregates, repoKey.AppendEvents, createAggregate)
if err != nil {
return nil, err
}
return model.KeyPairToModel(repoKey), nil
}

View File

@ -0,0 +1,33 @@
package eventsourcing
import (
"context"
"github.com/caos/zitadel/internal/errors"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/key/repository/eventsourcing/model"
)
func KeyPairQuery(latestSequence uint64) *es_models.SearchQuery {
return es_models.NewSearchQuery().
AggregateTypeFilter(model.KeyPairAggregate).
LatestSequenceFilter(latestSequence)
}
func KeyPairAggregate(ctx context.Context, aggCreator *es_models.AggregateCreator, pair *model.KeyPair) (*es_models.Aggregate, error) {
if pair == nil {
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-d5HNJA", "existing key pair must not be nil")
}
return aggCreator.NewAggregate(ctx, pair.AggregateID, model.KeyPairAggregate, model.KeyPairVersion, pair.Sequence)
}
func KeyPairCreateAggregate(aggCreator *es_models.AggregateCreator, pair *model.KeyPair) func(ctx context.Context) (*es_models.Aggregate, error) {
return func(ctx context.Context) (*es_models.Aggregate, error) {
agg, err := KeyPairAggregate(ctx, aggCreator, pair)
if err != nil {
return nil, err
}
return agg.AppendEvent(model.KeyPairAdded, pair)
}
}

View File

@ -0,0 +1,90 @@
package model
import (
"encoding/json"
"time"
"github.com/caos/logging"
"github.com/caos/zitadel/internal/crypto"
es_models "github.com/caos/zitadel/internal/eventstore/models"
"github.com/caos/zitadel/internal/key/model"
)
const (
KeyPairVersion = "v1"
)
type KeyPair struct {
es_models.ObjectRoot
Usage int32 `json:"usage"`
Algorithm string `json:"algorithm"`
PrivateKey *Key `json:"privateKey"`
PublicKey *Key `json:"publicKey"`
}
type Key struct {
Key *crypto.CryptoValue `json:"key"`
Expiry time.Time `json:"expiry"`
}
func KeyPairFromModel(pair *model.KeyPair) *KeyPair {
return &KeyPair{
ObjectRoot: pair.ObjectRoot,
Usage: int32(pair.Usage),
Algorithm: pair.Algorithm,
PrivateKey: KeyFromModel(pair.PrivateKey),
PublicKey: KeyFromModel(pair.PublicKey),
}
}
func KeyPairToModel(pair *KeyPair) *model.KeyPair {
return &model.KeyPair{
ObjectRoot: pair.ObjectRoot,
Usage: model.KeyUsage(pair.Usage),
Algorithm: pair.Algorithm,
PrivateKey: KeyToModel(pair.PrivateKey),
PublicKey: KeyToModel(pair.PublicKey),
}
}
func KeyFromModel(key *model.Key) *Key {
return &Key{
Key: key.Key,
Expiry: key.Expiry,
}
}
func KeyToModel(key *Key) *model.Key {
return &model.Key{
Key: key.Key,
Expiry: key.Expiry,
}
}
func (k *KeyPair) AppendEvents(events ...*es_models.Event) error {
for _, event := range events {
if err := k.AppendEvent(event); err != nil {
return err
}
}
return nil
}
func (k *KeyPair) AppendEvent(event *es_models.Event) error {
k.ObjectRoot.AppendEvent(event)
switch event.Type {
case KeyPairAdded:
return k.AppendAddKeyPair(event)
}
return nil
}
func (k *KeyPair) AppendAddKeyPair(event *es_models.Event) error {
if err := json.Unmarshal(event.Data, k); err != nil {
logging.Log("EVEN-Je92s").WithError(err).Error("could not unmarshal event data")
return err
}
return nil
}

View File

@ -0,0 +1,9 @@
package model
import "github.com/caos/zitadel/internal/eventstore/models"
const (
KeyPairAggregate models.AggregateType = "key_pair"
KeyPairAdded models.EventType = "key_pair.added"
)

Some files were not shown because too many files have changed in this diff Show More